Razor Madness
概述
Fusion Razor Madness範例是一個針對8名以上玩家的刷新制平台競速遊戲。結合平順和精確的玩家動作,並能針對地面進行牆面跳躍,這在您以各種方式跳躍及躲避危險時帶來良好的控制感和滿足感。
下載
版本 | 發佈日期 | 下載 | ||
---|---|---|---|---|
1.1.6 | 2023年4月12日 | Fusion Razor Madness 1.1.6組建162 |
網路平台遊戲2D控制器
精確的玩家預測
當處理平台遊戲動作的時候,讓玩家看見和感受到他們的決定所帶來的立即性的結果是相當重要的。有了這樣的想法之後,玩家動作使用已預測的且完美地匹配快照動作的客戶端物理。
為了啟用客戶端預測,前往Network Project Config
並將Server physics Mode
設定為Client Prediction
。
然後,在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;
}
}
這樣子,玩家如果想要的話可以跳躍的更高,而且在沿著牆壁滑行時緩緩落下。
同步死亡狀態
OnChanged
回調代理圖形的使用被一個由伺服器所確認的死亡所停用,而不是由一個客戶端的已預測/已模擬而未由伺服器所確認的死亡所停用;在伺服器權威性判斷上這也允許重新啟用的代理的圖形。
C#
[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);
}
}
網路物件以維持玩家資料
做一個層級以維持任何與玩家相關的[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
,以及與目前的且玩家有輸入權限的NetworkObject
的參照。OnPlayerDataSpawnedEvent
是一個自訂事件,以在這個範例中處理大廳同步。
當玩家加入後,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()
。
請記住OnCollisionEnter
及OnCollisionExit
在重新模擬時並不是穩定的。
旋轉鋸子
旋轉鋸子使用NetworkTransform
元件,以在所有客戶端中保持同步。它以一個[Networked]
屬性來計算FixedUpdateNetwork
上的一個圓形上的一個位置,以確保其在重新模擬時安全並且可以應用。
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]
的狀態。因為針對各個RotatingSaw
指令碼,_speed
只在編輯器中被定義一次且不可改變,它可以是一個普通的 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]
。
專案
資料架結構
本專案被細分在類別資料夾內。
- 美術:包含所有在專案中使用的美術資產,以及圖塊地圖資產及動畫檔案。
- 聲音:包含音效及音樂檔案。
- Photon:Fusion套件。
- 物理材料:玩家物理材料。
- 預製件:所有在專案中使用的預製件,最重要的是玩家預製件。
- 場景:大廳和關卡場景。
- 可用指令碼的物件:包含可用指令碼的物件,用於聲音通道和聲音資產等等。
- 指令碼:示範版本的核心,指令碼資料夾也被細分在邏輯類別內。
- URP:在專案上使用的通用轉譯管線資產。
大廳
大廳使用一個Network Debug Start GUI
的經修改的版本。在玩家輸入他們希望的暱稱之後,玩家可以選擇進行單人遊戲、主辦一個遊戲或以客戶端的身份加入一個已存在的房間。
此時,主機端將以玩家的資料為房間內的各個玩家建立一個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()
方法中處理這個資料結構。在這個實例中,所有這些程序是由InputController
層級使用InputData
結構來執行,雖然其將實際狀態更改轉交到PlayerMovement
及PlayerBehaviour
層級。
完成賽車競賽
LevelBehaviour
維持一個贏家的序列,以獲得前三名的玩家的帳號。
C#
[Networked, Capacity(3)] private NetworkArray<int> _winners => default;
public NetworkArray<int> Winners { get => _winners; }
當玩家跨過終點線,它將通知LevelBehaviour
。之後LevelBehaviour
將檢查是否已經達到正確的贏家數量;如果如此,該關卡就結束,並顯示結果。