This document is about: FUSION 1
SWITCH TO

Projectiles Essentials

Level 4

概述

Fusion Projectiles展示了多種方式,以執行已連線的拋射物。為了達成一個更簡單的展示及學習體驗,示例執行方式已經被分成兩個單獨的專案——拋射物基礎及拋射物進階。

這個專案——拋射物基礎——作為這個主題的進入點。它的目標在於解釋及提供所有常用方法的簡單示例。每個示例都已加入註解,並且有一個獨立的架構,可以在沒有複雜關係的情況下取得有用的程式碼片段並理解核心概念。

拋射物進階透過解決組建射擊遊戲時的常見使用案例,及展示針對不同的拋射物類型(彈跳、自導引、爆炸性等等)的解決方案,來作為一個深度的遊戲範例。

這份文檔只涵蓋拋射物基礎。請參見一個獨立的拋射物進階專案頁面。

這個範例使用主機端模式拓撲。
projectiles overview

功能

下載

版本 發佈日期 下載
1.1.6 2023年4月14日 Fusion拋射物基礎1.1.6組建183

需求

  • Unity 2021.3
  • Fusion應用程式帳號:為了運行範例,首先在PhotonEngine儀表板中建立一個Fusion應用程式帳號,並且將它貼上到即時設定(可從Fusion選單中到達)中的App Id Fusion欄位之中。繼續遵循開始遊戲章節中的指引。

選擇正確的方法

在進入專案之前,讓我們討論一些針對Fusion中的連線拋射物的可用的變體選項。

A) 附有網路轉換/網路剛體的網路物件

繁衍拋射物為一個NetworkObject,使用NetworkTransformNetworkRigidbody以同步位置。

  • ✅ 當不涉及已預測繁衍時的簡單的解決方案
  • ✅ 開箱即用的興趣管理
  • ✅ 可以使用適當的物理(PhysX)
  • NetworkObject具現化額外負荷
  • NetworkTransform/NetworkRigidbody意味著透過網路來發送每個轉換更新
  • ❌ 按照需要來處理已預測繁衍

重點: 只適用於特定情況下的少量拋射物——舉例而言,當需要複雜物理拋射物的情形(比如,滾動的球)。其他情況下,應避免使用這個方法。

這個方法展示於示例1

B) 附有發射資料的網路物件

繁衍拋射物為一個NetworkObject,使用在已繁衍物件上的一個NetworkBehaviour中的已連線自訂資料(通常是發射刷新、發射位置及發射方向),以在伺服器及客戶端上計算完整的拋射物軌道。

  • ✅ 更新位置而無需花費頻寬
  • NetworkObject具現化額外負荷
  • ❌ 按照需要來處理已預測繁衍
  • ❌ 手動處理興趣管理

重點: 只適用於特定情況下的少量拋射物——舉例而言,針對一個持續時間非常長的拋射物,其可以比射手的存活時間還長(比如,在虛幻錦標賽中的核彈)。雖然對於非物理拋射物,這個方法通常比A更好,它仍然有顯著的缺陷,因此除了特別提及的使用案例之外,應該避免使用,以利於進一步的解決方案。

這個方法展示於示例2,並且拋射物進階針對持續時間非常長的拋射物來使用這個方法(請參見獨立拋射物)。

C) 拋射物計數屬性

只同步一個已發射的拋射物計數,儲存在任何NetworkBehaviour中。拋射物只是一個視覺效果,並且被繁衍為一個標準Unity物件。在輸入授權及狀態授權上,射擊將是精確的,然而對於代理而言,射擊是基於它們的已內插補點的位置及旋轉。當以一個較低的節奏發射拋射物時,可以儲存上一個拋射物的資訊(比如,衝擊位置),從而緩解這個解決方案的精確度問題。

  • ✅ 簡單的解決方案
  • ✅ 最少的透過網路的資料傳輸量
  • ✅ 可以在伺服器上忽略視覺效果的繁衍
  • ✅ 興趣管理作為控制物件的一部分(玩家、武器)
  • ✅ 不需要預測性繁衍
  • ❌ 只適用於即時命中判定拋射物
  • ❌ 在代理上不精確。代理在快照已內插補點位置中,因此它們的發射位置/方向可能非常不同。 (這一點只適用於由於高射擊節奏而無法儲存上一個拋射物的資料的情況)
  • ❌ 沒有關於拋射物目標位置(命中位置)的資料,所以在代理上精確拋射物路徑是未知的。這意味著有些拋射物視覺效果可能移動穿過牆及其他物件,這是因為拋射物路徑末端是未知的。 (這一點只適用於由於高射擊節奏而無法儲存上一個拋射物的資料的情況)

重點。 對於簡單的使用案例而言是一個好的解決方案,在這些使用案例中,我們不是非常關心代理上的精確度及視覺效果——比如具有簡單拋射物及短拋射物視覺效果(軌跡)的從上到下的射擊——或是只有上一個拋射物的額外已連線資訊就足夠的時候 => 最大武器節奏非常低,而且不需要同時發射多個拋射物(散彈槍風格)。通常情況下,使用一個拋射物資料緩衝的解決方案是一個更好的選項,只是會稍微增加複雜性——請參見示例4。

這個方法展示於示例3

D) 拋射物資料緩衝

針對各個拋射物來同步自訂資料——以一個NetworkBehaviour中的環形緩衝的形式來儲存在一個已連線序列。拋射物只是一個視覺效果,其被繁衍為一個標準Unity物件。為了最佳頻寬使用,拋射物資料集應該是疏鬆的——比如,從發射位置、發射方向及其他資料來計算拋射物軌道——但是針對一些特殊使用案例,也可以持續更新位置。

  • ✅ 好的頻寬耗用
  • ✅ 涵蓋大多數拋射物使用案例
  • ✅ 可以在伺服器上忽略視覺效果的繁衍
  • ✅ 興趣管理作為控制物件的一部分(玩家、武器)
  • ✅ 不需要預測性繁衍
  • ❌ 拋射物沒有控制物件就無法存在 = 無法比它存續更久
  • ❌ 只能手動計算拋射物物理,比如彈跳

重點: 除了存續時間非常長或重要的拋射物(比擁有者存續時間更長的拋射物——比如,在 虛幻錦標賽 中的 救世主 拋射物,其甚至在擁有者取消繁衍或中斷連線時仍然在關卡中繼續移動)或移動距離非常長(因為興趣管理)的拋射物之外,是針對大多數情況的非常好的解決方案。

這個方法展示於示例4示例5,並且也是拋射物進階範例的一個核心部分。

專案組織

專案被架構為一個5個小的獨立示例的集。

/01_NetworkObject 示例1——附有網路剛體/網路轉換的網路物件
/02_NetworkObjectFireData 示例2——附有發射資料的網路物件
/03_ProjectileCountProperty 示例3——拋射物計數屬性
/04_ProjectileDataBuffer_Hitscan 示例4——拋射物資料緩衝——即時命中判定
/05_ProjectileDataBuffer_Kinematic 示例5——拋射物資料緩衝——運動學
/Common 在所有示例中常用的預製件、指令碼及材質
/ThirdParty 第三方資產(模型、效果)

開始遊戲

各個示例有其自己的可以開啟及遊玩的場景檔案。另一個方式是所有示例都可以從開始場景來開始(/Common/Start)。

在開始一個示例場景之後,將顯示Fusion標準NetworkDebugStart視窗的一個稍微修改的版本,在其中可以選擇遊戲應該開始的模式。

debug start

多重同儕節點

選擇Start Host + 1 ClientStart Host + 2 Clients,將以多重同儕節點模式來開始遊戲。強烈建議使用此方法來測試拋射物在代理上的行為。

為了在同儕節點之間切換,按下數字鍵盤上的012,及3鍵。

也可以使用Runner Visibility Controls視窗(頂層選單Fusion/Windows/Runner Visibility Controls)來切換同儕節點。

runner visibility controls

為了看見在一個代理上的射擊行為,只設定 客戶端A 為可見,並且只啟用 客戶端B 為一個輸入提供者。現在可以從 客戶端A 的角度來觀察您從 客戶端B 的射擊。

runner visibility controls

控制

使用WSAD來移動,及使用Mouse1來發射。

使用ENTER鍵以鎖住或釋放您的游標。

示例

拋射物基礎提供了在選擇正確的方法章節中介紹的每個方法的示例。所有指令碼都附有註解,所以請自在地深入研究特定程式碼。

所有示例使用相同的物理遊樂場,在其中玩家可以射擊虛擬盒。為了更好地展示在發射拋射物時的預測,開啟了 物理預測網路專案設定資產/伺服器物理模式被設定為客戶端預測)。預測物理仍然是吃重效能的,所以除非您的遊戲是基於物理,否則應該關閉這個設定。

示例1及示例2使用網路物件的繁衍。它代表了對於初學者而言最明顯的方法,然而由於上述的許多原因,並不建議該方法,其中一個原因是複雜性(特別是因為繁衍預測)。為了取得一個簡單的拋射物方法,請前往示例3。

示例1——附有網路剛體/網路轉換的網路物件

示例1展示了一個物理拋射物,其可以在環境中彈跳及滾動。拋射物是一個附有其自己的行為的已繁衍網路物件。拋射物的位置及旋轉(以及其他的物理屬性)透過NetworkRigidbody來同步。

projectiles example 1

可以簡單地以NetworkRunner來繁衍拋射物,並且在自訂拋射物指令碼上調用Fire,來發射該拋射物:

C#

var projectile = Runner.Spawn(projectilePrefab, fireTransform.position, fireTransform.rotation, Object.InputAuthority);
projectile.Fire(fireTransform.forward * 10f);

Fire方法可以新增脈衝到物理拋射物:

C#

public void Fire(Vector3 impulse)
{
    _rigidbody.AddForce(impulse, ForceMode.Impulse);
}

然而這個簡單的方法卻有一個缺點。因為不預測拋射物繁衍,當網路條件不理想時,在實際發射拋射物之前,在發射輸入之後將有顯著的暫停。正常的解決這個問題的方法是使用繁衍預測

在示例2中展示繁衍預測。然而針對物理拋射物,比如在這個示例中的拋射物,不建議繁衍預測,這是因為在場景中在預測性已繁衍物件(比如,拋射物虛擬物)及已經註冊的已連線物件之間的,錯綜複雜的物理互動。最簡單的解決方案是預先繁衍一些拋射物,儲存它到緩衝,並且在發射輸入後,只需使用緩衝內已經完全繁衍的拋射物。在示例1的一部分中展示了該方法,請查看Predicted子資料夾。這個解決方案更為先進,所以在查看其他示例後,請考慮返回這個方案。

示例2——附有發射資料的網路物件

示例2展示了一個運動學的拋射物,其隨著時間移動。拋射物仍然是一個已繁衍網路物件,但是在每個客戶端上基於一些初始發射資料來計算其位置及旋轉,而非在每個刷新透過網路來同步位置/旋轉。

注意事項:即時命中判定與運動學拋射物的不同,將在拋射物進階範例中的拋射物類型章節中說明。

發射是類似於之前的示例中的情形:

C#

var projectile = Runner.Spawn(projectilePrefab, fireTransform.position, fireTransform.rotation, Object.InputAuthority);
projectile.Fire(fireTransform.position, fireTransform.forward * 10f);

public void Fire(Vector3 position, Vector3 velocity)
{
    // Save fire data
    _fireTick = Runner.Tick;
    _firePosition = position;
    _fireVelocity = velocity;
}

然而從初始參數來計算拋射物的移動:

C#

[Networked]
private int _fireTick { get; set; }
[Networked]
private Vector3 _firePosition { get; set; }
[Networked]
private Vector3 _fireVelocity { get; set; }

// Same method can be used both for FUN and Render calls
private Vector3 GetMovePosition(float currentTick)
{
    float time = (currentTick - _fireTick) * Runner.DeltaTime;

    if (time <= 0f)
        return _firePosition;

    return _firePosition + _fireVelocity * time;
}

通常情況下,只在Render調用中移動實際的拋射物轉換,就已經足夠。在FixedUpdateNetwork中,可以計算上一個及下一個位置,以發射射線並且處理潛在的碰撞。

C#

public override void FixedUpdateNetwork()
{
    if (IsProxy == true)
        return;

    // Previous and next position is calculated based on the initial parameters.
    // There is no point in actually moving the object in FUN.
    var previousPosition = GetMovePosition(Runner.Tick - 1);
    var nextPosition = GetMovePosition(Runner.Tick);

    var direction = nextPosition - previousPosition;

    if (Runner.LagCompensation.Raycast(previousPosition, direction, direction.magnitude, Object.InputAuthority,
             out var hit, _hitMask, HitOptions.IncludePhysX | HitOptions.IgnoreInputAuthority))
    {
        // Resolve collision
    }
}

因為涉及網路物件的繁衍。需要使用繁衍預測來在不同的網路狀況下達到最好的射擊體驗。為了啟用繁衍預測,以預測鍵來調用Runner.Spawn方法:

C#

var key = new NetworkObjectPredictionKey()
{
    Byte0 = (byte)Runner.Tick, // Low number part is enough
    Byte1 = (byte)Object.InputAuthority.RawEncoded,
};

Runner.Spawn(projectilePrefab, fireTransform.position, fireTransform.rotation,
        Object.InputAuthority, predictionKey: key);

預測性已繁衍物件需要執行IPredictedSpawnBehaviour介面,其基本上模仿標準NetworkBehaviour調用。這個示例針對已預測繁衍來使用相同的物件,這意味著所有到已連線屬性的存取,需要有本機欄位作為後端,這是因為當物件沒有一個已連線狀態時,無法存取已連線屬性。當網路屬性是一個單一已連線架構的一部分時,這個方法稍微簡單一些:

C#

public struct FireData : INetworkStruct
{
    public int FireTick;
    public Vector3 FirePosition;
    public Vector3 FireVelocity;
}

示例3——拋射物計數屬性

示例3展示一個非常高效的方法,可以直接在武器本身上面同步一個拋射物計數。這個方法代表了即時命中判定拋射物——在發射時立即評估拋射物命中。它意味著立即應用命中效果(比如,損傷、物理衝擊)到目標,但通常在許多情況下,仍然可以繁衍短時間內在空中移動的虛擬視覺效果。在這個示例中使用虛擬拋射物(請參見Common/Scripts/DummyFlyingProjectile.cs)。

C#

public class Weapon : NetworkBehaviour
{
    [SerializeField]
    private LayerMask _hitMask;
    [SerializeField]
    private Transform _fireTransform;

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

    private int _visibleFireCount;

    public void FireProjectile()
    {
        // Whole projectile path and effects are immediately processed (= hitscan projectile)
        if (Runner.LagCompensation.Raycast(_fireTransform.position, _fireTransform.forward,
                100f, Object.InputAuthority, out var hit, _hitMask))
        {
            // Resolve collision
        }

        _fireCount++;
    }

    public override void Spawned()
    {
        _visibleFireCount = _fireCount;
    }

    public override void Render()
    {
        if (_visibleFireCount < _fireCount)
        {
            // Show fire effect
        }

        _visibleFireCount = _fireCount;
    }
}

這個方法非常簡單、高效及開箱即用地使用預測(不需要處理繁衍預測,因為不涉及網路物件繁衍)。如同選擇正確的方法章節中所說明,它有一些缺點,這些缺點可以透過使用拋射物資料緩衝來輕易地解決(示例4)。

示例4——拋射物資料緩衝——即時命中判定

示例4展示了拋射物資料緩衝的使用。緩衝的即時命中判定版本是先前的示例中的 拋射物計數屬性 的一個升級,同時保持了簡單性。

projectiles example 4

拋射物資料緩衝 方法使用一個ProjectileData架構的固定的序列,其作為一個環形緩衝。這意味著當發射時,輸入/狀態授權基於目前的緩衝頭(fireCount屬性)來以資料填入緩衝,並且所有客戶端基於它們的本機頭(visibleFireCount)來在視覺效果上追補轉譯調用。ProjectileData含有所有必要的資料,以在所有客戶端上重建拋射物。

C#

public class Weapon : NetworkBehaviour
{
    [SerializeField]
    private LayerMask _hitMask;
    [SerializeField]
    private Transform _fireTransform;

    [Networked]
    private int _fireCount { get; set; }
    [Networked, Capacity(32)]
    private NetworkArray<ProjectileData> _projectileData { get; }

    private int _visibleFireCount;

    public void FireProjectile()
    {
        var hitPosition = Vector3.zero;

        var hitOptions = HitOptions.IncludePhysX | HitOptions.IgnoreInputAuthority;

        // Whole projectile path and effects are immediately processed (= hitscan projectile)
        if (Runner.LagCompensation.Raycast(_fireTransform.position, _fireTransform.forward,
                100f, Object.InputAuthority, out var hit, _hitMask))
        {
            // Resolve collision

            hitPosition = hit.Point;
        }

        _projectileData.Set(_fireCount % _projectileData.Length, new ProjectileData()
        {
            HitPosition = hitPosition,
        });

        _fireCount++;
    }

    public override void Spawned()
    {
        _visibleFireCount = _fireCount;
    }

    public override void Render()
    {
        if (_visibleFireCount < _fireCount)
        {
            // Play fire effects (e.g. fire sound, muzzle particle)
        }

        for (int i = _visibleFireCount; i < _fireCount; i++)
        {
            var data = _projectileData[i % _projectileData.Length];

            // Show projectile visuals (e.g. spawn dummy flying projectile or trail
            // from fireTransform to data.HitPosition or spawn impact effect on data.HitPosition)
        }

        _visibleFireCount = _fireCount;
    }

    private struct ProjectileData : INetworkStruct
    {
        public Vector3 HitPosition;
    }
}

建議讓ProjectileData架構維持的盡可能小,並且使用不隨著時間改變的資料(如果可以的話),以在發射大量拋射物時仍然達到好的頻寬。

這個方法相對簡單、高效及具有彈性,並且建議用於大多數遊戲。如果在您的遊戲中需要運動學的拋射物,則需要使用一個稍微複雜的緩衝版本——請繼續參見示例5。

示例5——拋射物資料緩衝——運動學

在許多案例中,遊戲中的拋射物在它們命中某個東西或過期之前,在環境中移動一些時間。我們稱呼該拋射物為 運動學的拋射物 或簡單稱為 拋射物。需要調整拋射物資料緩衝解決方案,以根據輸入/狀態授權上的拋射物狀態來更新拋射物資料,並且在轉譯中針對所有客戶端來處理拋射物的視覺效果代表的繁衍及更新。

C#

private struct ProjectileData : INetworkStruct
{
    public int FireTick;
    public int FinishTick;

    public Vector3 FirePosition;
    public Vector3 FireVelocity;
    public Vector3 HitPosition;
}

ProjectileData架構應該盡可能小的要求仍然適用,並且資料最好不要變化太大。在這個示例中,在每個單一拋射物中,設定拋射物資料兩次——一次當發射拋射物時(設定FirePositionFireVelocityFireTick),以及一次當拋射物碰撞環境時(設定HitPositionFinishTick)。在特殊的適當案例中,當然可以以一個更頻繁的頻率來更新資料——請參見拋射物進階中的自導引拋射物

關於拋射物時間

注意事項:請確保先了解在Fusion中的預測的工作方式。

在Fusion中,我們了解到兩個主要的時間範圍。本機 時間範圍是本機玩家及本機物件的時間。遠端 時間範圍是遠端玩家及遠端物件的時間。總是在 本機 時間範圍中模擬及轉譯主機端/伺服器上的所有物件。客戶端在兩個時間範圍中轉譯物件——通常 本機 用於本機物件,而 遠端 用於遠端物件。對於模擬,客戶端通常只模擬它們的本機物件(也就是,針對代理,不執行FixedUpdateNetwork),除非在 本機 時間範圍中轉譯代理物件(其他玩家的拋射物),並且需要關於可能的碰撞及其他互動的資料,以避免發射過頭。

實際上,這意味著當兩個不同的玩家A及B站在一起並且在同一個刷新發射一個拋射物時。從玩家A的角度,拋射物A被立即反射,然而玩家A還沒有任何關於拋射物B的資訊,因為這個資訊仍然在從玩家B移動到伺服器、被處理然後再發送給玩家A的過程中。當這個資訊終於到達,玩家A已經領先好幾個刷新了(也就是,從上一個已知的服務器狀態預測未來),並且拋射物A的轉譯也領先了。假如拋射物A已經距離玩家A有3公尺了。現在我們有兩個選項以轉譯拋射物B。我們可以選擇在 本機 時間範圍中,在它應該在的位置「正確地」轉譯它——距離玩家B有3公尺——但是這會意味著,拋射物視覺效果會在距離玩家B有3公尺的地方突然出現。因為這樣的行為通常讓人無法接受,我們選擇在 遠端 時間範圍中轉譯拋射物B。這意味著直接從玩家B的武器來發射拋射物B,但是即使是在同一刷新來發射拋射物,在玩家A的拋射物之後3公尺處來轉譯拋射物B。這就是為什麼在這個範例的所有相關示例中,拋射物的轉譯時間計算如下:

C#

float renderTime = Object.IsProxy ? Runner.InterpolationRenderTime : Runner.SimulationRenderTime;

進階拋射物時間注意事項

只有對於複雜的時間主體感到興趣的話,才需要閱讀這個注意事項。如果您只是希望發射一些拋射物的話,並不需要如此詳細地了解這個主題。

技術上而言,轉譯(或更確切地說,從Render調用中讀取值)有幾個時間範圍:

  • 本機
    • 也就是來自上一個向前的刷新的值
    • 轉譯中的值:在輸入/狀態授權上讀取網路屬性
    • 轉譯中的時間:Runner.Tick * Runner.DeltaTimeRunner.SimulationTime
  • 本機已內插補點
    • 也就是在前兩個向前的刷新之間內插補點的值
    • 轉譯中的值:在輸入/狀態授權上讀取內插運算
    • 轉譯中的時間:當在伺服器上調用時(狀態授權),(Runner.Tick - 1 + Runner.StateAlpha) * Runner.DeltaTimeRunner.SimulationRenderTimeRunner.InterpolationRenderTime
  • 本機已外插
    • 也就是來自上一個向前的刷新的已外插的值
    • 轉譯中的值:在輸入/狀態授權上讀取網路屬性+在本機上計算值的轉譯部分
    • 轉譯中的時間:(Runner.Tick + Runner.StateAlpha) * Runner.DeltaTimeRunner.SimulationRenderTime + Runner.DeltaTime
  • 遠端
    • 也就是來自上一個伺服器刷新的值(=> 上一個已接收刷新)
    • 轉譯中的值:在代理上讀取網路屬性(如果在重新模擬時,在代理上沒有修改屬性)
    • 轉譯中的時間:Runner.Simulation.LatestServerState.Tick * Runner.DeltaTimeRunner.Simulation.LatestServerState.Time
  • 遠端已內插補點
    • 也就是在模擬的InterpFromInterpTo之間內插補點的值(在使用上一個伺服器刷新進行內插補點之前,需要有一個安全視窗,因此InterpFromInterpTo在過去的伺服器刷新中甚至比在上一個伺服器刷新中更多)
    • 轉譯中的值:在代理上讀取內插運算
    • 轉譯中的時間:Runner.InterpolationRenderTime(Runner.Simulation.InterpFrom.Tick + (Runner.Simulation.InterpTo.Tick - Runner.Simulation.InterpFrom.Tick) * Runner.Simulation.InterpAlpha) * Runner.DeltaTime

根據這樣的時間範圍定義,示例5中的拋射物針對輸入/狀態授權在 本機已內插補點 時間範圍中轉譯,針對代理在 遠端已內插補點 時間範圍中轉譯。然而,為了讓這個做法準確無誤,應該從相應的內插運算來讀取fireCountprojectileData屬性。為了簡單起見,這一點被省略了——我們使用 遠端已內插補點 時間及 遠端 值(以及 本機已內插補點 時間及 本機 值)。因此在代理上的射擊的時間比起它技術上應該射擊的時間來的快一點。這樣的差異比起這個案例中的複雜性来得更加合理。如需取得完整的內插補點示例,請查看拋射物進階

Back to top