GBAゲームを自作して遊ぶ方法【環境構築~サンプル実行編】
GBA(ゲームボーイアドバンス)のプログラミングが楽しいので、今回は先っちょだけやり方を紹介します。
開発環境を構築して、サンプルを実行するところまで解説します。
GBA開発に必要な前提知識
C/C++がちょっとわかる
【推奨】Makefileが書ける
【推奨】gitがわかる
devkitProのインストール
devkitProという非常にありがたい非公式の開発キットが存在します。これを使います。これを使わない方法もあるようですが、それはこれを使ってみてから考えても遅くないです。
現在(2022/12時点)では、githubで公開されているようです。
これをダウンロードして実行しましょう。
https://github.com/devkitPro/installer/releases
開発環境はこれだけで整います。スゴイ!
インストールを終えると、PCに「MSys2」なるターミナルが入ってます。
Windowsの検索機能で探すと出てきます。
実行すると、ターミナルが起動します。
このターミナルを通さなくてもmakeの実行環境がある人は大丈夫かも。
サンプルプロジェクトをmakeしてgbaファイルを生成する
以下URLからサンプルプロジェクトをgit cloneして落としてきます。
このリポジトリにいくつかのサンプルがあるので、実装の参考になります。
cloneできたら、ターミナルでサンプルプロジェクトのルートまで移動して、makeを実行します。
> make
これだけで、中にあるサンプルが再帰的にmakeされて
gbaファイルを生成してくれます。
gbaファイルは、いわゆるROMと呼ばれるファイルで、これをエミュレータに食わせるかカートリッジに焼き込んで実行します。
makeすると、各サンプルのディレクトリの中にgbaファイルがあると思います。
PC上でGBAファイルを実行してくれる素敵なフリーソフトがあります。
いわゆるエミュレータです。
ここではオススメエミュレータを2つほど紹介します。
VisualBoyAdvance-M
特徴
他のエミュレータより実機の動作に近い(気がする)
※エミュレータでは動作するのに実機で動作しないことも多々あります。後述のエミュレータより少し入力ラグがある
OAMViewer/PaletteViewerなど、デバッグに使える機能がある
SkyEmu
特徴
VisualBoyAdvance-Mよりも入力ラグが少なくサクサク動作する
開発向けというより、快適に遊びたい人向けな雰囲気
エミューレータでサンプルを実行する
先程makeしたgbaファイルをエミューレータで実行してみましょう。
ソースコードをいじってみる
GBAの開発には、VSCodeがオススメです。僕のVSCodeのカラーテーマがキモいことには触れないでください。
VSCodeを以下コマンドで起動しましょう。
code サンプルプロジェクトのパス
souce/main.c
このサンプルのソースコードは、このファイル1つだけです。実際にはlibgbaというdevkitProによるサポートを受けているのですが、libgbaの中身については興味があったらgithubで公開されているので見てみてください。
それでは楽しいGBA開発ライフを。
【オマケ①】VSCodeで補完が効くようにする
今時、補完が効かないなんて嫌ですよね。
C++のExtensionを導入して設定すれば、ライブラリコードまでしっかり補完してくれます。
libgbaをcloneして配置
まず、libgbaをgit cloneして、C:\devkitPro以下に配置します。
もともと存在するlibgbaはリネームしてとっておいてください。
そして、もとのlibgbaから以下のファイルをlibgba/lib以下にコピーします。
libfat.a
libmm.a
(追記:2022/12/23)
このあとlibgbaをmakeしてlibgba.aを生成してください。
main.cをmain.cppにリネーム
こうすると、c++コンパイラでコンパイルしてくれます
C/C++を導入
この拡張機能の導入後、includeのエラーにカーソルをあわせると、ちいさな豆電球が出てきて、エラーを修正する方法をサジェストしてくれます。素敵。
Edit "includePath" setting を選択しましょう。
そうすると、プロジェクトのルートに .vscode というディレクトリが生成されて、中をみると c_cpp_properties.json が爆誕しています。
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"${vcpkgRoot}/arm64-windows-static/include",
"${vcpkgRoot}/x64-windows/include",
"${vcpkgRoot}/x64-windows-static/include",
"${vcpkgRoot}/x86-windows/include"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.19041.0",
"compilerPath": "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.30.30705/bin/Hostx64/x64/cl.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-msvc-x64",
"configurationProvider": "ms-vscode.makefile-tools"
}
],
"version": 4
}
このファイルの includePath, compilerPath を以下のように設定します。
絶対パス直書きでゴメンナサイ
"includePath": [
"C:\\devkitPro\\libgba\\include",
"compilerPath": "C:\\devkitPro\\devkitARM\\bin\\arm-none-eabi-c++.exe",
これを設定すると、main.cppのエラーが消えているはずです。そして補完もしっかり行ってくれます。
【オマケ②】GBAでコルーチンが使えて草
なんと、GBAという古の環境かと思いきや、C++20が使えます
つまりコルーチンが使える!?
ゲームの実装にコルーチンがあると便利なこと、結構ありますよね。
フレームまたいで文字送りしたいときとか。
そんなときにはまさしくコルーチンの出番です。
Makefileにて -fcoroutines を追記
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -fcoroutines
coroutine.h
以下のようなヘッダーファイルを作ると、コルーチンが使えるようになります。C++コルーチンについての知識が浅いので、各関数の解説は省きますが、これで一応エミュレータ上では動きました。実機だとどうなんだろう。
(追記:実機でもちゃんと動作しました)
#pragma once
#include <coroutine>
#include <iostream>
struct generator
{
struct promise_type;
using handle = std::coroutine_handle<promise_type>;
struct promise_type
{
int current_value;
static auto get_return_object_on_allocation_failure() { return generator{nullptr}; }
auto get_return_object() { return generator{handle::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
auto yield_value(int value)
{
current_value = value;
return std::suspend_always{};
}
};
bool move_next() { return coro ? (coro.resume(), !coro.done()) : false; }
int current_value() { return coro.promise().current_value; }
generator(generator const &) = delete;
generator(generator &&rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
~generator()
{
if (coro)
coro.destroy();
}
private:
generator(handle h) : coro(h) {}
handle coro;
};
実装
STEP 1. クラスにコルーチンを定義
generator *m_pCoroutine;
STEP 2. コルーチン実装
generator Test()
{
for (u32 i = 0; i < 60; i++)
{
co_yield 0;
}
}
STEP 3. クラス変数の初期化でコルーチンを確保
Hoge::Hoge() : m_pCoroutine{new generator(Test())}
STEP 4. Update関数で呼び出し
この例では、コルーチンが終了したらdeleteし、nullptrを代入して
以降呼び出されないようにしています
if (m_pCoroutine && !m_pCoroutine->move_next())
{
m_pCoroutine = nullptr;
}