Raylib - 更适合 OIer 体质的易上手的图形库

· · 科技·工程

0. 说在前面

很久之前就想推荐这样一个 API 简单易懂,几乎是傻瓜式的图形库了,现在终于是来写这篇文章了,那么事不宜迟,我们直接开始吧!

1. 综述

Raylib 是一款简单,易使用的库来享受电子游戏编程。
(出处:官网界面“raylib is a simple and easy-to-use library to enjoy videogames programming.”)

它具有以下很多特性:

但我只是 OIer 欸,我只是想看到不是一个黑框框的控制台,我才不关心这些特性呢,能画出一个长方形就可以了欸。只是直接操作 OpenGL 太困难了,这个会不会简单一些。

嗯是的,相信点开文章的你也是这样想的,那我们直接上手吧。

2. 安装

很明显在 Windows 环境下配置任何库都是极其折磨的。
不过不用担心,小熊猫 c++ 为我们提供了一站式服务。
直接点击这里下载一个带编译器的版本就可以了。
当然作为一个库肯定是有硬件限制的,当然这里不过多介绍了,因为我假定你们没有人会拿一个超级老古董设备,甚至不支持 OpenGL 3.3 来玩的吧。 走完安装流程之后,打开小熊猫 c++,开始你的图形编程之旅吧!

Linux 用户可以去看这里,既然你都用 Linux 了,那就自己折腾一下吧(逃)。
别的平台也是支持的(甚至有树莓派),不过这里只介绍 Windows 了喵。

3. 正在取得启动(过去式)

额嗯对我是想说“getting started”。

首先一个典型的示例程序是:

#include "raylib.h"
const int screenWidth = 640, screenHeight = 480;
int main() {
    SetConfigFlags(FLAG_MSAA_4X_HINT); // 打开抗锯齿
    InitWindow(screenWidth, screenHeight, "Sample"); // 初始化窗口
    SetTargetFPS(30); // 设置目标 FPS
    while (!WindowShouldClose()) { // 当窗口不应该关掉时
        BeginDrawing(); // 开始画画
        ClearBackground(WHITE); // 清空背景为白色,即清空上一帧
        EndDrawing(); // 结束画画
    }
    CloseWindow(); // 关闭窗口
    return 0;
}

直接编译运行,你可以看到一个纯白的界面。
正如前文所说,API 命名真的是傻瓜式的,光看就能看懂。

你也大概能靠猜得出以下结论:

欸我去好像确实很简单易懂欸,不过,只有纯白怎么行,比如说怎么画一个矩形呢?

4. 初级

让我们查询 API 接口:

void DrawRectangle(int posX, int posY, int width, int height, Color Color); // 绘制填充颜色的Rectangle
void DrawRectangleV(Vector2 position, Vector2 size, Color color); // 绘制彩色填充Rectangle(矢量版)

接着发动俺寻思之力:

while (!WindowShouldClose()) {
  BeginDrawing();
  ClearBackground(WHITE);

  DrawRectangle(0, 0, 100, 100, BLACK);

  EndDrawing();
}

(顺带一提这些颜色常量都是库中的宏定义。如 #define WHITE CLITERAL(Color){ 255, 255, 255, 255 } // White

可以看到,屏幕左上角出现了一个黑色方形!
你很快猜出了以下结论:

现在让方块动起来吧。

你继续发动发动俺寻思之力:

int posX = 0, posY = 0;
while (!WindowShouldClose()) {
  posX++, posY++;
  if (posX > screenWidth)
    posX = 0;
  if (posY > screenHeight)
    posY = 0;
  BeginDrawing();
  ClearBackground(WHITE);
  DrawRectangle(posX, posY, 100, 100, BLACK);
  EndDrawing();
}

黑色方形动起来了!

距离游戏只差一步了吧,侦测键盘输入。
以前你可能会使用 Windows API 干这个,现在你可以直接使用:

bool IsKeyPressed(int key); // 检查是否按下了一次键
bool IsKeyDown(int key); // 检查是否按下了键
bool IsKeyReleased(int key); // 检查按键是否已释放一次
bool IsKeyUp(int key); // 检查是否未按下按键

不难写出(注意坐标系的方向):

if (IsKeyDown(KEY_D))
  posX++;
if (IsKeyDown(KEY_A))
  posX--;
if (IsKeyDown(KEY_S))
  posY++;
if (IsKeyDown(KEY_W))
  posY--;

你惊人的发现,这已经足够让你写出简化版的 surf 了!
(请忽略古早代码抽搐的马蜂。)

#include <raylib.h>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <random>
typedef long long ll;
const int screenWidth = 1600;
const int screenHeight = 900, BRICK_COUNT = 15,  Player_COLOR_COUNT = 9;
struct Player_struct {
    Vector2 position;
    Vector2 size;
    int life, tili, fall;
    int wudi, chongci, in_air;
};
Player_struct player;
Player_struct octopus;
#define common_brick 0
#define speed_brick 1
#define health_brick 2
#define flying_brick 3
struct Brick_struct {
    Vector2 position;
    Vector2 size;
    Color color;
    int type;
};
Brick_struct Brick[BRICK_COUNT];
bool game_start = 0, game_over = 0, showtxt = 0;
double Meters = 0, BestMeters = 0;
int player_color_ptr = 0, chao_guo = 0;
Color defualt_color = YELLOW;
Color player_color[Player_COLOR_COUNT] = {
    YELLOW, RED, RAYWHITE,
    (Color){232, 3, 232, 255}, (Color){249, 151, 153, 255},
    DARKGREEN, BLUE, DARKBLUE, PURPLE
};
void InitGame(void) {
    chao_guo = 0;
    octopus.life = -1;
    octopus.position = (Vector2){800, -400};
    octopus.size = (Vector2){100, 100};
    octopus.fall = 0;
    Meters = 0;
    showtxt = 0;
    game_over = 0;
    player.wudi = 0;
    player.chongci = 0;
    player.tili = 2;
    player.fall = 0;
    player.in_air = 0;
    player.life = 3;
    player.position = (Vector2) {
        screenWidth / 2, screenHeight / 2 - 120
    };
    player.size = (Vector2) {
        50, 100
    };
    for (int i = 0; i < BRICK_COUNT; i++) {
        Brick[i].size.x = Brick[i].size.y = 0;
        Brick[i].position.y = -114;
        Brick[i].type = common_brick;
    }

}
std::mt19937 rnd(time(0));
bool rand_bool() {
    return rnd() & 1;
}
int rand_range(int l, int r) {
    return rnd() % (r - l + 1) + l;
}
bool touch(Vector4 tmp1, Vector4 tmp2) {
    Vector4 jiao = (Vector4) {
        std::max(tmp1.x, tmp2.x), std::max(tmp1.y, tmp2.y), std::min(tmp1.z, tmp2.z), std::min(tmp1.w, tmp2.w)
    };
    return (jiao.z - jiao.x > 4 && jiao.w - jiao.y > 4);
}
void updatopus(float t1, float t2) {
    if (octopus.position.x > player.position.x) octopus.position.x += t1;
    if (octopus.position.x < player.position.x) octopus.position.x += t2;
}
void UpdateGame(void) {
    if (!game_start) {
        if (!showtxt) {
            if (IsKeyPressed(KEY_SPACE))
                game_start = 1;
            if (Meters - 0 < 1e-9) {
                if (IsKeyPressed(KEY_LEFT)) {
                    player_color_ptr--;
                    if (player_color_ptr < 0)
                        player_color_ptr = Player_COLOR_COUNT - 1;
                    defualt_color = player_color[player_color_ptr];
                }
                if (IsKeyPressed(KEY_RIGHT)) {
                    player_color_ptr++;
                    if (player_color_ptr == Player_COLOR_COUNT)
                        player_color_ptr = 0;
                    defualt_color = player_color[player_color_ptr];
                }
            }
        }
        if (Meters - 0 < 1e-9) {
            if (IsKeyDown(KEY_I) && IsKeyDown(KEY_N) && IsKeyDown(KEY_T)) {
                player.life = -1;
                player.tili = 114514;
            }
            if (IsKeyPressed(KEY_F1))
                showtxt ^= 1;
        }
    } else {
        if (!game_over) {
            if (IsKeyPressed(KEY_SPACE)) {
                game_start = 0;
                return ;
            }
            if (!player.fall) {
                if (IsKeyDown(KEY_LEFT)) {
                    if (player.in_air == 0) {
                        player.position.x -= 5;
                        if (player.position.x < 0)
                            player.position.x = 0;
                    }
                }
                if (IsKeyDown(KEY_RIGHT)) {
                    if (player.in_air == 0) {
                        player.position.x += 5;
                        if (player.position.x + player.size.x > screenWidth)
                            player.position.x = screenWidth - player.size.x;
                    }
                }
                if (IsKeyDown(KEY_DOWN)) {
                    if (player.in_air == 0) {
                        if (player.tili > 0 && player.chongci == 0) {
                            player.tili--;
                            player.chongci = 155;
                        }
                    }
                }
            }
            for (int i = 0; i < BRICK_COUNT; i++) {
                Brick_struct& brick = Brick[i];
                if (brick.position.y + brick.size.y < 0) {
                    if (rand_range(1, 100) == 2) {
                        if (rand_range(1, 50) == 10) {
                            brick.type = speed_brick;
                            brick.position.x = rand() % (screenWidth - 75);
                            brick.position.y = screenHeight;
                            brick.size.y = brick.size.x = 50;
                            brick.color = (Color) {
                                161, 255, 175, 255
                            };
                        } else if (rand_range(1, 100) == 10) {
                            brick.type = health_brick;
                            brick.position.x = rand() % (screenWidth - 75);
                            brick.position.y = screenHeight;
                            brick.size.y = brick.size.x = 50;
                            brick.color = (Color) {
                                232, 39, 39, 255
                            };
                        } else if (rand_range(1, 30) == 10) {
                            brick.type = flying_brick;
                            brick.position.x = rand() % (screenWidth - 100);
                            brick.position.y = screenHeight;
                            brick.size.y = 100;
                            brick.size.x = 75;
                            brick.color = (Color) {
                                48, 223, 232, 255
                            };
                        } else {
                            brick.type = common_brick;
                            brick.size.y = rand_range(50, 150);
                            brick.size.x = rand_range(50, 150);
                            brick.position.x = rand() % screenWidth + 1;
                            brick.position.y = screenHeight;
                            brick.color = (Color) {
                                (unsigned char)rand_range(50, 80), (unsigned char)rand_range(50, 80), (unsigned char)rand_range(50, 80), 255
                            };
                        }
                    }
                }
                if (player.fall != 0) {
                    if (player.fall > 10) ;
                    else brick.position.y -= 7;
                } else if (player.chongci || player.in_air)
                    brick.position.y -= 14;
                else
                    brick.position.y -= 7;
                Vector4 tmp1 = (Vector4) {
                    player.position.x, player.position.y, player.position.x + player.size.x, player.position.y + player.size.y
                };
                Vector4 tmp3 = (Vector4) {
                    octopus.position.x, octopus.position.y, octopus.position.x + octopus.size.x, octopus.position.y + octopus.size.y
                };
                Vector4 tmp2 = (Vector4) {
                    brick.position.x, brick.position.y, brick.position.x + brick.size.x, brick.position.y + brick.size.y
                };
                bool attach = touch(tmp1, tmp2);
                bool oct = touch(tmp3, tmp2);
                if(oct && (brick.type == common_brick || brick.type == flying_brick)) {
                    octopus.fall = 75;
                }
                if (attach && player.in_air == 0) {
                    if (brick.type == common_brick) {
                        if (player.wudi == 0) {
                            player.life--;
                            if (player.life == 0) {
                                game_over = 1;
                                break;
                            } else {
                                player.fall = 45;
                                player.wudi = 95;
                                break;
                            }
                        }
                    } else if (brick.type == speed_brick) {
                        brick.position.y = -1;
                        brick.size.x = brick.size.y = 0;
                        brick.type = 0;
                        if (player.tili < 3)
                            player.tili++;
                    } else if (brick.type == health_brick) {
                        brick.position.y = -1;
                        brick.size.x = brick.size.y = 0;
                        brick.type = 0;
                        if (player.life < 3)
                            player.life++;
                    } else if (brick.type == flying_brick && player.in_air == 0) {
                        player.in_air = 155;
                    }
                }
            }
            if(octopus.position.y < -500)
                octopus.position.y = -400;
            Vector4 tmp1 = (Vector4) {
                player.position.x, player.position.y, player.position.x + player.size.x, player.position.y + player.size.y
            };
            Vector4 tmp3 = (Vector4) {
                octopus.position.x, octopus.position.y, octopus.position.x + octopus.size.x, octopus.position.y + octopus.size.y
            };
            bool attach = touch(tmp1, tmp3);
            if(attach)
                game_over = 1;
            if (player.fall != 0) {
                if(!octopus.fall) {
                    updatopus(-5, 5);
                    if(octopus.position.y < player.position.y)
                        octopus.position.y += 7;
                }
            } else if (player.chongci || player.in_air) {
                if(!octopus.fall) {
                    updatopus(-5, 5);
                    octopus.position.y -= 6;
                }
                else
                    octopus.position.y -= 14;
            } else {
                if(!octopus.fall) {
                    updatopus(-5, 5);
                    if(octopus.position.y < player.position.y)
                        octopus.position.y += 2;
                }
                else
                    octopus.position.y -= 7;
            }
            if(!octopus.fall) {
                if (rand() & 1)
                    octopus.position.x -= rand_range(1, 20);
                else octopus.position.x += rand_range(1, 20);
            }
            if (octopus.fall)
                octopus.fall--;
            if (player.fall)
                player.fall--;
            if (player.in_air)
                player.in_air--;
            if (player.in_air == 1)
                player.wudi = 55;
            if (player.wudi)
                player.wudi--;
            if (player.chongci) {
                player.chongci--;
                Meters += 0.5;
            } else Meters += 0.25;
            if (Meters > BestMeters && chao_guo == 0)
                chao_guo = 155;
            if (chao_guo > 2) chao_guo--;
        } else {
            if (IsKeyDown(KEY_ENTER)) {
                InitGame();
                game_over = 0;
            }
        }
    }
}
const int STRING_SIZE = 1005;
char updatelog[STRING_SIZE] = "Press [F1] to quit\n\
This is a little surf game.\n\
Based on raylib.\n";
void Draw_brick(void) {
    for (int i = 0; i < BRICK_COUNT; i++) {
        Brick_struct& brick = Brick[i];
        DrawRectangle(brick.position.x, brick.position.y, brick.size.x, brick.size.y, brick.color);
        if (brick.type == flying_brick) {
            DrawRectangle(brick.position.x + 27, brick.position.y + 20, 20, 30, LIME);
            Vector2 t1 = (Vector2) {
                brick.position.x + 17, brick.position.y + 50
            };
            Vector2 t2 = (Vector2) {
                brick.position.x + 57, brick.position.y + 50
            };
            Vector2 t3 = (Vector2) {
                brick.position.x + 37, brick.position.y + 70
            };
            DrawTriangle(t3, t2, t1, LIME);
        }
    }
}
void Draw_player(Color color) {
    if (player.in_air) DrawRectangle(player.position.x - 7, player.position.y - 7, player.size.x + 10, player.size.y + 10, color);
    else DrawRectangle(player.position.x, player.position.y, player.size.x, player.size.y, color);
    DrawRectangle(player.position.x + 15, player.position.y + 15, 5, 7, BLACK);
    DrawRectangle(player.position.x + 35, player.position.y + 15, 5, 7, BLACK);
    DrawRectangle(player.position.x + 25, player.position.y + 35, 15, 5, BLACK);
}
void Draw_octopus() {
    DrawRectangle(octopus.position.x, octopus.position.y, octopus.size.x, octopus.size.y, PURPLE);
}
void DrawGame(void) {
    BeginDrawing();
    ClearBackground((Color) {
        65, 188, 255, 192
    });
    if (!game_start) {
        if (showtxt) {
            DrawText(updatelog, 5, 5, 20, BLACK);
        } else {
            Draw_player(defualt_color);
            if (Meters - 0 < 1e-9) {
                DrawText("Let's surf on internet!", GetScreenWidth() / 2 - 230, 0, 40, BLACK);
                DrawText("Press [space] to start, [F1] to view log.", GetScreenWidth() / 2 - 170, GetScreenHeight() / 2, 20, BLACK);
                DrawText("Press [<-] or [->] to change colors.", GetScreenWidth() / 2 - 150, GetScreenHeight() / 2 + 25, 20, BLACK);
            } else {
                DrawText("PAUSE.", GetScreenWidth() / 2 - 50, GetScreenHeight() / 2, 20, BLACK);
                DrawText("Press [space] to continue.", GetScreenWidth() / 2 - 150, GetScreenHeight() / 2 + 25, 20, BLACK);
            }

        }
    } else {
        if (!game_over) {
            //Draw Player
            if (player.in_air) {
                Draw_brick();
                Draw_player((Color) {
                    48, 223, 232, 255
                });
            } else if (player.wudi) {
                Draw_player((Color) {
                    255, 255, 206, 255
                });
                Draw_brick();

            } else if (player.chongci) {
                Draw_player(GREEN);
                Draw_brick();
            } else {
                Draw_player(defualt_color);
                Draw_brick();
            }
            Draw_octopus();
            //Draw text
            static char tss[99] = {};
            sprintf(tss, "%.lf m", Meters);
            DrawText(tss, GetScreenWidth() / 2, 0, 20, BLACK);
            //Draw life
            int last = 0;
            for (int i = 1; i <= player.life; i++) {
                DrawRectangle(last, 0, 25, 25, RED);
                last = last + 35;
            }
            if (player.life == -1) {
                DrawRectangle(last, 0, 25, 25, RED);
                DrawText("INF", 35, 0, 20, RED);
            }
            //Draw tili
            last = GetScreenWidth() - 95;
            for (int i = 1; i <= player.tili; i++) {
                DrawRectangle(last, 0, 25, 25, GREEN);
                last = last + 35;
            }
            if (chao_guo > 2)
                DrawText("New best!\n", 1000, 0, 20, BLACK);
        } else {
            static char tss[99] = {};
            BestMeters = std::max(BestMeters, Meters);
            sprintf(tss, "You surfed %.lf m\nBest record: %.lf m\nPRESS [ENTER] TO PLAY AGAIN", Meters, BestMeters);
            DrawText(tss, GetScreenWidth() / 2 - MeasureText(tss, 20) / 2, GetScreenHeight() / 2 - 50, 20, BLACK);
        }
    }
    EndDrawing();
}
void UpdateDrawFrame(void) {
    UpdateGame();
    DrawGame();
}
int main() {
    //qwq
    InitWindow(screenWidth, screenHeight, "surf");
    InitGame();
    SetTargetFPS(60);
    while (!WindowShouldClose()) {
        UpdateDrawFrame();
    }
    CloseWindow();
    return 0;
}

5. 进阶

事实上,会调用简单 API 已经足够了,游戏的核心还是在于主要逻辑是怎么写的。
不过还有很多 API。

音频

InitAudioDevice(); // 初始化音频设备
Music msc = LoadMusicStream("__libp_start_main.mp3");
msc.looping = 1; // 是否循环
PlayMusicStream(msc); 
while (!WindowShouldClose()) {
  UpdateMusicStream(msc);
  UpdateDrawFrame();
}
UnloadMusicStream(msc); // 记得释放资源
CloseAudioDevice(); // 同理

上者适合长的背景音乐,如果是音效的话适合使用

Sound sound = LoadSound("boom.mp3");
PlaySound(sound);

就不用 UpdateSoundStream了,也同样记得释放资源。

文本

其实上文的 surf 代码就提供了示范。

void DrawText(const char *text, int posX, int posY, int fontSize, Color color);

其他

小熊猫同时集成了 raymath 与 raygui,特别感兴趣的话可以查看这里或者查阅官方 API 文档等。
(其实就是我懒了)

等等,3D 部分你咋不讲。

单纯是我懒了。

6. 写在最后

可以看到我其实讲的也不太详细,不过这个库的简单程度可以使你看一些示例代码就能搞懂。
如果你是单独装的 raylib,你可以看到实际上几乎每个模块都有示例代码。
本文只作为领进门的引子,真正的探索就留给读者了。(或者说还有什么想了解的发在评论区?也许我会去填坑吧)

不过在这个大 AI 时代,还会有 OIer 想用纯 C 来写小游戏吗。(苦笑)
生成的 HTML,画面会比一个个 rectangle 加 sin 函数搓的缓动精美的多吧。

不过就算当我是愚蠢的呢,我情愿看着一条条亲手搭起来的底层代码,我不敢代表所有 OIers,不过,我觉得很浪漫。