役演亭 -Yakuentei- Roleplay with your own characters.
  1. ホーム
  2. 技術記事
  3. ゲーム開発

【暫定】RPG Maker Unite のアドオンからゲーム動作に介入する方法

公開日 (Published) : 更新日 (Modified) :

従来ツクールのように、既存ゲーム動作に介入するための手法を調べた暫定記事です。

【暫定】アドオン用 C# Script から RPG Maker Unite ゲーム動作に介入

RPG Maker Unite 発売から間もない中、アドオンからゲーム本体の動作に介入する手法を調べた暫定的な内容をまとめています。

現時点 (2023-06-29) の状況

2023-06-27 に公開された公式サンプルゲーム「復讐する王子と竜が残した剣」の作者 (Toya Shiwasu 様) の手法により、IL2CPP 環境下でもメソッド差し替えができるようになりました!

既に本記事が肥大化しているため、上記の記事に分割しましたので、興味のある方は是非ご確認下さい!

本記事で引き続き公開している ExchangeFunctionPointer() を使用する方法は、iOS 版など IL2CPP が必要なプラットフォームでは動きませんので、IL2CPP 対応が必要なアドオン作者の方は上記の記事をご確認下さい。

  • 「シーンを跨いで常駐し、実行中の複数シーンの動作に介入」の方は問題ありませんので、引き続きご利用下さい。

本記事を執筆するにあたって情報、アドバイスなどをいただいた方々

  • VRYGON 様、瑞祥様 (フレームレートのオーバーレイ表示など、Unity の描画周りのテクニックに関して)
  • 紫苑もみじ様 (ポインター差替による既存メソッドの上書きに関して)
  • Agoaj 様 (上記ポインター差替を行ったアドオンの作者)
  • トリナー様 (IL2CPP で発生する問題とその現象に関して)
  • ぽん太様 (RPG Maker Unite の不具合とその対策を GitHub に整理)
  • Toya Shiwasu 様 (公式サンプルゲーム作者であり、ソースコード解析による既存メソッドの上書きに関して)

目次

まえがき (対象読者など)

本記事は、従来の RPG ツクール (具体的には XP から MZ までの、RGSSJavaScript をベースに動いていたツクール) でスクリプト素材プラグインを作られていた方で、かつ RPG Maker Unite でもアドオンを作りたいと考えている方向けの暫定情報記事です。

既にアドオンについて調べた方はご存じかもしれませんが、本記事の執筆時点 (2023-05-11) では、公式の情報源は主に以下の 2 つのみ (公式アドオン数 4 個) です。

加えて、上記で紹介されているアドオンは、大別すると以下の 2 種類のみです。

  1. カスタムイベント命令の追加
  2. エディター機能の追加

つまり、本来アドオンに求められる「実行中のゲーム動作を書き換える」手段を実現するための、公式による情報提供が「無い」と言っても過言ではない状況です。

しかし、発売直後であり情報も少ないことから、今の筆者にできるレベルでもある程度の貢献はできると考え、暫定的な内容ではありますが、記事化を断行することにしました。

本記事で紹介するゲーム動作介入アドオンの機能

本記事で、実際の C# Script とともに紹介するアドオンは、以下の機能を持ちます。

  • 既存のメソッドを直接オーバーライド (=差し替え)
    • GameBattlerBase.SkillMPCost() を直接オーバーライドし、MP 消費を 500 加算する。
  • シーンを跨いで常駐し、実行中の複数シーンの動作に介入
    • 画面上部に、現在のフレームレート (FPS) を表示する。
    • 戦闘画面でスペースキーが押されたとき、強制的に戦闘勝利扱いにする。
    • マップ画面でスペースキーが押されたとき、強制的にゲームオーバー扱いにする。

シーンを跨いで常駐し、動作に介入するアドオン

アドオン用 C# Script ファイルの作成・貼り付け

既に公式のアドオン作成ドキュメントを読まれた方には不要かと思いますが、アドオン用の C# Script ファイルを作成するところから始めていきます。

本記事のアドオンを簡単に (=コピペで) 導入できるようにするため、C# Script のファイル名と保存先パスを、上記公式ドキュメントと同じものにして「中身だけを変える」方針とします。

AddonSample フォルダの作成

公式ドキュメントの「スクリプト作成」と同じ場所・同じ名前でフォルダを作成します。

つまり、プロジェクトフォルダ内の「Asset/RPGMaker/Codebase/Add-ons」フォルダ配下に「AddonSample」という名前のフォルダを作成して下さい。

(以下の画像も公式ドキュメントから引用させていただきました。)

AddonSample フォルダの作成

C# Script (AddonSample.cs) の作成

公式ドキュメントの「スクリプト作成」と同じ場所・同じ名前で C# Script を作成します。

つまり、先ほど作成したフォルダー内にフォルダ名と同じCsファイルを作成します。

(以下の画像も公式ドキュメントから引用させていただきました。)

C# Script (AddonSample.cs) の作成

C# Script (AddonSample.cs) の中に貼り付けるコードの中身

肝となる部分の解説は後で行いますので、とりあえず下記コードを丸ごとコピペして下さい。

また、日本語の文字化けを避けるため、文字コードは「UTF-8」として保存して下さい。

using System;
using System.Reflection;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using TMPro;
using RPGMaker.Codebase.Runtime.Addon;
using RPGMaker.Codebase.Runtime.Map;
using RPGMaker.Codebase.Runtime.Battle;
using RPGMaker.Codebase.Runtime.Battle.Objects;

#if UNITY_EDITOR
// [Unity Editor 時専用処理] 下記 namespace は Editor 専用
using UnityEditor;
#endif

/*:
* 
*/

/*:ja
 * @addondesc 既存メソッドのオーバーライドと、シーンを跨ぐ常駐の例
 * @author 鈴木YE(役演亭)
 * @help <機能>
 *  ・既存メソッドのオーバーライド
 *    ・スキルの MP 消費コストに 500 を加算。
 *  ・シーンを跨ぐ常駐
 *    ・フレームレート (FPS) 表示を行う。
 *    ・戦闘シーンのみ、スペース押下で強制勝利。
 *    ・マップシーンのみ、スペース押下で強制ゲームオーバー。
 * @url https://yakuentei.jp
 * 
 * @param isDispFPS
 * @text フレームレート (FPS) 表示 ON/OFF
 * @desc フレームレート (FPS) の表示 ON/OFF を設定します。
 * @type boolean
 * @default true
 * @on FPS 表示 ON
 * @off FPS 表示 OFF
 */

namespace RPGMaker.Codebase.Addon
{
    public class AddonSample
    {
        public static bool IsDispFPS { get; private set; }
        private static PropertyInfo _infoMcr;

        // コンストラクタ (アドオン有効化時に呼ばれる)
        public AddonSample(bool isDispFPS)
        {
            Debug.Log("[DEBUG] AddonSample Activated.");
            IsDispFPS = isDispFPS;

            // 「既存メソッドのオーバーライド」の初期化
            InitializeOverrideAddon();

            // 「シーンを跨ぐ常駐」の初期化
            InitializeResidentAddon();
        }

        // ■■■■■ 「既存メソッドのオーバーライド」関連 ■■■■■

        private void InitializeOverrideAddon()
        {
            // GameBattlerBase.SkillMpCost() のオーバーライド
            OverrideMethod(
                typeof(GameBattlerBase), "SkillMpCost",
                BindingFlags.Public | BindingFlags.Instance,
                typeof(AddonSample), "SkillMpCost_Override",
                BindingFlags.Public | BindingFlags.Instance);

            // GameBattlerBase.Mcr (MP 消費率) への参照情報の取得
            _infoMcr = GetPropertyInfo(
                typeof(GameBattlerBase), "Mcr",
                BindingFlags.Public | BindingFlags.Instance);
        }

        // RPG Maker Unite 公式の
        // AddonInstance.ExchangeFunctionPointer() を呼び出して
        // メソッドのオーバーライド (ポインター差替) を行うメソッド
        private static void OverrideMethod(
            Type orgType, string orgName, BindingFlags orgFlags,
            Type newType, string newName, BindingFlags newFlags)
        {
            // 指定された型のクラスから、メソッドに関する情報を
            // C# (.NET) の MethodInfo 型として取り出す。
            var orgInfo = orgType.GetMethod(orgName, orgFlags);
            var newInfo = newType.GetMethod(newName, newFlags);

            if (orgInfo != null && newInfo != null)
            {
                AddonInstance.ExchangeFunctionPointer(orgInfo, newInfo);
                Debug.Log("[Override Succeeded] name = " + orgName +
                    ", orgInfo = " + orgInfo + ", newInfo = " + newInfo);
            }
            else
            {
                // いずれかのメソッド情報を取り出せなかった場合は
                // オーバーライド (ポインター差替) できないのでエラー
                Debug.LogError("[Override FAILED] name = " + orgName +
                    ", orgInfo = " + orgInfo + ", newInfo = " + newInfo);
            }
        }

        // 指定した型のクラスから、プロパティに関する情報を
        // C# (.NET) の PropertyInfo 型として取り出すメソッド
        private static PropertyInfo GetPropertyInfo(
            Type orgType, string orgName, BindingFlags orgFlags)
        {
            var orgInfo = orgType.GetProperty(orgName, orgFlags);

            if (orgInfo != null)
            {
                Debug.Log("[GetPropertyInfo Succeeded] name = " +
                    orgName + ", orgInfo = " + orgInfo);
            }
            else
            {
                // プロパティ情報を取り出せなかった場合はエラー
                Debug.LogError("[GetPropertyInfo FAILED] name = " +
                    orgName + ", orgInfo = " + orgInfo);
            }
            return orgInfo;
        }

        // GameBattlerBase.SkillMpCost() の置き換え先メソッドの定義
        public int SkillMpCost_Override(GameItem item)
        {
            // オリジナルの MP 消費計算結果に 500 を加算
            return (int)Math.Floor(
                item.MpCost * (double)_infoMcr.GetValue(this)) + 500;
        }

        // ■■■■■ 「シーンを跨ぐ常駐」関連 ■■■■■

        private void InitializeResidentAddon()
        {
#if UNITY_EDITOR
            // [Unity Editor 時専用処理]
            // Play ボタンが押されたときに
            // アドオンの実行時クラスを登録
            EditorApplication.playModeStateChanged +=
                OnPlayModeStateChanged;
#else
            // [実配布時 (例:Windows なら exe) 専用処理]
            // 既にゲーム起動済のはずなので、直ちに登録
            CreateAddonSampleRunner();
#endif
        }

#if UNITY_EDITOR
        // [Unity Editor 時専用処理]
        // Play ボタンが押されたとき用の
        // アドオンの実行時クラスを登録処理
        private static void OnPlayModeStateChanged(
            PlayModeStateChange state)
        {
            if (state == PlayModeStateChange.EnteredPlayMode)
            {
                CreateAddonSampleRunner();
            }
        }
#endif

        // アドオンの実行時クラスを登録する処理
        private static void CreateAddonSampleRunner()
        {
            UnityEngine.Object.DontDestroyOnLoad(
                new GameObject("AddonSampleRunner")
                .AddComponent<AddonSampleRunner>());
        }
    }

    // ■■■■■ 「シーンを跨ぐ常駐」で使用するクラス ■■■■■

    public class AddonSampleRunner : MonoBehaviour
    {
        TextMeshProUGUI _fpsText;

        // ゲーム開始時の処理
        private void Start()
        {
            Debug.Log("[DEBUG] AddonSampleRunner Started.");

            // FPS 表示用キャンバスを作成
            var fpsCanvasParent =
                new GameObject("AddonSampleRunner - Canvas");
            var fpsCanvas =
                fpsCanvasParent.AddComponent<Canvas>();
            var fpsScaler =
                fpsCanvasParent.AddComponent<CanvasScaler>();
            fpsCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
            fpsCanvas.sortingOrder = 10000; // 他の Unite UI より優先
            fpsScaler.uiScaleMode =
                CanvasScaler.ScaleMode.ScaleWithScreenSize;
            fpsScaler.screenMatchMode =
                CanvasScaler.ScreenMatchMode.Expand;
            fpsScaler.referenceResolution = new(1920, 1080);

            // FPS 表示用テキストオブジェクトを作成
            _fpsText = new GameObject("AddonSampleRunner - FPS Disp")
                .AddComponent<TextMeshProUGUI>();
            _fpsText.fontSize = 128;
            _fpsText.fontWeight = FontWeight.Bold;
            _fpsText.alignment = TextAlignmentOptions.Center;
            _fpsText.enableWordWrapping = false;
            _fpsText.color = new(0, 1, 0, 1);
            _fpsText.outlineColor = new(255, 0, 0, 255);
            _fpsText.outlineWidth = 0.1f;
            _fpsText.fontSharedMaterial.EnableKeyword("OUTLINE_ON");
            _fpsText.UpdateMeshPadding();

            // FPS キャンバスの下にテキストオブジェクトをぶら下げる
            _fpsText.transform.SetParent(fpsCanvas.transform);
            _fpsText.rectTransform.localPosition = new Vector3(0, 480);

            // シーンが変わっても FPS 表示が破棄されないよう抑制
            // ※ シーンの直下にぶら下がるキャンバスのみ登録で OK
            DontDestroyOnLoad(fpsCanvasParent);
        }

        // 毎フレーム呼ばれる処理
        void Update()
        {
            // FPS 表示
            if (AddonSample.IsDispFPS)
            {
                _fpsText.text = "Frame Rate = " +
                    Mathf.RoundToInt(1.0f / Time.deltaTime) + " FPS";
            }

            // スペース押下チェック
            if (Input.GetKeyDown(KeyCode.Space))
            {
                if ((BattleManager.IsBattle ||
                     BattleManager.IsBattleTest()) &&
                    !BattleManager.IsBusy())
                {
                    // 戦闘時 (テスト戦闘含む) は強制勝利
                    BattleManager.ProcessVictory();
                }
                else if (
                    SceneManager.GetActiveScene().name == "SceneMap")
                {
                    // マップシーンのときは強制ゲームオーバー
                    // ※ Unite だと戦闘時も SceneMap がアクティブ
                    //   なので、先に戦闘時判定が必要。
                    MapManager.ShowGameOver();
                }
            }
        }
    }
}

アドオンの有効化・動作確認

アドオンの有効化

公式ドキュメントの「アドオン有効化」と同じ手順で有効化します。

ざっくりと 1 枚にまとめると以下のようになります。

アドオンの有効化方法

アドオンを動かす前に…

本アドオンでは、スキルの消費 MP を書き換えているため、例えばアクター「クラフト」のレベルを 99 に設定するなどして、MP 消費型のスキル (必殺技) が使える状態にします。

アクター「クラフト」のレベルを 99 にする

アドオンの動作確認

本アドオンに含まれる 各機能 (オーバーライド、シーンを跨ぐ常駐) が正しく動いていることを順番に確認していきます。

実行開始とフレームレート (FPS) 表示の確認

実行開始とフレームレート (FPS) 表示の確認

ニューゲームの選択

ニューゲームの選択

最初の会話イベントをスキップし、外に出る

最初の会話イベントをスキップし、外に出る

メニューを開き、スキル (必殺技) の消費 MP 500 加算を確認

メニューを開き、スキル (必殺技) の消費 MP 500 加算を確認

町の外に出る

町の外に出る

フィールドを歩いて敵とエンカウント

フィールドを歩いて敵とエンカウント

エンカウント後、スペースキーを押下 → 強制勝利

エンカウント後、スペースキーを押下 → 「強制勝利」を確認

戦闘終了後、スペースキーを押下 → 強制ゲームオーバー

戦闘終了後、スペースキーを押下 → 「強制ゲームオーバー」を確認

肝となる処理内容の解説

ソースコード中にコメントも入れたため、1 行ずつの解説は行いませんが、重要な箇所や、技術的に難しいと思われる箇所を順番に解説していきます。

#if UNITY_EDITOR

実は、Unity の API (ツクールで言うランタイム API に相当) の中には、開発環境である Unity Editor 上でしか呼び出せないものが大量に存在します。

  • 例えば Unity Editor 上では、Play ボタン (再生マーク) を押すことでゲームのデバッグ実行を開始することができますが、このときのイベントを拾うための API も Unity Editor 専用です。

これらの API を含めた状態で、リリース版のビルド (例えば Windows 用の exe) を行うと、問答無用でビルドエラーになります。

なので、例えば以下の箇所のように、Unity Editor 使用時のデバッグルートと、本番用のリリースルートを #if, #else, #endif 等を使ってビルド分岐する必要があります。

#if UNITY_EDITOR
// [Unity Editor 時専用処理] 下記 namespace は Editor 専用
using UnityEditor;
#endif

コンストラクターについて

コンストラクター (下記) が呼ばれるタイミングに注意が必要です。

// コンストラクタ (アドオン有効化時に呼ばれる)
public AddonSample(bool isDispFPS)
{
    Debug.Log("[DEBUG] AddonSample Activated.");
    IsDispFPS = isDispFPS;

    // 「既存メソッドのオーバーライド」の初期化
    InitializeOverrideAddon();

    // 「シーンを跨ぐ常駐」の初期化
    InitializeResidentAddon();
}

実際に exe 等として配布するゲームでは、ゲームの起動直後に呼ばれるため問題になることは少ないですが、Unity Editor 上での開発時は、ダイアログ上でアドオンを有効化した時点で直ちに呼ばれます。

裏を返すと、Play ボタンを押すたびに呼んでくれる訳ではないので、「シーンを跨ぐ常駐」では後述の小細工が必要です。

Debug.Log(), Debug.LogError() など

上記コンストラクタの中でも使われていますが、これらのメソッドは、Unity の「コンソール」タブ上にデバッグ出力を行うためのメソッドです。

アドオンの実装は何度も試行錯誤が必要になると思われますので、デバッグの手掛かりとなるメッセージを適宜 Debug.Log() 等で出力すると、動作確認するのがぐっと楽になるはずです。

なお、Debug.LogError() を使うと赤×で出力できるため、特に「起こって欲しくない現象」が起こった場合のデバッグ出力で利用するのがおすすめです。

既存メソッド直接オーバーライド (=差し替え)

本箇所については、C# の Reflection 周りの記述が厄介ですので、もう 1 つのアドオンの例 (オーバーライドのみ) も掲載しました。

筆者自身も、マイクロソフトのドキュメントを読むだけでは理解しきれず、実際に試しながら理解を深めましたので、読者の方でも様々なメソッドやフィールド、プロパティなどにアクセスを試みて理解を深めていただくことをおすすめします。

InitializeOverrideAddon() メソッドの中身

コンストラクターから直接呼んでいる、オーバーライド関連の処理の入口です。

オリジナル (GameBattlerBase) の SkillMpCost() を、本クラス (AddonSample) で定義した SkillMpCost_Override() に置き換えるイメージです。

  • 今回差し替え対象となる GameBattlerBase.SkillMpCost() は、public かつ非 static なので BindingFlags.Public | BindingFlags.Instance を指定します。
  • private や protected のメソッドも対象にできます。例えば private (protected) かつ static であれば BindingFlags.NonPublic | BindingFlags.Static のようになります。
// GameBattlerBase.SkillMpCost() のオーバーライド
OverrideMethod(
    typeof(GameBattlerBase), "SkillMpCost",
    BindingFlags.Public | BindingFlags.Instance,
    typeof(AddonSample), "SkillMpCost_Override",
    BindingFlags.Public | BindingFlags.Instance);

なお、GameBattlerBase.SkillMpCost() の途中計算では GameBattlerBase.Mcr プロパティも参照していますので、この情報も併せて取得する必要があります。

// GameBattlerBase.Mcr (MP 消費率) への参照情報の取得
_infoMcr = GetPropertyInfo(
    typeof(GameBattlerBase), "Mcr",
    BindingFlags.Public | BindingFlags.Instance);

OverrideMethod() の中身について

まずは、C# (.NET) の Reflection と呼ばれる機能で提供されている GetMethod() メソッドを使用し、「書き換え元」と「書き換え先」のメソッド情報を取得しています。

// 指定された型のクラスから、メソッドに関する情報を
// C# (.NET) の MethodInfo 型として取り出す。
var orgInfo = orgType.GetMethod(orgName, orgFlags);
var newInfo = newType.GetMethod(newName, newFlags);

これを、RPG Maker Unite のアドオン管理クラス (※ つまり「公式」な手法です) が提供している ExchangeFunctionPointer() メソッドに渡すことで、「関数ポインターを差し替える」という強引な方法で、呼び出されるメソッドの差し替えを実現しています。

そのため、本来 C# の文法上は許されないはずの、Ruby (RGSS) や JavaScript 時代のツクールのような「上書き」型のオーバーライドが実現可能です。

AddonInstance.ExchangeFunctionPointer(orgInfo, newInfo);

GetPropertyInfo() の中身について

やっていることは OverrideMethod() と似ており、C# (.NET) の Reflection と呼ばれる機能で提供されている GetProperty() メソッドを使用し、参照 (読み書き) したいプロパティ情報を取得しています。

ただし、メソッドの場合と異なりオーバーライトする訳ではない (値を読み書きするだけ) ため、ExchangeFunctionPointer() に相当する処理は行いません。

var orgInfo = orgType.GetProperty(orgName, orgFlags);

ちなみに、フィールド (メンバー変数) を読み書きしたい場合は、GetProperty() の代わりに GetField() メソッドを使用します。

オーバーライド後のメソッド SkillMpCost_Override() の定義

オリジナルの GameBattlerBase.SkillMpCost() では、

(int) Math.Floor(item.MpCost * Mcr);

のように計算していますが、上記の式中にある Mcr は AddonSample クラスではなく GameBattlerBase クラスの持ち物なので、直接参照することができません。

そこで、別途、上の方で取得した _infoMcr をもちいた間接的なアクセス (C# の Reflection 機能をもちいたアクセス) を行う必要があります。

// GameBattlerBase.SkillMpCost() の置き換え先メソッドの定義
public int SkillMpCost_Override(GameItem item)
{
    // オリジナルの MP 消費計算結果に 500 を加算
    return (int)Math.Floor(
        item.MpCost * (double)_infoMcr.GetValue(this)) + 500;
}

今回のように値を「取得」する場合は GetValue() メソッドをもちいますが、「設定」する場合は SetValue() メソッドをもちいます。

また、今回のように非 static のメンバーを読み書きする場合は this を、static のメンバーを読み書きする場合は null を渡します。

  • this は、本来の C# 文法上は AddonSample 自体のインスタンスを指すことになります (※ Visual Studio 側の IntelliSense だとこちらを認識するようです) が、AddonInstance.ExchangeFunctionPointer() をもちいて強制的にポインター差し替えを行ったことにより GameBattlerBase 側のインスタンスを指すようになります。

「シーンを跨ぐ常駐」における初期化処理の分岐

先述のとおり、Unity Editor から実行した場合、コンストラクターが Play ボタンを押すたびに呼ばれる訳ではないため、以下のような特殊な分岐を、先述の #if 等を使ってビルド時分岐する必要があります。

        private void InitializeResidentAddon()
        {
#if UNITY_EDITOR
            // [Unity Editor 時専用処理]
            // Play ボタンが押されたときに
            // アドオンの実行時クラスを登録
            EditorApplication.playModeStateChanged +=
                OnPlayModeStateChanged;
#else
            // [実配布時 (例:Windows なら exe) 専用処理]
            // 既にゲーム起動済のはずなので、直ちに登録
            CreateAddonSampleRunner();
#endif
        }

アドオン作者の方では、Unity Editor でのデバッグ実行時に限り、Play ボタン (再生マーク) が押されるたびに実行時用のクラス (実際に処理を行うクラス) を毎回登録するという処理を用意する必要があります。

  • 一方、アドオンでやりたいことが「既存メソッドのオーバーライド」だけであれば、呼び出されるメソッドの書き換えは 1 回だけで済むため、本処置は不要です。

アドオン実行時のクラスの登録

ツクールと同様、Unity にも Scene (シーン) の概念があります。

通常、Unity の各オブジェクト (ゲーム中のキャラクターや UI など) は、異なる Scene にまたがって存在することはできません。Scene 移動の際に自動的に破棄されます。

それを迂回するために、DontDestroyOnLoad() という API が用意されています。

// アドオンの実行時クラスを登録する処理
private static void CreateAddonSampleRunner()
{
    UnityEngine.Object.DontDestroyOnLoad(
        new GameObject("AddonSampleRunner")
        .AddComponent<AddonSampleRunner>());
}

本記事で紹介するアドオンも、ゲーム実行中、複数の Scene にまたがって常駐する必要があるオブジェクトにする必要があるため、DontDestroyOnLoad() の呼び出しが必要です。

MonoBehaviour クラスの継承

Unity の各 Scene 上 (複数 Scene をまたがる場合も含む) で実際に C# のコードを動かす (=ゲームとして実行したときの処理を動かす) ためには、MonoBehaviour というクラスを継承したうえで、シーン上に登録する必要が出てきます。

今回のアドオンも、実際に複数の Scene にまたがって動かなければならない C# コードが必要になりますので、MonoBehaviour を継承しています。

public class AddonSampleRunner : MonoBehaviour

この MonoBehaviour クラスを継承したうえで Scene 上に登録を済ませることで、例えば以下のようなメソッドを Unity 側で自動的に呼んでくれるようになります。

  • Start() メソッド
    • Scene の初期化完了時に 1 回だけ呼ばれます。
    • 今回の AddonSampleRunner のように複数 Scene にまたがる MonoBehaviour の場合は、登録時 (=ゲームの起動直後) の 1 回だけ呼ばれます。
  • Update() メソッド
    • フレームの更新が必要になるたびに呼ばれます。
    • 例えば 60 FPS のゲームなら 1/60 秒毎に呼ばれますし、モニターの垂直同期 (V-Sync) に従う設定であればその間隔に合わせて呼ばれます。

Start() メソッドの中身

前述のとおり、ゲーム起動時に 1 回だけ呼ばれる処理を意味するので、基本的には初期化処理を記述します。

そのため、フレームレート (FPS) 用に文字列を画面表示するためのキャンバスおよびテキストオブジェクトの初期化処理をここに記述しています。

補足 1 : キャンバスサイズの自動調整に関して

RPG Maker Unite 本体側でメニューなどの UI 表示を行う際、基準解像度を 1920x1080 ピクセルとして、画面の拡大・縮小時に文字などの UI サイズや位置を自動的に調整しています。

CanvasScaler によるサイズの自動調整設定

これを実現するために使われている Unity の機能が CanvasScaler オブジェクトです。

var fpsCanvas =
    fpsCanvasParent.AddComponent<Canvas>();
fpsScaler.uiScaleMode =
    CanvasScaler.ScaleMode.ScaleWithScreenSize;
fpsScaler.screenMatchMode =
    CanvasScaler.ScreenMatchMode.Expand;
fpsScaler.referenceResolution = new(1920, 1080);

FPS 表示のフォントサイズの指定 (ピクセル単位) や、座標位置の指定に関しても、画面サイズ 1920x1080 ピクセルを基準として行うことになります。

なお、Unity は 3DCG における「左手座標系」を採用しているため、画面の中央の座標が (0, 0) で、かつ Y 座標はプラス方向が上となりますので、ご注意下さい。

_fpsText.rectTransform.localPosition = new Vector3(0, 480);

今回の FPS 表示のように画面上部に表示させる場合、Y 座標もプラス方向となります。

補足 2 : 文字列オブジェクト (TextMesh Pro) の色指定について

色の RGBA 指定については、Unity だと 0~255 の代わりに 0~1 で設定する場合が多いです。

しかし、レアケースとして以下の outlineColor (縁取りの色) のように 0~255 で指定する API もあるので注意が必要です。

_fpsText.color = new(0, 1, 0, 1);
_fpsText.outlineColor = new(255, 0, 0, 255);

補足 3 : DontDestroyOnLoad()

元締めの AddonSampleRunner クラスと同様、FPS 表示用のオブジェクトに対しても、シーンを跨いで使用するため DontDestroyOnLoad() が必要です。

// シーンが変わっても FPS 表示が破棄されないよう抑制
// ※ シーンの直下にぶら下がるキャンバスのみ登録で OK
DontDestroyOnLoad(fpsCanvasParent);

基本的に、Unity Editor 側の画面 (=RPG Maker Unite ではない方の画面) の「ヒエラルキー」タブに出ているもので、Scene の直下にぶら下がるオブジェクトが登録対象です。

Update() メソッドの中身

フレームレートの表示値の更新や、スペースキーの押下監視など、常に行い続ける必要があるものをこの中に書きます。

今回のアドオンでは、戦闘時とフィールド時でスペースキーの処理内容を切り替える必要がありますから、そのための分岐処理もこの中に入れています。

// 毎フレーム呼ばれる処理
void Update()
{
    // FPS 表示
    if (AddonSample.IsDispFPS)
    {
        _fpsText.text = "Frame Rate = " +
            Mathf.RoundToInt(1.0f / Time.deltaTime) + " FPS";
    }

    // スペース押下チェック
    if (Input.GetKeyDown(KeyCode.Space))
    {
        if ((BattleManager.IsBattle ||
             BattleManager.IsBattleTest()) &&
            !BattleManager.IsBusy())
        {
            // 戦闘時 (テスト戦闘含む) は強制勝利
            BattleManager.ProcessVictory();
        }
        else if (
            SceneManager.GetActiveScene().name == "SceneMap")
        {
            // マップシーンのときは強制ゲームオーバー
            // ※ Unite だと戦闘時も SceneMap がアクティブ
            //   なので、先に戦闘時判定が必要。
            MapManager.ShowGameOver();
        }
    }
}

リリースビルド版 (exe など) で発生する問題に関して

ニューゲームを選択後、長時間固まる

リリースビルド版 (exe など) でニューゲームを選択すると、以下のように暗転したまま、長時間 (筆者の PC では 20 秒以上) 固まる現象が発生します。

これは、アドオンの不具合ではなく、RPG Maker Unite 側の不具合です。

リリースビルド版 (exe など) でニューゲーム直後に発生する硬直

RPG Maker Unite 公式のロードマップによると、本問題の修正予定時期は 2024 年 3 月となっていますので、残念ながら当面の間は公式による修正が見込めないと思われます。

Now Loading 表示による対策

本記事において、FPS 表示の改善方法をご教授していただいた瑞祥さんが、本手法を応用した Now Loading 表示の追加を試みておられます。

当面の間は、このようにアドオン経由などで Now Loading 表示を行い、ユーザーに対して「固まっているわけではない」ことを分かりやすく明示することが、現実的対策の 1 つになると思われます。

補足:リリース用オブジェクト (exe など) をビルドする方法

exe などのリリース用オブジェクトのビルドは、「ファイル」メニューの「ビルド設定」から行います。

ファイル → ビルド設定

Windows 用の exe であれば、そのまま「ビルド」を押せば OK です。(どのフォルダに exe 等の一式を出力されるかが聞かれます)

ただし、RPG Maker Unite 用の exe のビルドは、筆者 PC だと 初回は 15 分ほどかかりましたので、所要時間にあらかじめご注意下さい。

ビルド

ビルドが終わると、以下のように exe とその一式が出力されます。ファイル名は、なぜか 16 進数のような文字列になっています。(おそらく変更はできると思います)

生成される exe ファイル (Windows の場合)

IL2CPP 版でメソッド差し替えが動かない

冒頭に記載のとおり、以下の別記事で紹介している方法をもちいてメソッド差し替えを行う必要があります。

Windows 版 IL2CPP で exe のビルドを行う方法

そもそも IL2CPP って何?

通常の Unity のリリース版ビルド (exe 等) の仕組みは Mono と呼ばれており、異なるプラットフォーム間 (Windows、Android、iOS 等) での互換性を保つために IL (Intermediate Language = 中間言語) という仕組みをもちいてゲームを動作させています。

IL2CPP は、簡単に言うと、上記を各プラットフォームに依存するコードに変換 (厳密にはその一歩手前の CPP = C++ のソースコードを経由してから変換) するためのもので、プラットフォーム依存になる代わりにパフォーマンス (速度など) の向上が見込めるという特徴があります。

そのため、特に、パソコンと比べてスペックに制約を受ける携帯電話 (スマホ) 向けのプラットフォームでは、IL2CPP が推奨される場合もあり、マルチプラットフォーム対応をするうえでは避けては通れない技術です。

IL2CPP で動作確認が必要な理由

本記事の冒頭に記載のとおり、IL2CPP 版だとポインター差し替えによるメソッド上書きが動作しないという制約があります。

RPG Maker Unite の 1 つの売りは「マルチプラットフォーム対応」ですから、iOS 版のように IL2CPP 版でなければ Apple Store で配信できないゲームアプリでも、アドオン作者が作ったアドオンが使用される可能性は高いと言えます。

また、別記事で紹介している方法でメソッド書き換えを行った場合でも、最低限、Windows 版の IL2CPP を使用して手元での動作確認を行っておいた方がベターであることは、言うまでもないでしょう。

Windows 版 IL2CPP の導入とビルド方法

IL2CPP は、標準の Unity 構成ではインストールされないため、Unity Hub の「モジュールを加える」から追加でインストールを行う必要があります。

また、IL2CPP でビルドを行うためには Visual Studio のインストールも必要ですので、普段別の開発環境 (VS Code など) を使っている方は注意が必要です。

Unity Hub の「モジュールを加える」

加えるべきモジュール

Visual Studio が入っていない方は、ここで Visual Studio にチェックを入れます。
(既により新しいバージョンが入っている場合は、基本的には不要です)

モジュール - Visual Studio 2019

Windows Build Support (IL2CPP) にチェックを入れ、インストールを開始します。

モジュール - Windows Build Support (IL2CPP)

IL2CPP でビルドを行うための設定

IL2CPP をインストールしたら、Unity で RPG Maker Unite のプロジェクトを開き、編集 → プロジェクト設定を開きます。

編集 - プロジェクト設定

「プレイヤー」の中の「その他の設定」を展開し、スクリプティングバックエンドを Mono から IL2CPP に変更します。

スクリプティングバックエンド

IL2CPP によるビルドの実行

通常 (Mono) の場合と同様、「ファイル」メニューの「ビルド設定」から行います。

IL2CPP が有効になっている場合、以下のように「IL2CPP コード生成」という項目が表示されているはずです。(設定は変更する必要はありません)

IL2CPP コード生成

上記の表示があることを確認したら、ビルドを開始しましょう。
(既に Mono 版の exe をビルド済の方はフォルダを分けた方が無難です)

IL2CPP でのビルドが終わると、通常 (Mono) 版とは異なり、BackUpThisFolder_ButDontShipItWithYourGame というフォルダが一緒に生成されるはずです。(このフォルダの中に C++ ソースコード一式が生成されていますが、あくまでも中間生成物なので、ゲームを配信する際には本フォルダは不要です)

BackUpThisFolder_ButDontShipItWithYourGame

実行の仕方は、通常 (Mono) 版と同様、exe をダブルクリックで起動できます。

もう 1 つのアドオンの例 (オーバーライドのみ)

オーバーライドを行うアドオンについては、C# (.NET) の Reflection 周りの記述が非常に厄介で、理解するのが大変だと思われます。

そのため、オーバーライドのみを行うアドオンの事例を 1 つ追加しました。

ちょうど公式コミュニティで話題になっていた「プレイヤーの速度変更」の件を題材にしました。

本例のソースコードの解説は、前述の「肝となる処理内容の解説」と内容が被りますので、割愛させていただきます。

プレイヤーの速度変更を行うアドオンのソースコード

同じく AddonSample.cs にそのまま貼り付けることで動作します。

初期値では、通常の 4 倍速にしていますので、好みに応じて適宜ご変更下さい。

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using RPGMaker.Codebase.Runtime.Addon;
using RPGMaker.Codebase.Runtime.Map;
using RPGMaker.Codebase.Runtime.Map.Component.Character;

/*:
* 
*/

/*:ja
 * @addondesc プレイヤーの移動速度を変える
 * @help プレイヤーの移動速度に指定した倍率を掛けます。
 *  ・倍率は、右側のオプション playerSpeedMultiple から変更できます。
 *  ・初期値は 4 倍です。
 * 
 * @param playerSpeedMultiple
 * @text プレイヤーの移動速度の倍率
 * @desc 大きければ大きいほど移動速度が上がります。
 * @type number
 * @default 4
 */

namespace RPGMaker.Codebase.Addon
{
    public class AddonSample
    {
        private static float _playerSpeedMultiple;

        private static FieldInfo _info_actorOnMap;
        private static FieldInfo _info_partyOnMap;

        public AddonSample(double playerSpeedMultiple)
        {
            _playerSpeedMultiple = (float)playerSpeedMultiple;

            OverrideMethod(
                typeof(MapManager),
                "SetDashForAllPlayerCharacters",
                BindingFlags.NonPublic | BindingFlags.Static,
                typeof(AddonSample),
                "SetDashForAllPlayerCharacters_Override",
                BindingFlags.Public | BindingFlags.Static);

            _info_actorOnMap = GetFieldInfo(typeof(MapManager),
                "_actorOnMap",
                BindingFlags.NonPublic | BindingFlags.Static);
            _info_partyOnMap = GetFieldInfo(typeof(MapManager),
                "_partyOnMap",
                BindingFlags.NonPublic | BindingFlags.Static);
        }

        private static void OverrideMethod(
            Type orgType, string orgName, BindingFlags orgFlags,
            Type newType, string newName, BindingFlags newFlags)
        {
            var orgInfo = orgType.GetMethod(orgName, orgFlags);
            var newInfo = newType.GetMethod(newName, newFlags);

            if (orgInfo != null && newInfo != null)
            {
                AddonInstance.ExchangeFunctionPointer(orgInfo, newInfo);
                Debug.Log("[Override Succeeded] name = " + orgName +
                    ", orgInfo = " + orgInfo + ", newInfo = " + newInfo);
            }
            else
            {
                Debug.LogError("[Override FAILED] name = " + orgName +
                    ", orgInfo = " + orgInfo + ", newInfo = " + newInfo);
            }
        }

        private static FieldInfo GetFieldInfo(
            Type orgType, string orgName, BindingFlags orgFlags)
        {
            var orgInfo = orgType.GetField(orgName, orgFlags);

            if (orgInfo != null)
            {
                Debug.Log("[GetFieldInfo Succeeded] name = " +
                    orgName + ", orgInfo = " + orgInfo);
            }
            else
            {
                Debug.LogError("[GetFieldInfo FAILED] name = " +
                    orgName + ", orgInfo = " + orgInfo);
            }
            return orgInfo;
        }

        public static void SetDashForAllPlayerCharacters_Override(
            bool isDash)
        {
            var actorOnMap =
                (ActorOnMap)_info_actorOnMap.GetValue(null);
            var partyOnMap =
                (List<ActorOnMap>)_info_partyOnMap.GetValue(null);

            actorOnMap.SetCharacterSpeed(
                3.75f * _playerSpeedMultiple);
            actorOnMap.SetDash(isDash);
            partyOnMap?.ForEach(v => v.SetCharacterSpeed(
                3.75f * _playerSpeedMultiple));
            partyOnMap?.ForEach(v => v.SetDash(isDash));
        }
    }
}

おわりに

初版執筆時点 (2023-05-11) では、不具合が取り切れていないアドオンのサンプルを公開する、という暫定的な記事になってしまい、申し訳ありませんでしたが、まえがきにも書いたように、発売直後のため、現段階の情報でも RPG Maker Unite 本体やアドオンのソースコード解析者の方に役に立つだろうと推測し、ここまで執筆してきました。

筆者は、ツクール経験自体もそれほどある訳ではなく、スクリプト素材やプラグインとして第 3 者に配布するようなスクリプトを書いた経験もないので、至らない点は多いかと思いますが、本記事の内容がこれから RPG Maker Unite やアドオンの仕組みを理解しようとしている方への手助けになったら幸いです。

主な更新履歴

  • 2023-06-29
    • IL2CPP 環境下でも動くメソッド差し替え方法が、公式サンプルゲーム作者により提示されたため、当該記事へのリンクも含めて情報を追記。
      (公式サンプルゲーム作者の Toya Shiwasu 様から情報をいただけました! 感謝!)
  • 2023-06-16
  • 2023-05-22
    • 公式サイトに関しては英語版の URL も存在するため、追記。
    • マイクロソフトのドキュメントへのリンクを国際化対応版に修正。(ja-jp を削除)
  • 2023-05-17
    • IL2CPP でうまく動かない問題について、筆者の方で確認が取れた内容を追記。
    • Windows 版 IL2CPP でビルドする方法を追記。
  • 2023-05-16
    • 非 public メソッドや、プロパティ、フィールドにアクセスする方法を追加。
    • もう 1 つのアドオンの例として、プレイヤーの速度変更を行う例を掲載。
    • IL2CPP を使用した場合 (iOS 向けなど) にうまく動かない問題について追記。
      (トリナー様に調査を行っていただきました! 感謝!)
  • 2023-05-15
    • 既存のメソッドを直接オーバーライド (=差し替え) する方法を追加。
      (紫苑もみじ様から情報提供をいただきました! 感謝!)
  • 2023-05-14
    • FPS 表示がメニュー画面で正しく機能しなかった問題を解決。
      (瑞祥様からアイデアをいただきました! 感謝!)
    • リリース版 (exe) でニューゲーム直後に固まる問題が、RPG Maker Unite 本体の不具合であることが判明したため、当該箇所の記述を全体的に見直し。
  • 2023-05-12
    • FPS 表示がシーンを跨げなかった問題を解決。
      (VRYGON 様からアイデアをいただきました! 感謝!)
    • 新たに見つかった問題点などの情報について追記。
  • 2023-05-11
    • 初版

「ゲーム開発」一覧

Unity でプログラミングのみ (C# Script のみ) でゲームを作る方法
プログラマーの方向けに、Unity で極力デザイナに頼らずゲームを作る方法を紹介します。
Unity の文字列描画 (TextMesh Pro) を C# Script のみで行う方法
Unity で極力デザイナに頼らず TextMesh Pro で文字列を描画する方法を紹介します。
Unity エディター拡張スクリプトで「手作業」を減らす・無くす
実際のゲーム開発で作成したエディター拡張スクリプトを例に自動化方法を説明します。
Unity の C# Script で「無」から音を産み出す方法
音声ファイル無し、スクリプトのみでゼロから音を作って鳴らす方法を紹介します。
Unity アセット暗号化 実例付き入門
AssetStudio 等のツールで素材をぶっこ抜かれないように保護する方法の基本説明です。
Unity アセットバンドル (AssetBundle) 実例付き入門
外部のアセットファイルを読み込み可能にする「アセットバンドル」機能の説明です。
Unity の Time.deltaTime で測った時間がおかしい? (float 型に注意)
Unity で Time.deltaTime をもちいて時間計測を行う際の注意点を解説します。
【暫定】RPG Maker Unite のアドオンからゲーム動作に介入する方法
(現在開いているページです)
【IL2CPP対応】RPG Maker Unite のアドオンからメソッド差替する方法
iOS など IL2CPP でのビルドが必要な場合でも使えるメソッド差替方法を紹介します。