Random ideas around writing an FSM library, with examples from, and motivations behind hfsm.dev, a header-only library created using #cpp template #metaprogramming with #gamedev, #embedded and #robotics in mind.
The first important decision is whether the structure of FSM could be built/changed at runtime.
The implementation of a dynamic FSM relies on heap-allocated memory, resizable containers and pointers, all of which come at a steep perf price.
(Which is the way of hfsm.dev).
At a bare minimum, transitions should:
struct FSM {
State* states[];
State* active;
void changeTo(StateId targetId) {
active->exit();
active = states[target];
active->enter():
}
};
If your platform's operation is based on periodical updates (e.g. UI, #gamedev), FSM needs to support that:
struct State {
void update();
};
struct FSM {
State* active;
void update() {
active->update();
}
};
FSM fsm;
while (true)
fsm.update();
In C++ land, anything is an event:
- message (with or w/o payload)
- function call
- variable value change
Which makes polling viable (even if limited) event handling strategy for periodically updating FSMs.
First take:
enum StateID {
STATE_1,
STATE_2,
..
STATE_COUNT
};
struct FSM {
State* getState(StateID id) { return states[id]; }
..
State* states[STATE_COUNT];
};
Simple but clumsy.
Too much boilerplate burden on user.
Use std::type_info:
struct FSM {
template <class TState>
State* getState() { return states[typeid(TState)]; }
..
hash_table<std::type_index, State*> states;
};
Deprecated github.com/andrew-gresyk/… used this.
⚠️ TMP warning ⚠️
hfsm.dev uses type list to map state types to sequential array indices.
See hfsm2::_TL<>::index<>() in
github.com/andrew-gresyk/…
Ultimately, using state types to refer to states:
- is more natural
- eliminates spelling mistakes
- makes maintenance easier
- allows for compile-time validation
- benefits from refactoring tools and search/replace
- is a major quality of life improvement
Event polling FSMs need R[+W] access outside:
struct Ctx;
struct State {
void update(Ctx&);
};
struct FSM {
FSM(Ctx& c) : ctx{c} {}
void update() { active->update(ctx); }
State* active;
Ctx& ctx;
};
Ctx ctx;
FSM<Ctx> fsm{ctx};
fsm.update();
Take 1: Return transition from State::update():
struct State {
std::optional<StateID> update(Ctx&);
};
void FSM::update() {
if (auto tr = active->update(ctx)) {
active->exit(ctx);
active = states[*tr];
active->enter(ctx);
}
}
struct Ctrl {
template <class S> changeTo() {
dest = typeid(S);
}
Ctx& ctx();
std::optional<StateID> dest;
};
void State::update(Ctrl& c) {
c.changeTo<State2>();
}
void FSM::update() {
Ctrl c{ctx};
active->update(c);
if (auto dest = *c.dest)
...
}
struct State1;
struct State2;
void State1::update(Ctrl& c) {
c.changeTo<State2>(); // internal
}
Ctx ctx;
FSM fsm{ctx};
fsm.changeTo<State1>(); // external
Important: FSM processes transitions outside of the state methods to avoid nested out-of-order enter() / exit() calls:
void FSM::update() {
prev->update();
if (transition) {
prev->exit();
next->enter();
}
}
Event listener-style FSM:
struct Evt;
void State::react(Evt&, Ctrl&);
void FSM::react(Evt& e) {
Ctrl c{ctx};
active->react(e, c);
if (c.dest) {
active->exit(c);
active = states[*c.dest];
active->enter(c);
}
}
struct FSM {
template <typename S> bool isActive() const {
return active == states[typeid(S)];
}
};
You can find the example, illustrating the concepts in this thread, implementing a minimally functional FSM in under 100 mloc: github.com/andrew-gresyk/…