Animation
概述
動畫為玩家提供了至關重要的遊戲遊玩回饋,例如玩家輸入引起的移動和動作。
本文檔介紹了在使用Fusion組建多人遊戲時選擇正確的動畫方法。這些示例將側重於角色動畫;然而,本文中提出的透過網路同步動畫的概念,也適用於任何其他動畫物件。
關於實際的動畫範例,請參考Fusion動畫技術範例。
動畫準確度
在開始Fusion中的動畫之前,重要的是要建立動畫系統所需和目標的基線準確性。
一般來說,基於準確性,有兩種類型的動畫方法:
- 渲染準確的動畫
- 刷新準確的動畫
渲染準確的動畫
渲染準確的動畫在模擬之外執行(從Render
或MonoBehaviour.Update
調用),或者僅在FUN Forward
* 階段執行。動畫的開始通常只是鬆散同步的,在重新模擬過程中,角色姿勢不會倒退到以前的狀態。由於完整的動畫狀態(當前播放的動畫、動畫時間、動畫權重等)不是已連網資料的一部分,因此無法保證角色在指定時間在所有客戶端上都處於 完全 相同的姿勢。
使用渲染準確的方法意味著根據肢體位置的計算可能會非常不準確。不準確的程度在很大程度上取決於動畫的類型。靜態動畫(如Idle或Aim)可能足夠準確,而跑步或快速動作可能會明顯更差。建議不要在動畫層次中使用變換進行模擬計算(例如從手掌進行雷射射線計算)。然而,這種用法很少是必要的。有關更多資訊,請查看我需要刷新準確的動畫嗎?部分。
✅ 優點:
- 能夠使用常見的動畫解決方案(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頁面)。
為了在代理物件突然出現時(例如在延遲加入後或代理物件進入興趣區域時)獲得更好的動畫行為,請啟用StateRoot
(動畫器控制器中的第一層)和StateLayers
(所有其他層)的同步,這將確保在正確的時間播放正確的狀態。
注意: 啟用此選項將顯著增加資料流量。
有關實際示例,請參閱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狀態或玩家行為狀態。
有關實際示例,請參閱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
會自動捕獲命中框資料。對於重新模擬,已保存的命中框位置用於正確的光線投射計算,因此代理角色上的動畫不需要回到過去。這正是為什麼即使 渲染準確的方法,如果執行正確,也能產生足夠精確的延遲補償命中。
伺服器假設客戶端在遠端時間框架內渲染代理角色,因此伺服器上使用此時間進行延遲補償的投射。因此,客戶端上的動畫也需要尊重代理的遠端時間框架。換句話說,客戶端上的代理需要基於內插補點資料進行動畫處理,以獲得準確的延遲補償。
使用刷新準確的動畫可以更好、更精確地實現正確的動畫時間,因為內插補點的動畫狀態資料可以用於每個FUN Forward
和Render
中的代理,以在給定的刷新或渲染時間重建他們的角色姿勢。在這種情況下,代理角色只是顯示來自伺服器的內插補點資料,它們本身並沒有真正「播放」動畫。
另一方面,渲染準確的動畫完全在代理角色上運行。重要的是參數設置為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
做的。它同步第一層上當前播放的動畫狀態及其時間;除非動畫器在狀態之間過渡,否則它會連續這樣做。連續同步標準化時間並不理想,因為值會不斷變化並消耗網路資源。
刷新準確的狀態同步的訣竅
無論動畫狀態是基於Playables
、AnimancerStates
還是Unity的AnimationStates
驅動,請確保僅同步正確重新計算所需的資料,最好不要同步隨時間變化的值。例如,不必每個刷新都發送 時間 和 重量 更改,但至關重要的是同步狀態的 開始刷新、淡出速度 和 目標重量,以計算每個客戶端上的當前 重量 和 時間 值。