Love2d: Shooting things

At some point of time people were asking me about how bullets were implemented in love.blast() so I wrote this post.

While the algorithms in the actual [mini]game were slightly more complex than what is described here, the difference is not apparent until you have thousands of bullets.

Movement

Let's start with something that will be shooting the bullets - a player.

The player needs to move around and be visible on screen. That is accomplished like so:

player = {
	x = 100,
	y = 100,
	speed = 200
}
 
function love.update(dt)
	if love.keyboard.isDown('a') then player.x = player.x - dt * player.speed end
	if love.keyboard.isDown('d') then player.x = player.x + dt * player.speed end
	if love.keyboard.isDown('w') then player.y = player.y - dt * player.speed end
	if love.keyboard.isDown('s') then player.y = player.y + dt * player.speed end
end
 
function love.draw()
	love.graphics.setColor(255, 255, 255, 224)
	love.graphics.circle('fill', player.x, player.y, 15, 8)
end
 
function love.load()
	love.graphics.setBackgroundColor(50, 75, 125)
end

At the start, a table is created to hold the player's variables.
Positions are in pixels, speed is in pixels per second.

love.update moves the player around by changing position by dt * player.speed (which grants framerate-independent pacing) if the according keys are pressed.

love.draw draws a white 8-sided circle octagon at the player's position.

love.load sets the background color to that nondescript blue.

So far not too different from the official "first game" tutorial for love2d.

Shooting

Now, to make the player actually shoot anything.

First, the player will need a few new variables to indicate the time until a bullet can be shot again, and the general duration of such pauses.

Here these are named heat and heatp ("heatPlus").

Updated player' initialization looks as following:

player = {
	x = 100,
	y = 100,
	speed = 200,
	heat = 0,
	heatp = 0.1
}

Then create a table that will contain references to the current bullets (after player' init):

bullets = { }

... and to the actual action (goes into love.update):

if love.mouse.isDown('l') and player.heat <= 0 then
	local direction = math.atan2(love.mouse.getY() - player.y, love.mouse.getX() - player.x)
	table.insert(bullets, {
		x = player.x,
		y = player.y,
		dir = direction,
		speed = 400
	})
	player.heat = player.heatp
end
player.heat = math.max(0, player.heat - dt)

What's going on here is:

  • If the left mouse button is down and the player's heat level is zero or lower (can shoot again),

    • Find the direction from player towards mouse cursor. This is accomplished by passing the difference between the mouse' position and player' position into the atan2, which returns the vector's "direction" in radians.
    • Create a bullet and insert it to the bullets-table. A table is created to hold the bullet's position/direction/speed, much like with player.
    • Reset the player's heat variable to have a delay before the next bullet can be shot.
  • Decrease the player's heat level accordingly (until it reaches 0).

Now that bullets are being added to the table, it is time to make them visible, have them move around, and have them disappear once leaving the player area.

Movement part is simple enough and goes into love.update:

for i, o in ipairs(bullets) do
	o.x = o.x + math.cos(o.dir) * o.speed * dt
	o.y = o.y + math.sin(o.dir) * o.speed * dt
end

Here the ipairs function is used to pick through all the bullets, moving them around based on their direction, velocity, and the delta time variable.

Removing bullets is a little trickier - you cannot remove elements from an array/table while looping through it, as that causes the program to "skip" elements (which are shifted in place of the current element prior to increasing index). A solution to that is to have the loop going backwards (causing no harm as shifted elements have already been checked):

for i = #bullets, 1, -1 do
	local o = bullets[i]
	if (o.x < -10) or (o.x > love.graphics.getWidth() + 10)
	or (o.y < -10) or (o.y > love.graphics.getHeight() + 10) then
		table.remove(bullets, i)
	end
end

The logic is pretty simple here - for each of the bullets, if it's coordinates are out of screen' bounds (with a 10px margin), it is removed from the bullets-table, never to be seen again.

Provided a sufficient quantity of bullets, you could instead move them to another table, and reuse instead of creation where possible ("pooling"), but that's a story for another day.

And a 10px radius octagon is to be drawn for each bullet in love.draw:

for i, o in ipairs(bullets) do
	love.graphics.circle('fill', o.x, o.y, 10, 8)
end

(same things as with moving the bullets around)

Finished code

The combined version of the program looks as following, allowing to move around with WASD keys and shoot by holding the left mouse button:

player = {
	x = 100,
	y = 100,
	speed = 200,
	heat = 0,
	heatp = 0.1
}
bullets = { }
 
function love.update(dt)
	if love.keyboard.isDown('a') then player.x = player.x - dt * player.speed end
	if love.keyboard.isDown('d') then player.x = player.x + dt * player.speed end
	if love.keyboard.isDown('w') then player.y = player.y - dt * player.speed end
	if love.keyboard.isDown('s') then player.y = player.y + dt * player.speed end
	-- shooting:
	if love.mouse.isDown('l') and player.heat <= 0 then
		local direction = math.atan2(love.mouse.getY() - player.y, love.mouse.getX() - player.x)
		table.insert(bullets, {
			x = player.x,
			y = player.y,
			dir = direction,
			speed = 400
		})
		player.heat = player.heatp
	end
	player.heat = math.max(0, player.heat - dt)
	-- update bullets:
    for i, o in ipairs(bullets) do
        o.x = o.x + math.cos(o.dir) * o.speed * dt
        o.y = o.y + math.sin(o.dir) * o.speed * dt
    end
	-- clean up out-of-screen bullets:
	for i = #bullets, 1, -1 do
        local o = bullets[i]
        if (o.x < -10) or (o.x > love.graphics.getWidth() + 10)
        or (o.y < -10) or (o.y > love.graphics.getHeight() + 10) then
            table.remove(bullets, i)
        end
    end
end
 
function love.draw()
	-- draw player:
	love.graphics.setColor(255, 255, 255, 224)
	love.graphics.circle('fill', player.x, player.y, 15, 8)
	-- draw bullets:
	love.graphics.setColor(255, 255, 255, 224)
	for i, o in ipairs(bullets) do
		love.graphics.circle('fill', o.x, o.y, 10, 8)
	end
end
 
function love.load()
	love.graphics.setBackgroundColor(50, 75, 125)
end

Hopefully this grants some insight on organization of instances in Love2d. Have fun!

Related posts:

5 thoughts on “Love2d: Shooting things

  1. thank you so much for this. I am transitioning from game maker studio to love2d and guides like this will help me catch up here a thousand times faster. thanks. im also guessing you updated this code, cause so many guides use no longer supported code. anyways thanks a mil.

  2. Your ipairs loop for the bullets will skip the next bullet whenever one is removed. The only way I know of to fix this is to use a while loop and make sure that you don’t increment your counter after removing a bullet.

    Also, since “metatable” has a specific meaning in Lua, you probably shouldn’t use that word unless you are actually referring to Lua metatables. It leads to confusion, I was looking for where you used them for a minute before I realized you just meant tables within tables.

    • Yes indeed, this post is a bit of a mess actually – is likely even older than the date says, judging by the paragraph-less formatting (which is what I had when my blog was still running on Chyrp).

      Corrected instances of “metatable” – it is correct that only tables were used here.

      As per skips, there’s also an option of making the element-removing loop go backwards – then it is of no concern if leading elements are shifted around, as they were already checked. Entity-oriented game frameworks usually queue up the things to be removed into a separate table, and then remove them all at once at the end of a frame to exclude various oddities. Changed that.

      Currently re-checking other text in the post.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.