Not often that I will post something, but here's a little something based from a chapter from the book 'Game Programming Patterns (https://gameprogrammingpatterns.com/)'. It's very primitive, and there are no comments, as the code is quite large.
There's also a BlitzMax (https://www.syntaxboom.com/forum/index.php?topic=130.0) version.
Demo video
(https://www.dropbox.com/scl/fi/9y6ra4650d2r3r0k2q180/example_001.webp?rlkey=gd5p2so7yavbnneh8t794ry87&st=nm4jc9lr&raw=1)
You will need to download the warpsara-nohelmet-anim-sheet-alpha_1.png (https://opengameart.org/sites/default/files/warpsara-nohelmet-anim-sheet-alpha_1.png) file from https://opengameart.org/content/space-sara (https://opengameart.org/content/space-sara) and remain it heroine.png, and place it in a directory called heroine.data the has to be created at the same level as the code below.
Save the code below as heroine.cxs.
Strict
Import mojo
Enumerate
MOVE_NONE = 0,
MOVE_LEFT = 1,
MOVE_RIGHT,
MOVE_JUMP,
MOVE_JUMP_LEFT,
MOVE_JUMP_RIGHT,
MOVE_DUCK
Const FLAG_FACE:Int = $01
Const FLAG_RUNNING:Int = $02
Class CControllerInput
Method Update:Void()
Self._inputState = PRESS_NONE
If KeyDown(KEY_S) _inputState |= PRESS_DUCK
If KeyDown(KEY_W) _inputState |= PRESS_JUMP
If KeyDown(KEY_A) _inputState |= PRESS_LEFT
If KeyDown(KEY_D) _inputState |= PRESS_RIGHT
If KeyDown(KEY_SPACE) _inputState |= PRESS_FIRE
End
Method GetAction:Int()
If _inputState & PRESS_JUMP
If _inputState & PRESS_LEFT
Return MOVE_JUMP_LEFT
Elseif _inputState & PRESS_RIGHT
Return MOVE_JUMP_RIGHT
Else
Return MOVE_JUMP
Endif
ElseIf _inputState & PRESS_DUCK
Return MOVE_DUCK
ElseIf _inputState & PRESS_LEFT
Return MOVE_LEFT
ElseIf _inputState & PRESS_RIGHT
Return MOVE_RIGHT
EndIf
Return MOVE_NONE
End
Private
Const PRESS_NONE:Int = $00
Const PRESS_LEFT:Int = $01
Const PRESS_RIGHT:Int = $02
Const PRESS_JUMP:Int = $04
Const PRESS_DUCK:Int = $08
Const PRESS_FIRE:Int = $10
Field _inputState:Int
End
Class CFrame
Field _srcX:Int
Field _srcY:Int
Field _srcW:Int
Field _srcH:Int
End
Class CAnimation
Field _frames:CFrame[]
Field _totalFrames:Int
Field _frameRate:Float
Method GetFrameRate:Float()
Return _frameRate
End
Method GetFrameData:CFrame(currentFrame:Int)
Return _frames[currentFrame]
End
Method AddFrames:Void(totalFrames:Int, frameRate:Float, column:Int, row:Int, frameWidth:Int = 48, frameHeight:Int = 48)
Local cfl:Int = 0
If _frames.Length() = 0
_frames = _frames.Resize(totalFrames)
Else
cfl = _frames.Length()
_frames = _frames.Resize(_frames.Length() + totalFrames)
EndIf
_frameRate = 1000 / frameRate
_totalFrames = _frames.Length()
Local i:Int = cfl, p:Int = column * frameWidth
While i < totalFrames
_frames[i] = New CFrame
_frames[i]._srcX = p
_frames[i]._srcY = row * frameHeight
_frames[i]._srcW = frameWidth
_frames[i]._srcH = frameHeight
i += 1
p += frameWidth
Wend
End
End
Class CHeroineState
Field _strID:String
Field _animationSequence:CAnimation
Field _idle1:CHeroineIdleState
Field _startRun:CHeroineStartRunState
Field _startRunJump:CHeroineStartRunJumpState
Field _runLoop:CHeroineRunLoopState
Field _stopRun:CHeroineStopRunState
Field _crouch:CHeroineCrouchState
Field _stand:CHeroineStandUpState
Field _startJump:CHeroineStandJumpState
Field _jumpLoop:CHeroineJumpLoopState
Field _stopJump:CHeroineStopJumpState
Method HandleInput:Void(heroine:CHeroine, action:Int)
End Method
Method Update:Void(heroine:CHeroine)
Local tick:Float = Millisecs()
Local elapsed_time:Float = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame < heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
Else
heroine._currentFrame = 0
EndIf
Motion(heroine)
Endif
End Method
Method Motion:Void(heroine:CHeroine)
If heroine._xPos < 24 heroine._xPos = 24
If heroine._xPos > DeviceWidth() - 25 heroine._xPos = DeviceWidth() - 25
End Method
Method GetCurrentFrameData:CFrame(heroine:CHeroine)
Return heroine._state._animationSequence.GetFrameData(heroine._currentFrame)
End Method
Method GetTotalFrameCount:Int()
Return _animationSequence._totalFrames
End Method
Method AddFrames:Void(totalFrames:Int, frameRate:Float, column:Int, row:Int, frameWidth:Int = 48, frameHeight:Int = 48)
If _animationSequence = Null _animationSequence = New CAnimation()
_animationSequence.AddFrames(totalFrames, frameRate, column, row, frameWidth, frameHeight)
End Method
Function CreateStates:CHeroineState()
Local state:CHeroineState = New CHeroineState()
state._idle1 = New CHeroineIdleState()
state._crouch = New CHeroineCrouchState()
state._stand = New CHeroineStandUpState()
state._startRun = New CHeroineStartRunState()
state._runLoop = New CHeroineRunLoopState()
state._stopRun = New CHeroineStopRunState()
state._startJump = New CHeroineStandJumpState()
state._jumpLoop = New CHeroineJumpLoopState()
state._stopJump = New CHeroineStopJumpState()
state._startRunJump = New CHeroineStartRunJumpState()
Return state
End Function
End
Class CHeroine
Method New()
#If CONFIG="debug"
_atlas = LoadImage("heroine_debug.png")
#Else
_atlas = LoadImage("heroine.png")
#Endif
_states = CHeroineState.CreateStates()
SetState(_states._idle1)
_xSpeed = 0
_ySpeed = 0
_flags|=FLAG_FACE
End
Method HandleInput:Void(action:Int)
_state.HandleInput(Self, action)
End
Method Update:Void()
_state.Update(Self)
End
Method Render:Void()
Local frame:CFrame = _state.GetCurrentFrameData(Self)
PushMatrix()
If _flags & FLAG_FACE
Translate(-24,-24)
DrawImageRect(_atlas, _xPos, _yPos, frame._srcX, frame._srcY, frame._srcW, frame._srcH)
ElseIf _flags & FLAG_FACE = 0
Translate(24,-24)
DrawImageRect(_atlas, _xPos, _yPos, frame._srcX, frame._srcY, frame._srcW, frame._srcH, 0, -1,1)
EndIf
PopMatrix
End
Method SetState:Void(state:CHeroineState)
_state = state
_totalFrameCount = state.GetTotalFrameCount()
_currentFrame = 0
End
Method TotalFrameCount:Int()
Return _totalFrameCount
End
Method FrameCountEnd:Int()
Return _totalFrameCount - 1
End
Method GetStateName:String()
Return _state._strID
End
Method GetLastFrameTime:Float()
Return _lastFrameTime
End
Method SetLastFrameTime:Void(value:Float)
_lastFrameTime = value
End
Method FaceLeft:Void()
_flags&=~FLAG_FACE
_currentFrame = 0
_xSpeed = -8.0
End
Method FaceRight:Void()
_flags|=FLAG_FACE
_currentFrame = 0
_xSpeed = 8.0
End
Method EnableRunning:Void()
_flags|=FLAG_RUNNING
End
Method DisableRunning:Void()
_flags&=~FLAG_RUNNING
End
Global _xPos:Float
Global _yPos:Float
Global _xSpeed:Float
Global _ySpeed:Float
Global _flags:Int
Global _currentFrame:Int
Field _atlas:Image
Field _states:CHeroineState
Field _state:CHeroineState
Field _totalFrameCount:Int
Field _lastFrameTime:Float
End
Class CHeroineCrouchState Extends CHeroineState
Method New()
_strID = "CROUCH DOWN STATE"
AddFrames(5, 16.5, 0, 13)
End
' Override the HandleInput to capture only the crouch action.
Method HandleInput:Void(heroine:CHeroine, action:Int)
' If the action input for ducking is no longer being pressed, set the action state to idle.
If Not (action = MOVE_DUCK)
If heroine._state = heroine._states._crouch heroine.SetState(heroine._states._stand)
EndIf
End
' Update any action for the crouch state.
' As this action state is not looped. It has to stop at the crouched position.
Method Update:Void(heroine:CHeroine)
Local tick:Int = Millisecs()
Local elapsed_time:Int = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame < heroine.FrameCountEnd() heroine._currentFrame = heroine._currentFrame + 1
Endif
End
End Class
' This is that animation for leaving the crouched state.
Class CHeroineStandUpState Extends CHeroineState
Method New()
_strID = "STANB UP STATE"
AddFrames(4, 16.5, 0, 14)
End
Method HandleInput:Void(heroine:CHeroine, action:Int)
End
Method Update:Void(heroine:CHeroine)
Local tick:Int = Millisecs()
Local elapsed_time:Int = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame < heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
Else
heroine.SetState(heroine._states._idle1)
EndIf
EndIf
End
End
Class CHeroineIdleState Extends CHeroineState
Method New()
_strID = "IDLE STATE ONE"
AddFrames(4, 4.5, 0, 1)
End
' Override the HandleInput to capture all actions.
Method HandleInput:Void(heroine:CHeroine, action:Int)
' The player is idling, so all inputs need to be checked.
Select action
Case MOVE_DUCK
heroine.SetState(heroine._states._crouch)
Case MOVE_LEFT
heroine.SetState(heroine._states._startRun)
heroine.FaceLeft()
Case MOVE_RIGHT
heroine.SetState(heroine._states._startRun)
heroine.FaceRight()
Case MOVE_JUMP_LEFT
heroine.SetState(heroine._states._startRunJump)
heroine.FaceLeft()
heroine.EnableRunning()
Case MOVE_JUMP_RIGHT
heroine.SetState(heroine._states._startRunJump)
heroine.FaceRight()
heroine.EnableRunning()
Case MOVE_JUMP
heroine.SetState(heroine._states._startJump)
End Select
End
' Update any action for the idle state.
Method Update:Void(heroine:CHeroine)
Super.Update(heroine)
End
End
Class CHeroineJumpLoopState Extends CHeroineState
Method New()
_strID = "JUMP LOOP STATE"
AddFrames(4, 12.5, 0, 8)
End
Method HandleInput:Void(heroine:CHeroine, action:Int)
End
Method Update:Void(heroine:CHeroine)
Local tick:Int = Millisecs()
Local elapsed_time:Int = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
Motion(heroine)
If heroine._currentFrame < heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
If heroine._currentFrame > 2
heroine._ySpeed = 8.0
EndIf
Else
heroine.SetState(heroine._states._stopJump)
EndIf
EndIf
End
Method Motion:Void(heroine:CHeroine)
heroine._yPos += heroine._ySpeed
If heroine._flags & FLAG_RUNNING
heroine._xPos += heroine._xSpeed
Super.Motion(heroine)
Endif
End
End
Class CHeroineStartRunJumpState Extends CHeroineState
Method New()
_strID = "START RUN JUMP STATE"
AddFrames(4, 12.5, 0, 6)
End
Method HandleInput:Void(heroine:CHeroine, action:Int)
End
Method Update:Void(heroine:CHeroine)
Local tick:Float = Millisecs()
Local elapsed_time:Float = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame < heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
If heroine._currentFrame > 0 heroine._ySpeed = -8.0
Else
heroine.SetState(heroine._states._jumpLoop)
heroine._ySpeed = 0.0
EndIf
Motion(heroine)
EndIf
End
Method Motion:Void(heroine:CHeroine)
heroine._yPos += heroine._ySpeed
If heroine._flags & FLAG_RUNNING
heroine._xPos += heroine._xSpeed
Super.Motion(heroine)
Endif
End
End
Class CHeroineStartRunState Extends CHeroineState
Method New()
_strID = "START RUN STATE"
AddFrames(1, 12.5, 0, 3)
End
Method HandleInput:Void(heroine:CHeroine, action:Int)
Select action
Case MOVE_LEFT
If (heroine._flags & FLAG_FACE)
heroine.FaceLeft()
Else
heroine.EnableRunning()
EndIf
Case MOVE_RIGHT
If (heroine._flags & FLAG_FACE) = 0
heroine.FaceRight()
Else
heroine.EnableRunning()
EndIf
Case MOVE_JUMP_LEFT
heroine.SetState(heroine._states._startRunJump)
heroine.EnableRunning()
Case MOVE_JUMP_RIGHT
heroine.SetState(heroine._states._startRunJump)
heroine.EnableRunning()
Default
heroine.SetState(heroine._states._stopRun)
heroine.DisableRunning()
End Select
End
Method Update:Void(heroine:CHeroine)
Local tick:Int = Millisecs()
Local elapsed_time:Int = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame > heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
Else
heroine.SetState(heroine._states._runLoop)
EndIf
Motion(heroine)
Endif
End
Method Motion:Void(heroine:CHeroine)
If heroine._xPos > 23 And heroine._xPos < DeviceWidth() - 24 heroine._xPos += heroine._xSpeed
Super.Motion(heroine)
End
End
Class CHeroineRunLoopState Extends CHeroineState
Method New()
_strID = "RUN STATE"
AddFrames(10, 12.5, 0, 4)
End
' the HandleInput to capture all actions.
Method HandleInput:Void(heroine:CHeroine, action:Int)
Select action
Case MOVE_LEFT
If (heroine._flags & FLAG_FACE)
heroine.FaceLeft()
Else
heroine._flags |= FLAG_RUNNING
EndIf
Case MOVE_RIGHT
If (heroine._flags & FLAG_FACE) = 0
heroine.FaceRight()
Else
heroine._flags |= FLAG_RUNNING
EndIf
Case MOVE_JUMP_LEFT
heroine.SetState(heroine._states._startRunJump)
heroine.FaceLeft()
Case MOVE_JUMP_RIGHT
heroine.SetState(heroine._states._startRunJump)
heroine.FaceRight()
Default
heroine.SetState(heroine._states._stopRun)
heroine.DisableRunning()
End Select
End
Method Update:Void(heroine:CHeroine)
If heroine._flags & FLAG_RUNNING
Local tick:Int = Millisecs()
Local elapsed_time:Int = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame < heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
Else
heroine._currentFrame = 0
EndIf
Motion(heroine)
EndIf
EndIf
End
Method Motion:Void(heroine:CHeroine)
If heroine._xPos > 23 And heroine._xPos < DeviceWidth() - 24 heroine._xPos += heroine._xSpeed
Super.Motion(heroine)
End
End
Class CHeroineStopRunState Extends CHeroineState
Method New()
_strID = "STOP RUN STATE"
AddFrames(3, 12.5, 0, 5)
End
Method HandleInput:Void(heroine:CHeroine, action:Int)
End
Method Update:Void(heroine:CHeroine)
Local tick:Int = Millisecs()
Local elapsed_time:Int = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame > heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
Else
heroine.SetState(heroine._states._idle1)
heroine.DisableRunning()
EndIf
Motion(heroine)
Endif
End
Method Motion:Void(heroine:CHeroine)
If heroine._xPos > 23 And heroine._xPos < DeviceWidth() - 24 heroine._xPos += heroine._xSpeed
Super.Motion(heroine)
End
End
Class CHeroineStandJumpState Extends CHeroineState
Method New()
_strID = "START STAND JUMP STATE"
AddFrames(6, 12.5, 0, 7)
End
Method HandleInput:Void(heroine:CHeroine, action:Int)
End
Method Update:Void(heroine:CHeroine)
Local tick:Float = Millisecs()
Local elapsed_time:Float = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
If heroine._currentFrame < heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
If heroine._currentFrame > 2 heroine._ySpeed = -8.0
Else
heroine.SetState(heroine._states._jumpLoop)
heroine._ySpeed = 0.0
EndIf
Motion(heroine)
EndIf
End
Method Motion:Void(heroine:CHeroine)
heroine._yPos += heroine._ySpeed
End
End
Class CHeroineStopJumpState Extends CHeroineState
Method New()
_strID = "STOP JUMP STATE"
AddFrames(2, 12.5, 0, 9)
End
Method HandleInput:Void(heroine:CHeroine, action:Int)
End
Method Update:Void(heroine:CHeroine)
Local tick:Float = Millisecs()
Local elapsed_time:Float = tick - heroine.GetLastFrameTime()
If elapsed_time >= _animationSequence.GetFrameRate()
heroine.SetLastFrameTime(tick)
Motion(heroine)
If heroine._currentFrame < heroine.FrameCountEnd()
heroine._currentFrame = heroine._currentFrame + 1
Else
heroine.SetState(heroine._states._idle1)
heroine._ySpeed = 0.0
heroine.DisableRunning()
EndIf
EndIf
End
Method Motion:Void(heroine:CHeroine)
heroine._yPos += heroine._ySpeed
If heroine._flags & FLAG_RUNNING
heroine._xPos += heroine._xSpeed
Super.Motion(heroine)
Endif
End
End
#If TARGET="html"
#HTML5_CANVAS_WIDTH=800
#HTML5_CANVAS_HEIGHT=600
#Endif
Function Main:Int()
New CGame()
Return 0
End
Class CGame Extends App
Method OnCreate:Int()
#If TARGET<>"html"
SetDeviceWindow(800, 600, 4)
#Endif
SetUpdateRate(60)
player = New CHeroine()
player._xPos = DeviceWidth() / 2
player._yPos = DeviceHeight() / 2
controls = New CControllerInput()
Return 0
End
Method OnUpdate:Int()
#If TARGET="glfw"
If KeyDown(KEY_ESCAPE) EndApp()
#Endif
controls.Update()
player.HandleInput(controls.GetAction())
player.Update()
Return 0
End
Method OnRender:Int()
Cls
DrawRect(0, (DeviceHeight() / 2) + 24, DeviceWidth(), 8)
DrawText("EXAMPLE 001: ANIMATION STATES", 0, 0)
DrawText("BASED ON STATES CHAPTER FROM: https://gameprogrammingpatterns.com/",0, 12)
DrawText("MEDIA FROM: https://opengameart.org/content/space-sara", 0, 25)
DrawText("HEROINE STATE: " + player.GetStateName(), 0, 38)
DrawText("XPOS: " + player._xPos + " - YPOS: " + player._yPos, 0, 51)
DrawText("KEYS: W,A,D,S AND ESCAPE TO QUIT", 0, 64)
player.Render()
Return 0
End
Field player:CHeroine
Field controls:CControllerInput
End
Okay. If you want to see a better example of a Cerberus-X project.
Then you can download the sources from here (https://www.dropbox.com/scl/fi/ip01t2eyqd6o349g845y5/example_001.zip?rlkey=phecuvwf85ytumc8yz5yh50yk&st=awsocc64&dl=0).
You will still need to download the image file linked above.
It's a shame that BlitzMax doesn't support method properties. It would make it a lot easier to work with.