📌 この記事は Zenn に投稿した内容のアーカイブです。
はじめに
Unity で開発していると、
実行中にパラメータを触りながら挙動を確認したい
という場面は、ほぼ毎日のように出てきます。
最初のうちは、Unity 標準の仕組みを使って
簡単な Runtime Debug UI を用意すれば十分でした。
ただ、プロジェクトが進み、
デバッグ項目が増える
調整対象が増える
複数人で同時に触る
という状況になるにつれ、
「その場しのぎ」で作ったデバッグ UI が、確実に足を引っ張る
ようになってきます。
この段階で欲しくなったのが、
壊れにくく、拡張しやすく、
プロジェクトと一緒に「育てていける」デバッグ基盤
でした。
この記事では、
そのために設計した 自作 Runtime Debug システム について、
設計意図と実装の要点 を中心にまとめます。
何が問題だったのか
問題の本質は、「UI をどう実装するか」 ではありませんでした。
本当に困っていたのは、
Debug 項目の追加・削除が面倒
どこに何のデバッグ設定があるのか分からない
不要になった項目が整理されず、蓄積していく
という 運用面の破綻 です。
特に後半になると、
「デバッグ環境のせいで、デバッグそのものがやりにくい」
という、本末転倒な状態になりがちでした。
目指したゴール
今回の設計で目指したゴールは、かなり明確です。
UI を書かずに Debug 項目を追加できる
構造が自然に整理され続ける
無効化・除去が簡単
プロジェクト規模が大きくなっても破綻しない
つまり、
「運用しても壊れない設計」
を最優先にしました。
全体構成
全体の流れは、次のようになります。
起動
↓
RuntimeInitializeOnLoadMethod
↓
DebugManager 自動生成
↓
DebugOption 自動登録
↓
Reflection による項目収集
↓
UI 自動生成 & バインディング
ここで重視したのは、
「シーンや手動設定に一切依存しない」
という点です。
デバッグ環境の初期化でミスる、というのは
地味にストレスが溜まるため、
起動時に必ず立ち上がる構成 にしています。
DebugManager の自動生成
起動時に DebugManager を生成し、DontDestroyOnLoad で常駐させます。
#if ENABLE_RUNTIME_DEBUG
public static class DebugBootstrap
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Initialize()
{
var go = new GameObject("RuntimeDebugger");
Object.DontDestroyOnLoad(go);
go.AddComponent<RuntimeDebugManager>();
}
}
#endif
この形にしている理由は、
シーン依存を完全に排除
設定忘れを防止
Define 一発で無効化
という 運用上の事故を防ぐため です。
Debug 項目は「クラス」で定義する
Debug 項目は、INotifyPropertyChanged を実装したクラスとして定義します。
public abstract class DebugOptionBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
これにより、
明示的な管理が不要
項目単位で整理可能
チーム開発でも衝突しにくい
という構成になります。
特に チーム規模が大きくなるほど効いてくる設計 だと感じています。
実際の Debug クラス例
Debug クラスは、かなり素朴な形になります。
public partial class PlayerDebug : DebugOptionBase
{
[DisplayName("移動速度")]
public float MoveSpeed
{
get => _moveSpeed;
set
{
_moveSpeed = value;
OnPropertyChanged();
}
}
[DebugButton("HP全回復")]
public void RecoverHP()
{
player.RecoverFullHP();
}
private float _moveSpeed = 5f;
}
ここで書くのは、
「何を触りたいか」だけ
です。
UI の構築やレイアウトは、
すべてシステム側に任せています。
自動登録による拡張性
各 DebugOption は、
起動時に自動で DebugManager に登録されます。
#if ENABLE_RUNTIME_DEBUG
[RuntimeInitializeOnLoadMethod]
static void Register()
{
RuntimeDebugManager.Register(new PlayerDebug());
}
#endif
Reflection + Attribute による UI 構築
DebugManager 側では Reflection を用いて、
プロパティ
メソッド
Attribute
を取得し、UI を自動生成します。
var props = type.GetProperties();
var methods = type.GetMethods();
ここに、
DisplayName
Range
DebugButton
といった Attribute を組み合わせることで、
宣言的に UI の振る舞いを制御 できるようにしています。
この構成で意識したのは、
「UI を作る」のではなく、「UI を記述する」設計に寄せること
です。
各 DebugModule 側では、
「この値を触りたい」
「この処理をボタンにしたい」
という 意図だけをコード上に残し、
UI の生成やレイアウトはすべてシステム側に委ねます。
これにより、
UI 実装を一切意識せずに Debug 項目を追加できる
UI の構造が自然に統一される
不要になった項目の削除コストが極端に低い
という、運用フェーズで効く設計 になりました。
特に後半フェーズでは、
デバッグ項目の増減が日常的に発生するため、
「UI を書かなくていい」
という一点だけでも、
開発効率と精神的負担の両面で、かなり効いてきます。
実装イメージ
Reflection で取得した PropertyInfo から Attribute を読み取り、
対応する UI を生成・バインドします。
void CreateUI(object target, PropertyInfo prop, Transform parent)
{
var displayName = prop.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName
?? prop.Name;
var ui = Instantiate(_numberOptionPrefab, parent)
.GetComponent<NumberOption>();
ui.Bind(target, prop, displayName);
}
UI 側では、INotifyPropertyChanged を利用して
値の変更を双方向に同期 しています。
public abstract class OptionBase : MonoBehaviour
{
object _target;
PropertyInfo _prop;
public void Bind(object target, PropertyInfo prop, string name)
{
_target = target;
_prop = prop;
if (target is INotifyPropertyChanged notify)
notify.PropertyChanged += OnPropertyChanged;
RefreshUI();
}
void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == _prop.Name)
RefreshUI();
}
protected abstract void RefreshUI();
}
この構成により、
UI → 値変更 → 即ゲーム反映
ゲーム → 値変更 → 即 UI 反映
という リアルタイムな双方向同期 が、
自然な形で成立します。
この構成にして感じた効果
1. Debug 項目を「育てられる」
項目追加・削除のコストが低いため、
開発フェーズに合わせて 自然に進化していく
デバッグ環境になりました。
2. 変更コストが極端に低い
新しい Debug 項目は、
public int EnemyCount { get; set; }
これだけで完了します。
UI を一切意識しなくてよい、というのは
想像以上に 精神的な負担が軽く なります。
3. チーム開発との相性が良い
各担当が自分の DebugOption を持てるため、
修正範囲が局所化
衝突しにくい
責務が明確
という構造になります。
まとめ
今回作った Runtime Debug 基盤は、
自動生成
自動登録
宣言的設計
リアルタイム同期
を組み合わせることで、
壊れにくく、育てやすいデバッグ環境
を目指した設計です。
即席のデバッグ UI は、短期的には便利ですが、
中長期では 技術的負債 になります。
もし、
デバッグ環境そのものを、
ちゃんと「設計対象」として扱いたい
と感じている方がいれば、
一つの実践例として参考になれば嬉しいです。