This document is about: FUSION 2
SWITCH TO

Impostor

Level 4

概述

此範例使用主機端模式拓撲。

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的子資料夾,其中包括範例中的主要網路執行方式,以及已連網狀態機器。其他子資料夾,如PlayerManagers,分別包含遊戲遊玩行為和管理的邏輯。

玩家登錄

PlayerRegistry儲存對房間中每個玩家的參照,並提供對一個或多個玩家進行選擇和執行動作的公用程式方法。

進入一場遊戲

使用者可以使用房間代碼加入或主持一個房間。如果使用者選擇主持,輸入房間代碼是可選的。一旦進入房間,加入代碼將顯示在畫面頂部。

房間代碼的存取方式是透過:runner.SessionInfo.Name

NetworkStartBridge充當NetworkDebugStart的中介。如果未指定特定程式碼,StartHost()將從RoomCode中隨機獲取4個字元的字串。

遊戲前

在遊戲前階段,玩家可以從大廳區域中心的桌子上選擇設定自己的顏色,從設定中選擇自己喜歡的麥克風設備。主機端可以自訂遊戲設定,並負責啟動遊戲。

處理輸入

已連網輸入在PlayerInputBehaviour.cs指令碼中進行輪詢。輸入阻擋也是在這裡完成的。此外,在執行輸入之前,伺服器端檢查是在PlayerMovement.cs中完成的。

鍵盤

  • WASD以行走
  • E以互動
  • 數字鍵盤的Enter以開始遊戲(僅在遊戲前階段的主機端使用)

滑鼠

  • 左鍵按一下以行走
  • 按一下UI中的按鈕以互動

玩家

玩家行為由三個不同的元件定義:

  • PlayerObject:保存對與該物件相關聯的PlayerRef的參照,並包含玩家在房間中的索引、暱稱和所選顏色。
  • PlayerMovement:負責玩家的運動和輸入。 它還保存了遊戲遊玩的基本資料和方法,尤其是IsDeadIsSuspectEmergencyMeetingUses屬性。
  • 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附加元件,目的是更乾淨地組織不同的遊戲狀態及其伴隨的指令碼。在主遊戲物件的層次結構中,現在有以下內容:

The GameState hierarchy in the project
專案中的遊戲狀態層次結構

每個狀態都繼承自StateBehaviour(是狀態機系統使用的NetworkBehaviour)。它們具有OnEnterStateOnExitState以及在網路側處理的其他功能;以及其他方法,如OnEnterStateRenderOnExitStateRender,它們更多地用於轉譯遊戲中不影響已連網遊戲遊玩的更改。以下是使用本範例的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不再存在。為了解決這個問題,使用延遲補償將可互動物和其他玩家的碰撞檢查移動到PlayerMovementFixedUpdateNetwork方法。由於玩家在移動時可能會脫離冒名頂替者的殺傷範圍,尤其是在連線不良的遊戲中,因此將延遲補償整合到此範例中,以更準確地檢測碰撞。您可以在此閱讀更多關於延遲補償的資訊。以下程式碼顯示了更新後的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元件被移除。
The Hitbox component on the Player character
玩家預製件的擊殺半徑遊戲物件上的命中框元件。

指令碼與原型

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,並取消生成該玩家。

Back to top