游戏引擎 toybox
一个简易的游戏引擎,适合刚学了一点语法的小白。
项目地址:https://git.nju.edu.cn/jyy/toybox
源码阅读
阅读过程有 AI 协助。
toybox.h
下方代码展开约 280+ 行。
1 | // toybox.h |
这是一个简单的游戏和动画引擎,称为 “toybox”。它提供了一个函数 toybox_run,该函数接受三个参数:
- 整数
fps:表示每秒刷新的次数,也就是每秒调用update函数的次数。 - 函数指针
update:一个函数,定义为void update(int w, int h, draw_function draw),表示每次刷新时被调用的更新函数。它可以接受当前窗口的宽度和高度,并使用draw函数在屏幕上绘制图形。 - 函数指针
keypress:一个函数,定义为void keypress(int key),表示当按下键盘按键时被调用的函数。
在主循环中,程序会不断等待键盘输入或者根据设定的帧率调用 update 函数进行屏幕更新,然后根据更新后的画面重新绘制屏幕内容。
接下来我们从头到尾看一看里面的细节。
1 |
这段代码定义了一个宏 append_,用于将字符串追加到指定的缓冲区中。
#define append_(buf, str):这是宏的定义,append_是宏的名称,(buf, str)是宏的参数列表,这里有两个参数,buf表示目标缓冲区,str表示要追加的字符串。do { ... } while (0):这是一个 do-while 循环,它的主体是一系列语句,其中包括了复制字符串和移动指针的操作。do { ... }表示循环体,while (0)则是一个条件,由于条件为 0,因此循环只会执行一次。strcpy(buf, str):这一行使用strcpy函数将字符串str复制到缓冲区buf中。buf += strlen(str):这一行将指针buf向后移动,移动的距离是字符串str的长度,这样可以保证下一次追加的字符串会接在当前字符串的末尾。
这个宏的作用是将字符串追加到缓冲区中,类似于字符串拼接操作。在每次调用 append_ 宏时,它会将指定的字符串添加到目标缓冲区的末尾,并更新指针以指向新的末尾位置。
1 | static char canvas_[MAX_W_ * MAX_H_]; |
canvas_ 数组是用来表示绘图区域的缓冲区。在这个简单的游戏和动画引擎中,屏幕上的图像是通过在这个缓冲区中绘制字符来实现的。每个字符对应着屏幕上的一个像素或一个小图形。
在每次调用 update 函数时,会根据游戏逻辑更新 canvas_ 数组中的内容,然后将更新后的内容绘制到屏幕上。因此,canvas_ 数组存储了当前屏幕上的图像信息,通过更新这个数组,可以实现屏幕内容的动态变化。
1 | static int waitkey_(void); |
虽然在这段代码中,函数的声明和定义紧密相邻,看起来似乎有些多余,但这是一个良好的编程实践,可以帮助提高代码的可维护性和可读性。让读者快速了解函数的接口,包括返回类型和参数列表,而不必深入到函数的定义中去查找这些信息。
1 |
在这段代码中,它的作用是根据当前编译环境是否是 Windows 平台来进行条件编译。
这个技术常用于实现跨平台的编译,在不同的平台上使用不同的代码逻辑。
1 |
该头文件主要用于 Windows 平台下的一些系统调用和操作。
在这个代码中,<windows.h> 被用来进行以下操作:
- 获取系统时间和延时操作:通过
GetTickCount()函数可以获取系统启动后经过的毫秒数,用于实现定时器功能。另外,该头文件还定义了与时间相关的数据类型和函数,例如SYSTEMTIME结构体和GetSystemTime()函数。 - 控制台操作:例如
GetConsoleScreenBufferInfo()函数用于获取控制台屏幕缓冲区信息,SetConsoleCursorPosition()函数用于设置控制台光标位置,以及一些用于控制控制台文本属性和颜色的宏定义。 - 键盘输入操作:
<conio.h>头文件通常与<windows.h>一起使用,用于实现控制台下的键盘输入操作。在这个代码中,<conio.h>用于定义_kbhit()和_getch()函数,用于检测是否有键盘输入和获取键盘输入字符。
1 | static int waitkey_(void) { |
在 10 毫秒内轮询检查是否有键盘输入,若有则返回该输入,否则返回 -1.
1 | static void get_window_size_(int *w, int *h) { |
函数首先声明了一个 CONSOLE_SCREEN_BUFFER_INFO 结构体变量 csbi,用于存储获取到的控制台屏幕缓冲区信息。然后调用 GetConsoleScreenBufferInfo 函数,将获取到的信息存储在 csbi 变量中。
接着,函数通过计算 csbi 中的 srWindow 结构体中的 Right、Left、Bottom 和 Top 字段来计算控制台窗口的宽度和高度。具体地,控制台窗口的宽度等于 Right - Left,高度等于 Bottom - Top + 1。然后将计算得到的宽度和高度分别存储在传入的指针参数 w 和 h 所指向的位置。
如果调用 GetConsoleScreenBufferInfo 函数失败(可能是因为当前程序并非在控制台中运行),则函数将宽度和高度分别设为默认值 80 和 25。
1 | // copied from https://github.com/confluentinc/librdkafka |
这段代码看个大概就行。
这个函数名为 gettimeofday,它的功能是获取当前系统时间,并将其以秒和微秒的形式存储在 struct timeval 结构体指针 tp 中。 这个函数类似于 Unix/Linux 系统中的 gettimeofday 函数,但是实现方式有所不同。
具体来说,这个函数的步骤如下:
- 定义一个静态常量
EPOCH,用于表示从 1601 年 1 月 1 日 UTC 时间零点开始到 1970 年 1 月 1 日 UTC 时间零点之间的时间间隔,以 100 毫微秒(100纳秒)为单位。 - 调用 Windows 平台特有的
GetSystemTime函数,获取当前系统时间,并将结果存储在SYSTEMTIME结构体变量system_time中。 - 调用 Windows 平台特有的
SystemTimeToFileTime函数,将system_time转换为FILETIME结构体变量file_time,表示自 1601 年 1 月 1 日以来的时间。 - 将
file_time中的时间转换为以 100 毫微秒为单位的整数,存储在time变量中。 - 根据
time变量和EPOCH值的差值,计算出秒数并存储在tv_sec成员中,计算出微秒数并存储在tv_usec成员中。 - 返回 0,表示函数执行成功。
1 | static void clear_screen_() { |
这个函数的功能是清空控制台屏幕上的所有内容,并将光标移动到左上角位置。具体来说:
- 创建一个
COORD结构体变量topLeft,表示控制台屏幕的左上角位置。 - 获取标准输出控制台的句柄,并将其存储在
HANDLE类型的变量console中,使用GetStdHandle(STD_OUTPUT_HANDLE)函数实现。 - 声明一个
CONSOLE_SCREEN_BUFFER_INFO结构体变量screen,用于存储控制台屏幕缓冲区的信息。 - 调用
GetConsoleScreenBufferInfo函数,获取控制台屏幕缓冲区的信息,并将结果存储在screen变量中。 - 调用
FillConsoleOutputCharacterA函数,将控制台屏幕上所有位置的字符都填充为空格字符,使用空格字符' '。 - 调用
FillConsoleOutputAttribute函数,将控制台屏幕上所有位置的文本属性都填充为前景色为白色(红、绿、蓝三种颜色混合)。 - 最后,使用
SetConsoleCursorPosition函数将控制台光标移动到左上角位置,以确保下次输出从屏幕的左上角开始。
1 | uint64_t timer_ms_(void) { |
这个函数的功能是获取当前系统时间,并以毫秒为单位返回。
1 | static void __attribute__((constructor)) |
这个函数使用 __attribute__((constructor)) 属性,表示它会在程序运行时自动执行,并在其他代码之前被调用。
1 | static void |
其中,对于代码:
1 | if ((w_ << 16) + h_ != last_size) { |
这段代码的作用是在每次循环中检查当前窗口大小是否发生了变化,如果发生了变化,则清空屏幕,并将新的窗口大小记录下来,以便下次比较。
(w_ << 16) + h_:这一部分将当前窗口的宽度w_左移 16 位(相当于乘以 65536),然后加上窗口的高度h_。这个操作可以将窗口的宽度和高度合并成一个整数,用于唯一标识窗口的大小。append_(head, "\033[2J");:将清空屏幕的控制字符序列"\033[2J"追加到head中。
hello.cpp
该代码在整个小黑框内打印字符,按下按键后,小黑框内打印输入的字符。
效果:
1 | // hello.cpp |
可以看出,update() 和 keypress() 都是需要自己实现的。
值得一瞧的是:
1 | draw(0, 0, "-\\|/"[(t++) / 5 % 4]); |
这玩意实现了一个小动画。
使用方法
请参考 toybox.h 头部的注释和 hello.cpp 的例子。
C/C++ 都可以从以下模板开始,只需实现 “TODO” 中更新屏幕和响应按键逻辑 (可以不提供响应按键的 keypress) 即可:
1 |
|
1 |
|
例子
snake
1 | // snake.cpp |
tetris
1 | // tetris.cpp |
rasterize
1 | // rasterize.cpp |
demo:
飞机大战
自己写了一个,整体思路不是很难。
1 |
|










