recreational.tech/content/posts/data_oriented_fsm.md

81 lines
4.3 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Data Oriented Finite State Machines"
date: 2022-03-29T16:01:00+02:00
draft: false
type: "post"
---
Finite state machines (FSMs) are probably one of the most widespread techniques used for game AI.
For my own engine i thought about ways to implement FSMs with performance in mind. The fundamental assumption guiding the design was: “There will be many AI entities using the same state-machine.”[1](#fn:1)
The design itself is pretty simple and im sure that im not the first person to come up with this. The structure for a FSM is shown here:
#define XE_AI_FSM_MAX_STATES 16
#define XE_AI_FSM_STATE_FUNC(name) uint32_t name(xeAIEntity entity, uint32_t previousState)
typedef XE_AI_FSM_STATE_FUNC(xeAIFSMStateFunc);
typedef struct
{
uint32_t maxEntities;
uint32_t stateCount;
uint32_t entityCounts[XE_AI_FSM_MAX_STATES];
xeAIFSMStateEntity* stateEntities[XE_AI_FSM_MAX_STATES];
xeAIFSMStateEntity* nextStateEntities[XE_AI_FSM_MAX_STATES];
xeAIFSMStateFunc* stateFuncs[XE_AI_FSM_MAX_STATES];
} xeAIFSM;
I arbitrarily limited the number of states to 16 because this saves me from some memory management, but it would be trivial to remove this limit. For every state (which is identified by an unsigned integer between `0` and `stateCount`), the FSM saves the AI entities in that state (`stateEntities`). The type `xeAIFSMStateEntity` is shown below. It contains an reference to the entity data and the previous state of the entity, which can be used by the state function to determine if the entity just entered the state (for example, if a monster just entered the “chase player” state, we could play a special sound effect).
typedef struct
{
xeAIEntity entity;
uint32_t previousState;
} xeAIFSMStateEntity;
Every frame, all state functions need to be executed. For that, we iterate over all states and call the function for that state (`stateFuncs[stateIndex]`) for every entity in `stateEntities[stateIndex]`. This can be done in parallel, for example via a job system (which is what i do in my engine). The state function returns the new state of the entity and the run function of the FSM will place the append the entity to `nextStateEntities[newState]`. After all states were executed, `stateEntities` and `nextStateEntities` are swapped.
Code for this function is shown below; i removed all parallelization code for improved clarity.
void xe_ai_fsm_run(xeAIFSM* fsm)
{
uint32_t nextEntityCounts[XE_AI_FSM_MAX_STATES];
XE_ZERO_ARRAY(nextEntityCounts, XE_AI_FSM_MAX_STATES);
for (uint32_t state = 0; state < XE_AI_FSM_MAX_STATES; ++state) {
xeAIFSMStateFunc* stateFunc = fsm->stateFuncs[state];
assert(stateFunc);
uint32_t entityCount = fsm->entityCounts[state];
xeAIFSMStateEntity* entities = fsm->stateEntities[state];
for (uint32_t entityIdx = 0; entityIdx < entityCount; ++entityIdx) {
uint32_t nextState = stateFunc(entities[entityIdx].entity, entities[entityIdx].previousState);
entities[entityIdx].previousState = state;
if (nextState >= fsm->stateCount) {
xe_log_error("FSM %x entity (%u;%u) tried to move to invalid state %u",
(uintptr_t)fsm,
state, entityIdx,
nextState);
nextState = state;
assert(!"Tried to move an entity to an invalid state");
}
/* Move to (potentially) new state */
fsm->nextStateEntities[nextState][nextEntityCounts[nextState]] = entities[entityIdx];
++nextEntityCounts[nextState];
}
}
/* Swap pointers and update counts */
for (uint32_t state = 0; state < XE_AI_FSM_MAX_STATES; ++state) {
xeAIFSMStateEntity* tmp = fsm->stateEntities[state];
fsm->stateEntities[state] = fsm->nextStateEntities[state];
fsm->nextStateEntities[state] = tmp;
fsm->entityCounts[state] = nextEntityCounts[state];
}
}
* * *
1. “Where there is one, there are many.”, [as Mike action would say](https://youtu.be/rX0ItVEVjHc?t=871). [](#fnref:1)