Impostor
概要
HostMode
トポロジーを使用しています。Fusion Impostor は、最大8人のプレイヤー向けのソーシャルディダクションゲームのコアループを開発する方法と、FusionプロジェクトにおけるPhoton Voice SDKとの通信の統合と処理方法を示しています。Photon Voiceに関する詳細は、マニュアルのボイスページをご覧ください。
Fusion Impostorは、元々Fusion 1.0を使用して作成されましたが、Fusion 2.0に移植されており、Fusion 1.0バージョンの大部分の機能を保持しています。
このサンプルの主な特長は以下のとおりです:
- プレゲームロビーおよびゲーム内での音声通信
- プレゲーム、プレイ、会議、ポストゲームの結果を含む完全なネットワーク対応ゲームステートマシンとシステム
- タスクステーションやクルーメイトの遺体などの共有インタラクションポイント
- カスタマイズ可能なゲーム設定(インポスターの数、移動速度、プレイヤーの衝突など)
- 扉のようなワールド内のオブジェクトの同期された状態
- モジュラーインタラクションシステムに基づいたさまざまなクルーメイトタスク
- Photon Voiceを使用してさまざまな音声通信タイプを処理
- クライアントが参加するためのコードを使用してホストとして部屋を設定
- リージョン設定、ニックネーム、マイクの選択
技術情報
- Unity 2021.3.33f1
始める前に
サンプルを実行するには:
- PhotonEngine DashboardでFusion AppIdを作成し、それをFusionメニューからアクセスできるリアルタイム設定の「App Id Fusion」フィールドに貼り付けます。
- PhotonEngine DashboardでVoice AppIdを作成し、それをリアルタイム設定の「App Id Voice」フィールドに貼り付けます。
- その後、
Launch
シーンを読み込み、Play
を押します。
ダウンロード
バージョン | リリース日 | ダウンロード | |
---|---|---|---|
2.0.1 | May 30, 2024 | Fusion Impostor 2.0.1 Build 560 |
フォルダ構造
主なScriptsフォルダ/Scripts
には、サンプル内の主要なネットワーキング実装とネットワーク対応ステートマシンを含むNetworking
というサブフォルダがあります。他のサブフォルダには、それぞれゲームプレイの動作や管理に関するロジックを含むPlayer
やManagers
があります。
PlayerRegistry
PlayerRegistry
は、部屋内の各プレイヤーへの参照を保存し、1人または多数のプレイヤーに対して選択やアクションを実行するためのユーティリティメソッドを提供します。
ゲームへの参加
ユーザーは、ルームコードを使用してルームに参加するか、ホストすることができます。ホストする場合、ルームコードの入力はオプションです。ルームに入ると、参加するためのコードが画面の下部に表示されます。
runner.SessionInfo.Name
を介してアクセスされます。NetworkStartBridge
はNetworkDebugStart
の仲介役として機能します。StartHost()
は、特定のコードが指定されていない場合、RoomCode
からランダムな4文字の文字列を取得します。
プレゲーム
プレゲームフェーズでは、プレイヤーはロビーエリアの中央にあるテーブルから自分の色を選択したり、設定から好みのマイクデバイスを選択したりできます。ホストはゲーム設定をカスタマイズすることができ、ゲームを開始する責任があります。
入力の処理
ネットワーク対応の入力はPlayerInputBehaviour.cs
スクリプトでポーリングされます。ここでは入力のブロッキングも行われます。さらに、入力を実行する前にPlayerMovement.cs
でサーバー側のチェックが行われます。
キーボード
- 移動にはWASDを使用
- インタラクトにはEを使用
- ゲームを開始するにはテンキーのEnterを使用(ホストの場合、プレゲーム中のみ)
マウス
- 移動には左クリックを使用
- インタラクションにはUIのボタンをクリック
プレイヤー
プレイヤーの動作は、3つの異なるコンポーネントによって定義されます:
PlayerObject
: このオブジェクトが関連付けられているPlayerRef
への参照を保持し、部屋内でのプレイヤーのインデックス、ニックネーム、選択された色を含みます。PlayerMovement
: プレイヤーの移動と入力を担当します。また、ゲームプレイに必要なデータとメソッドを保持しており、特にIsDead
、IsSuspect
、EmergencyMeetingUses
プロパティがあります。PlayerData
: プレイヤーの視覚コンポーネントです。主にマテリアルを扱い、アニメーターのプロパティを設定し、ニックネームUIをインスタンス化します。
インタラクタブル
- カラーキオスク:プレゲームルームの中央テーブルに位置しています。プレイヤーは、他のプレイヤーによってすでに選ばれていない12のプリセットカラーの中から選択できます。
- 設定キオスク:プレゲームルームの上部に位置しており、ホストはここでゲーム設定を選択し、ゲームを開始できます。
- 緊急ボタン:緊急ボタンは、1ラウンドあたり限られた回数だけ押すことができ、会議を呼び出します。
- タスク:マップ全体に14のタスクステーションが配置されており、5つのユニークなタスクミニゲームがクルーメイトが完了するために用意されています。
- 遺体:殺害されたプレイヤーの遺体は、クルーメイトや自らの痕跡を隠そうとするインポスターが、無料で会議を呼び出すために報告できます。
タスク
タスクステーションはマップ全体に存在します。クルーメイトは、範囲内にいるときにこれらとインタラクトできます。
- サーモスタット (
TemperatureTask.cs
):上矢印と下矢印を押して2つの数字を等しくします。 - スライダー (
SlidersTask.cs
):各スライダーを赤いアウトラインに合わせてドラッグします。正しく配置されるとロックされます。 - パターンマッチ (
PatternMatchTask.cs
):左のパネルで点滅するライトのシーケンスに合わせて右のパネルのボタンを押します。 - 数字シーケンス (
NumberSequenceTask.cs
):各数字を昇順に押します(1-8)。 - ファイルダウンロード (
DownloadTask.cs
):ダウンロードボタンを押して、バーが満たされるのを待つことでタスクを完了します。
ボイス
Fusion ImpostorにおけるVoice 2統合では、Photon Voice 2が提供する2つのスクリプトが使用されています:
- FusionVoiceNetworkは
PrototypeRunner
プレハブに追加されています。 - VoiceNetworkObjectは
Player
プレハブで使用されており、指定されたプレハブの子としてSpeaker
も含まれています。
移行ノート
既に述べたように、Fusion ImpostorはFusion 1.0からFusion 2.0に移植されました。Fusion 1.0からFusion 2.0への移行に関する詳細はこちらで読むことができます。以下は、その移植プロセス中に行われた変更のいくつかです。
FSM
このサンプルのFusion 1.0バージョンでは、ゲームステートを管理するためにカスタムの有限状態機械(FSM)が使用されていました。このサンプルでは、Fusion 2.0のFSMアドオンを使用して、異なるゲームステートとその関連スクリプトをよりクリーンに整理することを目指しています。Main GameObjectの階層には、次のものが含まれるようになりました:
各ステートは、StateMachineシステムによって使用されるNetworkBehaviour
であるStateBehaviour
を継承します。これには、ネットワーク側で処理されるOnEnterState
やOnExitState
などの関数が含まれています。また、ネットワーク対応のゲームプレイには影響しないゲーム内のレンダリング変更のために使用されるOnEnterStateRender
やOnExitStateRender
などの他のメソッドも含まれています。以下は、このサンプルのVotingResultsStateBehaviour
を使用した例です:
C#
/// <summary>
/// State for handles the game once voting has finished
/// </summary>
public class VotingResultsStateBehaviour : StateBehaviour
{
/// <summary>
/// Which state will we go to next
/// </summary>
private StateBehaviour nextState;
/// <summary>
/// How long we will wait before going to the next state in seconds.
/// </summary>
private float nextStateDelay;
protected override void OnEnterState()
{
// If a player has been ejected...
if (GameManager.Instance.VoteResult is PlayerObject pObj)
{
pObj.Controller.IsDead = true;
pObj.Controller.Server_UpdateDeadState();
int numCrew = PlayerRegistry.CountWhere(p => !p.Controller.IsDead && p.Controller.IsSuspect == false);
int numSus = PlayerRegistry.CountWhere(p => !p.Controller.IsDead && p.Controller.IsSuspect == true);
if (numCrew <= numSus)
{ // impostors win if they can't be outvoted in a meeting
WinStateBehaviour winState = Machine.GetState<WinStateBehaviour>();
winState.crewWin = false;
nextState = winState;
}
else if (numSus == 0)
{ // crew wins if all impostors have been ejected
WinStateBehaviour winState = Machine.GetState<WinStateBehaviour>();
winState.crewWin = true;
nextState = winState;
}
else
{ // return to play if the game isn't over
nextState = Machine.GetState<PlayStateBehaviour>();
}
nextStateDelay = 3f;
}
else
{ // return to play if there was nobody ejected
nextState = Machine.GetState<PlayStateBehaviour>();
nextStateDelay = 2f;
}
}
protected override void OnEnterStateRender()
{
GameManager.im.gameUI.EjectOverlay(GameManager.Instance.VoteResult);
}
protected override void OnFixedUpdate()
{
if (Machine.StateTime > nextStateDelay)
{
Machine.ForceActivateState(nextState);
}
}
}
これにより、ステートに入ったときにOnEnterState
はステート権限を持つプレイヤー、つまりホストのみによって呼び出され、ゲームプレイに影響を与えるメソッドが実行されます。しかし、すべてのクライアントはOnEnterStateRender
を実行するため、ゲームのUIは投票の結果を正しく表示します。
KCCとラグ補償
このサンプルの元のバージョンは、Fusion 1.0のKCCを使用していました。このバージョンは、Fusion 2.0のAdvanced KCCに似ていますが、Fusion 2.0のSimple KCCはこのゲームには十分です。最大の変更点は、Simple KCCを使用することで、Advanced KCCに存在するOnCollisionEnterおよびOnCollisionExitがもはや存在しないことです。この問題を解決するために、インタラクタブルや他のプレイヤーとの衝突チェックは、PlayerMovement
のFixedUpdateNetwork
メソッドに移動し、ラグ補償を使用して行われます。プレイヤーは特に接続が不良なゲーム中に移動する際に、インポスターのキル範囲外に落ちる可能性があるため、ラグ補償がこのサンプルに統合され、衝突検出がより正確になりました。ラグ補償についての詳細はこちらで読むことができます。以下のコードは、PlayerMovement
のFixed Update Networkメソッド内で、更新されたKCCとラグ補償がどのように機能するかを示しています:
C#
public override void FixedUpdateNetwork()
{
bool hasInput = GetInput(out PlayerInput input);
if (hasInput && input.IsDown(PlayerInputBehaviour.BUTTON_START_GAME))
{
GameManager.Instance.Server_StartGame();
}
Vector3 direction = default;
bool canMoveOrUseInteractables = activeInteractable == null && GameManager.Instance.MeetingScreenActive == false && GameManager.Instance.VotingScreenActive == false && hasInput;
if (canMoveOrUseInteractables)
{
// BUTTON_WALK is representing left mouse button
if (input.IsDown(PlayerInputBehaviour.BUTTON_WALK))
{
direction = new Vector3(
Mathf.Cos((float)input.Yaw * Mathf.Deg2Rad),
0,
Mathf.Sin((float)input.Yaw * Mathf.Deg2Rad)
);
}
else
{
if (input.IsDown(PlayerInputBehaviour.BUTTON_FORWARD))
{
direction += TransformLocal ? transform.forward : Vector3.forward;
}
if (input.IsDown(PlayerInputBehaviour.BUTTON_BACKWARD))
{
direction -= TransformLocal ? transform.forward : Vector3.forward;
}
if (input.IsDown(PlayerInputBehaviour.BUTTON_LEFT))
{
direction -= TransformLocal ? transform.right : Vector3.right;
}
if (input.IsDown(PlayerInputBehaviour.BUTTON_RIGHT))
{
direction += TransformLocal ? transform.right : Vector3.right;
}
direction = direction.normalized;
}
}
simpleCC.Move(direction * Speed);
if (direction != Vector3.zero)
{
Quaternion targetQ = Quaternion.AngleAxis(Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg - 90, Vector3.down);
cc.SetLookRotation(Quaternion.RotateTowards(transform.rotation, targetQ, lookTurnRate * 360 * Runner.DeltaTime));
}
// Performs an overlap sphere test to see if the player is close enough to interactables
int lagHit = Runner.LagCompensation.OverlapSphere(transform.position, cc.Settings.Radius, Object.InputAuthority, lagCompensatedHits, _interactableLayerMask,
options: HitOptions.IncludePhysX);
// Can the player report, kill, or use the interactable.
bool canReport = false, canKill = false, canUse = false;
// The lists of nearby players and interactables are cleared with every check.
nearbyInteractables.Clear();
nearbyPlayers.Clear();
// Iterates through the results
for (int i = 0; i < lagHit; i++)
{
if (lagCompensatedHits[i].Hitbox is Hitbox hb)
{
// We don't bother tryingt to find nearby players if we are the suspect.
if (IsSuspect && !hb.transform.IsChildOf(transform) && hb.gameObject.layer == _playerRadiusLayerMask && hb.GetComponentInParent<PlayerObject>() is PlayerObject player)
{
nearbyPlayers.Add(player);
canKill = true;
}
continue;
}
GameObject hitGameObject = lagCompensatedHits[i].Collider.gameObject;
if (hitGameObject.TryGetComponent<Interactable>(out var hitInteractable))
{
if (!nearbyInteractables.Contains(hitInteractable))
nearbyInteractables.Add(hitInteractable);
if (hitInteractable is DeadPlayer)
canReport = true;
else
canUse = hitInteractable.CanInteract(this);
}
}
if (HasInputAuthority)
{
GameManager.im.gameUI.reportButton.interactable = canReport;
GameManager.im.gameUI.killButton.interactable = canKill;
GameManager.im.gameUI.useButton.interactable = canUse;
}
if (!canMoveOrUseInteractables)
return;
actionPerformed = false;
// When pressing the interact button, there's no clear way to know what action is being done, so this order is used.
if (input.IsDown(PlayerInputBehaviour.BUTTON_REPORT) || input.IsDown(PlayerInputBehaviour.BUTTON_INTERACT))
TryToReportDeadPlayer();
if (input.IsDown(PlayerInputBehaviour.BUTTON_USE) || input.IsDown(PlayerInputBehaviour.BUTTON_INTERACT))
TryToUseStation();
if (input.IsDown(PlayerInputBehaviour.BUTTON_KILL) || input.IsDown(PlayerInputBehaviour.BUTTON_INTERACT))
TryKill();
}
さらに、ラグ補償に対応するために以下の変更が行われました:
Hitbox Manager
コンポーネントがメインのNetworkRunner
プレハブに追加されました。Hitbox Root
コンポーネントがPlayer
プレハブのルートに追加されました。Hitbox
コンポーネントがPlayer
プレハブのKill Radius
GameObjectに追加され、そのCollider
コンポーネントが削除されました。
スクリプトとプロトタイピング
Fusion 1.0に存在し、PlayerSpawnerPrototype
などのプロトタイピング用ツールを含むAssets/Fusion/Scripts
フォルダは、Fusion 2.0には存在しなくなりました。このディレクトリ内の多くのスクリプトはアップグレード可能でしたが、多くは冗長で、過度に複雑で、または不要なものであったため、これらのアイテムはこのサンプルから削除されました。保存された唯一のスクリプトはInputBehaviourPrototype
で、Assets/Scripts/Networking
に移動されました。新たに作成されたもう1つのクラスはPlayerSpawner
で、これはプレイヤーが参加したときにNetworkObject
を生成する役割を持つ、メインのNetworkRunner
プレハブに付随するSimulationBehaviour
です。
C#
public class PlayerSpawner : SimulationBehaviour, IPlayerJoined
{
public NetworkObject playerObject;
public void PlayerJoined(PlayerRef player)
{
if (Runner.IsServer)
{
NetworkObject spawnedPlayer = Runner.Spawn(playerObject, position: GameManager.Instance.preGameMapData.GetSpawnPosition(player.AsIndex), inputAuthority: player);
}
}
public void PlayerLeft(PlayerRef player)
{
if (Runner.IsServer)
{
PlayerObject leftPlayer = PlayerRegistry.GetPlayer(player);
if (leftPlayer != null)
{
Runner.Despawn(leftPlayer.Object);
}
}
}
}
プレイヤーが参加すると、このスクリプトはサーバーが新しいインスタンスのplayerObject
を生成することを確実にし、適切な位置とinputAuthority
を持つようにします。プレイヤーが離れると、PlayerRegistry
は離脱したプレイヤーのPlayerRef
に関連付けられたPlayerObject
を返し、そのプレイヤーをデスポーンさせます。