Razor Madness
概述
Fusion Razor Madness 範例是一款基於刷新的平台賽車遊戲,適用於8名以上玩家。流暢而精確的玩家移動加上向層級跳牆的能力,在跳躍和避免各種危險時帶來了良好的控制感和滿足感。
下載
版本 | 發佈日期 | 下載 | |
---|---|---|---|
2.0.1 | Jun 17, 2024 | Fusion Razor Madness 2.0.1 Build 575 |
已連網平台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;
}
}
這樣,如果玩家願意,他們可以跳得更高,在牆上滑動時可以慢慢落下。
同步死亡狀態
透過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
和一個參照到玩家具有輸入授權的目前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
元件以在所有客戶端之間同步。它計算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
的修改版本。輸入想要的暱稱後,玩家可以在玩單人玩家遊戲、成為遊戲主機端,或作為客戶端加入現有房間之間進行選擇。
此時,主機端將為房間中的每個玩家及其資料建立一個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
類別執行的,儘管它將實際狀態更改交給PlayerMovement
與PlayerBehaviour
類別。
完成競賽
LevelBehaviour
維持獲勝者的序列,以獲得前3名玩家的ID。
C#
[Networked, Capacity(3)] private NetworkArray<int> _winners => default;
public NetworkArray<int> Winners { get => _winners; }
當玩家越過終點線時,它會通知LevelBehaviour
。然後,LevelBehaviour
檢查是否達到了正確的獲勝者人數;如果是,則關卡結束,並顯示結果。
第三方資產
Razor Madness範例包括由其各自創作者提供的若干資產。您可以在他們各自的網站上為自己的專案獲取完整的套裝軟體:
- Bakudas的通用地牢套件
- Essssam的岩石路
- o_lobster的平台/銀河惡魔城像素美術資產套件
重要事項:為了在商業專案中使用它們,需要從相應的創作者那裡購買許可證。
Back to top