• Register
Post news RSS InnerSpace: Programming- Using Attributes in State Machines

Our director and lead programmer, Tyler, recently ran into a problem with the state machines he was using: their behaviors were distributed into multiple classes, which was becoming inefficient. He sought a method to group attached behaviors and behavior execution in one spot, finding a solution by using enum-attributes. In this post, he explains why he approached it in this way, provides code for examples, and explains how he plans to use it in player health regeneration and menus.

Posted by on

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
  • Epic Rare 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.

Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.