見出し画像

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ファイルをエミューレータで実行してみましょう。

SimpleBGScrollというサンプルを実行しました。文字が流れてくるだけのサンプルです。

ソースコードをいじってみる

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;
}


この記事が気に入ったらサポートをしてみませんか?