Warning: Intense programming ahead.
Reasoning
I recently ran into a problem with my state machines: their behaviors were distributed to multiple classes or multiple “Update” or “Enter” methods. This is okay when each state has truly unique behaviors, but many of my state machines had reoccurring actions throughout my states. A centralized location and structure for behaviors of states is what I wanted, I wanted to troubleshoot “attached behaviors” and “behavior execution” separately and efficiently. This meant grouping “attached behaviors” and handling “behavior execution” in one spot. The latter is pretty common, but the former is not.
I recently experimented with using enum-attributes to remedy this. It’s likely not the most efficient, but it made for some clean and short code. This isn’t the most efficient state machine I’ve written, but it is the most interesting one that I’ve actually utilized.
When I plan on using this:
- State machine with reoccurring actions between states
- e.g. regenerating health
- State machine with reoccurring values needed for each state
- e.g. Which Menu the “Back” button goes to in menu system
- When you wish your Enums were classes
The Enum
public enum States
{
[Regen_HP()]
[Regen_Stall()]
[Set_Actual()]
Standard,
[Regen_HP()]
[Regen_Stall()]
[Set_Actual()]
Submerged,
Skip,
[Regen_HP()]
Stall,
[Regen_HP()]
[Regen_Stall()]
[Set_Actual()]
Hyper,
[Regen_HP()]
[Regen_Stall()]
Dive,
[Set_Actual()]
Collide
}
Above we have a series of Enumerators (Standard, Submerged , Skip, …) and Attributes attached to each of them (Regen_HP, Regen_Stall, Set_Actual).
The Attribute
public class Regen_HP : Attribute
{
public void Regen (PlaneShip ship)
{
//Do stuff
}
}
They’re an extension of C#’s Attribute class. While an Interface or Abstract base class would be ideal for a more elaborate system, this will do for now.
How do we utilize these attributes?
Luckily we’re 21st century programmers with Google, so StackOverflow does most the work for us. The first example is an extension of the Enum class that we need, whereas the latter is the utilization of said extension.
public static T GetAttributeOfType<T> (this Enum enumVal) where T:System.Attribute
{
var type = enumVal.GetType ();
var memInfo = type.GetMember (enumVal.ToString ());
var attributes = memInfo [0].GetCustomAttributes (typeof(T), false);
return (attributes.Length > 0) ? (T)attributes [0] : null;
}
This should be inside a static extensions class.
Regen_HP rhp = this.GetState ().GetAttributeOfType<Regen_HP> ();
if (rhp != null)
rhp.Regen (planeShip);
This should go inside an Update or FixedUpdate loop (programmer's choice).
.GetState() is a method I’ve implemented to return the plane’s current State (it returns type “States”).
An Implementation
using UnityEngine;
using System.Collections;
using InnerSpace.Utility;
namespace InnerSpace
{
//SomeStateMachine being a state machine that receives an enum type for it's states
public class GameState : SomeStateMachine<GameState.States>
{
class StateInfo : System.Attribute
{
public string Name;
public bool Paused;
public States Parent;
public States Resume;
public StateInfo (string name, bool paused, States parent, States resume)
{
Name = name;
Paused = paused;
Parent = parent;
Resume = resume;
}
public StateInfo (string name, bool paused, States parent)
: this(name, paused, parent, parent) {}
}
public enum States
{
[StateInfo("Playing", false, States.Playing, States.Paused)]
Playing,
[StateInfo("Paused", true, States.Playing)]
Paused,
[StateInfo("RelicCatalogue", true, States.Paused)]
RelicCatalogue,
[StateInfo("Inspector", true, States.RelicCatalogue)]
RelicInspector,
[StateInfo("PlaneSwap", true, States.Paused)]
PlaneSwap,
[StateInfo("CompanionSwap", true, States.Paused)]
CompanionSwap,
[StateInfo("Settings", true, States.Paused)]
Settings,
[StateInfo("MainMenu", true, States.MainMenu)]
MainMenu,
[StateInfo("Save/Load", true, States.MainMenu)]
SaveLoad
}
void Update ()
{
if (Input.GetButtonDown (InnerStrings.INPUT_CANCEL)) {
StateInfo si = CurrentState.GetAttributeOfType<StateInfo> ();
SetStateAndDoActions (si.Parent);
}
else if (Input.GetButtonDown (InnerStrings.INPUT_PAUSE)) {
StateInfo si = CurrentState.GetAttributeOfType<StateInfo> ();
SetStateAndDoActions (si.Parent);
}
}
public void SetStateAndDoActions (States state)
{
StateInfo gs = state.GetAttributeOfType<StateInfo> ();
Time.timeScale = gs.Paused ? 0f : 1f;
//Some method from "SomeStateMachine"
this.ChangeState(state);
}
}
}
Summary
While was a pretty hasty implementation, I’ve found it very useful.
Pros
- Centralize common state-behaviors
- Reduced boilerplate code
- Readable behaviors (which states do what)
- Can be latched onto existing state machine
- Doesn’t use reflection
Cons
- Less performant
- Not chached
- Poorly typed
EpicRare implementation
I know I haven’t explained much, but hey, I have a game to make. If you have any questions or comments, feel free to drop a line into the comments or fire me a mention on Twitter.
This post was originally published on the PolyKnight Games blog, by Tyler Tomaseski.