Skip to content

発展 AnimatorとPlayerState

基本実習: 第06回 Enemyの状態制御とゲームフィール / 次回: 第07回 アイテムとオブジェクトプール

今回の発展内容

Enemyの攻撃と被弾Animationを確認しているゲーム画面

  • 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 ControllerClipの切り替えとブレンド攻撃判定、HP管理、AI判断

Animation ClipとAnimator Controllerの準備

Clip素材がない場合の簡易表現
Idle子Objectを静止させる
Move子Objectを上下へ小さく動かす
Attack武器または子Objectを短く回転させる
Damage子Objectを短時間だけ後方へ傾ける
Dead子Objectを横へ倒す

コードが移動させるルートObjectには、位置と回転のAnimation Keyを設定しません。AnimatorのApply Root Motionも無効にし、ゲーム上の移動はPlayerMoveEnemyControllerへ任せます。EnemyのDead ClipはEnemy.destroyDelayより短くするか、Clipを最後まで再生できるようにdestroyDelayを長くします。

Animator Controllerを作成する

Animator Controllerの遷移設定例

EnemyとPlayerで使用するAnimation Clipが異なる場合は、それぞれにAnimator Controllerを作成します。Parameter名と遷移構造は共通にできますが、設定するClipはキャラクターごとに分けることをおすすめします。

  1. ProjectウィンドウでEnemy用のAnimator Controllerを作成します
  2. IdleMoveAttackDamageDeadのステートを作ります
  3. IdleをDefault Stateにします
  4. IsMovingIsAttackingIsDamagedIsDeadのBool Parameterを作ります
  5. 各ステートへ対応するAnimation Clipを設定します

Idleは4つのBoolがすべてfalseの状態として扱います。

AnimatorControllerとパラメーター

IdleからMoveへの遷移条件としてIsMovingを設定した例 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);

遷移条件を設定する

遷移条件
IdleMoveIsMoving == true
MoveIdleIsMoving == false
Any State → AttackIsAttacking == true
AttackIdleIsAttacking == false
Any State → DamageIsDamaged == true
DamageIdleIsDamaged == false
Any State → DeadIsDead == true

ゲーム状態の変化をすぐ見た目へ反映するため、これらの遷移ではHas Exit Timeを無効にします。Transition Durationは0から0.1秒程度で確認します。Any Stateから作った遷移ではCan Transition To Selfも無効にし、同じステートを繰り返し開始しないようにします。Deadからほかのステートへ戻る遷移は作りません。

INFO

PlayerにもAnimationを設定する場合は、同じ構造のPlayer用Animator Controllerを作るところから始めてみましょう。

EnemyStateをAnimatorへ渡す

EnemyAnimatorEnemyController.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へAnimatorEnemyAnimatorを追加し、作成したAnimator Controllerを設定します。Play ModeでEnemyController.stateとAnimatorウィンドウの現在ステートが対応することを確認してください。

EnemyStateとPlayerStateを共通化するべきか

EnemyとPlayerには、停止、移動、攻撃、被弾、死亡など、似た名前の状態があります。ただし、状態名が同じであることと、その状態が同じ意味を持つことは別です。

PlayerのMoveは入力を受け付けて移動する状態ですが、EnemyのMoveは攻撃対象を追跡する状態です。どちらも移動していても、状態へ入る条件、終了する条件、実行する処理は異なります。

判断する項目共通化しやすい場合個別にした方がよい場合
状態の意味PlayerとEnemyで同じ意味を持つ同じ名前でも目的が異なる
遷移条件同じイベントと条件で遷移する入力、AI判断など異なる条件で遷移する
状態中の処理同じ行動を許可または禁止する片方だけに固有の処理がある
今後の追加同じ状態が同じ時期に増えるPlayerとEnemyが別方向へ拡張される
利用する側共通処理が同じ状態を必要とする各Controllerの内部だけで利用する

EnemyStateとPlayerStateを個別に作る場合

敵とプレイヤーは別の存在です。表面上の状態が同じに見えても、ステートはそれぞれの役割に合わせて定義するのが自然です。今回はPlayerStateEnemyStateを個別に定義します。

状態型を分けても、CharacterAttackHitReactionは共通部品として利用できます。共通部品はIsAttackingIsReactingなど、その部品自身が管理する事実だけを公開しますので、敵でもプレイヤーでも同じように利用できます。

ステートの取り扱いを個別にすることで、意識しなければならないのはステートマシンの実装だけです。

Playerに被弾リアクションとPlayerStateを追加する

基本実習では、HitReactionと状態管理をEnemyだけに実装しました。発展課題ではPlayerにも同じHitReactionを追加し、その結果をPlayer固有のPlayerStateへ反映します。

  1. PlayerへHitReactionを追加します
  2. Character AttackにPlayerのCharacterAttackを設定します
  3. Target RendererにPlayerのRendererを設定します
  4. PlayerState.csAssets/Scriptsへ追加します
  5. 基本版PlayerController.csを発展版で上書きします

EnemyとPlayerは同じHitReactionを利用しますが、状態へ入る条件と今後の拡張方向が異なるため、状態型は個別に定義します。

cs
// Playerが現在行っている振る舞い
public enum PlayerState
{
    Idle,
    Move,
    Attack,
    Damage,
    Dead
}

死亡はDamageReceiver.Diedから通知を受け、入力処理とは独立してDeadへ遷移します。被弾と攻撃状態は優先して確認し、それ以外では入力に応じてIdleMoveを切り替えます。

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

PlayerAnimatorLateUpdate()で、同じフレームに決まったPlayerStateをAnimatorへ反映します。PlayerへAnimatorPlayerAnimatorを追加し、Animator Controllerを設定します。これで、HitReaction.IsReactingから決定したDamage状態が、Damage Clipへ一方向に伝わります。

PlayerStateとAnimation Clipを確認する

操作PlayerStateAnimation Clip
入力しないIdleIdle
移動入力を行うMoveMove
攻撃入力を行うAttackAttack
攻撃を受けるDamageDamage
HPが0になるDeadDead

PlayerとEnemyのAnimation Clipを確認しているゲーム画面

ここでは攻撃中は移動入力を受け付けないようにしています。基本版では攻撃中も移動できたため、状態を排他的にしたことによる操作感の違いを確かめてください。

発展の完成条件

  • EnemyStateに応じてEnemyのAnimation Clipが切り替わる
  • Playerの被弾時に攻撃が中断され、色変更とのけぞりが発生する
  • Playerの状態がIdleMoveAttackDamageDeadへ変化する
  • 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にAnimatorEnemyControllerEnemyAnimatorが追加されているか確認します
  • EnemyController.stateがPlay Mode中に変化するか確認します
  • Dead Clipが途中で消える場合は、Enemy.destroyDelayをClipより長くします

Playerだけ切り替わらない

  • 基本版PlayerController.csを発展版で上書きしたか確認します
  • PlayerにHitReactionAnimatorPlayerControllerPlayerAnimatorが追加されているか確認します
  • HitReactionCharacter AttackTarget Rendererが設定されているか確認します
  • PlayerController.stateがPlay Mode中に変化するか確認します

まとめ

  • Animatorはゲーム状態を保存せず、State を受け取るだけの表示用ステートマシンとして利用する
  • EnemyStateをAnimatorへ接続してから、PlayerのHitReaction、PlayerState、PlayerAnimatorを追加した
  • 状態名だけでなく、意味と遷移条件から共通化の可否を判断した
  • Animator ControllerではParameter、遷移条件、Clipの役割を分けて設定した