- Zig 100%
| .forgejo/workflows | Update CI | |
| src | Zig 0.16.0 Update | |
| .gitignore | Add Easy FSM | |
| build.zig | Add Easy FSM | |
| build.zig.zon | Zig 0.16.0 Update | |
| LICENSE.md | Add Easy FSM | |
| README.md | Fix Version | |
Easy FSM
An easy way to create finite state machines with comptime magic.
Docs
https://7games.codeberg.page/EasyFSM/@docs
API
FsmStateTagType()- Get the tag type for a finite state machine.FsmState()- Get the state union for an FSM.Fsm()- Get the type of a finite state machine.fsm()- Create a finite state machine, has a single function calledrun().
Install
- Run
zig fetch --save=easy_fsm git+https://codeberg.org/7Games/EasyFSM#v2.0.0 - Place in your build.zig:
my_module.addImport("easy_fsm",b.dependency("easy_fsm",.{.target=target,.optimize=optimize},).module("easy_fsm"));Build Options
- Run
zig build testto run the unit tests.
Using Easy FSM
Let's create an FSM for a car's turn signal (simplified). We want states for none, left blinker, and right blinker with a blink on every second. The input will be the position of the driver's turn signal along with how much time has passed since the last update in seconds. We may represent this with:
constTurnSignalStates=struct{constStates=@This();constState=easy_fsm.FsmState(States);constUserData=struct{signal_position:enum{center,left,right},dt:f32,};user_data:UserData,// The `user_data` tag is special and lets EasyFSM know what user data we want for our states.none:struct{// Go to left or right state if signal position not in center.pubfnrun(current_state:@This(),// This can use current state as immutable.user_data:UserData,)?State{_=current_state;returnswitch(user_data.signal_position){.center=>null,// Do not change state..left=>.{.left=.{.animation_timer=0}},.right=>.{.right=.{}},// Showcase that defaults work.};}},left:struct{animation_timer:f32,// Go to center or right if signal position not in left.pubfnrun(current_state:*@This(),// Or we can use the current state as mutable.user_data:UserData,)?State{current_state.animation_timer+=user_data.dt;returnswitch(user_data.signal_position){.center=>.{.none=.{}},.left=>null,.right=>.{.right=.{.animation_timer=0}},};}},right:struct{animation_timer:f32=0,// Go to center or left if signal position not in right.pubfnrun(current_state:*@This(),user_data:UserData,)?State{current_state.animation_timer+=user_data.dt;returnswitch(user_data.signal_position){.center=>.{.none=.{}},.left=>.{.left=.{.animation_timer=0}},.right=>null,};}}};This is cool, but we never actually do anything with the animation timer to make the blinkers blink every second. We also want to blink as soon as the state transitions. Luckily, there is a way to have an on enter callback for when entering a state (and a state can even re-enter itself). Let's modify the right blinker as an example:
right:struct{animation_timer:f32=0,// Go to center or left if signal position not in right.pubfnrun(current_state:*@This(),user_data:UserData,)?State{current_state.animation_timer+=user_data.dt;returnswitch(user_data.signal_position){.center=>.{.none=.{}},.left=>.{.left=.{.animation_timer=0}},// Either re-enter state or continue being in state.// If re-entering, the `exit` function will be called if it exists this run and the `enter` function will be called if it exists next run..right=>if(current_state.animation_timer>1).{.right=.{.animation_timer=@mod(current_state.animation_timer,1)}}elsenull,};}// Blink when entering state.pubfnenter(current_state:@This(),// This is allowed to be a pointer if you want.previous_state:State,user_data:UserData,)void{_=current_state;_=previous_state;_=user_data;car_control.blink(.right);// Example code we can do.}}To make things more interesting, let's make it so when you go from the right turn signal to the left one, the car beeps at the user because they can not make up their mind:
right:struct{animation_timer:f32=0,// Go to center or left if signal position not in right.pubfnrun(current_state:*@This(),user_data:UserData,)?State{current_state.animation_timer+=user_data.dt;returnswitch(user_data.signal_position){.center=>.{.none=.{}},.left=>.{.left=.{.animation_timer=0}},// Either re-enter state or continue being in state.// If re-entering, the `exit` function will be called if it exists this run and the `enter` function will be called if it exists next run..right=>if(current_state.animation_timer>1).{.right=.{.animation_timer=@mod(current_state.animation_timer,1)}}elsenull,};}// Blink when entering state.pubfnenter(current_state:@This(),// This is allowed to be a pointer if you want.previous_state:State,user_data:UserData,)void{_=current_state;_=previous_state;_=user_data;car_control.blink(.right);// Example code we can do.}// Beep if user is too indecisive.pubfnexit(current_state:@This(),next_state:State,// This is allowed to be a pointer if you want.user_data:UserData,)void{_=current_state;_=user_data;if(next_state==.left)car_control.beep();}}Lastly, it's important to note the order of functions when running one run of the FSM:
- If the state has been changed, the
enterfunction for the current state will be ran. - The
runfunction for the current state will be ran. - If the state was changed in the
runfunction, a new current state will be set and theexitfunction for the now previous state will be ran. Now, we are ready to finally initialize and run the FSM:
varfsm=easy_fsm.fsm(TurnSignalStates,.{.none=.{}},// Starting state.);fsm.run(.{.signal_position=car_control.getSignalPosition(),.dt=dt});