Any Burst-enabled DOTS State Machines out there?

Here are 2 popular ways to do bursted state machines:

Enabled components state machine

  • Each state type is a enableable component
  • Each state type has a system/job that iterates that enableable component + other components relevant to that state
  • Switching states means controlling the activation of state enableable components

State ID + Switch statement state machine

  • Each state has an ID (a byte, an int, an enum, etc…)
  • A StateMachine component on your entity stores the ID of the current state
  • One jobs iterates the StateMachine component, does a switch statement on the “currentState”, and depending in it, executes the right logic

There are advantages and disadvantages to both, but the gist of it is that the “enableable components” approach has the advantage of better modularity, and the “switch statement” approach has the advantage of better performance in many cases.

You may also sometimes hear about “structural changes” state machines, which are essentially the same as the “enableable components” state machines except we add/remove components instead of enabling/disabling them, but I find that in most cases, the “enableable components” approach is a better bet.

However, if you need features like these:

  • State machines that can have multiple states of the same type, but with different data (especially problematic for enableable components state machines, since multiple components of the same type cannot be added to the same entity)
  • Hierarchical state machines, fully solvable in 1 frame
  • Adding/removing states at runtime
  • States and/or state machines that are generated procedurally
  • Perform state transitions and access the results immediately within a job. This is also something you’d need if you have a state machine that can potentially do multiple transitions within one frame and your states have state Enter/Exit logic
  • etc…

Then something more complex might be required. One approach that would support all of these features would be:

  • You have a DynamicBuffer of States on the entity
  • You have a StateMachine component that holds the index of the current state in the states buffer
  • “State” is a struct that can hold all the data required across all state types, and it has an ID of which state type it is. Designing this all-encompassing “State” struct and working with it is the main thing that complicates this approach.
  • One system/job iterates StateMachine + DynamicBuffer of States and:
    • gets the current “State” struct in the DynamicBuffer of States based on the “StateMachine.CurrentStateIndex”
    • does a switch statement on the “State.StateType”
    • based on “State.StateType”, execute correct state logic with the state’s data

And here are some more alternatives, with varying levels of support for the features described above:

Serialized states buffer
Do something similar to the “StateMachine + DynamicBuffer of States” approach, but instead of having one mega “State” struct holding all the possible state data, you’d serialize the data of each state one after the other in a DynamicBuffer of bytes. Each state data would be preceded by a “header” that would contain the “state type”, so when we read that buffer of bytes, we know how many bytes of data we must read for this state type. Instead of holding a “CurrentStateIndex”, you’d hold the index of the first byte of that state’s serialized data in the buffer of bytes. This approach requires much more work than the previous one, especially when you want to add/remove states, but it has the advantage of not being wasteful of memory space, and therefore might have better potential performance. Each state only takes the exact size it needs in memory, instead of taking up the worst-case-scenario size among all possible states.

Separate DynamicBuffer for every state type
Add a DynamicBuffer for every state type on every entity that can possibly have these states. Then have a StateMachine that holds a “CurrentStateType” and a “CurrentStateIndex”. A job will then do a switch statement on “CurrentStateType”, get the right dynamicBuffer corresponding to that type, and then get the state data at the “CurrentStateIndex” and update the state. I’ll admit I’m not quite sure about the performance implications this approach would have if we have lots of state types (let’s say 20+, which means 20+ DynamicBuffers on all your statemachine entities and 20+ buffers accessed by your state machine job), but in theory it can provide all the versatility you might need

Every state is an entity
Every state is an entity, and the main statemachine entity has a DynamicBuffer of State Entities and a StateMachine component holding the index of the current state entity. On state entities, you have a component corresponding to a specific state, and an enableable component that’s used as a way of knowing if that state should be updating now. Then systems/jobs iterate these state components to update them, but only of the “UpdateState” enableable component is enabled. This will be significantly worse for performance than other solutions due to having to do component lookups constantly, but should be the simplest to implement. You might need the state machine update to be main-thread only if you want to support hierarchical state machines solvable in 1 frame with this approach though, which would further increase the performance penalty of this approach