Unity ECS CharacterControllerPackageにHybridAnimationとRootMotion機能追加

# UnityECS CharacterControllerPackage
UnityECSにチャレンジしようとするとき、Unity既存のCharacterControllerと同じような機能を持つCharacterControllerPackageが存在します。

今回これを使ってモデルをアニメーションさせる方法と、RootMotionでCharacterControllerを動かす方法について紹介します。

※インストール方法や基本の使い方は割愛

# サンプルを取り込む
CharacterControllerのPackageからサンプルをインストールします
![スクリーンショット 2024-12-18 231746.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949075/56ffd164-8e52-769f-50a6-93ced595d1ff.png)

このサンプルで基本の動く操作ができるようになっています。

今回はこのサンプルに付け足して、アニメーションモデルの表示、アニメーション、RootMotion対応をやっていきます。
※サンプルの使い方は割愛

# HybridAnimation
Unityには自前ではECS用のアニメーションシステムがありません。
ですので今回はゲームオブジェクトを上に重ねる方法をとるHybridAnimationの方法をご紹介します。

UnityECSの世界といままでのUnityのゲームオブジェクトの世界はまるで別物で、両者同士は明示的に連携をさせないと一切関わりあうことができません。
しかし画面の上では、同じワールド座標に入れさえいればあたかも両者が同じ世界にいるように錯覚させることができるのです。(実際はただ別の世界線で重なってるだけ)

## HybridAnimation Component Authoring

ということでコードになります。
まず`ThirdPersonCharacterComponent`を変更します
```diff_c_sharp
[Serializable]
public struct ThirdPersonCharacterComponent : IComponentData
{
+   public Entity MeshRootEntity;
+   public bool UseRootMotion;
    
    public float RotationSharpness;
    public float GroundMaxSpeed;
    public float GroundedMovementSharpness;
    public float AirAcceleration;
    public float AirMaxSpeed;
    public float AirDrag;
    public float JumpSpeed;
    public float3 Gravity;
    public bool PreventAirAccelerationAgainstUngroundedHits;
    public BasicStepAndSlopeHandlingParameters StepAndSlopeHandling;
}

[Serializable]
public struct CharacterControl : IComponentData
{
    public float3 MoveVector;
+   public float3 RootMotionVelocity;
    public float3 AccumulatedDeltaPosition;
    public bool Jump;
}
```
自身のメッシュの位置情報をゲームオブジェクトに伝えるための`MeshRootEntity`
ルートモーションを使うか否かの`UseRootMotion`を追加します。
ルートモーションのヴェロシティの値を保持する`RootMotionVelocity`を追加

また、ECSでアニメーションを動かすために必要な`CharacterHybridData`コンポーネントとアニメーションを呼び出すために必要な`CharacterHybridLink`を作成します。
```csharp
[Serializable]
public class CharacterHybridData : IComponentData
{
    public GameObject MeshPrefab;
    public bool isCopyCollider;
}

[Serializable]
public class CharacterHybridLink : ICleanupComponentData
{
    public GameObject Object;
    public Animator Animator;
}
```
`struct`ではなく`class`になるので注意してください。

Authoringは以下の通りです。
先ほど作成した`CharacterHybridLink`は`System`で動的に追加していきます。

```diff_c_sharp
[DisallowMultipleComponent]
public class ThirdPersonCharacterAuthoring : MonoBehaviour
{
    public AuthoringKinematicCharacterProperties CharacterProperties = AuthoringKinematicCharacterProperties.GetDefault();

    [Header("References")]
+   public GameObject MeshPrefab;
+   public GameObject MeshRoot;
+   public bool UseRootMotion;

    public float RotationSharpness = 25f;
    public float GroundMaxSpeed = 10f;
    public float GroundedMovementSharpness = 15f;
    public float AirAcceleration = 50f;
    public float AirMaxSpeed = 10f;
    public float AirDrag = 0f;
    public float JumpSpeed = 10f;
    public float3 Gravity = math.up() * -30f;
    public bool PreventAirAccelerationAgainstUngroundedHits = true;
    public BasicStepAndSlopeHandlingParameters StepAndSlopeHandling = BasicStepAndSlopeHandlingParameters.GetDefault();

    public class Baker : Baker<ThirdPersonCharacterAuthoring>
    {
        public override void Bake(ThirdPersonCharacterAuthoring authoring)
        {
            KinematicCharacterUtilities.BakeCharacter(this, authoring.gameObject, authoring.CharacterProperties);

            Entity entity = GetEntity(TransformUsageFlags.Dynamic | TransformUsageFlags.WorldSpace);

            AddComponent(entity, new ThirdPersonCharacterComponent
            {
+               MeshRootEntity = GetEntity(authoring.MeshRoot, TransformUsageFlags.Dynamic),
+               UseRootMotion = authoring.UseRootMotion,
                RotationSharpness = authoring.RotationSharpness,
                GroundMaxSpeed = authoring.GroundMaxSpeed,
                GroundedMovementSharpness = authoring.GroundedMovementSharpness,
                AirAcceleration = authoring.AirAcceleration,
                AirMaxSpeed = authoring.AirMaxSpeed,
                AirDrag = authoring.AirDrag,
                JumpSpeed = authoring.JumpSpeed,
                Gravity = authoring.Gravity,
                PreventAirAccelerationAgainstUngroundedHits = authoring.PreventAirAccelerationAgainstUngroundedHits,
                StepAndSlopeHandling = authoring.StepAndSlopeHandling,
            });
            AddComponent(entity, new ThirdPersonCharacterControl());
+           AddComponentObject(entity, new CharacterHybridData 
+           { 
+               MeshPrefab = authoring.MeshPrefab,
+           });
        }
    }
}
```
動かしたいモデルにアニメーションコントローラーを付けそれをプレハブとして保存しておきます。

![スクリーンショット 2024-12-19 000644.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949075/84812557-390b-89ff-f4ee-1b5c415bb7e2.png)



このPackageではThirdPersonCharacterの位置がルートとずれているので、ルートの位置に合わせるようにThirdPersonCharacterの下の階層に`MeshRoot`オブジェクトを追加します。

![スクリーンショット 2024-12-19 000447.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949075/7ec19086-5020-3722-e560-ae75d3e04658.png)

作成後上記で作成したモデルのプレハブとMeshRootをThirdPersonCharacterにアタッチします。

## HybridAnimationSystem
HybridAnimationSystemとして以下を新規に追加します。
```csharp
public partial class PlayerHybridSystem : SystemBase
{
    protected override void OnUpdate()
    {
        EntityCommandBuffer ecb = SystemAPI.GetSingletonRW<EndSimulationEntityCommandBufferSystem.Singleton>().ValueRW.CreateCommandBuffer(World.Unmanaged);

        //Create
        // CharacterHybridDataを持つエンティティに対して処理を行う
        // この処理の中で`CharacterHybridLink`を追加しており、WithNone<CharacterHybridLink>で
        // 次回以降は呼び出さないようにしている
        foreach (var (hybridData, entity) in SystemAPI.Query<CharacterHybridData>()
                     .WithNone<CharacterHybridLink>()
                     .WithEntityAccess())
        {
            // EditorでアタッチしたGameObject(モデル)をインスタンシエイトして、そのオブジェクトを取得
            // そのオブジェクトが持っているAnimatorも取得しておく
            GameObject tmpObject = GameObject.Instantiate(hybridData.MeshPrefab);
            Animator animator = tmpObject.GetComponent<Animator>();

            // 取得したゲームオブジェクトとAnimatorをCharacterHybridLinkに入れ、現在のEntityにAddComponetする
            ecb.AddComponent(entity, new CharacterHybridLink
            {
                Object = tmpObject,
                Animator = animator
            });
        }

        // Update
        // 上記のCharacterHybridLinkのコンポーネントを付けたことでこの処理が動き出す。
        foreach (var (characterTransform, characterComponent, characterControl, hybridLink, entity) in SystemAPI.Query<
            LocalTransform,
            CharacterComponent,
            RefRW<CharacterControl>,
            CharacterHybridLink>()
            .WithEntityAccess())
        {
            if (hybridLink.Object)
            {
                // RootMotion実装処理(内容は後述)
                if (characterComponent.UseRootMotion)
                {
                    // CharacterControlのRootMotionVelocityにAnimatorから受け取ったdeltaPositionを渡す
                    characterControl.ValueRW.RootMotionVelocity = hybridLink.Animator.deltaPosition;
                }

                // ゲームオブジェクトをmeshRootの位置に合わせる
                // 上記でアタッチしたRootMeshの位置が取得できるので、その位置にゲームオブジェクトを持ってくる
                LocalToWorld meshRootLTW = SystemAPI.GetComponent<LocalToWorld>(characterComponent.MeshRootEntity);
                hybridLink.Object.transform.position = meshRootLTW.Position;
                hybridLink.Object.transform.rotation = meshRootLTW.Rotation;
            }
        }
    }
}
```
これでアニメーションを行うゲームオブジェクトとECSのCharacterControllerの位置が重ね合わさります。

RootMotionがOnの時、Animatorから取得できる`deltaPosition`を与えることで、CharacterControllerがアニメーションの移動速度に合わせることが常時できるようになっています。

次に入力に対してアニメーションを動かしていきます。

まずアニメーション用のコンポーネントを作成します。

```csharp
public struct PlayerAnimation : IComponentData
{
    // パラメーターを取得できたかどうか(アニメーションのパラメーターの取得は一回きりでいいため)
    public bool ParameterLoaded;

    //Hash値を保存
    public int SpeedParameterHash;
    //public int AttackParameterHash;

    //Parameterの値を保存
    public float SpeedMagnitude;
    //public bool AttackTrigger;
}

```

入力を処理する`PlayerAnimationSystem`を作成します。
```diff_c_sharp
public partial class PlayerAnimationSystem : SystemBase
{
    protected override void OnUpdate()
    {
        // Create
        // CharacterHybridLinkコンポーネントがEntityに対して処理を行う
        foreach (var (playerAnimation, hybridLink, entity) in SystemAPI.Query<RefRW<PlayerAnimation>, CharacterHybridLink>().WithEntityAccess())
        {
            // パラメーターのハッシュ値を設定
            if(!characterAnimation.ValueRO.ParameterLoaded)
            {
                Animator animator = hybridLink.Animator;
                // Find the clipIndex param
                for (int i = 0; i < animator.parameters.Length; i++)
                {
                    // speedのパラメーターのハッシュを取得
                    if (animator.parameters[i].name == "speed")
                    {
                        playerAnimation.ValueRW.SpeedParameterHash = animator.parameters[i].nameHash;
                    }
                    // 今回割愛
                    //if (animator.parameters[i].name == "attack")
                    //{
                    //    playerAnimation.ValueRW.AttackParameterHash = animator.parameters[i].nameHash;
                    //}
                }
                characterAnimation.ValueRW.ParameterLoaded = true;
            }
        }

        var ecb = new EntityCommandBuffer(Allocator.Temp);
        //Update
        // CharacterControl,PlayerAniamtion,CharacterHybridLinkを持つエンティティに対して実行
        foreach (var (playerAnimation,player, playerInputs, hybridLink, charactercontrol, entity) in SystemAPI.Query<
            RefRW<PlayerAnimation>,
            RefRW<PlayerComponent>,
            PlayerInputs,
            CharacterHybridLink,
            CharacterControl>()
            .WithEntityAccess())
        {
            if (hybridLink.Object)
            {
                // Animation
                if (hybridLink.Animator)
                {
                    // 入力から取得したMoveVectorを取得し絶対値を保持
                    playerAnimation.ValueRW.SpeedMagnitude = math.length(charactercontrol.MoveVector);
                    // PlayerAnimationHandlerを呼び出し、パラメーターの値を変える
                    // plyaerAniamtionなどに保存していた値をすべて投げる
                    PlayerAnimationHandler.UpdateSpeed(hybridLink.Animator, ref characterAnimation.ValueRW);
                    //if(characterAnimation.ValueRO.AttackTrigger)
                    //{
                    //    ecb.AddComponent(entity, new AttackTrigger
                    //    {
                    //        Animator = hybridLink.Animator,
                    //        AttackTriggerHash = characterAnimation.ValueRO.AttackParameterHash
                    //    });
                    //}
                }
            }
        }
    }
}
```
このシステムではUpdateでプレイヤーからの入力ベクトルを読み取りそれを`Anmator.SetFloat(hasmap,float);`の形に持っていきたいので全部`PlayerAnimationHandler`に投げ入れてます。

以下が`PlayerAnimationHandler`
```csharp
public static class PlayerAnimationHandler
{
    // これを呼び出す
    public static void UpdateSpeed(
        Animator animator,
        ref PlayerAnimation playerAnimation)
    {
        animator.SetFloat(playerAnimation.SpeedParameterHash, playerAnimation.SpeedMagnitude);
    }
    //public static void OnAttackTrigger(AttackTrigger trigger)
    //{
    //    trigger.Animator.SetTrigger(trigger.AttackTriggerHash);
    //}
}
```

これで入力した移動量によってアニメーションがなされるようになります。

# RootMotion対応
先ほど`Animator.deltaPosition`は`CharacterControl`のVelocityに入れていましたがそれをどう処理しているかについて解説します。

変更する場所は以下の通りです
`CharacterAspect`の`VariableUpdate()`と`HandleVelocityControl()`の以下の部分です
```diff_c_sharp
[BurstCompile]
public void VariableUpdate(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
    ref KinematicCharacterBody characterBody = ref KinematicAspect.CharacterBody.ValueRW;
    ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
    ref CharacterControl characterControl = ref CharacterControl.ValueRW;
    ref quaternion characterRotation = ref KinematicAspect.LocalTransform.ValueRW.Rotation;

    // AccumulatedDeltaPositionにRootMotionVelcityを追加する
+    characterControl.AccumulatedDeltaPosition += characterControl.RootMotionVelocity;

    // Add rotation from parent body to the character rotation
    // (this is for allowing a rotating moving platform to rotate your character as well, and handle interpolation properly)
    //KinematicCharacterUtilities.AddVariableRateRotationFromFixedRateRotation(ref characterRotation, characterBody.RotationFromParent, baseContext.Time.DeltaTime, characterBody.LastPhysicsUpdateDeltaTime);
    
    // Rotate towards move direction
    if (math.lengthsq(characterControl.MoveVector) > 0f)
    {
        CharacterControlUtilities.SlerpRotationTowardsDirectionAroundUp(ref characterRotation, baseContext.Time.DeltaTime, math.normalizesafe(characterControl.MoveVector), MathUtilities.GetUpFromRotation(characterRotation), characterComponent.RotationSharpness);
    }
}

[BurstCompile]
private void HandleVelocityControl(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
+    float deltaTime = baseContext.Time.DeltaTime; //deltaTimeを取得
    ref KinematicCharacterBody characterBody = ref KinematicAspect.CharacterBody.ValueRW;
    ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
    ref CharacterControl characterControl = ref CharacterControl.ValueRW;

    // Rotate move input and velocity to take into account parent rotation
    if(characterBody.ParentEntity != Entity.Null)
    {
        characterControl.MoveVector = math.rotate(characterBody.RotationFromParent, characterControl.MoveVector);
        characterBody.RelativeVelocity = math.rotate(characterBody.RotationFromParent, characterBody.RelativeVelocity);
    }

    // Move on ground
    if (characterBody.IsGrounded)
    {
        // RootMotionを使うときの処理
        if(characterComponent.UseRootMotion == true)//UseRootMotion
        {
            //先ほど追加したAccumulatedDeltaPosisonにdeltaTimeを割ったvelocityを渡している
+            float3 targetVelocity = characterControl.AccumulatedDeltaPosition / deltaTime;
            CharacterControlUtilities.StandardGroundMove_Interpolated(ref characterBody.RelativeVelocity, targetVelocity, characterComponent.GroundedMovementSharpness, deltaTime, characterBody.GroundingUp, characterBody.GroundHit.Normal);
            // Jump
            if (characterControl.Jump)
            {
                CharacterControlUtilities.StandardJump(ref characterBody, characterBody.GroundingUp * characterComponent.JumpSpeed, true, characterBody.GroundingUp);
            }
        }
        else//NoRootMotion
        {
            float3 targetVelocity = characterControl.MoveVector * characterComponent.GroundMaxSpeed;
            CharacterControlUtilities.StandardGroundMove_Interpolated(ref characterBody.RelativeVelocity, targetVelocity, characterComponent.GroundedMovementSharpness, deltaTime, characterBody.GroundingUp, characterBody.GroundHit.Normal);

            // Jump
            if (characterControl.Jump)
            {
                CharacterControlUtilities.StandardJump(ref characterBody, characterBody.GroundingUp * characterComponent.JumpSpeed, true, characterBody.GroundingUp);
            }
        }            
    }
```

これでアニメーションの移動量に対してしっかりと追随してくれるようになります。

少し駆け足になりましたがこれで終わります。