😎

無限に増殖?ステートマシン×C#でNPCの行動パターンを完全管理

2025/02/10に公開

無限に増殖?ステートマシン×C#でNPCの行動パターンを完全管理

ゲームに登場するNPC(ノンプレイヤーキャラクター)の多様な行動ロジックをどう整理するかは、開発規模が大きくなるほど悩ましい問題になってきます。NPCが待機しているのか、攻撃しているのか、警戒逃走を選んでいるのか――こうした行動パターンをスクリプト上で一括管理しようとすると、複雑な条件分岐に埋もれてしまいがちです。そこでおすすめしたいのが、**ステートマシン(State Machine)**という設計手法です。本記事では、C#でステートマシンを実装する際のポイントや、実際のコード例、アニメーションイベントとの連動、大規模RPGやMMOへの応用などを解説します。

Unity初心者でも最短5日で3D FPSが完成!今すぐ始める入門チュートリアルはこちら

https://zenn.dev/ryuryu_game/books/fd28de9d8e963a/viewer/0570af

ステートマシンとは何か

ステートマシンは、オブジェクトがとりうる状態(ステート)と、その状態間を移り変わる条件(遷移)を定義するアーキテクチャです。NPCの行動をそれぞれのステートに“切り分け”ておき、現在どのステートにいるのか、どの条件で次のステートへ移行するのかを管理します。

メリット

  • 複雑なif文の入れ子構造から脱却
  • デバッグやメンテナンスがしやすい
  • 挙動の追加・修正が容易

このアプローチは、RPGやMMOなどキャラクター数・行動パターンが多いプロジェクトほど威力を発揮します。

参考リンクと導入事例

  • ステートマシンの実装例(ImtStateMachine)

https://qiita.com/BelColo/items/a94c9ccc2d5174dc29a3
シンプルでハイパフォーマンス、サーバーサイドでも活用可能

  • Unityを使ったFSMのジェネリック実装例

https://elekibear.com/post/20211230_01
基本クラスを用意し、ステート処理を分割して汎用化

  • ステートパターンを利用した敵AI

https://zenn.dev/twugo/books/21cb3a6515e7b8/viewer/b48713
NPCのAI実装にも応用できるため、大規模RPGやMMOにも活かしやすい

基本のステートマシンクラス

以下は、C#でステートマシンを実装するうえでの簡易例です。ジェネリック型を使い、どんなキャラクターやコンテキスト(状況)でも使いまわせるベースクラスを想定します。

using UnityEngine;
using System.Collections.Generic;

// T: ステートを識別するためのEnumやintなど
// TContext: NPCや敵キャラクターなど、状態間で共有したいデータを持つクラス
public class StateMachine<T, TContext>
{
    private Dictionary<T, IState<TContext>> _states = new Dictionary<T, IState<TContext>>();
    private IState<TContext> _currentState;
    private TContext _context;

    public StateMachine(TContext context)
    {
        _context = context;
    }

    public void AddState(T key, IState<TContext> state)
    {
        _states[key] = state;
    }

    public void ChangeState(T key)
    {
        // 現在のステートのExitを呼ぶ
        if (_currentState != null)
        {
            _currentState.Exit(_context);
        }

        // 新しいステートに遷移
        if (_states.TryGetValue(key, out var nextState))
        {
            _currentState = nextState;
            _currentState.Enter(_context);
        }
        else
        {
            Debug.LogWarning($"State {key} not found in StateMachine.");
        }
    }

    public void Update()
    {
        if (_currentState != null)
        {
            _currentState.Update(_context);
        }
    }

    // 現在どのステートかを判定
    public T GetCurrentStateKey()
    {
        foreach (var pair in _states)
        {
            if (pair.Value == _currentState)
            {
                return pair.Key;
            }
        }
        return default;
    }
}

// ステート用インターフェース
public interface IState<TContext>
{
    void Enter(TContext context);
    void Update(TContext context);
    void Exit(TContext context);
}
  • AddState: ステートを登録する
  • ChangeState: 状態を切り替える
  • Update: 毎フレームやFixedUpdateで呼び出すなど運用方針はプロジェクト次第

このようにベースを作っておけば、特定のNPCの行動を管理する具体的なステートを定義していくだけで運用できます。

ステートの実装例

ここでは、**Idle(待機)Attack(攻撃)**の2ステートを持つNPCを想定しましょう。NPCの基本情報やパラメータを持つNpcContextクラスを用意しておきます。

using UnityEngine;

// NPCで共有する情報。体力や位置情報、ターゲットなど
public class NpcContext
{
    public float health = 100f;
    public Transform target;
    // NPCの移動速度や攻撃力など、必要なパラメータを追加
}

// 待機状態
public class IdleState : IState<NpcContext>
{
    public void Enter(NpcContext context)
    {
        Debug.Log("Enter IdleState");
    }

    public void Update(NpcContext context)
    {
        // 例: ターゲットが一定距離に入ったらAttackステートへ
        if (context.target != null)
        {
            float distance = Vector3.Distance(context.target.position, Vector3.zero); // 仮にNPCの座標をVector3.zeroとする
            if (distance < 5f)
            {
                // ステートマシンの ChangeState を呼びたいが、ここでは後述
            }
        }
    }

    public void Exit(NpcContext context)
    {
        Debug.Log("Exit IdleState");
    }
}

// 攻撃状態
public class AttackState : IState<NpcContext>
{
    public void Enter(NpcContext context)
    {
        Debug.Log("Enter AttackState");
        // 攻撃開始。アニメーション再生など
    }

    public void Update(NpcContext context)
    {
        // 攻撃中のロジック。クールダウンや再攻撃タイミング
        if (context.health < 20f)
        {
            // 体力が少ないのでIdleに戻るなど
        }
    }

    public void Exit(NpcContext context)
    {
        Debug.Log("Exit AttackState");
        // 攻撃アニメ終了処理など
    }
}

実際には状態間の切り替え(ChangeState) を呼ぶときは、NPCを管理するMonoBehaviourクラス(例: NpcController)などで実施します。下記のように、Update()メソッド内でステートマシンのUpdate()を呼び出すイメージです。

using UnityEngine;

public class NpcController : MonoBehaviour
{
    [SerializeField] private Transform player;
    
    private StateMachine<string, NpcContext> _stateMachine;
    private NpcContext _context;

    void Start()
    {
        // NpcContextの初期設定
        _context = new NpcContext
        {
            health = 100f,
            target = player // プレイヤーをターゲット
        };

        // ステートマシンを初期化
        _stateMachine = new StateMachine<string, NpcContext>(_context);

        // ステート登録
        _stateMachine.AddState("Idle", new IdleState());
        _stateMachine.AddState("Attack", new AttackState());

        // 初期ステートをIdleに設定
        _stateMachine.ChangeState("Idle");
    }

    void Update()
    {
        // ステートマシンを更新
        _stateMachine.Update();

        // サンプル: IdleStateからAttackStateへの遷移
        // IdleState内ではStateMachineに直接アクセスしない設計として、
        // ここで距離チェックを行い、遷移させるやり方もあり
        float dist = Vector3.Distance(player.position, transform.position);
        if(dist < 5f && _stateMachine.GetCurrentStateKey() == "Idle")
        {
            _stateMachine.ChangeState("Attack");
        }
        else if(dist >= 5f && _stateMachine.GetCurrentStateKey() == "Attack")
        {
            _stateMachine.ChangeState("Idle");
        }
    }
}

このように、状態を追加する場合はAddStateで登録し、どのタイミングで状態を変えるかUpdate内で距離やHPなどの条件をチェックしてChangeStateを呼ぶだけで済みます。大規模プロジェクトであっても、各ステートを独立したクラスにすることで、行動パターンの増殖に柔軟に対応できます。

アニメーションイベントとステート連動

NPCがステート遷移するタイミングを、アニメーションの再生フレームに合わせると、より自然な挙動が得られます。例えば、攻撃アニメーションの半ばで当たり判定を有効化し、アニメーション終了時に次のステートへ移行するといった流れです。

// 攻撃アニメーションで呼ばれるイベント
public void OnAttackAnimationHit()
{
    // ここで実際の攻撃判定を行う
}

public void OnAttackAnimationEnd()
{
    // 攻撃が終わったのでIdleに戻る
    _stateMachine.ChangeState("Idle");
}

UnityのAnimatorやAnimationClipにイベントを仕込み、そのイベントをキャラクターのスクリプトで受け取ってステートマシンの遷移を呼ぶようにすると、視覚とロジックがずれにくくなります。

大規模プロジェクトに活かす方法

1. ステート数が大量でも安心

RPGやMMOでは、敵やNPCが多種多様な行動をとる必要があります。ステートマシンを導入すると、新たなステートを追加するだけで個々の行動を拡張できるため、if文が密集して保守が難しくなる状況を避けられます。

2. サーバーサイド連携

MMOではサーバー側でもNPCの状態を管理する必要があります。ImtStateMachineのようにピュアC#で組まれたフレームワークであれば、クライアントとサーバーが同じロジックを共有する設計にしやすいです。

https://qiita.com/BelColo/items/a94c9ccc2d5174dc29a3

3. テストとデバッグがしやすい

NPCが現在どの状態にあるか、状態がいつ切り替わったかをログやUIで可視化することで、不具合が起きたときの原因特定が容易になります。ステート遷移そのものをテストコードで扱うこともできるため、大規模プロジェクトほどステートマシンの恩恵を受けやすいのです。

ステートマシン導入時の注意点

以下の表に、よくあるステートマシン運用時の注意点と対策を整理します。

注意点 詳細 対策
ステートの増えすぎ 不要なステートを濫用するとコード量が膨大に 大まかな行動カテゴリでまず分割し、細分化は慎重に検討
遷移条件が複雑化 分岐が多いと読みにくいロジックになる 条件処理をまとめるクラスを用意するなど、責務を分離
アニメーションシステムとの整合性 Animatorだけでステート管理している場合と競合しやすい どちらで何を管理するかを明確化。連携処理はイベント経由で
デバッグのしづらさ ステート切り替えが多発して追跡しにくい ステート遷移時のログ出力・ステートビジュアル化ツールの活用

まとめと今後のステップ

C#でステートマシンを導入すると、NPCの行動パターンを**「状態単位」で整理できる**ため、どれだけ行動が増えても構造が崩壊しにくいという利点があります。特に、大規模RPGやMMOでは数多くのNPCが複雑に動き回るため、ステートマシンの導入がプロジェクト全体の保守性と拡張性を高めます。

  • ImtStateMachineのようなライブラリ活用:シンプル・高性能
  • 独自クラスで構築:プロジェクト特化の柔軟なカスタマイズ
  • アニメーションイベントとの連動:演出とロジックの同期で違和感を除去
  • サーバーサイド対応:クライアントと共通のステートマシンを共有して整合性を保つ

より詳しくは、以下のリンクでも様々な実例やコードが紹介されています。

https://elekibear.com/post/20211230_01
https://zenn.dev/twugo/books/21cb3a6515e7b8/viewer/b48713

次のアクション:

  1. 小規模なサンプルNPCを用意し、ステート数を2~3に抑えたデモプロジェクトを作る
  2. デバッグやビジュアル化の仕組みを整え、ステート遷移を明確に把握する
  3. 必要に応じてアニメーションイベントとの連携を検討し、大規模RPGやMMOに対応できるか検証する

ステートマシンを活用すれば、NPCが“無限に増殖”しても混乱せず、整理された行動管理フレームワークを築けるはずです。ぜひ自分のプロジェクトに取り入れてみてください。ゲームの世界観をより豊かにし、快適な開発サイクルを実現できるでしょう。

この記事を読んでもっと実践したいと感じたあなたへ

Unity開発を効率よく進めるためには、実践的なスキルと仲間との交流が欠かせません。
そんな方におすすめのステップが、下記の3つです。

1. 有料教材「どこでもUnity教室」でゲーム制作を短期マスター

  • 5日でシンプルなFPS完成:初心者向けに要点を押さえたカリキュラム
  • C#や最新のInputSystem、FPS実装まで網羅:つまずきやすいポイントを先回りで解説
  • 購入特典:Discord招待+サンプルプロジェクトDLで、疑問や実装例を即確認

Unity初心者でも最短5日で3D FPSが完成!今すぐ始める入門チュートリアルはこちら

https://zenn.dev/ryuryu_game/books/fd28de9d8e963a/viewer/0570af

2. 無料コミュニティで、疑問をすぐに解消&モチベーションUP

  • 初心者~中級者までOK:学習進度に合わせて質問や情報共有
  • 質問サポートが充実:わからないことを仲間や講師に即相談
  • 学習仲間と切磋琢磨:一緒に学ぶから続けやすい

Discordサーバー参加はこちら

https://discord.gg/5FwuKCacNy

3. 実績豊富な“ゲーム開発所RYURYU”があなたをトータルサポート

  • コナラ総販売200件超:さまざまなUnity開発の依頼を対応
  • VR/AR/AIなど最新技術にも精通:幅広いノウハウを活かして開発支援
  • ゲームクリエイター甲子園や東京ゲームショウなど出展実績多数

ご相談・お問い合わせはこちら

https://coconala.com/users/1772507

Discussion