一个简易的游戏引擎,适合刚学了一点语法的小白。

项目地址:https://git.nju.edu.cn/jyy/toybox

源码阅读

阅读过程有 AI 协助。

toybox.h

下方代码展开约 280+ 行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
// toybox.h
/*
* _____ _
* |_ _|___ _ _| |_ ___ _ _
* | | | . | | | . | . |_'_|
* |_| |___|_ |___|___|_,_|
* |___|
*
* C/C++ 初学者的第一个游戏 & 动画引擎
*
* MIT License
*
* Copyright (c) 2024 by Yanyan Jiang and Zesen Liu
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* toybox 只提供一个函数 void toybox_run(fps, update, keypress)
* toybox_run 接收三个参数,然后进入死循环:
*
* - 1. 整数 fps:
* 每秒刷新的次数 (每秒执行 fps 次 update)
*
* - 2. 函数 update:
* void updpate(int w, int h, draw_function draw);
* 每当时间到时,update 会被调用,其中可以调用 draw(x, y, ch);
* 在坐标 (x, y) 绘制一个字符 ch。坐标系统:
*
* (0,0) ---- x ---->
* | |
* | |
* | |
* y ------ (x,y) = ch // draw(x, y, ch)
* |
* v
*
* - 3. 函数 keypress:
* void keypress(int key);
* 每当收到按键时,keypress 会被调用,key 是按键的 ASCII 码
*/

/* -= Toybox API =------------------------------------- */
typedef void (*draw_function)(int x, int y, char ch);

static void
toybox_run(int fps,
void (*update)(int, int, draw_function draw),
void (*keypress)(int));
/* ---------------------------------------------------- */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define MAX_W_ 128
#define MAX_H_ 64

#define append_(buf, str) \
do { \
strcpy(buf, str); \
buf += strlen(str); \
} while (0)

static uint64_t start_time_;
static int w_, h_;
static char canvas_[MAX_W_ * MAX_H_];
static int waitkey_(void);
static void get_window_size_(int *w, int *h);

#ifdef _WIN32
#include <windows.h>
#include <conio.h>

static int waitkey_(void) {
int startTime = GetTickCount();
while (GetTickCount() - startTime < 10) {
if (_kbhit()) {
return _getch();
}
}
return -1;
}

static void get_window_size_(int *w, int *h) {
CONSOLE_SCREEN_BUFFER_INFO csbi;
if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
*w = csbi.srWindow.Right - csbi.srWindow.Left;
*h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
} else {
*w = 80;
*h = 25;
}
}

// copied from https://github.com/confluentinc/librdkafka
static int gettimeofday(struct timeval * tp, struct timezone * tzp)
{
// Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's
// This magic number is the number of 100 nanosecond intervals since January 1, 1601 (UTC)
// until 00:00:00 January 1, 1970
static const uint64_t EPOCH = ((uint64_t) 116444736000000000ULL);

SYSTEMTIME system_time;
FILETIME file_time;
uint64_t time;

GetSystemTime( &system_time );
SystemTimeToFileTime( &system_time, &file_time );
time = ((uint64_t)file_time.dwLowDateTime ) ;
time += ((uint64_t)file_time.dwHighDateTime) << 32;

tp->tv_sec = (long) ((time - EPOCH) / 10000000L);
tp->tv_usec = (long) (system_time.wMilliseconds * 1000);
return 0;
}

static void clear_screen_() {
COORD topLeft = { 0, 0 };
HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO screen;
DWORD written;

GetConsoleScreenBufferInfo(console, &screen);
FillConsoleOutputCharacterA(
console, ' ', screen.dwSize.X * screen.dwSize.Y, topLeft, &written
);
FillConsoleOutputAttribute(
console, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE,
screen.dwSize.X * screen.dwSize.Y, topLeft, &written
);
SetConsoleCursorPosition(console, topLeft);
}

#else
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/ioctl.h>

static int waitkey_(void) {
struct timeval timeout;
fd_set readfds;
int retval;

timeout.tv_sec = 0;
timeout.tv_usec = 10000;

FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);

retval = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
if (retval == -1) {
exit(1);
} if (retval) {
char ch;
read(STDIN_FILENO, &ch, 1);
return ch;
} else {
return -1;
}
}

struct termios old_;

static void __attribute__((constructor))
termios_init_(void) {
struct winsize win;
struct termios cur;

if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &win) == -1) {
printf("Not a terminal window.\n");
exit(1);
}

tcgetattr(STDIN_FILENO, &old_);

cur = old_;
cur.c_lflag &= ~(ICANON | ECHO);
cur.c_cc[VMIN] = 0;
cur.c_cc[VTIME] = 1;

tcsetattr(STDIN_FILENO, TCSANOW, &cur);
}

static void __attribute__((destructor))
termios_restore_(void) {
tcsetattr(STDIN_FILENO, TCSANOW, &old_);
}

static void get_window_size_(int *w, int *h) {
struct winsize win;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &win);

*w = win.ws_col < MAX_W_ ? win.ws_col : MAX_W_;
*h = win.ws_row < MAX_H_ ? win.ws_row : MAX_H_;
}

static void clear_screen_() {
printf("\033[H");
}

#endif

uint64_t timer_ms_(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return (tv.tv_sec * 1000LL) + (tv.tv_usec / 1000);
}

static void __attribute__((constructor))
init_timer_(void) {
start_time_ = timer_ms_();
}

void draw_(int x, int y, char ch) {
if (0 <= x && x < w_ && 0 <= y && y < h_) {
canvas_[y * w_ + x] = ch;
}
}

static void
toybox_run(int fps,
void (*update)(int, int, draw_function draw),
void (*keypress)(int)) {
uint64_t last_time = 0;
int i, last_size = -1;
char buffer[MAX_W_ * MAX_H_ + MAX_H_ * 2 + 4096], *head;

while (1) {
int key = waitkey_();
if (key > 0) {
if (keypress) {
keypress(key);
}
continue;
} else {
uint64_t t = timer_ms_() - start_time_;
if (t - last_time <= 1000 / fps) {
continue;
}
last_time = t;
}

get_window_size_(&w_, &h_);
memset(canvas_, ' ', sizeof(canvas_));
update(w_, h_, draw_);

head = buffer;
clear_screen_();

if ((w_ << 16) + h_ != last_size) {
last_size = (w_ << 16) + h_;
append_(head, "\033[2J");
}

for (i = 0; i < h_; i++) {
if (i != 0) {
append_(head, "\r\n");
}
strncpy(head, &canvas_[i * w_], w_);
head += w_;
}

fwrite(buffer, head - buffer, 1, stdout);
fflush(stdout);
}
}

这是一个简单的游戏和动画引擎,称为 “toybox”。它提供了一个函数 toybox_run,该函数接受三个参数:

  1. 整数 fps:表示每秒刷新的次数,也就是每秒调用 update 函数的次数。
  2. 函数指针 update:一个函数,定义为 void update(int w, int h, draw_function draw),表示每次刷新时被调用的更新函数。它可以接受当前窗口的宽度和高度,并使用 draw 函数在屏幕上绘制图形。
  3. 函数指针 keypress:一个函数,定义为 void keypress(int key),表示当按下键盘按键时被调用的函数。

在主循环中,程序会不断等待键盘输入或者根据设定的帧率调用 update 函数进行屏幕更新,然后根据更新后的画面重新绘制屏幕内容。

接下来我们从头到尾看一看里面的细节。

1
2
3
4
5
#define append_(buf, str) \
do { \
strcpy(buf, str); \
buf += strlen(str); \
} while (0)

这段代码定义了一个宏 append_,用于将字符串追加到指定的缓冲区中。

  1. #define append_(buf, str):这是宏的定义,append_ 是宏的名称,(buf, str) 是宏的参数列表,这里有两个参数,buf 表示目标缓冲区,str 表示要追加的字符串。
  2. do { ... } while (0): 这是一个 do-while 循环,它的主体是一系列语句,其中包括了复制字符串和移动指针的操作。do { ... } 表示循环体,while (0) 则是一个条件,由于条件为 0,因此循环只会执行一次。
  3. strcpy(buf, str):这一行使用 strcpy 函数将字符串 str 复制到缓冲区 buf 中。
  4. buf += strlen(str):这一行将指针 buf 向后移动,移动的距离是字符串 str 的长度,这样可以保证下一次追加的字符串会接在当前字符串的末尾。

这个宏的作用是将字符串追加到缓冲区中,类似于字符串拼接操作。在每次调用 append_ 宏时,它会将指定的字符串添加到目标缓冲区的末尾,并更新指针以指向新的末尾位置。

1
static char canvas_[MAX_W_ * MAX_H_];

canvas_ 数组是用来表示绘图区域的缓冲区。在这个简单的游戏和动画引擎中,屏幕上的图像是通过在这个缓冲区中绘制字符来实现的。每个字符对应着屏幕上的一个像素或一个小图形。

在每次调用 update 函数时,会根据游戏逻辑更新 canvas_ 数组中的内容,然后将更新后的内容绘制到屏幕上。因此,canvas_ 数组存储了当前屏幕上的图像信息,通过更新这个数组,可以实现屏幕内容的动态变化。

1
static int waitkey_(void);

虽然在这段代码中,函数的声明和定义紧密相邻,看起来似乎有些多余,但这是一个良好的编程实践,可以帮助提高代码的可维护性和可读性。让读者快速了解函数的接口,包括返回类型和参数列表,而不必深入到函数的定义中去查找这些信息。

1
#ifdef _WIN32

在这段代码中,它的作用是根据当前编译环境是否是 Windows 平台来进行条件编译。

这个技术常用于实现跨平台的编译,在不同的平台上使用不同的代码逻辑。

1
#include <windows.h>

该头文件主要用于 Windows 平台下的一些系统调用和操作。

在这个代码中,<windows.h> 被用来进行以下操作:

  1. 获取系统时间和延时操作:通过 GetTickCount() 函数可以获取系统启动后经过的毫秒数,用于实现定时器功能。另外,该头文件还定义了与时间相关的数据类型和函数,例如 SYSTEMTIME 结构体和 GetSystemTime() 函数。
  2. 控制台操作:例如 GetConsoleScreenBufferInfo() 函数用于获取控制台屏幕缓冲区信息,SetConsoleCursorPosition() 函数用于设置控制台光标位置,以及一些用于控制控制台文本属性和颜色的宏定义。
  3. 键盘输入操作<conio.h> 头文件通常与 <windows.h> 一起使用,用于实现控制台下的键盘输入操作。在这个代码中,<conio.h> 用于定义 _kbhit()_getch() 函数,用于检测是否有键盘输入和获取键盘输入字符。
1
2
3
4
5
6
7
8
9
static int waitkey_(void) {
int startTime = GetTickCount();
while (GetTickCount() - startTime < 10) {
if (_kbhit()) {
return _getch();
}
}
return -1;
}

在 10 毫秒内轮询检查是否有键盘输入,若有则返回该输入,否则返回 -1.

1
2
3
4
5
6
7
8
9
10
static void get_window_size_(int *w, int *h) {
CONSOLE_SCREEN_BUFFER_INFO csbi;
if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
*w = csbi.srWindow.Right - csbi.srWindow.Left;
*h = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
} else {
*w = 80;
*h = 25;
}
}

函数首先声明了一个 CONSOLE_SCREEN_BUFFER_INFO 结构体变量 csbi,用于存储获取到的控制台屏幕缓冲区信息。然后调用 GetConsoleScreenBufferInfo 函数,将获取到的信息存储在 csbi 变量中。

接着,函数通过计算 csbi 中的 srWindow 结构体中的 RightLeftBottomTop 字段来计算控制台窗口的宽度和高度。具体地,控制台窗口的宽度等于 Right - Left,高度等于 Bottom - Top + 1。然后将计算得到的宽度和高度分别存储在传入的指针参数 wh 所指向的位置。

如果调用 GetConsoleScreenBufferInfo 函数失败(可能是因为当前程序并非在控制台中运行),则函数将宽度和高度分别设为默认值 80 和 25。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// copied from https://github.com/confluentinc/librdkafka
static int gettimeofday(struct timeval * tp, struct timezone * tzp)
{
// Note: some broken versions only have 8 trailing zero's, the correct epoch has 9 trailing zero's
// This magic number is the number of 100 nanosecond intervals since January 1, 1601 (UTC)
// until 00:00:00 January 1, 1970
static const uint64_t EPOCH = ((uint64_t) 116444736000000000ULL);

SYSTEMTIME system_time;
FILETIME file_time;
uint64_t time;

GetSystemTime( &system_time );
SystemTimeToFileTime( &system_time, &file_time );
time = ((uint64_t)file_time.dwLowDateTime ) ;
time += ((uint64_t)file_time.dwHighDateTime) << 32;

tp->tv_sec = (long) ((time - EPOCH) / 10000000L);
tp->tv_usec = (long) (system_time.wMilliseconds * 1000);
return 0;
}

这段代码看个大概就行。

这个函数名为 gettimeofday它的功能是获取当前系统时间,并将其以秒和微秒的形式存储在 struct timeval 结构体指针 tp 中。 这个函数类似于 Unix/Linux 系统中的 gettimeofday 函数,但是实现方式有所不同。

具体来说,这个函数的步骤如下:

  1. 定义一个静态常量 EPOCH,用于表示从 1601 年 1 月 1 日 UTC 时间零点开始到 1970 年 1 月 1 日 UTC 时间零点之间的时间间隔,以 100 毫微秒(100纳秒)为单位。
  2. 调用 Windows 平台特有的 GetSystemTime 函数,获取当前系统时间,并将结果存储在 SYSTEMTIME 结构体变量 system_time 中。
  3. 调用 Windows 平台特有的 SystemTimeToFileTime 函数,将 system_time 转换为 FILETIME 结构体变量 file_time,表示自 1601 年 1 月 1 日以来的时间。
  4. file_time 中的时间转换为以 100 毫微秒为单位的整数,存储在 time 变量中。
  5. 根据 time 变量和 EPOCH 值的差值,计算出秒数并存储在 tv_sec 成员中,计算出微秒数并存储在 tv_usec 成员中。
  6. 返回 0,表示函数执行成功。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void clear_screen_() {
COORD topLeft = { 0, 0 };
HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO screen;
DWORD written;

GetConsoleScreenBufferInfo(console, &screen);
FillConsoleOutputCharacterA(
console, ' ', screen.dwSize.X * screen.dwSize.Y, topLeft, &written
);
FillConsoleOutputAttribute(
console, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE,
screen.dwSize.X * screen.dwSize.Y, topLeft, &written
);
SetConsoleCursorPosition(console, topLeft);
}

这个函数的功能是清空控制台屏幕上的所有内容,并将光标移动到左上角位置。具体来说:

  1. 创建一个 COORD 结构体变量 topLeft,表示控制台屏幕的左上角位置。
  2. 获取标准输出控制台的句柄,并将其存储在 HANDLE 类型的变量 console 中,使用 GetStdHandle(STD_OUTPUT_HANDLE) 函数实现。
  3. 声明一个 CONSOLE_SCREEN_BUFFER_INFO 结构体变量 screen,用于存储控制台屏幕缓冲区的信息。
  4. 调用 GetConsoleScreenBufferInfo 函数,获取控制台屏幕缓冲区的信息,并将结果存储在 screen 变量中。
  5. 调用 FillConsoleOutputCharacterA 函数,将控制台屏幕上所有位置的字符都填充为空格字符,使用空格字符 ' '
  6. 调用 FillConsoleOutputAttribute 函数,将控制台屏幕上所有位置的文本属性都填充为前景色为白色(红、绿、蓝三种颜色混合)。
  7. 最后,使用 SetConsoleCursorPosition 函数将控制台光标移动到左上角位置,以确保下次输出从屏幕的左上角开始。
1
2
3
4
5
uint64_t timer_ms_(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return (tv.tv_sec * 1000LL) + (tv.tv_usec / 1000);
}

这个函数的功能是获取当前系统时间,并以毫秒为单位返回。

1
2
3
4
static void __attribute__((constructor))
init_timer_(void) {
start_time_ = timer_ms_();
}

这个函数使用 __attribute__((constructor)) 属性,表示它会在程序运行时自动执行,并在其他代码之前被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static void
toybox_run(int fps,
void (*update)(int, int, draw_function draw),
void (*keypress)(int)) {
uint64_t last_time = 0;
int i, last_size = -1; // 上一次窗口大小
char buffer[MAX_W_ * MAX_H_ + MAX_H_ * 2 + 4096], *head;

while (1) {
int key = waitkey_();
if (key > 0) {
if (keypress) { // 检查函数指针是否有效
keypress(key);
}
continue; // “懒绘制”
} else {
uint64_t t = timer_ms_() - start_time_;
if (t - last_time <= 1000 / fps) {
continue; // 继续等待
}
last_time = t; // 吉时已到,刷新
}

// 更新游戏状态

get_window_size_(&w_, &h_); // 更新窗口大小
memset(canvas_, ' ', sizeof(canvas_));
update(w_, h_, draw_); // 绘制画面到 canvas 数组

head = buffer;
clear_screen_();

if ((w_ << 16) + h_ != last_size) {
last_size = (w_ << 16) + h_;
append_(head, "\033[2J");
}

for (i = 0; i < h_; i++) {
if (i != 0) {
append_(head, "\r\n"); // 换行
}
strncpy(head, &canvas_[i * w_], w_); // 拷贝一行
head += w_; // 移动指针到下一行
}

fwrite(buffer, head - buffer, 1, stdout); // 数据写入标准输出流
fflush(stdout);
}
}

其中,对于代码:

1
2
3
4
if ((w_ << 16) + h_ != last_size) {
last_size = (w_ << 16) + h_;
append_(head, "\033[2J");
}

这段代码的作用是在每次循环中检查当前窗口大小是否发生了变化,如果发生了变化,则清空屏幕,并将新的窗口大小记录下来,以便下次比较。

  1. (w_ << 16) + h_:这一部分将当前窗口的宽度 w_ 左移 16 位(相当于乘以 65536),然后加上窗口的高度 h_。这个操作可以将窗口的宽度和高度合并成一个整数,用于唯一标识窗口的大小。
  2. append_(head, "\033[2J");:将清空屏幕的控制字符序列 "\033[2J" 追加到 head 中。

hello.cpp

该代码在整个小黑框内打印字符,按下按键后,小黑框内打印输入的字符。

效果:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// hello.cpp
#include "toybox.h"

int k = '?', t = 0;

void update(int w, int h, draw_function draw) {
for (int x = 0; x < w; x++)
for (int y = 0; y < h; y++)
draw(x, y, k);
draw(0, 0, "-\\|/"[(t++) / 5 % 4]);
}

void keypress(int ch) {
k = ch;
}

int main() {
toybox_run(30, update, keypress);
}

可以看出,update() 和 keypress() 都是需要自己实现的。

值得一瞧的是:

1
draw(0, 0, "-\\|/"[(t++) / 5 % 4]);

这玩意实现了一个小动画。

使用方法

请参考 toybox.h 头部的注释和 hello.cpp 的例子。

C/C++ 都可以从以下模板开始,只需实现 “TODO” 中更新屏幕和响应按键逻辑 (可以不提供响应按键的 keypress) 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "toybox.h"

// toybox_run(fps, update, keypress)
// - 进入游戏/动画主循环
// - 每秒 fps 次调用 update(w, h, draw)
// - 当任何时候有按键时,调用 keypress(key)

void update(int w, int h, draw_function draw) {
// 当前屏幕大小为 w x h (此时屏幕为空)
// 可以使用 draw(x, y, ch) 可以在第 x 列第 y 行绘制字符 h

// TODO
}

void keypress(int key) {
// 获得一个按键,例如 W, A, S, D

// TODO
}

int main() {
toybox_run(20, update, keypress);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "toybox.h"

int main() {
toybox_run(1, [](int w, int h, auto draw) {
static int t = 0;
t++;
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
draw(x, y, '0' + t % 10);
}
}
}, nullptr);
}

例子

snake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// snake.cpp
// Author: GPT-4-turbo

#include <vector>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#include <string>
#include "../toybox.h"
using namespace std;

// 定义蛇的方向
enum Direction { UP, DOWN, LEFT, RIGHT };

// 蛇的初始方向
Direction dir = RIGHT;

// 蛇的身体,用一系列的 x,y 坐标表示
std::vector<std::pair<int, int>> snake = {{5, 5}, {5, 4}, {5, 3}};

// 食物的位置
std::pair<int, int> food = {7, 7};

// 游戏是否结束
bool gameOver = false;

// 生成食物
void generateFood(int w, int h) {
srand(time(0));
food.first = rand() % w;
food.second = rand() % h;
}

void update();

// 渲染游戏
void render(int w, int h, void(*draw)(int, int, char)) {
update();
if (gameOver) {
return;
}

// 清屏
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
draw(x, y, ' ');
}
}

// 绘制蛇
for (auto &part : snake) {
draw(part.first, part.second, '*');
}

// 绘制食物
draw(food.first, food.second, '#');
}

// 处理按键
void keypress(int key) {
switch (key) {
case 'w': dir = UP; break;
case 's': dir = DOWN; break;
case 'a': dir = LEFT; break;
case 'd': dir = RIGHT; break;
}
}

// 更新游戏状态
void update() {
if (gameOver) {
return;
}

// 计算蛇头的新位置
std::pair<int, int> head = snake.front();
switch (dir) {
case UP: head.second--; break;
case DOWN: head.second++; break;
case LEFT: head.first--; break;
case RIGHT: head.first++; break;
}

// 检查蛇是否撞墙或撞到自己
if (head.first < 0 || head.second < 0 || head.first >= 80 || head.second >= 25 || std::find(snake.begin(), snake.end(), head) != snake.end()) {
gameOver = true;
return;
}

// 将新头部添加到蛇的身体中
snake.insert(snake.begin(), head);

// 检查是否吃到食物
if (head == food) {
generateFood(80, 25); // 假设屏幕大小为 80x25
} else {
// 移除蛇
snake.pop_back();
}
}

// 主函数
int main() {
toybox_run(10, render, keypress); // 假设 toybox_run 函数接受一个更新游戏状态的函数作为参数
}

tetris

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// tetris.cpp
// Author: Claude-3-Opus
// With a few small bug fixes.

#include "../toybox.h"
#include <cstdlib>
#include <ctime>

const int BOARD_WIDTH = 10;
const int BOARD_HEIGHT = 20;
const int BLOCK_SIZE = 4;

int board[BOARD_HEIGHT][BOARD_WIDTH] = {0};
int block[BLOCK_SIZE][BLOCK_SIZE] = {0};
int blockX, blockY;

void generateBlock() {
blockX = BOARD_WIDTH / 2 - BLOCK_SIZE / 2;
blockY = 0;

// Claude-3 made a mistake here: it forgot to clear the block.
for (int i = 0; i < BLOCK_SIZE; i++)
for (int j = 0; j < BLOCK_SIZE; j++)
block[i][j] = 0;

int blockType = rand() % 7;
switch (blockType) {
case 0: // I
block[1][0] = block[1][1] = block[1][2] = block[1][3] = 1;
break;
case 1: // J
block[0][1] = block[1][1] = block[2][0] = block[2][1] = 1;
break;
case 2: // L
block[0][0] = block[1][0] = block[2][0] = block[2][1] = 1;
break;
case 3: // O
block[0][0] = block[0][1] = block[1][0] = block[1][1] = 1;
break;
case 4: // S
block[1][0] = block[1][1] = block[0][1] = block[0][2] = 1;
break;
case 5: // T
block[0][0] = block[0][1] = block[0][2] = block[1][1] = 1;
break;
case 6: // Z
block[0][0] = block[0][1] = block[1][1] = block[1][2] = 1;
break;
}
}

bool isValid(int x, int y, int block[BLOCK_SIZE][BLOCK_SIZE]) {
for (int i = 0; i < BLOCK_SIZE; i++) {
for (int j = 0; j < BLOCK_SIZE; j++) {
if (block[i][j]) {
int newX = x + j;
int newY = y + i;
if (newX < 0 || newX >= BOARD_WIDTH || newY < 0 || newY >= BOARD_HEIGHT || board[newY][newX]) {
return false;
}
}
}
}
return true;
}

void rotateBlock() {
int temp[BLOCK_SIZE][BLOCK_SIZE] = {0};
for (int i = 0; i < BLOCK_SIZE; i++) {
for (int j = 0; j < BLOCK_SIZE; j++) {
temp[i][j] = block[BLOCK_SIZE - 1 - j][i];
}
}

// Claude-3 made a mistake here (now fixed):
// it wrote isValid(int x, int y) that tests the validity for the
// global block. temp is created but is never tested.
if (isValid(blockX, blockY, temp)) {
for (int i = 0; i < BLOCK_SIZE; i++) {
for (int j = 0; j < BLOCK_SIZE; j++) {
block[i][j] = temp[i][j];
}
}
}
}

void mergeBlock() {
for (int i = 0; i < BLOCK_SIZE; i++) {
for (int j = 0; j < BLOCK_SIZE; j++) {
if (block[i][j]) {
board[blockY + i][blockX + j] = 1;
}
}
}
}

void clearLines() {
for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {
bool isFull = true;
for (int j = 0; j < BOARD_WIDTH; j++) {
if (!board[i][j]) {
isFull = false;
break;
}
}

if (isFull) {
for (int k = i; k > 0; k--) {
for (int j = 0; j < BOARD_WIDTH; j++) {
board[k][j] = board[k - 1][j];
}
}
i++;
}
}
}

void drawBoard(draw_function draw) {
for (int i = 0; i < BOARD_HEIGHT; i++) {
for (int j = 0; j < BOARD_WIDTH; j++) {
if (board[i][j]) {
draw(j, i, '#');
} else {
draw(j, i, '.');
}
}
}

for (int i = 0; i < BLOCK_SIZE; i++) {
for (int j = 0; j < BLOCK_SIZE; j++) {
if (block[i][j]) {
draw(blockX + j, blockY + i, '@');
}
}
}
}

void update(int w, int h, draw_function draw) {
if (!isValid(blockX, blockY + 1, block)) {
mergeBlock();
clearLines();
generateBlock();

if (!isValid(blockX, blockY, block)) {
// Game Over
draw(3, 10, 'G');
draw(4, 10, 'A');
draw(5, 10, 'M');
draw(6, 10, 'E');
draw(8, 10, 'O');
draw(9, 10, 'V');
draw(10, 10, 'E');
draw(11, 10, 'R');
return;
}
} else {
blockY++;
}

drawBoard(draw);
}

void keypress(int key) {
switch (key) {
case 'a':
if (isValid(blockX - 1, blockY, block)) {
blockX--;
}
break;
case 'd':
if (isValid(blockX + 1, blockY, block)) {
blockX++;
}
break;
case 'w':
rotateBlock();
break;
case 's':
if (isValid(blockX, blockY + 1, block)) {
blockY++;
}
break;
}
}

int main() {
srand(time(0));
generateBlock();
toybox_run(3, update, keypress);
}

rasterize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// rasterize.cpp
// Author: GPT-4-turbo
// 有轻微手工修改

#include "../toybox.h"
#include <cmath> // For std::abs and std::round

struct Point {
int x, y;
Point(int x, int y): x(x), y(y) {}
};

Point p0(0, 0), p1(0, 0), p2(0, 0);

void drawLine(Point p0, Point p1, draw_function draw) {
int dx = std::abs(p1.x - p0.x), sx = p0.x < p1.x ? 1 : -1;
int dy = -std::abs(p1.y - p0.y), sy = p0.y < p1.y ? 1 : -1;
int err = dx + dy, e2; /* error value e_xy */

while (true) {
draw(p0.x, p0.y, '*'); // 使用 '*' 绘制线段
if (p0.x == p1.x && p0.y == p1.y) break;
e2 = 2 * err;
if (e2 >= dy) { err += dy; p0.x += sx; }
if (e2 <= dx) { err += dx; p0.y += sy; }
}
}

void update(int w, int h, draw_function draw) {
drawLine(p0, p1, draw);
drawLine(p1, p2, draw);
drawLine(p0, p2, draw);
}

void keypress(int key) {
switch (key) {
case 'w': p0.y -= 1; break;
case 's': p0.y += 1; break;
case 'a': p0.x -= 1; break;
case 'd': p0.x += 1; break;

case 'W': p1.y -= 1; break;
case 'S': p1.y += 1; break;
case 'A': p1.x -= 1; break;
case 'D': p1.x += 1; break;
}
}

int main() {
toybox_run(20, update, keypress);
}

demo:

飞机大战

自己写了一个,整体思路不是很难。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#include "../toybox.h"
#include <set>
#include <utility>
#include <random>
#include <string>

enum Direction { STILL, UP, DOWN, LEFT, RIGHT };
Direction dir = STILL;
bool fruit_mode, K_mode, flag = true;
const int MYFPS = 26;
int score, enemy_num, fpscnt, FPSCNT = 2*MYFPS;
std::random_device seed; // 硬件生成随机数种子
std::ranlux48 engine(seed()); // 利用种子生成随机数引擎

std::set<std::pair<int,int>> Kmode_bullet;
std::set<std::pair<int,int>> Kmode_bullet2; // 分裂弹
std::set<std::pair<int,int>> normal_bullet;
std::set<std::pair<int,int>> targets; // 敌人坐标

void keypress(int key) {
switch (key) {
case 'w': dir = UP; break;
case 's': dir = DOWN; break;
case 'a': dir = LEFT; break;
case 'd': dir = RIGHT; break;
case 'k': {
fruit_mode = false;
K_mode = !K_mode;
break;
}
case 'f': {
K_mode = false;
fruit_mode = !fruit_mode;
break;
}
}
}

int fly_x, fly_y;
// --x--------
// | /\
// y / \
// | ----
// |
void drawBody(int w, int h, draw_function draw) {
switch (dir) {
case STILL: break;
case UP:
if(fly_y-3 >= 0) fly_y--;
break;
case DOWN:
if(fly_y+1 < h) fly_y++;
break;
case LEFT:
if(fly_x-1 >= 0) fly_x--;
break;
case RIGHT:
if(fly_x+4 < w) fly_x++;
break;
}
dir = STILL;

draw(fly_x+1, fly_y-2, '/');
draw(fly_x+2, fly_y-2, '\\');
draw(fly_x, fly_y-1, '/');
draw(fly_x+3, fly_y-1, '\\');
for(int i = 0; i < 4; i++)
draw(fly_x+i, fly_y, '-');
}

int get_rand_num(int minr, int maxr)
{
std::uniform_int_distribution<> distrib(minr, maxr);
int my_random = distrib(engine); // 随机数
return my_random;
}

void updBullet(int w, int h, draw_function draw) {
auto it = normal_bullet.begin();
for(; it != normal_bullet.end();) {
int nx = it->first, ny = it->second-2;
it = normal_bullet.erase(it);
if(ny < 0) continue;
normal_bullet.insert({nx, ny});
}

auto it2 = Kmode_bullet.begin();
for(; it2 != Kmode_bullet.end();) {
int nx = it2->first, ny = it2->second-2;

if(ny >= 0) {
if(nx+1 <= w)
Kmode_bullet2.insert({nx+1, ny});
if(nx-1 >= 0)
Kmode_bullet2.insert({nx-1, ny});
}

it2 = Kmode_bullet.erase(it2);
if(ny < 0) continue;
Kmode_bullet.insert({nx, ny});
}

if(!K_mode) {
normal_bullet.insert({fly_x+1, fly_y-3});
normal_bullet.insert({fly_x+2, fly_y-3});
if(fruit_mode) {
normal_bullet.insert({fly_x, fly_y-3});
normal_bullet.insert({fly_x+3, fly_y-3});
}
} else {
Kmode_bullet.insert({fly_x+1, fly_y-3});
Kmode_bullet.insert({fly_x+2, fly_y-3});
}

Kmode_bullet.insert(Kmode_bullet2.begin(), Kmode_bullet2.end());
Kmode_bullet2.clear();
}

void collision_detection() {
// normal_bullet 不会与 Kmode_bullet 碰撞
for(auto it = targets.begin(); it != targets.end();) {
bool hit = false;
auto b = normal_bullet.begin();
for(; b != normal_bullet.end(); ++b) {
if(*it == *b) {
hit = true;
normal_bullet.erase(b);
break;
}
}
auto kb = Kmode_bullet.begin();
for(; kb != Kmode_bullet.end(); ++kb) {
if(*it == *kb) {
hit = true;
Kmode_bullet.erase(kb);
break;
}
}
if(hit) {
it = targets.erase(it);
++score;
}
else ++it;
}
}

void updEnemy(int w, int h, draw_function draw) {
auto it = targets.begin();
for(; it != targets.end();) {
int nx = it->first, ny = it->second+1;
it = targets.erase(it);
if(ny >= h) continue;
targets.insert({nx, ny});
}

collision_detection();

if(fpscnt > FPSCNT) {
fpscnt = 0;
int target_num = get_rand_num(1, static_cast<int>(w/4.0));
for(int i=1; i<=target_num; ++i) {
int nx = get_rand_num(1, w);
int ny = get_rand_num(-8, 1); // 扰动
targets.insert({nx, ny});
}
FPSCNT = get_rand_num(1, static_cast<int>(MYFPS/1.0));
}
}

void drawPic(int w, int h, draw_function draw) {
auto it = normal_bullet.begin();
for(; it != normal_bullet.end(); it++) {
draw(it->first, it->second, '^');
}

auto it2 = Kmode_bullet.begin();
for(; it2 != Kmode_bullet.end(); it2++) {
draw(it2->first, it2->second, '~');
}

auto it3 = targets.begin();
for(; it3 != targets.end(); it3++) {
draw(it3->first, it3->second, '@');
}
}

void drawScore(int w, int h, draw_function draw) {
draw(1, h-1, 's');
draw(2, h-1, 'c');
draw(3, h-1, 'o');
draw(4, h-1, 'r');
draw(5, h-1, 'e');
draw(6, h-1, ':');
draw(7, h-1, ' ');
std::string score_as_string = std::to_string(score);
for(int i=0; i<score_as_string.size(); i++) {
draw(8+i, h-1, score_as_string[i]);
}
}

void update(int w, int h, draw_function draw) {
if(flag) {
fly_x = w/2, fly_y = h-1;
flag = false;
}

for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
draw(x, y, ' ');
}
}

++fpscnt;
drawBody(w, h, draw);
updBullet(w, h, draw);
updEnemy(w, h, draw);
drawPic(w, h, draw);
drawScore(w, h, draw);
}

int main() {
toybox_run(MYFPS, update, keypress);
return 0;
}