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 可以用来模拟电路逻辑,如下图的二极管:

Wireworld_two-diodes.gif

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!,字体还蛮好看的。

basic_example.png

设置编辑器

我的 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 图形显示三个功能,只需要使用两个模块:

  • rcore: Window / Graphic Context / Inputs management.
  • rshapes: Basic 2D shapes drawing functions.

它们都共用 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);
cells.png
当前完整代码
 1#include "raylib.h"
 2
 3#define EMPTY_COLOR         CLITERAL(Color) { 59, 74, 107, 255 }
 4#define HEAD_COLOR          CLITERAL(Color) { 34, 178, 218, 255 }
 5#define TAIL_COLOR          CLITERAL(Color) { 242, 53, 87, 255 }
 6#define CONDUCTOR_COLOR     CLITERAL(Color) { 240, 212, 58, 255 }
 7
 8const int screenWidth = 800;
 9const int screenHeight = 450;
10
11const int cellSize = 20;
12
13int main(void) {
14
15    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
16
17    SetTargetFPS(60);
18
19    while (!WindowShouldClose()) {
20        BeginDrawing();
21        ClearBackground(RAYWHITE);
22
23        DrawRectangle(100, 100, cellSize, cellSize, EMPTY_COLOR);
24        DrawRectangle(120, 100, cellSize, cellSize, HEAD_COLOR);
25        DrawRectangle(140, 100, cellSize, cellSize, TAIL_COLOR);
26        DrawRectangle(160, 100, cellSize, cellSize, CONDUCTOR_COLOR);
27
28        EndDrawing();
29    }
30
31    CloseWindow();
32
33    return 0;
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,这样可以被细胞大小整除):

grid.png

这只是显示效果上的网格,对于网格数据,还是需要设计一个数据结构,比如二维数组。在二维数组中,每个元素存储细胞种类,比如「空」、「电子头」。

颜色、位置等信息不与单个细胞相关联,不需要额外保存在细胞中。

使用枚举表示细胞:

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;
    }
}

在主循环中遍历这个数组并绘制每个细胞:

 1grid[5][5] = HEAD;
 2grid[5][6] = TAIL;
 3grid[5][7] = CONDUCTOR;
 4
 5while (!WindowShouldClose()) {
 6    BeginDrawing();
 7    ClearBackground(RAYWHITE);
 8
 9    for (int i = 0; i < rows; i++) {
10        for (int j = 0; j < cols; j++) {
11            Color cellColor = GetCellColor(grid[i][j]);
12            DrawRectangle(j * cellSize, i * cellSize, cellSize, cellSize,
13                          cellColor);
14            DrawRectangleLines(j * cellSize, i * cellSize, cellSize,
15                               cellSize, BLACK);
16        }
17    }
18
19    EndDrawing();
20}
当前完整代码
 1#include "raylib.h"
 2
 3#define EMPTY_COLOR         CLITERAL(Color) { 59, 74, 107, 255 }
 4#define HEAD_COLOR          CLITERAL(Color) { 34, 178, 218, 255 }
 5#define TAIL_COLOR          CLITERAL(Color) { 242, 53, 87, 255 }
 6#define CONDUCTOR_COLOR     CLITERAL(Color) { 240, 212, 58, 255 }
 7
 8const int screenWidth = 800;
 9const int screenHeight = 460;
10
11const int cellSize = 20;
12
13const int rows = screenHeight / cellSize;
14const int cols = screenWidth / cellSize;
15
16typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
17
18Color GetCellColor(Cell cell) {
19    switch (cell) {
20        case EMPTY:
21            return EMPTY_COLOR;
22        case HEAD:
23            return HEAD_COLOR;
24        case TAIL:
25            return TAIL_COLOR;
26        case CONDUCTOR:
27            return CONDUCTOR_COLOR;
28        default:
29            return EMPTY_COLOR;
30    }
31}
32
33int main(void) {
34    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
35    SetTargetFPS(60);
36
37    Cell grid[rows][cols];
38
39    for (int i = 0; i < rows; i++) {
40        for (int j = 0; j < cols; j++) {
41            grid[i][j] = EMPTY;
42        }
43    }
44
45    grid[5][5] = HEAD;
46    grid[5][6] = TAIL;
47    grid[5][7] = CONDUCTOR;
48
49    while (!WindowShouldClose()) {
50        BeginDrawing();
51        ClearBackground(RAYWHITE);
52
53        for (int i = 0; i < rows; i++) {
54            for (int j = 0; j < cols; j++) {
55                Color cellColor = GetCellColor(grid[i][j]);
56                DrawRectangle(j * cellSize, i * cellSize, cellSize, cellSize,
57                              cellColor);
58                DrawRectangleLines(j * cellSize, i * cellSize, cellSize,
59                                   cellSize, BLACK);
60            }
61        }
62
63        EndDrawing();
64    }
65
66    CloseWindow();
67
68    return 0;
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),这意味着在上面的规则中,「相邻」表示在任何方向上(正交和对角线)都有一个单元(范围值为 1)。

Moore_neighborhood_with_cardinal_directions.svg

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); 加入主循环,并初始化一些细胞:

 1grid[5][5] = CONDUCTOR;
 2grid[5][6] = TAIL;
 3grid[5][7] = HEAD;
 4grid[5][8] = CONDUCTOR;
 5grid[5][9] = CONDUCTOR;
 6
 7grid[6][4] = CONDUCTOR;
 8grid[6][10] = CONDUCTOR;
 9
10grid[7][5] = CONDUCTOR;
11grid[7][6] = CONDUCTOR;
12grid[7][7] = CONDUCTOR;
13grid[7][8] = CONDUCTOR;
14grid[7][9] = CONDUCTOR;
15
16while (!WindowShouldClose()) {
17    BeginDrawing();
18    ClearBackground(RAYWHITE);
19
20    UpdateGrid(grid);

将 FPS 暂时设置为 5:

SetTargetFPS(5);

运行效果如图,一个时钟发射器:

当前完整代码
  1#include <string.h>
  2#include "raylib.h"
  3
  4#define EMPTY_COLOR         CLITERAL(Color) { 59, 74, 107, 255 }
  5#define HEAD_COLOR          CLITERAL(Color) { 34, 178, 218, 255 }
  6#define TAIL_COLOR          CLITERAL(Color) { 242, 53, 87, 255 }
  7#define CONDUCTOR_COLOR     CLITERAL(Color) { 240, 212, 58, 255 }
  8
  9const int screenWidth = 800;
 10const int screenHeight = 460;
 11
 12const int cellSize = 20;
 13
 14const int rows = screenHeight / cellSize;
 15const int cols = screenWidth / cellSize;
 16
 17typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
 18
 19Color GetCellColor(Cell cell) {
 20    switch (cell) {
 21        case EMPTY:
 22            return EMPTY_COLOR;
 23        case HEAD:
 24            return HEAD_COLOR;
 25        case TAIL:
 26            return TAIL_COLOR;
 27        case CONDUCTOR:
 28            return CONDUCTOR_COLOR;
 29        default:
 30            return EMPTY_COLOR;
 31    }
 32}
 33
 34int CountHeadNeighbors(Cell grid[rows][cols], int row, int col) {
 35    int headCount = 0;
 36
 37    for (int i = -1; i <= 1; i++) {
 38        for (int j = -1; j <= 1; j++) {
 39            if (i == 0 && j == 0)
 40                continue;
 41            int newRow = row + i;
 42            int newCol = col + j;
 43            if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
 44                if (grid[newRow][newCol] == HEAD) {
 45                    headCount++;
 46                }
 47            }
 48        }
 49    }
 50
 51    return headCount;
 52}
 53
 54void UpdateGrid(Cell grid[rows][cols]) {
 55    Cell newGrid[rows][cols];
 56
 57    for (int i = 0; i < rows; i++) {
 58        for (int j = 0; j < cols; j++) {
 59            switch (grid[i][j]) {
 60                case EMPTY:
 61                    newGrid[i][j] = EMPTY;
 62                    break;
 63                case HEAD:
 64                    newGrid[i][j] = TAIL;
 65                    break;
 66                case TAIL:
 67                    newGrid[i][j] = CONDUCTOR;
 68                    break;
 69                case CONDUCTOR: {
 70                    int headNeighbors = CountHeadNeighbors(grid, i, j);
 71                    if (headNeighbors == 1 || headNeighbors == 2) {
 72                        newGrid[i][j] = HEAD;
 73                    } else {
 74                        newGrid[i][j] = CONDUCTOR;
 75                    }
 76                } break;
 77            }
 78        }
 79    }
 80
 81    memcpy(grid, newGrid, sizeof(newGrid));
 82}
 83
 84int main(void) {
 85    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
 86    SetTargetFPS(5);
 87
 88    Cell grid[rows][cols];
 89
 90    for (int i = 0; i < rows; i++) {
 91        for (int j = 0; j < cols; j++) {
 92            grid[i][j] = EMPTY;
 93        }
 94    }
 95
 96    grid[5][5] = CONDUCTOR;
 97    grid[5][6] = TAIL;
 98    grid[5][7] = HEAD;
 99    grid[5][8] = CONDUCTOR;
100    grid[5][9] = CONDUCTOR;
101
102    grid[6][4] = CONDUCTOR;
103    grid[6][10] = CONDUCTOR;
104
105    grid[7][5] = CONDUCTOR;
106    grid[7][6] = CONDUCTOR;
107    grid[7][7] = CONDUCTOR;
108    grid[7][8] = CONDUCTOR;
109    grid[7][9] = CONDUCTOR;
110
111    while (!WindowShouldClose()) {
112        BeginDrawing();
113        ClearBackground(RAYWHITE);
114
115        UpdateGrid(grid);
116
117        for (int i = 0; i < rows; i++) {
118            for (int j = 0; j < cols; j++) {
119                Color cellColor = GetCellColor(grid[i][j]);
120                DrawRectangle(j * cellSize, i * cellSize, cellSize, cellSize,
121                              cellColor);
122                DrawRectangleLines(j * cellSize, i * cellSize, cellSize,
123                                   cellSize, BLACK);
124            }
125        }
126
127        EndDrawing();
128    }
129
130    CloseWindow();
131
132    return 0;
133}

暂停和播放

先实现简单的暂停和播放功能,按下空格键暂停,再按一次播放。

游戏只有两个状态:「暂停」和「播放」,不需要使用枚举。

int isPlaying = 0;

在按下空格时切换播放状态,且只有播放中才会更新网格:

if (IsKeyPressed(KEY_SPACE))
    isPlaying = !isPlaying;

if (isPlaying) {
    UpdateGrid(grid);
}

之前将 FPS 设置为 5 是因为一秒更新六十次网格实在太快,但此时又会因为帧率过低导致键盘输入概率捕获不到。所以我们需要真正意义上的每秒迭代 N 次(刷新 N 次网格),而不是依靠 FPS。

网格刷新速率

使用 GetFrameTime 获得最后一帧的绘制时间 (delta time),累加 elapsedTime,并在达到刷新间隔 refreshInterval 时刷新网格。

 1const int refreshRate = 5;
 2const float refreshInterval = 1.0f / refreshRate;
 3
 4float elapsedTime = 0.0f;
 5
 6while (!WindowShouldClose()) {
 7    BeginDrawing();
 8    ClearBackground(RAYWHITE);
 9
10    float frameTime = GetFrameTime();
11    elapsedTime += frameTime;
12
13    if (IsKeyPressed(KEY_SPACE))
14        isPlaying = !isPlaying;
15
16    if (isPlaying && elapsedTime >= refreshInterval) {
17        UpdateGrid(grid);
18        elapsedTime = 0.0f;
19    }
当前完整代码
  1#include <string.h>
  2#include "raylib.h"
  3
  4#define EMPTY_COLOR         CLITERAL(Color) { 59, 74, 107, 255 }
  5#define HEAD_COLOR          CLITERAL(Color) { 34, 178, 218, 255 }
  6#define TAIL_COLOR          CLITERAL(Color) { 242, 53, 87, 255 }
  7#define CONDUCTOR_COLOR     CLITERAL(Color) { 240, 212, 58, 255 }
  8
  9const int screenWidth = 800;
 10const int screenHeight = 460;
 11
 12const int cellSize = 20;
 13
 14const int rows = screenHeight / cellSize;
 15const int cols = screenWidth / cellSize;
 16
 17int isPlaying = 0;
 18
 19const int refreshRate = 5;
 20const float refreshInterval = 1.0f / refreshRate;
 21
 22typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
 23
 24Color GetCellColor(Cell cell) {
 25    switch (cell) {
 26        case EMPTY:
 27            return EMPTY_COLOR;
 28        case HEAD:
 29            return HEAD_COLOR;
 30        case TAIL:
 31            return TAIL_COLOR;
 32        case CONDUCTOR:
 33            return CONDUCTOR_COLOR;
 34        default:
 35            return EMPTY_COLOR;
 36    }
 37}
 38
 39int CountHeadNeighbors(Cell grid[rows][cols], int row, int col) {
 40    int headCount = 0;
 41
 42    for (int i = -1; i <= 1; i++) {
 43        for (int j = -1; j <= 1; j++) {
 44            if (i == 0 && j == 0)
 45                continue;
 46            int newRow = row + i;
 47            int newCol = col + j;
 48            if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
 49                if (grid[newRow][newCol] == HEAD) {
 50                    headCount++;
 51                }
 52            }
 53        }
 54    }
 55
 56    return headCount;
 57}
 58
 59void UpdateGrid(Cell grid[rows][cols]) {
 60    Cell newGrid[rows][cols];
 61
 62    for (int i = 0; i < rows; i++) {
 63        for (int j = 0; j < cols; j++) {
 64            switch (grid[i][j]) {
 65                case EMPTY:
 66                    newGrid[i][j] = EMPTY;
 67                    break;
 68                case HEAD:
 69                    newGrid[i][j] = TAIL;
 70                    break;
 71                case TAIL:
 72                    newGrid[i][j] = CONDUCTOR;
 73                    break;
 74                case CONDUCTOR: {
 75                    int headNeighbors = CountHeadNeighbors(grid, i, j);
 76                    if (headNeighbors == 1 || headNeighbors == 2) {
 77                        newGrid[i][j] = HEAD;
 78                    } else {
 79                        newGrid[i][j] = CONDUCTOR;
 80                    }
 81                } break;
 82            }
 83        }
 84    }
 85
 86    memcpy(grid, newGrid, sizeof(newGrid));
 87}
 88
 89int main(void) {
 90    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
 91    SetTargetFPS(60);
 92
 93    Cell grid[rows][cols];
 94    for (int i = 0; i < rows; i++) {
 95        for (int j = 0; j < cols; j++) {
 96            grid[i][j] = EMPTY;
 97        }
 98    }
 99
100    grid[5][5] = CONDUCTOR;
101    grid[5][6] = TAIL;
102    grid[5][7] = HEAD;
103    grid[5][8] = CONDUCTOR;
104    grid[5][9] = CONDUCTOR;
105
106    grid[6][4] = CONDUCTOR;
107    grid[6][10] = CONDUCTOR;
108
109    grid[7][5] = CONDUCTOR;
110    grid[7][6] = CONDUCTOR;
111    grid[7][7] = CONDUCTOR;
112    grid[7][8] = CONDUCTOR;
113    grid[7][9] = CONDUCTOR;
114
115    float elapsedTime = 0.0f;
116
117    while (!WindowShouldClose()) {
118        BeginDrawing();
119        ClearBackground(RAYWHITE);
120
121        float frameTime = GetFrameTime();
122        elapsedTime += frameTime;
123
124        if (IsKeyPressed(KEY_SPACE))
125            isPlaying = !isPlaying;
126
127        if (isPlaying && elapsedTime >= refreshInterval) {
128            UpdateGrid(grid);
129            elapsedTime = 0.0f;
130        }
131
132        for (int i = 0; i < rows; i++) {
133            for (int j = 0; j < cols; j++) {
134                Color cellColor = GetCellColor(grid[i][j]);
135                DrawRectangle(j * cellSize, i * cellSize, cellSize, cellSize,
136                              cellColor);
137                DrawRectangleLines(j * cellSize, i * cellSize, cellSize,
138                                   cellSize, BLACK);
139            }
140        }
141
142        EndDrawing();
143    }
144
145    CloseWindow();
146
147    return 0;
148}

高亮预选细胞

制作细胞位置预览效果:鼠标放置在的单元格边框会进行高亮。

使用 GetMousePosition 获得鼠标位置,并计算对应的细胞位置,使用 DrawRectangleLines 绘制高亮色边框。

 1for (int i = 0; i < rows; i++) {
 2    for (int j = 0; j < cols; j++) {
 3        Color cellColor = GetCellColor(grid[i][j]);
 4        DrawRectangle(j * cellSize, i * cellSize, cellSize, cellSize,
 5                      cellColor);
 6        DrawRectangleLines(j * cellSize, i * cellSize, cellSize,
 7                           cellSize, BLACK);
 8    }
 9}
10
11Vector2 mousePosition = GetMousePosition();
12int mouseYGridPos = (int)(mousePosition.y / cellSize);
13int mouseXGridPos = (int)(mousePosition.x / cellSize);
14DrawRectangleLines(mouseXGridPos * cellSize, mouseYGridPos * cellSize,
15                   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#include <string.h>
  2#include "raylib.h"
  3
  4#define EMPTY_COLOR      \
  5    CLITERAL(Color) {    \
  6        59, 74, 107, 255 \
  7    }
  8#define HEAD_COLOR        \
  9    CLITERAL(Color) {     \
 10        34, 178, 218, 255 \
 11    }
 12#define TAIL_COLOR       \
 13    CLITERAL(Color) {    \
 14        242, 53, 87, 255 \
 15    }
 16#define CONDUCTOR_COLOR   \
 17    CLITERAL(Color) {     \
 18        240, 212, 58, 255 \
 19    }
 20
 21const int screenWidth = 800;
 22const int screenHeight = 460;
 23
 24const int cellSize = 20;
 25
 26const int rows = screenHeight / cellSize;
 27const int cols = screenWidth / cellSize;
 28
 29int isPlaying = 0;
 30
 31const int refreshRate = 5;
 32const float refreshInterval = 1.0f / refreshRate;
 33
 34typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
 35
 36Color GetCellColor(Cell cell) {
 37    switch (cell) {
 38        case EMPTY:
 39            return EMPTY_COLOR;
 40        case HEAD:
 41            return HEAD_COLOR;
 42        case TAIL:
 43            return TAIL_COLOR;
 44        case CONDUCTOR:
 45            return CONDUCTOR_COLOR;
 46        default:
 47            return EMPTY_COLOR;
 48    }
 49}
 50
 51int CountHeadNeighbors(Cell grid[rows][cols], int row, int col) {
 52    int headCount = 0;
 53
 54    for (int y = -1; y <= 1; y++) {
 55        for (int x = -1; x <= 1; x++) {
 56            if (y == 0 && x == 0)
 57                continue;
 58            int newRow = row + y;
 59            int newCol = col + x;
 60            if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
 61                if (grid[newRow][newCol] == HEAD) {
 62                    headCount++;
 63                }
 64            }
 65        }
 66    }
 67
 68    return headCount;
 69}
 70
 71void UpdateGrid(Cell grid[rows][cols]) {
 72    Cell newGrid[rows][cols];
 73
 74    for (int y = 0; y < rows; y++) {
 75        for (int x = 0; x < cols; x++) {
 76            switch (grid[y][x]) {
 77                case EMPTY:
 78                    newGrid[y][x] = EMPTY;
 79                    break;
 80                case HEAD:
 81                    newGrid[y][x] = TAIL;
 82                    break;
 83                case TAIL:
 84                    newGrid[y][x] = CONDUCTOR;
 85                    break;
 86                case CONDUCTOR: {
 87                    int headNeighbors = CountHeadNeighbors(grid, y, x);
 88                    if (headNeighbors == 1 || headNeighbors == 2) {
 89                        newGrid[y][x] = HEAD;
 90                    } else {
 91                        newGrid[y][x] = CONDUCTOR;
 92                    }
 93                } break;
 94            }
 95        }
 96    }
 97
 98    memcpy(grid, newGrid, sizeof(newGrid));
 99}
100
101int main(void) {
102    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
103    SetTargetFPS(60);
104
105    Cell grid[rows][cols];
106    for (int y = 0; y < rows; y++) {
107        for (int x = 0; x < cols; x++) {
108            grid[y][x] = EMPTY;
109        }
110    }
111
112    float elapsedTime = 0.0f;
113
114    while (!WindowShouldClose()) {
115        BeginDrawing();
116        ClearBackground(RAYWHITE);
117
118        float frameTime = GetFrameTime();
119        elapsedTime += frameTime;
120
121        if (IsKeyPressed(KEY_SPACE))
122            isPlaying = !isPlaying;
123
124        if (isPlaying && elapsedTime >= refreshInterval) {
125            UpdateGrid(grid);
126            elapsedTime = 0.0f;
127        }
128
129        for (int y = 0; y < rows; y++) {
130            for (int x = 0; x < cols; x++) {
131                Color cellColor = GetCellColor(grid[y][x]);
132                DrawRectangle(x * cellSize, y * cellSize, cellSize, cellSize,
133                              cellColor);
134                DrawRectangleLines(x * cellSize, y * cellSize, cellSize,
135                                   cellSize, BLACK);
136            }
137        }
138
139        Vector2 mousePosition = GetMousePosition();
140        int mouseYGridPos = (int)(mousePosition.y / cellSize);
141        int mouseXGridPos = (int)(mousePosition.x / cellSize);
142        DrawRectangleLines(mouseXGridPos * cellSize, mouseYGridPos * cellSize,
143                           cellSize, cellSize, WHITE);
144
145        Color cellColor;
146        if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
147            if (grid[mouseYGridPos][mouseXGridPos] != EMPTY) {
148                grid[mouseYGridPos][mouseXGridPos] = EMPTY;
149                cellColor = EMPTY_COLOR;
150            } else {
151                grid[mouseYGridPos][mouseXGridPos] = CONDUCTOR;
152                cellColor = CONDUCTOR_COLOR;
153            }
154            DrawRectangle(mouseXGridPos * cellSize, mouseYGridPos * cellSize,
155                          cellSize, cellSize, cellColor);
156            DrawRectangleLines(mouseXGridPos * cellSize,
157                               mouseYGridPos * cellSize, cellSize, cellSize,
158                               BLACK);
159        } else if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) {
160            if (grid[mouseYGridPos][mouseXGridPos] != HEAD) {
161                grid[mouseYGridPos][mouseXGridPos] = HEAD;
162                cellColor = EMPTY_COLOR;
163            } else {
164                grid[mouseYGridPos][mouseXGridPos] = CONDUCTOR;
165                cellColor = CONDUCTOR_COLOR;
166            }
167            DrawRectangle(mouseXGridPos * cellSize, mouseYGridPos * cellSize,
168                          cellSize, cellSize, cellColor);
169            DrawRectangleLines(mouseXGridPos * cellSize,
170                               mouseYGridPos * cellSize, cellSize, cellSize,
171                               BLACK);
172        }
173
174        if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
175            DrawText("MOUSE: LEFT", 20, 420, 20, CONDUCTOR_COLOR);
176        } else if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
177            DrawText("MOUSE: RIGHT", 20, 420, 20, HEAD_COLOR);
178        } else {
179            DrawText("MOUSE: NONE", 20, 420, 20, BROWN);
180        }
181
182        EndDrawing();
183    }
184
185    CloseWindow();
186
187    return 0;
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);

个人认为预选高亮放在后面(优先级更高)会比较好,能时刻看清当前选中细胞在哪,哪怕是按住鼠标放置细胞时。

DrawCellDrawCellLines 是对 DrawRectangleDrawRectangleLines 的封装:

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#include <string.h>
  2#include "raylib.h"
  3
  4#define EMPTY_COLOR      \
  5    CLITERAL(Color) {    \
  6        59, 74, 107, 255 \
  7    }
  8#define HEAD_COLOR        \
  9    CLITERAL(Color) {     \
 10        34, 178, 218, 255 \
 11    }
 12#define TAIL_COLOR       \
 13    CLITERAL(Color) {    \
 14        242, 53, 87, 255 \
 15    }
 16#define CONDUCTOR_COLOR   \
 17    CLITERAL(Color) {     \
 18        240, 212, 58, 255 \
 19    }
 20
 21typedef enum { EMPTY, CONDUCTOR, HEAD, TAIL } Cell;
 22
 23const int screenWidth = 800;
 24const int screenHeight = 460;
 25
 26const int cellSize = 20;
 27
 28const int indicatorSize = cellSize;
 29const int indicatorPadding = cellSize / 2;
 30const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
 31const int indicatorY = indicatorPadding;
 32
 33const int rows = screenHeight / cellSize;
 34const int cols = screenWidth / cellSize;
 35
 36int isPlaying = 0;
 37
 38const int refreshRate = 5;
 39const float refreshInterval = 1.0f / refreshRate;
 40
 41Cell selectCellType = EMPTY;
 42
 43Color GetCellColor(Cell cell) {
 44    switch (cell) {
 45        case EMPTY:
 46            return EMPTY_COLOR;
 47        case HEAD:
 48            return HEAD_COLOR;
 49        case TAIL:
 50            return TAIL_COLOR;
 51        case CONDUCTOR:
 52            return CONDUCTOR_COLOR;
 53        default:
 54            return EMPTY_COLOR;
 55    }
 56}
 57
 58int CountHeadNeighbors(Cell grid[rows][cols], int row, int col) {
 59    int headCount = 0;
 60
 61    for (int y = -1; y <= 1; y++) {
 62        for (int x = -1; x <= 1; x++) {
 63            if (y == 0 && x == 0)
 64                continue;
 65            int newRow = row + y;
 66            int newCol = col + x;
 67            if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
 68                if (grid[newRow][newCol] == HEAD) {
 69                    headCount++;
 70                }
 71            }
 72        }
 73    }
 74
 75    return headCount;
 76}
 77
 78void UpdateGrid(Cell grid[rows][cols]) {
 79    Cell newGrid[rows][cols];
 80
 81    for (int y = 0; y < rows; y++) {
 82        for (int x = 0; x < cols; x++) {
 83            switch (grid[y][x]) {
 84                case EMPTY:
 85                    newGrid[y][x] = EMPTY;
 86                    break;
 87                case HEAD:
 88                    newGrid[y][x] = TAIL;
 89                    break;
 90                case TAIL:
 91                    newGrid[y][x] = CONDUCTOR;
 92                    break;
 93                case CONDUCTOR: {
 94                    int headNeighbors = CountHeadNeighbors(grid, y, x);
 95                    if (headNeighbors == 1 || headNeighbors == 2) {
 96                        newGrid[y][x] = HEAD;
 97                    } else {
 98                        newGrid[y][x] = CONDUCTOR;
 99                    }
100                } break;
101            }
102        }
103    }
104
105    memcpy(grid, newGrid, sizeof(newGrid));
106}
107
108void DrawCell(int xGridPos, int yGridPos, Color cellColor) {
109    DrawRectangle(xGridPos * cellSize, yGridPos * cellSize, cellSize, cellSize,
110                  cellColor);
111    DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
112                       cellSize, BLACK);
113}
114
115void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
116    DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
117                       cellSize, cellColor);
118}
119
120int main(void) {
121    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
122    SetTargetFPS(60);
123
124    Cell grid[rows][cols];
125    for (int y = 0; y < rows; y++) {
126        for (int x = 0; x < cols; x++) {
127            grid[y][x] = EMPTY;
128        }
129    }
130
131    float elapsedTime = 0.0f;
132
133    while (!WindowShouldClose()) {
134        BeginDrawing();
135        ClearBackground(RAYWHITE);
136
137        float frameTime = GetFrameTime();
138        elapsedTime += frameTime;
139
140        if (IsKeyPressed(KEY_SPACE))
141            isPlaying = !isPlaying;
142
143        if (isPlaying && elapsedTime >= refreshInterval) {
144            UpdateGrid(grid);
145            elapsedTime = 0.0f;
146        }
147
148        for (int y = 0; y < rows; y++) {
149            for (int x = 0; x < cols; x++) {
150                Color cellColor = GetCellColor(grid[y][x]);
151                DrawCell(x, y, cellColor);
152            }
153        }
154
155        Vector2 mousePosition = GetMousePosition();
156        int mouseYGridPos = (int)(mousePosition.y / cellSize);
157        int mouseXGridPos = (int)(mousePosition.x / cellSize);
158
159        if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1))
160            selectCellType = EMPTY;
161        else if (IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2))
162            selectCellType = CONDUCTOR;
163        else if (IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3))
164            selectCellType = HEAD;
165        else if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_KP_4))
166            selectCellType = TAIL;
167
168        if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
169            grid[mouseYGridPos][mouseXGridPos] = selectCellType;
170            DrawCell(mouseXGridPos, mouseYGridPos,
171                     GetCellColor(selectCellType));
172        }
173
174        DrawCellLines(mouseXGridPos, mouseYGridPos, WHITE);
175
176        Cell cellTypes[] = {EMPTY, CONDUCTOR, HEAD, TAIL};
177        for (int i = 0; i < 4; i++) {
178            int x = indicatorX + indicatorSize * i;
179            DrawRectangle(x, indicatorY, indicatorSize, indicatorSize,
180                          GetCellColor(cellTypes[i]));
181            DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize, BLACK);
182            if (selectCellType == cellTypes[i]) {
183                DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize,
184                                   WHITE);
185            }
186        }
187
188        EndDrawing();
189    }
190
191    CloseWindow();
192
193    return 0;
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 内存布局不一致:

 1void UpdateGrid(void) {
 2    Cell newGrid[rows][cols];
 3
 4    for (int y = 0; y < rows; y++) {
 5        for (int x = 0; x < cols; x++) {
 6            switch (grid[y][x]) {
 7                case EMPTY:
 8                    newGrid[y][x] = EMPTY;
 9                    break;
10                case HEAD:
11                    newGrid[y][x] = TAIL;
12                    break;
13                case TAIL:
14                    newGrid[y][x] = CONDUCTOR;
15                    break;
16                case CONDUCTOR: {
17                    int headNeighbors = CountHeadNeighbors(y, x);
18                    if (headNeighbors == 1 || headNeighbors == 2) {
19                        newGrid[y][x] = HEAD;
20                    } else {
21                        newGrid[y][x] = CONDUCTOR;
22                    }
23                } break;
24            }
25        }
26    }
27
28    for (int y = 0; y < rows; y++) {
29        for (int x = 0; x < cols; x++) {
30            grid[y][x] = newGrid[y][x];
31        }
32    }
33}

这样子主函数就非常干净了:

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;
}
当前完整代码
  1#include <stdlib.h>
  2#include "raylib.h"
  3
  4#define EMPTY_COLOR      \
  5    CLITERAL(Color) {    \
  6        59, 74, 107, 255 \
  7    }
  8#define HEAD_COLOR        \
  9    CLITERAL(Color) {     \
 10        34, 178, 218, 255 \
 11    }
 12#define TAIL_COLOR       \
 13    CLITERAL(Color) {    \
 14        242, 53, 87, 255 \
 15    }
 16#define CONDUCTOR_COLOR   \
 17    CLITERAL(Color) {     \
 18        240, 212, 58, 255 \
 19    }
 20
 21typedef enum { EMPTY, CONDUCTOR, HEAD, TAIL } Cell;
 22
 23const int screenWidth = 800;
 24const int screenHeight = 460;
 25
 26const int cellSize = 20;
 27
 28const int indicatorSize = cellSize;
 29const int indicatorPadding = cellSize / 2;
 30const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
 31const int indicatorY = indicatorPadding;
 32
 33const int buttonX = cellSize / 2;
 34const int buttonY = cellSize / 2;
 35const int buttonSize = cellSize;
 36const int barWidth = buttonSize / 4;
 37const int barGap = barWidth;
 38
 39const int rows = screenHeight / cellSize;
 40const int cols = screenWidth / cellSize;
 41
 42int isPlaying = 0;
 43
 44const int refreshRate = 5;
 45const float refreshInterval = 1.0f / refreshRate;
 46
 47Cell selectCellType = EMPTY;
 48
 49Cell** grid;
 50
 51Color GetCellColor(Cell cell) {
 52    switch (cell) {
 53        case EMPTY:
 54            return EMPTY_COLOR;
 55        case HEAD:
 56            return HEAD_COLOR;
 57        case TAIL:
 58            return TAIL_COLOR;
 59        case CONDUCTOR:
 60            return CONDUCTOR_COLOR;
 61        default:
 62            return EMPTY_COLOR;
 63    }
 64}
 65
 66int CountHeadNeighbors(int row, int col) {
 67    int headCount = 0;
 68
 69    for (int y = -1; y <= 1; y++) {
 70        for (int x = -1; x <= 1; x++) {
 71            if (y == 0 && x == 0)
 72                continue;
 73            int newRow = row + y;
 74            int newCol = col + x;
 75            if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
 76                if (grid[newRow][newCol] == HEAD) {
 77                    headCount++;
 78                }
 79            }
 80        }
 81    }
 82
 83    return headCount;
 84}
 85
 86void InitGrid(void) {
 87    grid = malloc(rows * sizeof(Cell*));
 88    for (int y = 0; y < rows; y++) {
 89        grid[y] = malloc(cols * sizeof(Cell));
 90    }
 91
 92    for (int y = 0; y < rows; y++) {
 93        for (int x = 0; x < cols; x++) {
 94            grid[y][x] = EMPTY;
 95        }
 96    }
 97}
 98
 99void FreeGrid(void) {
100    for (int i = 0; i < rows; i++) {
101        free(grid[i]);
102    }
103    free(grid);
104}
105
106void UpdateGrid(void) {
107    Cell newGrid[rows][cols];
108
109    for (int y = 0; y < rows; y++) {
110        for (int x = 0; x < cols; x++) {
111            switch (grid[y][x]) {
112                case EMPTY:
113                    newGrid[y][x] = EMPTY;
114                    break;
115                case HEAD:
116                    newGrid[y][x] = TAIL;
117                    break;
118                case TAIL:
119                    newGrid[y][x] = CONDUCTOR;
120                    break;
121                case CONDUCTOR: {
122                    int headNeighbors = CountHeadNeighbors(y, x);
123                    if (headNeighbors == 1 || headNeighbors == 2) {
124                        newGrid[y][x] = HEAD;
125                    } else {
126                        newGrid[y][x] = CONDUCTOR;
127                    }
128                } break;
129            }
130        }
131    }
132
133    for (int y = 0; y < rows; y++) {
134        for (int x = 0; x < cols; x++) {
135            grid[y][x] = newGrid[y][x];
136        }
137    }
138}
139
140void DrawCell(int xGridPos, int yGridPos, Color cellColor) {
141    DrawRectangle(xGridPos * cellSize, yGridPos * cellSize, cellSize, cellSize,
142                  cellColor);
143    DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
144                       cellSize, BLACK);
145}
146
147void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
148    DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
149                       cellSize, cellColor);
150}
151
152void DrawIndicators(void) {
153    Cell cellTypes[] = {EMPTY, CONDUCTOR, HEAD, TAIL};
154    for (int i = 0; i < 4; i++) {
155        int x = indicatorX + indicatorSize * i;
156        DrawRectangle(x, indicatorY, indicatorSize, indicatorSize,
157                      GetCellColor(cellTypes[i]));
158        DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize, BLACK);
159        if (selectCellType == cellTypes[i]) {
160            DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize,
161                               WHITE);
162        }
163    }
164}
165
166void DrawPlayButton(void) {
167    if (isPlaying) {
168        DrawRectangle(buttonX, buttonY, barWidth, buttonSize, WHITE);
169        DrawRectangle(buttonX + barWidth + barGap, buttonY, barWidth, buttonSize,
170                      WHITE);
171    } else {
172        Vector2 v1 = (Vector2){buttonX, buttonY};
173        Vector2 v2 = (Vector2){buttonX, buttonY + buttonSize};
174        Vector2 v3 =
175            (Vector2){buttonX + buttonSize, (buttonY + buttonSize / 2.0)};
176        DrawTriangle(v1, v2, v3, WHITE);
177    }
178}
179
180void HandleUserInput(void) {
181    Vector2 mousePosition = GetMousePosition();
182    int mouseYGridPos = (int)(mousePosition.y / cellSize);
183    int mouseXGridPos = (int)(mousePosition.x / cellSize);
184
185    if (IsKeyPressed(KEY_SPACE))
186        isPlaying = !isPlaying;
187
188    if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1))
189        selectCellType = EMPTY;
190    else if (IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2))
191        selectCellType = CONDUCTOR;
192    else if (IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3))
193        selectCellType = HEAD;
194    else if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_KP_4))
195        selectCellType = TAIL;
196
197    if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
198        grid[mouseYGridPos][mouseXGridPos] = selectCellType;
199        DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
200    }
201
202    DrawCellLines(mouseXGridPos, mouseYGridPos, WHITE);
203}
204
205int main(void) {
206    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
207    SetTargetFPS(60);
208
209    InitGrid();
210
211    float elapsedTime = 0.0f;
212
213    while (!WindowShouldClose()) {
214        BeginDrawing();
215        ClearBackground(RAYWHITE);
216
217        float frameTime = GetFrameTime();
218        elapsedTime += frameTime;
219
220        if (isPlaying && elapsedTime >= refreshInterval) {
221            UpdateGrid();
222            elapsedTime = 0.0f;
223        }
224
225        for (int y = 0; y < rows; y++) {
226            for (int x = 0; x < cols; x++) {
227                Color cellColor = GetCellColor(grid[y][x]);
228                DrawCell(x, y, cellColor);
229            }
230        }
231
232        HandleUserInput();
233
234        DrawIndicators();
235
236        DrawPlayButton();
237
238        EndDrawing();
239    }
240
241    CloseWindow();
242
243    FreeGrid();
244
245    return 0;
246}

按钮点击

当鼠标左键按下时,使用 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);
    }
}

点击的实现与之前差不多,但要注意只有在暂停时(显示时)才可以点击:

 1Rectangle nextButtonRect = {nextButtonX, nextButtonY, nextButtonSize,
 2                            nextButtonSize};
 3
 4if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
 5    if (CheckCollisionPointRec(mousePosition, playButtonRect))
 6        isPlaying = !isPlaying;
 7
 8    if (CheckCollisionPointRec(mousePosition, nextButtonRect) && !isPlaying)
 9        UpdateGrid();
10
11    for (int i = 0; i < 4; i++) {
12        int x = indicatorX + indicatorSize * i;
13        Rectangle indicatorRect = {x, indicatorY, indicatorSize,
14                                   indicatorSize};
15        if (CheckCollisionPointRec(mousePosition, indicatorRect)) {
16            selectCellType = cellTypes[i];
17            break;
18        }
19    }
20}
21
22if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) &&
23    !CheckCollisionPointRec(mousePosition, playButtonRect) &&
24    !CheckCollisionPointRec(mousePosition, nextButtonRect) &&
25    !CheckCollisionPointRec(mousePosition, indicatorRect)) {
26    grid[mouseYGridPos][mouseXGridPos] = selectCellType;
27    DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
28}
当前完整代码
  1#include <stdlib.h>
  2#include "raylib.h"
  3
  4#define EMPTY_COLOR      \
  5    CLITERAL(Color) {    \
  6        59, 74, 107, 255 \
  7    }
  8#define HEAD_COLOR        \
  9    CLITERAL(Color) {     \
 10        34, 178, 218, 255 \
 11    }
 12#define TAIL_COLOR       \
 13    CLITERAL(Color) {    \
 14        242, 53, 87, 255 \
 15    }
 16#define CONDUCTOR_COLOR   \
 17    CLITERAL(Color) {     \
 18        240, 212, 58, 255 \
 19    }
 20
 21typedef enum { EMPTY, CONDUCTOR, HEAD, TAIL } Cell;
 22
 23const int screenWidth = 800;
 24const int screenHeight = 460;
 25
 26const int cellSize = 20;
 27
 28const int indicatorSize = cellSize;
 29const int indicatorPadding = cellSize / 2;
 30const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
 31const int indicatorY = indicatorPadding;
 32
 33const int playButtonX = cellSize / 2;
 34const int playButtonY = cellSize / 2;
 35const int playButtonSize = cellSize;
 36const int playButtonBarWidth = playButtonSize / 4;
 37const int playButtonBarGap = playButtonBarWidth;
 38
 39const int nextButtonX = playButtonX + playButtonSize + cellSize / 2;
 40const int nextButtonY = cellSize / 2;
 41const int nextButtonSize = cellSize;
 42const int nextButtonBarWidth = playButtonSize / 4;
 43
 44const int rows = screenHeight / cellSize;
 45const int cols = screenWidth / cellSize;
 46
 47int isPlaying = 0;
 48
 49const int refreshRate = 5;
 50const float refreshInterval = 1.0f / refreshRate;
 51
 52Cell selectCellType = EMPTY;
 53Cell cellTypes[] = {EMPTY, CONDUCTOR, HEAD, TAIL};
 54
 55Cell** grid;
 56
 57Color GetCellColor(Cell cell) {
 58    switch (cell) {
 59        case EMPTY:
 60            return EMPTY_COLOR;
 61        case HEAD:
 62            return HEAD_COLOR;
 63        case TAIL:
 64            return TAIL_COLOR;
 65        case CONDUCTOR:
 66            return CONDUCTOR_COLOR;
 67        default:
 68            return EMPTY_COLOR;
 69    }
 70}
 71
 72int CountHeadNeighbors(int row, int col) {
 73    int headCount = 0;
 74
 75    for (int y = -1; y <= 1; y++) {
 76        for (int x = -1; x <= 1; x++) {
 77            if (y == 0 && x == 0)
 78                continue;
 79            int newRow = row + y;
 80            int newCol = col + x;
 81            if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols) {
 82                if (grid[newRow][newCol] == HEAD) {
 83                    headCount++;
 84                }
 85            }
 86        }
 87    }
 88
 89    return headCount;
 90}
 91
 92void InitGrid(void) {
 93    grid = malloc(rows * sizeof(Cell*));
 94    for (int y = 0; y < rows; y++) {
 95        grid[y] = malloc(cols * sizeof(Cell));
 96    }
 97
 98    for (int y = 0; y < rows; y++) {
 99        for (int x = 0; x < cols; x++) {
100            grid[y][x] = EMPTY;
101        }
102    }
103}
104
105void FreeGrid(void) {
106    for (int i = 0; i < rows; i++) {
107        free(grid[i]);
108    }
109    free(grid);
110}
111
112void UpdateGrid(void) {
113    Cell newGrid[rows][cols];
114
115    for (int y = 0; y < rows; y++) {
116        for (int x = 0; x < cols; x++) {
117            switch (grid[y][x]) {
118                case EMPTY:
119                    newGrid[y][x] = EMPTY;
120                    break;
121                case HEAD:
122                    newGrid[y][x] = TAIL;
123                    break;
124                case TAIL:
125                    newGrid[y][x] = CONDUCTOR;
126                    break;
127                case CONDUCTOR: {
128                    int headNeighbors = CountHeadNeighbors(y, x);
129                    if (headNeighbors == 1 || headNeighbors == 2) {
130                        newGrid[y][x] = HEAD;
131                    } else {
132                        newGrid[y][x] = CONDUCTOR;
133                    }
134                } break;
135            }
136        }
137    }
138
139    for (int y = 0; y < rows; y++) {
140        for (int x = 0; x < cols; x++) {
141            grid[y][x] = newGrid[y][x];
142        }
143    }
144}
145
146void DrawCell(int xGridPos, int yGridPos, Color cellColor) {
147    DrawRectangle(xGridPos * cellSize, yGridPos * cellSize, cellSize, cellSize,
148                  cellColor);
149    DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
150                       cellSize, BLACK);
151}
152
153void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
154    DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
155                       cellSize, cellColor);
156}
157
158void DrawIndicators(void) {
159    for (int i = 0; i < 4; i++) {
160        int x = indicatorX + indicatorSize * i;
161        DrawRectangle(x, indicatorY, indicatorSize, indicatorSize,
162                      GetCellColor(cellTypes[i]));
163        DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize, BLACK);
164        if (selectCellType == cellTypes[i]) {
165            DrawRectangleLines(x, indicatorY, indicatorSize, indicatorSize,
166                               WHITE);
167        }
168    }
169}
170
171void DrawPlayButton(void) {
172    if (isPlaying) {
173        DrawRectangle(playButtonX, playButtonY, playButtonBarWidth,
174                      playButtonSize, WHITE);
175        DrawRectangle(playButtonX + playButtonBarWidth + playButtonBarGap,
176                      playButtonY, playButtonBarWidth, playButtonSize, WHITE);
177    } else {
178        Vector2 v1 = (Vector2){playButtonX, playButtonY};
179        Vector2 v2 = (Vector2){playButtonX, playButtonY + playButtonSize};
180        Vector2 v3 = (Vector2){playButtonX + playButtonSize,
181                               (playButtonY + playButtonSize / 2.0)};
182        DrawTriangle(v1, v2, v3, WHITE);
183    }
184}
185
186void DrawNextButton(void) {
187    if (!isPlaying) {
188        Vector2 v1 = (Vector2){nextButtonX, nextButtonY};
189        Vector2 v2 = (Vector2){nextButtonX, nextButtonY + nextButtonSize};
190        Vector2 v3 = (Vector2){nextButtonX + nextButtonSize,
191                               (nextButtonY + nextButtonSize / 2.0)};
192        DrawTriangle(v1, v2, v3, WHITE);
193        DrawRectangle(nextButtonX + nextButtonSize - nextButtonBarWidth,
194                      nextButtonY, nextButtonBarWidth, nextButtonSize, WHITE);
195    }
196}
197
198void HandleUserInput(void) {
199    Vector2 mousePosition = GetMousePosition();
200    int mouseYGridPos = (int)(mousePosition.y / cellSize);
201    int mouseXGridPos = (int)(mousePosition.x / cellSize);
202
203    Rectangle playButtonRect = {playButtonX, playButtonY, playButtonSize,
204                                playButtonSize};
205    Rectangle nextButtonRect = {nextButtonX, nextButtonY, nextButtonSize,
206                                nextButtonSize};
207    Rectangle indicatorRect = {indicatorX, indicatorY, indicatorSize * 4,
208                               indicatorSize};
209
210    if (IsKeyPressed(KEY_SPACE))
211        isPlaying = !isPlaying;
212
213    if (IsKeyPressed(KEY_N) && !isPlaying) {
214        UpdateGrid();
215    }
216
217    if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_KP_1))
218        selectCellType = EMPTY;
219    else if (IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_KP_2))
220        selectCellType = CONDUCTOR;
221    else if (IsKeyPressed(KEY_THREE) || IsKeyPressed(KEY_KP_3))
222        selectCellType = HEAD;
223    else if (IsKeyPressed(KEY_FOUR) || IsKeyPressed(KEY_KP_4))
224        selectCellType = TAIL;
225
226    if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
227        if (CheckCollisionPointRec(mousePosition, playButtonRect))
228            isPlaying = !isPlaying;
229
230        if (CheckCollisionPointRec(mousePosition, nextButtonRect) && !isPlaying)
231            UpdateGrid();
232
233        for (int i = 0; i < 4; i++) {
234            int x = indicatorX + indicatorSize * i;
235            Rectangle indicatorRect = {x, indicatorY, indicatorSize,
236                                       indicatorSize};
237            if (CheckCollisionPointRec(mousePosition, indicatorRect)) {
238                selectCellType = cellTypes[i];
239                break;
240            }
241        }
242    }
243
244    if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) &&
245        !CheckCollisionPointRec(mousePosition, playButtonRect) &&
246        !CheckCollisionPointRec(mousePosition, nextButtonRect) &&
247        !CheckCollisionPointRec(mousePosition, indicatorRect)) {
248        grid[mouseYGridPos][mouseXGridPos] = selectCellType;
249        DrawCell(mouseXGridPos, mouseYGridPos, GetCellColor(selectCellType));
250    }
251
252    DrawCellLines(mouseXGridPos, mouseYGridPos, WHITE);
253}
254
255int main(void) {
256    InitWindow(screenWidth, screenHeight, "Wireworld Simulator");
257    SetTargetFPS(60);
258
259    InitGrid();
260
261    float elapsedTime = 0.0f;
262
263    while (!WindowShouldClose()) {
264        BeginDrawing();
265        ClearBackground(RAYWHITE);
266
267        float frameTime = GetFrameTime();
268        elapsedTime += frameTime;
269
270        if (isPlaying && elapsedTime >= refreshInterval) {
271            UpdateGrid();
272            elapsedTime = 0.0f;
273        }
274
275        for (int y = 0; y < rows; y++) {
276            for (int x = 0; x < cols; x++) {
277                Color cellColor = GetCellColor(grid[y][x]);
278                DrawCell(x, y, cellColor);
279            }
280        }
281
282        HandleUserInput();
283
284        DrawIndicators();
285
286        DrawPlayButton();
287
288        DrawNextButton();
289
290        EndDrawing();
291    }
292
293    CloseWindow();
294
295    FreeGrid();
296
297    return 0;
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 真的很好用,找回了不少开发游戏的快乐。

下一部分

Copyright © 2024 13m0n4de · CC BY-NC 4.0