DEV Community

Cover image for State Machine for game development in GoLang
Ajinkya Borade
Ajinkya Borade

Posted on • Updated on

State Machine for game development in GoLang

Recently I started learning Go programming language and wanted to build a 2D game. So I choose pixel a GoLang 2D game library. Below post describes my learning process.

The pixel lib's examples are really good for learning Go and Pixel. I made this map using Tiled tilemap editor and loaded the .tmx files with tilepix, earlier I was using go-tmx.

GoLang 2D game

tl;dr;
I wanted to implement my characters different walk and wait states. But Wanted something easy to maintain and easy to manage the memory. Below is a my take on State Machine implementation with Go

p.s. StateMachine.go is at the bottom of article. Github Source

As I was looking for some solutions, I started with Graph nodes. https://github.com/steelx/go-graph-traversing/blob/master/main.go

And did a simple implementation with Nodes (a simple text based game with Go): https://github.com/steelx/go-story-mode

type Choice struct {
    cmd         string
    description string
    nextNode    *StoryNode
}

type StoryNode struct {
    Text    string
    choices []*Choice
}
Enter fullscreen mode Exit fullscreen mode

But I wanted to make a 2D old school type RPG game. Which is still a work in progress. You can see below work so far, using State Machine in Go.

GoLang 2D game

When we move the character he jumps from tile to tile.

My character has different frames for animations, but Wanted to keep my Update() loop clean so following code does not contain any code regarding Animations and Tween.

The following solution is using State Machine for my game in GO. So our game hero character will have an Entity (Entity can be any character, or NPC) and a Controller (state-machine)

Our Character Object

type Character struct {
    mEntity     *Entity
    mController *StateMachine
}
Enter fullscreen mode Exit fullscreen mode

My setup() is below (Setup runs outside of the Pixel FOR loop, gets called inside the Run function pixel Run guide )

gHero = Character{
        mEntity: CreateEntity(CharacterDefinition{
            texture: pic, width: 16, height: 24,
            startFrame: 1,
            tileX:      9,
            tileY:      2,
        }),
        mController: StateMachineCreate(
            map[string]func() State{
                "wait": func() State {
                    return WaitStateCreate(gHero, *CastleRoomMap)
                },
                "move": func() State {
                    return MoveStateCreate(gHero, *CastleRoomMap)
                },
            },
        ),
    }

// Give game Hero an Initial state
gHero.mController.Change("wait", Direction{0, 0})
Enter fullscreen mode Exit fullscreen mode

gHero is defined outside func main to be able to declare it as Global.

The state machine is made of two states, move and wait, as shown below. Input from the user is only checked during the wait state.

state-machine-logic

The StateMachine type requires each state to have four functions: Enter, Exit, Render and Update. But to keep 2 different return types; thats possible with Interface. Instead of specific struct as return type, we can specify State interface as return type for our mController which is a State Machine.

State Interface

type State interface {
    Enter(data Direction)
    Render()
    Exit()
    Update(dt float64)
}

Enter fullscreen mode Exit fullscreen mode

The state machine uses two state types, WaitState and MoveState. We need to implement both of these types but let’s start with the WaitState.

state_machine_wait_state.go

package main

import "github.com/faiface/pixel/pixelgl"

type WaitState struct {
    mCharacter  Character
    mMap        GameMap
    mEntity     *Entity
    mController *StateMachine
}

func WaitStateCreate(character Character, gMap GameMap) State {
    s := &WaitState{}
    s.mCharacter = character
    s.mMap = gMap
    s.mEntity = character.mEntity
    s.mController = character.mController
    return s
}


//State interface implemented below
func (s *WaitState) Enter(data Direction) {
    // Reset to default frame
    s.mEntity.SetFrame(s.mEntity.startFrame)
}

func (s *WaitState) Render() {
    //pixelgl renderer
}

func (s *WaitState) Exit() {}

func (s *WaitState) Update(dt float64) {
    if global.gWin.JustPressed(pixelgl.KeyLeft) {
        s.mController.Change("move", Direction{-1, 0})
    }
    if global.gWin.JustPressed(pixelgl.KeyRight) {
        s.mController.Change("move", Direction{1, 0})
    }
    if global.gWin.JustPressed(pixelgl.KeyDown) {
        s.mController.Change("move", Direction{0, 1})
    }
    if global.gWin.JustPressed(pixelgl.KeyUp) {
        s.mController.Change("move", Direction{0, -1})
    }
}


Enter fullscreen mode Exit fullscreen mode

The WaitState, shown in above, really does only one thing; it waits until an arrow key has been pressed and then changes the state to the MoveState, passing along the direction the player wants to move.

type Direction struct {
    x, y float64
}
Enter fullscreen mode Exit fullscreen mode

The OnEnter function for the WaitState resets the entity frame back to its original starting frame. This means if we tell a character to wait and they’re mid-run, they don’t stay stuck mid-run; instead they revert to the default standing frame.

In WaitState's Update func calls with an x and y field is passed into the Change function when the state changes. This data tells the MoveState which direction we want the player to move. A figure showing the movement offsets can be seen below;

direction

Let’s implement the MoveState

The MoveState has a bit more going on than the WaitState.

state_machine_move_state.go

package main

type MoveState struct {
    mCharacter  Character
    mMap        GameMap
    mEntity     *Entity
    mController *StateMachine
    // ^above common with WaitState
    mTileWidth       float64
    mMoveX, mMoveY   float64
    mPixelX, mPixelY float64
    mMoveSpeed       float64
}

func MoveStateCreate(character Character, gMap GameMap) State {
    s := &MoveState{}
    s.mCharacter = character
    s.mMap = gMap
    s.mTileWidth = gMap.mTileWidth
    s.mEntity = character.mEntity
    s.mController = character.mController
    s.mMoveX = 0
    s.mMoveY = 0
    //Additional motion tween can be added here e.g. 
        //s.mTween = TweenCreate(0, 0, 1),
    s.mMoveSpeed = 0.3
    return s
}

//StateMachine requires each state to have
// four functions: Enter, Exit, Render and Update

func (s *MoveState) Enter(data Direction) {
    //save Move X,Y value to used inside Update call
    s.mMoveX = data.x
    s.mMoveY = data.y
    s.mPixelX = s.mEntity.mTileX
    s.mPixelY = s.mEntity.mTileY
    //s.mTween = TweenCreate(0, 1, s.mMoveSpeed)
}

func (s *MoveState) Exit() {
    s.mEntity.TeleportAndDraw(s.mMap)
}

func (s *MoveState) Render() {
    //pending
}

func (s *MoveState) Update(dt float64) {
    //s.mTween.Update(dt)
    //update tween if any

    x := s.mPixelX + s.mMoveX
    y := s.mPixelY + s.mMoveY
    s.mEntity.mTileX = x
    s.mEntity.mTileY = y

    //If you have implemented tween animation
    // change the state here once its finished
    //Change("wait")

    s.mController.Change("wait", Direction{0, 0})
}

Enter fullscreen mode Exit fullscreen mode

At Enter() function we receive the Direction and keep a copy of direction; and set the next tile position

s.mMoveX = data.x
s.mMoveY = data.y
Enter fullscreen mode Exit fullscreen mode

Exit() function reads those tile position and draws the character to next tileX and tileY position.

In short;
Moving from the MoveState to the WaitState causes the state machine to call the MoveState.Exit() function. In the exit function we update the entity tile position and teleport to that position.

state_machine.go

package main

/*
    mController :
    StateMachineCreate({
        "wait" : func() return WaitStateCreate(Entity, GameMap),
        "move" : func() return MoveStateCreate(Entity, GameMap),
    })
*/

//StateMachine aka mController
type StateMachine struct {
    states  map[string]func() State
    current State
}

func StateMachineCreate(states map[string]func() State) *StateMachine {
    return &StateMachine{
        states:  states,
        current: nil,
    }
}

//Change state
// e.g. mController.Change("move", {x = -1, y = 0})
func (m *StateMachine) Change(stateName string, enterParams Direction) {
    if m.current != nil {
        m.current.Exit()
    }
    m.current = m.states[stateName]()
    m.current.Enter(enterParams) //thinking.. pass enterParams
}

//dt here is Delta time
func (m *StateMachine) Update(dt float64) {
    m.current.Update(dt)
}

func (m *StateMachine) Render() {
    m.current.Render()
}

Enter fullscreen mode Exit fullscreen mode

I hope my post will help you in implementation of State Machine in Go, you can even use this code in your animation projects for game. It's upto you now how to move ahead with the above code, good luck.

Top comments (0)