マニュアル
Animator View Updater
QuantumEntityViewUpdateスクリプトは、AnimatorComponentがアタッチされた各エンティティに対して、Animateコールバックをトリガーします。エンティティは、IAnimatorEntityViewComponentインターフェースを実装したQuantumEntityViewComponentを持つ必要があります。AnimatorアドオンにはAnimatorMecanimとAnimatorPlayablesが含まれていて、そのまま使用することも、よりカスタマイズされたワークフローのために拡張することもできます。
Animator Mecanim
Quantum AnimatorとUnityのAnimator間の連携を提供し、アニメーションのスムーズな遷移とフレーム単位の同期を可能にします。フレームレートの制御や、AnimatorPlayablesではサポートされていないアニメーションレイヤーの使用も可能にします。
 
遷移のミュート
AnimatorMecanim使用時、ごくまれに偽陽性の(誤検知による)遷移が発生する可能性があり、わずかなビジュアルの乱れが生じます。これを防ぐには、AimatorGraph設定のMute Graph Transitions On Exportを有効にしてください。AnimatorGraphをベイクすると、AnimatorControllerのすべての遷移がミュートされます。
Animator Playables
UnityのAnimatorのかわりにPlayableGraphを使用してアニメーションを制御することで、Quantum Animatorによるアニメーション更新で、よりカスタマイズしたアプローチを取ることができます。ただし、アニメーションレイヤーなど、UnityのAnimatorのすべての機能がサポートされているわけではありません。
Animatorイベント
Quantum Animatorは、Instant(即時)イベントとTime-Window(時間帯)イベントの両方をサポートします。指定クリップの再生時間が、イベント作成時に設定された時間になった際に、イベントコールバックがトリガーされます。つまり、同じクリップを利用する2つのステートは、イベントを共有することになります。
Instant Event
C#
public abstract class AnimatorInstantEventAsset : AnimatorEventAsset, IAnimatorEventAsset
  {
    /// <inheritdoc cref="AnimatorEventAsset.OnBake"/>
    public new AnimatorEvent OnBake(AnimationClip unityAnimationClip, AnimationEvent unityAnimationEvent)
    {
      var quantumAnimatorEvent = new AnimatorInstantEvent();
      quantumAnimatorEvent.AssetRef = Guid;
      quantumAnimatorEvent.Time = FP.FromFloat_UNSAFE(unityAnimationEvent.time);
      return quantumAnimatorEvent;
    }
  }
Instant Eventは、Animatorがイベントをベイクしたクリップを再生した際に、Execute()関数を一度だけトリガーします。基底クラスAnimatorInstantEventAssetを継承し、独自Frame.Signalsなどによる異なる処理でクラスメソッドをオーバーライドするのが、良いパターンです。
イベントアセットは、Create->Quantum->Assets...を使用して作成できます。
 
Time-Window Event
作成手順はInstant Eventと似ていますが、Unityのクリップ内で、イベント実行終了を示す同じ種類の2つ目のイベントも作成する必要があります。このイベントは、Unityのクリップで定義された開始フレームから終了フレームの間、毎フレームExecuteコールバックがトリガーされます。
 
OnEnter()・Execute()・OnExit()と同様の動作を持つ新しいイベントを作成したい場合は、AnimatorTimeWindowEventAssetクラスを継承してください。
C#
  /// <summary>
  /// SampleTimeWindowEvent使用方法のサンプルです。
  /// AnimatorInstantEventAssetを継承したクラスを作成し、独自ロジックを実装してください。
  /// </summary>
  [Serializable]
  public class ExampleTimeWindowEventAsset : AnimatorTimeWindowEventAsset
  {
    public override unsafe void OnEnter(Frame frame, AnimatorComponent* animatorComponent, LayerData* layerData)
    {
      Debug.Log($"[Quantum Animator ({frame.Number})] OnEnter animator time window event.");
    }
    
    public override unsafe void Execute(Frame frame, AnimatorComponent* animatorComponent, LayerData* layerData)
    {
      Debug.Log($"[Quantum Animator ({frame.Number})] Execute animator time window event.");
    }
    public override unsafe void OnExit(Frame frame, AnimatorComponent* animatorComponent, LayerData* layerData)
    {
      Debug.Log($"[Quantum Animator ({frame.Number})] OnExit animator time window event.");
    }
  }
イベントのセットアップ
アニメーションクリップでイベントを定義するには、以下の手順に従ってください。
- QuantumEntityPrototype内の- Animatorコンポーネントを持つゲームオブジェクト自身に、- AnimationEventDataコンポーネントを追加します。
 
- Animationウィンドウでクリップを選択し、新しいUnityのAnimationEventを追加します。
 
- 作成したイベントを選択し、以下の画像のようにFunctionとObjectを指定します。
 
Animatorステートシグナル
Animatorステートシグナルは、遷移やFadeToでステートが変更される際に呼び出されます。これらはAnimatorBehaviourSystemで実装されていて、他のシステムからでも実装可能です。
C#
// Animatorがステートに遷移した際の、最初のUpdateで呼び出される
void OnAnimatorStateEnter(Frame frame, EntityRef entity, AnimatorComponent* animator, AnimatorGraph graph, LayerData* layerData, AnimatorState state, FP time);
// Animatorがステートを更新している間、毎フレーム呼び出される
void OnAnimatorStateUpdate(Frame frame, EntityRef entity, AnimatorComponent* animator, AnimatorGraph graph, LayerData* layerData, AnimatorState state, FP time, AnimatorStateType stateType);
// Animatorが別のステートに遷移する前の、最後のUpdateで呼び出される
void OnAnimatorStateExit(Frame frame, EntityRef entity, AnimatorComponent* animator, AnimatorGraph graph, LayerData* layerData, AnimatorState state, FP time);
これらのシグナルは、以下の値を提供します。
- Frame:シグナルが呼び出された現在の- Frame
- EntityRef:シグナルに渡された- AnimatorComponentが関連する- EntityRef
- AnimatorComponentのポインタ
- AnimatorGraphと- AnimatorStateの参照
- LayerDataのポインタ
- シグナルが呼び出された際のLayerDataの対応する時間
OnAnimatorStateUpdateはAnimatorStateTypeも提供します。このenumの値は次の通りです。
- None
- FromState
- CurrentState
- ToState
遷移中は、ToStateとFromStateの両方がOnAnimatorStateUpdateシグナルを送信するため、どちらのステートが更新されたのかを判別するのにAnimatorStateTypeが役立ちます。例えば、これを使用して、遷移終了中にAnimatorStateBehaviourが更新を実行することを防ぐことができます。
Animator State Behaviour
Animatorイベントとは対照的に、Animator State Behaviourは同じクリップを使用するステート間で独立していて、OnStateEnter・OnStateUpdate・OnStateExitをサポートします。
セットアップ
- UnityのAnimatorウィンドウで、ステートを選択し、そこにAnimatorStateBehaviourHolderスクリプトをアタッチします。
 
- Animatorパッケージに含まれるDebugBehaviourの例のように、AnimatorStateBehaviourAssetsリストに新しい要素を追加します。
 
- メニューオプションTool->Quantum Animator->Bake All Graph Assetsから、グラフアセットをベイクします。
- AnimatorBehaviourSystemを- Default Config Systemsアセットに追加します。
- ステートが再生されるたびに、デバッグメッセージがログに表示されます。
AnimatorStateBehaviourは抽象クラスなので、継承したら以下のメソッドを実装する必要があります。
C#
bool OnStateEnter(Frame frame, EntityRef entity, AnimatorComponent* animator, AnimatorGraph graph, LayerData* layerData, AnimatorState state, FP time);
bool OnStateUpdate(Frame frame, EntityRef entity, AnimatorComponent* animator, AnimatorGraph graph, LayerData* layerData, AnimatorState state, FP time, AnimatorStateType stateType);
bool OnStateExit(Frame frame, EntityRef entity, AnimatorComponent* animator, AnimatorGraph graph, LayerData* layerData, AnimatorState state, FP time);
これらは前述のAnimatorステートシグナルとほぼ同等ですが、boolが返されます。AnimatorBehaviourSystem使用時にtrueを返すと、AnimatorStateの残りのAnimatorStateBehaviourのチェックが停止されます。例えば、AnimatorStateBehaviourがFadeToを使用して新しいステートに遷移する場合、trueを返すことで、残りのAnimatorStateBehaviourがOnAnimatorStateUpdateを実行することを防ぐことができます。
FadeTo
FadeToは、条件とは独立して別のステートに遷移するために使用できます。この機能はAllowFadeToTransitionsで有効にできます。
C#
  // public void FadeTo(Frame frame, AnimatorComponent* animatorComponent, string stateName, bool setIgnoreTransitions, FP deltaTime)
  // 使用例
  if (input->Run.WasPressed)
  {
    graph.FadeTo(frame, filter.AnimatorComponent, "Running", true, 0);
  }
ルートモーション
Quantum Animatorは、UnityのAnimationClip情報を使用して、モーションカーブデータをAnimatorGraphアセットにベイクします。ベイクされた情報はシミュレーションで使用され、再生中のクリップに応じてエンティティの位置や回転を変更します。
 
使用方法
モーションデータを処理できるようにするには、Root Motionオプションを有効にする必要があります。モデルのAnimationタブで、ルートモーション設定を正しくセットアップすることも重要です。詳細な情報は、Unityドキュメントのルートモーションをご覧ください。
 
ステートのルートモーション無効化
デフォルトでは、Root Motionオプションが有効である場合、AnimatorGraphのすべてのステートでモーションデータが有効になります。ステートにAnimator Disable Root Motion Behaviourを追加することで、そのステートはルートモーションの計算から除外されます。
 
使用例
Quantum Animatorは、各フレームのモーションの差分のみを計算します。ユーザーは、生成されたデータを使用して、エンティティのtransformを必要に応じて更新します。以下のコードは、OnAnimatorRootMotion3D/OnAnimatorRootMotion2Dの応用例を示します。
C#
  // 3Dルートモーションの処理
  public void OnAnimatorRootMotion3D(Frame frame, EntityRef entity, AnimatorFrame deltaFrame, AnimatorFrame currentFrame)
    {
      // モーション差分がない場合は終了する
      if (deltaFrame.Position == FPVector3.Zero && deltaFrame.RotationY == FP._0) return;
      if (frame.Unsafe.TryGetPointer<Transform3D>(entity, out var transform))
      {
        // 現在フレームのY軸の回転の逆を表すQuaternionを作成する
        var currentFrameRotation = FPQuaternion.CreateFromYawPitchRoll(currentFrame.RotationY, 0, 0);
        currentFrameRotation = FPQuaternion.Inverse(currentFrameRotation);
        // 移動を揃えるために、差分位置を逆回転させる
        var newPosition = currentFrameRotation * deltaFrame.Position;
        // 新しい位置にtransformの回転を適用して、ワールド座標系の位置を取得する
        var displacement = transform->Rotation * newPosition;
        var kccSettings = frame.FindAsset<KCCSettings>(frame.Unsafe.GetPointer<KCC>(entity)->Settings);
        // レイキャスト用に対象のヒット位置を計算する
        var targetHitPosition =(displacement.XOZ.Normalized * FP._0_33 * 2 ) + displacement;
        // 意図したモーション方向にレイキャストを飛ばし、静的オブジェクトとの潜在的な衝突を検知する
        var hits = frame.Physics3D.RaycastAll(transform->Position, targetHitPosition.XOZ, targetHitPosition.Magnitude, -1,
          QueryOptions.HitStatics);
        if (hits.Count <= 0)
        {
          // 衝突がない場合、キャラクターコントローラーを一時的に無効にする
          if (frame.Unsafe.TryGetPointer<KCC>(entity, out var kcc))
          {
            kcc->SetActive(false);
          }
          // transformにモーションと回転を適用する
          transform->Position += displacement;
          transform->Rotate(FPVector3.Up, deltaFrame.RotationY * FP.Rad2Deg);
        }
        else
        {
          // 衝突がない場合、キャラクターコントローラーを有効にする
          if (frame.Unsafe.TryGetPointer<KCC>(entity, out var kcc))
          {
            kcc->SetActive(true);
          }
        }
      }
    }
    public void OnAnimatorRootMotion2D(Frame frame, EntityRef entity, AnimatorFrame deltaFrame, AnimatorFrame currentFrame)
    {
      // モーション差分がない場合は終了する
      if (deltaFrame.Position == FPVector3.Zero && deltaFrame.RotationY == FP._0) return;
      if (frame.Unsafe.TryGetPointer<Transform2D>(entity, out var transform))
      {
        // 差分を適用して、新しい回転を計算する
        FP newRotation = transform->Rotation + deltaFrame.RotationY;
        // 回転を正規化して[-π, π]範囲に収める
        while (newRotation < -FP.Pi) newRotation += FP.PiTimes2;
        while (newRotation > FP.Pi) newRotation += -FP.PiTimes2;
        // 新しい回転に基づいて、移動差分を回転する
        var deltaMovement = FPVector2.Rotate(deltaFrame.Position.XZ, newRotation);
        
        // transformに移動と回転を適用する
        transform->Position += deltaMovement;
        transform->Rotation = newRotation;
      }
    }