Skip to content

Instantly share code, notes, and snippets.

@JettMonstersGoBoom
Last active August 10, 2023 21:44
Show Gist options
  • Save JettMonstersGoBoom/befcdf94953168299e20c9a442b5b5de to your computer and use it in GitHub Desktop.
Save JettMonstersGoBoom/befcdf94953168299e20c9a442b5b5de to your computer and use it in GitHub Desktop.
// build with
// millfork -t c64 state.mfk
// an example of using a state machine type interface in Millfork
// each entity has a STATE.
// each state contains
// an entry action: performed when entering the state, and
// an exit action: performed when exiting the state, and
// an updata action: performed once per frame while the state is active
// here you can change state by simply setting ``next_state = target_state.pointer``
// as shown we have a waiting state, a jumping state, and a falling state.
// NOTE "enter" & "exit" can be nullptr if you don't use them
// waiting state: waits a random amount of time, before switching to jumping state and choosing a random horizontal direction
// jumping state: floats the Y coordinates UP, once the DY ( velocity / direction ) is positive, we switch to falling
// falling state: applies stronger gravity to the Y direction (DY) until the Y coordinate is >200 then switches to waiting state
import random
// I've found using arrays like this is WAY faster than structs
const byte MAX_ENTITIES=8
// things the entity can be
enum Entity_Status { DEAD, ALIVE, BORN }
// position
array(word) Entities_X[MAX_ENTITIES]
array(word) Entities_Y[MAX_ENTITIES]
// direction / velocity
array(int16) Entities_DX[MAX_ENTITIES]
array(int16) Entities_DY[MAX_ENTITIES]
// timer
array(byte) Entities_Clock[MAX_ENTITIES]
// current status (DEAD,ALIVE,BORN)
array(Entity_Status) Entities_Status[MAX_ENTITIES]
// pointer to the current state being used
array(pointer.state_t) Entities_FSM[MAX_ENTITIES]
// State:
struct state_t {
function.void.to.void OnEnter
function.void.to.void OnUpdate
function.void.to.void OnExit
}
// ent = entity index in the above arrays
// we use this as a global vs passing it to each function to be faster
byte ent
// next state can be set in the update functions to switch state
pointer.state_t next_state
// clear and reset what's needed
void Entities_Reset()
{
byte i
for i,0,until,MAX_ENTITIES
{
Entities_Status[i] = DEAD
Entities_FSM[i] = nullptr
}
}
//----------------------------------------------
// give us the index to a free entity
//----------------------------------------------
byte Entity_New(pointer.state_t state)
{
byte i
for i,0,until,MAX_ENTITIES
{
// if it's DEAD then we can re-use it
if (Entities_Status[i] == DEAD)
{
// set up and return the index
Entities_Status[i] = BORN
Entities_FSM[i] = state
return i
}
}
// panic
return 0
}
//----------------------------------------------
// called once per object
//----------------------------------------------
inline void Entity_Tick()
{
// we don't have an FSM, so just bail out
if (Entities_FSM[ent]==nullptr) {
return
}
// if we're just born.
// call the OnEnter function
// and change status to ALIVE
if (Entities_Status[ent]==BORN)
{
if (Entities_FSM[ent]->OnEnter!=nullptr)
{
call(Entities_FSM[ent]->OnEnter)
}
Entities_Status[ent]= ALIVE
}
else if (Entities_Status[ent]==ALIVE) // NOTE: else here causes the update to not be called the same frame, change to just ``if`` if you want it the same frame
{
// if we're alive
// grab current state
next_state = Entities_FSM[ent]
// call the update function
call(Entities_FSM[ent]->OnUpdate)
// check if next_state isn't the current state
if (next_state!=Entities_FSM[ent])
{
// if it is we call exit
if (Entities_FSM[ent]->OnExit!=nullptr)
{
call(Entities_FSM[ent]->OnExit)
}
// if next state == NULLPTR then just kill us and don't do anything else
if (next_state==nullptr)
{
Entities_Status[ent]=DEAD
}
else
{
// change the runtime state to next_state
// setting status to BORN, will trigger the onEnter the next frame
// you may prefer just calling onEnter here directly, if you want it to happen in the same frame
Entities_Status[ent] = BORN
Entities_FSM[ent] =next_state
}
}
}
}
//----------------------------------------------
// it's recommended that each state is a seperate file
// this state just waits for a random amount of time before entering JUMP state
//----------------------------------------------
void StateWait_Enter()
{
Entities_DX[ent]=0
Entities_DY[ent]=0
Entities_Clock[ent]=60+(rand()&63)
}
void StateWait_Update()
{
vic_border = 5
Entities_Clock[ent]-=1
if (Entities_Clock[ent]==0)
{
next_state = state_jump.pointer
}
}
void StateWait_Exit()
{
// normally you'd do this in StateJump_Enter
// this shows how we can use the exit to do something once when we leave the state
Entities_DX[ent] = rand()
// make it signed -127 to 128
Entities_DX[ent] -= 128
// do a little jump
Entities_DY[ent].hi = 0-(rand()&7)
}
state_t state_wait = state_t(StateWait_Enter,StateWait_Update,StateWait_Exit)
//----------------------------------------------
// jump.
// applies gravity and motion to entity, switches to falling state if the DY ( direction ) is positive ( downwards )
//----------------------------------------------
void StateJump_Update()
{
vic_border = 7
Entities_X[ent]+=Entities_DX[ent]
Entities_Y[ent]+=Entities_DY[ent]
// some gravity
Entities_DY[ent]+=32
// if Y direction is down
// then we're falling
if ((Entities_DY[ent].hi&$80)==0)
{
next_state = state_fall.pointer
}
}
state_t state_jump = state_t(nullptr,StateJump_Update,nullptr)
//----------------------------------------------
// just fall until we hit 200
//----------------------------------------------
void StateFall_Update()
{
vic_border = 6
// apply downward motion
// switch to waiting state if we hit the bottom line ( 200 here )
Entities_X[ent]+=Entities_DX[ent]
Entities_Y[ent]+=Entities_DY[ent]
Entities_DY[ent]+=64 // MORE gravity than jumping
if (Entities_Y[ent].hi>200)
{
Entities_DY[ent]=0
Entities_Y[ent].lo=0
Entities_Y[ent].hi=200
next_state = state_wait.pointer
}
}
state_t state_fall = state_t(nullptr,StateFall_Update,nullptr)
//----------------------------------------------
void main()
{
byte i,i2
byte test
// C64 setup
asm {sei}
c64_ram_io()
init_rand_seed()
vic_spr_ena = $ff
// entity reset
Entities_Reset()
// make some randomly placed entities
for i,0,until,MAX_ENTITIES
{
test = Entity_New(state_wait.pointer)
Entities_X[test].hi = rand()
Entities_Y[test].hi = rand()
}
while(true)
{
// wait for raster
while vic_raster != $f3 {}
vic_border = 3
i2 = 0
// single step each entity
for ent,0,until,MAX_ENTITIES
{
if (Entities_Status[ent]!=DEAD)
{
vic_border+=1
Entity_Tick()
// copy to C64 sprite
vic_spr_coord[i2] = Entities_X[ent].hi
vic_spr_coord[i2+1] = Entities_Y[ent].hi
i2+=2
}
}
vic_border = 0
vic_bg_color0 = 0
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment