Hi All,
I’ve been working on a C Sharp State Machine that allows an Object to be in multiple states at once, and thought I would put it up for all to see.
It may be possible to recreate in JS, but I’m not sure if JS supports reflection - dynamic typing may make it unnecesarry, I’m not sure.
I have seen some implementations that use coroutines, or additional game objects, but those didn’t really suit my needs.
Essentially how it works is you have a class that inherits from the StateMachine, and a bunch of Static classes to use as states.
The StateMachine maintains a stack of all the states the object is currently in, and when a “State method” is called, it uses reflection to invoke the method on the state at the top of the stack.
(ie: InvokeMethod(Foo, Bar); will invoke the method Foo with arguments Bar on the current state)
If the top state returns null, the next state on the stack is called and so on.
I’ve also posted a Player.cs script that utilises the StateMachine, though it is currently very light on comments, I will rectify that in coming days, but should be able to answer any questions people have.
In order to use the Player.cs script, you will need to define a Shield gameobject in the asset viewer (make it inactive to start with), and assign a “Shield” button in preferences, beyond that it should work in anything.
You may also need to rename ApplyDamage in the Player class to whatever your damage method name is.
EDIT Also the Die function for the Player.cs script heals the player, no dying here
Let me know what you think!
StateMachine.cs
using UnityEngine;
using System;
using System.Reflection;
using System.Collections;
/// <summary>
/// StateMachine.cs
/// Class to represent a State Machine that allows an object to be in multiple states.
/// </summary>
public class StateMachine : MonoBehaviour
{
protected ArrayList stateStack;
/// <summary>
/// Use this for initialization
/// All subclasses must call this function in their own start.
/// </summary>
public void Start()
{
stateStack = new ArrayList();
}
/// <summary>
/// Enter a new State.
/// If the State is already in the StateStack, move it to the top.
/// </summary>
/// <param name="newState">Name of the State to Enter.</param>
/// <returns>object from invoked method</returns>
public object GoToState(string newState)
{
Type chkState = Type.GetType(newState);
if(stateStack.Contains(newState))
{
int i = stateStack.IndexOf(newState);
// Insert before removal to avoid "stuttering"
stateStack.Insert(0,newState);
// Increments i as the index has shifted.
stateStack.RemoveAt(i+1);
}
else
{
stateStack.Insert(0,newState);
return InvokeMethod(chkState,"BeginState",null);
}
return null;
}
/// <summary>
/// Leave a state if it is currently in the StateStack
/// </summary>
/// <param name="newState">Name of the State to leave.</param>
/// <returns>object from invoked method</returns>
public object ExitState(string newState)
{
Type chkState = Type.GetType(newState);
if(stateStack.Contains(newState))
{
stateStack.Remove(newState);
return InvokeMethod(chkState,"EndState",null);
}
return null;
}
/// <summary>
/// Invokes a method on the current state.
/// If there is no result, move through the statestack.
/// </summary>
/// <param name="method">Method to Invoke</param>
/// <param name="args">Arguments to pass</param>
/// <returns>object from invoked method</returns>
/// <exception cref="MissingMethodException">
/// Thrown when method doesn't exist for current State.
/// </exception>
public object InvokeMethod(string method, object[] args)
{
try
{
for(int i=0;i<stateStack.Count;i++)
{
Type callState = Type.GetType(stateStack[i].ToString());
object result = callState.InvokeMember(method,BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod ,null,null,args);
if( result != null)
return result;
}
}
catch (MissingMethodException e)
{
Debug.Log(e.Message);
}
return null;
}
/// <summary>
/// Invokes a method on a specific state.
/// </summary>
/// <param name="callState">State to call</param>
/// <param name="method">Method to Invoke</param>
/// <param name="args">Arguments to pass</param>
public object InvokeMethod(Type callState, string method, object[] args)
{
return callState.InvokeMember(method,BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod ,null,null,args);
}
/// <summary>
/// Prints the StateStack
/// </summary>
public void printStack()
{
for(int i=0;i<stateStack.Count;i++)
{
Debug.Log("State: "+i+" is: "+stateStack[i]);
}
}
}
Player.cs
using UnityEngine;
using System;
using System.Collections;
using System.Reflection;
public class Player : StateMachine
{
public GameObject Shield;
private double currentHealth;
private double currentShield;
public int shieldRechargeDelay = 5;
public int shieldRechargeRate = 2;
private float shieldLastHit = 5.0f;
private static double MAXHEALTH = 100.0;
private static double MAXSHIELD = 100.0;
private static int healthReturn = 0;
private static int shieldReturn = 1;
private static int lastHitReturn = 2;
// Use this for initialization
new void Start()
{
base.Start();
GoToState("DefaultPlayer");
currentHealth = MAXHEALTH;
currentShield = MAXSHIELD;
}
// Update is called once per frame
void Update ()
{
}
void LateUpdate()
{
shieldLastHit+=Time.deltaTime;
//Debug.Log(shieldLastHit);
Active();
RegenerateShield();
}
public void Active()
{
//Shield.active = (bool)InvokeMethod("Active",null);
object result = null;
if(Input.GetButton("Shield") currentShield > 0.0)
{
result = GoToState("ShieldActive");
}
else
{
result = ExitState("ShieldActive");
}
if(result != null)
Shield.active = (bool)result;
}
public void RegenerateShield()
{
object result;
if(currentShield < MAXSHIELD)
{
if(shieldLastHit > shieldRechargeDelay)
{
GoToState("ShieldRegen");
}
object[] args = new object[] {currentShield, shieldRechargeRate};
result = InvokeMethod("Regenerate", args);
if( result != null)
currentShield = (double)result;
}
else
ExitState("ShieldRegen");
}
public void ApplyDamage (double damage)
{
System.Object[] args = new System.Object[] {damage, currentHealth, currentShield, shieldLastHit};
ArrayList values = (ArrayList)InvokeMethod("ApplyDamage",args);
currentHealth = (double)values[healthReturn];
currentShield = (double)values[shieldReturn];
if((float)values[lastHitReturn] != shieldLastHit)
{
ExitState("ShieldRegen");
shieldLastHit = (float)values[lastHitReturn];
}
Debug.Log("Your health: " + currentHealth + ". Your shield: "+ currentShield);
if(currentHealth <= 0.0)
{
Die();
}
}
private void Die ()
{
Debug.Log("You be dead!");
currentHealth = MAXHEALTH;
}
}
public class ShieldActive
{
public static bool EndState()
{
Debug.Log("Leaving State: ShieldActive");
return false;
}
public static bool BeginState()
{
Debug.Log("Entering State: ShieldActive");
return true;
}
public static ArrayList ApplyDamage (double damage, double health, double shield, float hitTime)
{
ArrayList results = new ArrayList();
if(damage > shield)
{
double overload = damage - shield;
shield = 0.0;
health -= overload;
}
else
{
shield -= damage;
}
results.Add(health);
results.Add(shield);
results.Add(Time.deltaTime);
return results;
}
}
public class DefaultPlayer
{
public static void EndState()
{
//return "Leaving State: DefaultPlayer";
}
public static void BeginState()
{
//return "Entering State: DefaultPlayer";
}
public static ArrayList ApplyDamage (double damage, double health, double shield, float hitTime)
{
ArrayList results = new ArrayList();
health -= damage;
results.Add(health);
results.Add(shield);
results.Add(hitTime);
return results;
}
}
public class ShieldRegen
{
public static void EndState()
{
//Debug.Log("Leaving State: ShieldRegen");
}
public static void BeginState()
{
//Debug.Log("Entering State: ShieldRegen");
}
public static double Regenerate(double shield, int rate)
{
//Debug.Log("Regenerating!");
shield+=rate * Time.deltaTime;
Debug.Log(shield);
return shield;
}
}