
2025/12/19 22:00
**タイトル** *7日間で3 KBのカスタムバイトコードVMを使ってゲームを作る* --- ### 概要 - **目的:** 7日以内に遊べるゲームを完成させる。 - **制約:** プロジェクト全体のサイズは3 kBを超えてはいけない。 --- ### 計画 1. **VM の設計** - 最小限の命令セット(ロード、ストア、加算、ジャンプ)を定義。 - C か Rust で簡易インタプリタループを実装。 2. **バイトコード形式の作成** - 固定長オペコード+任意のオペランド。 - ファイルサイズを抑えるためにコンパクトなエンコーディングを採用。 3. **ゲームロジックの開発** - 「○×ゲーム」や「スネーク」のようなシンプルなゲームを選択。 - 高水準スクリプトは使わず、バイトコードだけでロジックを書く。 4. **リソースのパッキング** - スプライト・サウンドなどを生データとして格納。 - 必要なら LZ77 で圧縮し、制限内に収める。 5. **テスト & 最適化** - 各 VM 命令の単体テスト実行。 - メモリ使用量をプロファイルし、不要コードパスを削除。 6. **ドキュメント** - VM アーキテクチャとバイトコード組み立て方法を簡潔にまとめた README を作成。 --- ### 成果物 - `vm.c` / `vm.rs`: インタプリタソース(≈ 1 kB)。 - `game.bytecode`: コンパイル済みゲームスクリプト(≤ 500 bytes)。 - `assets.bin`: 圧縮リソース。 - `README.md`: 使用手順。 ---
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
Langjam Gamejam向けに、無限再生のシューティングゲームを実行する3 kB Windows 実行ファイルを作成しました。エンジンのコアはC++で書かれた小型カスタムバイトコード仮想マシンで、ゲームロジックはF#コンパイラによってfloat32配列にコンパイルされるミニマリスティックなCライク言語で記述されています。バイトコード設計: 値は1バイト定数(0–255)または2バイト浮動小数点数としてエンコードされ、スタック・レジスタ・型タグを排除しています。
のような変数はスコア用に配列スロットへ直接マップされます。state[5]データ構造:
配列はセル 0 に個数を格納し、最後の要素と入れ替えることで O(1) で削除します。missilesレンダリング: 全画面GLSLピクセルシェーダが前フレームとノイズ関数のフィードバックブレンドを行い、ShaderToy風のビジュアル効果を実現しています。
ライブコーディングワークフロー: ソースを編集するとコンパイラがバイトコードを書き出し、C++ランタイムは毎フレームそれをリロードします。またシェーダも編集時に自動再読み込みされ、即座にビジュアルフィードバックが得られます。
ゲームデザイン: 初期状態で3体の敵から開始し、7 秒ごとに1体追加。敵は決して死亡せず、ヒット時に画面外へテレポートします。想定プレイ時間は30–60 秒で、Super Hexagon の高速リスタートスタイルを反映しています。
サイズ比較: ロジックのC++移植版はバイトコード版より90 バイト大きかったものの、インタプリタオーバーヘッドがあるにも関わらず全体的にサイズ削減が達成されています。
背景と動機付け: デモシーンのサイズコーディングプロジェクト(Ikadalawampu の4 kBデモ、kkrieger の96 kBシューティング)や Langjam Gamejam が推奨するカスタム言語に触発されました。ソースコードはGitHubとitch.ioで公開されており、YouTube にゲームプレイのキャプチャが掲載されています。
これにより、極めて小型の実行ファイルでも魅力的なインタラクティブ体験を提供できることを示し、ホビ開発者や教育者へのインスピレーションとなります。
本文
数日前、私は小さなカスタムバイトコードVMを埋め込み、フルスクリーンのピクセルシェーダでグラフィックスを描画することでシューティングゲームを作成しました。結果として、3 kB の Windows 実行ファイルが完成しました。
これは Langjam Gamejam という7日間チャレンジ用に作られたものです。この大会ではまずプログラミング言語を作り、その後その言語でゲームを開発します。
このプロジェクトは、言語ツール、ゲーム開発、手続き的グラフィックス、そしてデモシーン風のサイズ制限という私の興味が組み合わさったものです。大会形式によりスコープを小さく保ち、新しいアイディアを探求することが強いられました—それに加えてとても楽しかったです!
コードは GitHub に公開していますし、itch.io でも見ることができます。便利のため、YouTube のスクリーンショットも添付します。
背景
ゲームジャムについて最初に聞いたとき、私はすぐに興味を持ちました。数日間、新しい言語で恩恵を受けるようなゲームコンセプトを見つけるのは難しいと考えていました(TIS‑100 のようなプログラミングゲーム以外)。その後、カスタムバイトコードを使ってサイズを小さくしたデモシーン作品――2010 年に Amiga 上で 4 kB で動作する Ikadalawampu を思い出しました。
私はまだ懐疑的でした:インタプリタを埋め込むだけでコードを縮小できるのか? 試してみるしかありませんでした。
もう一つのインスピレーションは、2004 年にリリースされた 96 kB の FPS ゲーム kkrieger です。それ以降、サイズが極端に小さい優れたビデオゲームはほとんど見られません。私はずっとこの分野を探求したいと思っていたので、ジャムは実験的作業の言い訳になるようなものに感じました。
計画
- 言語設計
- バイトコードへコンパイルするコンパイラ(F#)
- バイトコードインタプリタ(C++)
- カスタム言語でシューティングゲームを作成
- 1つの GLSL シェーダーでグラフィックスを描画
設計はサイズ重視で行いましたが、最適化に時間を費やしたくありませんでした。もともとゲームは 4–8 kB に収まると想定し、プロジェクト名 shmup8 としました。実際の実行ファイルは期待より小さくなり(音楽と3Dグラフィックスを除外したため)、シェーダーコードはミニファイされ、実行ファイルは Crinkler で圧縮しています。
ライブコーディングワークフロー
即時のビジュアルフィードバックがあるとコーディングは楽しくなります。C++ を毎回再コンパイルすることなく、ゲームロジックとビジュアルをすべて書きたいと思いました。アイデアは次の通りです:実行ファイルを一度起動し、その後ライブリロードで全てを反復します。
- IDE でソースコードを編集するとカスタムコンパイラが走り、バイトコードをファイルにダンプ
- C++ プロジェクトは毎フレームそのバイトコードを再読み込み
- GLSL シェーダーも自動的にリロード
初期開発のスクリーンショットでは、左上がゲーム実行中、左下がカスタム言語、右下が GLSL シェーダー、右上がコンソールログです。カスタム言語はシェーダーへ表示用データを送るため、2つのライブコーディング環境があるとイテレーションが非常に容易になります。
創造的な場面では結果が予測しづらいため、迅速な反復は生産性に不可欠です。
バイトコード設計
バイトコードとシェーダー間の通信を float 配列で行うことにしました。ミニマリズムを重視して、
float32 のみを扱います。すべての値は配列に格納され、ローカル変数はその配列内のスロットです。インデックスも float で表し、インタプリタが int にキャストします。条件判定は浮動小数点で行い、0.5 を超えると真となります。
バイトコードは以下の2種類の命令しかありません:
- 配列中のセルを更新
- 条件付き(あるいは無条件)ジャンプ
式は他のセルや
sin のような関数を参照する複雑な数学式でも構いません。0〜255 までの定数は1 バイトで格納し、その他の float は私が考案した2バイトトリックを使用します。これによりスタックやレジスター、型タグを排除し、インタプリタとバイトコードをコンパクトに保ちます。
言語設計
ミニマルなバイトコードの制限はあるものの、構文糖衣で使いやすくしています。C ライクな構文で代入、
if 条件、while ループを実装しました。糖衣は拡張代入と for ループを提供します。
コンパイラが変数を検出すると、その変数に float 配列内の位置を割り当てます。シェーダーへ共有したい値には特定の位置を指定します。例:
state[5] が現在のスコアを保持するようにします。読みやすさのため、インラインサポートも追加しました:
inline score = state[5];
これで
score を使って state[5] への読書きが可能になります。
イテレーション中には必要な機能を追加したり、制限を回避するために工夫したりします。例えば
&& の代わりに乗算を使い条件を書いたりします(値は 0 と 1)。
ミサイル位置を格納する配列
missiles を使用し、最初のセルには画面上のミサイル数を保持します。ループなしで要素を削除するために、最後の要素と入れ替える方法を採用しています:
// 画面外のミサイルを削除 if (missiles[i*2 + 2] > 0.5) { // O(1) 削除:配列内の最後の要素と交換 missiles[i*2 + 1] = missiles[(missiles[0] - 1)*2 + 1]; // position.x missiles[i*2 + 2] = missiles[(missiles[0] - 1)*2 + 2]; // position.y missiles[0] -= 1; }
(ファイルの残りも参照)
シェーダーグラフィックス
ShaderToy に似ており、ゲームエンジンから渡されたデータに基づいてピクセル色を計算します。時間が限られていたため、グラフィックはシンプルに保ちました。前フレームと現在フレームをブレンドしたフィードバック効果とノイズ関数を組み合わせることでビジュアルに興味深さを追加しました。
ゲームデザイン
ゲームは無限です。開始時に3体の敵が登場し、7 秒ごとに1体ずつ増えていきます。3 種類の敵がおり、それぞれ独自の挙動とビジュアルを持ちます。敵は死亡せず、ミサイルが当たると画面外へテレポートし、戻ってくることがあります—これによりコードは簡潔で難易度は上昇します。
Super Hexagon のように高速リスタートを設計し、リプレイを促進しています。ゲームの長さは 30〜60 秒程度になると予想されます。
結論
迅速な反復ワークフローが不可欠でした。バイトコードとゲームを並行して設計することで、どの機能が最初に必要か予測できませんでした。制約は実際にゲームを書き始めてから明らかになりました。
バイトコードが本当にコンパイル済み C++ より小さいか疑問に思いました。ゲームロジックを C++ に移植し、インタプリタを削除してサイズを確認した結果、純粋な C++ バージョンは 90 バイト大きくなりました。つまりバイトコードの節約効果がインタプリタオーバーヘッドを上回ります。数字は多少ばらつきがあるので慎重に解釈してください—どちらも完全に最適化されていませんでした。
全体として、このプロジェクトは楽しく、期待以上に動作し、多くのことを学びました。将来的にもゲーム開発実験を続けるつもりです。