Wireworld Simulator Using the Raylib: Part1¶
用 Raylib 写个 Wireworld 模拟器,试试自己能不能用 C 语言顺畅地做游戏。
这篇文章是制作过程的详细记录,记录编码、设计的思路和步骤,标题会非常细碎。当作一个 Step by Step 教程也许可以,每个阶段都附了完整代码可以对照。
项目地址:github.com/13m0n4de/wireworld
前言 ¶
Wireworld 是一种元胞自动机 (Cellular automaton),类似的还有康威生命游戏 (Conway's Game of Life)。Wireworld 可以用来模拟电路逻辑,如下图的二极管:
Raylib 是一个用于游戏制作的 C 语言库,设计上高度模块化、高度简洁,以下是它的官方说明:
NOTE for ADVENTURERS: raylib is a programming library to enjoy videogames programming; no fancy interface, no visual helpers, no debug button... just coding in the most pure spartan-programmers way
本来是打算用 Bevy 来写的,但最近的 Rust 含量太多,受够了复杂过头的东西,所以这次用 C 语言,并且放弃诸如 Make 或 CMake 之类的构建系统,尽量保持一切简单可控。
运行 Raylib 基本示例 ¶
首先安装 Raylib 库,传统派一点,手动从 GitHub 上下载解压,不使用系统的包管理器。
wget "https://github.com/raysan5/raylib/releases/download/5.0/raylib-5.0_linux_amd64.tar.gz"
tar zxvf raylib-5.0_linux_amd64.tar.gz
官方给出的基本示例:
#include "raylib.h"
int main(void) {
InitWindow(800, 450, "raylib [core] example - basic window");
while (!WindowShouldClose()) {
BeginDrawing();
ClearBackground(RAYWHITE);
DrawText("Congrats! You created your first window!", 190, 200, 20,
LIGHTGRAY);
EndDrawing();
}
CloseWindow();
return 0;
}
编译它需要指定头文件路径和静态库文件路径:
gcc main.c -o main -Wall -Wextra -pedantic -I raylib-5.0_linux_amd64/include/ -L raylib-5.0_linux_amd64/lib/ -l:libraylib.a -lm
这样得到的文件只依赖 libc 和 libm,Raylib 的部分被静态链接进去:
linux-vdso.so.1 (0x00007fff0e317000)
libm.so.6 => /usr/lib/libm.so.6 (0x000074151e365000)
libc.so.6 => /usr/lib/libc.so.6 (0x000074151e181000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x000074151e5a5000)
动态链接也是同理,我认为这种游戏程序静态链接更合理。
运行出现 800 x 450 的窗口,显示文字 Congrats! You created your first window!,字体还蛮好看的。
设置编辑器 ¶
我的 NeoVim 一片红,原因是我用了 clangd 作 LSP,它默认情况下没法识别 Raylib 库(没装在系统路径里bear
命令生成 compile_comannds.json
文件帮助 clangd 识别,只需要传入刚刚的编译命令就可以了:
bear -- gcc main.c -o main -Wall -Wextra -pedantic -I raylib-5.0_linux_amd64/include/ -L raylib-5.0_
linux_amd64/lib/ -l:libraylib.a -lm
[
{
"arguments": [
"/usr/bin/gcc",
"-c",
"-Wall",
"-Wextra",
"-pedantic",
"-I",
"raylib-5.0_linux_amd64/include/",
"-o",
"main",
"main.c"
],
"directory": "/path/to/wireworld",
"file": "/path/to/wireworld/main.c",
"output": "/path/to/wireworld/main"
}
]
总之先显示点什么 ¶
先简单摸索一下 Raylib 的 API。
模拟器比常规的游戏要简单,预计只有窗口管理、输入管理和 2D 图形显示三个功能,只需要使用两个模块:
它们都共用 raylib.h
头文件,不需要额外引入。
指定窗口的长宽和标题名称:
const int screenWidth = 800;
const int screenHeight = 450;
InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
设置 60 帧每秒:
SetTargetFPS(60);
绘制 Wirewold 的四种细胞 (Cell),颜色是从 Color Hunt 上找的,符合红黄蓝黑配色:
- 空:黑色 (
#3B4A6B
) - 电子头:蓝色 (
22B2DA
) - 电子尾:红色 (
F23557
) - 导体:黄色 (
F0D43A
)
#define EMPTY_COLOR CLITERAL(Color) { 59, 74, 107, 255 }
#define HEAD_COLOR CLITERAL(Color) { 34, 178, 218, 255 }
#define TAIL_COLOR CLITERAL(Color) { 242, 53, 87, 255 }
#define CONDUCTOR_COLOR CLITERAL(Color) { 240, 212, 58, 255 }
DrawRectangle(100, 100, cellSize, cellSize, EMPTY_COLOR);
DrawRectangle(120, 100, cellSize, cellSize, HEAD_COLOR);
DrawRectangle(140, 100, cellSize, cellSize, TAIL_COLOR);
DrawRectangle(160, 100, cellSize, cellSize, CONDUCTOR_COLOR);
当前完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
绘制网格 ¶
网格最讨人厌的是分界线,好在 Raylib 有一个绘制矩形边框的函数 DrawRectangleLines
,直接使用格子边框作为网格分界线可以省去不少工作量。
for (int i = 0; i < screenWidth / cellSize; i++) {
for (int j = 0; j < screenHeight / cellSize; j++) {
DrawRectangle(i * cellSize, j * cellSize, cellSize, cellSize,
EMPTY_COLOR);
DrawRectangleLines(i * cellSize, j * cellSize, cellSize,
cellSize, BLACK);
}
}
DrawRectangle(100, 100, cellSize, cellSize, EMPTY_COLOR);
DrawRectangleLines(100, 100, cellSize, cellSize, BLACK);
DrawRectangle(120, 100, cellSize, cellSize, HEAD_COLOR);
DrawRectangleLines(120, 100, cellSize, cellSize, BLACK);
DrawRectangle(140, 100, cellSize, cellSize, TAIL_COLOR);
DrawRectangleLines(140, 100, cellSize, cellSize, BLACK);
DrawRectangle(160, 100, cellSize, cellSize, CONDUCTOR_COLOR);
DrawRectangleLines(160, 100, cellSize, cellSize, BLACK);
效果如下(高度改成了 460,这样可以被细胞大小整除
这只是显示效果上的网格,对于网格数据,还是需要设计一个数据结构,比如二维数组。在二维数组中,每个元素存储细胞种类,比如「空
颜色、位置等信息不与单个细胞相关联,不需要额外保存在细胞中。
使用枚举表示细胞:
typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
创建网格并初始化所有格子为空:
const int rows = screenHeight / cellSize;
const int cols = screenWidth / cellSize;
Cell grid[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
grid[i][j] = EMPTY;
}
}
使用 switch
根据格子类型返回对应颜色:
Color GetCellColor(Cell cell) {
switch (cell) {
case EMPTY:
return EMPTY_COLOR;
case HEAD:
return HEAD_COLOR;
case TAIL:
return TAIL_COLOR;
case CONDUCTOR:
return CONDUCTOR_COLOR;
default:
return EMPTY_COLOR;
}
}
在主循环中遍历这个数组并绘制每个细胞:
grid[5][5] = HEAD;
grid[5][6] = TAIL;
grid[5][7] = CONDUCTOR;
while (!WindowShouldClose()) {
BeginDrawing();
ClearBackground(RAYWHITE);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
Color cellColor = GetCellColor(grid[i][j]);
DrawRectangle(j * cellSize, i * cellSize, cellSize, cellSize,
cellColor);
DrawRectangleLines(j * cellSize, i * cellSize, cellSize,
cellSize, BLACK);
}
}
EndDrawing();
}
当前完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
|
Wireworld 规则 ¶
时间以离散的步伐进行,单位是「代」(generations)。
每代细胞行为规则:
- 空 -> 空
- 电子头 -> 电子尾
- 电子尾 -> 导体
- 当导体拥有一至两个电子头邻居时,导体 -> 电子头,否则导体不变
对应代码:
void UpdateGrid(Cell grid[rows][cols]) {
Cell newGrid[rows][cols];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
switch (grid[i][j]) {
case EMPTY:
newGrid[i][j] = EMPTY;
break;
case HEAD:
newGrid[i][j] = TAIL;
break;
case TAIL:
newGrid[i][j] = CONDUCTOR;
break;
case CONDUCTOR: {
int headNeighbors = CountHeadNeighbors(grid, i, j);
if (headNeighbors == 1 || headNeighbors == 2) {
newGrid[i][j] = HEAD;
} else {
newGrid[i][j] = CONDUCTOR;
}
} break;
}
}
}
memcpy(grid, newGrid, sizeof(newGrid));
}
不能边遍历边修改 grid
,会影响到之后细胞的判断,需要创建一个新的网格 newGrid
,在最后将 newGrid
复制给 grid
(可以直接使用 memcpy
Wireworld 使用摩尔邻域 (Moore neighborhood),这意味着在上面的规则中
CountHeadNeighbors
的实现:
int CountHeadNeighbors(Cell grid[rows][cols], int row, int col) {
int headCount = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0)
continue;
int newRow = row + i;
int newCol = col + j;
if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
if (grid[newRow][newCol] == HEAD) {
headCount++;
}
}
}
}
return headCount;
}
将 UpdateGrid(grid);
加入主循环,并初始化一些细胞:
grid[5][5] = CONDUCTOR;
grid[5][6] = TAIL;
grid[5][7] = HEAD;
grid[5][8] = CONDUCTOR;
grid[5][9] = CONDUCTOR;
grid[6][4] = CONDUCTOR;
grid[6][10] = CONDUCTOR;
grid[7][5] = CONDUCTOR;
grid[7][6] = CONDUCTOR;
grid[7][7] = CONDUCTOR;
grid[7][8] = CONDUCTOR;
grid[7][9] = CONDUCTOR;
while (!WindowShouldClose()) {
BeginDrawing();
ClearBackground(RAYWHITE);
UpdateGrid(grid);
将 FPS 暂时设置为 5:
SetTargetFPS(5);
运行效果如图,一个时钟发射器:
当前完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
|
暂停和播放 ¶
先实现简单的暂停和播放功能,按下空格键暂停,再按一次播放。
游戏只有两个状态
int isPlaying = 0;
在按下空格时切换播放状态,且只有播放中才会更新网格:
if (IsKeyPressed(KEY_SPACE))
isPlaying = !isPlaying;
if (isPlaying) {
UpdateGrid(grid);
}
之前将 FPS 设置为 5 是因为一秒更新六十次网格实在太快,但此时又会因为帧率过低导致键盘输入概率捕获不到。所以我们需要真正意义上的每秒迭代 N 次(刷新 N 次网格
网格刷新速率 ¶
使用 GetFrameTime
获得最后一帧的绘制时间 (delta time),累加 elapsedTime
,并在达到刷新间隔 refreshInterval
时刷新网格。
const int refreshRate = 5;
const float refreshInterval = 1.0f / refreshRate;
float elapsedTime = 0.0f;
while (!WindowShouldClose()) {
BeginDrawing();
ClearBackground(RAYWHITE);
float frameTime = GetFrameTime();
elapsedTime += frameTime;
if (IsKeyPressed(KEY_SPACE))
isPlaying = !isPlaying;
if (isPlaying && elapsedTime >= refreshInterval) {
UpdateGrid(grid);
elapsedTime = 0.0f;
}
当前完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
|
高亮预选细胞 ¶
制作细胞位置预览效果:鼠标放置在的单元格边框会进行高亮。
使用 GetMousePosition
获得鼠标位置,并计算对应的细胞位置,使用 DrawRectangleLines
绘制高亮色边框。
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
Color cellColor = GetCellColor(grid[i][j]);
DrawRectangle(j * cellSize, i * cellSize, cellSize, cellSize,
cellColor);
DrawRectangleLines(j * cellSize, i * cellSize, cellSize,
cellSize, BLACK);
}
}
Vector2 mousePosition = GetMousePosition();
int mouseYGridPos = (int)(mousePosition.y / cellSize);
int mouseXGridPos = (int)(mousePosition.x / cellSize);
DrawRectangleLines(mouseXGridPos * cellSize, mouseYGridPos * cellSize,
cellSize, cellSize, WHITE);
为了避免与红黄蓝细胞颜色相近,高亮色选了纯白,正好也和网格边框颜色形成对比。
创建细胞 ¶
方案一 ¶
方案一是 xvlv.io/WireWorld/ 网站的按键配置:
- 鼠标左击:放置导线,目标细胞不为空时将其设置为空
- 鼠标右击:放置电子头,或将电子头转换为导线
代码如下,顺带加上了按键显示:
Color cellColor;
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
if (grid[mouseYGridPos][mouseXGridPos] != EMPTY) {
grid[mouseYGridPos][mouseXGridPos] = EMPTY;
cellColor = EMPTY_COLOR;
} else {
grid[mouseYGridPos][mouseXGridPos] = CONDUCTOR;
cellColor = CONDUCTOR_COLOR;
}
DrawRectangle(mouseXGridPos * cellSize, mouseYGridPos * cellSize,
cellSize, cellSize, cellColor);
DrawRectangleLines(mouseXGridPos * cellSize,
mouseYGridPos * cellSize, cellSize, cellSize,
BLACK);
} else if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
if (grid[mouseYGridPos][mouseXGridPos] != HEAD) {
grid[mouseYGridPos][mouseXGridPos] = HEAD;
cellColor = EMPTY_COLOR;
} else {
grid[mouseYGridPos][mouseXGridPos] = CONDUCTOR;
cellColor = CONDUCTOR_COLOR;
}
DrawRectangle(mouseXGridPos * cellSize, mouseYGridPos * cellSize,
cellSize, cellSize, cellColor);
DrawRectangleLines(mouseXGridPos * cellSize,
mouseYGridPos * cellSize, cellSize, cellSize,
BLACK);
}
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
DrawText("MOUSE: LEFT", 20, 420, 20, CONDUCTOR_COLOR);
} else if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
DrawText("MOUSE: RIGHT", 20, 420, 20, HEAD_COLOR);
} else {
DrawText("MOUSE: NONE", 20, 420, 20, BROWN);
}
这种方案不能手动放置电子尾。
方案一完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
|
方案二 ¶
方案二是 danprince.github.io/wireworld/ 网站的按键配置。
它使用数字键 1234 分别代表空、导体、电子头、电子尾,按住鼠标左键放置对应细胞。我更喜欢这个方案,之后的代码都会基于这个方案。
代码实现如下:
Vector2 mousePosition = GetMousePosition();
int mouseYGridPos = (int)(mousePosition.y / cellSize);
int mouseXGridPos = (int)(mousePosition.x / cellSize);
if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1))
selectCellType = EMPTY;
else if (IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2))
selectCellType = CONDUCTOR;
else if (IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3))
selectCellType = HEAD;
else if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_KP_4))
selectCellType = TAIL;
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
grid[mouseYGridPos][mouseXGridPos] = selectCellType;
DrawCell(mouseXGridPos, mouseYGridPos,
GetCellColor(selectCellType));
}
DrawCellLines(mouseXGridPos, mouseYGridPos, WHITE);
个人认为预选高亮放在后面(优先级更高)会比较好,能时刻看清当前选中细胞在哪,哪怕是按住鼠标放置细胞时。
DrawCell
和 DrawCellLines
是对 DrawRectangle
和 DrawRectangleLines
的封装:
void DrawCell(int xGridPos, int yGridPos, Color cellColor) {
DrawRectangle(xGridPos * cellSize, yGridPos * cellSize, cellSize, cellSize,
cellColor);
DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
cellSize, BLACK);
}
void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
cellSize, cellColor);
}
接下来还需要在右上角添加四个色块,用于指示当前选中的细胞类型:
const int indicatorSize = cellSize;
const int indicatorPadding = cellSize / 2;
const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
const int indicatorY = indicatorPadding;
void DrawIndicators(void) {
Cell cellTypes[] = {EMPTY, CONDUCTOR, HEAD, TAIL};
for (int i = 0; i < 4; i++) {
int x = indicatorX + indicatorSize * i;
DrawRectangle(x, indicatorY, indicatorSize, indicatorSize,
GetCellColor(cellTypes[i]));
DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize, BLACK);
if (selectCellType == cellTypes[i]) {
DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize,
WHITE);
}
}
}
效果如下:
方案二完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
|
绘制播放按钮 ¶
按钮兼顾了状态切换和状态显示的功能,使用一个按钮即可表示当前是「播放」还是「暂停」状态,点击切换另一状态时也不会令人困惑。恰到好处的信息量与操作复杂程度。
在左上角绘制播放按钮,播放状态是两条竖着的矩形,暂停状态是个等腰三角:
const int buttonX = cellSize / 2;
const int buttonY = cellSize / 2;
const int buttonSize = cellSize;
const int barWidth = buttonSize / 4;
const int barGap = barWidth;
if (isPlaying) {
DrawRectangle(buttonX, buttonY, barWidth, buttonSize, WHITE);
DrawRectangle(buttonX + barWidth + barGap, buttonY, barWidth,
buttonSize, WHITE);
} else {
Vector2 v1 = (Vector2){buttonX, buttonY};
Vector2 v2 = (Vector2){buttonX, buttonY + buttonSize};
Vector2 v3 =
(Vector2){buttonX + buttonSize, (buttonY + buttonSize / 2.0)};
DrawTriangle(v1, v2, v3, WHITE);
}
整理代码 ¶
将处理用户输入的代码封装到 HandleUserInput
函数里:
void HandleUserInput(void) {
Vector2 mousePosition = GetMousePosition();
int mouseYGridPos = (int)(mousePosition.y / cellSize);
int mouseXGridPos = (int)(mousePosition.x / cellSize);
if (IsKeyPressed(KEY_SPACE))
isPlaying = !isPlaying;
if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1))
selectCellType = EMPTY;
else if (IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2))
selectCellType = CONDUCTOR;
else if (IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3))
selectCellType = HEAD;
else if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_KP_4))
selectCellType = TAIL;
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
grid[mouseYGridPos][mouseXGridPos] = selectCellType;
DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
}
DrawCellLines(mouseXGridPos, mouseYGridPos, WHITE);
}
将绘制指示器的代码封装到 DrawIndicators
函数里:
void DrawIndicators(void) {
Cell cellTypes[] = {EMPTY, CONDUCTOR, HEAD, TAIL};
for (int i = 0; i < 4; i++) {
int x = indicatorX + indicatorSize * i;
DrawRectangle(x, indicatorY, indicatorSize, indicatorSize,
GetCellColor(cellTypes[i]));
DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize, BLACK);
if (selectCellType == cellTypes[i]) {
DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize,
WHITE);
}
}
}
将绘制按钮的代码封装到 DrawPlayButton
函数里:
void DrawPlayButton(void) {
if (isPlaying) {
DrawRectangle(buttonX, buttonY, barWidth, buttonSize, WHITE);
DrawRectangle(buttonX + barWidth + barGap, buttonY, barWidth, buttonSize,
WHITE);
} else {
Vector2 v1 = (Vector2){buttonX, buttonY};
Vector2 v2 = (Vector2){buttonX, buttonY + buttonSize};
Vector2 v3 =
(Vector2){buttonX + buttonSize, (buttonY + buttonSize / 2.0)};
DrawTriangle(v1, v2, v3, WHITE);
}
}
将网格数组 grid
放在堆上,并将其指针作为全局变量,在 main
函数开始和结束时分别初始化和释放内存。
Cell** grid;
void InitGrid(void) {
grid = malloc(rows * sizeof(Cell*));
for (int y = 0; y < rows; y++) {
grid[y] = malloc(cols * sizeof(Cell));
}
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
grid[y][x] = EMPTY;
}
}
}
void FreeGrid(void) {
for (int i = 0; i < rows; i++) {
free(grid[i]);
}
free(grid);
}
UpdateGrid
函数中的 memcpy
不能用了,因为 malloc
分配出的 grid
内存布局与栈上的二维数组 newGrid
内存布局不一致:
void UpdateGrid(void) {
Cell newGrid[rows][cols];
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
switch (grid[y][x]) {
case EMPTY:
newGrid[y][x] = EMPTY;
break;
case HEAD:
newGrid[y][x] = TAIL;
break;
case TAIL:
newGrid[y][x] = CONDUCTOR;
break;
case CONDUCTOR: {
int headNeighbors = CountHeadNeighbors(y, x);
if (headNeighbors == 1 || headNeighbors == 2) {
newGrid[y][x] = HEAD;
} else {
newGrid[y][x] = CONDUCTOR;
}
} break;
}
}
}
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
grid[y][x] = newGrid[y][x];
}
}
}
这样子主函数就非常干净了:
int main(void) {
InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
SetTargetFPS(60);
InitGrid();
float elapsedTime = 0.0f;
while (!WindowShouldClose()) {
BeginDrawing();
ClearBackground(RAYWHITE);
float frameTime = GetFrameTime();
elapsedTime += frameTime;
if (isPlaying && elapsedTime >= refreshInterval) {
UpdateGrid();
elapsedTime = 0.0f;
}
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
Color cellColor = GetCellColor(grid[y][x]);
DrawCell(x, y, cellColor);
}
}
HandleUserInput();
DrawIndicators();
DrawPlayButton();
EndDrawing();
}
CloseWindow();
FreeGrid();
return 0;
}
当前完整代码
#include <stdlib.h>
#include "raylib.h"
#define EMPTY_COLOR \
CLITERAL(Color) { \
59, 74, 107, 255 \
}
#define HEAD_COLOR \
CLITERAL(Color) { \
34, 178, 218, 255 \
}
#define TAIL_COLOR \
CLITERAL(Color) { \
242, 53, 87, 255 \
}
#define CONDUCTOR_COLOR \
CLITERAL(Color) { \
240, 212, 58, 255 \
}
typedef enum { EMPTY, CONDUCTOR, HEAD, TAIL } Cell;
const int screenWidth = 800;
const int screenHeight = 460;
const int cellSize = 20;
const int indicatorSize = cellSize;
const int indicatorPadding = cellSize / 2;
const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
const int indicatorY = indicatorPadding;
const int buttonX = cellSize / 2;
const int buttonY = cellSize / 2;
const int buttonSize = cellSize;
const int barWidth = buttonSize / 4;
const int barGap = barWidth;
const int rows = screenHeight / cellSize;
const int cols = screenWidth / cellSize;
int isPlaying = 0;
const int refreshRate = 5;
const float refreshInterval = 1.0f / refreshRate;
Cell selectCellType = EMPTY;
Cell** grid;
Color GetCellColor(Cell cell) {
switch (cell) {
case EMPTY:
return EMPTY_COLOR;
case HEAD:
return HEAD_COLOR;
case TAIL:
return TAIL_COLOR;
case CONDUCTOR:
return CONDUCTOR_COLOR;
default:
return EMPTY_COLOR;
}
}
int CountHeadNeighbors(int row, int col) {
int headCount = 0;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
if (y == 0 && x == 0)
continue;
int newRow = row + y;
int newCol = col + x;
if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
if (grid[newRow][newCol] == HEAD) {
headCount++;
}
}
}
}
return headCount;
}
void InitGrid(void) {
grid = malloc(rows * sizeof(Cell*));
for (int y = 0; y < rows; y++) {
grid[y] = malloc(cols * sizeof(Cell));
}
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
grid[y][x] = EMPTY;
}
}
}
void FreeGrid(void) {
for (int i = 0; i < rows; i++) {
free(grid[i]);
}
free(grid);
}
void UpdateGrid(void) {
Cell newGrid[rows][cols];
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
switch (grid[y][x]) {
case EMPTY:
newGrid[y][x] = EMPTY;
break;
case HEAD:
newGrid[y][x] = TAIL;
break;
case TAIL:
newGrid[y][x] = CONDUCTOR;
break;
case CONDUCTOR: {
int headNeighbors = CountHeadNeighbors(y, x);
if (headNeighbors == 1 || headNeighbors == 2) {
newGrid[y][x] = HEAD;
} else {
newGrid[y][x] = CONDUCTOR;
}
} break;
}
}
}
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
grid[y][x] = newGrid[y][x];
}
}
}
void DrawCell(int xGridPos, int yGridPos, Color cellColor) {
DrawRectangle(xGridPos * cellSize, yGridPos * cellSize, cellSize, cellSize,
cellColor);
DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
cellSize, BLACK);
}
void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
cellSize, cellColor);
}
void DrawIndicators(void) {
Cell cellTypes[] = {EMPTY, CONDUCTOR, HEAD, TAIL};
for (int i = 0; i < 4; i++) {
int x = indicatorX + indicatorSize * i;
DrawRectangle(x, indicatorY, indicatorSize, indicatorSize,
GetCellColor(cellTypes[i]));
DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize, BLACK);
if (selectCellType == cellTypes[i]) {
DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize,
WHITE);
}
}
}
void DrawPlayButton(void) {
if (isPlaying) {
DrawRectangle(buttonX, buttonY, barWidth, buttonSize, WHITE);
DrawRectangle(buttonX + barWidth + barGap, buttonY, barWidth, buttonSize,
WHITE);
} else {
Vector2 v1 = (Vector2){buttonX, buttonY};
Vector2 v2 = (Vector2){buttonX, buttonY + buttonSize};
Vector2 v3 =
(Vector2){buttonX + buttonSize, (buttonY + buttonSize / 2.0)};
DrawTriangle(v1, v2, v3, WHITE);
}
}
void HandleUserInput(void) {
Vector2 mousePosition = GetMousePosition();
int mouseYGridPos = (int)(mousePosition.y / cellSize);
int mouseXGridPos = (int)(mousePosition.x / cellSize);
if (IsKeyPressed(KEY_SPACE))
isPlaying = !isPlaying;
if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1))
selectCellType = EMPTY;
else if (IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2))
selectCellType = CONDUCTOR;
else if (IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3))
selectCellType = HEAD;
else if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_KP_4))
selectCellType = TAIL;
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
grid[mouseYGridPos][mouseXGridPos] = selectCellType;
DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
}
DrawCellLines(mouseXGridPos, mouseYGridPos, WHITE);
}
int main(void) {
InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
SetTargetFPS(60);
InitGrid();
float elapsedTime = 0.0f;
while (!WindowShouldClose()) {
BeginDrawing();
ClearBackground(RAYWHITE);
float frameTime = GetFrameTime();
elapsedTime += frameTime;
if (isPlaying && elapsedTime >= refreshInterval) {
UpdateGrid();
elapsedTime = 0.0f;
}
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
Color cellColor = GetCellColor(grid[y][x]);
DrawCell(x, y, cellColor);
}
}
HandleUserInput();
DrawIndicators();
DrawPlayButton();
EndDrawing();
}
CloseWindow();
FreeGrid();
return 0;
}
按钮点击 ¶
当鼠标左键按下时,使用 CheckCollisionPointRec
检测鼠标位置是否在按钮范围内,以此判断是否按下按钮。当按下按钮时,不绘制细胞。
Rectangle buttonRect = {buttonX, buttonY, buttonSize, buttonSize};
Rectangle indicatorRect = {indicatorX, indicatorY, indicatorSize * 4,
indicatorSize};
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
if (CheckCollisionPointRec(mousePosition, buttonRect))
isPlaying = !isPlaying;
for (int i = 0; i < 4; i++) {
int x = indicatorX + indicatorSize * i;
Rectangle indicatorRect = {x, indicatorY, indicatorSize,
indicatorSize};
if (CheckCollisionPointRec(mousePosition, indicatorRect)) {
selectCellType = cellTypes[i];
break;
}
}
}
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) &&
!CheckCollisionPointRec(mousePosition, buttonRect) &&
!CheckCollisionPointRec(mousePosition, indicatorRect)) {
grid[mouseYGridPos][mouseXGridPos] = selectCellType;
DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
}
单次迭代 ¶
在暂停时按下 N 键进行单次迭代:
if (IsKeyPressed(KEY_G) && !isPlaying) {
UpdateGrid(grid);
}
当然按钮也是需要的。单次迭代的按钮形状,是暂停与播放状态的按钮形状相结合,放置在播放按钮后面:
const int nextButtonX = playButtonX + playButtonSize + cellSize / 2;
const int nextButtonY = cellSize / 2;
const int nextButtonSize = cellSize;
const int nextButtonBarWidth = playButtonSize / 4;
void DrawNextButton(void) {
if (!isPlaying) {
Vector2 v1 = (Vector2){nextButtonX, nextButtonY};
Vector2 v2 = (Vector2){nextButtonX, nextButtonY + nextButtonSize};
Vector2 v3 = (Vector2){nextButtonX + nextButtonSize,
(nextButtonY + nextButtonSize / 2.0)};
DrawTriangle(v1, v2, v3, WHITE);
DrawRectangle(nextButtonX + nextButtonSize - nextButtonBarWidth,
nextButtonY, nextButtonBarWidth, nextButtonSize, WHITE);
}
}
点击的实现与之前差不多,但要注意只有在暂停时(显示时)才可以点击:
Rectangle nextButtonRect = {nextButtonX, nextButtonY, nextButtonSize,
nextButtonSize};
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
if (CheckCollisionPointRec(mousePosition, playButtonRect))
isPlaying = !isPlaying;
if (CheckCollisionPointRec(mousePosition, nextButtonRect) && !isPlaying)
UpdateGrid();
for (int i = 0; i < 4; i++) {
int x = indicatorX + indicatorSize * i;
Rectangle indicatorRect = {x, indicatorY, indicatorSize,
indicatorSize};
if (CheckCollisionPointRec(mousePosition, indicatorRect)) {
selectCellType = cellTypes[i];
break;
}
}
}
if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) &&
!CheckCollisionPointRec(mousePosition, playButtonRect) &&
!CheckCollisionPointRec(mousePosition, nextButtonRect) &&
!CheckCollisionPointRec(mousePosition, indicatorRect)) {
grid[mouseYGridPos][mouseXGridPos] = selectCellType;
DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
}
当前完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
|
清除网格 ¶
将设置所有细胞为空的代码放在 ClearGrid
函数里:
void ClearGrid(void) {
for (int y = 0; y < rows; y++) {
for (int x = 0; x < cols; x++) {
grid[y][x] = EMPTY;
}
}
}
void InitGrid(void) {
grid = malloc(rows * sizeof(Cell*));
for (int y = 0; y < rows; y++) {
grid[y] = malloc(cols * sizeof(Cell));
}
ClearGrid();
}
按下 C 键时清除网格:
if (IsKeyPressed(KEY_C)) {
ClearGrid();
}
总结 ¶
最终代码见 github.com/13m0n4de/wireworld/blob/main/main.c。
暂时就到这里,设置和无限画布功能之后再写吧,这篇文章有点长了。
Raylib 真的很好用,找回了不少开发游戏的快乐。
下一部分。