浏览代码

State Machine Improvements (#314)

* Changes to Action and Condition script templates

- Uncommented `OnStateEnter` and `OnStateExit`
- Added property to cast `OriginSO` to the derived class

* Minor changes

- Moved call to method `OnStateEnter` of the initial State from `Awake` to `Start`
- Added summary for `OriginSO` properties
- Added buttons to edit 'To States' in the Transition Table Editor, just like with 'From States'

* [Bot] Automated dotnet-format update

* Fixed reordering transitions

Fixed a bug that was causing states to go out of order when reordering transitions.

* TransitionTableEditor code cleanup and visual improvements

- States and transitions will only show the up/down buttons when they can move up or down (first item can't move up, last item can't move down).
- Changed zebra colours to light grey/lighter grey.
- The state of the window is better preserved to not close an opened state after performing sorting, removing, and/or undo opera...
/release
GitHub 3 年前
当前提交
32df5510
共有 10 个文件被更改,包括 208 次插入178 次删除
  1. 4
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateAction.cs
  2. 4
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateCondition.cs
  3. 6
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachine.cs
  4. 16
      UOP1_Project/Assets/Scripts/StateMachine/Editor/Templates/StateAction.txt
  5. 16
      UOP1_Project/Assets/Scripts/StateMachine/Editor/Templates/StateCondition.txt
  6. 255
      UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionTableEditor.cs
  7. 19
      UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionTableEditorWindow.cs
  8. 10
      UOP1_Project/Assets/Scripts/StateMachine/Editor/Utilities/AddTransitionHelper.cs
  9. 4
      UOP1_Project/Assets/Scripts/StateMachine/Editor/Utilities/ContentStyle.cs
  10. 52
      UOP1_Project/Assets/Scripts/StateMachine/Editor/Utilities/TransitionDisplayHelper.cs

4
UOP1_Project/Assets/Scripts/StateMachine/Core/StateAction.cs


public abstract class StateAction : IStateComponent
{
internal StateActionSO _originSO;
/// <summary>
/// Use this property to access shared data from the <see cref="StateActionSO"/> that corresponds to this <see cref="StateAction"/>
/// </summary>
protected StateActionSO OriginSO => _originSO;
/// <summary>

4
UOP1_Project/Assets/Scripts/StateMachine/Core/StateCondition.cs


private bool _isCached = false;
private bool _cachedStatement = default;
internal StateConditionSO _originSO;
/// <summary>
/// Use this property to access shared data from the <see cref="StateConditionSO"/> that corresponds to this <see cref="Condition"/>
/// </summary>
protected StateConditionSO OriginSO => _originSO;
/// <summary>

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


private void Awake()
{
_currentState = _transitionTableSO.GetInitialState(this);
_currentState.OnStateEnter();
}
private void Start()
{
_currentState.OnStateEnter();
}
public new bool TryGetComponent<T>(out T component) where T : Component

16
UOP1_Project/Assets/Scripts/StateMachine/Editor/Templates/StateAction.txt


public class #RUNTIMENAME# : StateAction
{
protected new #SCRIPTNAME# OriginSO => (#SCRIPTNAME#)base.OriginSO;
// public override void OnStateEnter()
// {
// }
public override void OnStateEnter()
{
}
// public override void OnStateExit()
// {
// }
public override void OnStateExit()
{
}
}

16
UOP1_Project/Assets/Scripts/StateMachine/Editor/Templates/StateCondition.txt


public class #RUNTIMENAME# : Condition
{
protected new #SCRIPTNAME# OriginSO => (#SCRIPTNAME#)base.OriginSO;
// public override void OnStateEnter()
// {
// }
public override void OnStateEnter()
{
}
// public override void OnStateExit()
// {
// }
public override void OnStateExit()
{
}
}

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


// Property with all the transitions.
private SerializedProperty _transitions;
// _fromStates and _transitionsByFromStates form an Object->Transitions dictionary.
// _fromStates and _transitionsByFromStates form a State->Transitions dictionary.
// _toggles for the opened states. Only one should be active at a time.
private bool[] _toggles;
// Index of the state currently toggled on, -1 if none is.
internal int _toggledIndex = -1;
// Helper class to add new transitions.
private AddTransitionHelper _addTransitionHelper;

private void OnEnable()
{
_addTransitionHelper = new AddTransitionHelper(this);
Undo.undoRedoPerformed += ResetIfRequired;
Undo.undoRedoPerformed += Reset;
Undo.undoRedoPerformed -= ResetIfRequired;
Undo.undoRedoPerformed -= Reset;
_addTransitionHelper?.Dispose();
}

internal void Reset()
{
serializedObject.Update();
var toggledState = _toggledIndex > -1 ? _fromStates[_toggledIndex] : null;
_toggles = new bool[_fromStates.Count];
_toggledIndex = toggledState ? _fromStates.IndexOf(toggledState) : -1;
}
public override void OnInspectorGUI()

Separator();
// Back button
if (GUILayout.Button(EditorGUIUtility.IconContent("scrollleft"), GUILayout.Width(35), GUILayout.Height(20)))
if (GUILayout.Button(EditorGUIUtility.IconContent("scrollleft"), GUILayout.Width(35), GUILayout.Height(20))
|| _cachedStateEditor.serializedObject == null)
{
return;
}
Separator();

Separator();
EditorGUILayout.HelpBox("Click on any State's name to see the Transitions it contains, or click the Pencil/Wrench icon to see its Actions.", MessageType.Info);
Separator();
serializedObject.UpdateIfRequiredOrScript();
// For each fromState
for (int i = 0; i < _fromStates.Count; i++)

{
var toggleRect = headerRect;
toggleRect.width -= 140;
_toggles[i] = EditorGUI.BeginFoldoutHeaderGroup(toggleRect,
foldout: _toggles[i],
content: label,
style: ContentStyle.StateListStyle);
_toggledIndex =
EditorGUI.BeginFoldoutHeaderGroup(toggleRect, _toggledIndex == i, label, ContentStyle.StateListStyle) ?
i : _toggledIndex == i ? -1 : _toggledIndex;
}
Separator();

{
bool Button(Rect position, string icon) => GUI.Button(position, EditorGUIUtility.IconContent(icon));
var buttonRect = new Rect(
x: headerRect.width - 105,
y: headerRect.y,
width: 35,
height: 20);
var buttonRect = new Rect(x: headerRect.width - 25, y: headerRect.y, width: 35, height: 20);
// Switch to state editor
if (Button(buttonRect, "SceneViewTools"))
// Move state down
if (i < _fromStates.Count - 1)
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;
if (Button(buttonRect, "scrolldown"))
{
ReorderState(i, false);
EarlyOut();
return;
}
buttonRect.x -= 40;
buttonRect.x += 40;
if (Button(buttonRect, "scrollup"))
if (i > 0)
if (ReorderState(i, true))
if (Button(buttonRect, "scrollup"))
{
ReorderState(i, true);
EarlyOut();
}
buttonRect.x -= 40;
buttonRect.x += 40;
// Move state down
if (Button(buttonRect, "scrolldown"))
// Switch to state editor
if (Button(buttonRect, "SceneViewTools"))
if (ReorderState(i, false))
return;
DisplayStateEditor(transitions[0].SerializedTransition.FromState.objectReferenceValue);
EarlyOut();
return;
}
void EarlyOut()
{
EndHorizontal();
EndFoldoutHeaderGroup();
EndVertical();
EndHorizontal();
// If state is open
if (_toggles[i])
if (_toggledIndex == i)
DisableAllStateTogglesExcept(i);
// Display all the transitions in the state
foreach (var transition in transitions)
foreach (var transition in transitions) // Display all the transitions in the state
if (transition.Display(ref stateRect))
if (transition.Display(ref stateRect)) // Return if there were changes
{
EditorGUI.EndChangeCheck();
EndFoldoutHeaderGroup();
EndVertical();
EndHorizontal();
}
Separator();
}
if (EditorGUI.EndChangeCheck())

EndFoldoutHeaderGroup();
EndVertical();
//GUILayout.HorizontalSlider(0, 0, 0);
Separator();
}

EndHorizontal();
}
internal void DisplayStateEditor(Object state)
{
if (_cachedStateEditor == null)
_cachedStateEditor = CreateEditor(state, typeof(StateEditor));
else
CreateCachedEditor(state, typeof(StateEditor), ref _cachedStateEditor);
_displayStateEditor = true;
}
/// <returns>True if changes were made and returning is required. Otherwise false.</returns>
internal bool ReorderState(int index, bool up)
internal void ReorderState(int index, bool up)
if ((up && index == 0) || (!up && index == _fromStates.Count - 1))
return false;
var toggledState = _toggledIndex > -1 ? _fromStates[_toggledIndex] : null;
// 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;
}
if (!up)
index++;
private void MoveStateUp(int index)
{
serializedObject.ApplyModifiedProperties();
Reset();
ApplyModifications($"Moved {_fromStates[index].name} State {(up ? "up" : "down")}");
if (toggledState)
_toggledIndex = _fromStates.IndexOf(toggledState);
}
/// <summary>

CopyConditions(transition.Conditions, source.Conditions);
serializedObject.ApplyModifiedProperties();
Reset();
ApplyModifications($"Added transition from {transition.FromState} to {transition.ToState}");
_toggles[fromIndex >= 0 ? fromIndex : _toggles.Length - 1] = true;
_toggledIndex = fromIndex >= 0 ? fromIndex : _fromStates.Count - 1;
}
/// <summary>

/// <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)
internal void 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;
}
int stateIndex = _fromStates.IndexOf(serializedTransition.FromState.objectReferenceValue);
var stateTransitions = _transitionsByFromStates[stateIndex];
int index = stateTransitions.FindIndex(t => t.SerializedTransition.Index == serializedTransition.Index);
if (targetIndex == -1)
return false;
(int currentIndex, int targetIndex) = up ?
(serializedTransition.Index, stateTransitions[index - 1].SerializedTransition.Index) :
(stateTransitions[index + 1].SerializedTransition.Index, serializedTransition.Index);
_transitions.MoveArrayElement(serializedTransition.Index, targetIndex);
serializedObject.ApplyModifiedProperties();
Reset();
_transitions.MoveArrayElement(currentIndex, targetIndex);
_toggles[
_fromStates.IndexOf(
_transitions.GetArrayElementAtIndex(targetIndex)
.FindPropertyRelative("FromState")
.objectReferenceValue)] = true;
ApplyModifications($"Moved transition to {serializedTransition.ToState.objectReferenceValue.name} {(up ? "up" : "down")}");
return true;
_toggledIndex = stateIndex;
/// Remove a transition by index.
/// Remove a transition.
/// <param name="index">Index of the transition in the transition table</param>
internal void RemoveTransition(int index)
/// <param name="serializedTransition">Transition to delete.</param>
internal void RemoveTransition(SerializedTransition serializedTransition)
var state = _transitions.GetArrayElementAtIndex(index).FindPropertyRelative("FromState").objectReferenceValue;
_transitions.DeleteArrayElementAtIndex(index);
bool toggle = DoToggleAndSort(state, index);
serializedObject.ApplyModifiedProperties();
Reset();
int stateIndex = _fromStates.IndexOf(serializedTransition.FromState.objectReferenceValue);
var stateTransitions = _transitionsByFromStates[stateIndex];
int count = stateTransitions.Count;
int index = stateTransitions.FindIndex(t => t.SerializedTransition.Index == serializedTransition.Index);
int deleteIndex = serializedTransition.Index;
if (index == 0 && count > 1)
_transitions.MoveArrayElement(stateTransitions[1].SerializedTransition.Index, deleteIndex++);
if (toggle)
{
int i = _fromStates.IndexOf(state);
if (i >= 0)
_toggles[i] = true;
}
}
_transitions.DeleteArrayElementAtIndex(deleteIndex);
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;
}
}
}
ApplyModifications($"Deleted transition from {serializedTransition.FromState.objectReferenceValue.name} " +
"to {serializedTransition.ToState.objectReferenceValue.name}");
return ret;
if (count > 1)
_toggledIndex = stateIndex;
private void DisableAllStateTogglesExcept(int index)
internal List<SerializedTransition> GetStateTransitions(Object state)
for (int i = 0; i < _toggles.Length; i++)
if (i != index)
_toggles[i] = false;
return _transitionsByFromStates[_fromStates.IndexOf(state)].Select(t => t.SerializedTransition).ToList();
}
private void CopyConditions(SerializedProperty copyTo, SerializedProperty copyFrom)

private bool TryGetExistingTransition(SerializedProperty from, SerializedProperty to, out int fromIndex, out int toIndex)
{
fromIndex = _fromStates.IndexOf(from.objectReferenceValue);
toIndex = -1;
{
toIndex = -1;
}
}
private void ResetIfRequired()
{
if (serializedObject.UpdateIfRequiredOrScript())
Reset();
}
private void GroupByFromState()

{
Debug.LogError("Transition with invalid \"From State\" found in table " + serializedObject.targetObject.name + ", deleting...");
_transitions.DeleteArrayElementAtIndex(i);
serializedObject.ApplyModifiedProperties();
Reset();
ApplyModifications("Invalid transition deleted");
return;
}
if (serializedTransition.ToState.objectReferenceValue == null)

serializedObject.ApplyModifiedProperties();
Reset();
ApplyModifications("Invalid transition deleted");
return;
}

groupedProps.Add(new TransitionDisplayHelper(serializedTransition, this));
}
_fromStates = groupedTransitions.Keys.Distinct().ToList();
_fromStates = groupedTransitions.Keys.ToList();
}
private void ApplyModifications(string msg)
{
Undo.RecordObject(serializedObject.targetObject, msg);
serializedObject.ApplyModifiedProperties();
Reset();
}
}
}

19
UOP1_Project/Assets/Scripts/StateMachine/Editor/TransitionTableEditorWindow.cs


rootVisualElement.styleSheets.Add(styleSheet);
minSize = new Vector2(480, 360);
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
private void OnDisable()
{
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
}
private void OnPlayModeStateChanged(PlayModeStateChange obj)
{
if (obj == PlayModeStateChange.EnteredPlayMode)
_doRefresh = true;
}
/// <summary>

listView.bindItem = (element, i) => ((Label)element).text = assets[i].name;
listView.selectionType = SelectionType.Single;
listView.onSelectionChanged -= OnListSelectionChanged;
if (_transitionTableEditor && _transitionTableEditor.target)
listView.selectedIndex = System.Array.IndexOf(assets, _transitionTableEditor.target);
}
private void OnListSelectionChanged(List<object> list)

if (_transitionTableEditor == null)
_transitionTableEditor = UnityEditor.Editor.CreateEditor(table, typeof(TransitionTableEditor));
else
else if (_transitionTableEditor.target != table)
UnityEditor.Editor.CreateCachedEditor(table, typeof(TransitionTableEditor), ref _transitionTableEditor);
editor.onGUIHandler = () =>

10
UOP1_Project/Assets/Scripts/StateMachine/Editor/Utilities/AddTransitionHelper.cs


internal void Display(Rect position)
{
position.x += 8;
position.width -= 16;
var rect = position;
float listHeight = _list.GetHeight();
float singleLineHeight = EditorGUIUtility.singleLineHeight;

// State Fields
{
position.y += 10;
position.x += 25;
position.x += 20;
position.x = rect.width / 2 + 25;
position.x = rect.width / 2 + 20;
StatePropField(position, "To", SerializedTransition.ToState);
}

position.x = rect.x + 10;
position.x = rect.x + 5;
position.width -= 20;
position.width -= 10;
_list.DoList(position);
}

4
UOP1_Project/Assets/Scripts/StateMachine/Editor/Utilities/ContentStyle.cs


DarkGray = EditorGUIUtility.isProSkin ? new Color(0.283f, 0.283f, 0.283f) : new Color(0.7f, 0.7f, 0.7f);
LightGray = EditorGUIUtility.isProSkin ? new Color(0.33f, 0.33f, 0.33f) : 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);
ZebraDark = new Color(0.4f, 0.4f, 0.4f, 0.1f);
ZebraLight = new Color(0.8f, 0.8f, 0.8f, 0.1f);
Focused = new Color(0.5f, 0.5f, 0.5f, 0.5f);
Padding = new RectOffset(5, 5, 5, 5);
LeftPadding = new RectOffset(10, 0, 0, 0);

52
UOP1_Project/Assets/Scripts/StateMachine/Editor/Utilities/TransitionDisplayHelper.cs


{
bool Button(Rect pos, string icon) => GUI.Button(pos, EditorGUIUtility.IconContent(icon));
var buttonRect = new Rect(
x: rect.width - 90,
y: rect.y + 5,
width: 30,
height: 18);
var buttonRect = new Rect(x: rect.width - 25, y: rect.y + 5, width: 30, height: 18);
// Move transition up
if (Button(buttonRect, "scrollup"))
if (_editor.ReorderTransition(SerializedTransition, true))
return true;
int i, l;
{
var transitions = _editor.GetStateTransitions(SerializedTransition.FromState.objectReferenceValue);
l = transitions.Count - 1;
i = transitions.FindIndex(t => t.Index == SerializedTransition.Index);
}
buttonRect.x += 35;
// Remove transition
if (Button(buttonRect, "Toolbar Minus"))
{
_editor.RemoveTransition(SerializedTransition);
return true;
}
buttonRect.x -= 35;
if (Button(buttonRect, "scrolldown"))
if (_editor.ReorderTransition(SerializedTransition, false))
if (i < l)
{
if (Button(buttonRect, "scrolldown"))
{
_editor.ReorderTransition(SerializedTransition, false);
}
buttonRect.x -= 35;
}
buttonRect.x += 35;
// Move transition up
if (i > 0)
{
if (Button(buttonRect, "scrollup"))
{
_editor.ReorderTransition(SerializedTransition, true);
return true;
}
buttonRect.x -= 35;
}
// Remove transition
if (Button(buttonRect, "Toolbar Minus"))
// State editor
if (Button(buttonRect, "SceneViewTools"))
_editor.RemoveTransition(SerializedTransition.Index);
_editor.DisplayStateEditor(SerializedTransition.ToState.objectReferenceValue);
rect.x = position.x + 5;
rect.y += rect.height;

正在加载...
取消
保存