So, after changing the scope from the large project we were working on – that both of us dreaded, we are still deciding on how to proceed as of yet. Though we are both working on polishing what we have already made and want to keep, that way it doesn’t feel so scary when looking foward.
Finite state machine
I decided to work on the AI – it was getting a little messy at the start, knowing that it was all crammed into one class, it was getting hard to work on and harder to bug fix – so I created something even harder to bug fix and work on for the benifit of OOP modularity (during the development of the system, I wondered if it was worth sacrificing my sanity).
So, the first thing I did was create a base logic flow and immediately started on a Custom Editor to visualize what the heck I was doing.
The concept is: Have a base FSM class which will essentially hold a pointer of its current branch of decisions, the decisions consist of an array of Conditions and the next branch. Easy right? Yeah – well I dug myself a deep OOP hole.
The FSM, as said above contains the reference “currentBlock” and the state change event which initializes the next condition block.
public class FSM : MonoBehaviour
{
[SerializeField] BFSMConditionBlock currentBlock = null;
public BFSMConditionBlock CurrentBlock
{ get => currentBlock; private set => currentBlock = value; }
[SerializeField] private string Debug_Name;
private bool fsmPaused;
public bool PauseFSM { get => fsmPaused; set => fsmPaused = value; }
private void Awake()
{
BFSMConditionBlock[] blocks = this.GetComponentsInChildren<BFSMConditionBlock>();
for (int i = 0; i < blocks.Length; i++)
{
blocks[i]._ConditionReached.AddListener(OnChangeState);
blocks[i].Initialize(this);
}
if (currentBlock)
currentBlock.StartModule();
}
private void FixedUpdate()
{
if (currentBlock && !PauseFSM)
currentBlock.ConditionLoop();
Debug_Name = currentBlock._OptionalName;
}
public void OnChangeState(ConditionsPair conditionEvent)
{
if (currentBlock)
currentBlock.EndModule();
CurrentBlock = conditionEvent.Module;
CurrentBlock.StartModule();
}
}
Here we have the “Branch” or the “ConditionBlock”, it contains a list of Modules to use (which will run while the FSM reference sits on this condition block) and a List of Conditions
[System.Serializable]
public class ConditionEvent : UnityEvent<ConditionsPair> { }
public class BFSMConditionBlock : MonoBehaviour
{
[SerializeField] public string _OptionalName;
[HideInInspector] public ConditionEvent _ConditionReached;
[HideInInspector] public ConditionEvent _ConditionFailed;
public List<ConditionsPair> _Conditions;
public List<BFSMModule> _ModuleToUse = new List<BFSMModule>();
protected bool _LastChange = false;
protected FSM _FSM;
public virtual void Initialize(FSM fsm)
{
_FSM = fsm;
}
protected virtual void Start() { }
public virtual void ConditionLoop()
{
for (int i = 0; i < _Conditions.Count; i++)
{
bool hasPassed = true;
for (int condition = 0; condition < _Conditions[i].ConditionsToMeet.Count; condition++)
{
if (!_Conditions[i]._FlipCondition)
{
if (_Conditions[i].ConditionsToMeet[condition].GetConditionState().Equals(false))
{
hasPassed = false;
break;
}
}else
{
if (_Conditions[i].ConditionsToMeet[condition].GetConditionState().Equals(true))
{
hasPassed = false;
break;
}
}
}
if (hasPassed != _LastChange)
{
if (hasPassed)
_ConditionReached?.Invoke(_Conditions[i]);
_LastChange = hasPassed;
}
}
}
protected virtual void FixedUpdate()
{
if (_FSM.CurrentBlock != this)
return;
for (int i = 0; i < _ModuleToUse.Count; i++)
{
if (_ModuleToUse[i])
_ModuleToUse[i].UseModuleFixedUpdate();
}
}
protected virtual void LateUpdate()
{
if (_FSM.CurrentBlock != this)
return;
for (int i = 0; i < _ModuleToUse.Count; i++)
{
if (_ModuleToUse[i])
_ModuleToUse[i].UseModuleLateUpdate();
}
}
protected virtual void Update()
{
if (_FSM.CurrentBlock != this)
return;
for(int i = 0; i < _ModuleToUse.Count; i++)
{
if (_ModuleToUse[i])
_ModuleToUse[i].UseModuleUpdate();
}
}
protected virtual void Awake() { }
public virtual void EndModule()
{
for (int i = 0; i < _ModuleToUse.Count; i++)
{
if (_ModuleToUse[i])
_ModuleToUse[i].OnModuleEnd();
}
for (int i = 0; i < _Conditions.Count; i++)
{
for (int j = 0; j < _Conditions[i].ConditionsToMeet.Count; j++)
_Conditions[i].ConditionsToMeet[j].Uninitialize();
}
}
public virtual void StartModule()
{
Debug.Log("Started < " + _OptionalName);
for (int i = 0; i < _ModuleToUse.Count; i++)
{
if (_ModuleToUse[i])
_ModuleToUse[i].OnModuleStart();
}
for (int i = 0; i < _Conditions.Count; i++)
{
for (int j = 0; j < _Conditions[i].ConditionsToMeet.Count; j++)
_Conditions[i].ConditionsToMeet[j].Initialize();
}
}
}
ConditionPairs contain a name, the conditions that need to return true and the next condition block to move to.
public struct ConditionsPair
{
[SerializeField] string _OptionalName;
[SerializeField] BFSMConditionBlock _Module;
[SerializeField] public bool _FlipCondition;
[SerializeField] List<Condition> _ConditionsToMeet;
public ConditionsPair(string optionalName, List<Condition> conditionsToMeet, BFSMConditionBlock module, bool flipCondition = false) : this()
{
_OptionalName = optionalName;
ConditionsToMeet = conditionsToMeet ?? throw new ArgumentNullException(nameof(conditionsToMeet));
Module = module ?? throw new ArgumentNullException(nameof(module));
_FlipCondition = flipCondition;
}
public List<Condition> ConditionsToMeet { get => _ConditionsToMeet; private set => _ConditionsToMeet = value; }
public BFSMConditionBlock Module { get => _Module; private set => _Module = value; }
public string OptionalName { get => _OptionalName; private set => _OptionalName = value; }
}
Condition is a container that will get used for inheritance.
public class Condition : MonoBehaviour
{
[SerializeField] public string _ConditionString = "default";
[SerializeField] protected ConditionSettings _Settings;
protected virtual void Awake()
{
}
public virtual void Initialize() { }
public virtual void Uninitialize() { }
public virtual bool GetConditionState()
{
return false;
}
}
And the last piece of the pie is the Module, which is the logic to run while inside the Condition Block, which again, is a container for inheritance.
public abstract class BFSMModule : MonoBehaviour
{
protected virtual void Start() { }
protected virtual void Update() { }
protected virtual void Awake() { }
public virtual void UseModuleUpdate() { }
public virtual void UseModuleFixedUpdate() { }
public virtual void UseModuleLateUpdate() { }
public virtual void OnModuleStart() { }
public virtual void OnModuleEnd() { }
}
Hows it look? Absolutely terrible – so lets get to work on the Editor window.

Much better to look at right? Well, I did it all wrong – so I had to start again.
This is much better – it uses the correct conditions, and the modules.

And its still bad – alright, so that’s what I’ll be working on for the next post.
