[DevLog #002]UE5でDungeon Architectをマルチプレイ運用するための「排他制御」と「物理干渉回避」の実装

DevLog

1. 今回の実装目的と解決した課題

Dungeon Architect(以下DA)を用いてマルチプレイ用のダンジョンを自動生成・遷移するシステムを構築する際、「ダンジョン生成(Build Dungeon)を連続で行うと、UnrealEditor_Chaos(物理エンジン)による ACCESS_VIOLATION が発生し、エディタごとクラッシュする」という致命的な課題に直面しました。

この課題を解決するため、生成リクエストの二重送信を防ぐ「排他制御(関所)」の仕組みと、サーバーとクライアント間で生成完了を同期する「ハンドシェイク(同期・タイムアウト管理)」システムを構築し、堅牢なマルチプレイダンジョン遷移を実装しました。

2. 開発の背景と設計思想 (Why & How)

【事象の分析:なぜクラッシュするのか?】
クラッシュ時のスタックトレースを解析した結果、原因は非同期処理における「競合状態(Race Condition)」でした。
Destroy Dungeon で古いメッシュを破棄しても、裏で動いているChaosエンジン側の当たり判定(コリジョン)データの解放・ガベージコレクション(GC)が完了する前に新しい生成命令が走ってしまい、メモリ上で物理データが衝突して ACCESS_VIOLATION が発生していました。

【仮説の立証と設計思想】
DAには On Dungeon Build Complete という生成完了を知らせるイベント(Callback)が存在します。しかし、これはあくまで「Game Thread上のアクター生成処理の終了(論理的な完了)」を通知するものであり、「Physics Threadのコリジョン計算・登録の完了(物理的な完了)」を保証するものではありません。

この知見から、以下の設計思想(Why & How)で解決を図りました。

  1. アセットのイベント(Callback)を盲信しない:外部の複雑な非同期処理(DA)を扱う際は、標準の通知イベントを「目安」程度に捉え、独自に状態を管理するフラグ(Mutex)を用意する。
  2. 権限の集中化:クライアントは「行動報告」のみを行い、生成の進行管理や判定(権限)はすべてサーバー(GameState)が一元管理する。
  3. フェイルセーフの構築:マルチプレイではクライアントの切断やフリーズが起こり得るため、進行不能を防ぐためのタイムアウト処理(Timer)を必ず仕込む。

3. 実装と修正の詳細 (Technical Diff)【要約厳禁】

  • BP_MyGameState
  • 対象の関数/イベント: Event_InitDungeon (Custom Event / Server RPC)
  • 追加・変更した変数:
    • IsDungeonBuilding (Boolean / 初期値: False / Replication: Replicated)
  • 実装内容:
    • ダンジョン生成要求の入り口で、排他制御(二重生成防止)を行います。
    • Event_InitDungeon の実行ピンを Branch に繋ぎ、Conditionに IsDungeonBuilding を指定。
    • False の場合: Set IsDungeonBuilding (True) を実行後、Get Actor Of ClassBP_DungeonManager を取得し、後続のダンジョン生成ロジックへ流す。
    • True の場合: Print String で「ダンジョン生成中のため要求を破棄します」と出力し、処理を強制終了させる(関所)。
    • 生成ロジック内で、古いダンジョンの破棄(Destroy)の後に Delay (0.5s) を挿入し、GCと物理スレッドのクリアを待機させる猶予時間を設ける。
Event_InitDungeon の「関所(Branch)」と Delay を含むBlueprint全体図の画像
  • BP_MyGameState
  • 対象の関数/イベント: Server_CompleteDungeonGeneration (Custom Event / Server RPC)
  • 追加・変更した変数:
    • BuildTimeoutHandle (TimerHandle)
  • 実装内容:
    • クライアント全員の生成完了通知を待つハンドシェイク処理と、強制進行のタイムアウトを実装します。
    • 事前にイベント外で Set Timer by Event (Time: 15.0) を実行し、戻り値を変数 BuildTimeoutHandle にセットしておく。
    • Server_CompleteDungeonGeneration(全員の準備完了を検知した瞬間)の直後に、Clear and Invalidate Timer by Handle (Handle: BuildTimeoutHandle) を接続してタイムアウトを解除する。
    • 一連のワープ・遷移処理の一番最後に Set IsDungeonBuilding (False) を接続し、次のダンジョン生成を許可する。
 Server_CompleteDungeonGeneration に組み込んだ Clear and Invalidate Timer by Handleのタイマー制御処理の画像
  • BP_MyGameState
  • 対象の関数/イベント: 階層移動のインクリメント修正(内部ロジック)
  • 追加・変更した変数:
    • Floor Index (Integer / Replication: RepNotify)
  • 実装内容:
    • プレイヤーを次階層にワープさせる For Each LoopCompletedピン から、加算後の値を用いて Set Floor Index を実行するようにノードを繋ぎ直す(ループ中ではなく、確実に全員が移動した後に階層を進める)。
    • その後、Branch の判定条件を Floor Index > 3(3階層以上でボス部屋やクリアなどの条件)に修正。

4. トラブルシューティング(発生した不具合と解決策)

不具合: 連続生成による UnrealEditor_ChaosACCESS_VIOLATION
解決策:
抜本的な解決にはC++レイヤーでのPhysics Thread完了検知が必要ですが、今回はBlueprintsレイヤーでの堅牢化を行いました。

  1. IsDungeonBuilding による入り口でのリクエスト破棄により、物理エンジンが悲鳴を上げる前に論理的に命令を遮断しました。
  2. 古いダンジョンのDestroyと新しいBuildの間に Delay (0.5s) を挿入することで物理エンジンに掃除の時間を与えました。
    ※Delayによる対応は「クラッシュ率を下げる暫定処置」ですが、現在のゲーム仕様(短時間で連続してダンジョンを生成・遷移するプレイングは不可能)と排他制御(フラグ)を組み合わせることで、実質的な回避策として成立させています。

5. 未来の自分へ:次回実装時のテンプレート(標準手順)

今回得た知見をもとに、非同期アセットやマルチプレイでの状態遷移を実装する際のチェックリストを残します。

  1. 防衛的プログラミングの徹底
  • 外部アセットの完了イベント(On ○○ Complete)は、Game Threadの終了であって物理(Chaos)やレンダリングの終了ではないと疑うこと。
  • 連続で呼ばれるとマズい処理には、必ず先頭に Boolean による Branch の関所を作る(排他制御)。
  1. マルチプレイの同期原則
  • クライアントはRPCで「行動報告」だけを行う。
  • GameState などのサーバー権限側でフラグを管理する。
  • 同期待ちが発生する処理には、必ず Set Timer by Event でタイムアウト(10〜15秒など)を設定し、万が一の切断・フリーズ時に Clear and Invalidate Timer by Handle で解除するか、強制進行させるフェイルセーフを構築する。完成時の「面白さ」と同じくらい、「壊れないためのガード」にコストをかけること。

※ English version below.
※ The English text is AI-translated from the original Japanese article.

[English Version]

UE5 + Dungeon Architect: Implementing Concurrency Control and Physics Interference Avoidance for Multiplayer

1. Objective and Problem Solved

While building an automatic dungeon generation and transition system for a multiplayer game using Dungeon Architect (DA), I encountered a critical issue: Executing dungeon generation (Build Dungeon) repeatedly caused an ACCESS_VIOLATION in UnrealEditor_Chaos (the physics engine), crashing the entire editor.

To solve this, I implemented a robust multiplayer dungeon transition system by introducing a “Mutex (Mutual Exclusion)” to prevent duplicate generation requests and a “Handshake (Synchronization & Timeout Management)” system to synchronize generation completion between the server and clients.

2. Design Philosophy (Why & How)

[Analysis of the Issue: Why does it crash?]
Analyzing the stack trace upon crashing revealed the root cause to be a “Race Condition” in asynchronous processing.
Even if the old meshes are destroyed using Destroy Dungeon, the next generation command triggers before the background Chaos engine finishes clearing collision data and performing Garbage Collection (GC). This results in a memory collision of physics data, triggering the ACCESS_VIOLATION.

[Hypothesis Validation and Design Philosophy]
DA provides an event (Callback) called On Dungeon Build Complete. However, this only notifies the “completion of Actor generation on the Game Thread (Logical Completion)” and does NOT guarantee the “completion of collision calculation and registration on the Physics Thread (Physical Completion)”.

Based on this insight, I applied the following design philosophies:

  1. Never blindly trust asset callbacks: When dealing with complex asynchronous external systems (like DA), treat standard notification events as approximations. Always implement custom state management flags (Mutex).
  2. Centralize Authority: Clients should only send “Action Reports.” The server (GameState) must handle all progression management and validation (Authority).
  3. Build Failsafes: In multiplayer environments, client disconnects or freezes are inevitable. Always implement a timeout mechanism (Timer) for processes waiting for synchronization to prevent softlocks.

3. Implementation Details (Technical Diff) [DO NOT SUMMARIZE]

  • BP_MyGameState
  • Target Function/Event: Event_InitDungeon (Custom Event / Server RPC)
  • Variables Added/Modified:
    • IsDungeonBuilding (Boolean / Default: False / Replication: Replicated)
  • Details:
    • Implemented a mutex at the entry point of the dungeon generation request to prevent duplicate execution.
    • Connected the execution pin of Event_InitDungeon to a Branch, setting the Condition to IsDungeonBuilding.
    • If False: Execute Set IsDungeonBuilding (True), then use Get Actor Of Class (BP_DungeonManager) to proceed with the dungeon generation logic.
    • If True: Execute Print String (“Dungeon generation in progress, discarding request”) and immediately terminate the execution (Gatekeeper).
    • Inside the generation logic, inserted a Delay (0.5s) after destroying the old dungeon to provide a buffer period for GC and the Physics Thread to clear data.

📸 [Insert image of the entire BP showing the Gatekeeper (Branch) and Delay in Event_InitDungeon here]
📸 [Insert image of the console log showing “Dungeon generation in progress, discarding request” here]

  • BP_MyGameState
  • Target Function/Event: Server_CompleteDungeonGeneration (Custom Event / Server RPC)
  • Variables Added/Modified:
    • BuildTimeoutHandle (TimerHandle)
  • Details:
    • Implemented a handshake process waiting for all clients to report completion, along with a forced progression timeout.
    • Previously, outside this event, executed Set Timer by Event (Time: 15.0) and saved the return value to the BuildTimeoutHandle variable.
    • Immediately after Server_CompleteDungeonGeneration (the moment all clients are verified as ready), connected Clear and Invalidate Timer by Handle (Handle: BuildTimeoutHandle) to cancel the timeout.
    • At the very end of the warping/transition sequence, connected Set IsDungeonBuilding (False) to allow subsequent dungeon generation requests.

📸 [Insert image of the Clear and Invalidate Timer by Handle logic inside Server_CompleteDungeonGeneration here]

  • BP_MyGameState
  • Target Function/Event: Floor Transition Increment Fix (Internal Logic)
  • Variables Added/Modified:
    • Floor Index (Integer / Replication: RepNotify)
  • Details:
    • Rerouted the execution from the Completed pin of the For Each Loop (which warps players to the next floor) to execute Set Floor Index using the incremented value. This ensures the floor increments only after everyone has definitely warped, not during the loop.
    • After that, modified the Branch condition to Floor Index > 3 (e.g., conditions for boss rooms or clearing after 3 floors).

4. Troubleshooting

Bug: ACCESS_VIOLATION in UnrealEditor_Chaos due to consecutive generation requests.
Fix:
While a fundamental fix requires detecting the completion of the Physics Thread on the C++ level, I hardened the system at the Blueprint level.

  1. By discarding requests at the entry point using IsDungeonBuilding, I logically blocked commands before the physics engine could overload.
  2. By inserting a Delay (0.5s) between destroying the old dungeon and building the new one, I gave the physics engine time to clean up.
    Note: Although the Delay is a “provisional measure to reduce crash rates,” combining it with the mutex flag and the actual game design (which prevents players from triggering transitions repeatedly in a short timeframe) successfully functions as a practical workaround.

5. Template for Future Implementation

Based on this experience, here is a standardized checklist for implementing asynchronous assets or state transitions in multiplayer in the future:

  1. Strict Defensive Programming:
  • Always suspect that an external asset’s completion event (e.g., On XX Complete) only means the Game Thread has finished, not the Physics (Chaos) or Render threads.
  • For processes that cause fatal errors if called consecutively, always set up a Gatekeeper using a Branch with a Boolean at the very beginning (Mutex).
  1. Multiplayer Synchronization Principles:
  • Clients must only use RPCs for “Action Reports.”
  • Flags must be managed by the server authority (e.g., GameState).
  • For any process requiring synchronization waits, always set a timeout (e.g., 10-15 seconds) using Set Timer by Event. Build a failsafe to either force progression or use Clear and Invalidate Timer by Handle upon success, preventing softlocks due to client crashes or disconnects. Budgeting time for “guards to prevent breaks” is just as vital as building the “fun factor.”

コメント

タイトルとURLをコピーしました