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

見えないイベントチェーン:見えないものはデバッグできない

TinyGiants
GES Creator & Unity Games & Tools Developer

プレイヤーが死ぬ。死亡サウンドが鳴る。ラグドールが起動する。UIポップアップに「You Died」が表示される。ゲームがオートセーブする。アナリティクスイベントが発火する。リスポーンタイマーがカウントダウンを開始する。6つの異なるシステムが、1つのイベントOnPlayerDeathに応答している。でも質問がある——それはどこに文書化されている?

コードの中ではない。プロジェクト管理ツールの中でもない。どんなダイアグラムの中でもない。それが存在するのは1つの場所だけ:元々それをセットアップした人の頭の中。そしてその人が6ヶ月前にチームを離れていたら、どこにも存在しない。

これがイベント駆動アーキテクチャのダーティシークレットだ。システムを疎結合にするからこそ採用する。AudioManagerUIManagerへの参照を持つ必要がないことを喜ぶ。でも決して語られないコストがある:実行フローが見えなくなる。そして見えないものは、定義上、視覚的にデバッグすることが不可能だ。

可視性の問題は思っているより深刻

「見えないイベントチェーン」が実践で何を意味するか、正直に向き合おう。抽象的なアーキテクチャの懸念ではない。非常に具体的で、非常に痛い形で現れる。

Grepの儀式

新しい開発者がチームに加わる。最初の週。死亡画面が時々表示されないバグを調べている。「プレイヤーが死んだ時何が起きるの?」と聞いてくる。

答えにはOnPlayerDeathが関わっていることは知っている。そこでコードベースをgrepする。そのイベント名を参照する20個のファイルが見つかる。サブスクリプションもある。アンサブスクリプションもある。コメントもある。8ヶ月前に「一時的に」無効にされたデッドコードもある。結果を仕分けるのに1時間かけ、チェーンのメンタルマップを構築する。

そして君が言う:「あ、PlayerHealthコンポーネントにOnDeathを発火するUnityEventもあるよ。」それは別のサブスクリプションメカニズムだ。文字列"OnPlayerDeath"がその近くのどこにも出現しないため、grepでは見つからなかった——Inspector内で配線され、シーンファイルにシリアライズされている。

さらに1時間消えた。そしてまだすべてを見つけたか確信が持てない。

チェーンのチェーン問題

ここが本当に醜くなる。OnPlayerDeathOnDisableInputをトリガーし、OnPauseEnemyAIをトリガーする。OnPlayerDeathはまたOnStartRespawnSequenceをトリガーし、OnFadeToBlackをトリガーし、OnLoadCheckpointをトリガーし、OnResetEnemyPositionsをトリガーする。

イベントAがBをトリガーし、BがCとDをトリガーする。DがEとFをトリガーする。FがGをトリガーする。

このチェーンをコードでたどってみよう。InputManager.csOnPlayerDeathへのサブスクリプションを見つける。そのハンドラーがOnDisableInputを発行する。そこでOnDisableInputへのサブスクリプションを検索する。EnemyAIController.csで1つ見つかる。そのハンドラーが発行するのは... 何もない?それともする?確認する。特定のフラグが設定されている場合にのみOnAIPausedを発行する。つまりチェーンは条件付きで分岐する。

これにリスポーンシーケンスが並行して走る。オーディオチェーンも。アナリティクスチェーンも。

これはイベント関係の有向非巡回グラフであり、個々のファイルを読むことで再構築しようとしている。街の道路網を個々の住所を読んで理解しようとしているようなものだ。

オンボーディングの税

新しいチームメンバーは全員同じ税を払う。「Xが起きたら何が起きる?」にはコードベースのガイドツアーが必要。「イベントフロー:プレイヤー死亡」のようなセクションに、何が何に応答して発火するかの15の箇条書きがあるオンボーディングドキュメントを見たことがある。それらのドキュメントは2番目のスプリントまでに古くなる。

問題はチームがドキュメントを書くのが下手なことではない。問題はイベントフローがテキスト形式で文書化できないことだ。それはグラフだ——ノードとエッジ、分岐とマージ、並列と順次のパス。グラフを箇条書きで説明するのは、回路図を散文で説明するようなものだ。できるけど、誰も実際に使えない。

シーケンス調整の悪夢

6つの死亡レスポンスのうちいくつかは同時に起きるべきだ。死亡サウンドとラグドールは同時に始まるべき——片方を待ってからもう片方を始める理由がない。しかし画面フェードはリスポーンロードの前に起きなければならない。リスポーンロードはテレポートの前に完了しなければならない。テレポートはフェードインの前に完了しなければならない。

同じフロー内でパラレルとシーケンシャル。

Unityでこれを表現するにはコルーチンを使う。コルーチンがコルーチンを呼ぶ。フェードが完了したかを追跡するコールバック。入力をゲートする_isRespawningブール。次のステップをトリガーする_fadeCompleteフラグ。DeathStateFadingStateLoadingStateTeleportingStateFadingInStateを持つステートマシンかもしれない。

すべて見えない。すべて脆い。2つのステップの順序を変えればコルーチンチェーンのリファクタリング。新しいステップを追加すれば正しい場所に挿入できたことを祈る。ステップを削除すれば下流が何もタイミングに依存していないことを願う。

他のドメインが解決したこと

イライラすることがある。他のソフトウェアドメインはこの問題をとっくに解決した。

CI/CDパイプライン?ビジュアルパイプラインエディターですべてのステップ、すべての依存関係、すべての並列ブランチが見える。GitHub Actions、Jenkins Blue Ocean、GitLab CI——すべてDAGを表示する。

データエンジニアリング?Apache Airflowがデータパイプラインを有向グラフとして表示する。すべてのタスク、すべての依存関係、すべての条件分岐が一目で見える。

Web開発?Chrome DevToolsがリクエストウォーターフォールを表示する。すべてのネットワーク呼び出し、そのタイミング、その依存関係がすべてビジュアル。

マイクロサービスアーキテクチャ?JaegerやZipkinのような分散トレーシングツールがサービス間のリクエストフローをビジュアルタイムラインとして表示する。

ゲームイベントシステム?何もない。Debug.Logとgrepがある。それだけ。今までは。

GESのFlow Graph Editor:イベントチェーンを可視化する

GES Flow Graph Editorは、以前はコードの中(あるいは頭の中)にしかなかったイベント関係を、ビジュアルノードグラフとしてレンダリングする。イベントがノード。関係がコネクション。フロー全体が一箇所で見える。

これが何でないかをはっきりさせておこう。ビジュアルスクリプティングではない。C#のゲームロジックを置き換えない。AudioManagerはC#でサウンドを再生する。UIManagerはC#で画面を管理する。Flow Graph Editorはそれらのシステム間の関係を可視化する——イベントAが発火した時、他のどのイベントが応答し、どの順序で、どの条件で。

ゲームのイベントオーケストラの指揮者のスコアだと考えてほしい。個々の楽器(C#スクリプト)がパートを演奏する。スコア(Flow Graph)がいつ各楽器が演奏し、どう関係し、全体がどうまとまるかを示す。

Flow Graph Editor Overview

2つの基本パターン:TriggerとChain

ゲームのすべてのイベントフローは2つの実行パターンに帰着し、Node Editorはそれらを視覚的に区別する。

Trigger(パラレル、オレンジ): ソースイベントが発火すると、接続されたすべてのターゲットが同時に発火する。ファイア&フォーゲット。1つのターゲットが失敗しても、他は実行される。「サウンド再生とパーティクルスポーンとUI更新」のパターンだ。

Flow Graph Trigger

Chain(シーケンシャル、グリーン): ソースイベントが発火すると、接続されたターゲットが厳密な順序で1つずつ実行される。各ステップは前のステップの完了を待つ。「画面フェードしてからシーンロードしてからプレイヤーテレポート」のパターンだ。

Flow Graph Chain

視覚的な区別は即座だ。オレンジの線がファンアウト——パラレル。グリーンの線がシーケンスで流れる——シーケンシャル。誰でもグラフを見て即座に実行モデルを理解できる。コードを読む必要なし。メンタルトレースなし。線を辿るだけ。

そしてもちろん、ミックスできる。冒頭のプレイヤー死亡フロー?3本のオレンジTrigger線(サウンド+ラグドール+アナリティクス、パラレル)と1本のグリーンChainシーケンス(フェード→ロード→テレポート→フェードイン、シーケンシャル)。ビジュアルレイアウトでパラレル/シーケンシャルの分割が一目瞭然。

キャンバスナビゲーション

エディターは無限キャンバスだ。中クリックまたはAlt+左ドラッグでパン。スクロールホイールでズーム。Fを押してすべてのノードをビューに収める、または選択だけをフレーミング。上部のツールバーにセーブ、検索、ミニマップトグル、グリッドスナップ、デバッグモードがある。

Flow Graph Toolbar

空いた場所を右クリックしてノードを追加。ノードを右クリックしてノードごとのオプション。コネクションを右クリックして設定。コンテキストメニューはコンテキスト依存——クリックしたものによって異なるオプションが表示される。

Flow Graphの構築

フローの作成は他のノードエディターと同じように機能する:

  1. キャンバスを右クリックしてプロジェクト内の各イベントのノードを追加
  2. 出力ポートから入力ポートにクリック&ドラッグしてコネクションを作成
  3. コネクションがTrigger(パラレル)かChain(シーケンシャル)かを選択
  4. コネクションプロパティを設定——条件、引数トランスフォーマー、タイミング

各ノードはイベント名、引数タイプ、入出力ポートを表示する。コネクション線は色分けで実行タイプ(Trigger vs Chain)を示す。設定中、エディターは接続されたイベント間の型互換性を検証し、不一致について警告する。

Flow Graph Editor Example

複雑なフローを整理するグループ

50ノードのグラフを整理なしで見るのは、置き換えたコードよりもひどい。グループシステムがこれを解決する。ノードを選択し、右クリックし、グループを作成。名前を付ける——「Player Death Flow」「Audio Events」「Boss Phase 2」。色を割り当てる。グラフにドメイン境界を一目で伝えるビジュアル領域ができる。

Flow Graph Groups

グループは純粋に組織的——実行には影響しない。しかし読みやすさには不可欠。チームの色の規約を早めに確立することを勧める:青はシステムイベント、緑はゲームプレイ、オレンジはUI、紫はオーディオ。グラフが一貫した色を使えば、ズームアウトして1つのノードラベルも読まずに構造を即座に理解できる。

グループはネストもサポートする。「Boss Fight」グループに「Phase 1」「Phase 2」「Phase 3」サブグループを含められる。各フェーズに「Effects」と「Gameplay」サブサブグループを含められる。この階層的な組織は何百ものノードを持つフローにスケールする。

ランタイムビジュアライゼーション:フローの実行を見る

これがすべてを変える機能だ。Node Editorを開いたままデバッグモードを有効にしてPlayモードに入ると、グラフが生きてくる。

アクティブなノードはイベントが発火するとパルスする。コネクション線はデータがソースからターゲットに流れる様子をアニメーションで示す。Chainステップは各ステップが実行されるにつれてシーケンスでハイライトされる。失敗した条件はブロックしたコネクション上で赤くフラッシュする。文字通り、イベントフローがリアルタイムで実行されるのを見ることができる。

冒頭のプレイヤー死亡デバッグシナリオを覚えているか?グラフを開く。死ぬ。見る。OnPlayerDeathノードが光る。オレンジの線がサウンドとラグドールノードに同時にアニメーションする。グリーンのチェーンがフェード、ロード、テレポート、フェードインを順に進む。死亡画面が表示されない場合、どのノードが発火しなかったか、なぜかが正確に見える——条件がブロックしたのか、コネクションが欠けているのか、チェーンが前のステップで壊れたのか。

「OnPlayerDeathが発火した」「OnFadeToBlackが発火した」というコンソール出力を読んでタイミングを頭の中で再構築しようとするのと、フロー全体がリアルタイムで実行されるのを視覚的に見るのとの違いだ。

デバッグビジュアライゼーションはフレームあたり約0.5-1msのオーバーヘッドを追加するが、開発中は問題ない。ビルドでは自動的に無効になる——出荷ゲームでランタイムコストゼロ。

本当のポイント

Node Editorはコードを置き換えるためのものではない。すべてのイベント駆動アーキテクチャが抱える可視性の問題を解決するためのものだ。C#スクリプトは実装。Flow Graphはマップ。

他のすべての成熟したソフトウェアドメインには、実行フローを理解するためのビジュアルツーリングがある。ゲーム開発がついに追いついてきた。

新しい開発者が「プレイヤーが死んだら何が起きるの?」と聞いた時、もうコードを2時間かけて案内する必要はない。グラフを開く。画面を指す。「これが起きる。」

それだけでも価値がある。


🚀 グローバル開発者サービス

🇨🇳 中国開発者コミュニティ

🌐 グローバル開発者コミュニティ

📧 サポート