4 minute read

I’ve been playing around with LÖVE lately and making a game with it. One of first problems I decided to solve was animated sprites so I decided to make an “object” (well really a table) to handle the drawing and animating of my sprite.

TF2 Spy Animated Sprite

The first bit is the creation of my AnimatedSprite:

AnimatedSprite = {}
AnimatedSprite.__index = AnimatedSprite

function AnimatedSprite:create(file, width, height, frames, animations)
	local object = {}

	setmetatable(object, AnimatedSprite)

	object.width = width
	object.height = height
	object.frames = frames
	object.animations = animations
	object.sprite_sheet = love.graphics.newImage(file)
	object.sprites = {}
	object.current_frame = 1
	object.current_animation = 1
	object.delay = 0.08
	object.delta = 0
	object.animating = false
	object.Directions = {
		["Down"] = 1,
		["Left"] = 2,
		["Right"] = 3,
		["Up"] = 4
	}

	return object
end

Most of the files that I am reading in are straight forward, but a couple of the special values are object.delay which is a constant I use to set the delay between updating frames of the animation. Down below you can see object.Directions which I use elsewhere as sort of like an enumerated type to be able to easily tell the code which animation to play. The numbers associated with each direction correlate for the animations for that direction on the sprite sheet. This is extremely useful and prevents me from passing around magic numbers.

The next step is loading in the individual sprites from the sprite sheet into the sprites table. All of this happens within the load function:

function AnimatedSprite:load()
	for i = 1, self.animations do
		local h = self.height * (i-1)
		self.sprites[i] = {}
		for j = 1, self.animations do
			local w = self.width * (j-1)
			self.sprites[i][j] = love.graphics.newQuad(	w,
														h,
														self.width,
														self.height,
														self.sprite_sheet:getWidth(),
														self.sprite_sheet:getHeight())
		end
	end
end

Fairly straight forward looping over the animations and frames within an animation. Unfortunately, this only supports the same number of frames in each animation. However, just padding out some extra frames with nothing and having the code detect those frames you can easily make this support animations with different numbers of frames in each. One important note about Lua (that you may noticed in this code) is that it is a 1-indexed language.

Next, we need a function to tell our animation how to update.

function AnimatedSprite:update(dt)
	if self.animating then
		self.delta = self.delta + dt

		if self.delta >= self.delay then
			self.current_frame = (self.current_frame % self.frames) + 1
			self.delta = 0
		end
	end
end

This code checks if the sprite is animating and if it is it will update the frame. The passed in dt is the delta-time for the main game loop. The self.delta keeps track of the time passed and if it has passed self.delay and if it has the frame should update and the delta gets reset back down to zero. Note here the updating of the frames uses the modulos the current_frame with the number of frames which is so the frames wrap to the beginning. However, if you have a variable number of frames self.frames should probably instead be the length of the array here.

The next few functions are fairly straight forward,

function AnimatedSprite:draw(x, y)
	-- note that drawq was removed in 0.9.0 and this should just be draw
	love.graphics.drawq(self.sprite_sheet, self.sprites[self.current_animation][self.current_frame], x, y, 0, 1, 1)
end

function AnimatedSprite:set_animation(animating)
	self.animating = animating

	if not animating then
		self.current_frame = 1
	end
end

function AnimatedSprite:set_animation_direction(direction)
	self.animating = true
	self.current_animation = direction
end

draw will draw the sprite to the screen at the provided coordinates. set_animation is used to start and stop animation and most importantly set_animation_direction uses the enumerated directions from our create to set which animation to play and tell our code that we should play the animation.

Actual usage of all of this looks something like the following,

require "animated_sprite"

function love.load()
	hero = {}
	hero.x = 300
	hero.y = 400
	hero.speed = 300
	animation = AnimatedSprite:create("sprites/hero.png", 32, 32, 4, 4)
	animation:load()
end

function love.update(dt)
	animation:update(dt)

	if love.keyboard.isDown("left") then
		hero.x = hero.x - hero.speed * dt
		animation:set_animation_direction(animation.Directions.Left)
	elseif love.keyboard.isDown("right") then
		hero.x = hero.x + hero.speed*dt
		animation:set_animation_direction(animation.Directions.Right)
	elseif love.keyboard.isDown("up") then
		hero.y = hero.y - hero.speed*dt
		animation:set_animation_direction(animation.Directions.Up)
	elseif love.keyboard.isDown("down") then
		hero.y = hero.y + hero.speed*dt
		animation:set_animation_direction(animation.Directions.Down)
	elseif love.keyboard.isDown("q") then
		love.event.push('quit')
	else
		animation:set_animation(false)
	end
end

function love.draw()
	animation:draw(hero.x, hero.y)
end

So once LÖVE is loaded up we create our hero and the AnimatedSprite associated with the hero. We then load the animation. In our love.update which is our primary game loop we check for which keyboard keys are down and then move the hero in that direction and set the animation direction to also be that direction. Note that our else statement just calls set_animation(false) to keep our hero where they currently are and stop the animation from playing. Last we have the draw() which just calls our animation’s draw function.

In the latest version of the code most of this functionality has been moved into a separate Hero object that takes care of most of these concerns.

There you have it; animated sprites in Lua using LÖVE.

Updated: