アニメーション
概要
アニメーションは、プレイヤーの入力によって引き起こされる動きやアクションなど、ゲームプレイに重要なフィードバックを与えます。
本ドキュメントは、Fusionでマルチプレイヤーゲームを作成する際に、アニメーションに対して適切なアプローチを選択する方法について説明します。ここでは、キャラクターアニメーションを中心に説明しますが、ネットワーク上でアニメーションを同期させるためのコンセプトは、他のアニメーションオブジェクトにも適用可能です。
実用的なアニメーションサンプルは、Fusion Animations technical sample を参照してください。
アニメーションの精度
Fusionでアニメーションを始める前に、アニメーションシステムが必要とするベースライン精度と、目標とする精度を設定することが重要です。
一般的に、アニメーションは、その精度により2種類のアプローチがあります。
- レンダーアキュレートアニメーション
- ティックアキュレートアニメーション
レンダリングアキュレートアニメーション
レンダリングアキュレートアニメーションはシミュレーションの外(Render
, MonoBehaviour.Update
, OnChanged
コールから)、または FUN Forward
* ステージで実行されます。アニメーションの開始は通常、緩やかにしか同期しておらず、再シミュレーション中にキャラクターのポーズが以前の状態に巻き戻されることはありません。アニメーションの完全な状態(現在再生中のアニメーション、アニメーション時間、アニメーションの重さなど)はネットワークデータの一部ではないので、キャラクターが特定の時間にすべてのクライアントで 正確に 同じポーズになることは保証されません。
レンダーアキュレートアプローチを使用すると、手足の位置に依存する計算がかなりずれてしまう可能性があります。不正確さの程度は、アニメーションの種類に大きく依存します。Idle やAim などの静的なアニメーションの精度は十分ですが、走行や素早い動作は著しく劣る可能性があります。シミュレーション計算(手のひらからのレーザー光線の計算など)には、アニメーション階層内の変換を使用しないことをお勧めします。ただし、このような使い方が必要になることはほとんどありません。詳しくは ティックアキュレートアニメーションは必要?の項をご覧ください。
✅ 長所:
- 一般的なアニメーションソリューション(Animator、Animancer)を使用することができる。
- スタジオのアニメーションパイプラインを変更する必要がない。
- サードパーティのアニメーションアドオンがすぐに使える。
❌ 短所:
- アニメーション階層内のトランスフォームを正確なシミュレーション計算に使用することはできない。
- アニメーションに依存するヒットボックスの位置が不正確 - ただし、これはいくつかの解決策で軽減できる ( アニメーションと遅延補償 を確認してください)
*FUN Forward
= FixedUpdateNetwork
method call with Forward
simulation stage (Runner.IsForward == true
)
ティックアキュレートアニメーション
Tick Accurate Animationとは、アニメーションが任意のtickにおいて、すべてのクライアントとサーバー上で全く同じ状態であることを意味します。言い換えれば、完全なアニメーション状態がネットワークデータの一部であり、再シミュレーションの際にアニメーションを巻き戻して再生することができます(というより、ステップスルー)。すべてのクライアントでキャラクターを同じポーズにすることは、キャラクターの手足にある遅延補償コライダーにとって重要です。たとえば、クライアントで走行アニメーションの途中で他のプレイヤーの脚にぶつかると、サーバーでもまったく同じようにぶつかることになります。
✅ 長所:
- キャラクターは、すべてのクライアントとサーバーで常に同じポーズをとっているため、すべてのヒット計算が正確に行えます。
- 手のひらから発射されるレーザー光線や、近接攻撃のヒートボックスの計算など、手足の正確な位置関係を基にした計算が可能になります。
❌ 短所:
- カスタムアニメーションソリューションが必要なため、実現が難しい(Unity Animatorはティックアキュレートアニメーションを未対応)。
- サードパーティのアニメーションアドオンが動作しない可能性がある(Animatorに依存している、再シミュレーションを扱えない、などの理由による)
- アニメーションソリューションはコード中心で、Animator Controllers などの他のソリューションと比較すると、アニメーションを設定する際の開発者体験が損なわれる可能性があります。
最新のティックデータがサーバーから届き、クライアント側で再シミュレーションが開始されると、コードはローカルプレイヤー(= 入力権限)のアニメーションを新しいデータに基づいて、ローカル予測ティックに再び達するまで再シミュレーションします。プロキシはスナップショットで補間された時間(=サーバーからの検証済みデータに基づく)でアニメーションするため、再シミュレーション中に特別な処理を行う必要はなく、プロキシのアニメーションは再シミュレーションで同一になります。
遅延を正しく補正するには、FUN Forward* 呼び出しで、HitboxManager がヒットボックスの位置を保存する前に、プロキシのアニメーションを一度評価すれば十分です。再シミュレーション中、プロキシのキャラクターポーズは変更されず、すでに保存されたヒットボックス位置が正しい遅延補償キャストに使用されます。
*FUN Forward
= FixedUpdateNetwork
メソッドコールに Forward
シミュレーションステージを追加しました。 (Runner.IsForward == true
)
ティックアキュレートアニメーションは必要ですか?
ティックアキュレートアニメーションソリューションの構築は複雑で時間がかかります。プロジェクトに絶対必要なのか、追加作業負荷が正当化されるのか、検討してください。多くの場合、ティックアキュレートアニメーションは不要であり、レンダーアキュレートアニメーションでプロジェクトを成功させれば、プレイヤーにその違いを気づかれることはありません。どのアプローチを選択するかは、非常にゲームに依存します。
一般的には、プロジェクトが以下の2つのケースのいずれかに該当する場合、ティックアキュレートアプローチが必要となります。
- キャラクタのアニメーションパーツに配置されたヒットボックスに対して、100%正確なヒットが必要な場合
- アニメーションの影響を大きく受け、手動計算では簡単に置き換えられないトランスフォーム位置を使用した計算がある - 例: 近接攻撃中に拳に接続されているヒートボックスを駆動する場合。
キャラクターの位置や方向に基づく正確な計算は、レンダーアキュレートアプローチでも実現できます。ただ、アニメーションされたトランスフォーム階層(=ボーン)の影響を受けるトランスフォームに基づいて計算することはできないということです。ゲーム状態での計算位置とその視覚的表現は、以下の例で説明するように異なる可能性があります。
*例1: FPSゲームでは、射撃の計算をアニメーションに影響される実際の銃身からではなく、カメラから行うのが一般的な方法です。Fusion Projectiles で、弾丸のビジュアルが実際の軌道にどのように補間されるかを確認してください。しかし、実際に銃身から計算する必要があり、銃の位置や回転がプレイヤーのアニメーションに影響される場合は、より正確なアプローチが必要です。
例2:近接攻撃を行うとき、ダメージを受けたボックスは攻撃中にあらかじめ定義されたスクリプト化された経路をたどることができます。このパスは再シミュレーション時にキャラクターのアニメーションに依存せずに実行することができます。一方、ヒルトボックスがキャラクタの拳の複雑な軌道をアニメーションでデザインされた通りにたどらなければならない場合、ティックアキュレートアニメーションが必要です。
レンダーアキュレートなアニメーションソリューション
以下は、推奨されるレンダーアキュレートなアニメーションソリューションのリストです。
注:以下の段落では、Unity Mecanim Animator をAnimator と呼びます。
Animator/Animancer + 既存のネットワーク状態
レンダリングアキュレートアプローチ
通常、既存のネットワーク状態を使用してアニメーションを制御することができます。アニメーションの設定は、Render
メソッドやOnChanged
コールバックのネットワークデータに基づいて行われます。この方法は実装が簡単で、ネットワークリソースを無駄にせず、ゲームのニーズに応じてより正確に設定できるため、ほとんどのゲームに簡単に推奨できます。
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()
{
if (Object.IsProxy == true)
return;
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;
}
OnChangedコールバックを代わりに使用する類似の機能。
C#
[Networked]
private NetworkButtons _lastButtonsInput { get; set; }
[Networked(OnChanged = nameof(OnJumpChanged))]
private int _jumpCount { get; set; }
public override void FixedUpdateNetwork()
{
if (Object.IsProxy == true)
return;
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 static void OnJumpChanged(Changed<Player> changed)
{
changed.LoadOld();
int previousJumpCount = changed.Behaviour._jumpCount;
changed.LoadNew();
if (changed.Behaviour._jumpCount > previousJumpCount)
{
changed.Behaviour._animator.SetTrigger("Jump");
// Play jump sound/particle effect
}
}
OnChangedコールバックの簡易版を使用することも可能です(下記参照)。しかし、このような使い方は、サーバー上でジャンプが起こらない可能性があり(例えば、プレイヤーがジャンプの前に実際に相手によって固まった)、ローカルプレイヤーに対して2回(ローカル予測中に値が増加したときとサーバーからデータを受け取った後に値が返されたとき)Jump をトリガーすることになるため、若干エラーが起こりやすくなっています。
C#
public static void OnJumpChanged(Changed<Player> changed)
{
changed.Behaviour._animator.SetTrigger("Jump");
}
実用例としては、Fusion Animations technical sample (例1-3)を参照してください。
Animator + NetworkMecanimAnimator
レンダリングアキュレートアプローチ
Fusion に付属するビルトインの NetworkMecanimAnimator
コンポーネントを使用して、Animator のプロパティを同期させることができます。NetworkMecanimAnimator
コンポーネントにアニメーターをアサインした後、アニメーターのプロパティを設定すれば、NetworkMecanimAnimator
が全てのクライアントに自動的に同期します。例外はトリガーで、これも NetworkMecanimAnimator
コンポーネントで設定する必要があります (詳しくはマニュアルの Pre-built Components ページを参照してください)。
プロキシオブジェクトが突然現れたときのアニメーションの挙動を改善するために、StateRoot
(アニメーターコントローラーの最初のレイヤー) と StateLayers
(他のすべてのレイヤー) の同期を有効にすることで、正しいステートが正しい時間に再生されるようにします。
備考: このオプションを有効にすると、データ通信量が大幅に増加します。
実用例としては、Fusion Animations technical sample (例4)を参照してください。
アニメーター/アニメーター+FSMの同期化
レンダリングアキュレートのアプローチ
FSM (Finite State Machine)の状態からアニメーションを再生する - 例えば、ジャンプ状態、攻撃状態、運動状態。現在の状態はネットワーク上で同期され、アニメーションに必要な追加データは標準的なネットワーク上のプロパティに保存することができます。
C#
public class JumpState : PlayerState
{
protected override void OnEnterState()
{
DoJump();
}
protected override void OnEnterStateRender()
{
Animator.SetTrigger("Jump");
}
}
アニメーション制御を状態に分割することで、複雑なアニメーション設定を管理し、アニメーションと一緒に他の視覚効果を簡単に制御できます(例:ジャンプ状態に入るとジャンプ音とジャンプVFXを再生)。FSMが再シミュレーションの対応と適切に同期している場合、他のシミュレーションロジックにも使用できます - 状態は、実際にはAIの状態やプレイヤーの動作状態になることがあります。
実用例としては、Fusion Animations technical sample (例5)を参照してください。
ティックアキュレートなアニメーションソリューション
標準のUnity Animatorは、アニメーションの状態を制御するためのオプションが限られており、ティックアキュレートなアニメーションに使用することができません。しかし、Unity の下位レベルの Playables API では、そのような機能を使用することができます。そのため、ティックアキュレートアニメーションを対象としたプロジェクトであれば、Playablesをベースにした実装が必要になる可能性が高いです。
Animancer + ティックアキュレートなアニマンサー状態のラッパー
ティックアキュレートアプローチ
Animancer は Playables をベースにしているので、AnimancerState
のプロパティを同期させ、FixedUpdateNetwork
から手動で Animancer をステップさせれば、ティックアキュレートなアニメーションを実現することができます。
この方法は、プロジェクトで「十分なティック精度」が許容される場合にのみ推奨されます。Animancer はバックグラウンドでいくつかの動作を行っているので(特定のフェードの間に無重力の状態を作成するなど)、すべての Animancer 機能で 100% のティック精度を達成するには時間がかかり、すぐに Playables に基づいたカスタムソリューションにしたほうが、長期的には好ましいかもしれません。
1. すべての
AnimancerState
のプロパティを個別に同期します2. 状態の配列全体 (
AnimancerLayer.States
) をそのまま同期しますプロジェクトに必要なアニメーションソリューションの複雑さによっては、2.の方が簡単で確実かもしれません。
Playables API 上のカスタムソリューション
ティックアキュレートアプローチ
Playables APIは、本マニュアルの範囲外です。基本的には、アニメーションの状態を表すカスタムデータ構造を使って、必要なデータをネットワーク上で同期させ、PlayableGraph
を構築します。そして、プロジェクトのニーズに応じて、標準的なFusionのメソッドで PlayableGraph
を評価します。
Fusion BR サンプルで、バトルテスト済みのコード駆動型ソリューション、または Fusion Animations technical sample の洗練・簡略化されたバージョン(例6)を確認して下さい。
レガシー Unity アニメーション + AnimationStates のティックアキュレートラッパー
ティックアキュレートなアプローチ。
AnimationStates
のプレイに関するデータを同期させるために、カスタムデータ構造を使用します。Animation.Sample
を使用して、ティックに合わせた正しいキャラクターのポーズを実現します。
プロジェクトのアニメーションパイプラインが既にレガシーアニメーションシステムを使用していない限り、これは推奨されない解決策です。
アニメーションと遅延補償
遅延補正 は、あるクライアントで登録されたヒットが、サーバーでも正しく認識されることを保証します。
ヒットボックスがアニメーションの影響を受ける場合、ヒットボックスの状態がキャプチャされる前に、キャラクターが正しいポーズであることを確認することが重要です。ヒットボックスデータは FUN Forward
コールの HitboxManager
によって自動的にキャプチャされることに注意してください。再シミュレーションでは、すでに保存されているヒットボックスの位置が正しいレイキャストの計算に使用されるため、プロキシキャラクターのアニメーションは時間を遡る必要がありません。これがまさに、レンダーアキュレートアプローチであっても、正しく実行すれば十分に正確な遅延補償ヒットになる 理由です。
HitboxManger
の前に評価する必要がある場合、その前に実行する必要があるSimulationBehaviour
またはNetworkBehaviour
スクリプトで[OrderBefore(typeof(HitboxManger))]
属性を使用することができます。
サーバーは、クライアントがプロキシ文字をスナップショット補間された時間でレンダリングすることを想定しており、このタイミングはサーバーで遅延補償キャストに使用されます。したがって、クライアントのアニメーションは、プロキシのスナップショット補間されたタイミングを尊重する必要があります。言い換えると、正確な遅延補償を行うには、クライアントのプロキシは補間されたデータに基づいてアニメーション化される必要があります。
アニメーションのタイミングを正確にすることで、より正確なアニメーションが実現できます。なぜなら、補間されたアニメーションステートデータは、すべての FUN Forward
と Render
でプロキシに使われ、与えられた tick やレンダー時間にキャラクターのポーズを再構築することができるからです。この場合、プロキシはサーバーから送られたデータを表示するだけで、実際にアニメーションを再生するわけではありません。
一方、レンダーアキュレートアニメーションは、プロキシキャラクター上で完全に実行されます。重要なのは、Animator にパラメータが設定されたとき、またはアニメーションクリップが開始されるときです。単純な方法としては、トータルな精度を無視し、最新のネットワークデータに基づいて動作します(下の例参照)。しかし、より正確なアニメーションのためには、補間されたデータに基づいてアニメーションパラメータを設定したり、クリップの再生を開始したりすることが必要です。
また、通常、再生中のアニメーション(アニメーションの状態と時間)を定期的に、または特別な場合(キャラクターがクライアントの関心領域に入るなど)に同期させる必要があります。詳しくは、「正確な状態をレンダリングするためのヒント」(#tips_for_render_accurate_state_synchronization)のセクションで説明しています。
最新のネットワークデータ(_jumpCount
)に基づいて正確なアニメーションをレンダリングします。
C#
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;
public override void Spawned()
{
_lastVisibleJump = _jumpCount;
}
public override void FixedUpdateNetwork()
{
if (HasJumpInput() == true)
{
DoJump();
_jumpCount++;
}
}
public override void Render()
{
if (_lastVisibleJump < _jumpCount)
{
_animator.SetTrigger("Jump");
}
_lastVisibleJump = _jumpCount;
}
補間されたデータに基づいて正確なアニメーションをレンダリングします (_jumpCountInterpolator
)。補間データを使用すると、アニメーションがより正確になりますが、その分複雑になります。
C#
[Networked]
private int _jumpCount { get; set; }
private int _lastVisibleJump;
private Interpolator<int> _jumpCountInterpolator;
public override void Spawned()
{
_lastVisibleJump = _jumpCount;
_jumpCountInterpolator = GetInterpolator<int>(nameof(_jumpCount));
}
public override void FixedUpdateNetwork()
{
if (HasJumpInput() == true)
{
DoJump();
_jumpCount++;
}
}
public override void Render()
{
if (_jumpCountInterpolator.Value > _lastVisibleJump)
{
_animator.SetTrigger("Jump");
}
_lastVisibleJump = _jumpCountInterpolator.Value;
}
ヒント
レンダーアキュレートな状態同期のためのヒント
レンダーアキュレートアプローチは、アニメーションの状態の同期に重点を 置くのではなく、変更を同期し、その変更を正しいタイミングで適用することに重点を置 きます。しかし、レンダーアキュレートアプローチの精度を上げるには、少なくともある種の状態同期を実装するのがよいでしょう。これは、キャラクタアニメーションの現在の再生状態、特に現在の状態時間を、定期的または特定のイベントで同期させることを意味します。
例えば、プロキシキャラクターが走っていることと、そのスピードは分かっています。しかし、これらの値が初めてアニメーターに設定されたとき(ゲームに参加した後、プロキシキャラクタがローカルプレイヤーの関心領域に入ったときなど)、リモートプレイヤーがすでに20秒間走っていたにもかかわらず、ロコモーションループが最初から再生されます。これにより、クライアント間でアニメーションのタイミングが異なるため、アニメーションの同期が不正確になります。この問題は通常、リモートプレイヤーが Jump などの別のアクションを実行すると解決しますが、プレイヤーがそのようなアクションを実行するまでは、アニメーションのタイミングがかなりずれることがあります。
Unity Animator の場合は Animator.GetCurrentAnimatorStateInfo()
を使って現在の状態のフルパスハッシュと正規化時間を取得し、標準のネットワークプロパティとして同期させ、 Animator.Play(int stateNameHash, int layer, float normalizedTime)
を呼んで代理キャラクターにそれを適用させることができます。ただし、同期を行うのはアニメーターが遷移してないときだけにして、不要な視覚的な不具合を防ぐ必要があります。
StateRoot
同期が有効な場合、NetworkMecanimAnimator
が行うのはまさにこれです。アニメーターが状態間を遷移するとき以外は、常に同期されます。正規化された時間を継続的に同期させることは、値が常に変化し、ネットワークリソースを消費するため、理想的ではありません。
ティックアキュレートな状態の同期を正確に行うためのヒント
アニメーションの状態が Playables
や AnimancerStates
、あるいは Unity の AnimationStates
に基づいて駆動される場合でも、正しい再計算に必要なデータのみを同期させ、時間の経過による値の変更を同期させないことが理想的です。例えば、Time と Weight の変更をティックごとに送信する必要はありませんが、状態の StartTick、FadeSpeed、TargetWeight を同期して、すべてのクライアントで現在の Weight と Time 値を計算することは非常に重要です。