Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】How I fit a bubble shooter into 3,810 bytes of C64 assembly

2026年10月01日に「Dev.to」が公開したITニュース「How I fit a bubble shooter into 3,810 bytes of C64 assembly」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

C64という古いPCで、わずか4KBの容量にバブルシューター「TinyBubbles」を実装した話。アセンブリ言語を使い、スタックやハードウェアを徹底的に活用し、メモリを極限まで節約する様々な技術的工夫で開発コンペに優勝した。

ITニュース解説

コモドール64(C64)という1980年代の古いコンピューターで、たった4KBという現代では考えられないほどの小さな容量に、本格的なバブルシューターゲームを収めた技術的な挑戦が、あるチャリティコンテストで行われた。このコンテストのルールは非常に厳しく、ゲーム全体が4KB、つまり4096バイトに収まらなければならないというものだった。その中で「TinyBubbles」というゲームが、わずか3,810バイトで見事に優勝を飾った。これは、コンピューターが直接理解できる「6502アセンブリ言語」という非常に低レベルな言語で書かれているため、一つ一つの命令を綿密に制御し、容量を徹底的に削減する工夫が凝らされている。

まず、ゲームがどこにロードされるかという点から工夫が始まる。C64のメモリは、$0000番地から始まるが、通常プログラムが使わない$0120から$0FFFまでのアドレス空間にゲーム全体が収められている。この領域は、コンピューターの基本的な動作に必要な「スタック」や「システムルーチン」、そして「画面表示のためのメモリ」などが配置されている場所であり、普通は触らない領域だ。TinyBubblesは、この通常使われない隙間を巧みに利用してメモリを確保している。例えば、画面のレイアウトは$0400番地から始まる画面表示用のメモリに直接ロードされ、カスタムの文字セット(ゲーム内で使う文字や絵柄)もその直後の$0800番地に配置される。残りの領域には、ゲームのプログラムコードやデータテーブルが詰め込まれ、あらゆるものを再利用する発想で容量を節約している。

さらに、ゲームの起動方法にも特別な工夫がある。通常、C64でプログラムをロードした後は「RUN」と入力して実行するが、TinyBubblesは「RUN」コマンドを不要にしている。これは、ゲームがロードされる際に、スタックという一時的にデータを保存する領域が上書きされることを逆手に取ったものだ。ゲームのプログラムファイル内に特定のバイト列を仕込み、$01F8番地というスタックの一部を書き換える。これにより、ファイルロードが完了した際に、本来であればKernal(C64の基本的なOSのようなもの)がスタックから読み出す「戻り先アドレス」が、ゲームの開始アドレスに書き換えられる。結果として、「LOAD"*",8,1」というロード命令を実行するだけでゲームが即座に起動する。この方法で「RUN」コマンドや、BASIC言語で書かれた起動用の小さなプログラムを省くことができ、貴重なバイトを節約している。ただし、この方法はKernalの重要なルーチンが格納されているメモリ領域も上書きしてしまうため、ゲームのプログラムにはそれらのルーチンのデフォルト値も含まれており、ロード中にKernalが問題なく動作し続けるよう配慮されている。

ゲームのメインループも非常に独特だ。全ての準備が整った後、ゲームのメインループはたった1つの命令で構成されている。「jmp *」という命令は、自分自身に常にジャンプし続けるという意味で、実質的には何もしない。では、ゲームはどのように動いているのかというと、ゲームの全ての処理(ジョイスティック入力、弾の軌道、衝突判定、バブルを消す処理、スコア計算、サウンド)は、C64の「ラスタ割り込み」という特殊な仕組みを使って実行されている。ラスタ割り込みとは、画面の表示走査線(ラスタースキャンライン)が特定の場所に来たときにコンピューターが一時的にメインの処理を中断し、別の処理を実行する仕組みだ。TinyBubblesでは、この割り込みを1フレームにつき1回、画面の252行目で発生させ、その中でゲームの全てのロジックを処理する。この割り込み処理は、Registersというコンピューターの状態を保存する領域を自身で復元し、Kernalの割り込み処理(キーボードスキャンやカーソル点滅など)が一切実行されないようにする。これにより、ゲームの背後で不要な処理が一切実行されず、全ての処理能力をゲームに集中させることができる。

4KBという容量の中でプログラムを開発するには、可能な限りコンピューターのハードウェアの機能を活用することが重要になる。TinyBubblesも、チップの能力を最大限に引き出している。例えば、飛んでいるバブルがボード上の他のバブルに衝突したかどうかを判定する際に、距離計算の複雑なルーチンは使っていない。代わりに、C64のグラフィックチップであるVIC-IIの機能を利用する。飛んでくるバブルは「スプライト」というハードウェアで制御されるキャラクター、ボード上のバブルは通常の「キャラクター」で描画されており、VIC-IIはスプライトが背景のピクセルに触れた瞬間に特定のレジスタ($D01F)のビットを立てる機能を持っている。ゲームは、このレジスタを読み取るだけで衝突を検知し、衝突したバブルを最も近いグリッドの位置に固定する。

ランダムな数値を生成する際も、複雑な擬似乱数生成アルゴリズムは使われていない。C64のサウンドチップであるSIDチップの機能が利用されている。SIDチップの3番目のボイス(音色)を「ホワイトノイズ」に設定し、音を鳴らさないようにゲートを閉じた状態で、最大周波数で動作させる。そして、$D41Bというレジスタを読み取ることで、このノイズオシレータの出力値を取得し、それを乱数として利用する。この乱数で、次に発射されるバブルの色や、バブルが弾けるときの音の高さなどを決定している。

ボード上のバブルも、実は「スプライト」ではなく通常の「キャラクター」として描画されている。1つのバブルは4つのマルチカラーキャラクター($0A〜$0D)で構成され、全てのバブルが同じ4つのキャラクターパターンを共有している。それぞれのバブルの色は、カラーRAMというメモリに書き込まれる色情報によって区別される。スプライトは限られた数しか使えないため、実際にスプライトとして使われているのは、プレイヤーが発射するバブル、照準器、そして発射台の2つの部分のみで、これでスプライトのリソースを大幅に節約している。文字セットも、ゲームに必要な48種類のキャラクター(レンガ、フレーム、数字、画面に表示される最小限の文字)だけに厳選されている。さらに徹底的に容量を削るため、「SCORE」の「O」や「HISCORE」の「I」は、実際には数字の「0」や「1」のキャラクターを代用している。

ゲームボードは、ハチの巣のように互い違いに配置された8x10の六角形グリッドを採用している。このグリッドの状態は、「ゼロページ」というC64の最も高速にアクセスできるメモリ領域に、1セルあたり1バイトで格納される。この1バイトは、バブルが空かどうか、バブルの色、そして処理中に一時的にセルをマークするためのビット情報を保持している。ゲームプレイの中核をなすのは、2種類の「洪水塗りつぶし(Flood Fill)」と呼ばれるアルゴリズムだ。1つは、バブルが発射された後に同じ色のつながったバブルを探し、3つ以上つながっていればそれらを消す処理。もう1つは、消された後に天井とつながっていないバブルを探し、それらを落下させる処理だ。この2つの処理は、隣接する6つのバブルを訪問するルーチンを共有しており、間接ジャンプを使って呼び出すことで、同じロジックを重複して書く必要がなく、容量をさらに節約している。

アセンブリ言語での再帰処理(関数が自分自身を呼び出すこと)は、スタックという一時的なメモリ領域を消費する。C64のスタックは非常に小さく、TinyBubblesの初期化中はわずか32バイトに制限されている。しかし、ゲームが実行されると、初期化コードが占めていた領域を含むページ1全体をスタックとして利用できるようになる。それでもスタックオーバーフロー(スタックの容量を超えること)のリスクがあるため、再帰関数は、次に深く再帰する前にスタックに十分な空きがあるかをチェックする仕組みが組み込まれている。もしスタックに十分な空きがなければ、それ以上深く再帰せずに処理を中断する。

照準器は、アセンブラが事前に計算したサインカーブのテーブルを使って、画面上を滑らかに移動する。このサインカーブのデータは、手作業で入力するのではなく、アセンブラがビルド時に自動で計算して生成する。バブルが発射されると、バブルから照準器までのベクトル(方向と速さ)が、固定小数点数という形式の速度データに変換される。これは16ビットのデータで、小数点以下5ビット分の精度を持つことで、ピクセルよりも細かいサブピクセルレベルでの滑らかな動きを実現する。フレームごとにこの速度をバブルの位置に加算していき、バブルが画面の左右の壁に当たると、水平方向の速度の符号を反転させるだけで跳ね返りを表現している。

他にも、小さな容量削減の工夫が数多く凝らされている。レベルデータは、1バイトに2つのバブルの色情報を詰め込むことで(1つの色情報が4ビット)、圧縮されている。1つのレベルは24バイトに収まり、ロード時に展開されてグリッドに配置される。ゲームには3つのレベルがあり、それらをループしてプレイする仕組みだ。スコア表示も効率化されている。スコアは10進数で1桁ずつバイトとして格納されており、C64の文字セットのコード0から9がちょうど数字の文字に対応しているため、スコアの値をそのまま画面表示用のメモリにコピーするだけで表示できる。レベルカウンターに至っては、画面表示用のメモリに直接インクリメント(値を増やす)処理を行うことで、さらに効率化している。スプライトの画像データは、ゲームがディスクからロードされた後には不要になる「テープバッファ領域」($0380と$03C0)に配置されており、この領域を再利用することでメモリを節約している。さらに、初期化ルーチンの中には、自身のコードの一部を動的に書き換えることで、コードを短縮する工夫も見られる。これは、画面の行を1つずつ処理する「sta」命令のオペランド(処理対象のアドレス)を、ループごとに変更していくという高度なテクニックだ。

サウンドエフェクトも、SIDチップのレジスタを直接操作して実現されている。バブルが弾ける「ポップ」音は、ボイス1でランダムな高さの三角波を生成し、これは乱数生成に使ったノイズオシレータの出力を利用している。バブルの「爆発」音は、ボイス2でノイズを鳴らすことで表現。レベルクリア時の「ベル」音は、ボイス1の音をボイス3でリング変調(音と音を掛け合わせることで、独特な音色を作る)するという、SIDチップの高度な機能を使っている。

これらの驚くべき技術は、数年前に行われた開発中に筆者が見つけた古いUSBメモリから、ソースコードが再発見されたことで明らかになった。現代のアセンブラ(KickAssembler 5.16)を使ってビルドすると、当時の最終ビルドとバイト単位で完全に一致することが確認されている。現在、このプロジェクトはオープンソースとして公開されており、詳細なREADMEファイルとともに、現代のブラウザで手軽にプレイできるバージョンも提供されている。当時のC64の知識と技術の粋を集めた、容量との戦いの結晶と言えるだろう。

関連コンテンツ

関連IT用語