nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

Unity で状態遷移のために有限オートマトンの実装

はじめに

有限オートマトンクラス

  • 状態を管理するクラス
  • 状態を管理したいオブジェクトごとに作成する
  • 後述の状態定義を登録し、Update などから呼び出して更新する
using UnityEngine;
using System.Collections.Generic;
using System;

public class FiniteStateMachine <T,U>  {
	private T Owner;
	public FSMState<T,U> CurrentState { get; private set; }
	private FSMState<T,U> PreviousState;

	private Dictionary<U,FSMState<T,U>> stateRef;

	public void Awake()
	{		
		CurrentState = null;
		PreviousState = null;
	}

	public FiniteStateMachine(T owner) {
		Owner = owner;
		stateRef = new Dictionary<U, FSMState<T, U>>();
	}

	public void Update()
	{
		foreach (U nextStateId in CurrentState.NextStateIDs) {
			FSMState<T, U> nextState = stateRef [nextStateId];
			if (nextState.CanEnter (CurrentState)) {
				ChangeState (nextState);
				return;
			}
		}
		if (CurrentState != null) CurrentState.Execute();
	}

	public void ChangeState(FSMState<T,U> NewState)
	{	
		PreviousState = CurrentState;

		if (CurrentState != null)
			CurrentState.Exit();

		CurrentState = NewState;

		if (CurrentState != null)
			CurrentState.Enter();
	}

	public void  RevertToPreviousState()
	{
		if (PreviousState != null)
			ChangeState(PreviousState);
	}

	public FSMState<T, U> RegisterState(FSMState<T,U> state)
	{
		state.RegisterEntity(Owner);
		stateRef.Add(state.StateID, state);
		return state;
	}

	public void UnregisterState(FSMState<T,U> state)
	{
		stateRef.Remove(state.StateID);

	}
};

状態定義の基底クラス

  • その状態のID、次の状態のID、その状態に遷移できるか?、初期化処理、更新処理、終了処理、などを状態毎に定義できるようなクラス
using System;
using System.Collections.Generic;

public class FSMState <T, U>   {

	protected T entity;

	public void RegisterEntity(T entity)
	{
		this.entity = entity;
	}

	virtual public U StateID 
	{
		get{
			throw new ArgumentException("State ID not spicified in child class");
		}
	}

	virtual public List<U> NextStateIDs {
		get { 
			throw new ArgumentException("Next State ID List not spicified in child class");
		}
	}

	virtual public bool CanEnter (FSMState<T, U> currentState){
		return false;
	}

	virtual public void Enter (){}

	virtual public void Execute (){}

	virtual public void Exit(){}
}

使用例

  • 状態管理するクラスの定義と状態の定義をし、Unity Test Tools を使って動作確認をする。
  • Main クラスが、Init, Run, Goal の3つの状態を持つ場合。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;

namespace FSM.Test {

	public class Main {
	
		public FiniteStateMachine<Main, Main.States> FSM;
		public bool Initialized = false;
		public int Counter = 0;

		public enum States {
			Init,
			Run,
			Goal,
		}

		public Main() {

			FSM = new FiniteStateMachine<Main, Main.States> (this);

			FSM.ChangeState (FSM.RegisterState (new StateInit ()));
			FSM.RegisterState (new StateRun ());
			FSM.RegisterState (new StateGoal ());
		}
	}

	public class StateInit : FSMState<Main, Main.States> {

		public override Main.States StateID { get { return Main.States.Init; } }

		public override List<Main.States> NextStateIDs { get { return new List<Main.States> { Main.States.Run }; } }

		public override void Enter () {
			entity.Initialized = true;
		}
		public override void Execute () {
		}
		public override void Exit () {
		}
	}

	public class StateRun : FSMState<Main, Main.States> {

		public override Main.States StateID { get { return Main.States.Run; } }

		public override List<Main.States> NextStateIDs { get { return new List<Main.States> { Main.States.Goal }; } }

		public override bool CanEnter (FSMState<Main, Main.States> currentState) {
			if (entity.Initialized) {
				return true;
			} else {
				return false;
			}
		}
		public override void Enter () {
		}
		public override void Execute () {
			entity.Counter++;
		}
		public override void Exit () {
		}
	}

	public class StateGoal : FSMState<Main, Main.States> {

		public override Main.States StateID { get { return Main.States.Goal; } }

		public override bool CanEnter (FSMState<Main, Main.States> currentState) {
			if (entity.Counter >= 3) {
				return true;
			} else {
				return false;
			}
		}
		public override void Enter () {
		}
		public override void Execute () {
		}
		public override void Exit () {
		}
	}

	[TestFixture]
	[Category("FSM")]
	internal class FSMTest  {

		[Test]
		public void MyFSMTest () {

			Main main = new Main ();

			Assert.That (main.FSM.CurrentState.StateID == Main.States.Init);
			Assert.That (main.Initialized == true);

			main.FSM.Update ();
			main.FSM.Update ();

			Debug.Log (main.Counter.ToString ());

			Assert.That (main.FSM.CurrentState.StateID == Main.States.Run);
			Assert.That (main.Counter == 1);

			main.FSM.Update ();
			main.FSM.Update ();

			Assert.That (main.Counter == 3);

			main.FSM.Update ();

			Assert.That (main.FSM.CurrentState.StateID == Main.States.Goal);
			Assert.That (main.Counter == 3);
		}
	}

}