Raylib - 更适合 OIer 体质的易上手的图形库
0. 说在前面
很久之前就想推荐这样一个 API 简单易懂,几乎是傻瓜式的图形库了,现在终于是来写这篇文章了,那么事不宜迟,我们直接开始吧!
1. 综述
Raylib 是一款简单,易使用的库来享受电子游戏编程。
(出处:官网界面“raylib is a simple and easy-to-use library to enjoy videogames programming.”)
它具有以下很多特性:
- 免费开源
- 跨平台支持
- 全部用 C 语言实现
- 简单易用
- 使用 OpenGL 硬件加速
- 3D 支持,着色器支持等等等等
但我只是 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 命名真的是傻瓜式的,光看就能看懂。
你也大概能靠猜得出以下结论:
- 抗锯齿,初始化,主循环,关闭窗口等就是公式代码每次都写就可以了。
- 主循环每次都在画一帧。
- 感觉应该把所有绘画代码都放在
BeginDrawing和EndingDrawing之间。
欸我去好像确实很简单易懂欸,不过,只有纯白怎么行,比如说怎么画一个矩形呢?
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)
可以看到,屏幕左上角出现了一个黑色方形!
你很快猜出了以下结论:
- 原来屏幕左上角才是原点,x 坐标向右增加,y 坐标向下增加啊。
- 同理那么
DrawRectangle()参数也是传的左上角坐标。
现在让方块动起来吧。
你继续发动发动俺寻思之力:
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,不过,我觉得很浪漫。