KITAQGB 應用程式指南 · GB / CGB

HARAPEKO SHIROHEBI

程式設計指南 · 執行流程、移動演算法與程式庫運用

KITAQ 系列手冊English日本語한국어简体中文繁體中文FrançaisEspañolDeutschREADMEDAISUKE OBA · MIT

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 bank,VBlank 音樂中斷都必須能存取所需程式碼。指令碼確認四個中斷相關符號位於固定 bank 0,在 $0040 安裝 JP 指令,再重新計算卡匣的兩個校驗和。若中斷向量不符預期,建置就會停止。暫存音訊原始碼副本會停用一般音樂服務,避免主迴圈與中斷重複推進音樂;另一份暫存副本負責配置音訊輔助函式,並使用位址可達的絕對跳躍。儲存庫內的程式庫原始碼不會被改寫。

完整的輸入順序與編譯選項請見 build.ps1。主要選項為 --profile=release --stack-bank=fixed --rst-disable --cgb=cgb --cart=mbc5_ram_battery --romsize=64k --ramsize=8k。

↑ 返回目錄

03程式流程與狀態

初始化硬體、顯示與共用狀態
啟用中斷音訊掛鉤、讀取成績、標題畫面
主迴圈輸入 → 狀態更新 → 顯示
一次主迴圈:維護音訊狀態 → 讀取按住與剛按下的按鍵 → 處理快捷鍵與放開按鍵的等待機制 → 執行目前狀態的處理函式 → 建立 OAM 影子緩衝區 → 等待 VBlank → 傳送 OAM → 處理音效 → 更新需要重繪的覆蓋文字,然後重複。
圖 1:主程式流程。主迴圈執行期間,硬體仍可能觸發 VBlank 中斷。
狀態轉換觸發條件與結果
TITLE → READY → PLAY按 START 開始遊戲;START 提示演出結束後才開始移動。
PLAY ⇄ PAUSE按 START 暫停或繼續。暫停期間停止物理更新與身體跟隨。
PAUSE → RETURN_CONFIRM按 SELECT 開啟返回標題的確認視窗,預設選項為 NO。取消後回到原本暫停的遊戲;選 YES 則返回 TITLE。
PLAY → MISS碰到地雷或自己的身體時,播放對應的失敗演出。
MISS → NAME_ENTRY / GAMEOVER成績達到入榜條件時進入姓名輸入,否則直接詢問是否重試。確認姓名後儲存成績,再進入重試畫面。
GAMEOVER → READY / TITLE重試選 YES 開始新的一局;選 NO 返回標題。
TITLE ⇄ ERASESELECT+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 的座標環繞。

相關原始碼:shirohebi_snake.c/shirohebi_flow.c。

↑ 返回目錄

05白蛇身體的跟隨演算法

ChainBody 直接參照 SND_TraceX、SND_TraceY 與 SND_TraceDir。這些陣列儲存每一節目前的位置與朝向,而非蛇頭走過的軌跡。碰撞與繪圖讀取同一組陣列。白蛇起初由一個頭與三節身體組成,最多可長到 31 節(含頭)。

畫面較窄時,可左右捲動此圖。

蛇頭的新朝向逐節向後傳遞蛇頭 [0]朝向 = 0身體 [1]原 15 → 新 0身體 [2]原 14 → 新 15015傳給下一節的是事先儲存的原朝向。
圖 2:一次更新中的方向傳遞。先儲存原朝向,再寫入新朝向,彎曲便會逐節向後傳遞;位置計算使用前一節更新後的座標。
  1. 將蛇頭的像素座標與朝向寫入索引 0,並將 pull 設為該朝向。
  2. 處理第 i 節時,先儲存其原朝向,再從前一節更新後的位置減去對應方向的小偏移量,求得目標位置。
  3. 分別求 X、Y 軸跨越邊界時的最短位移,依下表在各軸移動 0、1 或 2 像素。
  4. 將本節朝向設為 pull,對新位置做環繞處理,再把儲存的原朝向傳給第 i + 1 節。
帶正負號的位移 d各軸的修正量
d > 4+2
0 < d ≤ 4+1
d = 00
−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 次更新。程式庫包裝檔將跟隨函式及其輔助函式一起放在 bank 2,避免每處理一節都要切換 bank。

/* 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 則使用灰階調色盤。

shirohebi_collision.c / shirohebi_apples.c

↑ 返回目錄

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 讀取固定 bank 中的音樂串流。每筆一般紀錄依等待時間、CH1、CH2、CH3、CH4排列,分別對應帶掃頻的脈波、脈波、波形與雜訊聲道,順序與實體硬體一致。音樂控制使用 AudioVBlank_PlayMusic、AudioVBlank_Stop 與 AudioVBlank_SetEnabled。

一般的 audio.c 驅動程式負責遊戲音效;建置時透過暫時的巨集定義排除其音樂服務。遊戲以 __bankof 取得音效資料所在的 bank,並利用優先權控制播放;地雷效果也直接使用 CH4 雜訊。暫停操作會協調音樂與音效。主迴圈呼叫的 SND_AudioServiceWork 刻意保持為空,避免處理更多身體節時,音樂時鐘也跟著加速。

AudioVBlank_FrameHook 呼叫遊戲固定 bank 中的 SND_DemoVBlankHook。這個掛鉤遞增一個 8 位元展示計數器,並在保留 P1 選擇值的前提下取樣系統按鍵組。啟用相關功能時,它會鎖存標題畫面的 SELECT+START 與暫停時的 SELECT,避免短按因主迴圈忙碌而遺漏。掛鉤內不繪圖,也不呼叫可切換 bank 中的函式。

/* Excerpt from initialization, after shared state has been set up. */
SND_AudioInit();
AudioVBlank_FrameHook = SND_DemoVBlankHook;
AudioVBlank_EnableIrq();

shirohebi_audio.c / shirohebi_lib_bank0.c / music.c

↑ 返回目錄

09成績儲存與姓名輸入

槽位 0 儲存 42 位元組的應用程式資料:識別標記 0x53、版本 2、十個單位元組分數與三十個姓名字元。存檔程式庫負責外層長度與校驗和驗證。讀取程式也能辨識 5 位元組與 15 位元組的成績資料,寫入時則統一使用 42 位元組格式。

符合入榜條件的非零分數以遞減順序插入。比較採用嚴格大於,因此同分成績排在既有紀錄之後,姓名隨對應分數一起移動。新紀錄先在 RAM 中以 AAA 為初始姓名,玩家確認三個字元後才寫入 SRAM。若放棄尚未確認的成績,遊戲會重新讀取已儲存的排行榜,而不提交該筆紀錄。

UP/DOWN 選擇 A–Z 或句點。RIGHT/SELECT 循環移到下一個字元,LEFT 循環移到上一個;B 僅在游標不位於第一個字元時後退。A 移到下一位,在最後一位則確認;START 可在任意位置確認。等待按鍵放開的機制可避免這次確認同時選中重試選項。

shirohebi_save.c / shirohebi_flow.c

↑ 返回目錄

10KITAQGB 程式庫的使用方式

請搭配使用儲存庫中的程式庫原始碼與標頭檔。遊戲專用的包裝檔和設定也是範例的一部分,尤其要注意 bank 配置及兩套音訊處理的協調。

程式庫入口與用途範例原始碼
system.csystem_init, system_wait_vblank
初始化系統狀態並同步畫面提交。音訊中斷掛鉤須另行安裝。
shirohebi_main.c
chain.cchain_body_init, chain_body_reset, chain_body_step, chain_body_grow
綁定持續有效的陣列、建立初始姿態、跟隨蛇頭並追加尾節。本遊戲使用 ChainBody,不使用環形歷史紀錄的 Chain API。
shirohebi_snake.c
physics2d.ckq2d_world_init, kq2d_step
更新一個 Q4 蛇頭剛體;重力與接觸求解迭代次數均設為零。
shirohebi_snake.c
sprite.csprite_init;kq_sprite_oam 及相關狀態
初始化並使用 OAM 影子緩衝區。遊戲直接填入項目後統一提交,須遵守槽位與座標規則。
shirohebi_render.c
audio_vblank.cAudioVBlank_Init, PlayMusic, Stop, SetEnabled, EnableIrq, FrameHook
播放常駐音樂串流,並在中斷中呼叫遊戲回呼。IRQ 程式碼與資料須位於固定 ROM;此處首項之後的名稱皆省略了 AudioVBlank_ 前綴。
shirohebi_audio.c
audio.cAudio_Init, Audio_Update, Audio_PlaySFXBanked, Audio_StopSfx, Audio_SetPaused
控制音效播放、優先權與暫停,並與 VBlank 音樂驅動程式協調。
shirohebi_audio.c
save.csave_exists, save_read, save_write
在槽位 0 讀寫含版本資訊的排行榜。save_read 驗證資料是否符合要求的精確長度與校驗和後,回傳成功或失敗;遊戲另外檢查自身的識別標記。
shirohebi_save.c
cgb_palette.ccgb_bg_palette, cgb_obj_palette
設定 CGB 色彩;DMG 的灰階暫存器則另外明確設定。
shirohebi_display.c
map.cmap_load 與匯出的 map_t 描述結構
支援素材檔案中的地圖輔助函式;遊戲還原背景時自行逐格寫入。
shirohebi_assets.c
fixed.c定點數支援
作為支援程式碼參與連結。蛇頭移動透過 physics2d 與整數 Q4 運算實現。
shirohebi_common.c
audio_hwregs_gb.c共用音訊暫存器定義
提供兩套音訊處理共同需要的 Game Boy 硬體暫存器定義。
shirohebi_audio.c

與程式庫搭配使用的編譯器內建函式

__readpadex(previous) 在低位元組回傳持續按住的按鍵,在高位元組回傳剛按下的按鍵。__cgb_is_cgb() 用於選擇彩色處理路徑。__settile、__settileattr 等內建函式更新背景格子;__bankof 提供音效資料的 ROM bank 編號。這些是編譯器提供的功能,不需要另外複製一份 C 實作到遊戲中。

↑ 返回目錄

11原始碼導覽與修改要點

檔案職責
shirohebi_common.c型別、常數、共用狀態與前置宣告。追蹤符號時建議先看這裡。
shirohebi_main.c啟動流程,以及輸入 → 更新 → 繪製 → VBlank 的主迴圈。
shirohebi_flow.c狀態分派:標題選項、READY、PLAY、暫停、失敗、姓名輸入與重試。
shirohebi_input.c等待按鍵放開、標題快捷鍵與返回標題確認。
shirohebi_snake.c蛇頭物理移動、加速、相對轉向、穿屏,以及跟隨鏈的初始化、更新與增長。
shirohebi_chain.cbank 2 的包裝檔,包含完整的共用 chain 實作。
shirohebi_collision.c地雷與自身碰撞、碰撞類型判別及身體散開動畫。
shirohebi_apples.c石榴候選位置選擇、收集、計分與增長;內部識別字使用 Apple。
shirohebi_demo.c展示模式轉向、長蛇姿態,以及利用 VBlank 計數器統計經過的計時單位。
shirohebi_render.cOAM 影子緩衝區填入、頭身尾圖塊、優先順序輪換與失敗特效。
shirohebi_display.cLCD/VRAM 初始化、調色盤與字形載入,以及標題與排行榜配置。
shirohebi_background.c遊戲背景格子、地雷及遭覆寫格子的還原。
shirohebi_pause.cSTART/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 輸入旗標與固定 bank 回呼宣告。
shirohebi_lib_bank0.c固定 bank 的 VBlank 輸入/時鐘回呼、共用 WRAM 旗標,並為後續系統及音訊編譯輸入選擇 bank 0。
shirohebi_lib_bank1.c為後續支援程式庫的編譯輸入選擇 bank 1。

修改遊戲時的檢查要點

重新建置後,除錯時使用本次產生的位址表。分別在 DMG 與 CGB 模式下檢查標題、START 提示、移動、轉向、吃水果、自身/地雷碰撞、暫停/繼續、返回確認與排行榜。編譯成功本身無法證明這些操作流程正確,也不能取代實機驗證。

↑ 返回目錄