01从这里开始
本指南从按键输入讲到屏幕绘制,介绍 HARAPEKO SHIROHEBI(はらぺこしろへび)的实现。游戏由 DAISUKE OBA 创作。玩家操纵白蛇吃石榴、增长身体,同时避开地雷和自己的身体。随附的程序、原创素材及本指南采用 MIT 许可证。
画面尺寸为 160 × 144 像素。同一个 ROM 支持初代 Game Boy(DMG)和 Game Boy Color(CGB),程序在运行时选择显示方式。卡带配置为 MBC5、64 KiB ROM 和 8 KiB 电池备份 RAM。可玩的发行版见 游戏的 itch.io 页面。
建议阅读顺序:先看流程图,再结合阅读蛇头运动和身体跟随两节。库的使用表会指出相应入口及示例源码。源码标识符中的 Apple 指画面上的石榴,Block 指地雷。
02构建与文件组织
使用 Rust 1.85 或更高版本,可为 Windows、Linux、macOS ARM 和 macOS Intel 构建编译器及全部辅助工具。原生程序运行不需要 .NET,素材处理工具也不需要 Python 或 Pillow。
PowerShell 示例构建使用根目录的 Windows 程序。在 Linux/macOS 上使用原生编译器并传入相同的 C 输入和选项,或使用 PowerShell 7。CHR 图形与 C 源码是不同输入;font.chr 保留原示例字体。
powershell -NoProfile -ExecutionPolicy Bypass -File .\apps\harapeko_shirohebi\build.ps1
构建脚本按指定顺序拼接 17 个 C 源码片段。音乐、导出的素材和 ROM 存储体选择包装文件作为独立编译输入。构建使用仓库中的 lib/,无需私有美术工程或素材导出器。各片段通过拼接后的编译单元共享声明,不能把它们逐个加入普通 C 项目单独编译。
| 选项/输出文件 | 用途 |
|---|---|
-KitaqgbRoot | 指定包含 kitaqgb.exe 和 lib/ 的目录。按仓库默认目录结构使用时可省略。 |
-OutputDirectory | 指定输出目录,默认为游戏目录下的 out/。 |
-KeepBuildFiles | 构建成功后保留拼接源码、临时音频适配文件和编译诊断。 |
shirohebi.gb | 本次编译生成的 ROM。构建失败后,不要把之前遗留的 ROM 当作本次结果。 |
shirohebi.map | 本次构建的符号地址表,调试时应使用与 ROM 对应的这份文件。 |
build_manifest.json | 记录编译器和 ROM 的哈希值、输出大小及中断向量地址。 |
为什么需要构建后的处理步骤
无论当前切换到了哪个 ROM 存储体,VBlank 音乐中断都必须能访问其代码。脚本检查四个中断相关符号是否位于固定存储体 0,在 $0040 安装 JP 指令,并重新计算卡带的两个校验和。若中断向量与预期不符,构建会停止。临时音频源码副本会禁用主循环的音乐服务,防止主循环和中断重复推进音乐;另一份临时副本负责安排音频辅助函数并使用可达的绝对跳转。仓库中的库源码保持不变。
完整的输入顺序和编译选项见 build.ps1。主要选项为 --profile=release --stack-bank=fixed --rst-disable --cgb=cgb --cart=mbc5_ram_battery --romsize=64k --ramsize=8k。
03程序流程与状态
| 状态转换 | 触发条件与结果 |
|---|---|
| TITLE → READY → PLAY | 按 START 开始游戏;START 提示演出结束后才开始移动。 |
| PLAY ⇄ PAUSE | 按 START 暂停或继续。暂停期间停止物理更新和身体跟随。 |
| PAUSE → RETURN_CONFIRM | 按 SELECT 打开返回标题确认框,默认选择 NO。取消后回到原来的暂停状态;选择 YES 返回标题。 |
| PLAY → MISS | 碰到地雷或自身身体时,播放对应的失败演出。 |
| MISS → NAME_ENTRY / GAMEOVER | 成绩进入排行榜时打开姓名输入,否则直接询问是否重试。确认姓名后保存成绩,再进入重试界面。 |
| GAMEOVER → READY / TITLE | 重试选 YES 开始新一局,选 NO 返回标题。 |
| TITLE ⇄ ERASE | SELECT+START 打开成绩删除确认框。A 确认,B 或 START 取消;默认选择 NO。 |
松键门控会屏蔽输入,直到所有按键都已松开;检测到松开的那一次循环也不会执行操作。这样,打开界面的按键就不会紧接着确认下一项。演示模式用自动转向输入驱动同一套运动与碰撞代码。标题画面的空闲时间按 900 次主循环计数,演示时长按 1,800 次 VBlank 计数,两者都不应表述为保证准确的现实时间。
04蛇头运动:定点数与转向
只有蛇头使用 KQBody2D。它所在的 KQWorld2D 中只有一个活动刚体,重力为零,接触求解迭代次数为零;移动后的碰撞判断由游戏自行完成。Q4 坐标以 16 个整数单位表示一个像素,例如 160 表示 10 像素,因而不使用浮点数也能保留不足一个像素的运动。
base = 8 + 2 * min(score / 5, 10) // Q4 component limit
first UP update: limit = base + (base >> 2)
held UP: limit += max(base >> 2, 1), capped at 2 * base
released UP: limit -= max(base >> 2, 1), floored at base
pixel_center = (q4_position + 8) >> 4
基础速度分量上限为每次更新 0.5~1.75 像素。方向按顺时针分为 16 档:0 向右、4 向下、8 向左、12 向上。LEFT/RIGHT 每次使目标朝向转过一档;长按时先使用 8 次更新的倒计时,再以 4 次更新的倒计时重复转向,同时按下左右键则不转向。
目标速度分量由完整值、四分之三值、四分之一值或零组成。实际速度的每个分量在一次更新中最多向目标靠近 3 个 Q4 单位。这是计算量较小的整数近似,向量没有经过三角函数归一化。因此,画面上的朝向可能先改变,而运动方向仍在逐步跟上。
调用 kq2d_step 后,蛇头按中心坐标边界穿屏:X 小于 4 时设为 156,大于 156 时设为 4;Y 使用 4 和 140 两个边界。随后统一取整为像素坐标,供绘制和碰撞使用。蛇头采用这套边界跳转规则,身体跟随则使用模 160/144 的坐标回绕。
05白蛇身体的跟随算法
ChainBody 直接引用 SND_TraceX、SND_TraceY 和 SND_TraceDir。这些数组保存每一节当前的位置与朝向,并非蛇头走过的轨迹。碰撞和绘制读取同一组数组。白蛇起初由一个头和三节身体组成,最多可增长到 31 节(含头)。
- 把蛇头的像素坐标和朝向写入索引 0,并将
pull设为该朝向。 - 处理第
i节时,先保存它的原朝向,再从前一节更新后的位置减去与pull对应的小偏移,得到目标位置。 - 分别求 X、Y 轴跨越边界时的最短位移,按下表在各轴移动 0、1 或 2 像素。
- 把本节朝向设为
pull,对新位置做回绕处理,再把保存的原朝向传给第i + 1节。
| 带符号位移 d | 该轴的移动量 |
|---|---|
| d > 4 | +2 |
| 0 < d ≤ 4 | +1 |
| d = 0 | 0 |
| −4 ≤ d < 0 | −1 |
| d < −4 | −2 |
方向 0~15 的 X 偏移依次为 [4,4,3,1,0,-1,-3,-4,-4,-4,-3,-1,0,1,3,4];Y 偏移使用同一张表,将索引偏移 12 档。坐标在 160 × 144 的区域内回绕。对长度为 W 的轴,位移大于 W/2 时减去 W,小于 −W/2 时加上 W;恰好相隔半个区域时保留原位移的符号。
屏幕边缘的计算示例
假设前一节位于 (2, 50) 且朝右,后一节的目标在其后方四像素处,即 X = (2 − 4) mod 160 = 158。若后一节当前 X=156,位移为 +2,本次移动 +1,得到 X=157。若当前 X=0,最短位移为 −2,本次移动到 X=159。这样身体会从屏幕边缘跟过去,而不是横穿整个屏幕。
增长时复制尾节的位置和朝向,然后增加有效节数。新尾节会在随后的跟随更新中逐渐分离。原尾节会高亮显示 20 次更新。库的包装文件将跟随函数及其辅助函数一起放入存储体 2,避免每处理一节都切换存储体。
/* Excerpt: the game owns these arrays for the entire run. */
chain_body_init(&SND_ChainBody, SND_TraceX, SND_TraceY, SND_TraceDir,
SND_MAX_SEGMENTS, SND_SCREEN_W, SND_SCREEN_H);
chain_body_reset(&SND_ChainBody, SND_Length,
SND_HeadX, SND_HeadY, SND_Facing);
/* Once per PLAY update, after integrating and rounding the head. */
SND_ChainBody.count = SND_Length;
chain_body_step(&SND_ChainBody, SND_HeadX, SND_HeadY, SND_Facing);
/* On fruit collection, within capacity. */
chain_body_grow(&SND_ChainBody);
SND_Length = SND_ChainBody.count;
这段代码展示数组的生命周期和调用顺序;完整游戏还会检查增长是否成功,并设置高亮效果。相关源码:shirohebi_snake.c/shirohebi_chain.c/chain.c。
06水果、碰撞与失败演出
PLAY 状态的更新顺序为:加速 → 转向 → 蛇头物理运动 → 蛇头穿屏与取整 → 身体跟随 → 地雷碰撞 → 自身碰撞 → 吃水果 → 高亮计时结束处理。同一次更新中,致命碰撞优先于吃到水果。
| 检测对象 | 实际规则 |
|---|---|
| 地雷 | 逐节检查中心坐标是否落在含边界的矩形内:X=46…58 或 102…114,Y=62…74。 |
| 自身身体 | 蛇头与索引 2 及其后的各节比较,要求回绕后的最短 |dx| < 3 且 |dy| < 3;紧邻蛇头的一节不参与此检测。 |
| 水果 | 蛇头与水果中心的普通坐标距离满足 |dx| < 7 且 |dy| < 7,不使用边界回绕距离。吃到后加分、尝试增长并选择下一个位置。 |
这些判断分别比较两轴距离,属于轴对齐范围判断,并非圆形距离检测。程序按确定的顺序检查 16 个固定水果候选位置。与地雷的最小图块距离随成绩变化:分数达到 0、10、20、30 时分别为 4、3、2、1;与地雷位于同一图块的候选始终被排除。一次搜索最多尝试 16 个位置。分数用 8 位数值保存,不是无限增长的计数器。
撞到自身时,身体保持碰撞时的姿态并逐渐淡出。碰到地雷时,各节以定点速度散开,配合延迟生效的重力和错开的爆炸时序,最后显示弹坑。失败计时结束后,符合条件的成绩进入姓名输入,其余进入重试提示。CGB 使用彩色效果,DMG 使用灰阶调色板。
07绘制:图块与精灵顺序
初始化时先等待安全的显示时机,再关闭 LCD、清空 VRAM、载入图块、字形和调色板,最后启用显示。CGB 路径设置背景属性和颜色调色板,DMG 路径设置灰阶寄存器。导出的背景每行 20 个图块,硬件背景图每行则占 32 个图块,索引计算不能混用。
每次循环在 RAM 中生成 OAM 影子缓冲。写入精灵坐标时需加上硬件规定的偏移:X+8、Y+16。调用 system_wait_vblank() 后再提交 OAM。白蛇占用槽位 0…30,文字使用剩余槽位,未使用的项目会被清空。
前六节的 OAM 优先顺序保持固定,后面的节每次绘制按增加四的相位轮换排列。这使每条扫描线最多显示十个精灵的限制在后半身各节之间轮流体现,并不会解除硬件限制,也不会删除身体节。修改最大长度时,需要同时检查数组容量和全部 40 个 OAM 槽位的分配。
START 和 PAUSE 覆盖文字分别记录期望状态、已显示状态以及待重绘标记。移除文字时,先根据当前水果状态恢复每个格子,再在需要时使用背景图。若一律填成某个空白图块,就会擦掉文字下方的水果。字形掩码由八行 1 位数据展开为十六字节的 2 位图块数据,字形像素优先于背景图案。
shirohebi_render.c / shirohebi_display.c / shirohebi_pause.c
08音乐与 VBlank 的分工
audio_vblank.c 读取固定存储体中的音乐流。每条普通记录按等待时间、CH1、CH2、CH3、CH4排列,对应带扫频的脉冲、脉冲、波形和噪声声道,顺序与硬件一致。音乐控制使用 AudioVBlank_PlayMusic、AudioVBlank_Stop 和 AudioVBlank_SetEnabled。
普通 audio.c 驱动负责游戏音效;构建时通过临时宏定义排除它的音乐服务。游戏用 __bankof 取得音效数据所在的存储体,并使用优先级控制播放;地雷效果还直接使用 CH4 噪声。暂停操作协调音乐与音效。主循环调用的 SND_AudioServiceWork 特意保持为空,防止身体越长、辅助调用越多,音乐就推进得越快。
AudioVBlank_FrameHook 调用固定存储体中的 SND_DemoVBlankHook。该钩子递增一个 8 位演示计数器,并在保留 P1 选择值的前提下采样系统按键组。相应功能启用时,它锁存标题画面的 SELECT+START 和暂停时的 SELECT,使短按不会因主循环繁忙而遗漏。钩子中不绘图,也不调用可切换存储体中的函数。
/* Excerpt from initialization, after shared state has been set up. */
SND_AudioInit();
AudioVBlank_FrameHook = SND_DemoVBlankHook;
AudioVBlank_EnableIrq();
09成绩保存与姓名输入
槽位 0 保存 42 字节的应用数据:标记 0x53、版本 2、十个单字节分数和三十个姓名字符。存档库负责外层长度与校验和验证。读取程序也能识别 5 字节和 15 字节的成绩数据,写入时统一使用 42 字节格式。
符合入榜条件的非零分数按降序插入。比较使用严格大于,因此相同分数排在已有记录之后,姓名随对应分数一起移动。新记录先在 RAM 中以 AAA 为初始姓名,玩家确认三个字符后才写入 SRAM。若放弃尚未确认的成绩,游戏重新读取已保存的排行榜。
UP/DOWN 选择 A–Z 或句点。RIGHT/SELECT 循环移到下一个字符,LEFT 循环移到前一个字符;B 仅在光标不位于第一个字符时后退。A 移到下一位,在最后一位则确认;START 可在任意位置确认。松键门控防止这次确认同时选中重试选项。
10KITAQGB 库的使用方法
请配套使用仓库中的库源码与头文件。游戏的包装文件和配置也是示例的一部分,尤其需要注意存储体布局及两套音频处理的协调。
| 库 | 入口与用途 | 使用示例 |
|---|---|---|
system.c | system_init, system_wait_vblank初始化系统状态并同步显示提交。音频中断钩子需另外安装。 | shirohebi_main.c |
chain.c | chain_body_init, chain_body_reset, chain_body_step, chain_body_grow绑定持续有效的数组、生成初始姿态、跟随蛇头并追加尾节。本游戏使用 ChainBody 接口。 | shirohebi_snake.c |
physics2d.c | kq2d_world_init, kq2d_step更新一个 Q4 蛇头刚体;重力和接触求解迭代次数均设为零。 | shirohebi_snake.c |
sprite.c | sprite_init; kq_sprite_oam and companion state初始化并使用 OAM 影子缓冲。游戏直接写入项目后统一提交,须遵守槽位和坐标规则。 | shirohebi_render.c |
audio_vblank.c | AudioVBlank_Init, PlayMusic, Stop, SetEnabled, EnableIrq, FrameHook播放常驻音乐流,并在中断中调用游戏钩子。IRQ 代码和数据须位于固定 ROM。此处首项之后的简称均带有 AudioVBlank_ 前缀。 | shirohebi_audio.c |
audio.c | Audio_Init, Audio_Update, Audio_PlaySFXBanked, Audio_StopSfx, Audio_SetPaused控制音效播放、优先级和暂停,并与 VBlank 音乐驱动协调。 | shirohebi_audio.c |
save.c | save_exists, save_read, save_write在槽位 0 读写带版本信息的排行榜。save_read 验证请求的精确长度和校验和后返回成功或失败;游戏另行检查自身数据标记。 | shirohebi_save.c |
cgb_palette.c | cgb_bg_palette, cgb_obj_palette设置 CGB 颜色;DMG 的灰阶寄存器则另外显式设置。 | shirohebi_display.c |
map.c | map_load and the exported map_t descriptor支持素材文件中的地图辅助函数;游戏恢复背景时自行逐格写入。 | shirohebi_assets.c |
fixed.c | fixed-point support作为支持代码参与链接。蛇头运动通过 physics2d 和整数 Q4 运算实现。 | shirohebi_common.c |
audio_hwregs_gb.c | shared audio register bindings提供两套音频处理共同需要的 Game Boy 硬件寄存器定义。 | shirohebi_audio.c |
与库配合使用的编译器内建函数
__readpadex(previous) 在低字节返回持续按下的按键,在高字节返回新按下的按键。__cgb_is_cgb() 用于选择彩色处理路径。__settile、__settileattr 等内建函数更新背景图格;__bankof 提供音效数据的 ROM 存储体编号。这些功能由编译器提供,无需另外复制一份 C 实现到游戏中。
11源码导航与修改要点
| 文件 | 职责 |
|---|---|
shirohebi_common.c | 类型、常量、共享状态及前置声明。追踪某个符号时建议先看这里。 |
shirohebi_main.c | 启动流程,以及输入 → 更新 → 绘制 → VBlank 的主循环。 |
shirohebi_flow.c | 状态分派:标题选项、READY、PLAY、暂停、失败、姓名输入和重试。 |
shirohebi_input.c | 松键门控、标题快捷键和返回标题确认。 |
shirohebi_snake.c | 蛇头物理运动、加速、相对转向、穿屏,以及身体跟随的初始化、更新和增长。 |
shirohebi_chain.c | 存储体 2 的包装文件,包含完整的共享 chain 实现。 |
shirohebi_collision.c | 地雷与自身碰撞、碰撞类型区分及身体散开动画。 |
shirohebi_apples.c | 石榴候选位置选择、收集、计分和增长;内部标识符使用 Apple。 |
shirohebi_demo.c | 演示模式转向、长蛇姿态及基于 VBlank 计数器的经过时间统计。 |
shirohebi_render.c | OAM 影子缓冲填充、头身尾图块、优先顺序轮换和失败特效。 |
shirohebi_display.c | LCD/VRAM 初始化、调色板和字形载入,以及标题与排行榜布局。 |
shirohebi_background.c | 游戏背景图格、地雷及被覆盖图格的恢复。 |
shirohebi_pause.c | START/PAUSE 覆盖文字的期望、已显示和待重绘状态。 |
shirohebi_text.c | 字符到图块的转换、字符串、数值成绩和菜单光标。 |
shirohebi_save.c | 排行榜验证与插入、三个字符的姓名及 SRAM 存储。 |
shirohebi_audio.c | 音乐与音效的初始化、启停、选项、暂停及声道协调。 |
music.c | 常驻的定长 VBlank 音乐记录,包含开始短曲、主曲及游戏结束曲。 |
sfx.c | 由游戏音频函数选择的音效字节流。 |
text_tiles.c | 字形掩码及其到 Game Boy 2 位图块行数据的转换。 |
shirohebi_assets.c | 导出的图块、背景图、属性、碰撞与事件表,以及地图描述符和辅助函数。 |
shirohebi_input_shared.h | 与中断共享的 WRAM 输入标志和固定存储体回调声明。 |
shirohebi_lib_bank0.c | 固定存储体中的 VBlank 输入/时钟回调、共享 WRAM 标志,并为后续系统和音频编译输入选择存储体 0。 |
shirohebi_lib_bank1.c | 为后续支持库编译输入选择存储体 1。 |
修改游戏时的检查要点
- 速度与转向:修改 Q4 常量后,检查低速转弯和 UP 加速。蛇头显示的朝向档位不等于当前精确的速度方向。
- 长度:同步调整数组容量、跟随链容量及 OAM 预留槽位,检查尾部的地雷/自身碰撞和扫描线精灵限制。
- 图形:保持图块编号、背景图行跨度和 CGB 属性一致。移除覆盖文字时恢复下方当前实际内容。
- 音频:保持中断代码常驻固定存储体,并确保音乐时钟只有一个推进来源。除了正常播放,也要检查暂停与继续。
- 存档:修改数据布局时更新版本号;访问数据前检查长度,并测试空白和无效 SRAM。
重新构建后,调试时使用本次生成的地址表。分别在 DMG 与 CGB 模式下检查标题、START 提示、移动、转向、吃水果、自身/地雷碰撞、暂停/继续、返回确认及排行榜。编译成功本身并不能证明这些操作路径正确,也不能替代实机验证。