Class 7 - Debugging and the Camera
Play homework
Topics
- Techniques in debugging
- Using a camera
Description
This class is all about mistakes: how to find them and solve them. We’ll also learn to work with the ‘camera’ so that our game can go beyond the limits of a single screen.
Class notes
Debugging
A bug is something that goes wrong in a program (or in our case game). Debugging is the art of fixing these bugs so that they don’t occur anymore. As a programmer it’s only natural to encounter lots of bugs, so debugging is a very valuable skill to have.
I wrote a short game.
function love.load()
circle = {x = 100, y = 100}
bullets = {}
end
function love.update(dt)
for i,v in ipairs(bullets) do
v.x = v.x + 400 * dt
end
end
function love.draw()
love.graphics.circle("fill", circle.x, circle.y, 50)
for i,v in ipairs(bullets) do
love.graphics.circle("fill", v.x, v.y, 10)
end
end
function love.keypressed()
if key == "space" then
shoot()
end
end
function shoot()
table.insert(bullets, {circle.x, circle.y})
end
When you press space
it will shoot a bullet. Or at least that’s what is supposed to happen, but it doesn’t work. I don’t see any bullets appearing. Let’s try to find out why that is.
Here are some of the reasons I can think of for why it’s not working.
- It doesn’t shoot the bullets.
- It’s shooting the bullets, but it’s not drawing them correctly.
- It’s drawing the bullets, but they are in the wrong position.
To figure out where it goes wrong we can use print
. For example, let’s use print
inside the for-loop in love.update
to check the x-position of the circle, and if it even gets there to begin with. Because if the bullets aren’t spawning, the print
will never be reached.
function love.update(dt)
for i,v in ipairs(bullets) do
v.x = v.x + 400 * dt
print(v.x)
end
end
Remember that you can add the following code at the top of your main.lua
to have the output appear right away, and you don’t need to close your game for it.
io.stdout:setvbuf("no")
Run the game and press space a few times to shoot. As you can see there are no prints in your output panel. Looks like we’re not filling our bullets
table. Let’s make sure that we actually call the method shoot
by putting a print
there.
function shoot()
table.insert(bullets, {circle.x, circle.y})
-- Did you know that print takes infinite amount of arguments?
print("How many bullets?", #bullets)
end
Try to shoot again and you’ll see that we still don’t see anything printed to your output panel. Weird, because our if-statement says if key == "space"
, and we’re definitely pressing space. But to be sure, let’s print the value of key
. And who knows, maybe we spelled love.keypressed
wrong and it doesn’t reach that code at all.
function love.keypressed()
-- Adding texts like these gives context to your print.
-- This is especially useful when you have multiple prints.
print("What is the key?", key)
if key == "space" then
shoot()
end
end
When you try to shoot again you’ll see that now there is something being printed. Looks like the value of key
is nil
. How is that possible, because LÖVE passes the key as first argument. But wait, we don’t have a parameter called key
. I forgot to add it when making the function. So let’s fix that.
function love.keypressed(key)
print("What is the key?", key)
if key == "space" then
shoot()
end
end
Okay so now when you press space, it still doesn’t shoot, but something else happens. You get an error.
Reading and fixing errors
An error occurs when the code is trying to execute something that isn’t possible. For example, you can’t multiply a number by a string. This will give you an error
print(100 * "test")
Another example is trying to call a function that doesn’t exist.
doesNotExist()
In our bullet shooting game we get the following error:
So what exactly does the error tell us? Because a mistake that a lot of beginners make is that they don’t realize that the error message tells you exactly why and where the error occurs.
main.lua:10:
This tells us the error occurs on line 10 (this may be a different line for you).
attempt to perform arithmetic on field 'x' (a nil value)
Arithmetic means a calculation, e.g. using +
, -
, *
, etc. It tried to calculate using the field x
, but x
is a nil
value.
So that’s weird, because we give the table an x
and y
value, right? Well no. We add the values to the table, but we don’t assign them to a field. Let’s fix that.
function shoot()
table.insert(bullets, {x = circle.x, y = circle.y})
end
And now it finally works. We can shoot bullets!
Let’s look at some new code where I create a circle class, and draw it a few times (you don’t necessarily have to write along).
Object = require "classic"
Circle = Object:extend()
function Circle:new()
self.x = 100
self.y = 100
end
function Circle:draw(offset)
love.graphics.circle("fill", self.x + offset, self.y, 25)
end
function love.load()
circle = Circle()
end
function love.draw()
circle:draw(10)
circle:draw(70)
circle.draw(140)
circle:draw(210)
end
Upon running this I get the following error:
“Attempt to index” means it tried to find a property of something. So in this case it tried to find the property x
on the variable self
. But according to the error, self
is a number value, so it can’t do that. So how did this happen? We use a colon (:) for our function so that it automatically has self
as first parameter, and we call the function with a colon so that it passes self
as first argument. But apparently somewhere something went wrong. To know where it went wrong, we can use the Traceback.
The bottom part of the error tells us the “path” it took before reaching the error. It’s read from down to top. You can ignore the xpcall
line. The next line says main.lua:21: in function 'draw'
. Interesting, let’s take a look. Ah yes, I see. On line 21 I used a dot instead of a colon (circle.draw(140)
). I changed it to a colon and now it works!
Syntax errors
A syntax error occurs when the game can’t start to begin with, because something is wrong with the code. It tries to “read” the code but can’t understand it. For example, remember how you can’t have a variable name starting with a number? When you try to do so it will give you an error:
Take a look at the following code (again, no need to type along)
function love.load()
timer = 0
show = true
end
function love.update(dt)
show = false
timer = timer + dt
if timer > 1 then
if love.keyboard.isDown("space") then
show = true
end
if timer > 2 then
timer = 0
end
end
function love.draw()
if show then
love.graphics.rectangle("fill", 100, 100, 100, 100)
end
end
It gives me the following error:
<eof>
means end of file. It expects an end
at the end of the file. Is that how we fix it, placing an end
at the end of the file? Well no. Somewhere I messed up, and we need to fix it correctly. It says that it expects the end
to close the function at line 6, so let’s start from there and go down. After opening the function I start an if-statement, and then another if-statement. I close the second if-statement and start another if-statement. I close that if-statement as well and then close the outer if-statement. No, wait, that’s not right. I should be closing the function at that point. I forgot to add an end
somewhere in the function. Let me fix that.
function love.update(dt)
show = false
timer = timer + dt
if timer > 1 then
if love.keyboard.isDown("space") then
show = true
end
if timer > 2 then
timer = 0
end
end
end
And now it works. This is why indentation in your code is so important. It helps you see where you made a mistake like this one.
Another common mistake is one like the following:
function love.load()
t = {}
table.insert(t, {x = 100, y = 50)
end
Which gives me the follow error:
It’s because I didn’t close the curly brackets.
function love.load()
t = {}
table.insert(t, {x = 100, y = 50})
end
Asking for help
Now maybe you have a bug, and you can’t fix it. You tried debugging it but you just can’t figure out why it won’t work, and you feel like you need help. Well then lucky for you there are plenty of people on the internet that are happy to help you. The best places to ask your question are on forums or in the Discord. But asking a question is not simply asking “Hey guys I got this bug where this happens, what do I do?”. You need to provide them information that they can use to help you. For example:
- What exactly goes wrong and what do you expect/want to happen?
- Explain what you have tried to do so far to fix it.
- Show the code of where (you think) it goes wrong.
- Share the .love file so that other people can try it out themselves.
But before you ask for help you may want to search for your question. It just might be that it’s a common question and it has been answered multiple times.
And remember, no one is obligated to help you, and the ones that do all do it for free, so be kind :)
Rubber duck debugging
You could also get a rubber duck. There’s this thing called rubber duck debugging. The idea is that when you explain what you’re doing in your code, you often realize what you’re doing wrong and fix the bug yourself. So instead of explaining it to someone, you explain it to your rubber duck.
Summary
We can use print
to find the source of our bug. Error message tell us exactly what is going wrong. Syntax errors are errors that occur because it can’t “read” the code correctly. Indentation is useful because it prevents us from getting end of line-errors. We can ask for help on the internet but we should provide enough information on what is going on to make it less difficult on the people helping us.
Cameras and canvases
Camera
When you have a big level that doesn’t fit the screen, we use a “camera” that follows the player around. Let’s add a camera to the game we made in the previous chapter, where we have a player that collects coins.
So what does a camera do exactly? Basically it moves everything that we draw in a certain way so that the player is in the center. Now how would you solve this? We could have a variable like camera_offset
and pass that to everything we’re drawing.
love.graphics.circle("line", player.x - camera.offset.x, player.y - camera.offset.y, player.size)
But if we have a very big game with a lot of things to draw, that’s a lot of extra work. What we instead can do is use love.graphics.translate()
. This function is part of the Coordinate System. Just like how you can move, rotate and scale an image, we can move, rotate and scale what we draw on. What we draw on is called the canvas (more on this later).
Normally when you draw something on position (x=0,y=0), it will be drawn in the upper-left corner. With love.graphics.translate(400, 200)
we move our canvas. Now if we draw something on (0,0) it will be drawn on position (400,200) instead. If we use love.graphics.scale(2)
then everything we draw will be scaled up. Play around with it to really understand what it’s doing.
So let’s use love.graphics.translate(x, y)
to create a camera. The question is in what way do we move the screen so that the player will appear in the center. First let’s get the player located in the top-left of the screen. We do this by making the negative value of the player’s position the (0,0) point.
With the upper-left corner on (0,0) (e.g. the default) the player is drawn on its position (100, 50) like normal. But if we were to move that point with love.graphics.translate(-100, -50)
it is now the player that is drawn in the upper-left corner. But we don’t want it there, we want it in the center. So we add half the width and half the height of the screen (400 and 300).
A demonstration of what is happening. First we translate with love.graphics.translate(-player.x, -player.y)
to get the player in the upper-left corner. Next we move the player to the center with love.graphics.translate(-player.x + 400, -player.y + 300)
.
--code
function love.draw()
love.graphics.translate(-player.x + 400, -player.y + 300)
love.graphics.circle("line", player.x, player.y, player.size)
love.graphics.draw(player.image, player.x, player.y,
0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
end
It works! Now let’s keep score of how many coins we have picked up. The score is something that you always want to see on screen. It’s not part of the play area, it’s part of the HUD. First let’s add the score
-- In love.load()
score = 0
for i=#coins,1,-1 do
if checkCollision(player, coins[i]) then
table.remove(coins, i)
player.size = player.size + 1
score = score + 1
end
end
function love.draw()
love.graphics.translate(-player.x + 400, -player.y + 300)
love.graphics.circle("line", player.x, player.y, player.size)
love.graphics.draw(player.image, player.x, player.y,
0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.print(score, 10, 10)
end
When you run the game you can now see a number. Now how do we make it so that the score’s position is not relative to the camera? One thing we could do is translate back to the normal position
function love.draw()
love.graphics.translate(-player.x + 400, -player.y + 300)
love.graphics.circle("line", player.x, player.y, player.size)
love.graphics.draw(player.image, player.x, player.y,
0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.translate(player.x - 400, player.y - 300)
love.graphics.print(score, 10, 10)
end
We can also use love.graphics.origin()
. This resets all the coordinate transformations. This does not affect what you have already drawn.
function love.draw()
love.graphics.translate(-player.x + 400, -player.y + 300)
love.graphics.circle("line", player.x, player.y, player.size)
love.graphics.draw(player.image, player.x, player.y,
0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.origin()
love.graphics.print(score, 10, 10)
end
But this is not always the best option. Perhaps you want to scale your game, so you always want to have love.graphics.scale(2)
and not have it reset. To fix that we can use love.graphics.push()
and love.graphics.pop()
. With love.graphics.push()
, we save our current coordinate transformations and add it to a stack. With love.graphics.pop()
we take the saved coordinate transformations from the top of the stack and apply it. Let’s use these functions to draw our score correctly.
--- code
function love.draw()
love.graphics.push() -- Make a copy of the current state and push it onto the stack.
love.graphics.translate(-player.x + 400, -player.y + 300)
love.graphics.circle("line", player.x, player.y, player.size)
love.graphics.draw(player.image, player.x, player.y,
0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop() -- Pull the copy of the state of the stack and apply it.
love.graphics.print(score, 10, 10)
end
Nice, now we have a camera with a score that is drawn on top of the camera. Now let’s make things shake.
Screenshake
How about adding some screenshake? First we need a timer for how long the shake needs to last and then make that timer go down.
-- In love.load()
shakeDuration = 0
-- In love.update(dt)
if shakeDuration > 0 then
shakeDuration = shakeDuration - dt
end
Next we translate a few pixels in random directions as long as the timer is higher than 0.
function love.draw()
love.graphics.push() -- Make a copy of the current state and push it onto the stack.
love.graphics.translate(-player.x + 400, -player.y + 300)
if shakeDuration > 0 then
-- Translate with a random number between -5 an 5.
-- This second translate will be done based on the previous translate.
-- So it will not reset the previous translate.
love.graphics.translate(love.math.random(-5,5), love.math.random(-5,5))
end
love.graphics.circle("line", player.x, player.y, player.size)
love.graphics.draw(player.image, player.x, player.y,
0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop() -- Pull the copy of the state of the stack and apply it.
love.graphics.print(score, 10, 10)
end
And finally we set the timer to a certain value, for example 0.3, every time we pick up a coin.
for i=#coins,1,-1 do
if checkCollision(player, coins[i]) then
table.remove(coins, i)
player.size = player.size + 1
score = score + 1
shakeDuration = 0.3
end
end
Try it out. It might be that the screen shakes too fast. And if not for you, it might happen on other computers. So again we need to use delta time. We can add a second timer to fix this. But then we also need an object to store our offset in.
-- In love.load()
shakeDuration = 0
shakeWait = 0
shakeOffset = {x = 0, y = 0}
-- In love.update(dt)
if shakeDuration > 0 then
shakeDuration = shakeDuration - dt
if shakeWait > 0 then
shakeWait = shakeWait - dt
else
shakeOffset.x = love.math.random(-5,5)
shakeOffset.y = love.math.random(-5,5)
shakeWait = 0.05
end
end
function love.draw()
love.graphics.push()
love.graphics.translate(-player.x + 400, -player.y + 300)
if shakeDuration > 0 then
love.graphics.translate(shakeOffset.x, shakeOffset.y)
end
love.graphics.circle("line", player.x, player.y, player.size)
love.graphics.draw(player.image, player.x, player.y,
0, 1, 1, player.image:getWidth()/2, player.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop()
love.graphics.print(score, 10, 10)
end
And now it’s consistent.
Now how about we make this game a multiplayer game, and add split screen?
Canvas
Whenever you use love.graphics to draw something, it is drawn on a canvas. This canvas is an object, and we can create our own canvas objects. This can be very useful, for a multiplayer game with split screen for example. You would draw the game on a separate canvas for each player, and then draw those canvases to the main canvas. Because a canvas is a Drawable. This is a superclass, and all LÖVE object that have this superclass can be drawn with love.graphics.draw()
.
The supertypes of Canvas in order are:
- Texture
- Drawable
- Object
Texture is a subtype of Drawable. Both Image and Canvas are a subtype of Texture, and both can be drawn using a Quad.
All LÖVE objects are of the type Object. Let this not confuse you with the variable we use for classes, which we also name Object
. These two are not related in any way, we just name them the same.
You can create a canvas with love.graphics.newCanvas
.
Let’s use a canvas to create a split screen in our game. We start by adding another player that can move with WASD, and remove the screen shake and save/load system.
function love.load()
player1 = {
x = 100,
y = 100,
size = 25,
image = love.graphics.newImage("face.png")
}
player2 = {
x = 300,
y = 100,
size = 25,
image = love.graphics.newImage("face.png")
}
coins = {}
for i=1,25 do
table.insert(coins,
{
x = math.random(50, 650),
y = math.random(50, 450),
size = 10,
image = love.graphics.newImage("dollar.png")
}
)
end
score1 = 0
score2 = 0
end
function love.update(dt)
if love.keyboard.isDown("left") then
player1.x = player1.x - 200 * dt
elseif love.keyboard.isDown("right") then
player1.x = player1.x + 200 * dt
end
if love.keyboard.isDown("up") then
player1.y = player1.y - 200 * dt
elseif love.keyboard.isDown("down") then
player1.y = player1.y + 200 * dt
end
if love.keyboard.isDown("a") then
player2.x = player2.x - 200 * dt
elseif love.keyboard.isDown("d") then
player2.x = player2.x + 200 * dt
end
if love.keyboard.isDown("w") then
player2.y = player2.y - 200 * dt
elseif love.keyboard.isDown("s") then
player2.y = player2.y + 200 * dt
end
for i=#coins,1,-1 do
if checkCollision(player1, coins[i]) then
table.remove(coins, i)
player1.size = player1.size + 1
score1 = score1 + 1
elseif checkCollision(player2, coins[i]) then
table.remove(coins, i)
player2.size = player2.size + 1
score2 = score2 + 1
end
end
end
function love.draw()
love.graphics.push()
love.graphics.translate(-player1.x + 400, -player1.y + 300)
love.graphics.circle("line", player1.x, player1.y, player1.size)
love.graphics.draw(player1.image, player1.x, player1.y,
0, 1, 1, player1.image:getWidth()/2, player1.image:getHeight()/2)
love.graphics.circle("line", player2.x, player2.y, player2.size)
love.graphics.draw(player2.image, player2.x, player2.y,
0, 1, 1, player2.image:getWidth()/2, player2.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop()
love.graphics.print("Player 1 - " .. score1, 10, 10)
love.graphics.print("Player 2 - " .. score2, 10, 30)
end
function checkCollision(p1, p2)
-- Calculating distance in 1 line
-- Subtract the x's and y's, square the difference, sum the squares and find the root of the sum.
local distance = math.sqrt((p1.x - p2.x)^2 + (p1.y - p2.y)^2)
-- Return whether the distance is lower than the sum of the sizes.
return distance < p1.size + p2.size
end
Now we need a canvas to draw on. We’re going to draw the canvas left and right, so it should be 400x600.
-- In love.load()
screenCanvas = love.graphics.newCanvas(400, 600)
Next we need to draw the game on to our new canvas. With love.graphics.setCanvas()
we can set the canvas we want to draw on. If we pass no arguments to this function then it will reset to the default canvas.
function love.draw()
love.graphics.setCanvas(screenCanvas)
love.graphics.push()
love.graphics.translate(-player1.x + 400, -player1.y + 300)
love.graphics.circle("line", player1.x, player1.y, player1.size)
love.graphics.draw(player1.image, player1.x, player1.y,
0, 1, 1, player1.image:getWidth()/2, player1.image:getHeight()/2)
love.graphics.circle("line", player2.x, player2.y, player2.size)
love.graphics.draw(player2.image, player2.x, player2.y,
0, 1, 1, player2.image:getWidth()/2, player2.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop()
love.graphics.setCanvas()
love.graphics.print("Player 1 - " .. score1, 10, 10)
love.graphics.print("Player 2 - " .. score2, 10, 30)
end
If you run the game you’ll see nothing. This is because the game is drawn onto our new canvas, but we’re not drawing our canvas onto the default canvas. Like I said before, a Canvas is a Drawable and Drawable’s can be drawn with love.graphics.draw
.
function love.draw()
love.graphics.setCanvas(screenCanvas)
love.graphics.push()
love.graphics.translate(-player1.x + 400, -player1.y + 300)
love.graphics.circle("line", player1.x, player1.y, player1.size)
love.graphics.draw(player1.image, player1.x, player1.y,
0, 1, 1, player1.image:getWidth()/2, player1.image:getHeight()/2)
love.graphics.circle("line", player2.x, player2.y, player2.size)
love.graphics.draw(player2.image, player2.x, player2.y,
0, 1, 1, player2.image:getWidth()/2, player2.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop()
love.graphics.setCanvas()
--Draw the canvas
love.graphics.draw(screenCanvas)
love.graphics.print("Player 1 - " .. score1, 10, 10)
love.graphics.print("Player 2 - " .. score2, 10, 30)
end
Now you should be able to see your game. But you’ll notice something weird when you start moving. You’re leaving a trail behind. This is because we are not clearing the canvas, meaning that everything we draw on it remains on the canvas. This is done by default on our default canvas. We can clear the canvas with love.graphics.clear()
, and we should put this right before drawing the game. And since we’re only using half of the screen, we need to reposition the camera to make the player in the center. Half the screen is now 200 instead of 400.
Since we’re drawing the game twice, let’s put everything we draw for our game in a function.
function love.draw()
love.graphics.setCanvas(screenCanvas)
love.graphics.clear()
drawGame()
love.graphics.setCanvas()
love.graphics.draw(screenCanvas)
love.graphics.setCanvas(screenCanvas)
love.graphics.clear()
drawGame()
love.graphics.setCanvas()
love.graphics.draw(screenCanvas, 400)
-- Add a line to separate the screens
love.graphics.line(400, 0, 400, 600)
love.graphics.print("Player 1 - " .. score1, 10, 10)
love.graphics.print("Player 2 - " .. score2, 10, 30)
end
function drawGame()
love.graphics.push()
love.graphics.translate(-player1.x + 200, -player1.y + 300)
love.graphics.circle("line", player1.x, player1.y, player1.size)
love.graphics.draw(player1.image, player1.x, player1.y,
0, 1, 1, player1.image:getWidth()/2, player1.image:getHeight()/2)
love.graphics.circle("line", player2.x, player2.y, player2.size)
love.graphics.draw(player2.image, player2.x, player2.y,
0, 1, 1, player2.image:getWidth()/2, player2.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop()
end
Now we have a split screen. The only problem is that both the left and the right screen focus on player 1. We need to make it so that the camera focuses on player 2 the second time we draw. We can fix this by adding a focus
parameter to drawGame
, and have the camera focus on what we pass as argument, which will be the players.
function love.draw()
love.graphics.setCanvas(screenCanvas)
love.graphics.clear()
drawGame(player1)
love.graphics.setCanvas()
love.graphics.draw(screenCanvas)
love.graphics.setCanvas(screenCanvas)
love.graphics.clear()
drawGame(player2)
love.graphics.setCanvas()
love.graphics.draw(screenCanvas, 400)
love.graphics.line(400, 0, 400, 600)
love.graphics.print("Player 1 - " .. score1, 10, 10)
love.graphics.print("Player 2 - " .. score2, 10, 30)
end
function drawGame(focus)
love.graphics.push()
love.graphics.translate(-focus.x + 200, -focus.y + 300)
love.graphics.circle("line", player1.x, player1.y, player1.size)
love.graphics.draw(player1.image, player1.x, player1.y,
0, 1, 1, player1.image:getWidth()/2, player1.image:getHeight()/2)
love.graphics.circle("line", player2.x, player2.y, player2.size)
love.graphics.draw(player2.image, player2.x, player2.y,
0, 1, 1, player2.image:getWidth()/2, player2.image:getHeight()/2)
for i,v in ipairs(coins) do
love.graphics.circle("line", v.x, v.y, v.size)
love.graphics.draw(v.image, v.x, v.y,
0, 1, 1, v.image:getWidth()/2, v.image:getHeight()/2)
end
love.graphics.pop()
end
Libraries
We made a rather simple camera, but there’s much more to it. Like zooming in and borders. I recommend checking out these libraries that provide advanced camera functionality:
Summary
With the Coordinate System we can change how stuff is drawn on screen. We can use love.graphics.translate(x, y)
create a camera. Everything that we draw is drawn on a canvas. We can create our own canvas that we can draw like an image, because they’re both of the LÖVE type Texture. By using a canvas we can create a split screen.
Code homework
For homework this week you will complete your escape game.
A complete game includes protagonist with animation states. Enemies with animation states. Collisions. A game objective. A win state. A lose state.
Your code should be complete, working, commented, and broken down into functions.
Your game should have sound effects.
The scene must have background graphics. Graphics should be consistent.
The game should have a clear objective, rules and gameplay and be bug-free.
Credits
- Copyright (c) 2022 Sheepolution
- Katamari Damacy screenshot, Nintendo
- Sokpop collective screenshot
- rubber duck by Tom Morris CC BY-SA