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.
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
}
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.
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
}
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})
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.
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)
}
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})
}
}
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
}
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;
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})
}
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
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()
}
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)