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:
- The grid coordinate string
"1-4"is one-indexed, not zero-indexed. Lua convention. - Animations do not auto-reset between state changes. Always pair
gotoFrame(1)withresume()or you will resume on the last frame and play nothing. pauseAtEndstops the animation but does not signal completion. To detect "fire animation finished", checkanimation.position == #animation.framesafter 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: