Appearance
発展 AnimatorとPlayerState
基本実習: 第06回 Enemyの状態制御とゲームフィール / 次回: 第07回 アイテムとオブジェクトプール。
今回の発展内容

EnemyStateをAnimator Parameterへ渡し、EnemyのAnimation Clipを切り替える- EnemyとPlayerの状態を共通化する条件と、個別に定義する条件を比較する
- Playerへ
HitReactionと、固有のPlayerStateを追加する PlayerStateをAnimator Parameterへ渡し、PlayerのAnimation Clipを切り替える
このページは、第05回の基本実習と完成条件を満たしたあとに取り組みます。
Animator発展コード
基本版のAssets/Scriptsへ、発展コードを追加または上書きします。導入時は、下表の使用方法に従ってファイルを追加または上書きします。
| ファイル | 使用方法 |
|---|---|
| EnemyAnimator.cs | 基本版へ追加する |
| PlayerState.cs | 基本版へ追加する |
| PlayerController.cs | 基本版を上書きする |
| PlayerAnimator.cs | 基本版へ追加する |
Animatorからゲーム状態を取得するのはNG
Animator Controllerも内部にステートマシンを持ちますが、ゲームロジックから利用するための汎用的な状態管理システムではありません。一応は、現在のステートに関する情報をGetCurrentAnimatorStateInfo()で取得することはできます。
cs
var stateInfo =
animator.GetCurrentAnimatorStateInfo(0);
if (stateInfo.IsName("Base Layer.Attack"))
{
// AttackのAnimation Clipを再生中
}ですが、取得できるAnimatorStateInfoは、指定したレイヤーにおけるその時点の再生情報です。EnemyState.Attackのような型付きのゲーム状態を取得できるわけではありません。ゲームオブジェクトの状態管理には使いづらいため、その目的での利用を推奨するものではありません。
| Animatorから状態を調べるときの制約 | 影響 |
|---|---|
| レイヤーごとに現在ステートがある | キャラクター全体の状態を1つに決められない |
| 遷移中は現在と次のステートがある | どちらをゲーム状態とするか別途決める必要がある |
| 判定に名前、タグ、ハッシュを使う | Animator Controllerの変更とコードが結合する |
| 状態変更を型付きイベントとして受け取れない | 毎フレーム確認するか、別の通知処理が必要になる |
| Blend Tree内では複数のClipが混ざる | ステート名だけでは実際の見た目を表せない |
通常は、ゲーム上の状態をコードで保持し、AnimatorへParameterを一方向に渡すのがおすすめです。
図を準備しています…
| 層 | 担当すること | 担当しないこと |
|---|---|---|
| Controller | 入力やAI判断、状態変更 | Animation Clipの再生 |
| State enum | ゲーム上の現在状態 | 遷移条件の判断 |
| Animator接続 | 状態をParameterへ変換 | 状態変更、ダメージ計算 |
| Animator Controller | Clipの切り替えとブレンド | 攻撃判定、HP管理、AI判断 |
Animation ClipとAnimator Controllerの準備
| Clip | 素材がない場合の簡易表現 |
|---|---|
Idle | 子Objectを静止させる |
Move | 子Objectを上下へ小さく動かす |
Attack | 武器または子Objectを短く回転させる |
Damage | 子Objectを短時間だけ後方へ傾ける |
Dead | 子Objectを横へ倒す |
コードが移動させるルートObjectには、位置と回転のAnimation Keyを設定しません。AnimatorのApply Root Motionも無効にし、ゲーム上の移動はPlayerMoveとEnemyControllerへ任せます。EnemyのDead ClipはEnemy.destroyDelayより短くするか、Clipを最後まで再生できるようにdestroyDelayを長くします。
Animator Controllerを作成する

EnemyとPlayerで使用するAnimation Clipが異なる場合は、それぞれにAnimator Controllerを作成します。Parameter名と遷移構造は共通にできますが、設定するClipはキャラクターごとに分けることをおすすめします。
- ProjectウィンドウでEnemy用のAnimator Controllerを作成します
Idle、Move、Attack、Damage、Deadのステートを作りますIdleをDefault StateにしますIsMoving、IsAttacking、IsDamaged、IsDeadのBool Parameterを作ります- 各ステートへ対応するAnimation Clipを設定します
Idleは4つのBoolがすべてfalseの状態として扱います。
AnimatorControllerとパラメーター
IdleからMoveへのトランジション指定の例。IsMoving が True のときに遷移するように設定されている。
AnimatorControllerのParameterは、AnimatorController内での遷移条件やブレンドのために使われます。
cs
// AnimatorControllerのパラメーターを更新する
_animator.SetBool(
"IsMoving",
state == EnemyState.Move);パラメーター名は文字列で指定しますが、文字列のままではアクセスに時間がかかるため、事前にハッシュ値を割り出しておくのが定石です。
cs
private static readonly int IsMovingId =
Animator.StringToHash("IsMoving");
// ハッシュ値を使ってAnimatorのパラメーターを更新する
_animator.SetBool(
IsMovingId,
state == EnemyState.Move);遷移条件を設定する
| 遷移 | 条件 |
|---|---|
Idle → Move | IsMoving == true |
Move → Idle | IsMoving == false |
Any State → Attack | IsAttacking == true |
Attack → Idle | IsAttacking == false |
Any State → Damage | IsDamaged == true |
Damage → Idle | IsDamaged == false |
Any State → Dead | IsDead == true |
ゲーム状態の変化をすぐ見た目へ反映するため、これらの遷移ではHas Exit Timeを無効にします。Transition Durationは0から0.1秒程度で確認します。Any Stateから作った遷移ではCan Transition To Selfも無効にし、同じステートを繰り返し開始しないようにします。Deadからほかのステートへ戻る遷移は作りません。
INFO
PlayerにもAnimationを設定する場合は、同じ構造のPlayer用Animator Controllerを作るところから始めてみましょう。
EnemyStateをAnimatorへ渡す
EnemyAnimatorはEnemyController.Stateを読み、各状態に対応するBool Parameterを更新します。
cs
using UnityEngine;
// EnemyStateをAnimator Parameterへ反映する
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(EnemyController))]
public class EnemyAnimator : MonoBehaviour
{
private static readonly int IsMovingId =
Animator.StringToHash("IsMoving");
private static readonly int IsAttackingId =
Animator.StringToHash("IsAttacking");
private static readonly int IsDamagedId =
Animator.StringToHash("IsDamaged");
private static readonly int IsDeadId =
Animator.StringToHash("IsDead");
private Animator _animator;
private EnemyController _enemyController;
// 必要なコンポーネントを取得する
private void Awake()
{
_animator = GetComponent<Animator>();
_enemyController =
GetComponent<EnemyController>();
}
// EnemyControllerの状態更新後にAnimator Parameterを更新する
private void LateUpdate()
{
var state = _enemyController.State;
_animator.SetBool(
IsMovingId,
state == EnemyState.Move);
_animator.SetBool(
IsAttackingId,
state == EnemyState.Attack);
_animator.SetBool(
IsDamagedId,
state == EnemyState.Damage);
_animator.SetBool(
IsDeadId,
state == EnemyState.Dead);
}
}LateUpdate()でParameterを更新することで、同じフレームのEnemyController.Update()で決まった状態を読み取ります。EnemyへAnimatorとEnemyAnimatorを追加し、作成したAnimator Controllerを設定します。Play ModeでEnemyController.stateとAnimatorウィンドウの現在ステートが対応することを確認してください。
EnemyStateとPlayerStateを共通化するべきか
EnemyとPlayerには、停止、移動、攻撃、被弾、死亡など、似た名前の状態があります。ただし、状態名が同じであることと、その状態が同じ意味を持つことは別です。
PlayerのMoveは入力を受け付けて移動する状態ですが、EnemyのMoveは攻撃対象を追跡する状態です。どちらも移動していても、状態へ入る条件、終了する条件、実行する処理は異なります。
| 判断する項目 | 共通化しやすい場合 | 個別にした方がよい場合 |
|---|---|---|
| 状態の意味 | PlayerとEnemyで同じ意味を持つ | 同じ名前でも目的が異なる |
| 遷移条件 | 同じイベントと条件で遷移する | 入力、AI判断など異なる条件で遷移する |
| 状態中の処理 | 同じ行動を許可または禁止する | 片方だけに固有の処理がある |
| 今後の追加 | 同じ状態が同じ時期に増える | PlayerとEnemyが別方向へ拡張される |
| 利用する側 | 共通処理が同じ状態を必要とする | 各Controllerの内部だけで利用する |
EnemyStateとPlayerStateを個別に作る場合
敵とプレイヤーは別の存在です。表面上の状態が同じに見えても、ステートはそれぞれの役割に合わせて定義するのが自然です。今回はPlayerStateとEnemyStateを個別に定義します。
状態型を分けても、CharacterAttackやHitReactionは共通部品として利用できます。共通部品はIsAttackingやIsReactingなど、その部品自身が管理する事実だけを公開しますので、敵でもプレイヤーでも同じように利用できます。
ステートの取り扱いを個別にすることで、意識しなければならないのはステートマシンの実装だけです。
Playerに被弾リアクションとPlayerStateを追加する
基本実習では、HitReactionと状態管理をEnemyだけに実装しました。発展課題ではPlayerにも同じHitReactionを追加し、その結果をPlayer固有のPlayerStateへ反映します。
- Playerへ
HitReactionを追加します Character AttackにPlayerのCharacterAttackを設定しますTarget RendererにPlayerのRendererを設定しますPlayerState.csをAssets/Scriptsへ追加します- 基本版
PlayerController.csを発展版で上書きします
EnemyとPlayerは同じHitReactionを利用しますが、状態へ入る条件と今後の拡張方向が異なるため、状態型は個別に定義します。
cs
// Playerが現在行っている振る舞い
public enum PlayerState
{
Idle,
Move,
Attack,
Damage,
Dead
}死亡はDamageReceiver.Diedから通知を受け、入力処理とは独立してDeadへ遷移します。被弾と攻撃状態は優先して確認し、それ以外では入力に応じてIdleとMoveを切り替えます。
cs
using UnityEngine;
using UnityEngine.Serialization;
// 入力に応じたPlayerの行動と状態遷移を管理する
[RequireComponent(typeof(DamageReceiver))]
[RequireComponent(typeof(HitReaction))]
public class PlayerController : MonoBehaviour
{
[Tooltip("Playerの現在の状態")]
[SerializeField] private PlayerState state = PlayerState.Idle;
[Tooltip("Playerの移動処理")]
[SerializeField] private PlayerMove playerMove;
[Tooltip("カメラの回転処理")]
[SerializeField] private CameraMove cameraMove;
[FormerlySerializedAs("playerAttack")]
[Tooltip("Playerの攻撃処理")]
[SerializeField] private CharacterAttack characterAttack;
private InputSystem_Actions _inputActions;
private DamageReceiver _damageReceiver;
private HitReaction _hitReaction;
private bool _controlEnabled = true;
public PlayerState State => state;
// 入力と状態判定に必要なオブジェクトを準備する
private void Awake()
{
_inputActions = new InputSystem_Actions();
_damageReceiver = GetComponent<DamageReceiver>();
_hitReaction = GetComponent<HitReaction>();
}
// Player用Input Actionを有効にする
private void OnEnable()
{
_inputActions.Player.Enable();
_damageReceiver.Died += OnDied;
}
// Player用Input Actionを無効にする
private void OnDisable()
{
_damageReceiver.Died -= OnDied;
_inputActions.Player.Disable();
}
// ゲーム状態に応じて入力と進行中の攻撃だけを停止・再開する
public void SetControlEnabled(bool controlEnabled)
{
_controlEnabled = controlEnabled;
if (controlEnabled)
return;
characterAttack.CancelAttack();
if (state != PlayerState.Dead)
SetState(PlayerState.Idle);
}
// 入力と優先度に従って状態を更新し、現在可能な行動を実行する
private void Update()
{
if (state == PlayerState.Dead ||
!_controlEnabled)
return;
var moveInput =
_inputActions.Player.Move.ReadValue<Vector2>();
var lookInput =
_inputActions.Player.Look.ReadValue<Vector2>();
cameraMove.Look(lookInput);
if (_hitReaction.IsReacting)
{
SetState(PlayerState.Damage);
return;
}
if (characterAttack.IsAttacking)
{
SetState(PlayerState.Attack);
return;
}
if (_inputActions.Player.Attack.WasPressedThisFrame())
{
characterAttack.Attack();
if (characterAttack.IsAttacking)
{
SetState(PlayerState.Attack);
return;
}
}
var forward = cameraMove.PlanarForward;
var right = cameraMove.PlanarRight;
var direction =
forward * moveInput.y + right * moveInput.x;
direction =
Vector3.ClampMagnitude(direction, 1f);
playerMove.Move(direction);
SetState(
direction == Vector3.zero
? PlayerState.Idle
: PlayerState.Move);
}
// 死亡通知を受けた時点で、入力処理とは独立して死亡状態へ遷移する
private void OnDied()
{
characterAttack.CancelAttack();
SetState(PlayerState.Dead);
}
// 現在の状態を指定された状態へ変更する
private void SetState(PlayerState nextState)
{
if (state == nextState)
return;
state = nextState;
}
}この発展版ではAttackを排他的な状態として扱うため、攻撃中は移動入力を処理しません。移動しながら攻撃できる設計にする場合は、移動状態と行動状態を分ける方法も検討しましょう。
SetControlEnabled(false)は入力処理だけを停止し、PlayerController自体は無効にしません。そのためゲーム終了時にも死亡状態とAnimator Parameterを更新できます。死亡状態はDiedイベントだけで確定し、コントロールの有効・無効から死亡を判断しません。
PlayerStateをAnimatorへ渡す
PlayerAnimatorもゲーム状態を変更せず、PlayerController.StateをBool Parameterへ変換します。
cs
using UnityEngine;
// PlayerStateをAnimator Parameterへ反映する
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(PlayerController))]
public class PlayerAnimator : MonoBehaviour
{
private static readonly int IsMovingId =
Animator.StringToHash("IsMoving");
private static readonly int IsAttackingId =
Animator.StringToHash("IsAttacking");
private static readonly int IsDamagedId =
Animator.StringToHash("IsDamaged");
private static readonly int IsDeadId =
Animator.StringToHash("IsDead");
private Animator _animator;
private PlayerController _playerController;
// 必要なコンポーネントを取得する
private void Awake()
{
_animator = GetComponent<Animator>();
_playerController =
GetComponent<PlayerController>();
}
// PlayerControllerの状態更新後にAnimator Parameterを更新する
private void LateUpdate()
{
var state = _playerController.State;
_animator.SetBool(
IsMovingId,
state == PlayerState.Move);
_animator.SetBool(
IsAttackingId,
state == PlayerState.Attack);
_animator.SetBool(
IsDamagedId,
state == PlayerState.Damage);
_animator.SetBool(
IsDeadId,
state == PlayerState.Dead);
}
}PlayerAnimatorもLateUpdate()で、同じフレームに決まったPlayerStateをAnimatorへ反映します。PlayerへAnimatorとPlayerAnimatorを追加し、Animator Controllerを設定します。これで、HitReaction.IsReactingから決定したDamage状態が、Damage Clipへ一方向に伝わります。
PlayerStateとAnimation Clipを確認する
| 操作 | PlayerState | Animation Clip |
|---|---|---|
| 入力しない | Idle | Idle |
| 移動入力を行う | Move | Move |
| 攻撃入力を行う | Attack | Attack |
| 攻撃を受ける | Damage | Damage |
| HPが0になる | Dead | Dead |

ここでは攻撃中は移動入力を受け付けないようにしています。基本版では攻撃中も移動できたため、状態を排他的にしたことによる操作感の違いを確かめてください。
発展の完成条件
- EnemyStateに応じてEnemyのAnimation Clipが切り替わる
- Playerの被弾時に攻撃が中断され、色変更とのけぞりが発生する
- Playerの状態が
Idle、Move、Attack、Damage、Deadへ変化する - PlayerStateに応じてPlayerのAnimation Clipが切り替わる
- Animatorからゲーム状態、攻撃判定、HPを変更していない
- Playerの基本版と発展版で、攻撃中の移動可否が変わることを説明できる
動作しない場合のチェックリスト
Animatorが切り替わらない
- Animator Controllerに4つのBool Parameterがあるか確認します
- Animatorに正しいAnimator Controllerが設定されているか確認します
- Parameter名の大文字と小文字がコードと一致しているか確認します
Has Exit Timeが無効になっているか確認します- Animatorの
Apply Root Motionが無効になっているか確認します - 各ステートへAnimation Clipが設定されているか確認します
Enemyだけ切り替わらない
- Enemyに
Animator、EnemyController、EnemyAnimatorが追加されているか確認します EnemyController.stateがPlay Mode中に変化するか確認します- Dead Clipが途中で消える場合は、
Enemy.destroyDelayをClipより長くします
Playerだけ切り替わらない
- 基本版
PlayerController.csを発展版で上書きしたか確認します - Playerに
HitReaction、Animator、PlayerController、PlayerAnimatorが追加されているか確認します HitReactionのCharacter AttackとTarget Rendererが設定されているか確認しますPlayerController.stateがPlay Mode中に変化するか確認します
まとめ
- Animatorはゲーム状態を保存せず、State を受け取るだけの表示用ステートマシンとして利用する
- EnemyStateをAnimatorへ接続してから、PlayerのHitReaction、PlayerState、PlayerAnimatorを追加した
- 状態名だけでなく、意味と遷移条件から共通化の可否を判断した
- Animator ControllerではParameter、遷移条件、Clipの役割を分けて設定した