0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりで完走_C# is GODAdvent Calendar 2024

Day 20

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

Last updated at Posted at 2024-12-20

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
これで選択したPlayerActionAssetに設定してあるデータを使って同じ処理をC#クラスとして作ってくれます。
このクラスで作成されたメソッドを呼ぶことでボタンが押された処理を受け取ることができるようになります。

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

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

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コンポーネントはこれ

[Serializable]
public struct PlayerInputs : IComponentData
{
    public float2 Move;
    public float2 Look;

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

VariableInputEventFixedInputEventを参考に作成

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を参考に作成する

[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の中にこう記述してある

/// <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);
            }
        }
    }
}

ここでPlayerInputsJumpPressed.IsSet(tick);でCharacterControlコンポーネントに同じフレームでセットされているか(このフレームでボタンが押されたか)を取得している。

なのでVariableUpdateを処理するPlayerVariableStepControlSystemFirePressedを書いていく


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で処理する。
コンポーネントは前回同様

public struct PlayerAnimation : IComponentData
{
    // パラメーターがちゃんとロードしきったか
    public bool ParameterLoaded;

    //Hash
    public int SpeedParameterHash;
    public int AttackParameterHash;

    //Parameter
    public float SpeedMagnitude;
    public bool AttackTrigger; // ボタンが押されたかどうかをboolで格納する
}

上記で格納した値でPlayerAnimationSystemを動かす

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

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が取れますのでそれで発火しても良いということに気づきましたので追記します。

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();
    }
}
[Serializable]
public struct PlayerInputs : IComponentData
{
    public float2 Move;
    public float2 Look;

    public bool SprintHeld;
        
    public FixedInputEvent JumpPressed;
+   public bool FirePressed;
}
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();
    }
}

一応記録として前回の記事を残しておきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?