Tanknarok
概要
Fusion Tanknarokのサンプルは、小規模なマルチプレイヤーのアリーナスタイルのタンクゲームを構築する方法を示しています。このゲームはホストモード
または共有モード
のいずれかで実行されます。ホストモードでは、独自のサーバー(スタンドアロンアプリケーションまたはサーバーとクライアントの両方を実行する単一のアプリケーション)がゲームを管理します。サーバーは、ゲームオブジェクトとその動作を制御します。共有モードでは各クライアントが自分自身のオブジェクトに権限を持ち、1つのクライアントが「共有」オブジェクトを制御します。つまり、各クライアントは自分自身のオブジェクトに対して権威を持ちますが、すべてのプレーヤー間で共有されるオブジェクトを管理するクライアントが1つあります。
Polyblock Studiosが提供したゲームプレイのロジック、オーディオ、素晴らしいグラフィックスが使用されているため、このサンプルに類似したゲームをSteamで見かけるかもしれません。このサンプルはPhoton Fusionに移植された実際のゲームのほんの一部です。元のゲームはPhoton Boltを使用して作成されました。
Fusion Tanknarokサンプルは、予測ネットワークシステムと非決定論的な物理エンジンであるPhysXの組み合わせによる複雑さを避けながら、物理的な効果をどのように実現するかを示しています。Fusionは、必要に応じてUnityのrigidbodyの同期を完全にサポートしています。
始める前に
3Dテンプレートで新しいUnityプロジェクトを作成します。Project Settings > Player > Other Settings > Color Space
でカラースペースを「Linear」に設定してください。
サンプルのダウンロードやインポートをおこなう前に、プロジェクトにUnity Post Processingパッケージが含まれていることを確認してください。
Window > Package Manager
を開きますPackages: Unity Registry
を選択します- "Post Processing"を検索して、
- パッケージをインストールします。
スクリーンショット
ダウンロード
バージョン | リリース日 | ダウンロード | |
---|---|---|---|
2.0.1 | Jun 10, 2024 | Fusion Tanknarok 2.0.1 Build 569 |
ハイライト
- 共有モードとホストモードに対応
- ラグ補償されたレイキャスト
- 予測スポーン
- オブジェクトプーリング
- 完全なゲームループ
プロジェクト
デモを実行する前に、Photon CloudのFusionのAppIDを作成しPhotonAppSettings
アセットにコピーアンドペーストする必要があります。AppIDは Photonダッシュボードから作成できます。RealtimeIDだけでなく__Fusion__ AppIDを作成してください。
Photonアプリ設定のアセットはFusionメニュー「Fusion > Realtime Settings」から選択できます。
生成されたAppIDをApp Id Fusion
フィールドにペーストします。
フォルダ構造
Tanknarokから派生するサンプルのコードは/Scripts
フォルダにあります。一般的なユーティリティのための Utilityサブフォルダと、この例に特化しないFusionユーティリティのための
FusionHelpers`フォルダがあります。
残りのTanknarok
フォルダには実際のゲームコードが、以下のサブフォルダに分かれて格納されています:
- Audio - サウンドエフェクトと音楽
- Camera - カメラの配置コード
- Level - すべてのレベルロジック、挙動、パワーアップ、およびその他の非プレイヤーアイテム
- Player - すべてのタンクコントロール、タンクのビジュアルとエフェクトのほか武器と弾丸のロジック
- UI - ユーザーインターフェースコンポーネント
メインフォルダでのメインエントリーポイントはGameLauncher
クラスで、トップレベルマネージャーはGameManager
、LevelManager
およびPlayerManager
です。
Quick Fusion Primer
Fusionでは、NetworkObject
コンポーネントによってネットワークの状態を識別します。ネットワークの状態を持つゲームオブジェクトはNetworkObject
も必ず持っている必要があります。NetworkObject
自体は、ゲームオブジェクトにネットワーク全体の識別子を割り当てるだけで、実際のネットワークの状態はNetworkBehaviour
から派生したコンポーネントに格納されます。Fusionにはいくつかのデフォルトの挙動が含まれており、例えばUnityのTransformを同期するNetworkTransform
があります。
物理敵な挙動が物理状態をFixedUpdate()
で変化させるのと同様に、NetworkBehaviour
はネットワークの状態をFixedUpdateNetwork()
メソッドで変化させます。これはレンダリングのフレームレートやネットワークからの更新とは別に、tickと呼ばれる固定の時間ステップによって発生します。各アップデートでは前のtickの状態を元に作業が行われます。特定のtickの状態がネットワークによって確認されるとFusionはオブジェクトの状態をそのtickに戻し、そのtickから現在のtickまでのすべてのFixedUpdateNetwork()の中間呼び出しを再び適用します。
現在のローカルのtickは常に最後に確認されたtickよりも進んでいるため、そのアップデートは「予測」と呼ばれ、確認された状態の適用およびその後のFixedUpdateNetwork
メソッドの再実行は「ロールバック」、「再シミュレーション」と呼ばれます。
コンポーネントがネットワークの状態を持たない場合でも、シミュレーションの一部となることは可能です。ただし、オーバーヘッドを減らすためにNetworkBehaviour
ではなくSimulationBehaviour
から派生する必要があります。再シミュレーションのため、FixedUpdateNetwork()
メソッドが1フレーム中に何度も呼び出される可能性がある点に留意してください。これは、ネットワークの状態のみを扱う場合にはリセットされるため問題ありませんが、非ネットワークの状態にデルタ変更を適用する際には注意が必要です。
シミュレーション、予測、ネットワークオブジェクトの詳細については、Fusionのマニュアルを参照してください。
GameLauncher
タンクゲームのメインUIはGameLauncher
クラスで処理されます。
ゲームモードが選択されるとGameLauncherはFusionLauncher.Launch()
を呼び出してセッションを確立します。 FusionLauncher
はFusion接続イベントに応答し、提供されたコールバックを呼び出して初期ネットワークオブジェクトをスポーンします:
- GameManager (ホストモードでホストによって、または共有モードでマスタークライアントによってスポーンされます)
- Player (ホストモードでホストによって、または共有モードで各クライアントによってスポーンされます)
Ready Up
プレイヤーのタンクはプレイヤーが接続するとすぐにスポーンし、プレイヤーは他のタンクがスポーンするのを待機する間に操作可能な「ロビー」モードでプレイ可能です。
ゲーム自体はすべての接続されたプレイヤーが準備ができたことを示すまで開始しません。このロジックはすべてのクライアントで実行されますが、GameManager
インスタンスのStateAuthority
を持つクライアントのみがレベルをロードできます。
注: この簡略化されたサンプルではレベルは「ロード」されて「有効化」されるのではなく、最初から初期シーンに両方のレベルが含まれています。
ロードはリモートプロシージャコール(RMC)でおこなわれます。呼び出し元はランダムなレベルインデックスを生成し、それをすべてのクライアントに渡すことで全員が同じレベルをロードすることが保証されます。
C#
if (Object.HasStateAuthority) {
RPC_ScoreAndLoad(-1,0, _levelManager.GetRandomLevelIndex());
}
RPC自体は以下のように定義されます
C#
[Rpc(sources: RpcSources.StateAuthority, targets: RpcTargets.All, InvokeLocal = true, Channel = RpcChannel.Reliable)]
private void RPC_ScoreAndLoad(int winningPlayerIndex, byte winningPlayerScore, int nextLevelIndex)
{
...
}
レベル移行
ロビーからレベルへの移行、およびその逆は TransitionSequence()
コルーチンのLevelManager
で処理されます。移行そのものは完全にローカル時間で実行されますが、トリガーされた場合(RPC_ScoreAndLoadリモートプロシージャコールによって)と終了した場合(GameManager
のステート権限によってのみ設定できるネットワークプロパティであるplayStateをLEVELに設定することによって)、クライアント間で同期されます。
ゲーム終了時にはレベル移行がロビーに戻って勝者が同様の方法で表示され、ループが完了してゲームが「Ready Up」の状態に戻ります。
入力処理
FusionはUnityの標準的な入力処理メカニズムを使用してプレーヤーの入力を取得し、ネットワークを介して送信可能なデータ構造に保存した後にこのデータ構造をFixedUpdateNetwork()
メソッドで処理します。この例では、実際の状態の変更はInputController
クラスによって実装されていますが、実際の状態の変更はPlayer
クラスに委譲されます。
シューティング
このサンプルの戦車には4つの武器があり、2種の当たり判定があります:
- 即時ヒット
- 飛翔体
これらはそれぞれ、HitScan
クラスとBullet
クラスによって実装されます。これらはともにオブジェクトプーリング、予測スポーン、ラグ補償といったFusionの3つの重要な機能を使用します。
オブジェクトプーリング
新しいオブジェクトを生成する際にフレームのドロップを回避するために、常にオブジェクトを破棄してインスタンス化するのではなく古いオブジェクトを再利用することをお勧めします。これは、すべてのゲームにおいて特にUnityやFusionにおいても真です。
これを実現するために、Fusionではアプリケーションが再利用可能なゲームオブジェクトを提供および収集するためのフックを指定することができます。
オブジェクトプールはNetworkObjectPoolを実装する必要があります。これは基本的に、プレハブに基づいてプールからオブジェクトを取得するメソッドとオブジェクトをプールに返して再利用するメソッドを持っています。
予測スポーン
予測スポーンは、クライアントが新しいネットワークオブジェクトの作成を予測し、ステート権限が作成を確認するまで一時的なローカルプレースホルダーを作成します。Fusionは、確認後にプレースホルダーを実際のネットワークオブジェクトに自動的に昇格させます。
手動処理が必要なのは予測が失敗した場合です。これはプレースホルダーを破棄するだけであるかもしれません(プレースホルダーは単なるUnityオブジェクトです)。アプリケーションは、フェードアウトや失敗のビジュアライゼーションなどさまざまな形式の処理を実装することができます。
また、プレースホルダーには状態がないため予測段階での移動をネットワークプロパティにアクセスせずに処理する必要があります。
ラグ補償
ローカルでは、各プレーヤーは自分自身(Input Authority)のオブジェクトの予測された未来バージョンと、他のクライアントのオブジェクトの補完または外挿されたバージョンを見ることができます。どちらもサーバーが見ているものと完全に一致しているわけではありません。したがって、高速移動するオブジェクト(たとえば弾丸)は各マシン上で異なるものに当たる可能性が非常に高いです。弾丸を発射した人が何かがおかしいと気づく可能性が最も高いです。しかし、これと同時にサーバーは当たり判定に権限を持っているため、単に成功したヒットを決定するだけの誤ったクライアントを防ぐ必要があります。
この問題を解決するため、Fusionはラグ補償されたレイキャストをサポートしています。これは、サーバーで実行されている場合でもショットが撃たれた時点でクライアントが見たものを尊重するレイキャストです。この機能を実現するため、シーンの裏側で多くのスナップショット補間の処理が行われていますが、開発者にとってはラグ補償されたレイキャストの実装と使用は通常のUnityのレイキャストと同じくらい簡単におこなえます。
唯一の留意点は、Fusionには独自のコライダーオブジェクトであるHitBox
が存在するということです。HitBox
はオブジェクト階層で完全に包括的なHitBoxRoot
ノードの兄弟または子ノードである必要があります。これにより、Fusionは子ノードのより高価なチェックを行う前にルートを迅速に除外できます。
パフォーマンス上の理由から、HitBox
静的なエンティティに適用されるべきではありません。ただし、レイキャストをブロックするには静的な環境が必要です。このため、ラグ補償されたレイキャストは必要に応じてUnityのコライダーをチェックすることもできます。
ラグ補償はPhysXコライダーのような動的な(つまり移動する)コライダーには適用されないということです。さらに、Unityは静的なコライダーと動的なコライダーの区別をおこなうレイキャストクエリを提供していません。したがって、PhysXコライダーをフィルタリングして静的な結果のみを得るには、HitBox
/HitBoxRoot
とPhysXのCollider
(両方が必要な場合)を動的なオブジェクトの異なるレイヤーに配置することをおすすめします。