Impostor
概述
主機端模式
拓撲。Fusion Impostor 展示了如何為最多8名玩家開發社交推理遊戲的核心迴圈,以及如何將 Photon Voice SDK與Fusion專案整合和處理通信。有關Photon Voice的更多資訊,請參閱操作手冊中的語音頁面。
Fusion Impostor 最初是使用Fusion 1.0建立的;然而,它已經移轉到Fusion 2.0,但保留了Fusion 1.0版本的大部分功能。
此範例的一些聚焦點是:
- 遊戲前大廳和遊戲內的語音通信
- 完全已連網的遊戲狀態機器和系統,包括遊戲前、遊玩、會議和遊戲後結果
- 共享互動點,如任務站和船員身體
- 可自訂的遊戲設定(冒名頂替者數量、移動速度、玩家碰撞等)
- 世界中物件(如門)的已同步狀態
- 組建在模組化互動系統上的各種船員任務
- 使用Photon Voice提供處理各種語音通信類型
- 使用代碼將房間設定為主機端以供客戶端加入
- 地區設置、暱稱和麥克風選擇
技術資訊
- Unity 2021.3.33f1
在您開始之前
為了運行範例
- 在Photon引擎儀表板中建立一個Fusion應用程式帳號,然後將它貼上到Real Time設定(可從Fusion選單到達)中的
App Id Fusion
欄位。 - 在Photon引擎儀表板中建立一個Voice應用程式帳號,然後將它貼上到Real Time設定中的
App Id Voice
欄位。 - 然後載入
Launch
場景並且按下Play
。
下載
版本 | 發佈日期 | 下載 | |
---|---|---|---|
2.0.1 | May 30, 2024 | Fusion Impostor 2.0.1 Build 560 |
資料夾架構
主要指令碼資料夾/Scripts
有一個名為Networking
的子資料夾,其中包括範例中的主要網路執行方式,以及已連網狀態機器。其他子資料夾,如Player
和Managers
,分別包含遊戲遊玩行為和管理的邏輯。
玩家登錄
PlayerRegistry
儲存對房間中每個玩家的參照,並提供對一個或多個玩家進行選擇和執行動作的公用程式方法。
進入一場遊戲
使用者可以使用房間代碼加入或主持一個房間。如果使用者選擇主持,輸入房間代碼是可選的。一旦進入房間,加入代碼將顯示在畫面頂部。
runner.SessionInfo.Name
。NetworkStartBridge
充當NetworkDebugStart
的中介。如果未指定特定程式碼,StartHost()
將從RoomCode
中隨機獲取4個字元的字串。
遊戲前
在遊戲前階段,玩家可以從大廳區域中心的桌子上選擇設定自己的顏色,從設定中選擇自己喜歡的麥克風設備。主機端可以自訂遊戲設定,並負責啟動遊戲。
處理輸入
已連網輸入在PlayerInputBehaviour.cs
指令碼中進行輪詢。輸入阻擋也是在這裡完成的。此外,在執行輸入之前,伺服器端檢查是在PlayerMovement.cs
中完成的。
鍵盤
- WASD以行走
- E以互動
- 數字鍵盤的Enter以開始遊戲(僅在遊戲前階段的主機端使用)
滑鼠
- 左鍵按一下以行走
- 按一下UI中的按鈕以互動
玩家
玩家行為由三個不同的元件定義:
PlayerObject
:保存對與該物件相關聯的PlayerRef
的參照,並包含玩家在房間中的索引、暱稱和所選顏色。PlayerMovement
:負責玩家的運動和輸入。 它還保存了遊戲遊玩的基本資料和方法,尤其是IsDead
、IsSuspect
和EmergencyMeetingUses
屬性。PlayerData
:玩家的視覺效果元件。它主要處理材質、設定動畫屬性和具現化暱稱UI。
可互動
- 色彩台:位於遊戲前房間的中央桌子上。玩家可以從12種預設顏色中選擇任何一種未被其他玩家使用的顏色。
- 設定台:位於遊戲前房間的頂部,主機端可以選擇遊戲設定並從這裡開始遊戲
- 緊急按鈕:每回合可按下有限次數的緊急按鈕來召集會議
- 任務:地圖上有14個任務站,配有5個獨特的任務迷你遊戲,供船員完成
- 屍體:船員或試圖掩蓋行踪的冒名頂替者,可以報告被謀殺玩家的屍體來自由召開會議
任務
任務站可以在地圖上找到。當它們在互動範圍內時,船員可以與它們互動。
- 溫度計 (
TemperatureTask.cs
):按上下箭頭,使兩個數字相等 - 滑塊 (
SlidersTask.cs
):拖動每個滑塊以與紅色輪廓對齊。正確定位後,它們將被鎖定。 - 圖案配對 (
PatternMatchTask.cs
):按下右側面板上的按鈕,使其與左側面板上閃爍的燈光序列相匹配。 - 數字序列 (
NumberSequenceTask.cs
):按升冪(1-8)推每個數字 - 下載檔案 (
DownloadTask.cs
):按下下載按鈕,等待條形圖填滿即可完成。
Voice
對於Fusion Impostor中的Voice 2整合,使用了Photon Voice 2提供的兩個指令碼:
- FusionVoiceNetwork 新增到
PrototypeRunner
預製件中。 - VoiceNetworkObject 用於
Player
預製件,Speaker
也作為給定預製件的下層。
移轉備註
如前所述,Fusion Impostor 從Fusion 1.0移轉到Fusion 2.0。您可以在此閱讀更多關於從Fusion 1.0移轉到Fusion 2.0的資訊。以下是在移轉過程中所做的一些更改。
FSM
本範例的Fusion 1.0版本使用自訂的有限狀態機來管理遊戲狀態。本範例使用Fusion 2.0的FSM附加元件,目的是更乾淨地組織不同的遊戲狀態及其伴隨的指令碼。在主遊戲物件的層次結構中,現在有以下內容:
每個狀態都繼承自StateBehaviour
(是狀態機系統使用的NetworkBehaviour
)。它們具有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的進階KCC;然而,簡單KCC對於Fusion 2.0來說已經足夠了。最大的變化是,透過使用簡單KCC,進階KCC中的OnCollisionEnter和OnCollisionExit不再存在。為了解決這個問題,使用延遲補償將可互動物和其他玩家的碰撞檢查移動到PlayerMovement
的FixedUpdateNetwork
方法。由於玩家在移動時可能會脫離冒名頂替者的殺傷範圍,尤其是在連線不良的遊戲中,因此將延遲補償整合到此範例中,以更準確地檢測碰撞。您可以在此閱讀更多關於延遲補償的資訊。以下程式碼顯示了更新後的KCC和延遲補償如何在PlayerMovement
的固定更新網路方法中工作:
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
遊戲物件中,其Collider
元件被移除。
指令碼與原型
Fusion 1.0中呈現的以下資料夾Assets/Fusion/Scripts
包含各種原型工具,如PlayerSpawnerPrototype
,在Fusion 2.0中已不存在。雖然該目錄中的許多指令碼都可以升級,但其中許多都是多餘的、過於複雜的和/或不必要的,因此這些項目已從該範例中刪除。唯一保留的指令碼是InputBehaviourPrototype
,其已經被移動到Assets/Scripts/Networking
。另一個唯一建立的新類別是PlayerSpawner
,這是一個附加在主NetworkRunner
預製件上的SimulationBehaviour
,用於在玩家加入時處理NetworkObject
的生成:
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
,並取消生成該玩家。