EnumeratedDelegate
A method to setup a quick and dirty Finite State Machine of sorts by means of generics, delegates and an enum.
Contents |
Description
A C# generic class that takes an enum and a delegate as parameters and lightly emulates a finite state machine. Just provide an enum representing the different states, a delegate representing the type of function that all of the states will be represented by, and then one function for each member of the enum, matching the delegate signature.
Usage
(For this example, I am using a GUI navigation menu system)
First create an enum representing the different states (names and naming scheme are irrelevant, it's the order that's important):
public enum EMenuMode { eMenu_MainMenu, eMenu_Review, eMenu_Preview, eMenu_Teach, eMenu_Practice, eMenu_Apply, eMenu_Play, eMenu_StudentDB };
Then create the delegate to which all your state functions match:
public delegate void GUIMethod();
Next, declare the member variable representing the EnumeratedDelegate itself:
protected EnumeratedDelegate<EMenuMode, GUIMethod> m_MenuEnum = null;
Meow, create all the functions that will represent the different states, making sure that they all have the same signature: (Meow, what is so damn funny?!)
void DoMainMenuGUI() { //Do stuff } void DoReviewGUI() { //Do stuff } void DoPreviewGUI() { //Do stuff } void DoTeachGUI() { //Do stuff } void DoPracticeGUI() { //Do stuff } void DoApplyGUI() { //Do stuff } void DoPlayGUI() { //Do stuff } void DoStudentDBGUI() { //Do stuff }
At this point, you only need to instantiate the EnumeratedDelegate object:
void Awake() { //Create the enumerated delegate and pass in the correct functions, //in the exact order specified by the enum m_MenuEnum = new EnumeratedDelegate<EMenuMode, GUIMethod> ( new GUIMethod[] { DoMainMenuGUI, //eMenu_MainMenu, DoReviewGUI, //eMenu_Review, DoPreviewGUI, //eMenu_Preview, DoTeachGUI, //eMenu_Teach, DoPracticeGUI, //eMenu_Practice, DoApplyGUI, //eMenu_Apply, DoPlayGUI, //eMenu_Play, DoStudentDBGUI //eMenu_StudentDB } ); }
Note there are 4 different constructors for EnumeratedDelegate that take either the functions and/or the initial state. Initial state is the very first in the enum by default.
Last but definitely not least, you must actually call the current state function of the EnumeratedDelegate from whereever you need it (this is usually in Update() or OnGUI()):
void OnGUI() { //Call the current GUI (enumerated delegate) function m_MenuEnum.CurrentFunction(); }
You can change the current state of the EnumeratedDelegate through its CurrentMode property.
m_MenuEnum.CurrentMode = EMenuMode.eMenu_MainMenu;
EnumeratedDelegate class members of interest
Properties
| Property | Get/Set | Description |
|---|---|---|
| Count | Get | Returns the number of states represented by the enum.
|
| CurrentFunction | Get | Returns the function delegate of the current state. |
| CurrentMode | Get/Set | Gets and sets the current mode. Takes a value from within the enum as only correct input.
|
| Functions | Get/Set | Gets and sets the array of functions representing the states enumerated in the enum. Takes delegate[] as the value type.
|
Constructors
public EnumeratedDelegate()
| Parameter | Description |
|---|---|
| (None) | (N/A) |
Default Constructor. Reads in the names of the enum and stores them. Sets CurrentMode to the first value declared in the enum (technically whatever value is assigned to zero).
Note: The state functions are still null at this point, so you will still need to set the Functions property like so:
m_MenuEnum.Functions = new GUIMethod[] { DoMainMenuGUI, //eMenu_MainMenu, DoReviewGUI, //eMenu_Review, DoPreviewGUI, //eMenu_Preview, DoTeachGUI, //eMenu_Teach, DoPracticeGUI, //eMenu_Practice, DoApplyGUI, //eMenu_Apply, DoPlayGUI, //eMenu_Play, DoStudentDBGUI //eMenu_StudentDB };
public EnumeratedDelegate(int intialState)
| Parameter | Description |
|---|---|
| initialState | The initial value of CurrentMode.
|
Reads in the names of the enum and stores them. Sets CurrentMode to the enum value specified by initialState (technically whatever value is assigned to initialState).
Note: The state functions are still null at this point. See note above.
public EnumeratedDelegate(DelegateType[] arrFuncs)
| Parameter | Description |
|---|---|
| arrFuncs | The state functions representing the enum values. Sets via Functions property.
|
Reads in the names of the enum and stores them. Sets CurrentMode to the first value declared in the enum (technically whatever value is assigned to zero). Sets the state functions specified by arrFuncs.
public EnumeratedDelegate(DelegateType[] arrFuncs, int intialState)
| Parameter | Description |
|---|---|
| arrFuncs | The state functions representing the enum values. Sets via Functions property.
|
| initialState | The initial value of CurrentMode.
|
Reads in the names of the enum and stores them. Sets CurrentMode to the enum value specified by initialState (technically whatever value is assigned to initialState). Sets the state functions specified by arrFuncs.
Code
EnumeratedDelegate.cs
using UnityEngine; using System; using System.Collections; //Needs to be included because conversion to an int doesn't work any other way as far as I can tell, //and apparently it can't perform ENUM == ENUM or ENUM == int in generic functions //If you can solve, please feel free to update the Unify page for this class using System.Globalization; public class EnumeratedDelegate<ENUM, DelegateType> where ENUM : struct, IConvertible, IComparable, IFormattable where DelegateType : class { private ENUM m_eCurrentMode; //The current enum value private DelegateType m_fnCurrentFunction; //The current function being called private string[] m_arrNames; //The names of the enum values private DelegateType[] m_arrFunctions; //The functions corresponding to the enum values ////////////////////////////////////////////////////////////////////////////////////////////// public int Count { get { return m_arrNames.Length; } } public DelegateType CurrentFunction //Get the current function { get { return m_fnCurrentFunction; } } public DelegateType[] Functions //Set the functions safely { get { return m_arrFunctions; } set { if (m_arrNames == null || m_arrNames.Length == 0) throw new UnityException("Must set enum first!"); if (value.Length == 0) throw new UnityException("Empty arrays not allowed!"); if (value.Length != m_arrNames.Length) throw new UnityException("Must have same number of functions as enum values!"); m_arrFunctions = value; } } public ENUM CurrentMode //Get and set the current mode. When setting, change the function delegate appropriately { get { return m_eCurrentMode; } set { //Make sure that the value being passed in even corresponds to an existing enum value if (!Enum.IsDefined(typeof(ENUM), value)) throw new UnityException("'" + value + "' is not a valid member of the enum '" + typeof(ENUM).Name + "'!"); for (int i = 0; i < this.Count; i++) { //TODO: Check to see if we should try and set the index by name instead and avoid the CultureInfo //print(value.ToString()); //Must convert because conversion to an int doesn't work any other way as far as I can tell, //and apparently it can't perform ENUM == ENUM or ENUM == int in generic functions int nValue = value.ToInt32(new CultureInfo("en-us")); if (i == nValue) { SetFunctionByIndex(i); m_eCurrentMode = value; return; } } throw new UnityException("Couldn't find the enum somehow!"); } } ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// static EnumeratedDelegate() { //Assert that this class's first generic type is an enum if (!typeof(ENUM).IsEnum) throw new UnityException("First argument of EnumeratedDelegate must be an enum!"); //Debug.Log(typeof(DelegateType).); //Debug.Log(typeof(Delegate).Name); //Assert that this class's second generic type is a delegate //if (!(typeof(DelegateType).Name.Equals(typeof(Delegate).Name))) // throw new UnityException("Second argument of EnumeratedDelegate must be a Delegate!"); } public EnumeratedDelegate() { m_arrNames = Enum.GetNames(typeof(ENUM)); CurrentMode = (ENUM)Enum.GetValues(typeof(ENUM)).GetValue(0); } public EnumeratedDelegate(int intialState) { m_arrNames = Enum.GetNames(typeof(ENUM)); CurrentMode = (ENUM)Enum.ToObject(typeof(ENUM), intialState); } public EnumeratedDelegate(DelegateType[] arrFuncs) { m_arrNames = Enum.GetNames(typeof(ENUM)); this.Functions = arrFuncs; CurrentMode = (ENUM)Enum.GetValues(typeof(ENUM)).GetValue(0); } public EnumeratedDelegate(DelegateType[] arrFuncs, int intialState) { m_arrNames = Enum.GetNames(typeof(ENUM)); this.Functions = arrFuncs; CurrentMode = (ENUM)Enum.ToObject(typeof(ENUM), intialState); } void SetFunctionByIndex(int index) { Debug.Log("Setting function to '" + ((m_arrFunctions[index]) as Delegate).Method.ToString() + "'"); m_fnCurrentFunction = m_arrFunctions[index]; } void SetFunctionByName(string name) { int i = 0; foreach (string _name in m_arrNames) { if (_name == name) { Debug.Log("Setting function to '" + name + "'"); m_fnCurrentFunction = m_arrFunctions[i]; return; } i++; } throw new UnityException("Couldn't find an enum state representing given name: " + name); } }
Example usage as a GUI system for several different screens:
GUISetup.cs
using UnityEngine; using System.Collections; public class GUISetup : MonoBehaviour { ///////////////////////////////////////////////////// //The enumatered delegate setup ///////////////////////////////////////////////////// //Delegate declaration for EnumeratedDelegate public delegate void GUIMethod(); //The enum itself public enum EMenuMode { eMenu_MainMenu, eMenu_Review, eMenu_Preview, eMenu_Teach, eMenu_Practice, eMenu_Apply, eMenu_Play, eMenu_StudentDB }; protected EnumeratedDelegate<EMenuMode, GUIMethod> m_MenuEnum = null; //QADS (quick and dirty singleton, not fully //necessary if you don't plan on changing levels) private static bool mk_bAlreadyCreated = false; ///////////////////////////////////////////////////// ///////////////////////////////////////////////////// private GUILayout m_Layout; public bool BGFullScreen = true; public Texture2D BGImage; private bool m_bMainMenuVert = true; // Use this for initialization void Awake() { //If object has been created, but the menu deleted or destroyed, //destroy it if (mk_bAlreadyCreated && m_MenuEnum == null) { Destroy(this); return; } //Create the enumerated delegate and pass in the correct functions, //in the exact order specified by the enum m_MenuEnum = new EnumeratedDelegate<EMenuMode, GUIMethod> ( new GUIMethod[] { DoMainMenuGUI, //eMenu_MainMenu, DoReviewGUI, //eMenu_Review, DoPreviewGUI, //eMenu_Preview, DoTeachGUI, //eMenu_Teach, DoPracticeGUI, //eMenu_Practice, DoApplyGUI, //eMenu_Apply, DoPlayGUI, //eMenu_Play, DoStudentDBGUI //eMenu_StudentDB } ); //We want this game object to stick around DontDestroyOnLoad(this.gameObject); //QADS creation mk_bAlreadyCreated = true; PreviewGUI.Init(); } // Update is called once per frame void Update() { } void OnGUI() { int width = this.BGImage.width, height = this.BGImage.height; //if (BGFullScreen) GUI.Box(new Rect(0, 0, Screen.width, Screen.height), BGImage); //else //GUI.Box(new Rect(512 - width / 2, 384 - height / 2, width, height), BGImage); //Call the current GUI (enumerated delegate) function m_MenuEnum.CurrentFunction(); } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// void DoMainMenuGUI() { DoMainMenuButtons(); if (GUI.Button(new Rect(10, Screen.height * 0.9f, 100, 100), "Students")) { m_MenuEnum.CurrentMode = EMenuMode.eMenu_StudentDB; Application.LoadLevel("StudentDB"); } } void OnClickMainMenuButton(EMenuMode eMenuMode, string label) { m_MenuEnum.CurrentMode = eMenuMode; Application.LoadLevel(label); } void DoMainMenuButtons() { int ix = 512 - 150, iy = 250, dx = 5, dy = dx, w = 290, h = 30; string [] arrLabels = {"Main Menu", "Review", "Preview", "Teach", "Practice", "Apply", "Play"}; int numButtons = arrLabels.Length; if (!m_bMainMenuVert) { ix = 212; iy = 20; w = 80; //arrLabels = new string[]{"Main Menu", "Review", "Preview", "Teach", "Practice", "Apply", "Play"}; } else numButtons--; GUI.Box(new Rect(ix + dx, iy + dy, w + (2 * dx) + (m_bMainMenuVert ? 0 : (numButtons - 1) * (w + dx)), 5 * dy + (m_bMainMenuVert ? (numButtons * (h + dy)) : h + dy)), arrLabels[0/*(int)m_eMenuMode*/]); int index = 0; foreach (string label in arrLabels) { if (index > 0 || (index == 0 && !m_bMainMenuVert)) if (GUI.Button(new Rect(ix + 2 * dx + (m_bMainMenuVert ? 0 : (index - 0) * (w + dx)), iy + 6 * dy + (m_bMainMenuVert ? (index - 1) * (h + dy) : 0), w, h), label)) OnClickMainMenuButton((EMenuMode)index, label); index++; } } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// void DoReviewGUI() { m_bMainMenuVert = false; DoMainMenuGUI(); ReviewGUI.DoGUI(); } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// void DoPreviewGUI() { m_bMainMenuVert = false; DoMainMenuGUI(); PreviewGUI.DoGUI(); } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// void DoTeachGUI() { m_bMainMenuVert = false; DoMainMenuGUI(); } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// void DoPracticeGUI() { m_bMainMenuVert = false; DoMainMenuGUI(); } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// void DoApplyGUI() { m_bMainMenuVert = false; DoMainMenuGUI(); } ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////// void DoPlayGUI() { m_bMainMenuVert = false; DoMainMenuGUI(); PlayGUI.DoGUI(); } void DoStudentDBGUI() { if (GUI.Button(new Rect(10, Screen.height - 55, 200, 50), "Back To Main Menu")) { //m_bMainMenuVert m_MenuEnum.CurrentMode = EMenuMode.eMenu_MainMenu; Application.LoadLevel("Main Menu"); } else { StudentDBGUI.DoGUI(); } } }
Coming Soon
- State transition in/out support
- More/better error checking