This is how i would implement above statemachine with my current expertise, my TryActions&Queues(see below) are not needed here because all transition logic is located within the states, and not driven by external input when implemented.
StateMachinePacman.cs
using UnityEngine;
using System.Collections;
using System;//Action
public enum PacmanState { Wander, Chase, Flee, Return};
public class StateMachinePacman : MonoBehaviour {
//STATEMACHINE
[HideInInspector] public PacmanState state = PacmanState.Wander;
public PacmanState stateSetter{
get { return state; }
set {
ExitState();
if(state != value){ if(stateChanged != null){ stateChanged(value); }}
state = value;
EnterState(state);
}
}
private IEnumerator curCoroutine;
private void ExitState(){
if(curCoroutine != null){ StopCoroutine(curCoroutine); }
//stuff that needs to be done before changing state
switch(state){
case PacmanState.Wander: break;
case PacmanState.Chase: break;
case PacmanState.Flee: break;
case PacmanState.Return: break;
}
print ("exiting:\t"+System.Enum.GetName(typeof(PacmanState), state)+"\t at"+Time.time);
}
private void EnterState(PacmanState newState){
print ("entering:\t"+System.Enum.GetName(typeof(PacmanState), state)+"\t at"+Time.time);
switch(newState){
case PacmanState.Wander: curCoroutine = PerformWander(); StartCoroutine( curCoroutine ); break;
case PacmanState.Chase: curCoroutine = PerformChase(); StartCoroutine( curCoroutine ); break;
case PacmanState.Flee: curCoroutine = PerformFlee(); StartCoroutine( curCoroutine ); break;
case PacmanState.Return: curCoroutine = PerformReturn(); StartCoroutine( curCoroutine ); break;
}
}
//
//NOTIFIERS
public event Action<PacmanState> stateChanged;
public event Action died;
//
void Start(){
stateSetter = PacmanState.Wander;
}
#region PerformActions
private IEnumerator PerformWander(){
while(true){
//DO STUFF
//normally we would yield at the end, but we use keyinput here, so we have to yield before checking keys, otherwise the input would be still be valid for new state
yield return null;
//state transition event simulated by keyInput:
if(Input.GetKeyDown("1")){ print("X"); }
if(Input.GetKeyDown("2")){ print("->chase"); stateSetter = PacmanState.Chase; }
if(Input.GetKeyDown("3")){ print("->flee"); stateSetter = PacmanState.Flee; }
if(Input.GetKeyDown("4")){ print("X"); }
}
}
private IEnumerator PerformChase(){
while(true){
//DO STUFF
yield return null;
if(Input.GetKeyDown("1")){ print("->wander"); stateSetter = PacmanState.Wander; }
if(Input.GetKeyDown("2")){ print("X"); }
if(Input.GetKeyDown("3")){ print("->flee"); stateSetter = PacmanState.Flee; }
if(Input.GetKeyDown("4")){ print("-X"); }
}
}
private IEnumerator PerformFlee(){
while(true){
//DO STUFF
yield return null;
if(Input.GetKeyDown("1")){ print("->wander"); stateSetter = PacmanState.Wander; }
if(Input.GetKeyDown("2")){ print("->chase"); stateSetter = PacmanState.Chase; }
if(Input.GetKeyDown("3")){ print("X"); }
if(Input.GetKeyDown("4")){ print("->return"); stateSetter = PacmanState.Return; }
}
}
private IEnumerator PerformReturn(){
while(true){
//DO STUFF
yield return null;
if(Input.GetKeyDown("1")){ print("->wander"); stateSetter = PacmanState.Wander; }
if(Input.GetKeyDown("2")){ print("X"); }
if(Input.GetKeyDown("3")){ print("X"); }
if(Input.GetKeyDown("4")){ print("X"); }
}
}
#endregion
}
Adding Queues:
After your remarks i am afraid my contraption it is not really an FSM. It controls a subject in the game and has states and commands:
- States are Coroutines that the subject currently performs, so i named them with the prefix “Perform” like “private IEnumerator PerformJump”.
- Commands are ways for ordering the subject to do smth, but letting the Subject check for itself if it is possible. They return a bool and are named with the prefix “Try” like “public bool TryJump”
You can control my StateMachine by:
- setting the variable “stateSetter” to any MachineState (this is an enum) OR
- using one of the try-functions like TryMelee to command the subject to perform a MeleeAttack. These try-functions determine if the action of state-switching will be enqueued, performed immediately or is forbidden (which returns false)
This way i can make a normal state-machine where every transition is programmed right into each state. Or i can manually ask for a state change from the outside by using the try-functions which contain more general rules to check if the subject is currently allowed to enter a state.
This is the barebone version of my current script, stripped of all special functionality used for my Coded Abilities that require other stuff from my project like Melee, Spells and MovementForces and { Jump, Focus, Melee, Aim, Shoot, Stagger, Stun, Dead} and grounded-checks. Also changed some things like the Hit function which normally takes an attack instance instead of raw dmg
StateMachine.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;//List
using System;//Action
//controls actor by inputs like PerformAttack or move and sets the corresponding animations, must have no link to the Game-UI, status indication is on-object
//Add Statechange Events for player character that are catched by Main-UI (OnFocus...)
public enum MachineState { None, Idle, Jump, Dash, Block, Cast, Melee, Stagger, Dead};
//Actor Controller, driven by Input through TryXXXX() functions and steerVector
//Inconsistency: is that Input is gathered in Update, and Timers run in Fixed Update, so first executionFrame is done in Update, following in Fixed
// - this could be changed by adding a WaitForFixedUpdate() before starting execution but that adds at least one render frame delay until input can be seen
public class StateMachine : MonoBehaviour {
//STATEMACHINE
[HideInInspector] public MachineState state = MachineState.None;
public MachineState stateSetter{
get { return state; }
set {
ExitState();
if(state != value){ if(stateChanged != null){ stateChanged(value); }}
state = value;
EnterState(state);
}
}
private IEnumerator curCoroutine;
private void ExitState(){
if(curCoroutine != null){ StopCoroutine(curCoroutine); }
//stuff that needs to be done before changing state
switch(state){
case MachineState.None: break;
case MachineState.Idle: break;
case MachineState.Jump: break;
case MachineState.Dash: break;
case MachineState.Block: break;
case MachineState.Cast: break;
case MachineState.Melee: break;
case MachineState.Stagger: break;
case MachineState.Dead: break;
}
print ("exiting:\t"+System.Enum.GetName(typeof(MachineState), state)+"\t at"+Time.time);
}
private void EnterState(MachineState newState){
print ("entering:\t"+System.Enum.GetName(typeof(MachineState), state)+"\t at"+Time.time);
switch(newState){
case MachineState.None: break;
case MachineState.Idle: curCoroutine = PerformIdle(); StartCoroutine( curCoroutine ); break;
case MachineState.Jump: break;
case MachineState.Dash: break;
case MachineState.Block: break;
case MachineState.Cast: break;
case MachineState.Melee: break;
case MachineState.Stagger: actionQueue.Clear(); break;
case MachineState.Dead: actionQueue.Clear(); break;
}
}
//
//NOTIFIERS
public event Action<MachineState> stateChanged;
public event Action died;
//
//ACTION QUEUE
//Rules:
// Dash and Block interrupt all Actions even Dash itself
// Melee, Casts should have a shared timeout, preventing high speed Melee->Dash->Melee->Dash... attack speeds caused by mutually interruptable states
private int maxQueueSize = 1; //increase due to preference, for user input controlled machine keep the queue size 1 and always overwrite the contained item with the last user input if the stack is not empty
private Queue<QueueAbleAction> actionQueue = new Queue<QueueAbleAction>(); //Action Queue
public class QueueAbleAction{
public MachineState type;
public Action action;
public QueueAbleAction(MachineState type, Action action){
this.type = type;
this.action = action;
}
}
//
// void Awake(){
// }
void Start(){
stateSetter = MachineState.Idle;
// TickManager.instance.lateUpdateTick1 += new System.Action(LateTick1);
// TickManager.instance.lateUpdateTick2 += new System.Action(LateTick2);
}
void OnDestroy(){
// TickManager.instance.lateUpdateTick1 -= new System.Action(LateTick1);
// TickManager.instance.lateUpdateTick2 -= new System.Action(LateTick2);
}
void Update(){
if(Input.GetKeyDown("1")){ print("Trying Dash"); TryDash(Vector3.forward); }
if(Input.GetKeyDown("2")){ print("Trying Block"); TryBlock(Vector3.forward); }
if(Input.GetKeyDown("3")){ print("Trying Melee"); TryMelee(Vector3.forward); }
if(Input.GetKeyDown("4")){ print("Dealing 1DMG"); Hit(1,Vector3.forward); }
}
// void LateUpdate(){
// }
// void LateTick1(){
// }
// void LateTick2(){
// }
private int hp = 3;
//this gets called when subject is hit
public bool Hit( int dmg, Vector3 dir, Vector3? sourceDir = null){
if(state != MachineState.Dead){
if(state != MachineState.Block){
hp -= dmg;
if(hp < 1){
stateSetter = MachineState.Dead;
if(died!=null){died();}
return true;
}else{
stateSetter = MachineState.Stagger;
return false;
}
}
}
return false;
}
private IEnumerator PerformIdle(){
while(true){
//check for Actions in Queue
if(actionQueue.Count>0){
MachineState type = actionQueue.Peek().type;
//you can perform special checks like time delay between melee attacks or such things before dequeuing here -> if( type == MachineState.Melee && meleeCastTimeOutRest != 0)...
print ("dequeued "+System.Enum.GetName(typeof(MachineState), type)+" from Idle() at "+Time.time);
actionQueue.Dequeue().action.Invoke();
}
yield return new WaitForFixedUpdate();
}
}
#region TryActions
//DASH
public bool TryDash(Vector3 dir){
if( state == MachineState.Dead){ return false; }
bool interrupt = true; //Dash interrupts all actions
if(interrupt){ actionQueue.Clear(); }
actionQueue.Enqueue(
new QueueAbleAction( MachineState.Dash,
()=>{
stateSetter = MachineState.Dash;
curCoroutine = PerformDash(dir);
StartCoroutine( curCoroutine );
}
)
);
//interrupt to Idle, Idle() checks for Dequeue, so this happens this very frame, not next frame
if(interrupt){ stateSetter = MachineState.Idle;}
return false;
}
//BLOCK
public bool TryBlock(Vector3 dir){
if( state == MachineState.Dead) { return false; }
bool interrupt = true; //Block interrupts all actions
if(interrupt){ actionQueue.Clear(); }
actionQueue.Enqueue(
new QueueAbleAction( MachineState.Block,
()=>{
stateSetter = MachineState.Block; //stops curCoroutine via set function
curCoroutine = PerformBlock(dir);
StartCoroutine( curCoroutine );
}
)
);
if(interrupt){ stateSetter = MachineState.Idle;}
return true;
}
//MELEE
public bool TryMelee(Vector3 dir){
if( state == MachineState.Dead) { return false; }
bool interrupt = false;
interrupt = state == MachineState.Dash || state == MachineState.Block;//can interrupt Block and Dash, otherwise queues
if(interrupt){ actionQueue.Clear(); }
if(actionQueue.Count == maxQueueSize){ return false; }
actionQueue.Enqueue(
new QueueAbleAction( MachineState.Melee,
()=>{
stateSetter = MachineState.Melee; //stops curCoroutine via set function
curCoroutine = PerformMelee(dir);
StartCoroutine( curCoroutine );
}
)
);
//interrupt PerformChargeAction
if(interrupt){ stateSetter = MachineState.Idle;}
return true;
}
//CAST
public bool TryCast(Vector3 dir){
if( state == MachineState.Dead ) { return false; }
bool interrupt = state == MachineState.Dash || state == MachineState.Block;//can interrupt Block and Dash
if(interrupt){ actionQueue.Clear(); }
if(actionQueue.Count == maxQueueSize){ return false; }
actionQueue.Enqueue(
new QueueAbleAction( MachineState.Cast,
()=>{
stateSetter = MachineState.Cast; //stops curCoroutine via set function
curCoroutine = PerformCast(dir/*, attack*/);
StartCoroutine( curCoroutine );
}
)
);
//interrupt PerformChargeAction
if(interrupt){ stateSetter = MachineState.Idle;}
return true;
}
#endregion
#region PerformActions
//ACTUAL JUMP (currently no manual jump, its to unhandy for touch)
private IEnumerator PerformJump(Vector3 dir){
//Do a Jump e.g:
//this.GetComponent<RigidBody>().AddForce(Vector3.up * intensity, ForceMode.Impulse);
//while(!grounded){ yield return new WaitForFixedUpdate(); }
yield return new WaitForFixedUpdate();
//at the end transition to Idle;
stateSetter = MachineState.Idle;
}
//BLOCK
private IEnumerator PerformBlock(Vector3 dir){
//Do a block for certain duration, rotate a shieldObject or whatever
for(int i = 0; i<25; i++){ //0.5s
//shieldTrans.Rotate(new Vector3(-900F*Time.deltaTime,0F,0F));
yield return new WaitForFixedUpdate();
}
stateSetter = MachineState.Idle;
}
//DASH
private IEnumerator PerformDash(Vector3 dir){
int dur = 50;
//Dash
for(int i = 0; i<dur; i++){
//steerVector = dir.normalized *36F;
yield return new WaitForFixedUpdate();
}
stateSetter = MachineState.Idle;
}
//MELEE
private IEnumerator PerformMelee(Vector3 dir/*, Attack attack*/){
//Do Attack
// attack.Invoke(dir);
int dur = 50;//attack.duration;
for(int i = 0; i<dur; i++){
yield return new WaitForFixedUpdate();
}
stateSetter = MachineState.Idle;
}
//SPELL
private IEnumerator PerformCast(Vector3 dir/*, Attack attack*/){
//Do Attack
// attack.Invoke(dir);
int dur = 50;//attack.duration;
for(int i = 0; i<dur; i++){
yield return new WaitForFixedUpdate();
}
stateSetter = MachineState.Idle;
}
#endregion
}
i hope it is understandable, since i really tinkered a lot after the queue addition, i run it in FixedUpdate() but i am aware that user Inputs arrive in Update() meaning that the first iteration of any perform loop will occur in Update, but then continue in FixedUpdate(). I could add a WaitForFixedUpdate() at the begin of every Perform, but this would increase the input-lag by up to a frameLength
Here is a small taste of what i am actually doing with it:

and my actual file consisting out of 2k5 lines of tinker mayhem
2671836–188562–ActorController.cs (80.2 KB)