Animation
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 Component
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 {
run bool
clips [][]sprite.Sprite
activeClip int
activeSprite int
waitTime time.Duration
frameTime time.Duration
}
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 {
clip := []sprite.Sprite{}
for i := range sprites {
sprite, _ := sprite.NewSprite(sprites[i], atlas, shader, renderOrder)
clip = append(clip, sprite)
}
a.clips = append(a.clips, clip)
return 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) {
a.frameTime = time.Duration((float64(time.Second) / framerate))
}
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.
Playing the animation
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) {
a.waitTime += dt
if a.waitTime >= a.frameTime && a.run {
a.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())
}
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) {
renderer.QueueRender(&a.clips[a.activeClip][a.activeSprite])
}
Utility Methods
The following are utility methods that do self-explanatory things.
func (a *Animation) SetClip(clip int) {
if clip < 0 {
clip = 0
} else if clip >= len(a.clips) {
clip = len(a.clips) - 1
}
a.activeClip = clip
a.activeSprite = 0
}
func (a *Animation) Run() {
a.run = true
}
func (a *Animation) Stop() {
a.activeSprite = 0
a.run = false
}
func (a *Animation) Freeze() {
a.run = false
}
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.
Setting up Knight
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 {
Atlas *sprite.Atlas
Shader shaders.Shader
}
func main() {
err := platform.InitializeWindow(500, 500, "Knight vs Trolls", true, false)
if err != nil {
panic(err)
}
renderer := sprite.NewRenderer()
bgColor := color.NewColorRGBAFromBytes(4, 5, 8, 255)
renderer.SetBGColor(bgColor.ToArray())
Game.Atlas, err = sprite.NewEmptyAtlas(1024, 1024)
if err != nil {
panic(err)
}
Game.Shader, _ = shaders.NewDefaultShader()
level := NewLevel()
timer := time.Now()
for {
dt := time.Since(timer)
timer = time.Now()
level.Update(dt)
level.Render(renderer)
renderer.Render()
}
}
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 {
scene := game.Scene{}
scene.AddGameObject(NewKnight())
return &scene
}
Next, lets define our knight.
type Knight struct {
animation game.Animation
idleClip, runClip int
spriteSize math.Vector2[float32]
game.GameObjectCommon
}
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{}
knight.animation = game.NewAnimation()
// load idle animation
spriteImages := []*image.RGBA{}
for i := range knightIdleFrames {
img, _ := sprite.RgbaFromFile("data/" + knightIdleFrames[i])
spriteImages = append(spriteImages, img)
}
indices, err := Game.Atlas.AddImages(spriteImages)
panicOnError(err)
knight.idleClip = knight.animation.AddClip(indices, Game.Atlas, &Game.Shader, 0)
// load run animation
spriteImages = nil
for i := range knightRunFrames {
img, _ := sprite.RgbaFromFile("data/" + knightRunFrames[i])
spriteImages = append(spriteImages, img)
}
indices, err = Game.Atlas.AddImages(spriteImages)
panicOnError(err)
knight.runClip = knight.animation.AddClip(indices, Game.Atlas, &Game.Shader, 0)
knight.animation.SetClip(knight.idleClip)
knight.animation.Run()
knight.animation.SetAnimationSpeed(5)
// starting position and size
knight.SetTranslation(math.Vector3[float32]{100, 100, 0})
spr := knight.animation.GetSprite(knight.idleClip, 0)
spriteSize := math.Vector2ConvertType[int, float32](spr.GetOriginalSize())
knight.SetScale(spriteSize.Scale(3))
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) {
k.animation.Update(dt, k)
}
Knight’s render simply calls animation.Render which will render the currently active sprite.
func (k *Knight) Render(r *sprite.Renderer) {
k.animation.Render(r)
}
Running
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.