UnityECSでInputからの情報でアニメーションを変化させよう

# Inputからの情報を取得
多くのRPG系のゲームではプレイヤーの操作から様々な入力情報を読み取りアニメーションを変化させます。
UnityにもメカニムというものがAnimatorにあるのでそれを使ってUnityECSでも実現させていきます。
上記の通りHybridアプローチになります。
前回同様ゲームオブジェクトのAnimatorを使用してSetTrigger()を呼び出してみます。

# CharacterControllerPackageを使う
前回同様CharacterControllerPackageを使います。

これを参考にするのはJumpの操作と同様です。
ボタンを押したとき、その秒数を記録してその秒数が前回記録した秒数と同じ値ならジャンプ処理が発動するというものです。(1フレームごとに秒数を更新するため1フレームのみの発火)

# UnityInputSystemを使ってみる
入力処理には`UnityInputSystem`を使います。
普通にしていたらECSでは使えないのですが、ある秘密コマンドを使えばECSで使えるようになります。
それは`Generate C# Class`にチェックを入れ、`Apply`ボタンを押すです。
![スクリーンショット 2024-12-20 194348.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3949075/630229eb-70de-aa86-dd71-3f4f0d2ed656.png)
これで選択した`PlayerActionAsset`に設定してあるデータを使って同じ処理をC#クラスとして作ってくれます。
このクラスで作成されたメソッドを呼ぶことでボタンが押された処理を受け取ることができるようになります。

詳しくはこの人の記事を参考にしてください。

https://qiita.com/makihiro_dev/items/e05eaa3fdf05869725ac

`ThirdPersonPlayerInputSystem`を改造して`InputActionSystem`を作りました。

```diff_c_sharp
partial class InputActionSystem : SystemBase
{
    private PlayerAction.CharacterControllerActions _actionMap;

    // 最初の処理
    protected override void OnCreate()
    {
        // 上記の方法で作ったPlayerActionAssetのクラスをnew してインスタンシエイトする
+        PlayerAction playerActions = new PlayerAction();
+        playerActions.Enable();
+        // ActionAssetで作成した`ActionMaps`の名前になっている。
+        // つまりActionMapの切り替えは{クラス名}.{ActionMap名}.Enable()で呼び出せるということ
+        playerActions.CharacterController.Enable();
+        // アクションマップをシステムに保持する
+        _actionMap = playerActions.CharacterController;
    }
    [BurstCompile]
    protected override void OnUpdate()
    {
        // FixedUpdateの時の秒数を記録(Jump)とか物理を使う場合はサンプル通り
        uint fixedTick = SystemAPI.GetSingleton<FixedTickSystem.Singleton>().Tick;
        // VariableUpdateの時の秒数を記録(一般的な描画用Update)
+       uint variableTick = SystemAPI.GetSingleton<VariableTickSystem.Singleton>().Tick;

        foreach (var (input, player, animator) in SystemAPI.Query<RefRW<PlayerInputs>, PlayerComponent, RefRW<PlayerAnimation>>())
        {
            input.ValueRW.Move = Vector2.ClampMagnitude(_actionMap.Move.ReadValue<Vector2>(), 1f);
            input.ValueRW.Look = _actionMap.Look.ReadValue<Vector2>();

            input.ValueRW.SprintHeld = _actionMap.Sprint.IsPressed();
            // 保持したActionMap.{Action名}.WasPressedThisFrame()でボタンがこのフレームで押されたかどうかを処理
            if (_actionMap.Jump.WasPressedThisFrame())
            {
                // 押されていた場合Inputコンポーネントに秒数を記録
                input.ValueRW.JumpPressed.Set(fixedTick);
            }
            // 攻撃ボタンとしてFireとした。こちらはアニメーション用(描画用)なのでVariable
+            if (_actionMap.Fire.WasPressedThisFrame())
+            {
+                // Fixedと同様
+                input.ValueRW.FirePressed.Set(variableTick);
+            }
        }
    }
}
```

`PlayerInputs`コンポーネントはこれ
``` csharp
[Serializable]
public struct PlayerInputs : IComponentData
{
    public float2 Move;
    public float2 Look;

    public bool SprintHeld;
        
    public FixedInputEvent JumpPressed; // 押したか押してないかの処理は構造体として持つこれはFixed用
    public VariableInputEvent FirePressed; // これはVariable用
}
```

`VariableInputEvent`は`FixedInputEvent`を参考に作成
```diff_c_sharp
public struct FixedInputEvent
{
    private byte _wasEverSet;
    private uint _lastSetTick;
    
    public void Set(uint tick)
    {
        _lastSetTick = tick;
        _wasEverSet = 1;
    }
    
    public bool IsSet(uint tick)
    {
        if (_wasEverSet == 1)
        {
            return tick == _lastSetTick;
        }

        return false;
    }
}
+public struct VariableInputEvent
+{
+    private byte _wasEverSet;
+    private uint _lastSetTick;
+
+    public void Set(uint tick)
+    {
+        _lastSetTick = tick + 1;
+        _wasEverSet = 1;
+    }

+    public bool IsSet(uint tick)
+    {
+        if (_wasEverSet == 1)
+        {
+            return tick == _lastSetTick;
+        }
+        return false;
+    }
+}
```

そしてVaribleUpdateの秒数を生成するシステムも`FixedTickSystem`を参考に作成する
```csharp
[BurstCompile]
partial struct VariableTickSystem : ISystem
{
    public struct Singleton : IComponentData
    {
        public uint Tick;
    }

    public void OnCreate(ref SystemState state)
    {
        if (!SystemAPI.HasSingleton<Singleton>())
        {
            Entity singletonEntity = state.EntityManager.CreateEntity();
            state.EntityManager.AddComponentData(singletonEntity, new Singleton());
        }
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        ref Singleton singleton = ref SystemAPI.GetSingletonRW<Singleton>().ValueRW;
        singleton.Tick++;
    }
}
```

最後にこの処理を行ったあとどう他の処理へつなげていくかだが、サンプルのJumpを参考にする

サンプルでは`ThirdPersonPlayerSystems`の中にこう記述してある
```diff_c_sharp
/// <summary>
/// Apply inputs that need to be read at a fixed rate.
/// It is necessary to handle this as part of the fixed step group, in case your framerate is lower than the fixed step rate.
/// </summary>
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup), OrderFirst = true)]
[BurstCompile]
public partial struct ThirdPersonPlayerFixedStepControlSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        uint tick = SystemAPI.GetSingleton<FixedTickSystem.Singleton>().Tick;
        
        foreach (var (playerInputs, player) in SystemAPI.Query<ThirdPersonPlayerInputs, ThirdPersonPlayer>().WithAll<Simulate>())
        {
            if (SystemAPI.HasComponent<ThirdPersonCharacterControl>(player.ControlledCharacter))
            {
                ～割愛～
                // Move
                characterControl.MoveVector = (playerInputs.MoveInput.y * cameraForwardOnUpPlane) + (playerInputs.MoveInput.x * cameraRight);
                characterControl.MoveVector = MathUtilities.ClampToMaxLength(characterControl.MoveVector, 1f);

                // Jump
+               characterControl.Jump = playerInputs.JumpPressed.IsSet(tick);

                SystemAPI.SetComponent(player.ControlledCharacter, characterControl);
            }
        }
    }
}
```
ここで`PlayerInputs`の`JumpPressed.IsSet(tick);`でCharacterControlコンポーネントに同じフレームでセットされているか(このフレームでボタンが押されたか)を取得している。

なのでVariableUpdateを処理する`PlayerVariableStepControlSystem`に`FirePressed`を書いていく
```diff_c_sharp

public partial struct PlayerVariableStepControlSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<VariableTickSystem.Singleton>();
        state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<PlayerComponent, PlayerInputs>().Build());
    }
    [BurstCompile] //Jobにしているが別にどこで呼んだって良い
    public partial struct PlayerVariableInputJob : IJobEntity
    {
        public ComponentLookup<CameraControl> CC;
        public uint ticks;
        [BurstCompile]
        void Execute(PlayerInputs inputs, PlayerComponent player, RefRW<PlayerAnimation> animation, Entity entity)
        {
            var cameraControl = CC.GetRefRW(player.ControlledCamera);
            cameraControl.ValueRW.FollowedCharacterEntity = entity;
            var delta = inputs.Look;
            cameraControl.ValueRW.LookDegreesDelta = math.lerp(inputs.Look, delta, 0.1f);
            // PlayerAnimationコンポーネントを作り、その中にtrueかfalseかを格納している
+            animation.ValueRW.AttackTrigger = inputs.FirePressed.IsSet(ticks);
        }
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        uint tick = SystemAPI.GetSingleton<VariableTickSystem.Singleton>().Tick;

        var job = new PlayerVariableInputJob 
        { 
            ticks = tick,
            CC = SystemAPI.GetComponentLookup<CameraControl>()
        };
        job.Schedule();
    }
}
```

# Animation処理
前回と同様にHybridで処理する。
コンポーネントは前回同様
```CSharp
public struct PlayerAnimation : IComponentData
{
    // パラメーターがちゃんとロードしきったか
    public bool ParameterLoaded;

    //Hash
    public int SpeedParameterHash;
    public int AttackParameterHash;

    //Parameter
    public float SpeedMagnitude;
    public bool AttackTrigger; // ボタンが押されたかどうかをboolで格納する
}
```
上記で格納した値で`PlayerAnimationSystem`を動かす
```csharp
public partial class PlayerAnimationSystem : SystemBase
{
    protected override void OnUpdate()
    {
        foreach (var (characterAnimation, 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++)
                {
                    if (animator.parameters[i].name == "speed")
                    {
                        characterAnimation.ValueRW.SpeedParameterHash = animator.parameters[i].nameHash;
                    }
                    if (animator.parameters[i].name == "attack")
                    {
                        // Attack(Trigger)の場合パラメーターハッシュを取得
                        characterAnimation.ValueRW.AttackParameterHash = animator.parameters[i].nameHash;
                    }
                }
                characterAnimation.ValueRW.ParameterLoaded = true;
            }
        }

        foreach (var (characterAnimation,player, playerInputs, hybridLink, charactercontrol, entity) in SystemAPI.Query<
            RefRW<PlayerAnimation>,
            RefRW<PlayerComponent>,
            PlayerInputs,
            CharacterHybridLink,
            CharacterControl>()
            .WithEntityAccess())
        {
            if (hybridLink.Object)
            {
                // Animation
                if (hybridLink.Animator)
                {
                    characterAnimation.ValueRW.SpeedMagnitude = math.length(charactercontrol.MoveVector);
                    PlayerAnimationHandler.UpdateSpeed(hybridLink.Animator, ref characterAnimation.ValueRW);
                    // PlayerAnimationコンポーネントにtrueと入っていた場合はPlayerAnimationHandlerを呼び出してAnimator.SetTrigger(attackTriggerHash);の形にする
                    if(characterAnimation.ValueRO.AttackTrigger)
                    {
                        PlayerAnimationHandler.OnAttackTrigger(hybridLink.Animator, ref characterAnimation.ValueRO);
                    }
                }
            }
        }
    }
}
```

前回同様`PlayerAnimationHandelr`
```csharp
public static class PlayerAnimationHandler
{
    public static void UpdateSpeed(Animator animator, ref PlayerAnimation playerAnimation)
    {
        //Debug.Log(playerAnimation.SpeedMagnitude);
        animator.SetFloat(playerAnimation.SpeedParameterHash, playerAnimation.SpeedMagnitude);
    }
    // ここではAttackTriggerコンポーネント丸ごと渡して、そこの値を使用している
    public static void OnAttackTrigger(Animator animator, ref PlayerAnimation playerAnimation)
    {
        animator.SetTrigger(playerAnimation.AttackTriggerHash);
    }
}
```
これで攻撃モーションが呼び出せました。

# 訂正(追記)
すみません。上記の説明で`InputSystem`の入力を取得するために`FixedTickSystem`などを参考に`VariableTickSystem`などを追加で書きましたが、素直に`WasPressedThisFrame()`を取得すればそのフレーム単位のtrue,falseが取れますのでそれで発火しても良いということに気づきましたので追記します。

```diff_c_sharp
protected override void OnUpdate()
{
    uint fixedTick = SystemAPI.GetSingleton<FixedTickSystem.Singleton>().Tick;

    foreach (var (input, player, animator) in SystemAPI.Query<RefRW<PlayerInputs>, PlayerComponent, RefRW<PlayerAnimation>>())
    {
        input.ValueRW.Move = Vector2.ClampMagnitude(_actionMap.Move.ReadValue<Vector2>(), 1f);
        input.ValueRW.Look = _actionMap.Look.ReadValue<Vector2>();

        input.ValueRW.SprintHeld = _actionMap.Sprint.IsPressed();
        if (_actionMap.Jump.WasPressedThisFrame())
        {
            input.ValueRW.JumpPressed.Set(fixedTick);
        }
+       input.ValueRW.FirePressed = _actionMap.Fire.WasPressedThisFrame();
    }
}
```
```diff_c_sharp
[Serializable]
public struct PlayerInputs : IComponentData
{
    public float2 Move;
    public float2 Look;

    public bool SprintHeld;
        
    public FixedInputEvent JumpPressed;
+   public bool FirePressed;
}
```

```diff_c_sharp
public partial struct PlayerVariableStepControlSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<PlayerComponent, PlayerInputs>().Build());
    }

    [BurstCompile]
    public partial struct PlayerVariableInputJob : IJobEntity
    {
        void Execute(RefRO<PlayerInputs> inputs, RefRO<PlayerComponent> player, RefRW<PlayerAnimation> animation, Entity entity)
        {
            cameraControl.ValueRW.LookDegreesDelta = math.lerp(inputs.ValueRO.Look, delta, 0.1f);
+           animation.ValueRW.AttackTrigger = inputs.ValueRO.FirePressed;
        }
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var job = new PlayerVariableInputJob 
        { 
            CC = SystemAPI.GetComponentLookup<CameraControl>()
        };
        job.Schedule();
    }
}
```
一応記録として前回の記事を残しておきます。