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: 
 8: const int screenWidth = 800;
 9: const int screenHeight = 450;
10: 
11: const int cellSize = 20;
12: 
13: int 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;
    }
}

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

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: #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: 
 8: const int screenWidth = 800;
 9: const int screenHeight = 460;
10: 
11: const int cellSize = 20;
12: 
13: const int rows = screenHeight / cellSize;
14: const int cols = screenWidth / cellSize;
15: 
16: typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
17: 
18: Color 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: 
33: int 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); 加入主循环,并初始化一些细胞:

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: #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: 
  9: const int screenWidth = 800;
 10: const int screenHeight = 460;
 11: 
 12: const int cellSize = 20;
 13: 
 14: const int rows = screenHeight / cellSize;
 15: const int cols = screenWidth / cellSize;
 16: 
 17: typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
 18: 
 19: Color 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: 
 34: int 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: 
 54: void 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: 
 84: int 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 时刷新网格。

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: #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: 
  9: const int screenWidth = 800;
 10: const int screenHeight = 460;
 11: 
 12: const int cellSize = 20;
 13: 
 14: const int rows = screenHeight / cellSize;
 15: const int cols = screenWidth / cellSize;
 16: 
 17: int isPlaying = 0;
 18: 
 19: const int refreshRate = 5;
 20: const float refreshInterval = 1.0f / refreshRate;
 21: 
 22: typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
 23: 
 24: Color 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: 
 39: int 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: 
 59: void 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: 
 89: int 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 绘制高亮色边框。

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: #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: 
 21: const int screenWidth = 800;
 22: const int screenHeight = 460;
 23: 
 24: const int cellSize = 20;
 25: 
 26: const int rows = screenHeight / cellSize;
 27: const int cols = screenWidth / cellSize;
 28: 
 29: int isPlaying = 0;
 30: 
 31: const int refreshRate = 5;
 32: const float refreshInterval = 1.0f / refreshRate;
 33: 
 34: typedef enum { EMPTY, HEAD, TAIL, CONDUCTOR } Cell;
 35: 
 36: Color 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: 
 51: int 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: 
 71: void 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: 
101: int 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: 
 21: typedef enum { EMPTY, CONDUCTOR, HEAD, TAIL } Cell;
 22: 
 23: const int screenWidth = 800;
 24: const int screenHeight = 460;
 25: 
 26: const int cellSize = 20;
 27: 
 28: const int indicatorSize = cellSize;
 29: const int indicatorPadding = cellSize / 2;
 30: const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
 31: const int indicatorY = indicatorPadding;
 32: 
 33: const int rows = screenHeight / cellSize;
 34: const int cols = screenWidth / cellSize;
 35: 
 36: int isPlaying = 0;
 37: 
 38: const int refreshRate = 5;
 39: const float refreshInterval = 1.0f / refreshRate;
 40: 
 41: Cell selectCellType = EMPTY;
 42: 
 43: Color 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: 
 58: int 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: 
 78: void 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: 
108: void 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: 
115: void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
116:     DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
117:                        cellSize, cellColor);
118: }
119: 
120: int 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 内存布局不一致:

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;
}
当前完整代码
  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: 
 21: typedef enum { EMPTY, CONDUCTOR, HEAD, TAIL } Cell;
 22: 
 23: const int screenWidth = 800;
 24: const int screenHeight = 460;
 25: 
 26: const int cellSize = 20;
 27: 
 28: const int indicatorSize = cellSize;
 29: const int indicatorPadding = cellSize / 2;
 30: const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
 31: const int indicatorY = indicatorPadding;
 32: 
 33: const int buttonX = cellSize / 2;
 34: const int buttonY = cellSize / 2;
 35: const int buttonSize = cellSize;
 36: const int barWidth = buttonSize / 4;
 37: const int barGap = barWidth;
 38: 
 39: const int rows = screenHeight / cellSize;
 40: const int cols = screenWidth / cellSize;
 41: 
 42: int isPlaying = 0;
 43: 
 44: const int refreshRate = 5;
 45: const float refreshInterval = 1.0f / refreshRate;
 46: 
 47: Cell selectCellType = EMPTY;
 48: 
 49: Cell** grid;
 50: 
 51: Color 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: 
 66: int 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: 
 86: void 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: 
 99: void FreeGrid(void) {
100:     for (int i = 0; i < rows; i++) {
101:         free(grid[i]);
102:     }
103:     free(grid);
104: }
105: 
106: void 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: 
140: void 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: 
147: void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
148:     DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
149:                        cellSize, cellColor);
150: }
151: 
152: void 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: 
166: void 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: 
180: void 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: 
205: int 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);
    }
}

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

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: #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: 
 21: typedef enum { EMPTY, CONDUCTOR, HEAD, TAIL } Cell;
 22: 
 23: const int screenWidth = 800;
 24: const int screenHeight = 460;
 25: 
 26: const int cellSize = 20;
 27: 
 28: const int indicatorSize = cellSize;
 29: const int indicatorPadding = cellSize / 2;
 30: const int indicatorX = screenWidth - indicatorSize * 4 - indicatorPadding;
 31: const int indicatorY = indicatorPadding;
 32: 
 33: const int playButtonX = cellSize / 2;
 34: const int playButtonY = cellSize / 2;
 35: const int playButtonSize = cellSize;
 36: const int playButtonBarWidth = playButtonSize / 4;
 37: const int playButtonBarGap = playButtonBarWidth;
 38: 
 39: const int nextButtonX = playButtonX + playButtonSize + cellSize / 2;
 40: const int nextButtonY = cellSize / 2;
 41: const int nextButtonSize = cellSize;
 42: const int nextButtonBarWidth = playButtonSize / 4;
 43: 
 44: const int rows = screenHeight / cellSize;
 45: const int cols = screenWidth / cellSize;
 46: 
 47: int isPlaying = 0;
 48: 
 49: const int refreshRate = 5;
 50: const float refreshInterval = 1.0f / refreshRate;
 51: 
 52: Cell selectCellType = EMPTY;
 53: Cell cellTypes[] = {EMPTY, CONDUCTOR, HEAD, TAIL};
 54: 
 55: Cell** grid;
 56: 
 57: Color 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: 
 72: int 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: 
 92: void 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: 
105: void FreeGrid(void) {
106:     for (int i = 0; i < rows; i++) {
107:         free(grid[i]);
108:     }
109:     free(grid);
110: }
111: 
112: void 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: 
146: void 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: 
153: void DrawCellLines(int xGridPos, int yGridPos, Color cellColor) {
154:     DrawRectangleLines(xGridPos * cellSize, yGridPos * cellSize, cellSize,
155:                        cellSize, cellColor);
156: }
157: 
158: void 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: 
171: void 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: 
186: void 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: 
198: void 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: 
255: int 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