-
-
Save RandyGaul/bc2ae030f806ff41dd75e6d67bfbab9b to your computer and use it in GitHub Desktop.
Coro powered FSM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Coroutine-powered state machine. Each state is a separate coroutine function. | |
| // A generic driver manages one state coroutine at a time. | |
| // | |
| // Example: | |
| // init_sm(e, MY_IDLE, | |
| // [MY_IDLE] = my_idle_fn, | |
| // [MY_ATTACK] = my_attack_fn, | |
| // ); | |
| // e->sm.on_exit = my_on_exit; | |
| // e->sm.on_enter = my_on_enter; | |
| typedef struct StateMachine | |
| { | |
| int state; // Current state (readable externally). | |
| int next; // Pending transition (-1 = none). Set by sm_set (inside state) or sm_request (outside). | |
| bool paused; | |
| CF_Coroutine co; | |
| void (**fns)(CF_Coroutine); // Pointer to static state function table. | |
| int num_fns; | |
| void (*on_exit)(Entity*, int from, int to); | |
| void (*on_enter)(Entity*, int to, int from); | |
| } StateMachine; | |
| // Set next state, can be done from within a state, or externally. | |
| #define sm_set(sm, s) do { (sm)->next = (s); } while(0) | |
| #define SM_STACK_SIZE (64*1024) | |
| // State machine init -- state table defined inline as a static local. | |
| // Usage: init_sm(e, INITIAL_STATE, [STATE_A] = fn_a, [STATE_B] = fn_b, ...); | |
| #define init_sm(e, initial, ...) do { \ | |
| static void (*_sm_fns[])(CF_Coroutine) = { __VA_ARGS__ }; \ | |
| (e)->has_sm = true; \ | |
| (e)->sm = (StateMachine){ .state = (initial), .requested = -1, \ | |
| .fns = _sm_fns, .num_fns = ARRAY_SIZE(_sm_fns) }; \ | |
| } while(0) | |
| // Generic state machine driver. Loops until current state yields. | |
| void tick_state_machine(Entity* e) | |
| { | |
| StateMachine* sm = &e->sm; | |
| if (sm->paused) return; | |
| // Create coroutine on first tick (only allocation for this entity's SM). | |
| if (!sm->co.id) { | |
| sm->co = cf_make_coroutine(sm->fns[sm->state], SM_STACK_SIZE, (void*)e); | |
| if (sm->on_enter) sm->on_enter(e, sm->state, sm->state); | |
| } | |
| int MAX_ITERS = 20; | |
| for (int i = 0; i < MAX_ITERS; i++) { | |
| bool state_dead = cf_coroutine_state(sm->co) == CF_COROUTINE_STATE_DEAD; | |
| bool forced = sm->requested >= 0; | |
| if (forced || state_dead) { | |
| int old = sm->state; | |
| int next = forced ? sm->requested : sm->next; | |
| sm->requested = -1; | |
| if (sm->on_exit) sm->on_exit(e, old, next); | |
| sm->state = next; | |
| if (sm->on_enter) sm->on_enter(e, next, old); | |
| // Reinit coroutine to run new state function (zero alloc). | |
| cf_coroutine_reinit(sm->co, sm->fns[next]); | |
| } | |
| cf_coroutine_resume(sm->co); | |
| if (cf_coroutine_state(sm->co) != CF_COROUTINE_STATE_DEAD) break; | |
| if (i == MAX_ITERS - 1) { | |
| printf("WARNING: tick_state_machine hit 20 transitions in one tick (state %d)\n", sm->state); | |
| } | |
| } | |
| } | |
| //-------------------------------------------------------------------------------------------------- | |
| // Example use: | |
| Entity* spawn_player() | |
| { | |
| Entity* e = entity_alloc(); | |
| e->kind = ENTITY_PLAYER; | |
| // snip ... | |
| init_sm(e, PLAYER_IDLE, | |
| [PLAYER_IDLE] = ps_idle, | |
| [PLAYER_RUN] = ps_run, | |
| [PLAYER_JUMP] = ps_jump, | |
| [PLAYER_FALLING] = ps_falling, | |
| [PLAYER_WALL_SLIDE] = ps_wall_slide, | |
| [PLAYER_GLIDE] = ps_glide, | |
| [PLAYER_DASH] = ps_dash, | |
| [PLAYER_SLASH] = ps_slash, | |
| ); | |
| e->sm.on_exit = player_on_exit_state; | |
| e->sm.on_enter = player_on_enter_state; | |
| return e; | |
| } | |
| // Each state is just a function. yield() pauses until next frame. | |
| // Local variables survive across yields. return exits the state. | |
| void coro ps_dash(CF_Coroutine co) | |
| { | |
| Entity* e = (Entity*)co_udata(co); | |
| Player* player = &e->player; | |
| Mover* m = &e->mover; | |
| StateMachine* sm = &e->sm; | |
| player->dashing = true; | |
| play(&e->sprite, "run"); | |
| while (player->dash_timer > 0) { | |
| if (hit_wall(e, player->facing)) { | |
| sm_set(sm, PLAYER_WALL_SLIDE); | |
| return; // exits state, driver runs on_exit/on_enter, reinits coroutine | |
| } | |
| yield(); // pauses here, resumes next frame | |
| } | |
| sm_set(sm, m->on_ground ? PLAYER_IDLE : PLAYER_FALLING); | |
| // return is implicit -- coroutine dies, driver handles transition | |
| } | |
| // Two transition paths: | |
| // From inside a state: sm_set(sm, NEXT_STATE); return; | |
| // From outside: sm_request(&e->sm, NEXT_STATE); | |
| // | |
| // External requests interrupt the running state on next tick -- | |
| // the driver reinits the coroutine (abandoning the current call stack) | |
| // and starts the new state fresh. Zero allocation either way. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment