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 (100,100)(100,100). 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.