Hi, I’m trying to work with some OOP principles that I struggle with and would appreciate some help. I have a working state machine, the StateMachine simplified:
I have an IState interface which has 3 methods. The StateMachine runs the current state’s Tick method in Update and it’s SetState method assigns a new state, running the old states OnExit method and then the new states OnEnter method. I have a state machine with many states all implementing the IState interface working fine:
public interface IState
{
void Tick();
void OnEnter();
void OnExit();
}
public class StateMachine
{
protected IState _currentState;
public void Update()
{
_currentState.Tick()
}
public void SetState(IState state)
{
_currentState.OnExit()
_currentState = state;
_currentState.OnEnter()
}
}
What I’d like to do is extend the functionality and create a state with a new method, and have some states that have the new method and old states not have the method. I have a working StateMachine where the states do not want the new method (I assume there is a better way than adding the new method to IState and going into all the classes and creating a blank new implementation of the method). My idea was to have a new kind of State ISequentialState that inherits IState with just the new method:
public interface ISequentialState: IState
{
void IsInterrupted();
}
And then create a new StateMachine that inherits all the base StateMachine functionality, but extends the Update method with the new IsInterrupted method in addition to the base Update method:
class InterruptableStateMachine : StateMachine
{
public void Update()
{
base.Update();
_currentState.IsInterrupted();
}
}
It’s at this point I think I’m confused as this doesn’t work currently because the _currentState is an IState and not an ISequentialState, so cannot call IsInterrupted. I aren’t sure if I can make InterruptableStateMachine use the new state as all the base StateMachine code uses IState. I could copy all the StateMachine code into InterruptableStateMachine code but swap IState references for ISequentialState. But this duplication is clearly flawed. Any pointers on the design of this would be great. Thanks.
Ahh this brings out memories of times where i always kept asking my self the question of “How can i make this even more OOP?” , even when i had code that worked i always thought “there must be an even OOPer way , the CLEAAAAAAAANEST of em’ all , i’m just not OOping hard enough”.
I only say this because after getting out of the OOP rabbit-hole, imo the best way to look at OOP is a set of best practices and tools , and not a bible that you need to bend to code into EVEN if it’s already working.
If you only have those two IState
implementations in the whole project , just do a type-cast check and move on with life , only abstract as problems arise.
Assuming that it’s not only two IState
and you plan on having my different variations of state , here’s how i would do it , it all starts from a need of a solution to a problem , so : What’s the problem that needs to be solved here ?
What i’m seeing is different states that need to be treated differently , it’s not the StateMachine
that changes , the StateMachine
it still has one thing to do (and it’s very concrete) : Tick the sub state it has , so imo if there’s a variance to be made , i’d make it on the IState
level.
Now, this can depend of how many different IState
implementations you will have , i would go for intermediate abstract classs that implement IState
and that cover the different state types needed , here’s an example
public interface IState
{
void OnTick();
void OnEnter();
void OnExit();
}
// Now we will dictate how the different IState implementations will behave through an abstract class
// This can help us put all the "variation" logic in the base classes , and leave the rest to the implementing subclasses
// Inherit this class for a state that ticks every specified interval
public abstract class TimedTickState : IState
{
private float interval;
private float currentTimer;
protected TimedTickState(float interval)
{
this.interval = interval;
currentTimer = 0;
}
public void OnEnter() { }
public void OnExit() { }
// you can do the same thing for "Enter" and "Exit" but let's keep it short
protected abstract void Tick();
public void OnTick()
{
currentTimer += Time.deltaTime;
while(currentTimer > interval)
{
currentTimer -= interval;
Tick();
}
}
}
// Inherit this class for a state that ticks every two frames
public abstract class SkipTickState : IState
{
private bool canTick;
public void OnEnter()
{
canTick = true;
}
public void OnExit() { }
protected abstract void Tick();
public void OnTick()
{
canTick = !canTick;
if(canTick)
{
Tick();
}
}
}
// Inherit this class for state that ticks then gets interrupted
public abstract class InterruptableState : IState
{
public void OnEnter() { }
public void OnExit() { }
protected abstract void Tick();
protected abstract void IsInterrupted();
public void OnTick()
{
Tick();
IsInterrupted();
}
}
// now you can have multiple classes that indirectly inherit IState but still have a distinct feature
// example with IsInterrupted
public class InterruptExampleState : InterruptableState
{
protected override void IsInterrupted()
{
Debug.Log("I got interrupted");
}
protected override void Tick()
{
Debug.Log("Ticked");
}
}
Haha, your first paragraph nailed it. I’ve got a new job where they have some C# projects, so I’ve picked this Unity game back up and using it to try to OOP as much as you can OOP! I think I was getting bogged down in trying to have the new method in an Interface. Thank you for your response, this looks spot on for what I need.
I have a StateMachines gitHub repo that I’ve been using in several productions already. It suports hierarchical states and orthogonal regions (a state with multiple child states active at the same time). Feel free to use it, get inspiration, or even contribute if you like.
What it does, regarding your question about new methods perhaps, is that each message is usually implemented as an interface with a single method (it can have arguments, too). Then, a generic (can be static) delegate is created that calls this method on a state that implements the handler interface. When processing a message, the stateMachine visits the state hierarchy from the deepmost state and seeks the first handler.
Pretty much everything is statically typed and messages are pooled, so there’s no garbage production. Messages can be processed immediately or from a separate call to process them. Messages received during processing of another message are queued (so no multiple messages can be processed on the stack at the same time).
States can have entry arguments, so you can only transit to state with these arguments provided, and it’s statically typed too, so you get compile time errors when they are not.
Comes with a debug EditorWindow that shows the entire HSM and active states.
The repo is compatible with Unity’s package manager.
I apologize for “advertising” a repository like that. I’d love if it gained some users and contributors, and I hope that it helps.