Most of our game objects will be showing sprites. Some will show a single static sprite, such a level tiles, and others, players, enemies, effects etc, will be animated. In this tutorial we will create an animation component that we can add to our game objects.
Animation is a component that stores sprites. Sprites are organized in clips. Each clip is a sequence of sprites that shows a specific animation. On our player character for example, we could have a ‘run’ clip, a ‘walk’ clip and an ‘attack’ clip.
type Animation struct {
bool
run [][]sprite.Sprite
clips int
activeClip int
activeSprite .Duration
waitTime time.Duration
frameTime time}
The activeClip
parameter controls which clip is
currently playing and activeSprite
is the sprite
within that clip that is currently showing. We load sprites in the
animation component using the AddClip
method which
simply loads all the sprites for the clip into the clips array.
The sprites must already be in the atlas.
func (a *Animation) AddClip(sprites []int, atlas *sprite.Atlas, shader *shaders.Shader, renderOrder int) int {
:= []sprite.Sprite{}
clip for i := range sprites {
, _ := sprite.NewSprite(sprites[i], atlas, shader, renderOrder)
sprite= append(clip, sprite)
clip }
.clips = append(a.clips, clip)
areturn len(a.clips) - 1
}
The next two parameters waitTime
and
frameTime
are used to control how fast the animation
plays. Frame time is the desired animation speed. The user
supplies this in frames per second (which is probably more
intuitive than frame time) and it is then converted.
func (a *Animation) SetAnimationSpeed(framerate float64) {
.frameTime = time.Duration((float64(time.Second) / framerate))
a}
The animation component is supposed to live inside a
GameObject
like this:
type Knight struct{
animation Animation
GameObjectCommon}
This is important as the component needs data from the enclosing game object to operate. We will see how in a bit.
To play the animation we use the Update
function.
This function has two parts. The first determines which sprite to
display. We keep track of the elapsed time in the variable
waitTime
. Once waitTime
exceeds frame
time we go to the next sprite. The line
a.waitTime = a.waitTime - a.frameTime
makes sure that if we overshoot the frame time, maybe because the previous game loop iteration was slow, we don’t lag behind in our animation.
func (a *Animation) Update(dt time.Duration, parent GameObject) {
.waitTime += dt
aif a.waitTime >= a.frameTime && a.run {
.activeSprite = (a.activeSprite + 1) % len(a.clips[a.activeClip])
a.waitTime = a.waitTime - a.frameTime
a}
.clips[a.activeClip][a.activeSprite].SetPosition(parent.GetTranslation())
a.clips[a.activeClip][a.activeSprite].SetScale(parent.GetScale()
a.clips[a.activeClip][a.activeSprite].SetRotation(parent.GetRotation())
a}
The second part of Update sets the sprite transform parameters
to match that of the parent. This way, the sprite is in sync with
the parent game object. Note that the animation doesn’t run at all
if the run
variable is false. We use this to
start/stop the animation. To avoid unnecessary work, only the
active sprite is updated. To render the animation we use
Render
which queues the currently active sprite for
rendering.
func (a *Animation) Render(renderer *sprite.Renderer) {
.QueueRender(&a.clips[a.activeClip][a.activeSprite])
renderer}
The following are utility methods that do self-explanatory things.
func (a *Animation) SetClip(clip int) {
if clip < 0 {
= 0
clip } else if clip >= len(a.clips) {
= len(a.clips) - 1
clip }
.activeClip = clip
a.activeSprite = 0
a}
func (a *Animation) Run() {
.run = true
a}
func (a *Animation) Stop() {
.activeSprite = 0
a.run = false
a}
func (a *Animation) Freeze() {
.run = false
a}
func (a *Animation) GetSprite(clip, n int) sprite.Sprite {
return a.clips[clip][n]
}
SetClip
changes the currently running clip. This
is used to change the animation currently playing. We would use
this, for example, to change our character from the
running
animation to the attack
animation. Run
, Stop
and
Freeze
control playback. Freeze
stops
the animation at the frame where it is currently at while
Stop
stops and resets it to the first frame.
GetSprite
is used to access the sprites in the
animation. This can be useful to query sprite parameters such as
the size.
We will use our animation component to show our knight for the Knights vs Trolls game. Since this is a new app we need a bit of setup. Let’s begin with our main game loop.
var Game struct {
*sprite.Atlas
Atlas .Shader
Shader shaders}
func main() {
:= platform.InitializeWindow(500, 500, "Knight vs Trolls", true, false)
err if err != nil {
panic(err)
}
:= sprite.NewRenderer()
renderer := color.NewColorRGBAFromBytes(4, 5, 8, 255)
bgColor .SetBGColor(bgColor.ToArray())
renderer
.Atlas, err = sprite.NewEmptyAtlas(1024, 1024)
Gameif err != nil {
panic(err)
}
.Shader, _ = shaders.NewDefaultShader()
Game
:= NewLevel()
level
:= time.Now()
timer for {
:= time.Since(timer)
dt = time.Now()
timer .Update(dt)
level.Render(renderer)
level.Render()
renderer}
}
This code initializes a window, a renderer, an atlas and a
shader and sets up the main game loop. We use a globally
accessible struct, Game
to store our atlas and
shader. We will need to access these resources from various parts
of the code so this is convenient.
The duration of each loop iteration is measured using
dt := time.Since(timer)
. We pass this value to our
scene (level) update which will pass it to the update of each game
object. For now, our level only contains a knight and is created
with:
func NewLevel() *game.Scene {
:= game.Scene{}
scene .AddGameObject(NewKnight())
scenereturn &scene
}
Next, lets define our knight.
type Knight struct {
.Animation
animation game, runClip int
idleClip.Vector2[float32]
spriteSize math.GameObjectCommon
game}
Our knight has the newly created Animation
component with which it will show sprites. We have two animation
clips for our knight, a run animation and an idle animation.
Knight is a game object so we embed GameObjectCommon
to it. This will provide default implementations for the
GameObject
functions we haven’t implicitly
defined.
We have four sprites for the idle animation and four for the run animation and we define their filenames in two arrays.
var knightIdleFrames = [4]string{
"elf_m_idle_anim_f0.png",
"elf_m_idle_anim_f1.png",
"elf_m_idle_anim_f2.png",
"elf_m_idle_anim_f3.png",
}
var knightRunFrames = [4]string{
"elf_m_run_anim_f0.png",
"elf_m_run_anim_f1.png",
"elf_m_run_anim_f2.png",
"elf_m_run_anim_f3.png",
}
Our Knight construction function, NewKnight
is
responsible for loading the sprites into the animation
component.
func NewKnight() *Knight {
:= &Knight{}
knight .animation = game.NewAnimation()
knight
// load idle animation
:= []*image.RGBA{}
spriteImages for i := range knightIdleFrames {
, _ := sprite.RgbaFromFile("data/" + knightIdleFrames[i])
img= append(spriteImages, img)
spriteImages }
, err := Game.Atlas.AddImages(spriteImages)
indices(err)
panicOnError.idleClip = knight.animation.AddClip(indices, Game.Atlas, &Game.Shader, 0)
knight
// load run animation
= nil
spriteImages for i := range knightRunFrames {
, _ := sprite.RgbaFromFile("data/" + knightRunFrames[i])
img= append(spriteImages, img)
spriteImages }
, err = Game.Atlas.AddImages(spriteImages)
indices(err)
panicOnError.runClip = knight.animation.AddClip(indices, Game.Atlas, &Game.Shader, 0)
knight
.animation.SetClip(knight.idleClip)
knight.animation.Run()
knight.animation.SetAnimationSpeed(5)
knight
// starting position and size
.SetTranslation(math.Vector3[float32]{100, 100, 0})
knight:= knight.animation.GetSprite(knight.idleClip, 0)
spr := math.Vector2ConvertType[int, float32](spr.GetOriginalSize())
spriteSize .SetScale(spriteSize.Scale(3))
knight
return knight
}
Each sprite is loaded from storage using the
sprite.RgbaFromFile
and then added to the sprite
atlas using Game.Atlas.AddImages
. When the sprites
have been loaded in the atlas we create animation clips using
animation.AddClip
. We store the clip index for run
and idle in variables so we can switch between the two if needed.
The animation is set to idle
when the game starts and
we set the speed to 5 sprites per second.
The last configuration we need to do is set the knight’s transform parameters. We start our knight at an arbitrary position . We then set the knight’s scale to be three times that of the first sprite of the idle clip (all sprites are the same size in this example). The sprites in this example are really small which is why we triple the size. Remember that the animation component will use the knight’s scale to set the display size of the sprites shown.
Our knight’s update function calls animation’s update. It
passes dt
which is the loop time we measure in the
main loop. This will allow animation to update with the requested
framerate based on the waitTime
mechanism we saw
before.
func (k *Knight) Update(dt time.Duration) {
.animation.Update(dt, k)
k}
Knight’s render simply calls animation.Render which will render the currently active sprite.
func (k *Knight) Render(r *sprite.Renderer) {
.animation.Render(r)
k}
You can find the code for this tutorial here.
Run go build && ./animation
to run it. You
should see our brave knight idling excitedly. Try switching to the
run animation and try setting the knight’s scale to something that
is not a multiple of the sprite size. In the next tutorial we will
see how to move the knight around using the keyboard.