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

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

源码阅读

阅读过程有 AI 协助。

toybox.h

下方代码展开约 280+ 行。

// 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 函数进行屏幕更新,然后根据更新后的画面重新绘制屏幕内容。

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

#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_ 宏时,它会将指定的字符串添加到目标缓冲区的末尾,并更新指针以指向新的末尾位置。

static char canvas_[MAX_W_ * MAX_H_];

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

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

static int waitkey_(void);

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

#ifdef _WIN32

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

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

#include <windows.h>

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

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

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

在 10 毫秒内轮询检查是否有键盘输入,若有则返回该输入,否则返回 -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;
    }
}

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

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

如果调用 GetConsoleScreenBufferInfo 函数失败(可能是因为当前程序并非在控制台中运行),则函数将宽度和高度分别设为默认值 80 和 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;
}

这段代码看个大概就行。

这个函数名为 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,表示函数执行成功。
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 函数将控制台光标移动到左上角位置,以确保下次输出从屏幕的左上角开始。
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_();
}

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

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

其中,对于代码:

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

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

效果:


// 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() 都是需要自己实现的。

值得一瞧的是:

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

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

使用方法

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

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

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

// 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

// 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

// 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:

飞机大战

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

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