01はじめに
この解説では、HARAPEKO SHIROHEBIが入力を受け取り、白ヘビを動かし、画面に表示するまでの仕組みを説明します。作者はDAISUKE OBAです。ザクロを食べて白ヘビを伸ばし、地雷と自分の胴体を避けるゲームです。付属のプログラム・自作素材・この解説はMITライセンスで公開します。
画面は160×144ピクセルです。1本のROMでDMGとCGBに対応し、起動時にカラー表示の可否を判定します。構成はMBC5、ROM 64 KiB、バッテリーバックアップRAM 8 KiBです。公開ROMはitch.ioのゲーム紹介ページから入手できます。
読み進め方:まず全体のフローチャートを確認し、頭の移動と胴体の追従を続けて読むと、ヘビの動きを理解できます。ライブラリの一覧からは、利用箇所のソースへ移動できます。ソース内のAppleはザクロ、Blockは地雷を表します。
02ビルドとファイル構成
Rust 1.85以降で、Windows・Linux・macOS ARM・macOS Intel用のコンパイラと全補助ツールをビルドできます。ネイティブ実行ファイルの動作に.NETは不要です。素材処理用の補助ツールもPythonやPillowを必要としません。
PowerShellのサンプルビルドは直下のWindows用EXEを使用します。Linux/macOSでは同じC入力とオプションをネイティブコンパイラに渡すか、PowerShell 7を使用します。CHR画像とCソースは別の入力です。font.chrには元のサンプルフォントを保持しています。
powershell -NoProfile -ExecutionPolicy Bypass -File .\apps\harapeko_shirohebi\build.ps1
ゲーム本体は17個の分割Cファイルを、指定した順序で結合します。音楽、書き出し済み素材、バンク配置用ラッパーは別のコンパイラ入力です。リポジトリのlib/をリンクするので、制作時の素材プロジェクトや書き出しツールは不要です。分割ファイルは結合後の宣言を共有するため、一般的なCビルドに一つずつ追加しないでください。
| 指定・出力 | 内容 |
|---|---|
-KitaqgbRoot | kitaqgb.exeとlib/がある場所を指定。このリポジトリの配置なら省略できます。 |
-OutputDirectory | 出力先。既定値はゲームのout/です。 |
-KeepBuildFiles | 成功時にも結合ソース、音声の一時調整ファイル、コンパイラの診断資料を保持します。 |
shirohebi.gb | 新しく生成したROM。失敗時に残っている別のROMを成功結果と取り違えないでください。 |
shirohebi.map | このビルドのデバッグに使うシンボルアドレス。 |
build_manifest.json | コンパイラとROMのハッシュ、サイズ、割り込みベクタの位置。 |
ビルド後の処理が必要な理由
VBlank音楽割り込みは、どの切り替えバンクを選択している間でも呼び出せる必要があります。スクリプトは割り込みに関係する4シンボルが固定バンク0にあることを確認し、$0040にJPを設定して、カートリッジの2種類のチェックサムを計算し直します。想定外のベクタは上書きしません。音声ソースの一時コピーでは通常の音楽更新を除外し、メインループと割り込みからBGMが二重に進まないようにします。もう一つの一時コピーで補助処理の配置と絶対分岐を調整します。リポジトリのライブラリ本体は変更しません。
入力順とすべてのオプションは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周、デモの長さはVBlank 1,800回で数えます。いずれも固定の実時間と同一視しないでください。
04頭の移動:固定小数点と旋回
KQBody2Dを使うのは頭だけです。KQWorld2Dには有効な物体を1個登録し、重力と接触ソルバーの反復回数を0にします。当たり判定は移動後にゲーム側で行います。座標は1ピクセルを整数16で表すQ4形式なので、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
基本の成分上限は1更新当たり0.5〜1.75ピクセルです。向きは右を0として時計回りの16方向で、下が4、左が8、上が12です。左右操作で目標の向きを1段階変更します。押し続けた旋回は最初に8、その後4のカウントダウンを挟みます。左右同時押しでは旋回しません。
方向ベクトルは上限値・約4分の3・約4分の1・0の成分で表します。実際の速度は各軸とも1更新で最大Q4の3だけ目標に近づきます。これは整数による軽い近似で、正規化した三角関数のベクトルではありません。そのため、表示上の向きが変わってから移動方向が追いつきます。
kq2d_step後、頭の中心がX=4より小さければ156、156より大きければ4へ移ります。Yは4と140を使います。その後一度だけピクセルへ丸め、描画と衝突で同じ座標を使います。この頭の境界処理と、胴体の160・144を法とする座標の折り返しは別の規則です。
05白ヘビの胴体が追従する仕組み
ChainBodyにはSND_TraceX・SND_TraceY・SND_TraceDirを直接渡します。これらは各節の現在の位置と向きを保持する配列で、頭が通った座標の履歴ではありません。当たり判定と描画も同じ配列を読みます。開始時は頭と3節、最大で頭を含む31個になります。
- 配列の0番へ頭のピクセル座標と向きを書き込み、その向きを
pullに入れます。 - 節
iの変更前の向きを保存します。更新済みの直前の節の位置から、向きに応じた小さな間隔を引いて目標位置を求めます。 - 画面端をまたぐ最短のX・Y差分を求め、下表に従って各軸を0・1・2ピクセル動かします。
- 節の向きを
pullにし、新座標を画面内へ折り返します。保存しておいた変更前の向きを、次の節へ渡します。
| 符号付き差分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だけ動いて157になります。現在位置がX=0なら最短差分は−2となり、−1移動して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番以降の節について、折り返しを考慮した最短差分が両軸とも絶対値3未満なら衝突。頭のすぐ後ろの節は除外します。 |
| ザクロ | 頭と中心の通常の座標差が両軸とも絶対値7未満なら取得。加点・成長を行い、次の候補を選びます。ここでは端の最短差分を使いません。 |
これらは軸に平行な範囲判定で、円の距離計算ではありません。ザクロは固定の16候補を一定順序で巡ります。地雷から必要なタイル距離は得点0・10・20・30に応じて4・3・2・1へ小さくなり、同じタイルは常に除外します。探索は最大16候補で終了します。得点は8ビット値なので、無制限のカウンターではありません。
自分の胴体に当たった場合は、その姿勢を止めて色を変化させます。地雷に当たった場合は、固定小数点の速度と時間差で加わる重力で節を飛ばし、順に破裂させて跡を表示します。ミスタイマーが終了すると、ランク入りなら名前入力、それ以外は再挑戦へ進みます。CGBでは色、DMGでは濃淡のパレットを使います。
07描画:タイルとスプライトの表示順
初期化は安全な表示タイミングを待ち、LCDを停止してVRAMを初期化し、タイル・フォント・パレットを転送してから表示を開始します。CGBでは背景属性とカラーパレット、DMGでは濃淡レジスタを使います。素材の背景マップは横20タイルですが、本体のタイルマップは横32を1行とするため、添字を混同しないことが大切です。
ループ内では、まずRAM上のシャドーOAMにスプライトを配置します。座標には本体の規則でX+8・Y+16を加えます。system_wait_vblank()後にOAMを反映します。ヘビは0〜30番、文字は残りのスロットを使い、未使用の項目は消します。
先頭6個はOAM上の優先順を固定し、それより後ろは描画ごとに4ずつ位相を進めて表示順を循環させます。これにより、1走査線当たり10スプライトという制限の影響を後ろの節で分け合います。制限そのものをなくしたり、節を削除したりする処理ではありません。最大長を変更するときは、配列の容量とOAM全40項目の割り当てを一緒に確認します。
START・PAUSE表示は、要求・表示中・変更ありの状態を管理します。文字を消すときは、そのセルに現在のザクロがあれば優先して戻し、なければ背景を戻します。単一の空白タイルで消すと、下にあるザクロまで消してしまいます。フォントは1ビットの8行から2ビットの16バイトへ展開し、背景模様より文字の画素を優先して合成します。
shirohebi_render.c / shirohebi_display.c / shirohebi_pause.c
08音楽とVBlankの役割分担
固定バンクの音楽データはaudio_vblank.cが読みます。通常の1レコードは待機時間、CH1、CH2、CH3、CH4の順で、音源はスイープ付きパルス、パルス、波形、ノイズです。チャンネルの意味は実機の順序と一致します。BGMはAudioVBlank_PlayMusic・AudioVBlank_Stop・AudioVBlank_SetEnabledで制御します。
通常のaudio.cは効果音を担当します。ビルド時の一時的なdefineにより、このドライバの音楽更新は除外します。効果音は__bankofでバンクを指定し、優先度を付けて再生します。地雷の演出にはCH4の直接ノイズ設定も使います。一時停止はBGMと効果音の両方を調整します。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、1バイトの得点10個、名前30文字です。外側の長さ・チェックサムの検証はsaveライブラリが担当します。読み込みは5バイト・15バイトの得点形式も認識し、書き込みは42バイト形式を使います。
0点でなくランク入りする得点を降順に挿入します。比較は「より大きい」なので、同点なら既存の同点記録の後ろに入ります。得点と名前は一緒に移動します。新しい記録はRAM上でAAAとして準備し、プレイヤーが3文字の名前を確定した時点でSRAMへ書きます。保留中の結果を破棄する場合は、保存済みのランキングを読み直します。
上下でA〜Z・ピリオドを選び、右/SELECTで次の文字へ循環移動、左で前の文字へ循環移動します。Bは先頭以外にいるときだけ前へ戻ります。Aは次の位置へ進み、最後の文字で確定します。STARTはどの位置からでも確定できます。入力を一度離す仕組みにより、その確定ボタンでリトライまで選んでしまうことを防ぎます。
10KITAQGBライブラリの使い方
ライブラリの実装とヘッダーは同じリポジトリの組み合わせを使います。ゲーム固有のラッパーや設定も、この利用例の一部です。特にバンク配置と音声の役割分担を併せて確認してください。
| ライブラリ | 入口と利用目的 | 利用箇所 |
|---|---|---|
system.c | system_init, system_wait_vblankシステム初期化と表示タイミングの同期。音声IRQフックは別に設定します。 | shirohebi_main.c |
chain.c | chain_body_init, chain_body_reset, chain_body_step, chain_body_grow寿命の長い配列を結び付け、姿勢を作り、頭に追従し、尾を増やします。履歴リング型のChain APIは使いません。 | shirohebi_snake.c |
physics2d.c | kq2d_world_init, kq2d_stepQ4の頭1個を積分。重力0、接触ソルバーの反復0を設定します。 | 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_SetPausedVBlank音楽と調整しながら行う効果音の再生・優先度・停止管理。 | 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_paletteDMGの濃淡レジスタ設定と併せて、CGBの色を設定します。 | 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両方の音声経路が必要とするGBハードウェアレジスタの定義を提供します。 | shirohebi_audio.c |
ライブラリと併用する組み込み関数
__readpadex(previous)は下位バイトに押下中、上位バイトに新規押下を返します。__cgb_is_cgb()でカラー対応を判定します。__settile・__settileattrなどで背景セルを更新し、__bankofで効果音データのROMバンクを取得します。これらはコンパイラの機能で、別のC実装をゲームへコピーするものではありません。
11ソース一覧と変更時の確認
| ファイル | 担当する処理 |
|---|---|
shirohebi_common.c | 型・定数・共有変数・関数の前方宣言。変数の意味を調べるときの入口です。 |
shirohebi_main.c | 起動処理と入力→更新→描画準備→VBlankのメインループ。 |
shirohebi_flow.c | 状態ごとの振り分け、タイトル設定、開始演出、プレイ、一時停止、ミス、名前入力、再挑戦。 |
shirohebi_input.c | ボタンを離すまでの入力抑止、タイトルのショートカット、タイトルに戻る確認。 |
shirohebi_snake.c | 頭の物理計算、加速、相対旋回、画面端の移動、胴体の初期化・追従・成長。 |
shirohebi_chain.c | 共通chain実装全体をバンク2に配置するラッパー。 |
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 | ランキングの検証・挿入、3文字の名前、SRAMへの保存。 |
shirohebi_audio.c | BGM・効果音の初期化、開始・停止、設定、一時停止、チャンネルの調整。 |
music.c | 固定ROMに置く開始ジングル・メイン曲・ゲームオーバー曲のVBlank音楽レコード。 |
sfx.c | ゲーム側の音声関数が選択する効果音のバイト列。 |
text_tiles.c | フォントのマスクと、ゲームボーイの2ビットタイル行への変換。 |
shirohebi_assets.c | 書き出し済みのタイル・マップ・属性・衝突/イベント配列と、マップ定義・補助関数。 |
shirohebi_input_shared.h | 割り込みと共有するWRAM入力フラグと固定バンクのコールバックの宣言。 |
shirohebi_lib_bank0.c | 固定バンクのVBlank入力・時刻コールバック、共有WRAMフラグ、後続のシステム・音声入力をバンク0に置く指定。 |
shirohebi_lib_bank1.c | 後続の補助ライブラリ入力をバンク1に置く指定。 |
ゲームを変更するとき
- 速度・旋回:Q4定数を調整し、低速での旋回と上入力の加速を確認します。頭の表示方向と実際の速度方向は一致するとは限りません。
- 長さ:配列容量・chainの容量・OAMの予約を一緒に変更します。尾の地雷・自己衝突と、走査線のスプライト制限も確認します。
- 画像:タイル番号、マップの行幅、CGB属性を揃えます。文字表示を消すときは、その下の現在のセルを復元します。
- 音声:割り込みの固定バンク配置と、音楽の時計を進める処理が一つであることを保ちます。通常再生に加え、一時停止・再開も確認します。
- 保存:データ配置を変更するなら形式番号を変更し、添字で読む前に長さを検証します。未使用・不正なSRAMも確認します。
ビルド後は、そのビルドで生成したマップを使用します。タイトル、開始演出、移動、旋回、ザクロ取得、自分の胴体・地雷への衝突、一時停止・再開、タイトルへの復帰確認、ランキングを両方の本体モードで確認してください。コンパイル成功だけで、これらの経路や実機動作を検証できたことにはなりません。