Last active
September 9, 2020 01:07
-
-
Save RubenPineda/719634eef73ac57fc6defd04d9f4803e to your computer and use it in GitHub Desktop.
Finite state machine system for Unity. Includes a wizard to create new state machines with their own states. A state machine can be used for programming character behaviors, different player states or maybe different types of cameras.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Rubén Pineda 2020 | |
using UnityEngine; | |
using System.Collections; | |
using System; | |
namespace StateMachine { | |
public interface IState { | |
void Synchronize (); | |
void Desynchronize (); | |
void RequestChange (); | |
void Lock (); | |
void Unlock (); | |
bool Live { get; } | |
} | |
public abstract class State : MonoBehaviour, IState { | |
public abstract Type StateType { get; } | |
public abstract string IDName { get; } | |
/// <summary> | |
/// Is this state currently live? | |
/// </summary> | |
public bool Live { get { return m_Live; } } | |
private bool m_Live; | |
/// <summary> | |
/// Whether the state is currently waiting or not to update itself. | |
/// </summary> | |
public bool Waiting { get { return WaitingTime < m_WaitTime; } } | |
/// <summary> | |
/// Returns the elapsed time since the state started waiting. | |
/// </summary> | |
public float WaitingTime { get { return Mathf.Clamp(Time.time - m_WaitStart, 0f, m_WaitTime); } } | |
/// <summary> | |
/// Returns the elapsed percentage, from 0 to 1. | |
/// </summary> | |
public float WaitingPercentage { get { return WaitingTime / m_WaitTime; } } | |
private float m_WaitStart; | |
private float m_WaitTime; | |
private bool m_WaitingLastFrame; | |
/// <summary> | |
/// Returns whether this state is locked or not. | |
/// </summary> | |
public bool IsLocked { get { return m_IsLocked; } } | |
private bool m_IsLocked; | |
public abstract void Synchronize (); | |
public abstract void Desynchronize (); | |
public abstract void RequestChange (); | |
public abstract void Initialize (StateMachine stateMachine); | |
/// <summary> | |
/// Call this method to lock this state so it won't update. | |
/// </summary> | |
public void Lock () { | |
m_IsLocked = true; | |
m_WaitTime = 0f; | |
} | |
/// <summary> | |
/// Call this method to unlock this state so it will update. | |
/// </summary> | |
public void Unlock () { | |
m_IsLocked = false; | |
} | |
/// <summary> | |
/// Called whenever this state is synchronized. | |
/// </summary> | |
protected virtual void OnSynchronize () { | |
m_WaitingLastFrame = false; | |
} | |
/// <summary> | |
/// Called whenever this state is no longer synchronized. | |
/// </summary> | |
protected virtual void OnDesynchronize () { | |
m_WaitingLastFrame = false; | |
} | |
/// <summary> | |
/// Called automatically by the state machine after changing to a new state. | |
/// </summary> | |
protected virtual void OnStateEnter () { DefaultOnStateEnter(); } | |
/// <summary> | |
/// Called automatically by the state machine before changing to a new state. | |
/// </summary> | |
protected virtual void OnStateExit () { DefaultOnStateExit(); } | |
/// <summary> | |
/// It is called automatically by the FSMSystem after changing to a new state. | |
/// </summary> | |
protected void DefaultOnStateEnter () { | |
m_Live = true; | |
Register(); | |
} | |
/// <summary> | |
/// It is called automatically by the FSMSystem before changing to a new state. | |
/// </summary> | |
protected void DefaultOnStateExit () { | |
Unregister(); | |
m_Live = false; | |
} | |
/// <summary> | |
/// Subscribe this state to any event you want. | |
/// </summary> | |
protected virtual void Register () { } | |
/// <summary> | |
/// Unsubscribe this state from any event it's subscribed to. | |
/// </summary> | |
protected virtual void Unregister () { } | |
/// <summary> | |
/// This method is called to perform any action the current state should do. | |
/// </summary> | |
protected abstract void OnStateUpdate (); | |
/// <summary> | |
/// This method is called to perform any action the current state should do. | |
/// </summary> | |
protected virtual void OnStateFixedUpdate () { } | |
/// <summary> | |
/// Called right before the wait time begins. | |
/// </summary> | |
protected virtual void OnWaitBegan () { } | |
/// <summary> | |
/// Called if this state is currently waiting. | |
/// </summary> | |
protected virtual void WhileWaiting (float waitingTime) { } | |
/// <summary> | |
/// Called right after the wait time ends. | |
/// </summary> | |
protected virtual void OnWaitEnded () { } | |
/// <summary> | |
/// Called every update. | |
/// </summary> | |
protected virtual void OnUpdate () { DefaultUpdate(); } | |
/// <summary> | |
/// Called every fixed update. | |
/// </summary> | |
protected virtual void OnFixedUpdate () { DefaultFixedUpdate(); } | |
/// <summary> | |
/// Call this method to prevent this state to update for a time. | |
/// </summary> | |
/// <param name="time">The time it's gonna be waiting (in seconds).</param> | |
protected void Wait (float time) { | |
if (time > 0f) { | |
m_WaitTime = time; | |
m_WaitStart = Time.time; | |
OnWaitBegan(); | |
} | |
} | |
/// <summary> | |
/// This method is called every frame to update this state. | |
/// </summary> | |
protected void DefaultUpdate () { | |
if (!IsLocked) { | |
if (!Waiting) { | |
if (m_WaitingLastFrame) OnWaitEnded(); | |
else OnStateUpdate(); | |
} | |
else WhileWaiting(WaitingTime); | |
m_WaitingLastFrame = Waiting; | |
} | |
} | |
/// <summary> | |
/// This method is called every fixed frame to update this state. | |
/// </summary> | |
protected void DefaultFixedUpdate () { | |
if (!IsLocked && !Waiting) { | |
OnStateFixedUpdate(); | |
} | |
} | |
} | |
public abstract class State<States> : State where States : struct, IComparable, IConvertible, IFormattable { | |
public override Type StateType { get { return typeof(States); } } | |
public abstract States ID { get; } | |
public override string IDName { get { return ID.ToString(); } } | |
protected StateMachine<States> StateMachine { get; private set; } | |
/// <summary> | |
/// Set the Agent that controlls this state. | |
/// </summary> | |
public override void Initialize (StateMachine stateMachine) { | |
StateMachine = stateMachine as StateMachine<States>; | |
} | |
/// <summary> | |
/// Starts listening to the state machine callbacks. | |
/// </summary> | |
public override void Synchronize () { | |
StateMachine.onStateEnterCallback += OnStateEnter; | |
StateMachine.onStateExitCallback += OnStateExit; | |
StateMachine.updateCallback += OnUpdate; | |
StateMachine.fixedUpdateCallback += OnFixedUpdate; | |
OnSynchronize(); | |
} | |
/// <summary> | |
/// Stops listening to the state machine callbacks. | |
/// </summary> | |
public override void Desynchronize () { | |
StateMachine.onStateEnterCallback -= OnStateEnter; | |
StateMachine.onStateExitCallback -= OnStateExit; | |
StateMachine.updateCallback -= OnUpdate; | |
StateMachine.fixedUpdateCallback -= OnFixedUpdate; | |
OnDesynchronize(); | |
} | |
/// <summary> | |
/// Request a state change from the state machine. | |
/// </summary> | |
public override void RequestChange () { | |
StateMachine.ChangeState(ID); | |
} | |
/// <summary> | |
/// Tries to change the current state. | |
/// </summary> | |
protected bool ChangeState (States state) { | |
if (StateMachineHelper.IsValid(state)) return StateMachine.ChangeState(state); | |
else return false; | |
} | |
/// <summary> | |
/// Invokes given method with given delay. | |
/// </summary> | |
protected void Invoke (Action method, float delay) { | |
StartCoroutine(invoke(method, delay)); | |
} | |
private IEnumerator invoke (Action method, float delay) { | |
yield return new WaitForSeconds(delay); | |
method(); | |
} | |
/// <summary> | |
/// Invokes given method when given contition is satisfied. | |
/// </summary> | |
protected void InvokeWhen (Action method, Func<bool> condition) { | |
StartCoroutine(invokeWhen(method, condition)); | |
} | |
private IEnumerator invokeWhen (Action method, Func<bool> condition) { | |
yield return new WaitUntil(condition); | |
method(); | |
} | |
/// <summary> | |
/// Returns the ID of this state. | |
/// </summary> | |
public override string ToString () { return ID.ToString(); } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Rubén Pineda 2020 | |
using UnityEngine; | |
using System; | |
using System.Collections.Generic; | |
using System.Security.Permissions; | |
#pragma warning disable 0649 | |
namespace StateMachine { | |
public abstract class StateMachine : MonoBehaviour { | |
public State[] states = new State[0]; | |
public abstract Type StateType { get; } | |
public abstract string LastState { get; } | |
public abstract string CurrentState { get; } | |
public delegate void StateCallback(); | |
public StateCallback updateCallback; | |
public StateCallback fixedUpdateCallback; | |
public StateCallback onStateChangedCallback; | |
public StateCallback onLock; | |
public StateCallback onUnlock; | |
public StateCallback onStateExitCallback; | |
public StateCallback onStateEnterCallback; | |
/// <summary> | |
/// Returns whether this state is locked or not. | |
/// </summary> | |
public bool IsLocked { get { return m_IsLocked; } } | |
private bool m_IsLocked; | |
public abstract void Initialize(); | |
public abstract void Stop(); | |
/// <summary> | |
/// Does the State Machine contains given State? | |
/// </summary> | |
public bool Contains(string name) { | |
for (int i = 0; i < states.Length; i++) { | |
if (states[i].IDName == name) return true; | |
} | |
return false; | |
} | |
/// <summary> | |
/// Locks the state machine so it won't update. | |
/// </summary> | |
public void Lock() { | |
if (!IsLocked) { | |
m_IsLocked = true; | |
if (onLock != null) onLock(); | |
} | |
} | |
/// <summary> | |
/// Unlocks the state machine. | |
/// </summary> | |
public void Unlock() { | |
if (IsLocked) { | |
m_IsLocked = false; | |
if (onUnlock != null) onUnlock(); | |
} | |
} | |
/// <summary> | |
/// Updates the State Machine. | |
/// </summary> | |
public void UpdateMachine() { | |
if (!IsLocked) { | |
if (updateCallback != null) updateCallback(); | |
} | |
} | |
public void FixedUpdateMachine() { | |
if (!IsLocked) { | |
if (fixedUpdateCallback != null) fixedUpdateCallback(); | |
} | |
} | |
} | |
public abstract class StateMachine<States> : StateMachine where States : struct, IComparable, IConvertible, IFormattable { | |
public override Type StateType { get { return typeof(States); } } | |
[SerializeField] private bool m_DebugTransition; | |
[SerializeField] private States m_DefaultState; | |
public object UserData { get; set; } | |
public object UserData2 { get; set; } | |
private Dictionary<States, IState> m_States; | |
public override string LastState { get { return LastStateID.ToString(); } } | |
public override string CurrentState { get { return CurrentStateID.ToString(); } } | |
public States LastStateID { get; private set; } | |
public States NextStateID { get; private set; } | |
public States CurrentStateID { get; private set; } | |
public IState State { get { return m_States[CurrentStateID]; } private set { StateMachineHelper.LogError("Attempt to set a state by force."); } } | |
/// <summary> | |
/// Initialize a the State Machine. | |
/// </summary> | |
public override void Initialize() { | |
m_States = new Dictionary<States, IState>(); | |
for (int i = 0; i < states.Length; i++) { | |
State<States> state = states[i] as State<States>; | |
AddState(state); | |
} | |
SetState(m_DefaultState); | |
} | |
/// <summary> | |
/// Stops the machine. This forces any live state to exit. | |
/// </summary> | |
public override void Stop() { | |
if (StateMachineHelper.IsValid(CurrentStateID)) ExitState(); | |
else LastStateID = CurrentStateID; | |
} | |
/// <summary> | |
/// Sets by force given state. | |
/// </summary> | |
public bool SetState(States id) { | |
if (IsLocked) return false; | |
if (m_States.ContainsKey(id)) { | |
if (CurrentStateID.CompareTo(id) == 0) { | |
StateMachineHelper.LogWarning(id + " State is currently live."); | |
} | |
NextStateID = id; | |
if (StateMachineHelper.IsValid(CurrentStateID)) ExitState(); | |
else LastStateID = CurrentStateID; | |
PrintTransition(); | |
EnterState(); | |
return true; | |
} | |
else { | |
StateMachineHelper.LogError(id + " State is not on the State Machine."); | |
return false; | |
} | |
} | |
/// <summary> | |
/// Tries to change the current state. | |
/// </summary> | |
public bool ChangeState(States id) { | |
if (IsLocked) return false; | |
if (!StateMachineHelper.IsValid(id)) | |
StateMachineHelper.LogError("Invalid transition."); | |
else if (!m_States.ContainsKey(id)) | |
StateMachineHelper.LogError(id + " State is not on the State Machine."); | |
else { | |
if (CurrentStateID.CompareTo(id) == 0) { | |
StateMachineHelper.LogWarning(id + " State is currently live. Aborting."); | |
return false; | |
} | |
NextStateID = id; | |
if (StateMachineHelper.IsValid(CurrentStateID)) ExitState(); | |
else LastStateID = CurrentStateID; | |
PrintTransition(); | |
EnterState(); | |
return true; | |
} | |
return false; | |
} | |
private void ExitState() { | |
if (onStateExitCallback != null) onStateExitCallback(); | |
State.Desynchronize(); | |
LastStateID = CurrentStateID; | |
} | |
private void EnterState() { | |
CurrentStateID = NextStateID; | |
State.Synchronize(); | |
if (onStateEnterCallback != null) onStateEnterCallback(); | |
if (onStateChangedCallback != null) onStateChangedCallback(); | |
} | |
private void PrintTransition() { | |
if (m_DebugTransition) { | |
Debug.Log("From <b><color=red>" + LastStateID.ToString() + "</color></b> to <b><color=green>" + | |
NextStateID.ToString() + "</color></b> at time " + Time.time.ToString("F2") + "."); | |
} | |
} | |
/// <summary> | |
/// Places new states inside the FSM, or prints an ERROR message if the state was already inside the List. | |
/// </summary> | |
public void AddState(State<States> s) { | |
if (s == null) { | |
StateMachineHelper.LogError("Null reference is not allowed"); | |
} | |
else if (m_States.ContainsKey(s.ID)) { | |
StateMachineHelper.LogError("Impossible to add state <color=blue>" + s.ID.ToString() + "</color> because state has already been added."); | |
} | |
else { | |
m_States.Add(s.ID, s); | |
s.Initialize(this); | |
} | |
} | |
/// <summary> | |
/// Places new states inside the FSM, or prints an ERROR message if the state was already inside the List. | |
/// </summary> | |
public void AddStates(params State<States>[] s) { | |
for (int i = 0; i < s.Length; i++) AddState(s[i]); | |
} | |
/// <summary> | |
/// Removes a state from the FSM List if it exists. | |
/// </summary> | |
public void RemoveState(States id) { | |
if (!StateMachineHelper.IsValid(id)) { | |
StateMachineHelper.LogError("Null StateID is not allowed for a real state."); | |
} | |
else if (!m_States.ContainsKey(id)) { | |
StateMachineHelper.LogError("Impossible to delete state <color=blue>" + id.ToString() + "</color> state. It isn't on the list of states."); | |
} | |
else m_States.Remove(id); | |
} | |
/// <summary> | |
/// Returns true if the State Machine is currently in the given state. | |
/// </summary> | |
public bool IsInState<S>(S state) { return CurrentStateID.CompareTo(state) == 0; } | |
/// <summary> | |
/// Gets a state given its ID. | |
/// </summary> | |
public IState GetState(States id) { | |
return m_States.ContainsKey(id) ? m_States[id] : null; | |
} | |
/// <summary> | |
/// Gets a state given its ID. | |
/// </summary> | |
public T GetState<T>(States id) where T : State<States> { | |
return m_States.ContainsKey(id) ? (T)m_States[id] : null; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Rubén Pineda 2020 | |
using UnityEngine; | |
using UnityEditor; | |
using System; | |
using System.Collections.Generic; | |
using System.Reflection; | |
using System.Diagnostics; | |
namespace StateMachine.Editor { | |
[CustomEditor(typeof(StateMachine), true)] | |
public class StateMachineEditor : UnityEditor.Editor { | |
private UnityEditor.Editor[] subEditors; | |
private StateMachine m_StateMachine; | |
private Type m_EnumType; | |
private SerializedProperty m_StatesProperty; | |
private Type m_StateType; | |
private Type[] m_StatesTypes; | |
private string[] m_StatesTypeNames; | |
private int m_SelectedIndex; | |
private string m_Title; | |
private GUIContent m_IconMinus; | |
private const float k_ButtonWidth = 30.0f; | |
private const float k_ButtonHeight = 15.0f; | |
private GUIStyle m_BoxStyle; | |
private const float k_DropAreaHeight = 50f; | |
private const float k_ControlSpacing = 5f; | |
private const string k_StatesPropName = "states"; | |
private readonly float verticalSpacing = EditorGUIUtility.standardVerticalSpacing; | |
private void OnEnable () { | |
m_IconMinus = EditorGUIUtility.IconContent("Toolbar Minus"); | |
m_StateMachine = target as StateMachine; | |
m_EnumType = m_StateMachine.StateType; | |
m_StatesProperty = serializedObject.FindProperty(k_StatesPropName); | |
m_StateType = typeof(State); | |
m_Title = target.GetType().Name; | |
m_BoxStyle = new GUIStyle { | |
alignment = TextAnchor.MiddleCenter, | |
fontSize = 12, | |
fontStyle = FontStyle.Bold | |
}; | |
CheckAndCreateSubEditors(m_StateMachine.states); | |
SetStatesNamesArray(); | |
HideAllStates(true); | |
Undo.undoRedoPerformed += UndoRedoPerformed; | |
} | |
private void OnDisable () { | |
CleanupEditors(); | |
Undo.undoRedoPerformed -= UndoRedoPerformed; | |
} | |
private void UndoRedoPerformed () { | |
SetStatesNamesArray(); | |
HideAllStates(true); | |
} | |
public override void OnInspectorGUI () { | |
serializedObject.Update(); | |
CheckAndCreateSubEditors(m_StateMachine.states); | |
DrawInfo(); | |
DrawSubEditors(); | |
if (m_StateMachine.states.Length > 0) { | |
EditorGUILayout.Space(); | |
EditorGUILayout.Space(); | |
} | |
Rect left, right; | |
GetRects(k_DropAreaHeight, out left, out right); | |
TypeSelectionGUI(left); | |
DragAndDropAreaGUI(right); | |
DraggingAndDropping(right); | |
serializedObject.ApplyModifiedProperties(); | |
} | |
private void HideAllStates (bool value) { | |
State[] states = m_StateMachine.GetComponents<State>(); | |
for (int i = 0; i < states.Length; i++) states[i].hideFlags = value ? HideFlags.HideInInspector : HideFlags.None; | |
} | |
private void Refresh () { | |
serializedObject.Update(); | |
m_StatesProperty.ClearArray(); | |
serializedObject.ApplyModifiedProperties(); | |
CleanupEditors(); | |
State[] comps = m_StateMachine.GetComponents<State>(); | |
List<State> states = new List<State>(); | |
foreach (string n in Enum.GetNames(m_EnumType)) { | |
for (int i = 0; i < comps.Length; i++) { | |
if (comps[i].IDName == n) { | |
states.Add(comps[i]); | |
break; | |
} | |
} | |
} | |
for (int i = 0; i < states.Count; i++) { | |
serializedObject.Update(); | |
m_StatesProperty.InsertArrayElementAtIndex(m_StatesProperty.arraySize); | |
m_StatesProperty.GetArrayElementAtIndex(m_StatesProperty.arraySize - 1).objectReferenceValue = states[i]; | |
serializedObject.ApplyModifiedProperties(); | |
} | |
CheckAndCreateSubEditors(m_StateMachine.states); | |
SetStatesNamesArray(); | |
} | |
public void AddState (Type stateType) { | |
State newState = Undo.AddComponent(m_StateMachine.gameObject, stateType) as State; | |
newState.hideFlags = HideFlags.HideInInspector; | |
Refresh(); | |
} | |
public void RemoveState (State state) { | |
Undo.DestroyObjectImmediate(state); | |
Refresh(); | |
} | |
private void DefaultStatePopUp () { | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("m_DefaultState")); | |
//string[] options = new string[m_StateMachine.states.Length]; | |
//for (int i = 0; i < options.Length; i++) { | |
// options[i] = m_StateMachine.states[i].IDName; | |
//} | |
//EditorGUI.BeginChangeCheck(); | |
//int selection = EditorGUILayout.Popup(defaultState.displayName, defaultState.intValue, options); | |
//if (EditorGUI.EndChangeCheck()) { | |
// defaultState.intValue = selection; | |
//} | |
} | |
private void DrawInfo () { | |
GUILayout.Box(m_Title, GUILayout.ExpandWidth(true)); | |
EditorGUILayout.PropertyField(serializedObject.FindProperty("m_DebugTransition")); | |
DefaultStatePopUp(); | |
if (!PrefabUtility.GetCorrespondingObjectFromSource(m_StateMachine.gameObject) && PrefabUtility.GetPrefabInstanceHandle(m_StateMachine.gameObject)) { | |
EditorGUILayout.HelpBox("This is a prefab.\nIn order to see the State Machine info, select an instance.", MessageType.Info, true); | |
} | |
else { | |
Rect left, right; | |
GetRects(k_DropAreaHeight, out left, out right); | |
float y = left.y; | |
GUI.Box(left, GUIContent.none); | |
GUI.Box(right, GUIContent.none); | |
left.y = y - EditorGUIUtility.singleLineHeight; | |
right.y = y - EditorGUIUtility.singleLineHeight; | |
GUI.Label(left, "Last State", EditorStyles.centeredGreyMiniLabel); | |
GUI.Label(right, "Current State", EditorStyles.centeredGreyMiniLabel); | |
left.y = y; | |
right.y = y; | |
GUI.Label(left, Application.isPlaying ? m_StateMachine.LastState : "- - - - -", m_BoxStyle); | |
GUI.Label(right, Application.isPlaying ? m_StateMachine.CurrentState : "- - - - -", m_BoxStyle); | |
} | |
} | |
private void DrawSubEditors () { | |
if (subEditors.Length > 0) { | |
Rect left, right; | |
GetRects(EditorGUIUtility.singleLineHeight, out left, out right); | |
if (GUI.Button(left, "Show all States")) { | |
for (int i = 0; i < subEditors.Length; i++) m_StatesProperty.GetArrayElementAtIndex(i).isExpanded = true; | |
} | |
if (GUI.Button(right, "Hide all States")) { | |
for (int i = 0; i < subEditors.Length; i++) m_StatesProperty.GetArrayElementAtIndex(i).isExpanded = false; | |
} | |
for (int i = 0; i < subEditors.Length; i++) { | |
SerializedProperty property = m_StatesProperty.GetArrayElementAtIndex(i); | |
EditorGUILayout.BeginVertical(GUI.skin.box); | |
EditorGUILayout.BeginHorizontal(); | |
if (GUILayout.Button("- " + (subEditors[i].target as State).IDName + " State -", GUI.skin.button)) { | |
property.isExpanded = !property.isExpanded; | |
} | |
if (GUILayout.Button(m_IconMinus, GUILayout.Width(k_ButtonWidth))) { | |
RemoveState(m_StatesProperty.GetArrayElementAtIndex(i).objectReferenceValue as State); | |
EditorGUILayout.EndHorizontal(); | |
return; | |
} | |
EditorGUILayout.EndHorizontal(); | |
//GUILayout.Box("- " + (subEditors[i].target as State).IDName + " State -", GUI.skin.button, GUILayout.ExpandWidth(true)); | |
//Rect rect = GUILayoutUtility.GetLastRect(); | |
//rect.width -= k_ButtonWidth; | |
//if (GUI.Button(rect, GUIContent.none, GUI.skin.label)) { | |
// property.isExpanded = !property.isExpanded; | |
//} | |
//rect.x += rect.width; | |
//rect.width = k_ButtonWidth; | |
//rect.height = k_ButtonHeight; | |
//if (GUI.Button(rect, m_IconMinus)) { | |
// RemoveState(m_StatesProperty.GetArrayElementAtIndex(i).objectReferenceValue as State); | |
// EditorGUILayout.EndHorizontal(); | |
// return; | |
//} | |
if (property.isExpanded) { | |
EditorGUILayout.InspectorTitlebar(true, subEditors[i].target, false); | |
subEditors[i].OnInspectorGUI(); | |
} | |
EditorGUILayout.EndVertical(); | |
} | |
} | |
} | |
private void TypeSelectionGUI (Rect containingRect) { | |
Rect topHalf = containingRect; | |
topHalf.height *= 0.5f; | |
Rect bottomHalf = topHalf; | |
bottomHalf.y += bottomHalf.height; | |
m_SelectedIndex = EditorGUI.Popup(topHalf, m_SelectedIndex, m_StatesTypeNames); | |
if (GUI.Button(bottomHalf, "Add Selected State") && m_SelectedIndex > 0) { | |
Type stateType = m_StatesTypes[m_SelectedIndex - 1]; | |
AddState(stateType); | |
m_SelectedIndex = 0; | |
} | |
} | |
private void DragAndDropAreaGUI (Rect containingRect) { | |
GUIStyle centredStyle = GUI.skin.box; | |
centredStyle.alignment = TextAnchor.MiddleCenter; | |
centredStyle.normal.textColor = GUI.skin.button.normal.textColor; | |
GUI.Box(containingRect, "Drop new States here", centredStyle); | |
} | |
private void DraggingAndDropping (Rect dropArea) { | |
Event currentEvent = Event.current; | |
if (!dropArea.Contains(currentEvent.mousePosition)) return; | |
switch (currentEvent.type) { | |
case EventType.DragUpdated: | |
DragAndDrop.visualMode = IsDragValid() ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Rejected; | |
currentEvent.Use(); | |
break; | |
case EventType.DragPerform: | |
DragAndDrop.AcceptDrag(); | |
for (int i = 0; i < DragAndDrop.objectReferences.Length; i++) { | |
MonoScript script = DragAndDrop.objectReferences[i] as MonoScript; | |
Type stateType = script.GetClass(); | |
AddState(stateType); | |
} | |
currentEvent.Use(); | |
break; | |
} | |
} | |
private bool IsDragValid () { | |
for (int i = 0; i < DragAndDrop.objectReferences.Length; i++) { | |
if (DragAndDrop.objectReferences[i].GetType() != typeof(MonoScript)) return false; | |
MonoScript script = DragAndDrop.objectReferences[i] as MonoScript; | |
Type scriptType = script.GetClass(); | |
if (scriptType.IsSubclassOf(m_StateType) && !scriptType.IsAbstract) { | |
State state = Activator.CreateInstance(scriptType) as State; | |
if (state.StateType == m_EnumType) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
private void SetStatesNamesArray () { | |
Type[] allTypes = target.GetType().Assembly.GetTypes(); | |
List<Type> stateSubTypeList = new List<Type>(); | |
for (int i = 0; i < allTypes.Length; i++) { | |
if (allTypes[i].IsSubclassOf(m_StateType) && !allTypes[i].IsAbstract) { | |
State state = Activator.CreateInstance(allTypes[i]) as State; | |
if (state.StateType == m_EnumType) { | |
if (!m_StateMachine.Contains(state.IDName)) | |
stateSubTypeList.Add(allTypes[i]); | |
} | |
} | |
} | |
m_StatesTypes = stateSubTypeList.ToArray(); | |
List<string> stateTypeNameList = new List<string>(); | |
for (int i = 0; i < m_StatesTypes.Length; i++) { | |
State state = Activator.CreateInstance(m_StatesTypes[i]) as State; | |
stateTypeNameList.Add(state.IDName); | |
} | |
stateTypeNameList.Insert(0, "(none)"); | |
m_StatesTypeNames = stateTypeNameList.ToArray(); | |
ClearLog(); | |
} | |
private void CheckAndCreateSubEditors (State[] subEditorTargets) { | |
if (subEditors != null && subEditors.Length == subEditorTargets.Length) return; | |
CleanupEditors(); | |
subEditors = new UnityEditor.Editor[subEditorTargets.Length]; | |
for (int i = 0; i < subEditors.Length; i++) { | |
subEditors[i] = CreateEditor(subEditorTargets[i]) as UnityEditor.Editor; | |
} | |
} | |
private void CleanupEditors () { | |
if (subEditors == null) return; | |
for (int i = 0; i < subEditors.Length; i++) { | |
DestroyImmediate(subEditors[i]); | |
} | |
subEditors = null; | |
} | |
private void GetRects (float height, out Rect left, out Rect right) { | |
Rect fullWidthRect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(height + verticalSpacing)); | |
left = fullWidthRect; | |
left.y += verticalSpacing * 0.5f; | |
left.width *= 0.5f; | |
left.width -= k_ControlSpacing * 0.5f; | |
left.height = height; | |
right = left; | |
right.x += right.width + k_ControlSpacing; | |
} | |
public static void ClearLog () { | |
Assembly assembly = Assembly.GetAssembly(typeof(ActiveEditorTracker)); | |
Type type = assembly.GetType("UnityEditor.LogEntries"); | |
MethodInfo method = type.GetMethod("Clear"); | |
method.Invoke(new object(), null); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Rubén Pineda 2020 | |
using UnityEngine; | |
using System; | |
namespace StateMachine { | |
internal sealed class StateMachineHelper { | |
public static TEnum GetEnum<TEnum>(int index) where TEnum : struct, IComparable, IConvertible, IFormattable { | |
Type genericType = typeof(TEnum); | |
if (genericType.IsEnum) { | |
return (TEnum)(object)index; | |
} | |
else throw new ArgumentException("Given argument must be an Enum Type"); | |
} | |
public static bool IsValid<TEnum>(TEnum e) where TEnum : struct, IComparable, IConvertible, IFormattable { | |
Type genericType = typeof(TEnum); | |
if (genericType.IsEnum) { | |
return true; | |
} | |
else throw new ArgumentException("Given argument must be an Enum Type"); | |
} | |
public static TargetEnum TranslateEnum<SourceEnum, TargetEnum>(SourceEnum e) | |
where SourceEnum : struct, IComparable, IConvertible, IFormattable | |
where TargetEnum : struct, IComparable, IConvertible, IFormattable { | |
return (TargetEnum)Enum.Parse(typeof(TargetEnum), e.ToString()); | |
} | |
public static string[] GetEnumNames<TEnum>() { | |
Type genericType = typeof(TEnum); | |
if (genericType.IsEnum) return Enum.GetNames(typeof(TEnum)); | |
else return null; | |
} | |
public static int GetEnumEntries<TEnum>() { | |
Type genericType = typeof(TEnum); | |
if (genericType.IsEnum) return Enum.GetNames(typeof(TEnum)).Length; | |
else return -1; | |
} | |
public static void LogWarning(string warning) { Debug.LogWarning("<b><color=yellow>FSM WARNING:</color> " + warning + "</b>"); } | |
public static void LogError(string error) { Debug.LogError("<b><color=red>FSM ERROR:</color> " + error + "</b>"); } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Rubén Pineda 2020 | |
using UnityEngine; | |
using UnityEditor; | |
using UnityEditorInternal; | |
using System.Collections.Generic; | |
using System.CodeDom.Compiler; | |
using System.IO; | |
namespace StateMachine.Editor { | |
public class StateMachineWizard : EditorWindow { | |
private string m_Name = string.Empty; | |
private List<string> m_States = new List<string>(); | |
private ReorderableList m_ReorderableList; | |
private GUIContent m_IconMinus; | |
private const float k_ButtonWidth = 30.0f; | |
private readonly GUIContent m_NameContent = new GUIContent("Name", "This is the name that will represent the State Machine. It will be the basis for the class names so it is best not to use the postfixes like 'StateMachine' or 'State'."); | |
[MenuItem("Window/State Machine Wizard")] | |
public static void CreateWindow () { | |
StateMachineWizard wizard = GetWindow<StateMachineWizard>(true, "State Machine Wizard"); | |
wizard.minSize = new Vector2(400.0f, 400.0f); | |
wizard.Init(); | |
wizard.Show(); | |
} | |
public static bool IsValidName (string name) { | |
return !string.IsNullOrEmpty(name) && CodeGenerator.IsValidLanguageIndependentIdentifier(name); | |
} | |
public void Init () { | |
m_IconMinus = EditorGUIUtility.IconContent("Toolbar Minus"); | |
m_ReorderableList = new ReorderableList(m_States, typeof(string), true, true, true, false); | |
m_ReorderableList.drawHeaderCallback = (Rect rect) => { | |
rect.height = EditorGUIUtility.singleLineHeight; | |
GUI.Label(rect, m_Name + " States"); | |
}; | |
m_ReorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => { | |
if (index >= m_States.Count) return; | |
rect.height = EditorGUIUtility.singleLineHeight; | |
rect.width -= k_ButtonWidth; | |
m_States[index] = EditorGUI.TextField(rect, m_States[index]); | |
Rect r = new Rect(rect.x + rect.width, rect.y, k_ButtonWidth, rect.height); | |
if (GUI.Button(r, m_IconMinus)) { | |
m_States.RemoveAt(index); | |
return; | |
} | |
rect.width += k_ButtonWidth; | |
if (!IsValidName(m_States[index])) { | |
rect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; | |
rect.height *= 2; | |
EditorGUI.HelpBox(rect, "The State needs a name which starts with a capital letter and contains no spaces or special characters.", MessageType.Error); | |
} | |
}; | |
m_ReorderableList.onAddCallback = (ReorderableList list) => { | |
m_States.Add(string.Empty); | |
}; | |
m_ReorderableList.elementHeightCallback = (int index) => { | |
float height = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; | |
if (index >= m_States.Count) return height; | |
else return (IsValidName(m_States[index]) ? 1 : 3) * height; | |
}; | |
} | |
private void OnGUI () { | |
if (m_ReorderableList == null) Init(); | |
m_Name = EditorGUILayout.TextField(m_NameContent, m_Name); | |
EditorGUILayout.HelpBox(m_NameContent.tooltip, MessageType.Info); | |
m_ReorderableList.DoLayoutList(); | |
if (NameIsValid() && StatesAreValid() && NoDuplicates() && GUILayout.Button("Create " + m_Name + " State Machine")) { | |
CreateScripts(); | |
} | |
} | |
private bool NameIsValid () { | |
bool validName = IsValidName(m_Name); | |
if (!validName) { | |
EditorGUILayout.HelpBox("The State Machine needs a name which starts with a capital letter and contains no spaces or special characters.", MessageType.Error); | |
EditorGUILayout.Space(); | |
} | |
return validName; | |
} | |
private bool StatesAreValid () { | |
bool statesAreValid = true; | |
for (int i = 0; i < m_States.Count; i++) { | |
if (!IsValidName(m_States[i])) { | |
statesAreValid = false; | |
break; | |
} | |
} | |
if (!statesAreValid) { | |
EditorGUILayout.HelpBox("States must start with a capital letter and contain no spaces or special characters.", MessageType.Error); | |
EditorGUILayout.Space(); | |
} | |
return statesAreValid; | |
} | |
private bool NoDuplicates () { | |
bool areDuplicates = false; | |
for (int i = 0; i < m_States.Count; i++) { | |
for (int j = 0; j < m_States.Count; j++) { | |
if (i == j) continue; | |
if (m_States[i] == m_States[j]) { | |
areDuplicates = true; | |
break; | |
} | |
} | |
if (areDuplicates) break; | |
} | |
if (areDuplicates) { | |
EditorGUILayout.HelpBox("There are some States with the same name.", MessageType.Error); | |
EditorGUILayout.Space(); | |
} | |
return !areDuplicates; | |
} | |
private void CreateScripts () { | |
string fullPath = EditorUtility.SaveFolderPanel("Select parent folder", Application.dataPath, "Assets"); | |
if (string.IsNullOrEmpty(fullPath) || !fullPath.Contains("Assets")) return; | |
string parentFolder = fullPath.Remove(0, fullPath.IndexOf("Assets")); | |
string name = m_Name + " State Machine"; | |
AssetDatabase.CreateFolder(parentFolder, name); | |
AssetDatabase.CreateFolder(parentFolder + "/" + name, "States"); | |
string folder = fullPath + "/" + name; | |
CreateScript(folder, m_Name + "StateMachine", StateMachineContent()); | |
CreateScript(folder, m_Name + "State", AbstractStateContent()); | |
for (int i = 0; i < m_States.Count; i++) { | |
CreateScript(folder + "/States", StateName(m_States[i]), StateContent(m_States[i])); | |
} | |
AssetDatabase.SaveAssets(); | |
AssetDatabase.Refresh(); | |
Close(); | |
} | |
private void CreateScript (string folder, string fileName, string content) { | |
string path = folder + "/" + fileName + ".cs"; | |
using (StreamWriter writer = File.CreateText(path)) | |
writer.Write(content); | |
} | |
private string StateMachineName () { | |
return m_Name + "StateMachine"; | |
} | |
private string AbstractStateName () { | |
return m_Name + "State"; | |
} | |
private string StateName (string state) { | |
return m_Name + "_" + state + "State"; | |
} | |
private string EnumName () { | |
return m_Name + "States"; | |
} | |
private string GetEnumValues () { | |
string values = string.Empty; | |
if (m_States == null || m_States.Count == 0) values = "Default"; | |
else { | |
for (int i = 0; i < m_States.Count; i++) { | |
if (i == 0) values += m_States[i]; | |
else values += ", " + m_States[i]; | |
} | |
} | |
return values; | |
} | |
private string StateMachineContent () { | |
return | |
"using UnityEngine;\n" + | |
"using StateMachine;\n" + | |
"\n" + | |
"public enum " + EnumName() + " { " + GetEnumValues() + " }" + | |
"\n" + | |
"public class " + StateMachineName() + " : StateMachine<"+ EnumName()+ "> {\n" + | |
"\t" + "public override void Initialize () {\n" + | |
"\t" + "\t" + "base.Initialize();\n" + | |
"\t" + "}\n" + | |
"}"; | |
} | |
private string AbstractStateContent () { | |
return | |
"using UnityEngine;\n" + | |
"using StateMachine;\n" + | |
"\n" + | |
"public abstract class " + AbstractStateName() + " : State<" + EnumName() + "> {\n" + | |
"\t" + "public " + StateMachineName() + " " + StateMachineName() + " { get { return m_" + StateMachineName() + "; } }\n" + | |
"\t" + "private " + StateMachineName() + " m_" + StateMachineName() + ";\n" + | |
"\n" + | |
"\t" + "public override void Initialize (StateMachine.StateMachine stateMachine) {\n" + | |
"\t" + "\t" + "base.Initialize(stateMachine);\n" + | |
"\t" + "\t" + " m_" + StateMachineName() + " = StateMachine as " + StateMachineName() + ";\n" + | |
"\t" + "}\n" + | |
"}"; | |
} | |
private string StateContent (string state) { | |
return | |
"using UnityEngine;\n" + | |
"\n" + | |
"[AddComponentMenu(\"\")]\n" + | |
"public class " + StateName(state) + " : " + AbstractStateName() + " {\n" + | |
"\t" + "public override " + EnumName() + " ID { get { return " + EnumName() + "." + state + "; } }\n" + | |
"\n" + | |
"\t" + "protected override void OnStateEnter () {\n" + | |
"\t" + "\t" + "base.OnStateEnter();\n" + | |
"\t" + "}\n" + | |
"\n" + | |
"\t" + "protected override void OnStateExit () {\n" + | |
"\t" + "\t" + "base.OnStateExit();\n" + | |
"\t" + "}\n" + | |
"\n" + | |
"\t" + "protected override void OnStateUpdate () {\n" + | |
"\n" + | |
"\t" + "}\n" + | |
"}"; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment