リフレクションを駆使してデバッグメニューを自動構築してみる(その1)

デバッグメニューってどうしてる?

例えば、あなたが長編のロールプレイングゲームを作っていて、そのシナリオの最後に待ち構える最終ボスの実装をしたとしましょう。その実装確認を行わなければなりません。

まさかタイトル画面から始めてオープニングから順にシナリオを進めていくプログラマーはいないでしょう。当然、最終ボスの目の前から確認をスタートしたいはずです。キャラクターのレベルや装備品は、最終ボスとの戦闘時に想定されるものであってほしいです。そして、ボスに実装されている全ての行動を確認している間にやられてしまったり、あるいは逆にボスを倒してしまったりしないよう、操作キャラクターとボスを無敵にしたいはずです。

そんなわけでデバッグメニューの出番です。ゲームが起動したらすぐにデバッグメニューを開き、任意のシーンへ移動したり、キャラクターを強化したり、無敵にしたり、高速で移動させたり、壁をすり抜けて移動したり、周囲の邪魔な敵を一瞬で葬り去ったり…とゲームのデバッグには欠かせない様々な機能をメニューから呼び出すことができます。

デバッグメニューの実装にもコストがかかりますが、これが無いと実装確認に時間がかかって仕方がありません。デバッグメニューを実装することで、トータルの開発コストは大幅に削減できます。

しかし、実際にデバッグメニューを実装してみるとメンテナンスするのが大変で、チーム内のスタッフからは続々と「あんな機能もほしい」「こんな機能もほしい」と言われるものの、「余裕ができたら実装します」といって結局そのまま放置…なんてことも多いのではないでしょうか?(笑)

特にゲームそのものの実装で手一杯だったりすると、どうしてもデバッグメニューのメンテナンスは疎かになりがちです。何とかラクしてデバッグメニューをメンテナンスできないものでしょうか…。

ご挨拶

皆さん、はじめまして! 開発部・プログラマーの植田吉紀です。
気軽によしきんって呼んでくださいね(ハート)

技術ブログも始まったばかりですが、今回はいきなり3部構成のちょっと長いお話になります。そんなに難しいテーマでもないので、お菓子でも食べながら気楽に読んでいただけたら幸いです。

普通(?)のデバッグメニューの実装方法

まず、一般的にデバッグメニューをどうやって実装するか…ですが、これについては会社やチームによって結構文化が違ってたりするんじゃないでしょうか。というわけで、一般論では無いですが、僕が今までに経験したことのあるデバッグメニューの構築処理を C# で書き直してみたものです。

// 空のデバッグメニューページを生成
var page = new DebugMenuPage("キャラクターの編集");

// ページに項目を追加していく
page.AddItem(new IntEditBox("HP", () => chara.Hp, value => chara.Hp = value));
page.AddItem(new Separator());
page.AddItem(new FloatEditBox("X座標", () => chara.Position.x, value => chara.SetPositionX(value)));
page.AddItem(new FloatEditBox("Y座標", () => chara.Position.y, value => chara.SetPositionY(value)));
page.AddItem(new FloatEditBox("Z座標", () => chara.Position.z, value => chara.SetPositionZ(value)));
page.AddItem(new Separator());
page.AddItem(new CheckBox("無敵", () => chara.IsInvincible, value => chara.IsInvincible = value));
page.AddItem(new Button("即死", () => chara.Kill()));

// ページをデバッグメニューに表示させる
Application.DebugMenu.Push(page);

…こんな具合です。まず最初に空のデバッグメニューページを作成し、そのページに対してメニュー項目(GUIパーツ)を追加していき、最後にデバッグメニューページをデバッグメニューに表示させています。

IntEditBox, FloatEditBox はそれぞれint型、float型の値を編集するためのキャプション付きエディットボックスです。コンストラクタの第1引数はキャプション文字列で、第2引数は値の取得方法を示すデリゲート、第3引数は値の設定方法を示すデリゲートです。ここでは第2引数と第3引数はラムダ式で記述しています。C++ だったらここは変数へのポインタや参照を渡すオーバーロードがあったりするかもしれませんね。

CheckBox はキャプション付きチェックボックスです。編集対象の値の型がbool型である点を除けば IntEditBox と使い方は同じです。

Button はボタンです。コンストラクタの第1引数にキャプション文字列を与え、第2引数でボタンが押された時の処理を指定します。ここでは第2引数はラムダ式で記述しています。

Separator はただのセパレータです。項目と項目を線で区切ります。

また、ここでは登場しませんでしたが、他にも文字列を編集するための TextEditBox や、リストから項目を選択させるための ListBox などのGUIパーツが用意されているものとします。

この方法でもいいのだが…

上の例ではキャラクターの持つ多くのメンバの内、ほんの一部分だけをデバッグメニューで編集できるようにしました。しかし、デバッグメニューに表示させる項目を追加して欲しいという要望がチーム内からどんどん出てくることでしょう。気づいたらキャラクタークラスのほとんどのメンバがデバッグメニューに表示されていることになるかもしれません。

また、キャラクター以外にも武器や装備品、魔法、アイテム、クエストの進行状況 etc… 様々な情報の閲覧・編集したいはずです。それに、エフェクト、サウンド、コリジョン、AIなど、システム的な部分のデバッグメニューも必要になるでしょう。デバッグメニューの構築処理をたくさん記述しなければなりませんね。

ある程度自動でデバッグメニューを構築してくれるような救世主はいないのでしょうか…。

ところで…

話は変わりますが、Java や C# など一部のプログラミング言語は「リフレクション」と呼ばれる機能を持っています。これは実行時にクラスや構造体などの型のメタ情報にアクセスするためのもので、例えば特定のクラスのメンバの一覧を取得したり、メンバの名前(文字列)を用いてその名前を持つメンバ情報を検索し、フィールドの値を書き換えたり、メソッドを呼び出したりできます。

また、そのプログラミング言語が「属性(アトリビュート)」や「アノテーション」と呼ばれる機能(以降「属性」と呼びます)も持っている場合、型やメンバに付与された属性をリフレクションによって参照することもできます。

これってデバッグメニューの自動構築に使えるのでは…!?

いざ、実装!!

リフレクションを駆使してデバッグメニューを自動構築する DebugMenuPageBuilder クラスを実装することにしました。C# で実装してみましたが、リフレクションと属性さえ使えれば同じようなことは他の言語や環境でも実装できると思います。

まず、使い方のイメージから。

[DebugContract]
public class Character
{
    [DebugMenuItem(caption = "HP")]
    public int Hp { get; set; }
    
    [DebugMenuItem(caption = "座標")]
    public Vector3 Position { get; set; }

    [DebugMenuItem(caption = "無敵")]
    public bool IsInvincible { get; set; }
    
    [DebugMenuItem(caption = "即死")]
    public void Kill() { ... }
    
    :
    :
}

上のコードでは、既存の Character クラスに対していくつかの属性の付与だけを行っています。まず、クラスそのものに対して DebugContract という属性を付与しています。これは、DebugMenuPageBuilder に対して「この型は、DebugMenuItem 属性が付与されているメンバだけをデバッグメニューに表示してくださいね」という指示を示すものです。この属性についての詳細は次回の記事で説明します。

そして、デバッグメニューに表示すべきメンバに対しては DebugMenuItem 属性を付与しています。caption 引数でキャプション文字列も指定しています。

続いて、Character クラスのインスタンスからデバッグメニューページを構築する部分のソースコードを示します。

// キャラクターから自動でデバッグメニューページを生成
var builder = new DebugMenuPageBuilder();
var page = builder.Build(chara, "キャラクターの編集");

// ページをデバッグメニューに表示させる
Application.DebugMenu.Push(page);

…すごいスッキリしますね!!

実際に自動構築されたデバッグメニューを見ると、メニュー項目の順序が実際のメンバの定義順通りになっていないことがわかります。また、セパレータもありません。これらの問題は別途解決する必要がありますが、それは第3回にお話しします。とりあえず、イメージだけは伝わったかと思います。

次回は、DebugMenuPageBuilder.Build() メソッドの内部で具体的にどのようなことが行われているのかを説明していきます。