メインコンテンツまでスキップ

ビジュアルとAPIイベントのレイヤリング:スケーラブルなプロジェクトのためのベストプラクティスガイド

TinyGiants
GES Creator & Unity Games & Tools Developer

プロジェクトが成長するにつれて、最もよく聞かれる質問の一つが*「ビジュアルツールとスクリプトAPI、どちらを使うべきですか?」*です。答えは両方です。ただし、どこで各アプローチが輝くかを知ることが、クリーンでスケーラブルなイベントアーキテクチャと、自重で崩壊するアーキテクチャを分けるポイントです。

さまざまな規模のチームがGESをどう使っているかを見てきた経験から、イベントが50個でも500個でもプロジェクトの保守性を維持できるレイヤードアプローチを共有したいと思います。

ゲームオブジェクトの2つの世界

すべてのUnityプロジェクトには、根本的に異なる2つのカテゴリのオブジェクトがあります。

シーン常駐オブジェクト — 編集時にHierarchyに存在するもの。UIキャンバス、レベルジオメトリ、カメラ、永続マネージャー、環境トリガー。見ることも、選択することも、参照をドラッグすることもできます。

ランタイム生成オブジェクトInstantiate() でランタイムに作成されるプレハブインスタンス。敵、弾丸、ドロップアイテム、プーリングされたVFX、動的に生成されるUI要素。ゲームが動き出すまで存在しません。

この区別が、イベント使用のレイヤリングの基盤です。

レイヤー1:シーン常駐オブジェクトのビジュアル設定

シーンに存在するすべてのオブジェクトには、Editor、Behaviorウィンドウ、Flow Graphを使用しましょう。

なぜか?シーン常駐オブジェクトには安定したInspector参照があるからです。UIキャンバス上のヘルスバー、レベル内のドアトリガー、BGMコントローラー — これらのオブジェクトはHierarchyにそこにあるのです。以下のことができます:

  • Game Event Editor を開き、イベントを見つけてBehaviorボタンをクリック
  • Action Conditions をビジュアルに設定(例:health < 30% の時だけダメージフラッシュを発動)
  • Scheduleタイミング を設定(ヒット音の0.2秒遅延、画面振動を0.1秒間隔で3回繰り返し)
  • Hierarchyから直接ターゲットオブジェクトをドラッグして UnityEvent actions を接続
  • Flow Graph で複雑なシーケンスを構築(画面フェードアウト → シーン読み込み → プレイヤー再配置 → 画面フェードイン)

ここはデザイナー、アーティスト、サウンドエンジニアが最も活躍する場です。ゲームフィールの調整 — 遅延を0.2秒から0.35秒に変更、低HPの敵にエフェクトをスキップする条件を追加、Chainシーケンスの順序を変更 — コードに一切触れず、リコンパイルを待つ必要もありません。

最適な用途

  • UIレスポンス — ボタンクリック、パネルトランジション、HUD更新
  • レベルスクリプティング — ドア開放、トラップ発動、カットシーントリガー
  • オーディオイベント — ゲーム状態に基づく再生/停止/クロスフェード
  • カメラ動作 — シェイク、ズーム、追従ターゲット切り替え
  • 環境リアクション — ライティング変化、パーティクルエフェクト、天候遷移
  • チュートリアルシーケンス — 条件付きステップバイステップのChainガイド

OnPlayerDeath イベントで:画面を暗くする、「You Died」パネルを表示する、サウンドを再生する、プレイヤー入力を無効にする。4つのレスポンスすべてが、Hierarchyに既に存在するUIとシーンオブジェクトに接続されています。これはBehaviorウィンドウの教科書的ユースケース — 4つのAction、1つのイベント、コードゼロ。デザイナーは後からパネル表示に0.5秒の遅延を追加したり、水中ではサウンドをスキップする条件を追加できます。コード変更リクエストは一切不要です。

レイヤー2:ランタイム生成インスタンスのスクリプトAPI

ランタイムに作成されるプレハブインスタンスには、AddListener()RemoveListener()Raise() を使用しましょう。

なぜか?プレハブをインスタンス化する時、参照をドラッグするInspectorがないからです。オブジェクトプールから生成したばかりの敵は OnPauseGame をリッスンしてAIをフリーズさせる必要があります。弾丸はターゲットに衝突した時に OnEnemyHit を発火する必要があります。これらのバインディングはコードで、オブジェクトが生まれた瞬間に行う必要があります。

public class Enemy : MonoBehaviour
{
[GameEventDropdown] public GameEvent onPauseGame;
[GameEventDropdown] public GameEvent onResumeGame;

void OnEnable()
{
onPauseGame.AddListener(Freeze);
onResumeGame.AddListener(Unfreeze);
}

void OnDisable()
{
onPauseGame.RemoveListener(Freeze);
onResumeGame.RemoveListener(Unfreeze);
}

void Freeze() => agent.isStopped = true;
void Unfreeze() => agent.isStopped = false;
}

重要な点に注目してください:イベントアセット自体は、プレハブの [GameEventDropdown] 属性を通じて割り当てられています。イベント参照はビジュアル — プレハブアセット上のドラッグ&ドロップフィールドです。リスナー登録だけがコードです。なぜなら、インスタンスは編集時には存在しないからです。

最適な用途

  • 生成された敵/NPCがグローバルイベントに反応(ポーズ、スローモーション、エリアエフェクト)
  • 弾丸やVFXが衝突やライフタイム終了時にイベントを発火
  • プーリングされたオブジェクトがアクティブ/非アクティブ時にサブスクライブ/アンサブスクライブ
  • 動的に生成されるUI要素(インベントリスロット、チャットメッセージ、リーダーボード行)
  • 編集時にリスナー数が不明なすべてのシステム

タワーディフェンスゲームを作っているとします。タワーはランタイムに配置されます。各タワーは OnWaveStarted をリッスンして照準を開始し、OnWaveEnded でアイドル状態に入る必要があります。タワーは動的にインスタンス化されるため、各タワーが OnEnable() で独自のリスナーを登録し、OnDisable() でクリーンアップします。一方、OnWaveStarted発火するウェーブマネージャーは、タイミングがすべてBehaviorウィンドウで設定されたシーン常駐シングルトンかもしれません。

レイヤー3:ハイブリッド — 魔法が起きる場所

GESの真の力は、両方のレイヤーを意図的に組み合わせた時に現れます。

プログラマーがイベントアーキテクチャを定義し、ランタイムシステムの発火/リッスンコードを書きます。どんなイベントが存在するかどんなデータを運ぶかいつ発火するかを決定します。

デザイナーとアーティストがBehaviorウィンドウ、Flow Graph、Condition Treeを使ってレスポンスの内容を設定します。ゲームフィール、タイミング、条件、ビジュアル/オーディオの仕上げをコントロールします。

具体的なハイブリッドワークフロー:

[プログラマーが書く]
- EnemyHealth.cs: ヒット時に OnEnemyDamaged(int damage) を発火
- EnemyHealth.cs: HP <= 0 時に OnEnemyDeath(GameObject enemy) を発火
- WaveManager.cs: 敵の生成/消滅時にリスナーを動的に追加/削除

[デザイナーがBehaviorウィンドウで設定]
- OnEnemyDamaged -> ダメージ数字UIのフラッシュ、カメラシェイク(条件:damage > 20)
- OnEnemyDeath -> 死亡VFX再生、スコア加算、ウェーブ完了チェック

[デザイナーがFlow Graphで設定]
- OnLastEnemyDeath -> OnWaveCompleteをトリガー
- OnWaveComplete -> チェーン:報酬パネル表示 -> 3秒待機 -> 次のウェーブ生成

プログラマーは画面振動の持続時間を調整する必要がありません。デザイナーはリスナー登録コードを書く必要がありません。各人が自分の専門領域で作業し、イベントアセットが彼らの間の共有契約となります。

スケーリングのための実践ガイドライン

プロジェクトが成長するにつれて、以下の原則を心に留めてください。

1. オブジェクトのライフタイムに選択を委ねる

編集時にシーンにある → ビジュアル。ランタイムにインスタンス化 → API。このルール1つで90%の判断が解決します。

2. コード内でもイベント参照はビジュアルに保つ

MonoBehaviourフィールドには常に [GameEventDropdown] を使い、イベント検索をハードコーディングしないでください。プレハブに型安全で検索可能なドロップダウンが提供され、コード変更なしでイベントを差し替えられます。

3. レスポンス調整にはBehaviorウィンドウ、レスポンスロジックにはコード

レスポンスが「HPが50%以下の時、0.3秒遅延でこのサウンドを再生」なら設定 — Behaviorウィンドウへ。レスポンスが「アーマータイプ、属性耐性、バフスタックに基づいてダメージ軽減を計算」ならロジック — コードで書く。

4. ランタイムリスナーの後片付けを徹底する

OnEnable() → サブスクライブ。OnDisable() → アンサブスクライブ。例外なし。オブジェクトプールや頻繁なシーン読み込みがあるプロジェクトで、メモリリークやゴーストリスナーを防ぐための最も重要な習慣です。

5. 実行順序が重要な時はプライオリティリスナーを使用

複数のシステムが同じイベントに応答する場合、登録順序に頼らないでください。AddPriorityListener() で明示的なプライオリティ値を指定します。データ保存はプライオリティ1000、ゲームステート更新は100、UI更新は0、オーディオ再生は-100。実行順序が自己文書化されます。

6. Flow Graphで見えない関係を可視化する

イベントがTriggerやChainで他のイベントを発火する場合、必ずFlow Graphでモデリングしてください。6ヶ月後、OnDoorOpenedOnLightActivatedOnMusicChangedOnTutorialStep3 をトリガーすることを覚えている人はいません。Flow Graphならこれらの関係が一目で分かります。

7. 型ではなくドメインで整理する

イベントデータベースは技術的分類(Voidイベント、Intイベント)ではなく、ゲームドメイン(コンバット、UI、オーディオ、プログレッション)を軸に構成してください。コンバットデザイナーが何かを調整する必要がある時、コンバット関連のすべてを1か所で見つけられるべきです。

判断を一目で

レイヤーアプローチ担当者タイミング
シーンオブジェクトビジュアル(Editor、Behavior、Flow Graph)デザイナー、アーティスト、サウンド編集時にHierarchyに存在するオブジェクト
ランタイムインスタンススクリプトAPI(AddListener、Raise)プログラマーゲームプレイ中にインスタンス化されるプレハブ
ハイブリッド共有契約としてのイベント全員プログラマーが発火、デザイナーがレスポンス

まとめ

目標は2つのアプローチのどちらかを選ぶことではありません。目標は各チームメンバーがそれぞれの専門性に合ったツールで作業し、イベントシステムが各専門分野間のクリーンな境界として機能することです。

アーキテクチャはコードで構築する。体験はエディターで磨く。ゲームは一緒に出荷する。