浏览代码

Replaced StateTransitionSO with TransitionTableSO + Editor (#114)

* Initial commit. Replaced StateTransitionSO with TransitionTableSO.

* [Bot] Automated dotnet-format update

* Deleted old transitions assets.

* Editor

Created TransitionTableEditor and StateEditor.
- TransitionTableEditor allows you to fully configure the transition table in an easy way.
- A list of the states in the table is displayed, with the initial state at the top.
- States can be sorted manually to change the initial state or just for ordering.
- The order of the states bellow the initial shouldn't affect the behaviour of the state machine.
- Added buttons to easily switch back and forth between state editor and transition table editor.
- The state editor is a simple reorderable list with all the action in the state.
- Clicking on a state will display all of its transitions, along with their conditions.
- Transitions can be sorted to change the order in which checks are executed.
- Transit...
/main
GitHub 4 年前
当前提交
96e3b3f0
共有 23 个文件被更改,包括 1131 次插入30 次删除
  1. 2
      UOP1_Project/Assets/Prefabs/Pig.prefab
  2. 5
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachine.cs
  3. 19
      UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/StateSO.cs
  4. 99
      UOP1_Project/Assets/ScriptableObjects/Protagonist/CharacterTransitionTable.asset
  5. 8
      UOP1_Project/Assets/ScriptableObjects/Protagonist/CharacterTransitionTable.asset.meta
  6. 8
      UOP1_Project/Assets/Scripts/StateMachine/Editor.meta
  7. 95
      UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/TransitionTableSO.cs
  8. 11
      UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/TransitionTableSO.cs.meta
  9. 171
      UOP1_Project/Assets/Scripts/StateMachine/Editor/AddTransitionHelper.cs
  10. 11
      UOP1_Project/Assets/Scripts/StateMachine/Editor/AddTransitionHelper.cs.meta
  11. 50
      UOP1_Project/Assets/Scripts/StateMachine/Editor/ContentStyle.cs
  12. 11
      UOP1_Project/Assets/Scripts/StateMachine/Editor/ContentStyle.cs.meta
  13. 47
      UOP1_Project/Assets/Scripts/StateMachine/Editor/SerializedTransition.cs
  14. 11
      UOP1_Project/Assets/Scripts/StateMachine/Editor/SerializedTransition.cs.meta
  15. 83
      UOP1_Project/Assets/Scripts/StateMachine/Editor/StateEditor.cs
  16. 11
      UOP1_Project/Assets/Scripts/StateMachine/Editor/StateEditor.cs.meta
  17. 110
      UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionDisplayHelper.cs
  18. 11
      UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionDisplayHelper.cs.meta
  19. 379
      UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionTableEditor.cs
  20. 11
      UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionTableEditor.cs.meta
  21. 8
      UOP1_Project/Assets/ScriptableObjects/Protagonist/Transitions.meta

2
UOP1_Project/Assets/Prefabs/Pig.prefab


m_EditorClassIdentifier:
CurrentState:
debug: 0
_initialStateSO: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
_transitionTableSO: {fileID: 11400000, guid: f0baa4ca2bfa24e4cb6d1efe9fa81ea3, type: 2}
--- !u!1 &3791421269865525050
GameObject:
m_ObjectHideFlags: 0

5
UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachine.cs


public string CurrentState;
public bool debug;
#endif
[Tooltip("Set the initial state of this StateMachine")]
[SerializeField] private ScriptableObjects.StateSO _initialStateSO = null;
[SerializeField] private ScriptableObjects.TransitionTableSO _transitionTableSO = default;
private readonly Dictionary<Type, Component> _cachedComponents = new Dictionary<Type, Component>();
private State _currentState;

_currentState = _initialStateSO.GetState(this);
_currentState = _transitionTableSO.GetInitialState(this);
_currentState.OnStateEnter();
#if UNITY_EDITOR
CurrentState = _currentState.Name;

19
UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/StateSO.cs


public class StateSO : ScriptableObject
{
[SerializeField] private StateActionSO[] _actions = null;
[SerializeField] private StateTransitionSO[] _transitions = null;
public State GetState(StateMachine stateMachine)
{
return GetState(stateMachine, new Dictionary<ScriptableObject, object>());
}
internal State GetState(StateMachine stateMachine, Dictionary<ScriptableObject, object> createdInstances)
{

state.Name = name;
state._stateMachine = stateMachine;
state._transitions = GetTransitions(_transitions, stateMachine, createdInstances);
state._transitions = new StateTransition[0];
}
private static StateTransition[] GetTransitions(StateTransitionSO[] scriptableTransitions,
StateMachine stateMachine, Dictionary<ScriptableObject, object> createdInstances)
{
int count = scriptableTransitions.Length;
var transitions = new StateTransition[count];
for (int i = 0; i < count; i++)
transitions[i] = scriptableTransitions[i].GetTransition(stateMachine, createdInstances);
return transitions;
}
private static StateAction[] GetActions(StateActionSO[] scriptableActions,

99
UOP1_Project/Assets/ScriptableObjects/Protagonist/CharacterTransitionTable.asset


%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 872cbaa965d1f6e4e98365d74e2060df, type: 3}
m_Name: CharacterTransitionTable
m_EditorClassIdentifier:
_transitions:
- FromState: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
ToState: {fileID: 11400000, guid: 027d32476800b3543b2f5446a59054c8, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: a79b812272ab8314aa305b39f9a2740a, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: 0aa906b71b1b4504d950f5ee8e1f06e8, type: 2}
ToState: {fileID: 11400000, guid: c57ed1dc57a890943b2648ca8a581075, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: bf6a2f4642738ac4f8e84bcdf1cb5f93, type: 2}
Operator: 1
- ExpectedResult: 1
Condition: {fileID: 11400000, guid: 620f4efd93744084a8ac9ba272f093dc, type: 2}
Operator: 1
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: 41dbff2af0b6f7141a32bb464309ba69, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: c57ed1dc57a890943b2648ca8a581075, type: 2}
ToState: {fileID: 11400000, guid: 027d32476800b3543b2f5446a59054c8, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: c7258bdb2558a214f916c8b5b3c0fa1f, type: 2}
Operator: 0
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: a79b812272ab8314aa305b39f9a2740a, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: c57ed1dc57a890943b2648ca8a581075, type: 2}
ToState: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: c7258bdb2558a214f916c8b5b3c0fa1f, type: 2}
Operator: 0
- ExpectedResult: 1
Condition: {fileID: 11400000, guid: a79b812272ab8314aa305b39f9a2740a, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: 027d32476800b3543b2f5446a59054c8, type: 2}
ToState: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
Conditions:
- ExpectedResult: 1
Condition: {fileID: 11400000, guid: a79b812272ab8314aa305b39f9a2740a, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: 027d32476800b3543b2f5446a59054c8, type: 2}
ToState: {fileID: 11400000, guid: 0aa906b71b1b4504d950f5ee8e1f06e8, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: 620f4efd93744084a8ac9ba272f093dc, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: 027d32476800b3543b2f5446a59054c8, type: 2}
ToState: {fileID: 11400000, guid: 4c2c0dc4f7ee62b4d95b20ac3dce281e, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: c5090b215e4f1594f9d1ef6680cd15bb, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: 027d32476800b3543b2f5446a59054c8, type: 2}
ToState: {fileID: 11400000, guid: c57ed1dc57a890943b2648ca8a581075, type: 2}
Conditions:
- ExpectedResult: 1
Condition: {fileID: 11400000, guid: c7258bdb2558a214f916c8b5b3c0fa1f, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
ToState: {fileID: 11400000, guid: 4c2c0dc4f7ee62b4d95b20ac3dce281e, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: c5090b215e4f1594f9d1ef6680cd15bb, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
ToState: {fileID: 11400000, guid: 0aa906b71b1b4504d950f5ee8e1f06e8, type: 2}
Conditions:
- ExpectedResult: 0
Condition: {fileID: 11400000, guid: 620f4efd93744084a8ac9ba272f093dc, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
ToState: {fileID: 11400000, guid: c57ed1dc57a890943b2648ca8a581075, type: 2}
Conditions:
- ExpectedResult: 1
Condition: {fileID: 11400000, guid: c7258bdb2558a214f916c8b5b3c0fa1f, type: 2}
Operator: 0
- FromState: {fileID: 11400000, guid: 4c2c0dc4f7ee62b4d95b20ac3dce281e, type: 2}
ToState: {fileID: 11400000, guid: e128814ff6dbf63449bbc4dc8b6dc066, type: 2}
Conditions:
- ExpectedResult: 1
Condition: {fileID: 11400000, guid: c5090b215e4f1594f9d1ef6680cd15bb, type: 2}
Operator: 0

8
UOP1_Project/Assets/ScriptableObjects/Protagonist/CharacterTransitionTable.asset.meta


fileFormatVersion: 2
guid: f0baa4ca2bfa24e4cb6d1efe9fa81ea3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

8
UOP1_Project/Assets/Scripts/StateMachine/Editor.meta


fileFormatVersion: 2
guid: 3596ae92061fc0d4d9cd18d55258b236
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

95
UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/TransitionTableSO.cs


using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UOP1.StateMachine.ScriptableObjects
{
[CreateAssetMenu(fileName = "New Transition Table", menuName = "State Machines/Transition Table")]
public class TransitionTableSO : ScriptableObject
{
[SerializeField] private TransitionItem[] _transitions = default;
public State GetInitialState(StateMachine stateMachine)
{
var states = new List<State>();
var transitions = new List<StateTransition>();
var createdInstances = new Dictionary<ScriptableObject, object>();
var fromStates = _transitions.GroupBy(transition => transition.FromState);
foreach (var fromState in fromStates)
{
if (fromState.Key == null)
throw new ArgumentNullException(nameof(fromState.Key), $"TransitionTable: {name}");
var state = fromState.Key.GetState(stateMachine, createdInstances);
states.Add(state);
transitions.Clear();
foreach (var transitionItem in fromState)
{
if (transitionItem.ToState == null)
throw new ArgumentNullException(nameof(transitionItem.ToState), $"TransitionTable: {name}, From State: {fromState.Key.name}");
var toState = transitionItem.ToState.GetState(stateMachine, createdInstances);
ProcessConditionUsages(stateMachine, transitionItem.Conditions, createdInstances, out var conditions, out var resultGroups);
transitions.Add(new StateTransition(toState, conditions, resultGroups));
}
state._transitions = transitions.ToArray();
}
return states.Count > 0 ? states[0]
: throw new InvalidOperationException($"TransitionTable {name} is empty.");
}
private static void ProcessConditionUsages(
StateMachine stateMachine,
ConditionUsage[] conditionUsages,
Dictionary<ScriptableObject, object> createdInstances,
out StateCondition[] conditions,
out int[] resultGroups)
{
int count = conditionUsages.Length;
conditions = new StateCondition[count];
for (int i = 0; i < count; i++)
conditions[i] = conditionUsages[i].Condition.GetCondition(
stateMachine, conditionUsages[i].ExpectedResult == Result.True, createdInstances);
List<int> resultGroupsList = new List<int>();
for (int i = 0; i < count; i++)
{
int idx = resultGroupsList.Count;
resultGroupsList.Add(1);
while (i < count - 1 && conditionUsages[i].Operator == Operator.And)
{
i++;
resultGroupsList[idx]++;
}
}
resultGroups = resultGroupsList.ToArray();
}
[Serializable]
public struct TransitionItem
{
public StateSO FromState;
public StateSO ToState;
public ConditionUsage[] Conditions;
}
[Serializable]
public struct ConditionUsage
{
public Result ExpectedResult;
public StateConditionSO Condition;
public Operator Operator;
}
public enum Result { True, False }
public enum Operator { And, Or }
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/TransitionTableSO.cs.meta


fileFormatVersion: 2
guid: 872cbaa965d1f6e4e98365d74e2060df
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

171
UOP1_Project/Assets/Scripts/StateMachine/Editor/AddTransitionHelper.cs


using System;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UOP1.StateMachine.ScriptableObjects;
using static UnityEditor.EditorGUILayout;
namespace UOP1.StateMachine.Editor
{
internal class AddTransitionHelper : IDisposable
{
internal SerializedTransition SerializedTransition { get; }
private readonly SerializedObject _transition;
private readonly ReorderableList _list;
private readonly TransitionTableEditor _editor;
private bool _toggle = false;
internal AddTransitionHelper(TransitionTableEditor editor)
{
_editor = editor;
_transition = new SerializedObject(ScriptableObject.CreateInstance<TransitionItemSO>());
SerializedTransition = new SerializedTransition(_transition.FindProperty("Item"));
_list = new ReorderableList(_transition, SerializedTransition.Conditions);
SetupConditionsList(_list);
}
internal void Display()
{
// Display add button only if not already adding a transition
if (!_toggle)
{
if (GUILayout.Button(EditorGUIUtility.IconContent("Toolbar Plus"), GUILayout.Width(35)))
{
_toggle = true;
SerializedTransition.ClearProperties();
}
if (!_toggle)
return;
}
var rect = BeginVertical();
rect.x += 45;
rect.width -= 40;
EditorGUI.DrawRect(rect, ContentStyle.LightGray);
Separator();
// State Fields
BeginHorizontal();
{
Space(50, false);
StatePropField("From", SerializedTransition.FromState);
Space(10, false);
StatePropField("To", SerializedTransition.ToState);
}
EndHorizontal();
// Conditions List
BeginHorizontal();
{
Space(50, false);
BeginVertical();
_list.DoLayoutList();
EndVertical();
}
EndHorizontal();
Separator();
// Add and cancel buttons
BeginHorizontal();
{
Space(50, false);
if (GUILayout.Button("Add Transition"))
{
if (SerializedTransition.FromState.objectReferenceValue == null)
Debug.LogException(new ArgumentNullException("FromState"));
else if (SerializedTransition.ToState.objectReferenceValue == null)
Debug.LogException(new ArgumentNullException("ToState"));
else if (SerializedTransition.FromState.objectReferenceValue == SerializedTransition.ToState.objectReferenceValue)
Debug.LogException(new InvalidOperationException("FromState and ToState are the same."));
else
{
_editor.AddTransition(SerializedTransition);
_toggle = false;
}
}
else if (GUILayout.Button("Cancel"))
{
_toggle = false;
}
}
EndHorizontal();
EndVertical();
void StatePropField(string label, SerializedProperty prop)
{
BeginVertical();
LabelField(label);
BeginHorizontal();
Space(20, false);
PropertyField(prop, GUIContent.none, GUILayout.MaxWidth(180));
EndHorizontal();
EndVertical();
}
}
public void Dispose()
{
UnityEngine.Object.DestroyImmediate(_transition.targetObject);
_transition.Dispose();
GC.SuppressFinalize(this);
}
private static void SetupConditionsList(ReorderableList reorderableList)
{
reorderableList.elementHeight *= 2.3f;
reorderableList.drawHeaderCallback += rect => GUI.Label(rect, "Conditions");
reorderableList.onAddCallback += list =>
{
int count = list.count;
list.serializedProperty.InsertArrayElementAtIndex(count);
var prop = list.serializedProperty.GetArrayElementAtIndex(count);
prop.FindPropertyRelative("Condition").objectReferenceValue = null;
prop.FindPropertyRelative("ExpectedResult").enumValueIndex = 0;
prop.FindPropertyRelative("Operator").enumValueIndex = 0;
};
reorderableList.drawElementCallback += (Rect rect, int index, bool isActive, bool isFocused) =>
{
var prop = reorderableList.serializedProperty.GetArrayElementAtIndex(index);
rect = new Rect(rect.x, rect.y + 2.5f, rect.width, EditorGUIUtility.singleLineHeight);
var condition = prop.FindPropertyRelative("Condition");
if (condition.objectReferenceValue != null)
{
string label = condition.objectReferenceValue.name;
GUI.Label(rect, "If");
GUI.Label(new Rect(rect.x + 20, rect.y, rect.width, rect.height), label, EditorStyles.boldLabel);
EditorGUI.PropertyField(new Rect(rect.x + rect.width - 180, rect.y, 20, rect.height), condition, GUIContent.none);
}
else
{
EditorGUI.PropertyField(new Rect(rect.x, rect.y, 150, rect.height), condition, GUIContent.none);
}
EditorGUI.LabelField(new Rect(rect.x + rect.width - 120, rect.y, 20, rect.height), "Is");
EditorGUI.PropertyField(new Rect(rect.x + rect.width - 60, rect.y, 60, rect.height), prop.FindPropertyRelative("ExpectedResult"), GUIContent.none);
EditorGUI.PropertyField(new Rect(rect.x + 20, rect.y + EditorGUIUtility.singleLineHeight + 5, 60, rect.height), prop.FindPropertyRelative("Operator"), GUIContent.none);
};
reorderableList.onChangedCallback += list => reorderableList.serializedProperty.serializedObject.ApplyModifiedProperties();
reorderableList.drawElementBackgroundCallback += (Rect rect, int index, bool isActive, bool isFocused) =>
{
if (isFocused)
EditorGUI.DrawRect(rect, ContentStyle.Focused);
if (index % 2 != 0)
EditorGUI.DrawRect(rect, ContentStyle.ZebraDark);
else
EditorGUI.DrawRect(rect, ContentStyle.ZebraLight);
};
}
// SO to serialize a TransitionItem
internal class TransitionItemSO : ScriptableObject
{
public TransitionTableSO.TransitionItem Item = default;
}
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/Editor/AddTransitionHelper.cs.meta


fileFormatVersion: 2
guid: 00a48f7c82ea3bd46a36c3f64d759837
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

50
UOP1_Project/Assets/Scripts/StateMachine/Editor/ContentStyle.cs


using UnityEditor;
using UnityEngine;
namespace UOP1.StateMachine.Editor
{
internal static class ContentStyle
{
internal static Color DarkGray { get; private set; }
internal static Color LightGray { get; private set; }
internal static Color Focused { get; private set; }
internal static Color ZebraDark { get; private set; }
internal static Color ZebraLight { get; private set; }
internal static RectOffset Padding { get; private set; }
internal static RectOffset Margin { get; private set; }
internal static GUIStyle BoldCentered { get; private set; }
internal static GUIStyle StateListStyle { get; private set; }
internal static GUIStyle WithPadding { get; private set; }
internal static GUIStyle WithPaddingAndMargins { get; private set; }
private static bool _initialised = false;
[InitializeOnLoadMethod]
internal static void Initialize()
{
if (_initialised)
return;
_initialised = true;
DarkGray = new Color(0.7f, 0.7f, 0.7f);
LightGray = new Color(0.8f, 0.8f, 0.8f);
ZebraDark = new Color(0.1f, 0.5f, 0.9f, 0.1f);
ZebraLight = new Color(0.8f, 0.8f, 0.9f, 0.1f);
Focused = new Color(0.5f, 0.5f, 0.5f, 0.5f);
Padding = new RectOffset(5, 5, 5, 5);
Margin = new RectOffset(8, 8, 8, 8);
WithPadding = new GUIStyle { padding = Padding };
WithPaddingAndMargins = new GUIStyle { padding = Padding, margin = Margin };
BoldCentered = new GUIStyle { fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleCenter };
StateListStyle = new GUIStyle
{
alignment = TextAnchor.MiddleCenter,
fontStyle = FontStyle.Bold,
fontSize = 12,
margin = Margin
};
}
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/Editor/ContentStyle.cs.meta


fileFormatVersion: 2
guid: f3ccde57d7ba058488ea2c2a47ce27a9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

47
UOP1_Project/Assets/Scripts/StateMachine/Editor/SerializedTransition.cs


using UnityEditor;
namespace UOP1.StateMachine.Editor
{
internal readonly struct SerializedTransition
{
internal readonly SerializedProperty Transition;
internal readonly SerializedProperty FromState;
internal readonly SerializedProperty ToState;
internal readonly SerializedProperty Conditions;
internal readonly int Index;
internal SerializedTransition(SerializedProperty transition)
{
Transition = transition;
FromState = Transition.FindPropertyRelative("FromState");
ToState = Transition.FindPropertyRelative("ToState");
Conditions = Transition.FindPropertyRelative("Conditions");
Index = -1;
}
internal SerializedTransition(SerializedObject transitionTable, int index)
{
Transition = transitionTable.FindProperty("_transitions").GetArrayElementAtIndex(index);
FromState = Transition.FindPropertyRelative("FromState");
ToState = Transition.FindPropertyRelative("ToState");
Conditions = Transition.FindPropertyRelative("Conditions");
Index = index;
}
internal SerializedTransition(SerializedProperty transition, int index)
{
Transition = transition.GetArrayElementAtIndex(index);
FromState = Transition.FindPropertyRelative("FromState");
ToState = Transition.FindPropertyRelative("ToState");
Conditions = Transition.FindPropertyRelative("Conditions");
Index = index;
}
internal void ClearProperties()
{
FromState.objectReferenceValue = null;
ToState.objectReferenceValue = null;
Conditions.ClearArray();
}
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/Editor/SerializedTransition.cs.meta


fileFormatVersion: 2
guid: 6441ed07e21d7e4429c67fddfc22d061
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

83
UOP1_Project/Assets/Scripts/StateMachine/Editor/StateEditor.cs


using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UOP1.StateMachine.ScriptableObjects;
namespace UOP1.StateMachine.Editor
{
[CustomEditor(typeof(StateSO))]
public class StateEditor : UnityEditor.Editor
{
private ReorderableList _list;
private SerializedProperty _actions;
private void OnEnable()
{
Undo.undoRedoPerformed += DoUndo;
_actions = serializedObject.FindProperty("_actions");
_list = new ReorderableList(serializedObject, _actions, true, true, true, true);
SetupActionsList(_list);
}
private void OnDisable()
{
Undo.undoRedoPerformed -= DoUndo;
}
public override void OnInspectorGUI()
{
_list.DoLayoutList();
}
private void DoUndo()
{
serializedObject.UpdateIfRequiredOrScript();
}
private static void SetupActionsList(ReorderableList reorderableList)
{
reorderableList.elementHeight *= 1.5f;
reorderableList.drawHeaderCallback += rect => GUI.Label(rect, "Actions");
reorderableList.onAddCallback += list =>
{
int count = list.count;
list.serializedProperty.InsertArrayElementAtIndex(count);
var prop = list.serializedProperty.GetArrayElementAtIndex(count);
prop.objectReferenceValue = null;
};
reorderableList.drawElementCallback += (Rect rect, int index, bool isActive, bool isFocused) =>
{
var r = rect;
r.height = EditorGUIUtility.singleLineHeight;
r.y += 5;
r.x += 5;
var prop = reorderableList.serializedProperty.GetArrayElementAtIndex(index);
if (prop.objectReferenceValue != null)
{
var label = prop.objectReferenceValue.name;
r.width = 20;
EditorGUI.PropertyField(r, prop, GUIContent.none);
r.width = rect.width - 50;
r.x += 25;
GUI.Label(r, label, EditorStyles.boldLabel);
}
else
EditorGUI.PropertyField(r, prop, GUIContent.none);
};
reorderableList.onChangedCallback += list => list.serializedProperty.serializedObject.ApplyModifiedProperties();
reorderableList.drawElementBackgroundCallback += (Rect rect, int index, bool isActive, bool isFocused) =>
{
if (isFocused)
EditorGUI.DrawRect(rect, ContentStyle.Focused);
if (index % 2 != 0)
EditorGUI.DrawRect(rect, ContentStyle.ZebraDark);
else
EditorGUI.DrawRect(rect, ContentStyle.ZebraLight);
};
}
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/Editor/StateEditor.cs.meta


fileFormatVersion: 2
guid: 2aa6909dd9f17914ab9fcb1dd55e71a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

110
UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionDisplayHelper.cs


using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace UOP1.StateMachine.Editor
{
internal class TransitionDisplayHelper
{
internal SerializedTransition SerializedTransition { get; }
private readonly ReorderableList _reorderableList;
private readonly TransitionTableEditor _editor;
internal TransitionDisplayHelper(SerializedTransition serializedTransition, TransitionTableEditor editor)
{
SerializedTransition = serializedTransition;
_reorderableList = new ReorderableList(SerializedTransition.Transition.serializedObject, SerializedTransition.Conditions, true, false, true, true);
SetupConditionsList(_reorderableList);
_editor = editor;
}
internal bool Display()
{
// Transition Header
EditorGUI.DrawRect(EditorGUILayout.BeginHorizontal(), ContentStyle.DarkGray);
{
// Target state
EditorGUILayout.LabelField("To", GUILayout.Width(20));
EditorGUILayout.LabelField(SerializedTransition.ToState.objectReferenceValue.name, EditorStyles.boldLabel);
// Move transition up
if (GUILayout.Button(EditorGUIUtility.IconContent("scrollup"), GUILayout.Width(35), GUILayout.Height(16)))
{
if (_editor.ReorderTransition(SerializedTransition, true))
return true;
}
// Move transition down
if (GUILayout.Button(EditorGUIUtility.IconContent("scrolldown"), GUILayout.Width(35), GUILayout.Height(16)))
{
if (_editor.ReorderTransition(SerializedTransition, false))
return true;
}
// Remove transition
if (GUILayout.Button(EditorGUIUtility.IconContent("Toolbar Minus"), GUILayout.Width(35), GUILayout.Height(16)))
{
_editor.RemoveTransition(SerializedTransition.Index);
return true;
}
}
EditorGUILayout.EndHorizontal();
// Conditions
_reorderableList.DoLayoutList();
return false;
}
private static void SetupConditionsList(ReorderableList reorderableList)
{
reorderableList.elementHeight *= 2.3f;
reorderableList.headerHeight = 1f;
reorderableList.onAddCallback += list =>
{
int count = list.count;
list.serializedProperty.InsertArrayElementAtIndex(count);
var prop = list.serializedProperty.GetArrayElementAtIndex(count);
prop.FindPropertyRelative("Condition").objectReferenceValue = null;
prop.FindPropertyRelative("ExpectedResult").enumValueIndex = 0;
prop.FindPropertyRelative("Operator").enumValueIndex = 0;
};
reorderableList.drawElementCallback += (Rect rect, int index, bool isActive, bool isFocused) =>
{
var prop = reorderableList.serializedProperty.GetArrayElementAtIndex(index);
rect = new Rect(rect.x, rect.y + 2.5f, rect.width, EditorGUIUtility.singleLineHeight);
var condition = prop.FindPropertyRelative("Condition");
if (condition.objectReferenceValue != null)
{
string label = condition.objectReferenceValue.name;
GUI.Label(rect, "If");
var r = rect;
r.x += 20;
r.width = 20;
EditorGUI.PropertyField(r, condition, GUIContent.none);
r.x += 25;
r.width = rect.width;
GUI.Label(r, label, EditorStyles.boldLabel);
}
else
{
EditorGUI.PropertyField(new Rect(rect.x, rect.y, 150, rect.height), condition, GUIContent.none);
}
EditorGUI.LabelField(new Rect(rect.x + rect.width - 120, rect.y, 20, rect.height), "Is");
EditorGUI.PropertyField(new Rect(rect.x + rect.width - 60, rect.y, 60, rect.height), prop.FindPropertyRelative("ExpectedResult"), GUIContent.none);
EditorGUI.PropertyField(new Rect(rect.x + 20, rect.y + EditorGUIUtility.singleLineHeight + 5, 60, rect.height), prop.FindPropertyRelative("Operator"), GUIContent.none);
};
reorderableList.onChangedCallback += list => list.serializedProperty.serializedObject.ApplyModifiedProperties();
reorderableList.drawElementBackgroundCallback += (Rect rect, int index, bool isActive, bool isFocused) =>
{
if (isFocused)
EditorGUI.DrawRect(rect, ContentStyle.Focused);
if (index % 2 != 0)
EditorGUI.DrawRect(rect, ContentStyle.ZebraDark);
else
EditorGUI.DrawRect(rect, ContentStyle.ZebraLight);
};
}
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionDisplayHelper.cs.meta


fileFormatVersion: 2
guid: 3ec49868c2508ae4199e341bcbba3675
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

379
UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionTableEditor.cs


using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UOP1.StateMachine.ScriptableObjects;
using static UnityEditor.EditorGUILayout;
using Object = UnityEngine.Object;
namespace UOP1.StateMachine.Editor
{
[CustomEditor(typeof(TransitionTableSO))]
internal class TransitionTableEditor : UnityEditor.Editor
{
// Property with all the transitions.
private SerializedProperty _transitions;
// _fromStates and _transitionsByFromStates form an Object->Transitions dictionary.
private List<Object> _fromStates;
private List<List<TransitionDisplayHelper>> _transitionsByFromStates;
// _toggles for the opened states. Only one should be active at a time.
private bool[] _toggles;
// Helper class to add new transitions.
private AddTransitionHelper _addTransitionHelper;
// Editor to display the StateSO inspector.
private UnityEditor.Editor _cachedStateEditor;
private bool _displayStateEditor;
private void OnEnable()
{
_addTransitionHelper = new AddTransitionHelper(this);
Undo.undoRedoPerformed += ResetIfRequired;
Reset();
}
private void OnDisable()
{
Undo.undoRedoPerformed -= ResetIfRequired;
_addTransitionHelper?.Dispose();
}
/// <summary>
/// Method to fully reset the editor. Used whenever adding, removing and reordering transitions.
/// </summary>
internal void Reset()
{
_transitions = serializedObject.FindProperty("_transitions");
GroupByFromState();
_toggles = new bool[_fromStates.Count];
}
public override void OnInspectorGUI()
{
if (!_displayStateEditor)
TransitionTableGUI();
else
StateEditorGUI();
}
private void StateEditorGUI()
{
if (GUILayout.Button(EditorGUIUtility.IconContent("scrollleft"), GUILayout.Width(35), GUILayout.Height(20)))
_displayStateEditor = false;
EditorGUILayout.LabelField(_cachedStateEditor.target.name, EditorStyles.boldLabel);
_cachedStateEditor.OnInspectorGUI();
}
private void TransitionTableGUI()
{
serializedObject.UpdateIfRequiredOrScript();
// For each fromState
for (int i = 0; i < _fromStates.Count; i++)
{
EditorGUI.DrawRect(BeginVertical(ContentStyle.WithPaddingAndMargins), ContentStyle.LightGray);
var transitions = _transitionsByFromStates[i];
// State Header
BeginHorizontal();
{
BeginVertical();
string label = transitions[0].SerializedTransition.FromState.objectReferenceValue.name;
if (i == 0)
label += " (Initial State)";
// State toggle
_toggles[i] = BeginFoldoutHeaderGroup(
foldout: _toggles[i],
content: label,
style: ContentStyle.StateListStyle);
Separator();
EndVertical();
// State Header Buttons
{
bool Button(string icon) => GUILayout.Button(EditorGUIUtility.IconContent(icon), GUILayout.Width(35), GUILayout.Height(20));
// Switch to state editor
if (Button("SceneViewTools"))
{
if (_cachedStateEditor == null)
_cachedStateEditor = CreateEditor(transitions[0].SerializedTransition.FromState.objectReferenceValue, typeof(StateEditor));
else
CreateCachedEditor(transitions[0].SerializedTransition.FromState.objectReferenceValue, typeof(StateEditor), ref _cachedStateEditor);
_displayStateEditor = true;
return;
}
// Move state up
if (Button("scrollup"))
{
if (ReorderState(i, true))
return;
}
// Move state down
if (Button("scrolldown"))
{
if (ReorderState(i, false))
return;
}
}
}
EndHorizontal();
// If state is open
if (_toggles[i])
{
DisableAllStateTogglesExcept(i);
EditorGUI.BeginChangeCheck();
// Display all the transitions in the state
foreach (var transition in transitions)
{
if (transition.Display())
return;
Separator();
}
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
}
EndFoldoutHeaderGroup();
EndVertical();
GUILayout.HorizontalSlider(0, 0, 0);
Separator();
Separator();
}
var rect = BeginHorizontal();
Space(rect.width - 55);
// Display add transition button
_addTransitionHelper.Display();
EndHorizontal();
}
/// <summary>
/// Move a state up or down
/// </summary>
/// <param name="index">Index of the state in _fromStates</param>
/// <param name="up">Moving up(true) or down(true)</param>
/// <returns>True if changes were made and returning is required. Otherwise false.</returns>
internal bool ReorderState(int index, bool up)
{
if ((up && index == 0) || (!up && index == _fromStates.Count - 1))
return false;
// Moving a state up is easier than moving it down. So when moving a state down, we instead move the next state up.
MoveStateUp(up ? index : index + 1);
return true;
}
private void MoveStateUp(int index)
{
var transitions = _transitionsByFromStates[index];
int transitionIndex = transitions[0].SerializedTransition.Index;
int targetIndex = _transitionsByFromStates[index - 1][0].SerializedTransition.Index;
_transitions.MoveArrayElement(transitionIndex, targetIndex);
serializedObject.ApplyModifiedProperties();
Reset();
}
/// <summary>
/// Add a new transition. If a transition with the same from and to states is found,
/// the conditions in the new transition are added to it.
/// </summary>
/// <param name="source">Source Transition</param>
internal void AddTransition(SerializedTransition source)
{
SerializedTransition transition;
if (TryGetExistingTransition(source.FromState, source.ToState, out int fromIndex, out int toIndex))
{
transition = _transitionsByFromStates[fromIndex][toIndex].SerializedTransition;
}
else
{
int count = _transitions.arraySize;
_transitions.InsertArrayElementAtIndex(count);
transition = new SerializedTransition(_transitions.GetArrayElementAtIndex(count));
transition.ClearProperties();
transition.FromState.objectReferenceValue = source.FromState.objectReferenceValue;
transition.ToState.objectReferenceValue = source.ToState.objectReferenceValue;
}
CopyConditions(transition.Conditions, source.Conditions);
serializedObject.ApplyModifiedProperties();
Reset();
_toggles[fromIndex >= 0 ? fromIndex : _toggles.Length - 1] = true;
}
/// <summary>
/// Move a transition up or down
/// </summary>
/// <param name="serializedTransition">The transition to move</param>
/// <param name="up">Move up(true) or down(false)</param>
/// <returns>True if changes were made and returning is required. Otherwise false.</returns>
internal bool ReorderTransition(SerializedTransition serializedTransition, bool up)
{
int targetIndex = -1;
int fromId = serializedTransition.FromState.objectReferenceInstanceIDValue;
SerializedTransition st;
for (int i = 0; i < _transitions.arraySize; i++)
{
if (up && i >= serializedTransition.Index)
break;
if (!up && i <= serializedTransition.Index)
continue;
st = new SerializedTransition(_transitions, i);
if (st.FromState.objectReferenceInstanceIDValue != fromId)
continue;
targetIndex = i;
if (!up)
break;
}
if (targetIndex == -1)
return false;
_transitions.MoveArrayElement(serializedTransition.Index, targetIndex);
serializedObject.ApplyModifiedProperties();
Reset();
_toggles[
_fromStates.IndexOf(
_transitions.GetArrayElementAtIndex(targetIndex)
.FindPropertyRelative("FromState")
.objectReferenceValue)] = true;
return true;
}
/// <summary>
/// Remove a transition by index.
/// </summary>
/// <param name="index">Index of the transition in the transition table</param>
internal void RemoveTransition(int index)
{
var state = _transitions.GetArrayElementAtIndex(index).FindPropertyRelative("FromState").objectReferenceValue;
_transitions.DeleteArrayElementAtIndex(index);
bool toggle = DoToggleAndSort(state, index);
serializedObject.ApplyModifiedProperties();
Reset();
if (toggle)
{
int i = _fromStates.IndexOf(state);
if (i >= 0)
_toggles[i] = true;
}
}
private bool DoToggleAndSort(Object state, int index)
{
bool ret = false;
for (int i = 0; i < _transitions.arraySize; i++)
{
if (_transitions.GetArrayElementAtIndex(i).FindPropertyRelative("FromState").objectReferenceValue == state)
{
ret = true;
if (i > index)
{
_transitions.MoveArrayElement(i, index);
break;
}
}
}
return ret;
}
private void DisableAllStateTogglesExcept(int index)
{
for (int i = 0; i < _toggles.Length; i++)
if (i != index)
_toggles[i] = false;
}
private void CopyConditions(SerializedProperty copyTo, SerializedProperty copyFrom)
{
for (int i = 0, j = copyTo.arraySize; i < copyFrom.arraySize; i++, j++)
{
copyTo.InsertArrayElementAtIndex(j);
var cond = copyTo.GetArrayElementAtIndex(j);
var srcCond = copyFrom.GetArrayElementAtIndex(i);
cond.FindPropertyRelative("ExpectedResult").enumValueIndex = srcCond.FindPropertyRelative("ExpectedResult").enumValueIndex;
cond.FindPropertyRelative("Operator").enumValueIndex = srcCond.FindPropertyRelative("Operator").enumValueIndex;
cond.FindPropertyRelative("Condition").objectReferenceValue = srcCond.FindPropertyRelative("Condition").objectReferenceValue;
}
}
private bool TryGetExistingTransition(SerializedProperty from, SerializedProperty to, out int fromIndex, out int toIndex)
{
fromIndex = _fromStates.IndexOf(from.objectReferenceValue);
if (fromIndex < 0)
{
toIndex = -1;
return false;
}
toIndex = _transitionsByFromStates[fromIndex].FindIndex(
transitionHelper => transitionHelper.SerializedTransition.ToState.objectReferenceValue == to.objectReferenceValue);
return toIndex >= 0;
}
private void ResetIfRequired()
{
if (serializedObject.UpdateIfRequiredOrScript())
Reset();
}
private void GroupByFromState()
{
var groupedTransitions = new Dictionary<Object, List<TransitionDisplayHelper>>();
int count = _transitions.arraySize;
for (int i = 0; i < count; i++)
{
var serializedTransition = new SerializedTransition(_transitions, i);
if (serializedTransition.FromState.objectReferenceValue == null)
{
Debug.LogError("Transition with invalid state found in table " + serializedObject.targetObject.name + ", deleting...");
_transitions.DeleteArrayElementAtIndex(i);
serializedObject.ApplyModifiedProperties();
Reset();
return;
}
if (serializedTransition.FromState.objectReferenceValue == null)
{
Debug.LogError("Transition with invalid state found in table " + serializedObject.targetObject.name + ", deleting...");
_transitions.DeleteArrayElementAtIndex(i);
serializedObject.ApplyModifiedProperties();
Reset();
return;
}
if (!groupedTransitions.TryGetValue(serializedTransition.FromState.objectReferenceValue, out var groupedProps))
{
groupedProps = new List<TransitionDisplayHelper>();
groupedTransitions.Add(serializedTransition.FromState.objectReferenceValue, groupedProps);
}
groupedProps.Add(new TransitionDisplayHelper(serializedTransition, this));
}
_fromStates = groupedTransitions.Keys.Distinct().ToList();
_transitionsByFromStates = new List<List<TransitionDisplayHelper>>();
foreach (var fromState in _fromStates)
_transitionsByFromStates.Add(groupedTransitions[fromState]);
}
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionTableEditor.cs.meta


fileFormatVersion: 2
guid: 596d4a27cc6cd7f478e7912eb0488033
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
UOP1_Project/Assets/ScriptableObjects/Protagonist/Transitions.meta


fileFormatVersion: 2
guid: 7273e93cd813a8947b64537b38daed30
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存