Understanding frames in an animation and animating...

Started by Amon, March 17, 2023, 11:29:25

Previous topic - Next topic

Amon

I'm having problems with animating my dude in blitzmax. My dude's idle animation is below.

I use this to load him in.

Global JoeIdle:TImage = LoadAnimImage("data/graphics/JoeIdle.png", 64, 64, 0, 4)
Below is the entire loop for how I'm trying to control animations. Problem is I'm getting an index out of bounds for the frame and also I have misunderstood the logic of switching transitions to other animations like walk, run, jump.

All those years using Gamemaker have skewed my knowledge of how I originally used to do this. I guess that's what your forfeit when you go to wisywig style ways of setting up 2d games.

While Not KeyHit(KEY_ESCAPE)
 Cls
 If KeyDown(KEY_LEFT)
 JoeState = "run"
 JoeDirection = "left"
 Else If KeyDown(KEY_RIGHT)
 JoeState = "run"
 JoeDirection = "right"
 JoeFrame = 0
 Else If KeyDown(KEY_LCONTROL)
 JoeState = "jump"
 JoeDirection = "jump"
 Else
 JoeState = "idle"
 JoeDirection = "idle"
 EndIf
 Select JoeState
 Case "idle"
 DrawImage(JoeIdle, JoeX, JoeY, JoeFrame)
 If MilliSecs() > JoeAnimTimer + 100
 JoeFrame = JoeFrame + 1
 If JoeFrame >= 4
 JoeFrame = 0
 End If
 JoeAnimTimer = MilliSecs()
 Print "" + JoeFrame
 End If
 Case "run"
 If JoeDirection = "left"
 SetScale(-1, 1)
 DrawImage(JoeRun, JoeX, JoeY, JoeFrame)
 ElseIf JoeDirection = "right"
 SetScale(1, 1)
 DrawImage(JoeRun, JoeX, JoeY, JoeFrame)
 EndIf
 If MilliSecs() > JoeAnimTimer + 60
 JoeFrame = JoeFrame + 1
 If JoeFrame >= 6
 JoeFrame = 0
 End If
 JoeAnimTimer = MilliSecs()
 Print "" + JoeFrame
 End If
 Case "jump"
 DrawImage(JoeJump, 300, 300, JoeFrame)
 If MilliSecs() > JoeAnimTimer + 100
 JoeFrame = JoeFrame + 1
 If JoeFrame >= 3
 JoeFrame = 0
 End If
 JoeAnimTimer = MilliSecs()
 Print "" + JoeFrame
 End If
 End Select
 
 
 Flip
WEnd


 JoeIdle.png

Derron

- format your code (so indentation stays intact) as this helps to spot things

- when doing timed animation think of "lags" - should the animation stay at "constant speed" ? if so, then "nextAnimationTime" will be set with "Millisecs() + frameDuration", if you want to ensure all animations to be shown (but maybe "laggy") then do "nextFrameTime :+ frameDuration" (this can mean you will update to the next frame already in the next update)

- when adding to frames you do "frame = frame + 1" which is good if you properly check frame indexes. If speed is not an issue (modulo can still be a bit slower than "x + y; if x > z then x =0") you can always do "frame = (frame + 1) mod frameCount" as this will do 0,1,2,3, 0,1,2,3, 0,1,....

- and in your code I saw this:

Code (Blitzmax) Select
Select JoeState
  Case "idle"
    DrawImage(JoeIdle, JoeX, JoeY, JoeFrame)
    If MilliSecs() > JoeAnimTimer + 100
      JoeFrame = JoeFrame + 1
      If JoeFrame >= 4
      JoeFrame = 0
   End If

Somewhere earlier you adjust what "animation" to play ... what happens if "idle" has 5 frames and "running right" has 6 frames. Now you come from "running right" to "idle", frame was on "index 4" (so fifth frame). As JoeIdle has only 4 frames (indices 0 to 3) you get an out of bounds error.

See animation always as a "state".
Switch state? Also correct "frame to display"!
Means: either start at "0" or use a one which "blends best" between currently displayed sprite - and the one of your new movement.


bye
Ron

Amon

I'll try out your suggestions. There seems though to be a difference between the framecount used in LoadAnimImage and what it displays up to in DrawImage and I don't know why.

But I will try what you've suggested. Thanks for your time and help.

Midimaster

#3
I have another idea for you...

pack all frame for any animation of your joe into one single PNG-file and select between the different animation by changing the starting frame.

' all animation frames in one image
' f.e. 0=standing , running left =1-4 , running right=5-8 , jump=9-15 ....
Global JoeImage:TImage = LoadAnimImage("data/graphics/JoeIdle.png", 64, 64, 0, 16)

Const STANDING:Int=0, RUN_LEFT:Int=1, RUN_RIGHT:Int=5 , JUMP=9

Global JoeFrame:Int = STANDING
Global JoeJob:Int   = STANDING
Global JoeMax:Int   = STANDING+0

While Not KeyHit(KEY_ESCAPE)
Cls

'       This switches the job And restores start And End point of animation:

If KeyDown(KEY_LEFT)
JoeJob   = RUN_LEFT
JoeMax   = RUN_LEFT+3
JoeFrame = JoeJob

Else If KeyDown(KEY_RIGHT)
JoeJob   = RUN_RIGHT
JoeMax   = RUN_LEFT+3
JoeFrame = JoeJob

Else If KeyDown(KEY_LCONTROL)
JoeJob   = JUMP
JoeMax   = JUMP+6
JoeFrame = JoeJob

Else
JoeJob   = STANDING
JoeMax   = STANDING+0
JoeFrame = JoeJob
EndIf


'        this does always the neccessary depending on the limitions of the current animation:
JoeFrame = JoeFrame+1
If JoeFrame>JoeMax
JoeFrame = JoeJob
EndIf
DrawImage JoeImage 300, 300, JoeFrame
Flip
Wend


The variable JoeJob contains the starting point and also can be used for finding out which animation is running.
JoeMax defines the last frame of the animation. This can be different for each animation. In my Example STANDING has only one frame, running has 4 and JUPM needs 6 frames.


By using this CONSTANTS and the variable JoeJob and JoeMax you will need only one function to keep the frames inside the current animation.

...back from Egypt

Midimaster

#4
Here the same, but much shorter, because I use a function:
' all animation frames in one image
' f.e. 0=standing , running left =1-4 , running right=5-8 , jump=9-15 ....
Global JoeImage:TImage = LoadAnimImage("data/graphics/JoeIdle.png", 64, 64, 0, 16)

Const STANDING:Int=0, RUN_LEFT:Int=1, RUN_RIGHT:Int=5 , JUMP=9
Global JoeFrame:Int, JoeJob:Int ,JoeMax:Int
SwitchJob (STANDING,0)


While Not KeyHit(KEY_ESCAPE)
Cls

' This switches the job And restores start And End point of animation:
If KeyDown(KEY_LEFT)
SwitchJob (RUN_LEFT,3)
Else If KeyDown(KEY_RIGHT)
SwitchJob (RUN_RIGHT,3)
Else If KeyDown(KEY_LCONTROL)
SwitchJob (JUMP,6)
Else
SwitchJob (STANDING,0)
EndIf

' this does always the neccessary depending on the limitions of the current animation:
JoeFrame = JoeFrame+1
If JoeFrame>JoeMax
JoeFrame = JoeJob
EndIf
DrawImage JoeImage 300, 300, JoeFrame
Flip
Wend

Function SwitchJob(NewJob:Int, Frames:Int)
If NewJob=JoeJob Return
JoeJob   = NewJob
JoeMax   = NewJob+Frames
JoeFrame = NewJob
End Function
...back from Egypt

Kryzon

Quote from: Derron on March 17, 2023, 11:45:48See animation always as a "state".
Switch state? Also correct "frame to display"!
Means: either start at "0" or use a one which "blends best" between currently displayed sprite - and the one of your new movement.
To expand on what Derron is talking about, you need to create some abstractions / classes so you can assign each a specific purpose, making things more intuitive and easier to organize.

If I were in your position, I'd write these things:
- A class that receives input events (keypresses/releases, gamepad events, mouse events, recorded demo file steps etc) and translates them to in-game actions that your game objects will observe. Pressing the right arrow key, for example, turns on the "MOVE_RIGHT" action, and it remains on until the release of the right arrow key, which turns that action off.
- An abstract class for your character states. Each state is then a subclass of that, implementing the state with some specific code -- the falling state, for example, tests if the character has reached the ground so it can transition to the idle state. All states have three functions: enter, update, and exit. So the transition from state A to state B is: exit state A, enter state B, update state B, update state B (...).
- A class for animation controllers. Each instance of that class can be given a starting and ending frame, and an animated image asset reference. The sole responsibility of an instance is to advance a frame number that they keep track of, and loop / stop playing the animation in case it reaches the end.

So when your character is in their *idle* state, the update method of that state checks for any relevant actions that turned on that frame. In case it's "MOVE_RIGHT", the state will queue a transition to the *move right* state. The next frame, your character state handler code will call the exit method of the idle state, and then the enter method of the move right state. It's in that enter method that you'll reset the animation controller for the "moving right" animation, and maybe set the active animation controller of the character as that one that's owned by the move right state. When it's time to draw the character, your drawing code can just take the active animation controller --whatever it is-- and draw the animated image asset that it points to, with the frame that it's in.