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

「Scripting API」タグの記事が4件件あります

C# runtime API

全てのタグを見る

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

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番目で転職を考え始める。