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

「Game Event System」タグの記事が15件件あります

Game Event System related articles

全てのタグを見る

クロスシーンイベント:誰も語らないが誰もがハマる永続化の問題

TinyGiants
GES Creator & Unity Games & Tools Developer

AudioManagerがBGMを再生している。プレイヤーが新しいエリアに入った時にトラックを切り替えるため、OnLevelStartにサブスクライブしている。AudioManagerDontDestroyOnLoadオブジェクトに配置して、シーンロードをまたいで永続化させた。開発中は常に同じシーンでテストしているのですべて正常に動作する。

ある日、誰かが初めてレベル1からレベル2をロードする。BGMが切り替わらなくなる。AudioManagerはまだ生きている——DontDestroyOnLoadがその仕事を果たした——しかしイベントサブスクリプションはシーン遷移を生き延びなかった。あるいはもっと悪い状況:古いサブスクリプションがまだ残っていて、破棄されたレベル1のイベント発火元を指しており、次に何かがそれを呼び出そうとすると、ゲームプレイの最中にMissingReferenceExceptionが発生する。

これが永続化問題であり、複数のシーンを持つすべてのUnityプロジェクトがいずれぶつかるものだ。

見えないものをデバッグする:イベントシステムに専用のオブザーバビリティが必要な理由

TinyGiants
GES Creator & Unity Games & Tools Developer

QAテスターがバグを報告する:「鍵を拾ってもドアが開かない。」

シンプルだろう? おそらく参照漏れか条件の間違い。プロジェクトを開き、鍵を拾うと...ドアは問題なく開く。自分の環境では再現する。テスターに再現手順を聞くと「30%くらいの確率で発生する。大抵セーブ/ロードサイクルの後」とのこと。

デバッグ地獄に突入だ。鍵ピックアップイベントからインベントリ更新、クエスト進行チェック、ドアのアンロック条件に至るチェインのどこかで、何かが間欠的に失敗している。しかしどのリンクだ? イベントが発火されなかった? 発火されたがリスナーがサブスクライブされていなかった? サブスクライブされていたが条件がfalseに評価された? 条件は正しかったがロード後にドアの状態が古かった?

リリース後に発覚するイベントシステムの罠:メモリリーク、データ汚染、再帰トラップ

TinyGiants
GES Creator & Unity Games & Tools Developer

ゲームを5分間テストしてきた。快適に動作する。するとQAがレポートを上げてくる:「30分のプレイセッションでメモリ使用量が着実に増加。6シーンをロードした後、フレームレートが60から40に低下。」プロファイリングすると、本来12であるべきイベントに847のリスナーが登録されている。各シーンロードが新しいサブスクリプションを追加しつつ、古いものを削除していなかった。オブジェクトは破棄されていたが、デリゲート参照が残り続け、ガベージコレクタが手を出せない場所に死んだMonoBehaviourをピン留めしていた。

あるいはこっち:「2回目のPlay Modeセッションでヘルス値がおかしい。1回目は問題なし。」Playを押す。戦闘をテスト。停止。もう一度Play。プレイヤーのHPが100ではなく73でスタートする。前回のセッションのScriptableObjectの状態が持ち越されていた。誰もリセットしなかったから。

あるいは定番:3秒間ゲームがハングし、Unityがクラッシュする。イベントAのリスナーがイベントBを発火。イベントBのリスナーがイベントAを発火。スタックオーバーフロー。ただし、クラッシュしないこともある——目に見えるエラーを出さずにCPUを食い潰す無限ループでハングするだけ。

これらは仮定の話ではない。実際に本番ゲームで出荷されたのを見たバグだ。そしてすべて同じ根本原因を持つ:単独では正しく見えるが、スケールすると壊れるイベントシステムパターン。

ランタイムでイベントフローを構築する:ビジュアルエディタでは足りない時

TinyGiants
GES Creator & Unity Games & Tools Developer

プロシージャルダンジョンジェネレータが3つの感圧板とスパイクトラップのある部屋を生成した。次の部屋にはロックされたドアに繋がるレバーパズル。その次の部屋はボスアリーナで、ボスのヘルスフェーズに応じて環境ハザードが起動する。これらのイベントリレーションシップはエディタ時点では存在しなかった。ダンジョンレイアウトはプレイヤーが30秒前に入力したシードで決定されたものだ。

イベントをどうワイヤリングする?

従来のアプローチでは、巨大なswitch文を書く。各部屋タイプごとに手動でイベントハンドラをサブスクライブ・アンサブスクライブする。各AIの難易度ごとに手動で異なる攻撃パターンをチェインする。各MODコンテンツごとに手動でコンフィグファイルをパースしてイベント接続に変換する。「手動」の部分が問題だ——ランタイムでトポロジーが変わるたびにイベントワイヤリングロジックを再実装している。

ビジュアルノードエディタはデザインタイムで分かっているフローに最適だ。しかしゲームが実行されるまで存在しないフローを根本的に扱えない。そしてますます、最も興味深いゲームシステムはイベントグラフが動的なものだ。

実行順序のバグ:「誰が先に反応するか」に潜む危険

TinyGiants
GES Creator & Unity Games & Tools Developer

プレイヤーが25ダメージを受ける。ヘルスシステムが現在のHPからダメージを差し引く。UIがヘルスバーを更新する。...はずが、ヘルスバーに表示されるのは75ではなく100。20分間コードを見つめた末に気づく。UIのリスナーがヘルスシステムのリスナーより先に実行されていた。UIは古いHP値を読み取り、それを描画し、その後にヘルスシステムがデータを更新した。データが正しくなった頃には、フレームはすでに描画済みだった。

あなたが発見したのは実行順序バグだ。イベント駆動アーキテクチャで何かをリリースした経験があるなら、気づかないうちにこのバグをいくつも出荷している可能性が高い。テスト中はスクリプトがたまたま正しい順序で初期化されたから動いていただけで、本番環境ではUnityのロード順が変わって壊れる——そういう類のバグだ。

これはレアなエッジケースではない。ほとんどのイベントシステム——UnityのUnityEventや標準のC# eventデリゲートを含む——が持つ構造的な欠陥だ。そして一度理由を理解してしまうと、もう元には戻れない。

時間ベースイベント:なぜコルーチンは遅延と繰り返しに向かないのか

TinyGiants
GES Creator & Unity Games & Tools Developer

グレネードが着弾してから2秒後に爆発を遅延させたい。シンプルだ。コルーチンを書く。IEnumerator DelayedExplosion()、yield return new WaitForSeconds(2f)、爆発ロジックを呼ぶ。丁寧に書いて10行くらい。気分がいい。

次にデザイナーが「プレイヤーが爆弾を解除できるようにしたい」と言う。オーケー、StopCoroutine()を呼べるようにCoroutine参照を保存する必要がある。でも待って——コルーチンが開始する前にプレイヤーが解除したら?nullチェックが必要。待機中にゲームオブジェクトが破壊されたら?もう一つnullチェック。コルーチンが完了したまさにそのフレームでプレイヤーが解除したら?レースコンディション。10行が25行になり、まだ「解除メッセージを表示 vs. 爆発を表示」の分岐すら処理していない。

これがUnityのすべての時間ベースイベントの物語だ。最初の実装はクリーン。2番目の要件でコード量が倍増。3番目で転職を考え始める。

パラレルかシーケンシャルか:すべてのイベントシステムに必要な2つの実行パターン

TinyGiants
GES Creator & Unity Games & Tools Developer

プレイヤーが死ぬ。死亡サウンドと死亡パーティクルは同じ瞬間に始まるべきだ——片方を待ってからもう片方を始める理由がない。でも画面フェードはリスポーンポイントのロード前に絶対に完了しなければならない。リスポーンはプレイヤーのテレポート前に完了しなければならない。テレポートは画面フェードイン前に完了しなければならない。

1つのイベントからトリガーされる同じフロー内でのパラレルとシーケンシャル実行。そして不都合な真実:Unityのほとんどのイベントシステムはパターンを1つだけ提供する。イベントを発火し、すべてのリスナーが応答し、終わり。それらのレスポンスが同時に起きるべきか厳密な順序で起きるべきか?あなたの問題だ。

だから解決する。コルーチンで。コールバックで。_hasFadeFinishedという名前のブール値で。そして気づく前に、6つのファイルに散らばった場当たり的なステートマシンを構築してしまい、未来の自分を含めて誰もフォローできない。

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

TinyGiants
GES Creator & Unity Games & Tools Developer

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

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

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

if-else地獄からの脱出:ビジュアル条件ロジックの正しいやり方

TinyGiants
GES Creator & Unity Games & Tools Developer

すべてのゲームは基本的に条件の巨大な山だ。「敵が耐性を持っていない、かつプレイヤーが炎バフを持っている、かつランダムクリティカル判定をパスした場合にのみ炎ダメージを与える。」プロトタイプ中は、コールバックにif文を放り込んで先に進む。30秒。動く。生産性を感じる。

そしてプロトタイプがプロダクションに入る。その30秒のif文が増殖し始める。1つが5つに。5つが50に。50が「2体目のボスのルートドロップ率を制御する条件ってどこにあるの?」になる。そして今、デザイナーが後ろに立ってダメージ閾値を0.3から0.25に変更できるか聞いている。そして君は再コンパイルが必要だと説明している。

if-else地獄へようこそ。住民:3ヶ月以上続いたすべてのUnityプロジェクト。

デザイナーがコードなしでイベントを設定:デザイナーとプログラマーの連携問題

TinyGiants
GES Creator & Unity Games & Tools Developer

火曜の午後3時。デザイナーが横から声をかけてくる。「ねえ、プレイヤーが50以上のダメージを受けた時の画面シェイク、もう少し強くできない?あと、ヒットサウンドの前に0.5秒のディレイ入れたいんだけど。あ、毒エフェクトのティック間隔も2秒じゃなくて1.5秒にしたい。」

3つの変更。デザイナーの視点からすれば実質15秒の判断。でも実際に起きることはこうだ:Sceneビューを閉じる。IDEを開く。読み込みを待つ。ダメージハンドラーを検索する。メソッド内に埋もれた画面シェイクの強度値を見つける。変更する。次にオーディオのディレイを探す——これは別のクラスにある。変更する。次に毒のコルーチンを探す——これはさらに別のクラスで、ティックレートはWaitForSecondsの中にある。変更する。3つのファイルをすべて保存。Unityに戻る。再コンパイルを待つ。テスト。

8分後、デザイナーが言う。「やっぱりシェイクは前の方が良かったかも、あと毒は1.8秒で試せる?」