Class 13 - Putting it all together
Play homework
Topics
- Working with audio
- Putting together a complete game
- Distributing games
Description
This week is dedicated to making our own first original game, with programmed enemies. We’ll also add sound to make the game more exciting, and learn how to package our games to distribute for others to play.
Class Notes
Working with audio
We’re going to add audio to a game. This section is very short. That’s because just like with a lot of things, the LÖVE API makes it very easy for us to add audio to the game.
Download these two files that we will be using in this tutorial:
sfx.ogg and song.ogg sfx.ogg and song.ogg
Let’s start with the song and make it so that it loops endlessly.
First we need to create the audio. We call this a source (the source of the audio). We can create the source with love.audio.newSource(path)
. This function takes two arguments. First is the path to the file, second is what type of source it is. This can be either "static"
or "stream"
. Let me quote the LÖVE wiki: “A good rule of thumb is to use stream for music files and static for all short sound effects. Basically, you want to avoid loading large files at once.”
So in our case we want to use "stream"
, since it’s a song.
function love.load()
song = love.audio.newSource("path/to/song.ogg", "stream")
end
Next we want to play it. There are two ways to do it.
function love.load()
song = love.audio.newSource("path/to/song.ogg", "stream")
-- method 1
love.audio.play(song)
--method 2
song:play()
end
There isn’t really a difference between the two, and if there is it’s not a difference that you will notice. So use whatever you prefer. I prefer the second method.
When you run the game the song should now play. But it doesn’t loop. How can we fix this? I can tell you, but you should learn how to find these kind of things yourself. First, let’s go to wiki page of the Source object. Now we need to look for something that might help us loop the audio. Perhaps press Ctrl + F to search on the page, and then type “loop”.
Source:setLooping Sets whether the Source should loop.
Got it!
function love.load()
song = love.audio.newSource("path/to/song.ogg", "stream")
song:setLooping(true)
song:play()
end
Okay nice, now we got looping background music. Next we want to add a sound effect. Let’s make it so that the sound effect is played every time you press Space. We start with creating the new source.
function love.load()
song = love.audio.newSource("path/to/song.ogg", "stream")
song:setLooping(true)
song:play()
-- sfx is short for 'sound effect', or at least I use it like that.
sfx = love.audio.newSource("sfx.ogg", "static")
end
Next we add the keypressed callback, and make the sound effect play every time we press “space”.
function love.keypressed(key)
if key == "space" then
sfx:play()
end
end
And we’re done. Like I said, there isn’t much to say about audio, and everything you want to learn you can easily look up yourself in the API documentation. For example, how to set the volume, how to make the source pause or how to get the source’s position.
Summary
The LÖVE API makes it very easy to add audio. We call an audio object a Source. Often we can figure out how to do something by looking through the API documentation.
A complete Game: Shoot the enemy
Let’s use everything we learned so far to create a simple game. You can read about programming and making games all you want, but to really learn it you’ll have to do it.
A game is essentially a bunch of problems that you have to solve. When you ask an experienced programmer to make PONG, he won’t look up a How to make PONG. They can divide PONG into separate problems, and know how to solve each one of them. This chapter is to show you how to split a game into multiple tasks.
The game we’ll be making is simple: An enemy is bouncing against the walls. We have to shoot it. Each time we shoot it, the enemy goes a little faster. When you miss, it’s game over and you’ll have to start over again.
For this game we’re going to use images. You’re free to use your own images, but I’m going to use these 3:
These images are made by Kenney, who makes a lot of free assets that anyone can use in their games. Check him out!
Let’s start with the 3 main callbacks, and load classic, the library we use to simulate classes.
function love.load()
Object = require "classic"
end
function love.update(dt)
end
function love.draw()
end
Let’s start with the player. Create a new file called player.lua
.
We could make a base class for all our objects, but because it’s such a short simple game we’ll do without one. Though I encourage you to improve the code at the end of this chapter by adding a base class yourself.
Task: Create a moving player
Create a Player class:
--! file: player.lua
Player = Object:extend()
function Player:new()
end
I’m going to give the panda image to my player.
function Player:new()
self.image = love.graphics.newImage("panda.png")
end
function Player:draw()
love.graphics.draw(self.image)
end
Next let’s make it possible to move our player with our arrow keys.
function Player:new()
self.image = love.graphics.newImage("panda.png")
self.x = 300
self.y = 20
self.speed = 500
self.width = self.image:getWidth()
end
function Player:update(dt)
if love.keyboard.isDown("left") then
self.x = self.x - self.speed * dt
elseif love.keyboard.isDown("right") then
self.x = self.x + self.speed * dt
end
end
function Player:draw()
love.graphics.draw(self.image, self.x, self.y)
end
And now we should be able to move our player. Let’s go back to main.lua
and load our player.
--! file: main.lua
function love.load()
Object = require "classic"
require "player"
player = Player()
end
function love.update(dt)
player:update(dt)
end
function love.draw()
player:draw()
end
As you can see, we can move our player. But our player can move out of the window. Let’s fix this with if-statements.
--! file: player.lua
function Player:update(dt)
if love.keyboard.isDown("left") then
self.x = self.x - self.speed * dt
elseif love.keyboard.isDown("right") then
self.x = self.x + self.speed * dt
end
--Get the width of the window
local window_width = love.graphics.getWidth()
--If the x is too far too the left then..
if self.x < 0 then
--Set x to 0
self.x = 0
--Else, if the x is too far to the right then..
elseif self.x > window_width then
--Set the x to the window's width.
self.x = window_width
end
end
Oops, our player can still move too far to the right. We need to include our width when checking if we’re hitting the right wall.
--If the left side is too far too the left then..
if self.x < 0 then
--Set x to 0
self.x = 0
--Else, if the right side is too far to the right then..
elseif self.x + self.width > window_width then
--Set the right side to the window's width.
self.x = window_width - self.width
end
And now it’s fixed. Our player can’t move out of the window anymore.
Task: Create a moving enemy
Now let’s make the Enemy class. Create a new file called enemy.lua
, and type the following:
--! file: enemy.lua
Enemy = Object:extend()
function Enemy:new()
end
I’m going to give the enemy the snake image, and make it move by itself.
function Enemy:new()
self.image = love.graphics.newImage("snake.png")
self.x = 325
self.y = 450
self.speed = 100
end
function Enemy:update(dt)
self.x = self.x + self.speed * dt
end
function Enemy:draw()
love.graphics.draw(self.image, self.x, self.y)
end
We need to make the enemy bounce against the walls, but let’s load it first.
--! file: main.lua
function love.load()
Object = require "classic"
require "player"
require "enemy"
player = Player()
enemy = Enemy()
end
function love.update(dt)
player:update(dt)
enemy:update(dt)
end
function love.draw()
player:draw()
enemy:draw()
end
Okay now we can see the enemy move, and we can see it move out of our window. Let’s make sure it doesn’t move out of our window like with Player.
function Enemy:new()
self.image = love.graphics.newImage("snake.png")
self.x = 325
self.y = 450
self.speed = 100
self.width = self.image:getWidth()
self.height = self.image:getHeight()
end
function Enemy:update(dt)
self.x = self.x + self.speed * dt
local window_width = love.graphics.getWidth()
if self.x < 0 then
self.x = 0
elseif self.x + self.width > window_width then
self.x = window_width - self.width
end
end
Our enemy stops at the wall, but we want to make it bounce. How are we going to make it do that? It hits the right wall, and then what? It should move to the other direction. How do we make it move to the other direction? By changing the value of speed
. And what should the value of speed
become? It shouldn’t be 100 but -100.
So should we do self.speed = -100
? Well no. Because like I said before we’ll make the enemy speed up as it gets hit, and this way it would reset its speed when it bounces. Instead, we should invert the value of speed
. So speed
becomes -speed
. In other words, if the speed were to be increased to 120, it would then become -120.
And what if it hits the left wall? At that point speed is a negative number, and we should turn it into a positive number. How can we do that? Well, negative times negative makes positive. So if we say that speed
, which at that point is a negative number, becomes -speed
, it will turn into a positive number.
function Enemy:update(dt)
self.x = self.x + self.speed * dt
local window_width = love.graphics.getWidth()
if self.x < 0 then
self.x = 0
self.speed = -self.speed
elseif self.x + self.width > window_width then
self.x = window_width - self.width
self.speed = -self.speed
end
end
Alright we got a player and a moving enemy, now all that’s left is the bullet.
Task: Be able to shoot bullets
Create a new file called bullet.lua
, and write the following code:
--! file: bullet.lua
Bullet = Object:extend()
function Bullet:new()
self.image = love.graphics.newImage("bullet.png")
end
function Bullet:draw()
love.graphics.draw(self.image)
end
The bullets should move vertical instead of horizontal.
--We pass the x and y of the player.
function Bullet:new(x, y)
self.image = love.graphics.newImage("bullet.png")
self.x = x
self.y = y
self.speed = 700
--We'll need these for collision checking
self.width = self.image:getWidth()
self.height = self.image:getHeight()
end
function Bullet:update(dt)
self.y = self.y + self.speed * dt
end
function Bullet:draw()
love.graphics.draw(self.image, self.x, self.y)
end
Now we need to be able to shoot bullets. In main.lua load the file and create a table.
--! file: main.lua
function love.load()
Object = require "classic"
require "player"
require "enemy"
require "bullet"
player = Player()
enemy = Enemy()
listOfBullets = {}
end
Now we give Player a function that creates a bullet when space is pressed.
--! file: player.lua
function Player:keyPressed(key)
--If the spacebar is pressed
if key == "space" then
--Put a new instance of Bullet inside listOfBullets.
table.insert(listOfBullets, Bullet(self.x, self.y))
end
end
And we need to call this function in the love.keypressed
callback.
--! file: main.lua
function love.keypressed(key)
player:keyPressed(key)
end
And now we need to iterate through the table and update/draw all the bullets.
function love.load()
Object = require "classic"
require "player"
require "enemy"
require "bullet"
player = Player()
enemy = Enemy()
listOfBullets = {}
end
function love.update(dt)
player:update(dt)
enemy:update(dt)
for i,v in ipairs(listOfBullets) do
v:update(dt)
end
end
function love.draw()
player:draw()
enemy:draw()
for i,v in ipairs(listOfBullets) do
v:draw()
end
end
Awesome, our player can now shoot bullets.
Task: Make bullets affect the enemy’s speed
Now we need to make it so that the snake can get hit by the bullet. We give Bullet a collision detection function.
--! file: bullet.lua
function Bullet:checkCollision(obj)
end
Do you still know how to do it? Do you still know the 4 conditions that need to be true to assure collision is happening?
Instead of returning true and false, we increase the enemy’s speed. We give the property dead
to the bullet, which we’ll use to remove it from the list.
function Bullet:checkCollision(obj)
local self_left = self.x
local self_right = self.x + self.width
local self_top = self.y
local self_bottom = self.y + self.height
local obj_left = obj.x
local obj_right = obj.x + obj.width
local obj_top = obj.y
local obj_bottom = obj.y + obj.height
if self_right > obj_left
and self_left < obj_right
and self_bottom > obj_top
and self_top < obj_bottom then
self.dead = true
--Increase enemy speed
obj.speed = obj.speed + 50
end
end
Now we need to call checkCollision in main.lua.
function love.update(dt)
player:update(dt)
enemy:update(dt)
for i,v in ipairs(listOfBullets) do
v:update(dt)
--Each bullets checks if there is collision with the enemy
v:checkCollision(enemy)
end
end
And next we need to destroy the bullets that are dead.
function love.update(dt)
player:update(dt)
enemy:update(dt)
for i,v in ipairs(listOfBullets) do
v:update(dt)
v:checkCollision(enemy)
--If the bullet has the property dead and it's true then..
if v.dead then
--Remove it from the list
table.remove(listOfBullets, i)
end
end
end
Last thing to do is to restart the game when we miss the enemy. We need to check if the bullet is out of the screen.
--! file: bullet.lua
function Bullet:update(dt)
self.y = self.y + self.speed * dt
--If the bullet is out of the screen
if self.y > love.graphics.getHeight() then
--Restart the game
love.load()
end
end
And let’s test it out. You might notice that when you hit the enemy while it’s moving to the left, it slows down. This is because the enemy’s speed is at that point a negative number. So by increasing the number the enemy slows down. To fix this we need to check if the enemy’s speed is a negative number or not.
function Bullet:checkCollision(obj)
local self_left = self.x
local self_right = self.x + self.width
local self_top = self.y
local self_bottom = self.y + self.height
local obj_left = obj.x
local obj_right = obj.x + obj.width
local obj_top = obj.y
local obj_bottom = obj.y + obj.height
if self_right > obj_left
and self_left < obj_right
and self_bottom > obj_top
and self_top < obj_bottom then
self.dead = true
--Increase enemy speed
if obj.speed > 0 then
obj.speed = obj.speed + 50
else
obj.speed = obj.speed - 50
end
end
end
And with that our game is finished. Or is it? You should try adding features to the game yourself. Or make a whole new game. It doesn’t matter as long as you keep learning and keep making games!
Summary
A game is essentially a bunch of problems that need to be solved.
Distributing your game
You made a game, and you want to share it with others. You could make them install LÖVE on their computer, but that is not necessary.
First, we need to change the title and icon. For that we will use a config file.
Create a new file called conf.lua, and put in the following code
function love.conf(t)
t.window.title = "Panda Shooter!"
t.window.icon = "panda.png"
end
Save the file. Now when you run the game you’ll see the game has the title “Panda Shooter!”, and that the icon is the panda.
This is what the config file is for. LÖVE loads conf.lua
before it starts the game and applies the configurations. For a full list of options check out the wiki.
Now that our game has the correct title and icon, let’s turn it into an executable.
First we need to package our game in a zip file. Go to the folder of your game, select all the files. Now right click, go to Send to and click on Compressed (zipped) folder. The filename is not important, but we need to change the extension to .love
(by default .zip
).
Note: This part is Windows only. Go here to get info on building your game for other platforms.
If you can’t see file extensions
Press Windows + pause/break. In the upper-left corner of the new opened window click on Control Panel. Now go to Appearance and Personalization.
Click on File Explorer options.
A new window opens. Click on the tab View. In Advanced options, make sure that Hide extension for known filetypes is unchecked.
I wrote a bat file that packages the game for you. Download this zip file, and unzip all the files in a folder.
Now move your .love
file on top of build.bat
. This creates a .zip
file in the same folder. This is the file that you will want to share with people. They have to extract all the files in a folder and open the .exe
file.
Now you need to find a place to share your game. Check out itch.io.
For more information on building your game, check out the LÖVE wiki page for it. It also tells you how to build your game for other platforms.
Castle
Castle is a client that allows you to easily share your LÖVE games. Check it out!
Summary
With conf.lua you can configure things about your game like title and icon. Select all the files in the folder of your game, put them in a zip. Change the file’s extension from .zip
to .love
. Download this zip file, and unzip all the files in a folder. Move your .love
on top of build.bat
to create a .zip
. People will have to unzip all the files in a folder and open the .exe
to play your game. We can also use Castle which is a client that allows us to share our LÖVE games.
Code homework
Credits
- Copyright (c) 2022 Sheepolution
- Space Invaders poster from Taito to show visual elements of the game, 1978
- The Binding of Isaac screenshot, 2011