So, in summary: The naive implementations are the shortest and easiest to write and understand, produce very few lines of assembly, run the fastest most of the time and work on ancient compilers. It's a tough decision which approach to choose in your next project...
I work mostly with game code, so performance is the number one concern, followed by cross platform/compiler compatibility. State machines for players, enemies, etc. are the majority of the game logic. In the battle between performance and being safer to work with, performance always wins in games.
Take a look at the library examples that contain more states / actions / guards and compound state. You will see that the compiler is capable of seeing through that, and still capable of inlining the transition handling. A three state example is unsuitable to show the pain of growing a switch case/if-else solution.
That's actually pretty subjective, not an inherent truth to game development. For many people, delivering a functional product might be much more important than performance.
Or just throw enough artists at the game to make it look pretty, make it console exclusive and people will label is cinematic experience GOTY contender.
Nice ideas, but I like and trust the switch/enum approach. I once wrote a PGN (text file, containing several chess games) reader/writer. It worked with states, because almost all characters had different meaning, according to the state/section/stage it was currently reading. I kept a switch of the current character, dealing in each case with all states, this time not in a block of ifs nor switch, just a "spaghetti" code actually. Because there were some behaviours that could be reused for different states - _I love compact code, and think it's better to find bugs_ . If I instead had used a switch of states on the 1st place, each case would has a giant switch of characters, which would be awkward. For enum, I started with the header and moves. Later, I added things that the user could write by hand, like commentaries and (alternative moving) lines, resulting in new states. I used OO, because the user could save the game at any time, so those data from the game should be well hidden and secured. Everything worked nicely, like a high level language. Of course I faced many easy-to-find bugs, as in any normal project. But I don't remember ever spending more than 1h catching any of those. Each error was just a matter to look at the output, to know were it read wrongly. If it was a bit harder, I took a pick on the file itself.
....what? Just use a switch function with dispatch to functions that use the enum as a template parameter. Then wrap away the cases with a macro that is configured to be self repeating with substitutions for the specific enum. The compiler will even warn you if you miss one if you omit default, and the switch is skipped at runtime if you've used casting to convert to an out of range enum(which is bad, I'm not saying you should do it, just that this doesn't preclude use of the technique) As a bonus you can define the functions inline and the compiler is allowed to inline away all the overhead of the function calls. Wrap your enum in std::atomic and it's now re-entrant and thread safe(Though, again, please don't do this as every time I've seen it in practice it is much more of an anti pattern. PLEASE just set a flag instead, or a bitmask and read the update from their to handle the state transition). This also makes all your states easily searchable by the enum keys, and you can define the implementation in a separate file. And I'm pretty sure that most of that will work even on older compilers. I know it runs with c++17 support. I think the newest feature you need is function templates though. Also, class enums are a pain if you *want* them to be implicitly convertible in places, so I often use namespace Name{ enum Type{ZERO=0, ONE, TWO}; } but I replace "Name" with the enum name, in this case State. This still provides scoping so you don't clutter the namespace, and you can reference the type with State::Type which I quite like as it's very explicit that it is a type(which I find can sometimes be unclear if a codebase has poor style) If that really bothers you though, you can also leverage a using alias to define the type however you wish(Perhaps StateT(don't use State_t as the _t suffix is actually reserved in various environments and you may accidently generate a collision)
15:38 "need to allocate on the heap" WRONG! Ever heard of a buffer (ie. Placement new). And that's your only criticism that I can see. Yet it's MUCH easier to use and more capable than the boost solution. Therefore, it wins.
I was gonna go for MSM because of the 'ultra fast' references in the docs but maybe SML will be good enough to get started with... Thanks for the video. I'm sure it was a lot of work to put together all the details.
The proper way to implement it with a switch is one switch over all states. Call that regularly. If you need to change input for the state machine from other tasks ... let them set an input via setter. This way you have all states in one big switch. Add states? Just add a case. Modify a transition? Check out the state from which the transition leaves. Say you have state A and state B and Output O and B changes O from 5 to 6. if you transition from A to B the change of the output will occur one cycle later. Say: Void Cyclic_Worker() { Switch(state) Case A: Output = 5 if (transition needed) State = B break case B: Output = 6 if (other_transition_needed) State = A } If you need the state change and the output change on the same cycle, do two switches, one for the state change and then for the output Of course you can write functions for the cases to make it more readable and manageable once it gets bigger
Great talk. But... i think you gone wrong with coroutines. co_await, co_return and co_yield are for elide writing state machines by hand and you used them to handmade state machine again... So this "strange" code is a result of this. It's diffrent way of thinking but this change of thinkg must be level up at level "do i need manual sate machine?"
@@bitbangs "whereas the other style _sometimes_ puts the const on the left and _sometimes_ on the right." If that *"(...) **_sometimes_** on the right."* refers to constant pointers (char* const p), I have pratically no use for them.
The examples show are very contrived and simplistic. They don't tell real story in real code. Equating assembler code length to performance is totally silly. There is so many other things that are important. There shouldn't be any difference between switch, if and fold expressions. They are all just conditions to the compiler. However, the problem is you are comparing constant propagation and folding by the compiler, not actual generated code where there events are dynamically coming from somewhere else (file, network, other subsystem). There are no benchmarks at all in the presentation. Just glancing at irrelevant assembly code.
Interesting topic, but the presentation wasn't that enjoyable. Lots of too long awkward pauses(and then just saying "This is bad!" or "This is good!") and "um" "ya know" and other fillers.
Yeah!!! *goto* is back: 27:42
Thanks for the talk.
So, in summary: The naive implementations are the shortest and easiest to write and understand, produce very few lines of assembly, run the fastest most of the time and work on ancient compilers. It's a tough decision which approach to choose in your next project...
If all you do is write three state programs.. that never get extended or maintained.
I work mostly with game code, so performance is the number one concern, followed by cross platform/compiler compatibility. State machines for players, enemies, etc. are the majority of the game logic. In the battle between performance and being safer to work with, performance always wins in games.
Take a look at the library examples that contain more states / actions / guards and compound state. You will see that the compiler is capable of seeing through that, and still capable of inlining the transition handling. A three state example is unsuitable to show the pain of growing a switch case/if-else solution.
That's actually pretty subjective, not an inherent truth to game development. For many people, delivering a functional product might be much more important than performance.
Or just throw enough artists at the game to make it look pretty, make it console exclusive and people will label is cinematic experience GOTY contender.
Nice ideas, but I like and trust the switch/enum approach. I once wrote a PGN (text file, containing several chess games) reader/writer. It worked with states, because almost all characters had different meaning, according to the state/section/stage it was currently reading.
I kept a switch of the current character, dealing in each case with all states, this time not in a block of ifs nor switch, just a "spaghetti" code actually. Because there were some behaviours that could be reused for different states - _I love compact code, and think it's better to find bugs_ . If I instead had used a switch of states on the 1st place, each case would has a giant switch of characters, which would be awkward.
For enum, I started with the header and moves. Later, I added things that the user could write by hand, like commentaries and (alternative moving) lines, resulting in new states.
I used OO, because the user could save the game at any time, so those data from the game should be well hidden and secured.
Everything worked nicely, like a high level language. Of course I faced many easy-to-find bugs, as in any normal project. But I don't remember ever spending more than 1h catching any of those. Each error was just a matter to look at the output, to know were it read wrongly. If it was a bit harder, I took a pick on the file itself.
....what? Just use a switch function with dispatch to functions that use the enum as a template parameter. Then wrap away the cases with a macro that is configured to be self repeating with substitutions for the specific enum. The compiler will even warn you if you miss one if you omit default, and the switch is skipped at runtime if you've used casting to convert to an out of range enum(which is bad, I'm not saying you should do it, just that this doesn't preclude use of the technique) As a bonus you can define the functions inline and the compiler is allowed to inline away all the overhead of the function calls. Wrap your enum in std::atomic and it's now re-entrant and thread safe(Though, again, please don't do this as every time I've seen it in practice it is much more of an anti pattern. PLEASE just set a flag instead, or a bitmask and read the update from their to handle the state transition). This also makes all your states easily searchable by the enum keys, and you can define the implementation in a separate file. And I'm pretty sure that most of that will work even on older compilers. I know it runs with c++17 support. I think the newest feature you need is function templates though. Also, class enums are a pain if you *want* them to be implicitly convertible in places, so I often use namespace Name{ enum Type{ZERO=0, ONE, TWO}; } but I replace "Name" with the enum name, in this case State. This still provides scoping so you don't clutter the namespace, and you can reference the type with State::Type which I quite like as it's very explicit that it is a type(which I find can sometimes be unclear if a codebase has poor style) If that really bothers you though, you can also leverage a using alias to define the type however you wish(Perhaps StateT(don't use State_t as the _t suffix is actually reserved in various environments and you may accidently generate a collision)
15:38 "need to allocate on the heap" WRONG! Ever heard of a buffer (ie. Placement new). And that's your only criticism that I can see. Yet it's MUCH easier to use and more capable than the boost solution. Therefore, it wins.
Use enum for simple case and use his sml library when you need to. Do not use other options he mentioned.
What about good old switch + goto coroutines?
State machine and Behavior tree is very good abstraction for working flow. (Make debugging a lot easier.)
The link to the coroutine code sample godbolt.org/Z/SG106R does not work for me.
Oh dear. you don't use coroutines to implement state machines , coroutines **replace** state machines.
I was gonna go for MSM because of the 'ultra fast' references in the docs but maybe SML will be good enough to get started with... Thanks for the video. I'm sure it was a lot of work to put together all the details.
Take a shot of vodka everytime he says "ya know"
I don't want to die
Or 'that guy'.
@@921Ether lol
The proper way to implement it with a switch is one switch over all states. Call that regularly. If you need to change input for the state machine from other tasks ... let them set an input via setter.
This way you have all states in one big switch. Add states? Just add a case. Modify a transition? Check out the state from which the transition leaves.
Say you have state A and state B and Output O and B changes O from 5 to 6. if you transition from A to B the change of the output will occur one cycle later. Say:
Void Cyclic_Worker()
{
Switch(state)
Case A:
Output = 5
if (transition needed)
State = B
break
case B:
Output = 6
if (other_transition_needed)
State = A
}
If you need the state change and the output change on the same cycle, do two switches, one for the state change and then for the output
Of course you can write functions for the cases to make it more readable and manageable once it gets bigger
Great library, have used it myself in a Coupe of projects! Hope it will be included in Boost
Great talk, thanks for the library
Great talk. But... i think you gone wrong with coroutines. co_await, co_return and co_yield are for elide writing state machines by hand and you used them to handmade state machine again... So this "strange" code is a result of this.
It's diffrent way of thinking but this change of thinkg must be level up at level "do i need manual sate machine?"
Putting _const_ after the type is an awful eyesore.
consistent const. i find it easier to read but difficult to get people on board :)
@@bitbangs consistent is saying that *_const_*_ T& t_ is a reference to a _T_ constant, while _T& t_ is a reference to a _T_ variable.
i got my "consistent const" lingo from here:
isocpp.org/wiki/faq/const-correctness#const-ref-alt
@@bitbangs "whereas the other style _sometimes_ puts the const on the left and _sometimes_ on the right."
If that *"(...) **_sometimes_** on the right."* refers to constant pointers (char* const p), I have pratically no use for them.
"east const" vs "const west" was the bikeshed topic of last year's conference
The examples show are very contrived and simplistic. They don't tell real story in real code. Equating assembler code length to performance is totally silly. There is so many other things that are important. There shouldn't be any difference between switch, if and fold
expressions. They are all just conditions to the compiler. However, the
problem is you are comparing constant propagation and folding by the
compiler, not actual generated code where there events are dynamically
coming from somewhere else (file, network, other subsystem). There are
no benchmarks at all in the presentation. Just glancing at irrelevant
assembly code.
Interesting topic, but the presentation wasn't that enjoyable. Lots of too long awkward pauses(and then just saying "This is bad!" or "This is good!") and "um" "ya know" and other fillers.
"The STL is one of the best libraries available" 😂 Nice joke. It's only a specification and varies significantly in quality.
I like you. But probably not your best presentation...