Fusion Razor Madness

概述

Fusion Razor Madness範例是一個針對8名以上玩家的刷新制平台競速遊戲。結合平順和精確的玩家動作,並能針對地面進行牆面跳躍,這在您以各種方式跳躍及躲避危險時帶來良好的控制感和滿足感。

Back To Top

下載

版本 發佈日期 下載
1.1.1 Jun 14, 2022 Fusion Razor Madness 1.1.1 Build 8

Back To Top

網路平台遊戲2D控制器

精確的玩家預測

當處理平台遊戲動作的時候,讓玩家看見和感受到他們的決定所帶來的立即性的結果是相當重要的。有了這樣的想法之後,玩家動作使用已預測的且完美地匹配快照動作的客戶端物理。

為了啟用客戶端預測,前往Network Project Config並將Server physics Mode設定為Client Prediction

Setting Client Predicted Physics in the Network Project Config
在網路專案組態中設定客戶端已預測物理。

然後,在PlayerScript上,將NetworkRigidbody2D內插補點資料源設定為Predicted。只有輸入授權被預測為 本地的,而代理仍然透過快照內插補點進行更新。

public override void Spawned(){
    if(Object.HasInputAuthority)
    {
        // Set Interpolation data source to predicted if is input authority
        _rb.InterpolationDataSource = InterpolationDataSources.Predicted;
    }
}

Back To Top

更好的跳躍邏輯

有了輸入和目前的跳躍狀態,玩家可以更好地使用力量來建立一個更強的又可以控制的感受。

FixedUpdateNetwork()中調用這個功能是相當重要的,以允許完成重新模擬。此外必須使用Runner.DetaTime(對Fusion特定),而非使用來自Unity的一般的Time.deltaTime,以在同一給定時間下以相同方式同步所有客戶端。

private void BetterJumpLogic(InputData input)
{
    if (_isGrounded) { return; }
    if (_rb.Rigidbody.velocity.y < 0)
    {
        if (_wallSliding && input.AxisPressed())
        {
            _rb.Rigidbody.velocity += Vector2.up * Physics2D.gravity.y * (wallSlidingMultiplier - 1) * Runner.DeltaTime;
        }
        else
        {
            _rb.Rigidbody.velocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Runner.DeltaTime;
        }
    }
    else if (_rb.Rigidbody.velocity.y > 0 && !input.GetState(InputState.JUMPHOLD))
    {
        _rb.Rigidbody.velocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Runner.DeltaTime;
    }
}

這樣子,玩家如果想要的話可以跳躍的更高,而且在沿著牆壁滑行時緩緩落下。

Back To Top

同步死亡狀態

OnChanged回調代理圖形的使用被一個由伺服器所確認的死亡所停用,而不是由一個客戶端的已預測/已模擬而未由伺服器所確認的死亡所停用;在伺服器權威性判斷上這也允許重新啟用的代理的圖形。

[Networked(OnChanged = nameof(OnSpawningChange))]
private NetworkBool Respawning { get; set; }
public static void OnSpawningChange(Changed<PlayerBehaviour> changed)
{
    if (changed.Behaviour.Respawning)
    {
        changed.Behaviour.SetGFXActive(false);
    }
    else
    {
        changed.Behaviour.SetGFXActive(true);
    }
}

Back To Top

網路物件以維持玩家資料

做一個層級以維持任何與玩家相關的[Networked]資料是可行的,這可以藉由從NetworkBehaviour將其衍生出來並儲存在一個NetworkObject上。

public class PlayerData: NetworkBehaviour
{
    [Networked]
    public string Nick { get; set; }
    [Networked]
    public NetworkObject Instance { get; set; }

    [Rpc(sources: RpcSources.InputAuthority, targets: RpcTargets.StateAuthority)]
    public void RPC_SetNick(string nick)
    {
        Nick = nick;
    }

    public override void Spawned()
    {
        if (Object.HasInputAuthority)
            RPC_SetNick(PlayerPrefs.GetString("Nick"));

        DontDestroyOnLoad(this);
        Runner.SetPlayerObject(Object.InputAuthority, Object);
        OnPlayerDataSpawnedEvent?.Raise(Object.InputAuthority, Runner);
    }
}

在這個情況下,只需要玩家Nick,以及與目前的且玩家有輸入權限的NetworkObject的參照。OnPlayerDataSpawnedEvent是一個自訂事件,以在這個範例中處理大廳同步。 當玩家加入後,Nick可由一個文字輸入欄位或其他來源進行設定,然後生成NetworkObject預製件(它有一個PlayerData指令碼的執行個體)。然後這個NetworkObject將透過Spawned()上的Runner.SetPlayerObject功能,針對這個PlayerRef將設定自己為主要物件。

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
    if (runner.IsServer)
    {
        runner.Spawn(PlayerDataNO, inputAuthority: player);
    }

    if (runner.LocalPlayer == player)
    {
        LocalRunner = runner;
    }

    OnPlayerJoinedEvent?.Raise(player, runner);
}

當需要來自特定玩家的資料時,可藉由調用NetworkRunner.TryGetPlayerObject()方法,並且尋找在上述的NetworkObject上的PlayerData元件來擷取。

public PlayerData GetPlayerData(PlayerRef player, NetworkRunner runner)
{
    NetworkObject NO;
    if (runner.TryGetPlayerObject(player, out NO))
    {
        PlayerData data = NO.GetComponent<PlayerData>();
        return data;
    }
    else
    {
        Debug.LogError("Player not found");
        return null;
    }
}

這個資料可以根據需要而被使用和/或操作。

   //e.g
    PlayerData data = GetPlayerData(player, Runner);
    Runner.despawn(data.Instance);
    string playerNick = data.Nick;

Back To Top

觀眾模式

當一個玩家在達到所需贏家數量之前完成賽車競賽,他們將進入觀眾模式。在觀眾模式時他們無法控制他們的角色,而且他們的相機允許跟隨他們所選擇的玩家。觀眾模式的玩家可以使用箭頭鍵在剩餘玩家的視角中進行切換。

/// <summary>
/// Set player state as spectator.
/// </summary>
public void SetSpectating()
{
    _spectatingList = new List<PlayerBehaviour>(FindObjectsOfType<PlayerBehaviour>());
    _spectating = true;
    CameraTarget = GetRandomSpectatingTarget();
}

private void Update()
{
    if (_spectating)
    {
        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            CameraTarget = GetNextOrPrevSpectatingTarget(1);
        }
        else if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            CameraTarget = GetNextOrPrevSpectatingTarget(-1);
        }
    }
}

private void LateUpdate()
{
    if (CameraTarget == null)
    {
        return;
    }

    _step = Speed * Vector2.Distance(CameraTarget.position, transform.position) * Time.deltaTime;

    Vector2 pos = Vector2.MoveTowards(transform.position, CameraTarget.position + _offset, _step);
    transform.position = pos;
}

Back To Top

障礙物

固定鋸子

這個最簡單的鋸子是唯一一個Unity遊戲物件,將在安全刷新的方式下偵測碰撞情形,如同FixedNetworkUpdate()

請記住OnCollisionEnterOnCollisionExit在重新模擬時並不是穩定的。

Back To Top

旋轉鋸子

旋轉鋸子使用NetworkTransform元件,以在所有客戶端中保持同步。它以一個[Networked]屬性來計算FixedUpdateNetwork上的一個圓形上的一個位置,以確保其在重新模擬時安全並且可以應用。

[Networked] private int Index { get; set; }

public override void FixedUpdateNetwork()
{
    transform.position = PointOnCircle(_radius, Index, _origin);
    _line.SetPosition(1, transform.position);
    Index = Index >= 360 ? 0 : Index + (1 * _speed);
}

public static Vector2 PointOnCircle(float radius, float angleInDegrees, Vector2 origin)
{
    // Convert from degrees to radians via multiplication by PI/180        
    float x = (float)(radius * Mathf.Cos(angleInDegrees * Mathf.PI / 180f)) + origin.x;
    float y = (float)(radius * Mathf.Sin(angleInDegrees * Mathf.PI / 180f)) + origin.y;

    return new Vector2(x, y);
}

請確保每個可以更改並且用於計算位置的屬性都是[Networked]的狀態。因為針對各個RotatingSaw指令碼,_speed只在編輯器中被定義一次且不可改變,它可以是一個普通的 Unity屬性。

Back To Top

移動鋸子

移動鋸子使用與旋轉鋸子同樣的原理;然而,它並不是使用在一個圓形上的一個位置,它使用在編輯器中被定義的一個位置列表,並且在其中置入它的位置。

[Networked] private float _delta { get; set; }
[Networked] private int _posIndex { get; set; }
[Networked] private Vector2 _currentPos { get; set; }
[Networked] private Vector2 _desiredPos { get; set; }

public override void FixedUpdateNetwork()
{
    transform.position = Vector2.Lerp(_currentPos, _desiredPos, _delta);
    _delta += Runner.DeltaTime * _speed;

    if (_delta >= 1)
    {
        _delta = 0;
        _currentPos = _positions[_posIndex];
        _posIndex = _posIndex < _positions.Count - 1 ? _posIndex + 1 : 0;
        _desiredPos = _positions[_posIndex];
    }
}

如同前例,請記得將所有可在運行時被改變的,且影響位置計算的屬性標記成[Networked]

Back To Top

專案

資料架結構

本專案被細分在類別資料夾內。

  • 美術:包含所有在專案中使用的美術資產,以及圖塊地圖資產及動畫檔案。
  • 聲音:包含音效及音樂檔案。
  • Photon:Fusion套件。
  • 物理材料:玩家物理材料。
  • 預製件:所有在專案中使用的預製件,最重要的是玩家預製件。
  • 場景:大廳和關卡場景。
  • 可用指令碼的物件:包含可用指令碼的物件,用於聲音通道和聲音資產等等。
  • 指令碼:示範版本的核心,指令碼資料夾也被細分在邏輯類別內。
  • URP:在專案上使用的通用轉譯管線資產。

Back To Top

大廳

大廳使用一個Network Debug Start GUI的經修改的版本。在玩家輸入他們希望的暱稱之後,玩家可以選擇進行單人遊戲、主辦一個遊戲或以客戶端的身份加入一個已存在的房間。

Lobby Menu
大廳選單。

Inside the Room View
房間內的視角。

此時,主機端將以玩家的資料為房間內的各個玩家建立一個NetworkObject。如同在網路物件以維持玩家資料中所顯示的。 在加入房間之後,將會顯示玩家清單。只有主機端可以透過按下開始遊戲按鈕以開始遊戲。

Back To Top

遊戲開始

當主機端開始遊戲,LoadingManager指令碼會選擇下一個關卡。它使用runner.SetActiveScene(scenePath)以載入所需的關卡。

請注意: 只有主機端可以在NetworkRunner上設定一個啟用中的場景。

使用LevelBehaviour.Spawned()方法,則需要PlayerSpawner以生成所有在大廳註冊的玩家,並且將他們目前的Input Authority給他們。

為了公平起見,無論玩家們是否完成關卡的載入,五秒之後玩家都將被允許開始賽車競賽。這是為了避免個別的客戶端在載入過程中因為發生錯誤而導致無限的載入時間。

這些秒數由TickTimer進行倒數。

[Networked]
private TickTimer StartTimer { get; set; }

private void SetLevelStartValues()
{
    //...
    StartTimer = TickTimer.CreateFromSeconds(Runner, 5);
}

Back To Top

處理輸入

Fusion使用Unity的標準輸入處理機制來擷取玩家輸入,並將其儲存在一個可以在網路間傳輸的資料結構中,然後在FixedUpdateNetwork()方法中處理這個資料結構。在這個實例中,所有這些程序是由InputController層級使用InputData結構來執行,雖然其將實際狀態更改轉交到PlayerMovementPlayerBehaviour層級。

Back To Top

完成賽車競賽

LevelBehaviour維持一個贏家的序列,以獲得前三名的玩家的帳號。

[Networked, Capacity(3)] private NetworkArray<int> _winners => default;
public NetworkArray<int> Winners { get => _winners; }

當玩家跨過終點線,它將通知LevelBehaviour。之後LevelBehaviour將檢查是否已經達到正確的贏家數量;如果如此,該關卡就結束,並顯示結果。


To Document Top