GBStudio とは
gb-studio: A quick and easy to use drag and drop retro game creator for your favourite handheld video game system
GBStudio はプログラミング不要でGUI上でパーツをドラッグ&ドロップしていくことで、ゲームボーイのソフトを開発できる、最近流行りの NoCode(ノーコード) なソフトです。
chrismaltby さんが作っているOSSで、ソフトウェアの開発には React+Redux と electron を使用しています。
こちらの動画が、実際にゲームを作っている様子の参考になると思います。
GB Studio Tutorial 1: Getting Started
今回は GBStudio のバージョン1.2.1 の内部構造を解説していき、どのようにしてGUIソフトで作ったゲームがゲームボーイのソフトにビルドされるのかみていこうと思います。
💾 GBソフトについて
あまり難しく捉えなくて大丈夫です。
普通のコンピュータやOSなどで、elf形式など特別なフォーマットのファイルが実行ファイルとして実行できるように、ゲームボーイのソフト(以下 GBソフト)の実態はゲームボーイというコンピュータ上で実行できるフォーマットの実行ファイル(以下 GBファイル)です。
GBファイルを専用の記憶ディスク(CDのようなもの)に書き込んだのがGBのカセットです。
GBStudioが吐き出すのは、このGBファイルです。作ったGBファイルは市販のフラッシュカートリッジに書き込めば、GBカセットとしてGB本体で遊ぶことができます。
🧰 GBDK
GBStudioのワークフローをみていく前に知っておいて欲しいツールがあります。それが GBDK です。
GBDKは GB Development kit の略でGBファイルを作るためのSDKです。
GBDK は 内部にGBのアーキテクチャ向けにC言語をコンパイルできる組み込みデバイス向けのCコンパイラ SDCC を内包しており、 GBDK が提供する線やバンク切り替え、ボタン入力などGB開発に便利なAPIとコンパイラを使って比較的お手軽にGBファイルをC言語で作成することができます。
📁 ディレクトリ構成
GBStudioのレポジトリのディレクトリ構成はおおまかに次のようになっています。
- appData/
- buildTools/
- src/
appData
エミュレータやゲームエンジンなど、作成したゲームを遊ぶのに必要なものが入っています。
ファイル | 内容 |
---|---|
js-emulator/ | JS製のGBエミュレータをフォークして改造したもの 記事後半で説明 |
src/ | C言語を解釈するGB用のゲームエンジン ZGBを改造したもの |
templates/ | サンプルROMのアセットデータが入っている |
buildTools
GBDK が丸ごと入っています。 上述の appData/src/Makefile
から呼び出されGBファイルをビルドするのに利用されます。
src
GBStudioの本体です。
Reactのディレクトリ構成をメインとしており、electronのビルドに対応しています。
Javascriptメインで書かれています。
npmモジュール
利用しているnpmモジュールについて
npmモジュール | 概要 |
---|---|
react | アプリ構築 |
redux | ステート管理 |
gbdkjs | 未使用 |
ggbgfx | 画像をGameBoyで利用するグラフィックフォーマットに変換するライブラリ |
electron-notarize | Apple公証をパスするためのコード署名ライブラリ |
about-window | electronのウィンドウメニューに"About This App"を簡単に導入するためのモジュール |
ライブラリ
利用しているライブラリについて
lib/以下に存在
ライブラリ | 概要 |
---|---|
l10n | 多言語対応 |
ggbgfx | 画像をGameBoyで利用するグラフィックフォーマットに変換するライブラリ |
ファイル一覧
ファイル | 内容 |
---|---|
actions/ | reduxのActionを定義 |
assets/ | GBStudioのアプリで使うアセットを定義 |
components/ | Presentational Component(見た目を担当) |
containers/ | Container Component(ロジックを担当) |
hooks/ | Electron Forgeのhooks (Reactのhooksとは無関係) |
lang/ | 多言語対応 |
lib/ | npmパッケージ化されていないスクリプトや独自のライブラリ |
middleware/ | reduxのmiddlewareを定義 |
reducers/ | reduxのReducerを定義 |
store/ | reduxのStoreを定義 |
styles/ | CSS |
windows/ | プロジェクトの土台となるhtmlを定義している? |
consts.js | 定数定義 |
index.js | electronアプリ本体を定義 |
menu.js | electronアプリのウィンドウのメニューバーを定義 |
💎 アセット
GBStudioで作れるGBソフトは version1.2.1 時点ではRPG風のゲームのみになっています。(version2ではもっと増える模様)
GBStudioではユーザーは以下のゲーム内のアセットを組み合わせてゲームを作っていくことになります。
- tileset タイルという8*8pxのグラフィックデータの集合体
- background タイルを組み合わせて作った背景データ
- sprite 人物などのオブジェクト
- scene backgroundとspriteを組み合わせた実際のゲーム画面
- string 会話などのテキストデータ
- music 音声データ
- script イベントなどのスクリプト処理
⛏ Workflow
アプリの操作からゲームボーイのソフト(以下 GBソフト)になるまでのワークフローです。
アプリ -> gbsproj -> JSのオブジェクト -> Cのコード -> GBファイル
ビルドツールとビルド対象がフローの中でごっちゃになっていますが、そこはご容赦ください。
1. アプリ -> gbsproj
gbsprojは GBStudio の中で使われている jsonファイル で、ユーザーがGUI操作を通じて作成したゲームは全部このファイルに特別なJSONフォーマットで書き込まれていきます。
{
"scene": [
{
"id": "ID",
"name": "シーン名",
"backgroundId": "背景のタイルデータID",
"x": -1,
"y": -1,
"width": "画面X幅のタイル数",
"height": "画面Y幅のタイル数",
"actors": [{
"id": "ID",
"spriteSheetId": "スプライトのタイルデータID",
"x": "ActorのいるX座標(タイル単位)",
"y": "ActorのいるY座標(タイル単位)",
"movementType": "動くかどうか",
"direction": "向いている方向",
"script": [
// Aボタンを押すと次のイベントを実行する
{
"id": "7ba26d3c-c1ca-46c8-a4ae-ed0a431b1d03",
"command": "EVENT_TEXT", // テキスト表示
"args": {
"text": "話掛けるとしゃべるテキスト1枚目"
}
},
{
"id": "1cf687c1-e1fe-4e89-a7ef-9ddc6412ef4f",
"command": "EVENT_TEXT", // テキスト表示
"args": {
"text": "話掛けるとしゃべるテキスト2枚目"
}
},
{
"id": "a50d16e9-2660-4fde-94c2-825e6fe799ee",
"command": "EVENT_ACTOR_SET_POSITION", // Actorの位置を変更
"args": {
"actorId": "対象のActor",
"x": "X座標(タイル)",
"y": "Y座標(タイル)"
}
},
{
"id": "f5569ecd-cf2f-43e0-93ff-f6ccc1392f41",
"command": "EVENT_END" // イベント終了
}
],
"frame": 0
}],
"triggers": [],
"collisions": [0, 0, 0, 0],
"script": []
}
]
}
gbsprojファイルを保存しておけば、たとえ端末が変わってもgbsprojをインポートすれば続きからゲーム開発が再生できます。
2. gbsproj -> JSのオブジェクト
gbsproj は JSONファイルなので、そのままJSのオブジェクトとしてパースすればそのまま使えますが、次のステップでC言語のコードに変換しやすいように、GBDKのC言語にそったフォーマットに変換していきます。
この工程は src/lib/compiler/compileData.js
の precompile
という関数で行われます。
3. JSのオブジェクト -> Cのコード
gbsprojの内容をJSオブジェクト化(precompile)したら、GBファイルにGBDKがコンパイルできるように C言語のソースファイルを吐き出す作業になります。
ここで注意したいのは、GBStudioでは、GBDKのAPIをさらにラップしたものをゲームエンジンとして利用しています。(詳しくは割愛。ZGBというGBDKをラップしたゲームエンジンをさらにカスタマイズしたもの appData/src にファイルの実体があります。)
これらの処理は src/lib/compiler/compileData.js
の中の compile
で行われています。 上述の precompile
処理はこの関数の先頭で呼ばれています。
compile
では、
- BankedDataインスタンス を作成
-
compileEntityEvents
で precompileされたオブジェクトをバイト列に変換 - BankedDataインスタンスの
push
メソッドでバイト列を適したバンクに仕分けしていく - 最後にC言語のコードとして出力して終了
という手順でコンパイルが行われる。
実際に中を覗いてみると、
exportCData() {
return (
this.data.map((data, index) => {
const bank = this.dataWriteBanks[index];
const bankData = cIntArray(`bank_${bank}_data`, data);
return `#pragma bank=${bank}\n\n${bankData}\n`;
}).filter(i => i)
);
}
など確かにC言語のコードを出力している部分があります。
4. C言語のコード -> GBファイル
この時点で、C言語のソースファイルが得られています。
あとは、上述のGBDKでGBファイルにCをコンパイルするだけ...というわけにはならないです。
GBStudioでは、GBDKをさらにラップした独自のゲームエンジンを使っています。
このゲームエンジンの規定したフォーマットでC言語を書くことで、ゲームエンジンとGBDKを合わせてGBソフトをビルドすることが可能になっています。
このゲームエンジンのためのC言語は、かなりフォーマットが単純です。
そのおかげでJSからCのコードを吐き出すことが、割と単純にできるようになっています。
実際に中身をみてみましょう。
.
├── src
│ ├── bank_6.c
│ ├── bank_7.c
│ └── data_ptrs.c
├── music
│ └── music_7ced013b0.c
└── *.c, *.s
src/ と music/ 以外はゲームエンジン部分です。 バンク切り替えやスプライトの衝突処理、スクロールなど、低レベルなハードウェア操作を担当してくれています。
OSに近いものだと考えるといいかもしれません。
src/ と music/ にユーザーが作成したゲームアセットやスクリプト処理が記述されており、それらをゲームエンジン部分がゲームにしています。
GBStudioが単純なRPGゲームしか作成できないのは、このゲームエンジン部分がRPGゲームにしか対応していないのが理由になっています。
src
-
bank_6.c
,bank_7.c
各バンクのデータ -
data_ptrs.c
各アセットデータへのポインタ
バンク5まではゲームエンジンに使用されるため、ユーザーが作成したアセットデータのために利用されるのはバンク6以降からとなっています。 まずは bank_6.c
の中身をみていきましょう。
#pragma bank=6
const unsigned char bank_6_data[] = {
0x00,0x08,0x01,0x28,0x00,0x00,0x01,0x00,0x00,0x00,...};
見ての通りただのバイトデータです。 この中にユーザーの定義したアセットデータや、スクリプト処理が格納されているのですが、人間がパッとみただけではわかりません。 bank_7.c
も同様にバイトデータが並んでいるだけです。
ファイル先頭の#pragma bank=6
という記述はCコンパイラに、このファイルのデータは GBソフトのBank6部分に配置してねということを伝える役割を持っています。
次に data_ptr.c
をみていきましょう。
#pragma bank=5
#include "data_ptrs.h"
#include "banks.h"
const unsigned char (*bank_data_ptrs[])[] = {
0,0,0,0,0,0,&bank_6_data,&bank_7_data
};
const BANK_PTR tileset_bank_ptrs[] = {
{0x06,0x0561},{0x06,0x1152}
};
const BANK_PTR background_bank_ptrs[] = {
{0x06,0x16D3},{0x06,0x183E},{0x06,0x19A9},{0x06,0x1B14},{0x06,0x1C7F},{0x06,0x2082},{0x06,0x2485},{0x06,0x25F0}
};
const BANK_PTR sprite_bank_ptrs[] = {
{0x06,0x3C93},{0x06,0x3CD4},{0x06,0x3D95},{0x06,0x3DD6},{0x06,0x3E57},{0x06,0x3F58},{0x07,0x0000},{0x07,0x00C1},{0x07,0x0182},{0x07,0x0303},{0x07,0x0484},{0x07,0x04C5},{0x07,0x0506},{0x07,0x0547},{0x07,0x05C8},{0x07,0x0609}
};
const BANK_PTR scene_bank_ptrs[] = {
{0x07,0x064A},{0x07,0x074C},{0x07,0x07DC},{0x07,0x085E},{0x07,0x08F5},{0x07,0x092B},{0x07,0x0961},{0x07,0x0A36}
};
const BANK_PTR string_bank_ptrs[] = {
{0x06,0x01EC},{0x06,0x0202},{0x06,0x0222},{0x06,0x0241},{0x06,0x0248},{0x06,0x026C},{0x06,0x028A},{0x06,0x02A6},{0x06,0x02CA},{0x06,0x02E7},{0x06,0x030B},{0x06,0x031D},{0x06,0x033C},{0x06,0x0365},{0x06,0x0387},{0x06,0x03A5},{0x06,0x03C7},{0x06,0x03E6},{0x06,0x040D},{0x06,0x0429},{0x06,0x0440},{0x06,0x045A},{0x06,0x047C},{0x06,0x049F},{0x06,0x04B2},{0x06,0x04C9},{0x06,0x04F0},{0x06,0x0512},{0x06,0x051C},{0x06,0x053F}
};
const BANK_PTR avatar_bank_ptrs[] = {
{0x00,0x0000}
};
const unsigned char * music_tracks[] = {
music_7ced013b0_Data, 0
};
const unsigned char music_banks[] = {
8, 0
};
unsigned char script_variables[16] = { 0 };
data_ptr.c
は、上述した bank_6.c
や bank_7.c
のバイト列のバンク番号やオフセットを保持することです。
これは、オブジェクトや、スクリプト処理など、ゲーム内アセットのオフセットです。
これのおかげで無意味だったバイト列をゲームエンジンは意味のあるものとして解釈することが可能になっています。
例えば、 変数tileset_bank_ptrs
は タイルセット(タイルという8*8pxのグラフィックデータの集合体) が、bank_6_dataの 0x0561番目 と 0x1152番目から始まることを意味しています。
music
音楽データが入っています。
srcで使用したバンクの次のバンクから配置されます。
ビルド
これらのC言語のプロジェクトフォルダに対して、 src/lib/compiler/makeBuild.js
の makeBuild
関数を呼び出すことでビルドを行います。
makeBuild
関数では、 appData/src/gb/Makefile
用の makeコマンドを作ってそれを実行することで、 GBDK内の Cコンパイラによって GBファイルが出力されます。
長くなりましたがこれにてGBファイルの完成です!
💻 ブラウザ対応
GBStudioは、GBファイルとしてゲームを吐き出すだけでなく、作ったゲームをブラウザで遊んでもらうことも可能にしています。
makeBuild
関数には buildType
という引数があり、これに "web"を渡す(実際にはユーザーがGUIで指定)と作ったゲームがブラウザで遊べるようになります。
これは appData/js-emulator
の中にあるブラウザ上で動くGBエミュレータで作成したGBファイルを動かすことでブラウザ上で作ったゲームを遊べるようにしています。
🍬 gbdkjs
見たところ、現在は使われていませんでしたが package.jsonの dependencies に入っているため昔使われていたと思われる gbdkjs
についてもついでに調べてみました。
これは、GBStudioと同じく、 chrismaltby さんが作った npmパッケージです。 (Github)
gbdkjs
は レポジトリの説明書きにも Emscripten bindings for GBDK
と書いてあるように、GBDK向けのC言語で書かれたGBソフトのソースコードを、ブラウザ上で実行できるJavascriptにトランスパイルするものです。 おそらく昔は、ブラウザ上でのGBソフトの実行にこちらを使っていたと思われますが、ライセンスかなんらかの理由で素直なエミュレータ方式に修正したのだと思います。
ワークフローとしては
- emcc: GBDK仕様のC -> LLVM bitcode
- emcc: LLVM bitcode -> Javascript
となっています。
このとき吐き出されるJavascriptは、gbdkjsに内包されたGBエミュレータのようなもので動くようになっています。
ユーザーからはあたかもC言語のソースコードがJavascriptのWebAppになったように見えますが、正直、中途半端なJSのコードとエミュレータを使うなら、普通にブラウザ上で動く正確なGBエミュレータに GBファイルそのまま食わした方がいいのではと思いました。(作者もそう思ったのでしょうか...)
🎁 最後に
プログラミング不要でGUI操作だけからGBソフトが作れるという魔法のようなソフトですが、中をみてみると意外と単純でした。
RPGという形式に特化したゲームエンジンのおかげでC言語のフォーマットがかなり単純になっているのが、個人的には重要なポイントかなと思います。
GBStudio2.0ではRPG以外にもシューティングゲームなどの2Dアクションゲームに対応するようになっていて、おそらく2.0へのアップデートでゲームエンジン部分を改良したのでしょう。
ゲームエンジン部分は、ZGBというゲームエンジンをカスタマイズして作ったようです。
また、それなりに大きいプロジェクトなのに、Typescriptを使っていないのも少し驚きました。
GBStudio2.0も時間があればみてみようと思いました。
追記・修正などはいつでもウェルカムです。