This document is about: FUSION 2
SWITCH TO

Animation

概述

動畫為玩家提供了至關重要的遊戲遊玩回饋,例如玩家輸入引起的移動和動作。

本文檔介紹了在使用Fusion組建多人遊戲時選擇正確的動畫方法。這些示例將側重於角色動畫;然而,本文中提出的透過網路同步動畫的概念,也適用於任何其他動畫物件。

關於實際的動畫範例,請參考Fusion動畫技術範例

動畫準確度

在開始Fusion中的動畫之前,重要的是要建立動畫系統所需和目標的基線準確性。

一般來說,基於準確性,有兩種類型的動畫方法:

  • 渲染準確的動畫
  • 刷新準確的動畫
大多數遊戲使用渲染準確的動畫。只有在極少數情況下(如下所述),當動畫影響遊戲遊玩時,才需要刷新準確的動畫。

渲染準確的動畫

渲染準確的動畫在模擬之外執行(從RenderMonoBehaviour.Update調用),或者僅在FUN Forward* 階段執行。動畫的開始通常只是鬆散同步的,在重新模擬過程中,角色姿勢不會倒退到以前的狀態。由於完整的動畫狀態(當前播放的動畫、動畫時間、動畫權重等)不是已連網資料的一部分,因此無法保證角色在指定時間在所有客戶端上都處於 完全 相同的姿勢。

使用渲染準確的方法意味著根據肢體位置的計算可能會非常不準確。不準確的程度在很大程度上取決於動畫的類型。靜態動畫(如IdleAim)可能足夠準確,而跑步或快速動作可能會明顯更差。建議不要在動畫層次中使用變換進行模擬計算(例如從手掌進行雷射射線計算)。然而,這種用法很少是必要的。有關更多資訊,請查看我需要刷新準確的動畫嗎?部分。

✅ 優點:

  • 能夠使用常見的動畫解決方案(Animator、Animancer)
  • 無需更改工作室動畫管路
  • 開箱即用的第三方動畫附加元件

❌ 缺點:

  • 動畫層次中的變換不能用於準確的模擬計算
  • 依賴於動畫的命中框位置不準確-儘管可以通過一些解決方案來緩解(檢查動畫和延遲補償
注意:當在重新模擬過程中發生不同的事情時,預計動畫會隨著時間的推移自動糾正。想像一下,一個跳躍動畫是基於跳躍輸入命令啟動的,但在幾個刷新後,伺服器狀態會在沒有跳躍動作的情況下協調(例如,玩家在跳躍前實際上被對手凍結了)。動畫應立即取消,並應該應用與正確當前狀態的混合。

*FUN Forward = FixedUpdateNetwork方法調用附有Forward 模擬狀態(Runner.IsForward == true)

刷新準確的動畫

刷新準確的動畫意味著 動畫在給定刷新時在所有客戶端和伺服器上處於完全相同的狀態。換句話說,完整的動畫狀態是已連網資料的一部分,在重新模擬過程中,動畫可以重新重播並再次播放(或者更確切地說是逐步播放)。讓所有客戶端的角色都保持相同的姿勢,對於角色四肢上的延遲補償碰撞器尤為重要。例如,在客戶端上的跑步動畫中間擊打另一名玩家的腿,將在伺服器上產生完全相同的擊打。

✅ 優點:

  • 所有客戶端和伺服器上的角色,在任何給定時間都處於相同的姿勢,從而使所有命中計算精確
  • 能夠基於精確的四肢定位進行其他計算,例如手掌發出的雷射射線,或近戰攻擊的傷害框計算

❌ 缺點:

  • 很難實現,因為它需要自訂動畫解決方案(Unity動畫器不支持刷新準確的動畫)
  • 第三方動畫附加元件可能無法工作(由於它們依賴於動畫器、無法處理重新模擬等)
  • 與動畫器控制器等其他解決方案相比,動畫解決方案可能更以程式碼為中心,導致設定動畫時的開發人員體驗更差
進階說明:刷新準確的動畫是指一種提供足夠資料的方法,可以為任何給定的刷新重建角色姿勢,並應用這個資料,使角色在所有客戶端上的給定刷新以完全相同的姿勢出現。
當最新的刷新資料從伺服器到達,並且重新模擬在客戶端開始時,程式碼會根據新資料重新模擬本機玩家(=輸入授權)的動畫,直到它再次達到本機預測的刷新。對於代理,在重新模擬期間不需要進行特殊處理,因為它們是在遠端時間進行動畫製作的(=基於來自伺服器的已驗證資料);因此,代理的動畫在重新模擬時將是相同的。
為了獲得正確的延遲補償,在命中框管理器保存命中框的位置之前,只需在FUN Forward* 調用中評估一次代理動畫就足夠了。在重新模擬過程中,代理的角色姿勢實際上並沒有改變,已經保存的命中框位置用於正確的延遲補償投射。

*FUN Forward = FixedUpdateNetwork方法調用附有Forward 模擬階段(Runner.IsForward == true)

我需要刷新準確的動畫嗎?

組建一個刷新準確的動畫解決方案既複雜又耗時;考慮這對專案來說是否絕對必要,以及額外的工作量是否合理。在許多情況下,刷新準確的動畫是不必要的,專案可以通過渲染準確的動畫成功執行,而玩家不會注意到其中的差異。決定選擇哪種方法非常取決於遊戲。

一般來說,如果專案屬於以下兩種情況之一,則需要採用刷新準確的方法:

  • 需要對放置在角色動畫部分的命中框進行100%準確的命中
  • 有一些使用變換位置的計算受到動畫的嚴重影響,不能輕易被手動計算所取代,例如在近戰攻擊中驅動一個與拳頭相連結的傷害框

基於角色位置或方向的精確計算也可以透過渲染準確的方法來實現。這只是意味著計算不能基於受動畫變換層次(=骨骼)影響的變換。遊戲狀態中的計算位置及其視覺效果表示可能不同,如下例所示。

示例1:在FPS遊戲中,從相機進行射擊計算是一種常見的方法,而不是從受動畫影響的實際槍管進行射擊計算。 在Fusion拋射物中查看如何將拋射物的視覺效果內插補點到其實際路徑。然而,如果計算必須從槍管開始,並且槍的位置和旋轉受到玩家動畫的影響,則需要刷新準確的方法。

示例2:在執行近戰攻擊時,傷害框可以在攻擊過程中遵循預先定義的指令碼路徑。然後,可以在角色動畫的重新模擬過程中獨立執行此路徑。另一方面,如果傷害框必須遵循動畫中設計的角色拳頭的複雜路徑,那麼刷新準確的動畫是必要的。

渲染準確的動畫解決方案

下方列出了一些推薦的渲染準確的動畫解決方案。

注意: 在以下段落中, Unity Mecanim Animator 將簡稱為 Animator

Animator/Animancer + 現有網路狀態

渲染準確的方法

通常,可以使用現有網路狀態來控制動畫。動畫是基於Render方法中的已連網資料設定的。這種解決方案可以很容易地推薦給大多數遊戲,因為它相當容易實作,不會浪費網路資源,並且可以根據遊戲需求進行更精確的調整。

C#

public override void Render()
{
    _animator.SetFloat("Speed", _kcc.Data.RealSpeed);
}

需要時,可以透過標準已連網屬性輕鬆同步動畫的其他資料。

C#

[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;

public override void Spawned()
{
    _lastVisibleJump = _jumpCount;
}

public override void FixedUpdateNetwork()
{
    var input = GetInput<PlayerInput>();
    if (input.HasValue == false)
        return;

    if (input.Value.Buttons.WasPressed(_lastButtonsInput, EInputButtons.Jump) == true)
    {
        DoJump();
        _jumpCount++;
    }

    _lastButtonsInput = input.Value.Buttons;
}

public override void Render()
{
    if (_jumpCount > _lastVisibleJump)
    {
        _animator.SetTrigger("Jump");
        // Play jump sound/particle effect
    }

    _lastVisibleJump = _jumpCount;
}

使用ChangeDetector的類似功能:

C#

[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked]
private int _jumpCount { get; set; }

private ChangeDetector _changes;

public override void Spawned()
{
    _changes = GetChangeDetector(ChangeDetector.Source.SnapshotFrom);
}

public override void FixedUpdateNetwork()
{
    var input = GetInput<PlayerInput>();
    if (input.HasValue == false)
        return;

    if (input.Value.Buttons.WasPressed(_lastButtonsInput, EInputButtons.Jump) == true)
    {
        //DoJump();
        _jumpCount++;
    }

    _lastButtonsInput = input.Value.Buttons;
}

public override void Render()
{
    foreach (string propertyName in _changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
    {
        switch (propertyName)
        {
            case nameof(_jumpCount):
                var reader = GetPropertyReader<int>(nameof(_jumpCount));
                var values = reader.Read(previousBuffer, currentBuffer);

                if (values.Item2 > values.Item1)
                {
                    _animator.SetTrigger("Jump");
                    // Play jump sound/particle effect
                }

                break;
        }
    }
}

可以使用更簡單版本的DetectChanges區塊(見下文)。然而,這種用法有點容易出錯,因為伺服器上可能不會發生跳躍(例如,玩家在跳躍前實際上被對手凍結了),這將導致本機玩家兩次觸發 跳躍(一次是在本機預測期間值增加時,一次是從伺服器接收資料後值返回到之前的值時)。

C#

foreach (string propertyName in _changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
{
    switch (propertyName)
    {
        case nameof(_jumpCount):
            _animator.SetTrigger("Jump");
    }
}
注意: 在需要時使用內插補點及同步動畫狀態(目前遊玩動畫與動畫時間),可以實現更高動畫精確度。更多資訊在動畫與延遲補償渲染準確的狀態同步的訣竅部分。

對於實際使用示例,請參照Fusion動畫技術範例(示例1-3)。

Animator + NetworkMecanimAnimator

渲染準確的方法

使用Fusion附帶的內建NetworkMecanimAnimator元件同步Animator屬性。將Animator分配給NetworkMecanimAnimator元件後,只需設定Animator屬性,NetworkMecanimAnimator就會自動將它們同步到所有客戶端。例外情況是觸發器,也需要透過NetworkMecanimAnimator元件進行設定(有關更多資訊,請查看操作手冊中的Network Mecanim Animator頁面)。

NetworkMecanimAnimator
NetworkMecanimAnimator

為了在代理物件突然出現時(例如在延遲加入後或代理物件進入興趣區域時)獲得更好的動畫行為,請啟用StateRoot(動畫器控制器中的第一層)和StateLayers(所有其他層)的同步,這將確保在正確的時間播放正確的狀態。

注意: 啟用此選項將顯著增加資料流量。

NetworkMecanimAnimator
StateRoot及StateLayers同步選項
注意:不建議在複雜的動畫設定中使用觸發器(儘管支援),因為在某些情況下(特別是在使用子狀態機時)觸發器的消耗行為是不可預測的,在透過網路傳輸觸發器動作時,由於時間稍有不同,可能會變得更糟。例如,使用一個將在一段時間內處於活動狀態的HasJumped布林值,而不是一個Jump觸發器。

有關實際示例,請參閱Fusion動畫技術範例(示例4)。

Animator/Animancer + FSM同步

渲染準確的方法

播放FSM(有限狀態機)狀態的動畫,例如跳躍狀態、攻擊狀態、運動狀態。當前狀態透過網路同步,動畫所需的其他資料可以存儲在標準已連網屬性中。

C#

public class JumpState : PlayerState
{
    protected override void OnEnterState()
    {
        DoJump();
    }

    protected override void OnEnterStateRender()
    {
        Animator.SetTrigger("Jump");
    }
}

將動畫控制分解為狀態有助於管理複雜的動畫設定,並允許輕鬆控制其他視覺效果和動畫(例如,在進入跳躍狀態時播放跳躍聲音和跳躍視覺特效)。當FSM與對重新模擬的支援正確同步時,它也可以用於其他模擬邏輯——狀態實際上可以是AI狀態或玩家行為狀態。

注意:這種方法需要一個自訂的已連網FSM實作。Photon的NetworkFSM實作目前是預覽,作為Fusion Animations示例的一部分。

有關實際示例,請參閱Fusion動畫技術範例(示例5)。

刷新準確的動畫解決方案

請注意,標準Unity Animator在如何控制動畫狀態方面的選項有限,這使得它無法用於刷新準確的動畫。然而,Unity的較低級別可玩API確實允許這樣的功能。因此,如果專案的目標是刷新準確的動畫,那麼實作很可能需要基於Playables。

Animancer + Animancer狀態周圍的刷新準確的包裝器

刷新準確的方法

由於Animancer基於Playables,因此AnimancerState屬性可以同步,Animancer可以從FixedUpdateNetwork手動步進,以實現刷新準確的動畫。

只有當您的項目可以接受「足夠刷新準確」時,才建議使用這種方法。因為Animancer在後台做了一些魔法(比如在特定淡出期間建立失重狀態),使用所有Animancer功能實現100%的刷新準確可能很耗時,立即使用基於Playables的自訂解決方案可能是一個更可取的長期選項。

注意:有兩種方法可以解決這個問題:
1. 分別同步每個AnimancerState的屬性
2. 按原樣同步整個狀態數組(AnimancerLayer.States)
根據專案所需的動畫解決方案的整體複雜性,使用解決方案2可能會更容易、更防禦。

可玩API之上的自訂解決方案

刷新準確的方法

可玩API不在本手冊的範圍內。總體思路是使用表示動畫狀態的自訂資料結構,透過網路同步必要的資料,並構建PlayableGraph。然後根據專案的需求,使用標準Fusion方法評估PlayableGraph

Fusion動畫技術範例(示例6)中查看Fusion BR範例,以獲取經過戰鬥測試的程式碼驅動解決方案或改進和簡化版本。

經典Unity Animation + AnimationStates周圍的刷新準確的包裝器

刷新準確的方法

使用自訂資料結構同步有關遊玩AnimationStates的資料。使用Animation.Sample以實現刷新的正確角色姿勢。

除非專案的動畫管道已經使用經典動畫系統,否則這不是一個推薦的解決方案。

動畫與延遲補償

延遲補償確保在一個客戶端上登錄的命中也能在伺服器上正確識別。

如果命中框受到動畫的影響或驅動,在捕獲命中框的狀態之前,確保角色處於正確的姿勢至關重要。請注意,在FUN Forward調用中,HitboxManager會自動捕獲命中框資料。對於重新模擬,已保存的命中框位置用於正確的光線投射計算,因此代理角色上的動畫不需要回到過去。這正是為什麼即使 渲染準確的方法,如果執行正確,也能產生足夠精確的延遲補償命中

注意: HitboxManager的指令碼執行順序很高(2000),因此預設下,所有NetworkBehaviour都將在HitboxManager之前執行,不需要任何特殊操作。

伺服器假設客戶端在遠端時間框架內渲染代理角色,因此伺服器上使用此時間進行延遲補償的投射。因此,客戶端上的動畫也需要尊重代理的遠端時間框架。換句話說,客戶端上的代理需要基於內插補點資料進行動畫處理,以獲得準確的延遲補償。

使用刷新準確的動畫可以更好、更精確地實現正確的動畫時間,因為內插補點的動畫狀態資料可以用於每個FUN ForwardRender中的代理,以在給定的刷新或渲染時間重建他們的角色姿勢。在這種情況下,代理角色只是顯示來自伺服器的內插補點資料,它們本身並沒有真正「播放」動畫。

另一方面,渲染準確的動畫完全在代理角色上運行。重要的是參數設置為Animator或動畫片段開始的時間。簡單的方法是忽略總體準確性,並根據最新的網路資料採取行動(見下方的示例)。然而,對於更精確的動畫,需要設定動畫參數或根據內插補點資料開始播放動畫片段。

通常還需要定期或在特殊情況下(如角色進入客戶端感興趣的區域)同步播放動畫(動畫狀態和狀態時間)-更多資訊請參閱渲染準確的狀態同步訣竅部分。

基於最新的已連網資料(_speed_jumpCount)的渲染準確的動畫:

C#

[Networked]
private float _speed { get; set; }
[Networked]
private int _jumpCount { get; set; }

private int _lastVisibleJump;

public override void Spawned()
{
    _lastVisibleJump = _jumpCount;
}

public override void FixedUpdateNetwork()
{
    // _speed and _jumpCount is changing here
}

public override void Render()
{
    _animator.SetFloat("Speed", _speed);

    if (_lastVisibleJump < _jumpCount)
    {
        _animator.SetTrigger("Jump");
    }

    _lastVisibleJump = _jumpCount;
}

基於內插補點資料的渲染準確的動畫。內插補點器的使用使動畫更加準確,但複雜度略有增加:

C#

[Networked]
private float _speed { get; set; }
[Networked]
private int _jumpCount { get; set; }

public override void FixedUpdateNetwork()
{
    // _speed and _jumpCount is changing here
}

public override void Render()
{
    var interpolator = new NetworkBehaviourBufferInterpolator(this);

    _animator.SetFloat("Speed", interpolator.Float(nameof(_speed)));

    int interpolatedJumpCount = interpolator.Int(nameof(_jumpCount));

    if (_lastVisibleJump < interpolatedJumpCount)
    {
        _animator.SetTrigger("Jump");
    }

    _lastVisibleJump = interpolatedJumpCount;
}

訣竅

渲染準確的狀態同步的訣竅

渲染準確的方法不關注動畫狀態同步,而是關注同步更改並在正確的時間應用這些更改。然而,為了提高渲染準確的方法的精確度,最好至少實作某種狀態同步。這意味著定期或在某些事件中同步角色動畫的當前播放狀態,特別是當前狀態時間。

例如,已知代理角色正在跑步及其速度。但是,當這些值首次在Animator中設定時(在加入遊戲後,當代理角色進入本機玩家的感興趣區域等時),即使遠端玩家已經跑步了20秒,它也會從一開始就播放運動迴圈。這將導致客戶端之間的動畫時間不同,從而導致動畫同步不精確。當遠端玩家執行另一個動作(如跳躍)時,這個問題通常會自行解決,但在玩家執行此類動作之前,動畫時間可能會非常糟糕。

在Unity Animator的情況下。Animator.GetCurrentAnimatorStateInfo()可用於獲取當前狀態的完整路徑雜湊和標準化時間,將其作為標準已連網屬性同步,並透過調用Animator.Play(int stateNameHash, int layer, float normalizedTime)將其應用於代理角色。然而,只有當動畫器處於過渡狀態時,才應進行同步,以防止出現任何不必要的視覺效果故障。

注意: 這正是啟用StateRoot同步時,NetworkMecanimAnimator做的。它同步第一層上當前播放的動畫狀態及其時間;除非動畫器在狀態之間過渡,否則它會連續這樣做。連續同步標準化時間並不理想,因為值會不斷變化並消耗網路資源。

刷新準確的狀態同步的訣竅

無論動畫狀態是基於PlayablesAnimancerStates還是Unity的AnimationStates驅動,請確保僅同步正確重新計算所需的資料,最好不要同步隨時間變化的值。例如,不必每個刷新都發送 時間重量 更改,但至關重要的是同步狀態的 開始刷新淡出速度目標重量,以計算每個客戶端上的當前 重量時間 值。

Back to top