ゲーム開発におけるアクション状態管理について、私なりの考え

2026年3月20日
Daniel Luフルスタックエンジニア | コンテンツクリエイター

ゲーム開発でよくある「アクション状態の上書きと復帰の失敗」というバグをきっかけに、立ち上げ間もないチームでの実戦経験を交え、UnityでActive State Poolと優先度動態評価メカニズムを用いた、拡張性が高く疎結合なアクション状態マシンの構築方法を共有します。

カテゴリーテクノロジーUnityWeb 開発

業務の悩み:遭遇したアクション状態上書きバグ

最近、立ち上げ間もないチームに加わり、バトルシステムと表現レイヤーのロジック整理を手伝っていた際、非常に古典的で厄介なバグに遭遇しました。

具体的なシナリオはこうです: キャラクターに5秒間持続する「氷結」デバフが付与され、アクションが**[氷結](その場で凍りつき、震える演出)に切り替わります。その2秒後、キャラクターが敵から気絶効果のある重撃を受け、2秒間持続する[気絶]**アクション(頭を抱えてふらつく演出)がトリガーされました。

本当の問題は、気絶アクションが終わった後に起こりました: 2秒間の気絶演出が終わったとき、データ上のロジックではキャラクターにはまだ1秒間の「氷結」デバフが残っています。しかし実際には、アクションは**[氷結]状態に戻らず、そのままデフォルトの[待機(Idle)]**状態に落ちてしまいました。その結果、キャラクターは平然と待機モーションをしているのに、プレイヤーの操作を受け付けない(ロジック上はまだ凍っているため)という、見た目と内部ロジックの深刻な乖離が発生しました。

コードを調査したところ、原因は単純でした。チームが以前使っていた状態制御システムがトリガー式だったのです。気絶が発生したらAnimatorに強制的に気絶を再生させ、気絶が終わったらAnimatorを単純にデフォルト状態に戻すだけで、他に持続しているロジック効果を全く考慮していませんでした。

状態管理の普遍性と従来手法の限界

この問題はゲーム開発において極めて普遍的だと思います。複雑なBuffシステムや、複数の状態が重なるスキル効果が存在する限り、アクション状態の上書きと復帰の問題には必ず直面します。

初期段階のチームが早く成果を出すために採用しがちな、しかし私が実戦で反対している手法が以下の2つです:

リスクA:各システムがAnimator変数を直接操作する

Buffシステム、スキルシステム、ダメージシステムがそれぞれ直接 animator.SetBool("isFrozen", true) を呼び出す形です。システムが肥大化すると、どのコードがいつBoolをfalseに戻したのか把握できなくなり、ステートマシンが混乱してデバッグが極めて困難になります。

リスクB:Unity Mecanimの遷移網の濫用

Animatorウィンドウは遷移(Transition)が少ないうちは直感的です。しかし、そこに Speed > 0isFrozen == true といった大量の条件を詰め込むと、最終的にステートマシンは複雑な「蜘蛛の巣」になります。新しい状態を一つ追加するたびに無数の線を繋ぎ直す必要があり、保守コストは指数関数的に増大します。

私の解決策:Active State Poolと優先度動態評価

状態の上書き、復帰の失敗、そして密結合の問題を解決するために、私はチームにActive State Pool(アクティブ状態プール)と優先度動態評価という考え方を導入しました。

核心となるアイデアは一点のみです:「状態のリクエスト」と「状態の表現」を完全に切り離すことです。

ロジック層(Buffやスキルシステム)は、自分が必要なアクション状態を**登録(Register)および解除(Unregister)**することだけに責任を持ちます。そして、統合された AnimationStateManager が、現在のフレームでAnimatorにどの指令を出すべきかを最終決定します。

核心メカニズムのフローチャート:

Loading diagram...

このアーキテクチャでは、優先度100の「気絶」が終了して登録解除されると、マネージャーが再びプールを評価し、残っている最高優先度が50の「氷結」であることを発見します。すると自動的にAnimatorに指令を出し、**[氷結]**アクションに戻します。以前のバグは自然に解消されます。

Unityでの実装:Animatorを純粋なものにする

このアーキテクチャを適用すると、UnityのAnimatorの使い方も変える必要があります。複雑な遷移線やTrigger/Bool条件を捨て、Animatorを純粋なアクションプレイヤーへと退化させるのです。

Animator Controllerには、必要なStateを並べるだけで、遷移線を繋ぐ必要はありません(または全てAnyStateから条件なしで繋ぐ)。コード側で Animator.CrossFade()Animator.Play() を使い、直接ステートを切り替えます。

マネージャーの疑似コード例:

// 1. アクション状態のリクエスト構造体
public class AnimStateRequest
{
    public string StateName; // Animator内のステート名
    public int Priority;     // 優先度
    public object Source;    // ソース(解除時の検証用、特定のBuffインスタンスなど)
}

// 2. 統合マネージャー
public class AnimationStateManager : MonoBehaviour
{
    private Animator _animator;
    private List<AnimStateRequest> _activeStates = new List<AnimStateRequest>();
    private string _currentPlayingState;

    // 状態を登録
    public void AddStateRequest(string stateName, int priority, object source)
    {
        _activeStates.Add(new AnimStateRequest { StateName = stateName, Priority = priority, Source = source });
        EvaluateHighestPriorityState();
    }

    // 状態を解除
    public void RemoveStateRequest(object source)
    {
        _activeStates.RemoveAll(req => req.Source == source);
        EvaluateHighestPriorityState();
    }

    // 現在の最高優先度状態を評価
    private void EvaluateHighestPriorityState()
    {
        if (_activeStates.Count == 0)
        {
            PlayAnimation("Idle"); // デフォルト状態
            return;
        }

        // 最高優先度のリクエストを探す
        _activeStates.Sort((a, b) => b.Priority.CompareTo(a.Priority));
        string targetState = _activeStates[0].StateName;

        if (_currentPlayingState != targetState)
        {
            PlayAnimation(targetState);
        }
    }

    // Unity Animatorを駆動
    private void PlayAnimation(string stateName)
    {
        _currentPlayingState = stateName;
        // CrossFadeで滑らかに遷移(0.1fは遷移時間)
        _animator.CrossFade(stateName, 0.1f); 
    }
}

アーキテクチャの拡張:より複雑なアクションシステムへの構想

このアクティブプールと優先度評価の仕組みは、ゲームが複雑化しても高い拡張性を提供します。実戦では以下のような拡張も考えています:

  1. 同優先度の衝突解決:もし優先度50のリクエストが同時に存在する場合、「後から入った方を優先」や「残り時間が長い方を優先」といったロジックを追加できます。
  2. 部位マスク(Avatar Mask)の独立管理:上半身で射撃し、下半身で被弾したり走ったりする場合、2つの独立したManager(UpperBodyPoolとLowerBodyPool)を立て、それぞれ評価結果を CrossFade の異なるLayerインデックスに渡すことで完璧にデカップリングできます。
  3. Playable APIの活用:厳密なフレーム管理が必要なアクションでは、Animatorの更新遅延が問題になることがあります。その場合、このマネージャーをUnity低レイヤーのPlayable Graphと結合させ、C#側でAnimationClipを直接ミキシングすることで、アニメーションの主導権を完全に掌握できます。

以上が、実際のプロジェクト開発におけるアクション状態管理についての私の考えとまとめです。この手法は現在のチームの悩みを解決しましたが、決して全てのケースにおける「銀弾」ではありません。私の見解には偏りがあるかもしれませんので、もしより良い方法があったり、実装上の懸念点にお気づきでしたら、ぜひ交流させてください。ご指摘をお待ちしております。


  • 本文は iknowabit チームによるオリジナルです。テクニカルサポート:UnityエンジンとC#高度アーキテクチャパターンの探求。*