3
\$\begingroup\$

I would love some feedback on my linear finite state machine. I say linear, as it can only handle one state at a time.

I am also giving my method of how I -work- with the state machine, as I would love feedback on these bits also!

The things I wanted out of this, were:

  1. to be able to add x states to the stack, and let the state machine handle things when one state has finished (letting a state destroy/finish itself and not worry about the consequences). this is handled with shift
  2. to be able to pause the current state, add x new state(s) and when those x states have destroyed/finished themselves, to then to resume the paused state. this is handled with unshift
  3. to be able to add x states to the end of the stack. this is handled with push

These are the things I'd like to be able to do in the future:

  1. to be able to handle 2 states at one time, for instance, opening an inventory (so moving into an InventoryState) yet still let the world tick over (so keeping the RoundState running) .. I guess some kind of meta state handler?

State Machine

const stack = []
const states = {}

function pushHelper(method, s) {
    for(let key in s) {
        const state = states[s[key]]
        stack[method](state)
    }
}

class StateMachine {

    constructor() {
        this.onStep = this.handleStep.bind(this)
    }

    /**
    * add classes into the state
    */
    add(name, state) {
        states[name] = state

        return this
    }

    /**
     * push/adds new states to the end of the stack
     */
    push() {

        // transforms arguments into an array btw (as arguments isn't a real array)
        pushHelper('push', [].slice.call(arguments))

        return this
    }

    /**
     * how to: use this method if you want to pause the current state, and add new states to the beginning
     * end current state
     * then, optionally push new states to stack
     * start the top state
     */
    unshift() {

        if(stack[0]) {
            // isDirty is the check to see if the state has been paused or not
            stack[0].isDirty = true
            stack[0].onPause()
        }

        // if has new states to add, then add those too
        pushHelper('unshift', [].slice.call(arguments))

        // call the next state
        // isDirty is the check to see if the state has been paused or not
        if(stack[0].isDirty) {
            stack[0].onResume()
        } else {
            stack[0].onStart()
        }

        return this
    }

    /**
     * how to: use this method if you want to completely end the current state
     * pop current state off stack, calling it's end function
     * then start the top state
     */
    shift() {

        // shift state off stack and call it's onEnd fn
        stack.shift().onEnd()

        // call the next state
        // isDirty is the check to see if the state has been paused or not
        if(stack[0].isDirty) {
            stack[0].onResume()
        } else {
            stack[0].onStart()
        }

        return this
    }

    handleStep() {

        // a small bit of error handling, only run if there is a state on the stack
        if(stack[0]) {
            stack[0].onStep()
        }
    }
}

Instantiating the State Machine

Please note that the StateMachine's onStep/handleStep function is called every frame within a requestAnimationFrame. Often I have a main controller that does this.

const StateMachine = new StateMachine()

StateMachine.add('roundstate', new RoundState)
StateMachine.add('welcomestate', new WelcomeState)
StateMachine.add('storestate', new StoreState)
StateMachine.add('mapstate', new MapState)

// and now a controller calls StateMachine's onStep() function every frame

The states being added are extended from a default State class:

State class

import { EventEmitter } from 'events'

export default class State extends EventEmitter {

    constructor(props, context) {
        super(props, context)

        this.onStep = this.handleStep.bind(this)
        this.onStart = this.handleStart.bind(this)
        this.onFinish = this.handleFinish.bind(this)
        this.onPause = this.handlePause.bind(this)
        this.onResume = this.handleResume.bind(this)
    }

    handleStep() {

    }

    handleStart() {

    }

    handleFinish() {

    }

    handlePause() {

    }

    handleResume() {

    }
}

How a state destroys or finishes itself

Be in a round state and the player has died.. so I want the round state to end completely and to move into the gameover state to end the game.

// currently in round state
// and player has died, move to gameover state
// finishing the round state
if(playerHasDied) {
    StateMachine.shift('gameover')
}

Be in a round state, and move to the store. Here the user must be able to exit the store state, and pick up the round state where they left off.

handleGoToStore() {
    StateMachine.unshift('store')
}

User has just started the game. So I want to add the loading state (which just loads images and presents a loading bar to the user), then the initialising state (which may handle some technical initialisation) then welcome state (which is the main menu screen). So when the loading state destroys itself it will move to the init state, and when the init state destroys itself it will move to the welcome state.

StateMachine.push('loading', 'init', 'welcome')
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.