浏览代码

Improved debugging for the state machine (#131)

* Added a specialized class for debugging the state machine

The StateMachineDebugger class will generate logs for state changes together with the conditions results. Providing more in-depth information.

* [Bot] Automated dotnet-format update

* Improved the debugger's log with SO names

Added changes so the state machine debugger can use the ScriptableObjects names in the log, improving readabilty.
Now the log also shows the transition name.

* [Bot] Automated dotnet-format update

* Added a feature to also list the active StateActions

The debugger can now list the actions activated by the state.
Also added two options to toggle the logging of the conditions and the actions

* [Bot] Automated dotnet-format update

* Small improvements and documentation

Changed the State name for a reference to the SO of origin (like the other classes)
Removed the name of the transition for compatibility with the changes made...
/main
GitHub 4 年前
当前提交
f06b5a5e
共有 11 个文件被更改,包括 185 次插入23 次删除
  1. 7
      UOP1_Project/Assets/Prefabs/Pig.prefab
  2. 9
      UOP1_Project/Assets/Scripts/StateMachine/Core/State.cs
  3. 6
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateAction.cs
  4. 21
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateCondition.cs
  5. 16
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachine.cs
  6. 12
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateTransition.cs
  7. 1
      UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/StateActionSO.cs
  8. 4
      UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/StateConditionSO.cs
  9. 2
      UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/StateSO.cs
  10. 119
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachineDebugger.cs
  11. 11
      UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachineDebugger.cs.meta

7
UOP1_Project/Assets/Prefabs/Pig.prefab


m_Script: {fileID: 11500000, guid: 1eeda163c70b3cb4ebb88ba92f608fd8, type: 3}
m_Name:
m_EditorClassIdentifier:
CurrentState:
debug: 0
_debugger:
debugTransitions: 1
appendConditionsInfo: 1
appendActionsInfo: 1
currentState:
--- !u!1 &3791421269865525050
GameObject:
m_ObjectHideFlags: 0

9
UOP1_Project/Assets/Scripts/StateMachine/Core/State.cs


namespace UOP1.StateMachine
using UnityEngine;
namespace UOP1.StateMachine
public string Name { get; internal set; }
internal ScriptableObject _originSO;
internal StateMachine _stateMachine;
internal StateTransition[] _transitions;
internal StateAction[] _actions;

public State(
ScriptableObject originSO,
_originSO = originSO;
_stateMachine = stateMachine;
_transitions = transitions;
_actions = actions;

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


namespace UOP1.StateMachine
using UnityEngine;
namespace UOP1.StateMachine
{
/// <summary>
/// An object representing an action.

internal ScriptableObject _originSO;
/// <summary>
/// Called every frame the <see cref="StateMachine"/> is in a <see cref="State"/> with this <see cref="StateAction"/>.
/// </summary>

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


namespace UOP1.StateMachine
using UnityEngine;
namespace UOP1.StateMachine
{
/// <summary>
/// Class that represents a conditional statement.

/// </summary>
public readonly struct StateCondition
{
internal readonly ScriptableObject _originSO;
internal readonly StateMachine _stateMachine;
public StateCondition(Condition condition, bool expectedResult)
public StateCondition(ScriptableObject originSO, StateMachine stateMachine, Condition condition, bool expectedResult)
_originSO = originSO;
_stateMachine = stateMachine;
public bool IsMet => _condition.Statement() == _expectedResult;
public bool IsMet()
{
bool statement = _condition.Statement();
bool isMet = statement == _expectedResult;
#if UNITY_EDITOR
_stateMachine._debugger.TransitionConditionResult(_originSO.name, statement, isMet);
#endif
return isMet;
}
}
}

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


{
public class StateMachine : MonoBehaviour
{
[Tooltip("Set the initial state of this StateMachine")]
[SerializeField] private ScriptableObjects.TransitionTableSO _transitionTableSO = default;
public string CurrentState;
public bool debug;
[Space]
[SerializeField]
internal StateMachineDebugger _debugger = default;
[SerializeField] private ScriptableObjects.TransitionTableSO _transitionTableSO = default;
private readonly Dictionary<Type, Component> _cachedComponents = new Dictionary<Type, Component>();
private State _currentState;

_currentState = _transitionTableSO.GetInitialState(this);
_currentState.OnStateEnter();
#if UNITY_EDITOR
CurrentState = _currentState.Name;
_debugger.Awake(this, _currentState._originSO.name);
#endif
}

_currentState.OnStateExit();
_currentState = transitionState;
_currentState.OnStateEnter();
#if UNITY_EDITOR
if (debug)
Debug.Log($"{name} entering state {_currentState.Name}");
CurrentState = _currentState.Name;
#endif
}
}
}

12
UOP1_Project/Assets/Scripts/StateMachine/Core/StateTransition.cs


private bool ShouldTransition()
{
#if UNITY_EDITOR
_targetState._stateMachine._debugger.TransitionEvaluationBegin(_targetState._originSO.name);
#endif
_conditions[idx].IsMet :
_results[i] && _conditions[idx].IsMet;
_conditions[idx].IsMet() :
_results[i] && _conditions[idx].IsMet();
#if UNITY_EDITOR
_targetState._stateMachine._debugger.TransitionEvaluationEnd(ret, _targetState._actions);
#endif
return ret;
}

1
UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/StateActionSO.cs


var action = CreateAction();
createdInstances.Add(this, action);
action._originSO = this;
action.Awake(stateMachine);
return action;
}

4
UOP1_Project/Assets/Scripts/StateMachine/ScriptableObjects/StateConditionSO.cs


internal StateCondition GetCondition(StateMachine stateMachine, bool expectedResult, Dictionary<ScriptableObject, object> createdInstances)
{
if (createdInstances.TryGetValue(this, out var cond))
return new StateCondition((Condition)cond, expectedResult);
return new StateCondition(this, stateMachine, (Condition)cond, expectedResult);
var condition = new StateCondition(CreateCondition(), expectedResult);
var condition = new StateCondition(this, stateMachine, CreateCondition(), expectedResult);
createdInstances.Add(this, condition._condition);
condition._condition.Awake(stateMachine);
return condition;

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


var state = new State();
createdInstances.Add(this, state);
state.Name = name;
state._originSO = this;
state._stateMachine = stateMachine;
state._transitions = new StateTransition[0];
state._actions = GetActions(_actions, stateMachine, createdInstances);

119
UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachineDebugger.cs


using System;
using System.Text;
using UnityEngine;
namespace UOP1.StateMachine
{
/// <summary>
/// Class specialized in debugging the state transitions, should only be used while in editor mode.
/// </summary>
[Serializable]
internal class StateMachineDebugger
{
[SerializeField]
[Tooltip("Issues a debug log when a state transition is triggered")]
internal bool debugTransitions = false;
[SerializeField]
[Tooltip("List all conditions evaluated, the result is read: ConditionName == BooleanResult [PassedTest]")]
internal bool appendConditionsInfo = true;
[SerializeField]
[Tooltip("List all actions activated by the new State")]
internal bool appendActionsInfo = true;
[SerializeField]
[Tooltip("The current State name [Readonly]")]
internal string currentState;
private StateMachine _stateMachine;
private StringBuilder _logBuilder;
private string _targetState = string.Empty;
private const string CHECK_MARK = "\u2714";
private const string UNCHECK_MARK = "\u2718";
private const string THICK_ARROW = "\u279C";
private const string SHARP_ARROW = "\u27A4";
/// <summary>
/// Must be called together with <c>StateMachine.Awake()</c>
/// </summary>
internal void Awake(StateMachine stateMachine, string initialState)
{
_stateMachine = stateMachine;
_logBuilder = new StringBuilder();
currentState = initialState;
}
internal void TransitionEvaluationBegin(string targetState)
{
_targetState = targetState;
if (!debugTransitions)
return;
_logBuilder.Clear();
_logBuilder.AppendLine($"{_stateMachine.gameObject.name} state changed");
_logBuilder.AppendLine($"{currentState} {SHARP_ARROW} {_targetState}");
if (appendConditionsInfo)
{
_logBuilder.AppendLine();
_logBuilder.AppendLine($"Transition Conditions:");
}
}
internal void TransitionConditionResult(string conditionName, bool result, bool isMet)
{
if (!debugTransitions || _logBuilder.Length == 0 || !appendConditionsInfo)
return;
_logBuilder.Append($" {THICK_ARROW} {conditionName} == {result}");
if (isMet)
_logBuilder.AppendLine($" [{CHECK_MARK}]");
else
_logBuilder.AppendLine($" [{UNCHECK_MARK}]");
}
internal void TransitionEvaluationEnd(bool passed, StateAction[] actions)
{
if (passed)
currentState = _targetState;
if (!debugTransitions || _logBuilder.Length == 0)
return;
if (passed)
{
LogActions(actions);
PrintDebugLog();
}
_logBuilder.Clear();
}
private void LogActions(StateAction[] actions)
{
if (!appendActionsInfo)
return;
_logBuilder.AppendLine();
_logBuilder.AppendLine("State Actions:");
foreach (StateAction action in actions)
{
_logBuilder.AppendLine($" {THICK_ARROW} {action._originSO.name}");
}
}
private void PrintDebugLog()
{
_logBuilder.AppendLine();
_logBuilder.Append("--------------------------------");
Debug.Log(_logBuilder.ToString());
}
}
}

11
UOP1_Project/Assets/Scripts/StateMachine/Core/StateMachineDebugger.cs.meta


fileFormatVersion: 2
guid: 4fe968fa3a5601843b52a0cb3b94c2d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
正在加载...
取消
保存