Your First Love2d Game in 200 Lines - Part 3 of 3

Posted in Tutorials

8. Creating Enemies

The logic for our enemies is going to look a lot like the logic we're using for our bullets. We'll have a timer that tells us when to create enemies, a table of enemy objects, and a couple of loops to update and draw our enemies. Let's add declarations for our timers and enemy image at the top of out main.lua.

--More timers createEnemyTimerMax = 0.4 createEnemyTimer = createEnemyTimerMax -- More images enemyImg = nil -- Like other images we'll pull this in during out love.load function -- More storage enemies = {} -- array of current enemies on screen

Then we fill in our enemyImg variable in our love.load function like so:

enemyImg = love.graphics.newImage('assets/enemy.png')

Here is where we differ from our bullet code. While bullets are created on a keypress from the player, enemies are handled independently. Let's use our timers to create new enemies every so often. Add this code to our love.update function.

-- Time out enemy creation createEnemyTimer = createEnemyTimer - (1 * dt) if createEnemyTimer < 0 then createEnemyTimer = createEnemyTimerMax -- Create an enemy randomNumber = math.random(10, love.graphics.getWidth() - 10) newEnemy = { x = randomNumber, y = -10, img = enemyImg } table.insert(enemies, newEnemy) end

Still looks pretty similar to our bullet code, right? There are still a few new concepts so bear with me. First, we're using lua's built in math.random function to generate a number between 10 and the width of the screen minus ten. This gives us a good area to create an enemy in. We want to make sure that incoming enemies are spread out. The we start the enemies partially off-screen with a y coordinate of -10. This ensures that there is no weird "pop-ins" when enemies are created.

As with bullets we need to loop through and update their positions.

-- update the positions of enemies for i, enemy in ipairs(enemies) do enemy.y = enemy.y + (200 * dt) if enemy.y > 850 then -- remove enemies when they pass off the screen table.remove(enemies, i) end end

And draw them.

for i, enemy in ipairs(enemies) do love.graphics.draw(enemy.img, enemy.x, enemy.y) end

If everything goes according to plan you can fire up the game and see enemies streaming down from above.

9. Handling Collisions

That just leaves us with the connundrum of collisions. Bullets collide with enemies. Enemies collide with the player. We need to know when and what to do. For most Love projects, users will roll out a library like HardonCollider or bump.lua. These are overkill for our project. Instead, I'm going to lift a little collision function from the Love2d wiki and place it at the top of our main.lua.

-- Collision detection taken function from http://love2d.org/wiki/BoundingBox.lua -- Returns true if two boxes overlap, false if they don't -- x1,y1 are the left-top coords of the first box, while w1,h1 are its width and height -- x2,y2,w2 & h2 are the same, but for the second box function CheckCollision(x1,y1,w1,h1, x2,y2,w2,h2) return x1 < x2+w2 and x2 < x1+w1 and y1 < y2+h2 and y2 < y1+h1 end

While we're up here lets also add a real quick check to the status of our player and a score variable.

isAlive = true score = 0

Now things get really tricky. We have two arrays of objects that we need to check for collisions and a separate entity (the player) that we also need to check. In our game, the player cannot collide with his or her own bullets, so that simplifies things slightly. Let's loop through our list of enemies then through our bullets then finally our player. It's going to look like this jumbled mess:

-- run our collision detection -- Since there will be fewer enemies on screen than bullets we'll loop them first -- Also, we need to see if the enemies hit our player for i, enemy in ipairs(enemies) do for j, bullet in ipairs(bullets) do if CheckCollision(enemy.x, enemy.y, enemy.img:getWidth(), enemy.img:getHeight(), bullet.x, bullet.y, bullet.img:getWidth(), bullet.img:getHeight()) then table.remove(bullets, j) table.remove(enemies, i) score = score + 1 end end if CheckCollision(enemy.x, enemy.y, enemy.img:getWidth(), enemy.img:getHeight(), player.x, player.y, player.img:getWidth(), player.img:getHeight()) and isAlive then table.remove(enemies, i) isAlive = false end end

Looks pretty scary, but it's not so bad. For each enemy, check each bullet and the player for coordinate overlap. If there is overlap take the appropriate action -- usually destroying one or both of the entities and incrementing the score.

10. The Game Part

Shewww, we're almost done, but it's not quite a game yet. We're marking the player as dead, but failing to actually kill him or her. After that the player needs the ability to restart the game. Fortunately, this is really easy.

First, let's wrap our .draw(player.img, ... in an if block. We only need to draw the player when he or she is alive. If the player isn't alive we'll tell them how to restart the game.

if isAlive then love.graphics.draw(player.img, player.x, player.y) else love.graphics.print("Press 'R' to restart", love.graphics:getWidth()/2-50, love.graphics:getHeight()/2-10) end

Then we handle this button press at the bottom of our update function.

if not isAlive and love.keyboard.isDown('r') then -- remove all our bullets and enemies from screen bullets = {} enemies = {} -- reset timers canShootTimer = canShootTimerMax createEnemyTimer = createEnemyTimerMax -- move player back to default position player.x = 50 player.y = 710 -- reset our game state score = 0 isAlive = true end

There you go. That's everything. You have just completed a simple game in under 200 lines, and that's a fantastic first step.

If you want to continue learning go ahead and check out the exercises in the next section by clicking here.

Support Us

Like what we're doing? Want us to keep doing it? Buy us a beer or coffee!