游戏引擎 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 |
|