This document is about: FUSION 2
SWITCH TO

Razor Madness

Level 4

概述

Fusion Razor Madness 範例是一款基於刷新的平台賽車遊戲,適用於8名以上玩家。流暢而精確的玩家移動加上向層級跳牆的能力,在跳躍和避免各種危險時帶來了良好的控制感和滿足感。

下載

版本 發佈日期 下載
2.0.1 Jun 17, 2024 Fusion Razor Madness 2.0.1 Build 575

已連網平台2D控制器

精確玩家預測

在處理平台遊戲時,讓玩家看到並感受到他們的決定的直接結果是很重要的。考慮到這一點,玩家的移動使用了與快照位置完全匹配的已預測客戶端物理。

為了啟用客戶端側的預測,請前往Network Project Config並且設定Server physics ModeClient Prediction

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

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

C#

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

更好的跳躍邏輯

在輸入和目前跳躍狀態下,可以更好地使用力量為玩家創造一種沉重但可控的感覺。

FixedUpdateNetwork()中調用此函數以允許重新模擬非常重要。此外,使用Runner.DetaTime(特定於Fusion),而不是Unity的常規Time.deltaTime,以相同的方式同步給定刷新的所有客戶端。

C#

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;
    }
}

這樣,如果玩家願意,他們可以跳得更高,在牆上滑動時可以慢慢落下。

同步死亡狀態

透過ChangeDetector代理,圖形被伺服器確認的死亡停用,而不是伺服器未確認的客戶端已預測/已模擬死亡停用;這也允許在伺服器上重新啟用代理的圖形。

C#

[Networked]
private NetworkBool Respawning { get; set; }

public override void Render()
{
    foreach (var change in _changeDetector.DetectChanges(this))
    {
        switch (change)
        {
            case nameof(Respawning):
                SetGFXActive(!Respawning);
                break;
        }
    }
}

網路物件以持有玩家資料

可以做一個類別,來持有任何與玩家相關的[Networked]資料,方法是從NetworkBehaviour衍生它,並將其儲存在NetworkObject上。

C#

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和一個參照到玩家具有輸入授權的目前NetworkObjectOnPlayerDataSpawnedEvent是用於處理此範例中大廳同步的自訂事件。
當玩家加入時,可以從文字輸入欄位或其他源設定Nick,然後生成NetworkObject預製件(它有一個PlayerData指令碼的執行個體與)。然後,此NetworkObject透過Spawned()上的Runner.SetPlayerObject函數,將自己設定為此PlayerRef的主物件。

C#

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元件。

C#

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;
    }
}

這個資料可以按照需要來使用和/或操控。

C#

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

觀眾模式

當玩家在達到所需的獲勝人數之前完成比賽時,他們將進入觀眾模式。在觀看時,他們無法控制自己的角色,他們的相機可以跟隨自己選擇的玩家。觀眾玩家可以使用箭頭鍵在其餘玩家的檢視之間導航。

C#

/// <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;
}

障礙物

固定鋸

最簡單的鋸子,僅為一個Unity遊戲物件,以刷新安全的方式來偵測碰撞,像是FixedNetworkUpdate()

請記得,OnCollisionEnterOnCollisionExit在重新模擬時並不可靠。

旋轉鋸

旋轉鋸使用NetworkTransform元件以在所有客戶端之間同步。它計算FixedUpdateNetwork上具有[Networked]屬性的圓上的一個位置,以確保重新模擬的安全性並應用它。

C#

[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]。由於_speed只在編輯器中為每個RotatingSaw指令碼定義一次,並且從不更改,因此它可以是一個正常的Unity屬性。

移動鋸

移動鋸使用與旋轉鋸相同的原理;但是,它不是使用圓上的位置,而是使用編輯器中定義的位置清單,並在這些位置之間內插補點其位置。

C#

[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]

專案

資料夾架構

專案依照類別資料夾進行細分。

  • 美術:包含專案中使用的所有美術資產,以及瓷磚貼圖資產和動畫檔案。
  • 音訊:包含sfx和音樂檔。
  • Photon:Fusion套件。
  • 物理材質:玩家物理材質。
  • 預製件:專案中使用的所有預製件,最重要的是玩家預製件。
  • 場景:大廳和關卡場景。
  • 可指令碼物件:包含像音訊頻道和音訊資產的已使用的可指令碼物件。
  • 指令碼:Demo的核心,指令碼資料夾也依照邏輯類別被細分。
  • URP:專案中使用的通用轉譯管道資產。

大廳

大廳使用Network Debug Start GUI的修改版本。輸入想要的暱稱後,玩家可以在玩單人玩家遊戲、成為遊戲主機端,或作為客戶端加入現有房間之間進行選擇。

Lobby Menu
大廳選單。
Inside the Room View
在房間檢視中。

此時,主機端將為房間中的每個玩家及其資料建立一個NetworkObject。如網路物件以持有玩家資料中所示。
加入房間後,將顯示玩家清單。只有主機端才能透過按下 開始遊戲 按鈕來開始遊戲。

遊戲開始

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

注意事項: 只有主機端才能在NetworkRunner上設定活躍中場景。

使用LevelBehaviour.Spawned()方法,PlayerSpawner被要求生成所有在大廳中登錄的玩家,並賦予他們當前的Input Authority

公平地說,五秒鐘後,無論玩家是否完成了關卡載入,他們都可以開始比賽。這是為了避免在載入過程中出現問題時,由於單個客戶端的不一致而導致無限載入時間。

這些秒由TickTimer計數。

C#

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

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

處理輸入

Fusion使用Unity的標準輸入處理機制來捕捉玩家輸入,將其儲存在可以透過網路發送的資料架構中,然後在FixedUpdateNetwork()方法中處理此資料架構。在本例中,所有這些都是由使用InputData架構的InputController類別執行的,儘管它將實際狀態更改交給PlayerMovementPlayerBehaviour類別。

完成競賽

LevelBehaviour維持獲勝者的序列,以獲得前3名玩家的ID。

C#

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

當玩家越過終點線時,它會通知LevelBehaviour。然後,LevelBehaviour檢查是否達到了正確的獲勝者人數;如果是,則關卡結束,並顯示結果。

第三方資產

Razor Madness範例包括由其各自創作者提供的若干資產。您可以在他們各自的網站上為自己的專案獲取完整的套裝軟體:

重要事項:為了在商業專案中使用它們,需要從相應的創作者那裡購買許可證。

Back to top