
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
こんにちクマ〜〜〜

マイケル
今日はUnityで 有限ステートマシン(FSM)
について実装していくよ!
について実装していくよ!

エレキベア
ステートマシンって何クマ?

マイケル
有限ステートマシンはゲームAIでよく使われていて、
下記のように ステートを遷移させることでふるまいを表現するモデル のことなんだ!
下記のように ステートを遷移させることでふるまいを表現するモデル のことなんだ!
有限ステートマシン(Finite State Machine)とは
- 有限個のステートを持ち、入力を処理することで別のステートへ遷移する。
- 同時にとることが出来るステートは1つだけ。
- 有限オートマトン、有限状態機械とも呼ばれる。

マイケル
例えばUnityのAnimatorもステートマシンに含まれるよ!

エレキベア
なるほどクマ
ステートを制御するためのマシンなのクマね(?)
ステートを制御するためのマシンなのクマね(?)

マイケル
今回はこれをスクリプトで実装した例を紹介します!
実装したサンプルはGitHubにあげているので、よければ参考にお使いください!
実装したサンプルはGitHubにあげているので、よければ参考にお使いください!
GitHub – unity-state-machine-sample

マイケル
それでは早速やっていこう!
参考書籍

マイケル
書籍は主に下記を参考にさせていただきました!
ステートマシンは序章にすぎず、実例を交えて様々なゲームAIの実装が紹介されているので、
気になった方は読んでみてください!
ステートマシンは序章にすぎず、実例を交えて様々なゲームAIの実装が紹介されているので、
気になった方は読んでみてください!

マイケル
そしてステート遷移に関する実装はゲーム開発の様々な書籍で紹介されていて、
例えば下記の書籍内でも紹介されていました。
気になった方は是非いろいろ漁ってみてください!
例えば下記の書籍内でも紹介されていました。
気になった方は是非いろいろ漁ってみてください!
Game Programming Patterns ソフトウェア開発の問題解決メニュー impress top gearシリーズ

マイケル
どれも参考になりますが、全てC++で実装されている点には注意です。

エレキベア
ステート遷移は定番なのクマね
有限ステートマシンの実装

マイケル
それでは実装に移りたいと思います。
今回は下記のようなカニの動きを実現させます!
今回は下記のようなカニの動きを実現させます!

エレキベア
なんか見てると面白いクマね

マイケル
常に働き続ける社畜カニになります・・・
プロジェクト構成

マイケル
ゲームオブジェクトの構成は下記の通り!
各サンプルごとにシーンを用意していますが、ステージはどのシーンも共通で使用していて、
オブジェクトはStageManagerクラスで管理しています。
各サンプルごとにシーンを用意していますが、ステージはどのシーンも共通で使用していて、
オブジェクトはStageManagerクラスで管理しています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// ステージ管理クラス
/// </summary>
public class StageManager : MonoBehaviour
{
[SerializeField] public Transform homeTransform; // 家の位置情報
[SerializeField] public Transform seaTransform; // 海の位置情報
[SerializeField] public GameObject fishPrefab; // 魚Prefab
}

マイケル
サンプルは下記4つを用意していますが、実現する動きは全て同じになります。
また、凝れば凝るほど複雑さも増すため、どの方法がいいとも一概には言えません。
そのためケースバイケースで選ぶようにしてください。
また、凝れば凝るほど複雑さも増すため、どの方法がいいとも一概には言えません。
そのためケースバイケースで選ぶようにしてください。
[実装方法サンプル]
Sample01 enumを使用した実装
Sample02 classを使用した実装
Sample03 Sample02をステートマシンとして切り出したもの
Sample04 ステートマシンにステート状態の遷移情報を定義したもの

エレキベア
どれも一長一短あるクマね

マイケル
基本的には、規模が大きくなったり複雑なステート遷移をする場合には
ステートマシンとしてクラスを切り出すのがいいと思います。
ステートマシンとしてクラスを切り出すのがいいと思います。
Sample01: enumを使用した実装

マイケル
サンプル1つ目は、最もシンプルなステート遷移の実装です。
各ステートをenum型で定義してswitch文で切り替えるといった方法になります。
各ステートをenum型で定義してswitch文で切り替えるといった方法になります。
/// <summary>
/// ステート
/// </summary>
private enum StateType
{
MoveSea, // 海へ移動
Hunting, // 魚採取
MoveHome, // 家へ移動
Eating, // 食事
}
private StateType _state = StateType.MoveSea; // 現在のステート
private StateType _nextState = StateType.MoveSea; // 次のステート
private void Start()
{
// 最初のステートを開始する
MoveSeaStart();
}
private void Update()
{
// 現在のステートのUpdateを呼び出す
switch (_state)
{
case StateType.MoveSea:
MoveSeaUpdate();
break;
case StateType.Hunting:
HuntingUpdate();
break;
case StateType.MoveHome:
MoveHomeUpdate();
break;
case StateType.Eating:
EatingUpdate();
break;
}
// ステートが切り替わったら
if (_state != _nextState)
{
// 終了処理を呼び出して
switch (_state)
{
case StateType.MoveSea:
MoveSeaEnd();
break;
case StateType.Hunting:
HuntingEnd();
break;
case StateType.MoveHome:
MoveHomeEnd();
break;
case StateType.Eating:
EatingEnd();
break;
}
// 次のステートに遷移する
_state = _nextState;
switch (_state)
{
case StateType.MoveSea:
MoveSeaStart();
break;
case StateType.Hunting:
HuntingStart();
break;
case StateType.MoveHome:
MoveHomeStart();
break;
case StateType.Eating:
EatingStart();
break;
}
}
}
/// <summary>
/// 遷移先のステート設定
/// </summary>
/// <param name="nextState">次のステート</param>
private void ChangeState(StateType nextState)
{
_nextState = nextState;
}

マイケル
各ステートの処理については
以下のように関数ごとに分けたものになります。
以下のように関数ごとに分けたものになります。
// 各ステート処理
// ----- move sea -----
private void MoveSeaStart()
{
Debug.Log("start move sea");
}
private void MoveSeaUpdate()
{
var enemyPosition = transform.position;
var targetPosition = stageManager.seaTransform.position;
// 海へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
ChangeState(StateType.Hunting);
return;
}
// 海へ向かう
transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
transform.LookAt(targetPosition);
}
private void MoveSeaEnd()
{
Debug.Log("end move sea");
}
// ----- hunting -----
private void HuntingStart()
{
Debug.Log("start hunting");
// 採取スタート
_isFinishHunting = false;
StartCoroutine(HuntCoroutine());
}
private void HuntingUpdate()
{
// 採取が完了したら次のステートへ
if (_isFinishHunting)
{
ChangeState(StateType.MoveHome);
}
}
private void HuntingEnd()
{
Debug.Log("end hunting");
}
// 採取が完了しているか?
private bool _isFinishHunting;
// 採取コルーチン
private IEnumerator HuntCoroutine()
{
// 狩猟中、数秒待機
yield return new WaitForSeconds(2.0f);
// 魚取得
Instantiate(stageManager.fishPrefab, transform);
// 狩猟完了
_isFinishHunting = true;
}
// ----- move home -----
private void MoveHomeStart()
{
Debug.Log("start move home");
}
private void MoveHomeUpdate()
{
var enemyPosition = transform.position;
var targetPosition = stageManager.homeTransform.position;
// 家へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
ChangeState(StateType.Eating);
return;
}
// 家へ向かう
transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
transform.LookAt(targetPosition);
}
private void MoveHomeEnd()
{
Debug.Log("end move home");
}
// ----- eating -----
private void EatingStart()
{
Debug.Log("start eating");
// 食事開始
_isFinishEating = false;
StartCoroutine(EatCoroutine());
}
private void EatingUpdate()
{
// くるくる周る
transform.Rotate(Vector3.up * 500.0f * Time.deltaTime);
// 食事が完了したら次のステートへ
if (_isFinishEating)
{
ChangeState(StateType.MoveSea);
}
}
private void EatingEnd()
{
Debug.Log("end eating");
}
// 食事が完了しているか?
private bool _isFinishEating;
// 食事コルーチン
private IEnumerator EatCoroutine()
{
// 食事中、数秒待機
yield return new WaitForSeconds(5.0f);
// 子オブジェクト(魚)を破棄
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
// 食事完了
_isFinishEating = true;
}

マイケル
簡単な実装できるのが魅力ですが、ステートや条件が追加されるほどあっという間にスパゲティ状態になってしまうというデメリットがあります。

エレキベア
今回の例でも読むのが少し辛いクマね・・・
Sample02: classを使用した実装

マイケル
そこで各ステート処理をクラスごとに分けた場合が次の実装になります。
ステートの基底クラスを継承して定義するようにしています。
ステートの基底クラスを継承して定義するようにしています。
// 各ステート処理
// --- state基底クラス -----
private class StateBase
{
public virtual void OnStart(Enemy owner) { }
public virtual void OnUpdate(Enemy owner) { }
public virtual void OnEnd(Enemy owner) { }
}
// ----- move sea -----
private class StateMoveSea : StateBase
{
public override void OnStart(Enemy owner)
{
Debug.Log("start move sea");
}
public override void OnUpdate(Enemy owner)
{
var enemyPosition = owner.transform.position;
var targetPosition = owner.stageManager.seaTransform.position;
// 海へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
owner.ChangeState(StateType.Hunting);
return;
}
// 海へ向かう
owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
owner.transform.LookAt(targetPosition);
}
public override void OnEnd(Enemy owner)
{
Debug.Log("end move sea");
}
}
// ----- hunting -----
private class StateHunting : StateBase
{
public override void OnStart(Enemy owner)
{
Debug.Log("start hunting");
// 採取スタート
_isFinishHunting = false;
MonoBehaviorHandler.StartStaticCoroutine(HuntCoroutine(owner));
}
public override void OnUpdate(Enemy owner)
{
// 採取が完了したら次のステートへ
if (_isFinishHunting)
{
owner.ChangeState(StateType.MoveHome);
}
}
public override void OnEnd(Enemy owner)
{
Debug.Log("end hunting");
}
// 採取が完了しているか?
private bool _isFinishHunting;
// 採取コルーチン
private IEnumerator HuntCoroutine(Enemy owner)
{
// 狩猟中、数秒待機
yield return new WaitForSeconds(2.0f);
// 魚取得
Instantiate(owner.stageManager.fishPrefab, owner.transform);
// 狩猟完了
_isFinishHunting = true;
}
}
// ----- move home -----
private class StateMoveHome : StateBase
{
public override void OnStart(Enemy owner)
{
Debug.Log("start move home");
}
public override void OnUpdate(Enemy owner)
{
var enemyPosition = owner.transform.position;
var targetPosition = owner.stageManager.homeTransform.position;
// 家へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
owner.ChangeState(StateType.Eating);
return;
}
// 家へ向かう
owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
owner.transform.LookAt(targetPosition);
}
public override void OnEnd(Enemy owner)
{
Debug.Log("end move home");
}
}
// ----- eating -----
private class StateEating : StateBase
{
public override void OnStart(Enemy owner)
{
Debug.Log("start eating");
// 食事開始
_isFinishEating = false;
MonoBehaviorHandler.StartStaticCoroutine(EatCoroutine(owner));
}
public override void OnUpdate(Enemy owner)
{
// くるくる周る
owner.transform.Rotate(Vector3.up * 500.0f * Time.deltaTime);
// 食事が完了したら次のステートへ
if (_isFinishEating)
{
owner.ChangeState(StateType.MoveSea);
}
}
public override void OnEnd(Enemy owner)
{
Debug.Log("end eating");
}
// 食事が完了しているか?
private bool _isFinishEating;
// 食事コルーチン
private IEnumerator EatCoroutine(Enemy owner)
{
// 食事中、数秒待機
yield return new WaitForSeconds(5.0f);
// 子オブジェクト(魚)を破棄
foreach (Transform child in owner.transform)
{
Destroy(child.gameObject);
}
// 食事完了
_isFinishEating = true;
}
}

マイケル
クラスを分けることによってまとまりが分かりやすくなり、
下記のように呼び出し側もシンプルにすることができます。
下記のように呼び出し側もシンプルにすることができます。
/// <summary>
/// ステート
/// </summary>
private readonly StateMoveSea _stateMoveSea = new StateMoveSea();
private readonly StateHunting _stateHunting = new StateHunting();
private readonly StateMoveHome _stateMoveHome = new StateMoveHome();
private readonly StateEating _stateEating = new StateEating();
private enum StateType
{
MoveSea, // 海へ移動
Hunting, // 魚採取
MoveHome, // 家へ移動
Eating, // 食事
}
private StateBase GetState(StateType state)
{
switch (state)
{
case StateType.MoveSea:
return _stateMoveSea;
case StateType.Hunting:
return _stateHunting;
case StateType.MoveHome:
return _stateMoveHome;
case StateType.Eating:
return _stateEating;
default:
Debug.Log("not state!!");
return null;
}
}
private StateBase _state; // 現在のステート
private void Start()
{
_state = _stateMoveSea;
_state.OnStart(this);
}
private void Update()
{
_state.OnUpdate(this);
}
/// <summary>
/// ステート切替処理
/// </summary>
/// <param name="nextState"></param>
private void ChangeState(StateType nextState)
{
_state.OnEnd(this);
_state = GetState(nextState);
_state.OnStart(this);
}

エレキベア
だいぶ見やすくなったクマね
Sample03: ステートマシンとして切り出す

マイケル
このステート基底クラスと関連処理を別クラス(StateMachine)として切り出したのがSample03になります。
下記のようにジェネリック型で定義することで他のクラスからも利用できるようになっています。
下記のようにジェネリック型で定義することで他のクラスからも利用できるようになっています。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace Sample03
{
/// <summary>
/// ステートマシンクラス
/// class定義の基底クラス切り出し版
/// </summary>
public class StateMachine<TOwner>
{
/// <summary>
/// ステート基底クラス
/// 各ステートクラスはこのクラスを継承する
/// </summary>
public abstract class StateBase
{
public StateMachine<TOwner> StateMachine;
protected TOwner Owner => StateMachine.Owner;
public virtual void OnStart() { }
public virtual void OnUpdate() { }
public virtual void OnEnd() { }
}
private TOwner Owner { get; }
private StateBase _currentState; // 現在のステート
private StateBase _prevState; // 前のステート
private readonly Dictionary<int, StateBase> _states = new Dictionary<int, StateBase>(); // 全てのステート定義
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="owner">StateMachineを使用するOwner</param>
public StateMachine(TOwner owner)
{
Owner = owner;
}
/// <summary>
/// ステート定義登録
/// ステートマシン初期化後にこのメソッドを呼ぶ
/// </summary>
/// <param name="stateId">ステートID</param>
/// <typeparam name="T">ステート型</typeparam>
public void Add<T>(int stateId) where T : StateBase, new()
{
if (_states.ContainsKey(stateId))
{
Debug.LogError("already register stateId!! : " + stateId);
return;
}
// ステート定義を登録
var newState = new T
{
StateMachine = this
};
_states.Add(stateId, newState);
}
/// <summary>
/// ステート開始処理
/// </summary>
/// <param name="stateId">ステートID</param>
public void OnStart(int stateId)
{
if (!_states.TryGetValue(stateId, out var nextState))
{
Debug.LogError("not set stateId!! : " + stateId);
return;
}
// 現在のステートに設定して処理を開始
_currentState = nextState;
_currentState.OnStart();
}
/// <summary>
/// ステート更新処理
/// </summary>
public void OnUpdate()
{
_currentState.OnUpdate();
}
/// <summary>
/// 次のステートに切り替える
/// </summary>
/// <param name="stateId">切り替えるステートID</param>
public void ChangeState(int stateId)
{
if (!_states.TryGetValue(stateId, out var nextState))
{
Debug.LogError("not set stateId!! : " + stateId);
return;
}
// 前のステートを保持
_prevState = _currentState;
// ステートを切り替える
_currentState.OnEnd();
_currentState = nextState;
_currentState.OnStart();
}
/// <summary>
/// 前回のステートに切り替える
/// </summary>
public void ChangePrevState()
{
if (_prevState == null)
{
Debug.LogError("prevState is null!!");
return;
}
// 前のステートと現在のステートを入れ替える
(_prevState, _currentState) = (_currentState, _prevState);
}
}
}

エレキベア
ついにステートマシンのクラスが出来たクマね

マイケル
呼び出し側では下記のようにこのクラスを変数として持つようにします。
初期化の際にStateを登録する必要があることには注意が必要です。
初期化の際にStateを登録する必要があることには注意が必要です。
/// <summary>
/// ステート定義
/// </summary>
private enum StateType
{
MoveSea, // 海へ移動
Hunting, // 魚採取
MoveHome, // 家へ移動
Eating, // 食事
}
private StateMachine<Enemy> _stateMachine;
private void Start()
{
// ステートマシン定義
_stateMachine = new StateMachine<Enemy>(this);
_stateMachine.Add<StateMoveSea>((int) StateType.MoveSea);
_stateMachine.Add<StateHunting>((int) StateType.Hunting);
_stateMachine.Add<StateMoveHome>((int) StateType.MoveHome);
_stateMachine.Add<StateEating>((int) StateType.Eating);
// ステート開始
_stateMachine.OnStart((int) StateType.MoveSea);
}
private void Update()
{
// ステート更新
_stateMachine.OnUpdate();
}

エレキベア
めちゃくちゃシンプルになったクマね

マイケル
各ステート処理については下記のようになります。
ここはSample02とほとんど変わらないですね。
ここはSample02とほとんど変わらないですね。
// 各ステート処理
// ----- move sea -----
private class StateMoveSea : StateBase
{
public override void OnStart()
{
Debug.Log("start move sea");
}
public override void OnUpdate()
{
var enemyPosition = Owner.transform.position;
var targetPosition = Owner.stageManager.seaTransform.position;
// 海へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
StateMachine.ChangeState((int) StateType.Hunting);
return;
}
// 海へ向かう
Owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
Owner.transform.LookAt(targetPosition);
}
public override void OnEnd()
{
Debug.Log("end move sea");
}
}
// ----- hunting -----
private class StateHunting : StateBase
{
public override void OnStart()
{
Debug.Log("start hunting");
// 採取スタート
_isFinishHunting = false;
MonoBehaviorHandler.StartStaticCoroutine(HuntCoroutine());
}
public override void OnUpdate()
{
// 採取が完了したら次のステートへ
if (_isFinishHunting)
{
StateMachine.ChangeState((int) StateType.MoveHome);
}
}
public override void OnEnd()
{
Debug.Log("end hunting");
}
// 採取が完了しているか?
private bool _isFinishHunting;
// 採取コルーチン
private IEnumerator HuntCoroutine()
{
// 狩猟中、数秒待機
yield return new WaitForSeconds(2.0f);
// 魚取得
Instantiate(Owner.stageManager.fishPrefab, Owner.transform);
// 狩猟完了
_isFinishHunting = true;
}
}
// ----- move home -----
private class StateMoveHome : StateBase
{
public override void OnStart()
{
Debug.Log("start move home");
}
public override void OnUpdate()
{
var enemyPosition = Owner.transform.position;
var targetPosition = Owner.stageManager.homeTransform.position;
// 家へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
StateMachine.ChangeState((int) StateType.Eating);
return;
}
// 家へ向かう
Owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
Owner.transform.LookAt(targetPosition);
}
public override void OnEnd()
{
Debug.Log("end move home");
}
}
// ----- eating -----
private class StateEating : StateBase
{
public override void OnStart()
{
Debug.Log("start eating");
// 食事開始
_isFinishEating = false;
MonoBehaviorHandler.StartStaticCoroutine(EatCoroutine());
}
public override void OnUpdate()
{
// くるくる周る
Owner.transform.Rotate(Vector3.up * 500.0f * Time.deltaTime);
// 食事が完了したら次のステートへ
if (_isFinishEating)
{
StateMachine.ChangeState((int) StateType.MoveSea);
}
}
public override void OnEnd()
{
Debug.Log("end eating");
}
// 食事が完了しているか?
private bool _isFinishEating;
// 食事コルーチン
private IEnumerator EatCoroutine()
{
// 食事中、数秒待機
yield return new WaitForSeconds(5.0f);
// 子オブジェクト(魚)を破棄
foreach (Transform child in Owner.transform)
{
Destroy(child.gameObject);
}
// 食事完了
_isFinishEating = true;
}
}
Sample04: 遷移情報を定義する場合

マイケル
最後はステート状態の遷移情報を定義したステートマシンクラスの実装例です。
こちらは下記動画と記事を参考にさせていただきました!
天神いなさんの動画ではライブラリ化まで説明してくださってるので、是非みてみてください。
こちらは下記動画と記事を参考にさせていただきました!
天神いなさんの動画ではライブラリ化まで説明してくださってるので、是非みてみてください。

マイケル
この実装はステートの状態遷移図を元に定義する方法で、
下記のようにイベントごとに遷移情報を持たせる考え方(イベント駆動型)になっています。
下記のようにイベントごとに遷移情報を持たせる考え方(イベント駆動型)になっています。
イベント | 遷移元State | 遷移先State |
食事終了 | Eating | MoveSea |
海に到着 | MoveSea | Hunting |
採取完了 | Hunting | MoveHome |
家に到着 | MoveHome | Eating |

マイケル
StateMachineクラスの実装は以下のようになります。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Sample04
{
/// <summary>
/// ステートマシンクラス
/// 状態遷移の定義版
/// </summary>
public class StateMachine<TOwner>
{
/// <summary>
/// ステート基底クラス
/// 各ステートクラスはこのクラスを継承する
/// </summary>
public abstract class StateBase
{
public StateMachine<TOwner> StateMachine;
protected TOwner Owner => StateMachine.Owner;
public readonly Dictionary<int, StateBase> Transitions = new Dictionary<int, StateBase>(); // ステート遷移情報
public virtual void OnStart() { }
public virtual void OnUpdate() { }
public virtual void OnEnd() { }
}
private TOwner Owner { get; }
private StateBase _currentState; // 現在のステート
private readonly LinkedList<StateBase> _states = new LinkedList<StateBase>(); // 全てのステート定義
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="owner">StateMachineを使用するOwner</param>
public StateMachine(TOwner owner)
{
Owner = owner;
}
/// <summary>
/// ステート追加
/// </summary>
private T Add<T>() where T : StateBase, new()
{
// ステートを追加
var newState = new T
{
StateMachine = this
};
_states.AddLast(newState);
return newState;
}
/// <summary>
/// ステート取得、無ければ追加
/// </summary>
private T GetOrAdd<T>() where T : StateBase, new()
{
// 追加されていれば返却
foreach (var state in _states)
{
if (state is T result)
{
return result;
}
}
// 無ければ追加
return Add<T>();
}
/// <summary>
/// イベントIDに対応した遷移情報を登録
/// </summary>
/// <param name="eventId">イベントID</param>
/// <typeparam name="TFrom">遷移元ステート</typeparam>
/// <typeparam name="TTo">遷移先ステート</typeparam>
public void AddTransition<TFrom, TTo>(int eventId)
where TFrom : StateBase, new()
where TTo : StateBase, new()
{
// 既にイベントIDが登録済ならエラー
var from = GetOrAdd<TFrom>();
if (from.Transitions.ContainsKey(eventId))
{
Debug.LogError("already register eventId!! : " + eventId);
return;
}
// 指定のイベントIDで追加する
var to = GetOrAdd<TTo>();
from.Transitions.Add(eventId, to);
}
/// <summary>
/// ステート開始処理
/// </summary>
/// <typeparam name="T">開始するステート</typeparam>
public void OnStart<T>() where T : StateBase, new()
{
_currentState = GetOrAdd<T>();
_currentState.OnStart();
}
/// <summary>
/// ステート更新処理
/// </summary>
public void OnUpdate()
{
_currentState.OnUpdate();
}
/// <summary>
/// イベント発行
/// 指定されたIDのステートに切り替える
/// </summary>
/// <param name="eventId">イベントID</param>
public void DispatchEvent(int eventId)
{
// イベントIDからステート取得
if (!_currentState.Transitions.TryGetValue(eventId, out var nextState))
{
Debug.LogError("not found eventId!! : " + eventId);
return;
}
// ステートを切り替える
_currentState.OnEnd();
nextState.OnStart();
_currentState = nextState;
}
}
}

マイケル
呼び出し側では以下のようにイベントを定義して、初期化時に遷移情報を登録するようにしています。
今回の場合数に変わりはありませんが、ステートごとではなく遷移の数だけイベントを定義する必要があることには注意が必要です。
今回の場合数に変わりはありませんが、ステートごとではなく遷移の数だけイベントを定義する必要があることには注意が必要です。
/// <summary>
/// ステージ管理クラス
/// </summary>
[SerializeField] private StageManager stageManager;
/// <summary>
/// イベント定義
/// </summary>
private enum EventType
{
EatFinish, // 食事終了
ArriveSea, // 海に到着
HuntFinish, // 採取完了
ArriveHome, // 家に到着
}
private StateMachine<Enemy> _stateMachine;
private void Start()
{
// ステートマシン定義
_stateMachine = new StateMachine<Enemy>(this);
_stateMachine.AddTransition<StateEating, StateMoveSea>((int) EventType.EatFinish);
_stateMachine.AddTransition<StateMoveSea, StateHunting>((int) EventType.ArriveSea);
_stateMachine.AddTransition<StateHunting, StateMoveHome>((int) EventType.HuntFinish);
_stateMachine.AddTransition<StateMoveHome, StateEating>((int) EventType.ArriveHome);
// ステート開始
_stateMachine.OnStart<StateMoveSea>();
}
private void Update()
{
// ステート更新
_stateMachine.OnUpdate();
}

マイケル
各ステート定義は下記のようになります。
これまでとさほど変わりはありませんが、イベント発行によりステート切り替えを行うようにしています。
これまでとさほど変わりはありませんが、イベント発行によりステート切り替えを行うようにしています。
// 各ステート処理
// ----- move sea -----
private class StateMoveSea : StateBase
{
public override void OnStart()
{
Debug.Log("start move sea");
}
public override void OnUpdate()
{
var enemyPosition = Owner.transform.position;
var targetPosition = Owner.stageManager.seaTransform.position;
// 海へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
StateMachine.DispatchEvent((int) EventType.ArriveSea);
return;
}
// 海へ向かう
Owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
Owner.transform.LookAt(targetPosition);
}
public override void OnEnd()
{
Debug.Log("end move sea");
}
}
// ----- hunting -----
private class StateHunting : StateBase
{
public override void OnStart()
{
Debug.Log("start hunting");
// 採取スタート
_isFinishHunting = false;
MonoBehaviorHandler.StartStaticCoroutine(HuntCoroutine());
}
public override void OnUpdate()
{
// 採取が完了したら次のステートへ
if (_isFinishHunting)
{
StateMachine.DispatchEvent((int) EventType.HuntFinish);
}
}
public override void OnEnd()
{
Debug.Log("end hunting");
}
// 採取が完了しているか?
private bool _isFinishHunting;
// 採取コルーチン
private IEnumerator HuntCoroutine()
{
// 狩猟中、数秒待機
yield return new WaitForSeconds(2.0f);
// 魚取得
Instantiate(Owner.stageManager.fishPrefab, Owner.transform);
// 狩猟完了
_isFinishHunting = true;
}
}
// ----- move home -----
private class StateMoveHome : StateBase
{
public override void OnStart()
{
Debug.Log("start move home");
}
public override void OnUpdate()
{
var enemyPosition = Owner.transform.position;
var targetPosition = Owner.stageManager.homeTransform.position;
// 家へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
StateMachine.DispatchEvent((int) EventType.ArriveHome);
return;
}
// 家へ向かう
Owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
Owner.transform.LookAt(targetPosition);
}
public override void OnEnd()
{
Debug.Log("end move home");
}
}
// ----- eating -----
private class StateEating : StateBase
{
public override void OnStart()
{
Debug.Log("start eating");
// 食事開始
_isFinishEating = false;
MonoBehaviorHandler.StartStaticCoroutine(EatCoroutine());
}
public override void OnUpdate()
{
// くるくる周る
Owner.transform.Rotate(Vector3.up * 500.0f * Time.deltaTime);
// 食事が完了したら次のステートへ
if (_isFinishEating)
{
StateMachine.DispatchEvent((int) EventType.EatFinish);
}
}
public override void OnEnd()
{
Debug.Log("end eating");
}
// 食事が完了しているか?
private bool _isFinishEating;
// 食事コルーチン
private IEnumerator EatCoroutine()
{
// 食事中、数秒待機
yield return new WaitForSeconds(5.0f);
// 子オブジェクト(魚)を破棄
foreach (Transform child in Owner.transform)
{
Destroy(child.gameObject);
}
// 食事完了
_isFinishEating = true;
}
}

エレキベア
だいぶ完成されてきたクマね〜〜
アクタ間でのやり取り

マイケル
以上で実装4パターンの紹介は終わりになります。
しかしせっかくステートマシンを作ったので、最後にステートを持ったアクタ間のやり取りについても実装してみようと思います。
しかしせっかくステートマシンを作ったので、最後にステートを持ったアクタ間のやり取りについても実装してみようと思います。
Sample05: カニ夫婦の生活

マイケル
実装するのはカニ夫婦の生活になります。
下記のように、魚を採って帰った時に妻がいたらプレゼントする動きを作ってみます!
下記のように、魚を採って帰った時に妻がいたらプレゼントする動きを作ってみます!

エレキベア
なんか見てるとおもろいクマね〜〜
でも複雑で難しそうクマ
でも複雑で難しそうクマ

マイケル
ダーリン、ハニーでそれぞれステートマシンを定義して
図の通りに条件を定義してあげれば案外簡単に作れるよ!
とりあえず、StageManagerには追加で必要なオブジェクトを定義しておこう。
図の通りに条件を定義してあげれば案外簡単に作れるよ!
とりあえず、StageManagerには追加で必要なオブジェクトを定義しておこう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// ステージ管理クラス
/// </summary>
public class StageManager : MonoBehaviour
{
[SerializeField] public Transform homeTransform; // 家の位置情報
[SerializeField] public Transform seaTransform; // 海の位置情報
[SerializeField] public GameObject fishPrefab; // 魚Prefab
// *以下はSample05のみ使用
[SerializeField] public Transform homeHoneyTransform; // 家ハニーの位置情報
[SerializeField] public Transform walkTransform; // 散歩ポイントの位置情報
[SerializeField] public Sample05.Honey honey; // めんどくさいからハニーもいれちゃおう・・・
}

マイケル
基本的にはSample03を改変して実装します。
ダーリン(Enemyクラス)には新たなステートと家に到着した時の判定を追加してあげましょう!
ダーリン(Enemyクラス)には新たなステートと家に到着した時の判定を追加してあげましょう!
/// <summary>
/// ステート定義
/// </summary>
private enum StateType
{
MoveSea, // 海へ移動
Hunting, // 魚採取
MoveHome, // 家へ移動
Eating, // 食事
Looking, // 眺める ←★追加
}
private StateMachine<Enemy> _stateMachine;
private void Start()
{
// ステートマシン定義
_stateMachine = new StateMachine<Enemy>(this);
_stateMachine.Add<StateMoveSea>((int) StateType.MoveSea);
_stateMachine.Add<StateHunting>((int) StateType.Hunting);
_stateMachine.Add<StateMoveHome>((int) StateType.MoveHome);
_stateMachine.Add<StateEating>((int) StateType.Eating);
_stateMachine.Add<StateLooking>((int) StateType.Looking); // ←★追加
// ステート開始
_stateMachine.OnStart((int) StateType.MoveSea);
}
private void Update()
{
// ステート更新
_stateMachine.OnUpdate();
}
// 各ステート処理
・・・略・・・
// ----- move home -----
private class StateMoveHome : StateBase
{
public override void OnStart()
{
Debug.Log("<color=aqua>**kanio** 魚採れたので帰ります</color>");
}
public override void OnUpdate()
{
var enemyPosition = Owner.transform.position;
var targetPosition = Owner.stageManager.homeTransform.position;
// 家へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
// ★↓この辺を追加
// ハニーが家にいた場合
if (Owner.stageManager.honey.IsWaitingHome())
{
// 子オブジェクト(魚)をプレゼントしてまた海へ出かける
Debug.Log("<color=yellow>**kanio** あげるよハニー</color>");
foreach (Transform child in Owner.transform)
{
Owner.stageManager.honey.ReceiveFish(child.gameObject);
}
StateMachine.ChangeState((int) StateType.Looking);
return;
}
// いなかったら自分で食べる
StateMachine.ChangeState((int) StateType.Eating);
return;
}
// 家へ向かう
Owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
5.0f * Time.deltaTime);
Owner.transform.LookAt(targetPosition);
}
public override void OnEnd()
{
}
}
・・・略・・・
// ↓★追加
// ----- looking -----
private class StateLooking : StateBase
{
public override void OnStart()
{
Debug.Log("<color=aqua>**kanio** 眺めます</color>");
// 眺める開始
_isFinishLooking = false;
MonoBehaviorHandler.StartStaticCoroutine(LookCoroutine());
}
public override void OnUpdate()
{
// 眺めるのが完了したら次のステートへ
if (_isFinishLooking)
{
StateMachine.ChangeState((int) StateType.MoveSea);
}
}
public override void OnEnd()
{
}
// 眺めるのが完了しているか?
private bool _isFinishLooking;
// 眺めるコルーチン
private IEnumerator LookCoroutine()
{
// 数秒待機
yield return new WaitForSeconds(2.0f);
// 眺める完了
_isFinishLooking = true;
}
}

マイケル
そしてハニー(Honeyクラス)では、魚を受け取ったら食べるようにします!
それ以外はたまに散歩しています。
それ以外はたまに散歩しています。

エレキベア
(なまけものクマ・・・)
/// <summary>
/// Honeyクラス
/// 魚を受け取って食べるだけの生活
/// </summary>
public class Honey : MonoBehaviour
{
/// <summary>
/// ステージ管理クラス
/// </summary>
[SerializeField] public StageManager stageManager;
private enum StateType
{
WaitHome, // 待機
Walking, // 散歩
Eating, // 食事
}
private StateMachine<Honey> _stateMachine;
private void Start()
{
// ステートマシン定義
_stateMachine = new StateMachine<Honey>(this);
_stateMachine.Add<StateWaitHome>((int) StateType.WaitHome);
_stateMachine.Add<StateWalking>((int) StateType.Walking);
_stateMachine.Add<StateEating>((int) StateType.Eating);
// ステート開始
_stateMachine.OnStart((int) StateType.WaitHome);
}
private void Update()
{
// ステート更新
_stateMachine.OnUpdate();
}
/// <summary>
/// 家で待機中か?
/// </summary>
public bool IsWaitingHome()
{
return _stateMachine.IsCurrentState((int) StateType.WaitHome);
}
/// <summary>
/// 魚を受け取る
/// </summary>
/// <param name="fish"></param>
public void ReceiveFish(GameObject fish)
{
// 魚を受け取る
Debug.Log("<color=yellow>**honey** うれしいわダーリン</color>");
fish.transform.parent = transform;
var position = transform.position;
position.y = fish.transform.position.y;
fish.transform.position = position;
// 食事ステートに変更
_stateMachine.ChangeState((int) StateType.Eating);
}
// 各ステート処理
// ----- wait home -----
private class StateWaitHome : StateBase
{
public override void OnStart()
{
Debug.Log("<color=red>**honey** 家で待ちます</color>");
// 待機スタート
_isFinishWaiting = false;
MonoBehaviorHandler.StartStaticCoroutine(WaitCoroutine());
}
public override void OnUpdate()
{
// 待機が完了したら次のステートへ
if (_isFinishWaiting)
{
StateMachine.ChangeState((int) StateType.Walking);
}
}
public override void OnEnd()
{
}
// 待機が完了しているか?
private bool _isFinishWaiting;
// 待機コルーチン
private IEnumerator WaitCoroutine()
{
// 数秒待機
yield return new WaitForSeconds(5.0f);
// 待機完了
_isFinishWaiting = true;
}
}
// ----- walking -----
private class StateWalking : StateBase
{
public override void OnStart()
{
Debug.Log("<color=red>**honey** 散歩してきます</color>");
// 帰宅中フラグをfalseに設定
_isGoingHome = false;
}
public override void OnUpdate()
{
var enemyPosition = Owner.transform.position;
// 目的地の設定
var targetPosition = _isGoingHome
? Owner.stageManager.homeHoneyTransform.position // 帰宅中の場合、家の位置
: Owner.stageManager.walkTransform.position; // 帰宅中でない場合、散歩する位置
// 海へ到着したら次のステートへ
if (Vector3.Distance(enemyPosition, targetPosition) < 0.5f)
{
// 帰宅中フラグをONにしてreturn
if (!_isGoingHome)
{
_isGoingHome = true;
return;
}
// 次のステートへ遷移する
StateMachine.ChangeState((int) StateType.WaitHome);
return;
}
// 目的地へ向かう
Owner.transform.position = Vector3.MoveTowards(
enemyPosition,
targetPosition,
3.5f * Time.deltaTime);
Owner.transform.LookAt(targetPosition);
}
public override void OnEnd()
{
}
private bool _isGoingHome; // 帰宅中か?
}
// ----- eating -----
private class StateEating : StateBase
{
public override void OnStart()
{
Debug.Log("<color=red>**honey** 魚食べます</color>");
// 食事開始
_isFinishEating = false;
MonoBehaviorHandler.StartStaticCoroutine(EatCoroutine());
}
public override void OnUpdate()
{
// くるくる周る
Owner.transform.Rotate(Vector3.up * 500.0f * Time.deltaTime);
// 食事が完了したら次のステートへ
if (_isFinishEating)
{
StateMachine.ChangeState((int) StateType.WaitHome);
}
}
public override void OnEnd()
{
}
// 食事が完了しているか?
private bool _isFinishEating;
// 食事コルーチン
private IEnumerator EatCoroutine()
{
// 食事中、数秒待機
yield return new WaitForSeconds(3.0f);
// 子オブジェクト(魚)を破棄
foreach (Transform child in Owner.transform)
{
Destroy(child.gameObject);
}
// 食事完了
_isFinishEating = true;
}
}
}

マイケル
以上で実装は完了です!

エレキベア
なんとか動いたクマ〜〜〜
おわりに

マイケル
というわけで今回は有限ステートマシンの実装でした!
どうだったかな?
どうだったかな?

エレキベア
動きを作るのはおもしろいクマね
生きているみたいで感動したクマ
生きているみたいで感動したクマ

マイケル
命を吹き込む作業になるから楽しいね!
ゲームAIには他にもいろいろな方法があるから
どんどん試していこう!
ゲームAIには他にもいろいろな方法があるから
どんどん試していこう!

マイケル
それでは今日はこの辺で!
アデュー!!
アデュー!!

エレキベア
クマ〜〜〜〜〜
【Unity】カニの動きで学ぶ有限ステートマシン(FSM)【ゲームAI】 〜完〜