一个简易的游戏引擎,适合刚学了一点语法的小白。
项目地址: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,该函数接受三个参数:
- 整数
fps:表示每秒刷新的次数,也就是每秒调用update函数的次数。 - 函数指针
update:一个函数,定义为void update(int w, int h, draw_function draw),表示每次刷新时被调用的更新函数。它可以接受当前窗口的宽度和高度,并使用draw函数在屏幕上绘制图形。 - 函数指针
keypress:一个函数,定义为void keypress(int key),表示当按下键盘按键时被调用的函数。
在主循环中,程序会不断等待键盘输入或者根据设定的帧率调用 update 函数进行屏幕更新,然后根据更新后的画面重新绘制屏幕内容。
接下来我们从头到尾看一看里面的细节。
#define append_(buf, str) \
do { \
strcpy(buf, str); \
buf += strlen(str); \
} while (0)
这段代码定义了一个宏 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_ 宏时,它会将指定的字符串添加到目标缓冲区的末尾,并更新指针以指向新的末尾位置。
static char canvas_[MAX_W_ * MAX_H_];
canvas_ 数组是用来表示绘图区域的缓冲区。在这个简单的游戏和动画引擎中,屏幕上的图像是通过在这个缓冲区中绘制字符来实现的。每个字符对应着屏幕上的一个像素或一个小图形。
在每次调用 update 函数时,会根据游戏逻辑更新 canvas_ 数组中的内容,然后将更新后的内容绘制到屏幕上。因此,canvas_ 数组存储了当前屏幕上的图像信息,通过更新这个数组,可以实现屏幕内容的动态变化。
static int waitkey_(void);
虽然在这段代码中,函数的声明和定义紧密相邻,看起来似乎有些多余,但这是一个良好的编程实践,可以帮助提高代码的可维护性和可读性。让读者快速了解函数的接口,包括返回类型和参数列表,而不必深入到函数的定义中去查找这些信息。
#ifdef _WIN32
在这段代码中,它的作用是根据当前编译环境是否是 Windows 平台来进行条件编译。
这个技术常用于实现跨平台的编译,在不同的平台上使用不同的代码逻辑。
#include <windows.h>
该头文件主要用于 Windows 平台下的一些系统调用和操作。
在这个代码中,<windows.h> 被用来进行以下操作:
- 获取系统时间和延时操作:通过
GetTickCount()函数可以获取系统启动后经过的毫秒数,用于实现定时器功能。另外,该头文件还定义了与时间相关的数据类型和函数,例如SYSTEMTIME结构体和GetSystemTime()函数。 - 控制台操作:例如
GetConsoleScreenBufferInfo()函数用于获取控制台屏幕缓冲区信息,SetConsoleCursorPosition()函数用于设置控制台光标位置,以及一些用于控制控制台文本属性和颜色的宏定义。 - 键盘输入操作:
<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 结构体中的 Right、Left、Bottom 和 Top 字段来计算控制台窗口的宽度和高度。具体地,控制台窗口的宽度等于 Right - Left,高度等于 Bottom - Top + 1。然后将计算得到的宽度和高度分别存储在传入的指针参数 w 和 h 所指向的位置。
如果调用 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 函数,但是实现方式有所不同。
具体来说,这个函数的步骤如下:
- 定义一个静态常量
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,表示函数执行成功。
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);
}
这个函数的功能是清空控制台屏幕上的所有内容,并将光标移动到左上角位置。具体来说:
- 创建一个
COORD结构体变量topLeft,表示控制台屏幕的左上角位置。 - 获取标准输出控制台的句柄,并将其存储在
HANDLE类型的变量console中,使用GetStdHandle(STD_OUTPUT_HANDLE)函数实现。 - 声明一个
CONSOLE_SCREEN_BUFFER_INFO结构体变量screen,用于存储控制台屏幕缓冲区的信息。 - 调用
GetConsoleScreenBufferInfo函数,获取控制台屏幕缓冲区的信息,并将结果存储在screen变量中。 - 调用
FillConsoleOutputCharacterA函数,将控制台屏幕上所有位置的字符都填充为空格字符,使用空格字符' '。 - 调用
FillConsoleOutputAttribute函数,将控制台屏幕上所有位置的文本属性都填充为前景色为白色(红、绿、蓝三种颜色混合)。 - 最后,使用
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");
}
这段代码的作用是在每次循环中检查当前窗口大小是否发生了变化,如果发生了变化,则清空屏幕,并将新的窗口大小记录下来,以便下次比较。
(w_ << 16) + h_:这一部分将当前窗口的宽度w_左移 16 位(相当于乘以 65536),然后加上窗口的高度h_。这个操作可以将窗口的宽度和高度合并成一个整数,用于唯一标识窗口的大小。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;
}