Inputからの情報を取得
多くのRPG系のゲームではプレイヤーの操作から様々な入力情報を読み取りアニメーションを変化させます。
UnityにもメカニムというものがAnimatorにあるのでそれを使ってUnityECSでも実現させていきます。
上記の通りHybridアプローチになります。
前回同様ゲームオブジェクトのAnimatorを使用してSetTrigger()を呼び出してみます。
CharacterControllerPackageを使う
前回同様CharacterControllerPackageを使います。
これを参考にするのはJumpの操作と同様です。
ボタンを押したとき、その秒数を記録してその秒数が前回記録した秒数と同じ値ならジャンプ処理が発動するというものです。(1フレームごとに秒数を更新するため1フレームのみの発火)
UnityInputSystemを使ってみる
入力処理にはUnityInputSystem
を使います。
普通にしていたらECSでは使えないのですが、ある秘密コマンドを使えばECSで使えるようになります。
それはGenerate C# Class
にチェックを入れ、Apply
ボタンを押すです。
これで選択した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用
}
VariableInputEvent
はFixedInputEvent
を参考に作成
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);
}
}
}
}
ここでPlayerInputs
のJumpPressed.IsSet(tick);
でCharacterControlコンポーネントに同じフレームでセットされているか(このフレームでボタンが押されたか)を取得している。
なのでVariableUpdateを処理するPlayerVariableStepControlSystem
にFirePressed
を書いていく
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();
}
}
一応記録として前回の記事を残しておきます。