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.