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

Posted in Tutorials

In part 1 we drew the player object to the screen. You may have noticed something a tad bit counterintuitive -- the origin point (0,0) isn't in the bottom left like we were taught in algebra. Instead it is in the top left. Each individual image also has an origin point in the top left. So when we're picking a position for a image what we're really measuring is the distance from the top-left of the screen to the top-left of the image in pixels.

4. Creating A Player Object

In the last tutorial we placed the player at 100,100 but that value isn't going to be static. Instead, the player will be moving. We need to create variables to store our current player location. While we could just add an x and y variable to our file, I prefer to store related variables together. Let's change our player variable to be a "table" or "object."

Let's remove the playerImg variable. This will cause an error in the draw command, but we'll fix that in a second. Then lets add the following declaration where playerImg used to be.

player = { x = 200, y = 710, speed = 150, img = nil }

Now we have a player table that contains an x and y coordinate, a speed we'll be using later, and an img. Note that I've left the img value blank. That's because we need to fill it in at runtime. Where you currently have playerImg = love.graphics.newImage('assets/plane.png') change playerImg to player.img. Do the same to the playerImg used in the love.draw function.

If you run the application now -- and you should -- you'll see exactly the same thing as you did before.

Let's make use of our new player variables. Change our draw command to read:

love.graphics.draw(player.img, player.x, player.y)

5. Adding User Input

You should now see our player image centered in the bottom of the screen, right where it should be. If we change player.x or player.y the player will move accordingly. Let's add some user input. Replace your current empty love.update with

-- Updating function love.update(dt) -- I always start with an easy way to exit the game if love.keyboard.isDown('escape') then love.event.push('quit') end if love.keyboard.isDown('left','a') then player.x = player.x - (player.speed*dt) elseif love.keyboard.isDown('right','d') then player.x = player.x + (player.speed*dt) end end

When you run our game again you'll be able to move the player back and forth and exit out just by hitting the escape key. If you feel your player should be faster or slower you can change player.speed to a higher or lower number. We multiple by dt standing for delta-time or the change in time since the last update call to account for framerate differences between machines.

We do have a few issues though. The main one is concerning bounding. Our player can just fly right off screen. We'll have to check his current position before we move him. Change our movement key block in the update function to the following:

if love.keyboard.isDown('left','a') then if player.x > 0 then -- binds us to the map player.x = player.x - (player.speed*dt) end elseif love.keyboard.isDown('right','d') then if player.x < (love.graphics.getWidth() - player.img:getWidth()) then player.x = player.x + (player.speed*dt) end end

All we're doing is making sure our player.x variable is within our game world. For our right bound we get the width of the screen (love.graphics.getWidth()) and subtract the width of the player image (player.img:getWidth()). This gives us the correct value for the top-left corner position of our player.

6. Creating Bullets

Are you ready for the tricky part? We need to allow our player to fire. This means creating and tracking an entire table of bullet objects, drawing them, updating them, etc., and using timers to track when our player is allowed to fire. Let's start by declaring some variables at the top of the file.

-- Timers -- We declare these here so we don't have to edit them multiple places canShoot = true canShootTimerMax = 0.2 canShootTimer = canShootTimerMax -- Image Storage bulletImg = nil -- Entity Storage bullets = {} -- array of current bullets being drawn and updated

While we're here let's load our bullet image (stored in bulletImg) in our load function. Our load function needs to contain the following line:

bulletImg = love.graphics.newImage('assets/bullet.png')

You're probably wondering why we're not using an object for bullet like we did with player. Bullet will be an object but we're going to declare that object with each bullet created. To say ourselves some loading time we're going to load the image ahead of time, store it by itself, and just reference it later when we create the bullet itself.

Those timers aren't going to take care of themselves. We'll have to deal with them in our update loop. We'll be manually subtracting values each time the frame updates. Here is the code:

-- Time out how far apart our shots can be. canShootTimer = canShootTimer - (1 * dt) if canShootTimer < 0 then canShoot = true end

You'll note that we're not just subtracting 1 on each update. Instead we're subtracting 1 × dt. Like in our other calculations we want to account for varying framerates between computers.

After we do our subtraction we check our timer value and if it is under 0 we set our canShoot variable to true. Later in our update function we'll check this value and our keypresses. That code reads as follows:

if love.keyboard.isDown('space', 'rctrl', 'lctrl', 'ctrl') and canShoot then -- Create some bullets newBullet = { x = player.x + (player.img:getWidth()/2), y = player.y, img = bulletImg } table.insert(bullets, newBullet) canShoot = false canShootTimer = canShootTimerMax end

We're giving the player a lot of options here. They can use space or either control button as their fire key. However, we can't just create a new bullet if one of those keys is down. If we do that the player will be able to create a constant stream of bullets, literally hosing down enemies. You can see this by removing and canShoot from our code above.

The next line creates a new object called newBullet. Like the player object, we give bullets an x and y position as well as an image. Our y position is just the player's y position. This will put the bullet at the top of our player image. For our x position we need to do a little math. We take the player's x position and move over by half the width of the player's image. This gives us the center x position of the player, and means the bullet will be "fired" from the top center of our player image.

Finally we get to our table.insert call. This is one of lua's built in functions for dealing with tables. Earlier we declared an empty table called bullets. Now we'll use table.insert to add our entire newBullet object to the bullets table. Later we'll loop through this table and update each bullet object.

Actually, we're going to loop through those twice. First, in our draw function:

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

Then in our update function:

-- update the positions of bullets for i, bullet in ipairs(bullets) do bullet.y = bullet.y - (250 * dt) if bullet.y < 0 then -- remove bullets when they pass off the screen table.remove(bullets, i) end end

Both of these little code snippets are loops. Here we take a table full of bullet objects and perform the same operations on each one. The loop is just saying for each bullet in bullets do x. However, we're going to use a curious lua function called ipairs. In our loop we have both a bullet object, bullet, and the index (location) of that object, i. These values are returned by ipairs. We use that index in our table.remove function to ensure we're not wasting time drawing or updating bullets that have left the screen.

That concludes part 2. In part 3 we'll create enemies, handle collisions, and make a real game out of this.

Go to Part 3

Support Us

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