gamedev.
gamedev6 min read

Animating Tower Defense Towers with anim8 in LÖVE 11.5

A practical walkthrough for wiring spritesheet animations into tower defense entities using anim8 and LÖVE 11.5, covering state machines, frame timing, and rotation.

Animating Tower Defense Towers with anim8 in LÖVE 11.5

Tower defense lives or dies on readability. A player needs to glance at the field and instantly parse which tower is idle, which one is winding up a shot, and which one just fired. Static sprites kill that signal. Animated sprites carry it for free, but only if your animation system stays out of the way of the gameplay loop.

This walkthrough wires anim8 into a tower defense project running on LÖVE 11.5. The focus is the part most tutorials skip: how an animation library hooks into an entity that already has targeting, cooldowns, and rotation logic. By the end you will have a tower that rotates toward enemies, plays a fire animation on shot, and falls back to idle, all without your update loop turning into a switch statement.

Why anim8 over a hand-rolled animator

You can write a 30-line animator that walks frames on a timer. Many hobby projects do exactly that. So why pull in anim8?

The honest answer is timing math. A tower defense game has dozens of towers and hundreds of projectiles. Each entity needs frame-accurate animation, pause/resume, ping-pong loops for charging effects, and one-shot animations that fire once and stop. Writing that yourself the third time costs more than a dependency. anim8 handles all of it in roughly 200 lines of Lua and ships with a grid abstraction that makes spritesheet slicing readable.

The trade-off: anim8 holds animation state per instance, so a tower with three animations (idle, fire, reload) holds three Animation objects. That is roughly 1 KB of Lua tables per tower. For a field of 50 towers, you are spending about 50 KB on animation bookkeeping, which is nothing. If you ever ship 5000 entities, you would write a custom flat-array system. Until then, anim8 wins on developer time.

Project layout

A small tower defense project benefits from a flat structure early. Folders earn their existence when files in them outnumber files in the parent.

your_project/
  main.lua
  conf.lua
  lib/
    anim8.lua
  assets/
    towers/
      arrow_tower.png
  src/
    tower.lua
    enemy.lua

anim8.lua goes in lib/ because it is third-party and you do not edit it. Sprites go in assets/. Game logic goes in src/.

The spritesheet contract

anim8 expects a spritesheet laid out as a uniform grid. Each frame must be the same width and height, and frames must be packed left-to-right, top-to-bottom. For an arrow tower with three animations, a 4x3 sheet at 64x64 per frame works well:

  • Row 1 (4 frames): idle bobbing
  • Row 2 (4 frames): fire animation
  • Row 3 (4 frames): reload

Aseprite exports this layout natively. From Aseprite, choose File > Export Sprite Sheet, set rows = 3, columns = 4, and disable padding. Padding will throw your grid off by one pixel and you will spend an hour debugging why frame 2 has a seam.

Wiring anim8 into the tower entity

Here is the core tower module. Read it once, then I will walk through the parts that are not obvious.

local anim8 = require("lib.anim8")

local Tower = {}
Tower.__index = Tower

local SHEET = love.graphics.newImage("assets/towers/arrow_tower.png")
local GRID = anim8.newGrid(64, 64, SHEET:getWidth(), SHEET:getHeight())

function Tower.new(x, y)
  local self = setmetatable({}, Tower)
  self.x, self.y = x, y
  self.angle = 0
  self.range = 180
  self.cooldown = 0
  self.fire_rate = 1.2
  self.state = "idle"

  self.animations = {
    idle = anim8.newAnimation(GRID("1-4", 1), 0.15),
    fire = anim8.newAnimation(GRID("1-4", 2), 0.05, "pauseAtEnd"),
    reload = anim8.newAnimation(GRID("1-4", 3), 0.1, "pauseAtEnd"),
  }
  self.current = self.animations.idle
  return self
end

The single SHEET and GRID at module scope matter. If you call newImage inside Tower.new, every tower allocates its own GPU texture handle. With 50 towers that is 50 redundant uploads. Module-scope sharing is the default in LÖVE and you should use it whenever the asset is read-only.

The "pauseAtEnd" argument on the fire and reload animations is the part that breaks for most beginners. Without it, the fire animation loops forever and your tower looks like a malfunctioning sprinkler. With it, the animation plays once, stops on the last frame, and waits for you to call :gotoFrame(1) and :resume() to reset it.

The update loop, flat

Tower defense towers have a simple state machine: idle, find target, rotate to face, fire if in cooldown, reload, repeat. The temptation is to nest if-statements four deep. Resist it. Flatten with early returns and a state dispatch table.

function Tower:update(dt, enemies)
  self.current:update(dt)
  self.cooldown = math.max(0, self.cooldown - dt)

  local target = self:find_target(enemies)
  if not target then
    self:set_state("idle")
    return
  end

  self.angle = math.atan2(target.y - self.y, target.x - self.x)

  if self.cooldown > 0 then
    return
  end

  self:fire(target)
end

function Tower:set_state(name)
  if self.state == name then return end
  self.state = name
  self.current = self.animations[name]
  self.current:gotoFrame(1)
  self.current:resume()
end

function Tower:fire(target)
  self:set_state("fire")
  self.cooldown = self.fire_rate
  target:take_damage(10)
end

The set_state helper guards against re-triggering the same state every frame. Without that guard, a tower with a target locked would call gotoFrame(1) 60 times a second and the fire animation would be stuck on frame 1 forever. This is the kind of bug that takes two hours to find because the symptom (no animation) does not point at the cause (animation reset every tick).

Drawing with rotation

anim8 animations expose a :draw method that accepts the same arguments as love.graphics.draw, including rotation. Pass the angle and an origin offset so the tower rotates around its center, not its top-left corner.

function Tower:draw()
  self.current:draw(SHEET, self.x, self.y, self.angle, 1, 1, 32, 32)
end

The 32, 32 at the end is the origin offset, half of the 64x64 frame size. Forget this and your tower will pivot around its corner like a swinging sign. This is mentioned in the love.graphics.draw documentation but it is easy to miss when you copy a draw call from a static sprite tutorial.

Frame timing budget

A 60 FPS game gives you 16.67 ms per frame. anim8's :update call is roughly 5 microseconds per animation on a modern laptop, measured with love.timer.getTime() deltas across 10000 calls. For 100 towers that is 0.5 ms total, about 3% of your frame budget. Drawing dominates by an order of magnitude, so optimize draw batching first if you hit a ceiling.

If you do hit a ceiling, the next step is love.graphics.newSpriteBatch, which lets you submit all tower frames in one draw call. anim8 plays nicely with sprite batches because each Animation knows its current quad, accessible via animation:getFrameInfo(). That is a separate article.

Common breakage

Three failure modes show up in almost every project:

  1. The grid coordinate string "1-4" is one-indexed, not zero-indexed. Lua convention.
  2. Animations do not auto-reset between state changes. Always pair gotoFrame(1) with resume() or you will resume on the last frame and play nothing.
  3. pauseAtEnd stops the animation but does not signal completion. To detect "fire animation finished", check animation.position == #animation.frames after each update.

Where to go next

Once towers animate cleanly, the same pattern extends to enemies (idle/walk/death) and projectiles (spin/impact). The state machine grows but the entity code stays flat because each entity owns its own animation table.

For deeper LÖVE patterns including timer libraries and tween systems, the LÖVE wiki and the hump library docs are the canonical references.

References: