Class 3 - Tables, Game Loops
Play homework
Topics
- Collisions
- Tables
- For-loops
- Game loop
- Pong iteratively developed
Description
This week is all about Pong! Programming this game will challenge us to use all of our new skills. We need to create a ball, have it bounce around the screen, and take player input to move controllers. We’ll build the game one step at a time, in an iterative fashion.
Class Notes
Detecting collision
Let’s say we’re making a game where you can shoot down monsters. A monster should die when it is hit by a bullet. So what we need to check is: Is the monster colliding with a bullet?
We’re going to create a collision check function. We will be checking collision between rectangles. This is called AABB collision. So we need to know, when do two rectangles collide?
I created an image with three examples:
It’s time to turn on that programmer brain if you haven’t already. What is going on in the third example that isn’t happening in the first and second example?
“They are colliding”
Yes, but you have to be more specific. We need information that the computer can use.
Take a look at the positions of the rectangles. In the first example, Red is not colliding with Blue, because Red is too far to the left. If Red was a bit further to the right, they would touch. How far exactly? Well, if Red’s right side is further to the right than Blue’s left side. This is something that is true for example 3.
But it’s also true for example 2. We need more conditions to be sure there is collision. So example 2 shows we can’t go too far to the right. How far exactly can we go? How much would Red have to move to the left for there to be collision? When Red’s left side is further to the left than Blue’s right side.
So we have two conditions, is that enough to ensure there is collision?
Well no, look at the following image:
This situation agrees with our conditions. Red’s right side is further to the right than Blue’s left side. And Red’s left side is further to the left than Blue’s right side. Yet, there is no collision. That’s because Red is too high. It needs to move down. How far? Till Red’s bottom side is further to the bottom than Blue’s top side.
But if we move it too far down, there won’t be collision anymore. How far can Red move down, and still collide with Blue? As long as Red’s top side is further to the top than Blue’s bottom side.
Now we got four conditions. Are all four conditions true for these three examples?
Red’s right side is further to the right than Blue’s left side.
Red’s left side is further to the left than Blue’s right side.
Red’s bottom side is further to the bottom than Blue’s top side.
Red’s top side is further to the top than Blue’s bottom side.
Yes, they are! Now we need to turn this information into a function.
First let’s create two rectangles.
function love.load()
--Create 2 rectangles
r1 = {
x = 10,
y = 100,
width = 100,
height = 100
}
r2 = {
x = 250,
y = 120,
width = 150,
height = 120
}
end
function love.update(dt)
--Make one of rectangle move
r1.x = r1.x + 100 * dt
end
function love.draw()
love.graphics.rectangle("line", r1.x, r1.y, r1.width, r1.height)
love.graphics.rectangle("line", r2.x, r2.y, r2.width, r2.height)
end
Now we create a new function called checkCollision(), with 2 rectangles as parameters.
function checkCollision(a, b)
end
First we need the sides of the rectangles. The left side is the x position, the right side is the x position + the width. Same with y and height.
function checkCollision(a, b)
--With locals it's common usage to use underscores instead of camelCasing
local a_left = a.x
local a_right = a.x + a.width
local a_top = a.y
local a_bottom = a.y + a.height
local b_left = b.x
local b_right = b.x + b.width
local b_top = b.y
local b_bottom = b.y + b.height
end
Now that we have the four sides of each rectangle, we can use them to put our conditions in an if-statement.
function checkCollision(a, b)
--With locals it's common usage to use underscores instead of camelCasing
local a_left = a.x
local a_right = a.x + a.width
local a_top = a.y
local a_bottom = a.y + a.height
local b_left = b.x
local b_right = b.x + b.width
local b_top = b.y
local b_bottom = b.y + b.height
--If Red's right side is further to the right than Blue's left side.
if a_right > b_left
--and Red's left side is further to the left than Blue's right side.
and a_left < b_right
--and Red's bottom side is further to the bottom than Blue's top side.
and a_bottom > b_top
--and Red's top side is further to the top than Blue's bottom side then..
and a_top < b_bottom then
--There is collision!
return true
else
--If one of these statements is false, return false.
return false
end
Notice that the if-condition itself is a boolean value. checkCollision
returns true
when the if-condition is true
and vise-versa. Therefore checkCollision
can be simplified into the following form:
function checkCollision(a, b)
--With locals it's common usage to use underscores instead of camelCasing
local a_left = a.x
local a_right = a.x + a.width
local a_top = a.y
local a_bottom = a.y + a.height
local b_left = b.x
local b_right = b.x + b.width
local b_top = b.y
local b_bottom = b.y + b.height
--Directly return this boolean value without using if-statement
return a_right > b_left
and a_left < b_right
and a_bottom > b_top
and a_top < b_bottom
end
Okay, we have our function. Let’s try it out! We draw the rectangles filled or lined based on
function love.draw()
--We create a local variable called mode
local mode
if checkCollision(r1, r2) then
--If there is collision, draw the rectangles filled
mode = "fill"
else
--else, draw the rectangles as a line
mode = "line"
end
--Use the variable as first argument
love.graphics.rectangle(mode, r1.x, r1.y, r1.width, r1.height)
love.graphics.rectangle(mode, r2.x, r2.y, r2.width, r2.height)
end
It works! Now you know how to detect collision between two rectangles.
Summary
Collision between two rectangles can be checked with four conditions.
Where A and B are rectangles:
A’s right side is further to the right than B’s left side.
A’s left side is further to the left than B’s right side.
A’s bottom side is further to the bottom than B’s top side.
A’s top side is further to the top than B’s bottom side.
Tables and for-loops
Tables
Tables are basically lists in which we can store values.
You create a table with curly brackets ({ }):
function love.load()
fruits = {}
end
We just created a table called fruits. Now we can store values inside the table. There are multiple ways to do this.
One way is to put the values inside the curly brackets.
function love.load()
-- Each value is separated by a comma, just like with parameters and arguments
fruits = {"apple", "banana"}
end
We can also use the function table.insert
. The first argument is the table, the second argument is the value we want to store inside that table.
function love.load()
--Each value is separated by a comma, just like with parameters and arguments
fruits = {"apple", "banana"}
table.insert(fruits, "pear")
end
So now after love.load our table will contain "apple"
, "banana"
and "pear"
. To prove that, let’s put the values on screen. For that we’re going to use love.graphics.print(text, x, y)
.
function love.draw()
--Arguments: (text, x-position, y-position)
love.graphics.print("Test", 100, 100)
end
When you run the game, you should see the text “test” written. We can access the values of our table by writing the tables name, followed by brackets ([ ]) (So not curly but square brackets!). Inside these brackets, we write the position of the value we want.
Like I said, tables are a list of values. We first added "apple"
and "banana"
, so those are on the first and second position in the list. Next we added "pear"
, so that’s on the third position in the list. On position 4 there is no value (yet), since we only added 3 values.
So if we want to print "apple"
, we have to get the first value of the list.
function love.draw()
love.graphics.print(fruits[1], 100, 100)
end
And so now it should draw "apple"
. If you replace the [1]
with [2]
, you should get "banana"
, and with [3]
you get "pear"
.
Now we want to draw all 3 fruits. We could use love.graphics.print 3 times, each with a different table entry…
function love.draw()
love.graphics.print(fruits[1], 100, 100)
love.graphics.print(fruits[2], 100, 200)
love.graphics.print(fruits[3], 100, 300)
end
…but imagine if we had 100 values in our table. Luckily, there’s a solution for this: for-loops!
for-loops
A for-loop is a way to repeat a piece of code a certain amount of times.
You create a for-loop like this:
function love.load()
fruits = {"apple", "banana"}
table.insert(fruits, "pear")
for i=1,10 do
print("hello", i)
end
end
If you run the game you should see it prints hello 1, hello 2, hello 3, all the way to 10.
To create a for-loop, first you write for
. Next you write a variable and give it a numeric value. This is where the for-loop starts. The variable can be named anything, but it’s common to use i
. This variable can only be used inside the for-loop (just like with functions and parameters). Next you write the number to which it should count. So for example for i=6,18 do
will start at 6 and keep looping till it’s at 18.
There is also a third, optional number. This is by how much the variable increases. for i=6,18,4 do
would go: 6, 10, 14, 18. With this you can also make for-loops go backwards with -1.
Our table starts at 1 and has 3 values, so we will write:
function love.load()
fruits = {"apple", "banana"}
table.insert(fruits, "pear")
for i=1,3 do
print(fruits[i])
end
end
When you run the game you’ll see that it prints all 3 fruits. In the first loop it prints fruits[1]
, then in the second loop fruits[2]
and finally in the third loop fruits[3]
.
Editing tables
But what if we add or remove a value from a table? We would have to change the 3 into another number. For that we use #fruits
. With the #-sign, we can get the length of a table. The length of a table refers to the number of things in that table. That length would be 3
in our case, since we have 3 entries: apple
, banana
, and pear
in our fruits
table.
function love.load()
fruits = {"apple", "banana"}
print(#fruits)
--Output: 2
table.insert(fruits, "pear")
print(#fruits)
--Output: 3
for i=1,#fruits do
print(fruits[i])
end
end
Let’s use this knowledge to draw all 3 fruits.
function love.draw()
for i=1,#fruits do
love.graphics.print(fruits[i], 100, 100)
end
end
If you run the game you should see it draws all 3 fruits, except they’re all drawn on the same position. We can fix this by printing each number on a different height.
function love.draw()
for i=1,#fruits do
love.graphics.print(fruits[i], 100, 100 + 50 * i)
end
end
So now "apple"
will be drawn on the y-position 100 + 50 * 1, which is 150. Then "banana"
gets drawn on 200, and "pear"
on 250.
If we were to add another fruit, we won’t have to change anything. It will automatically be drawn. Let’s add "pineapple"
.
function love.load()
fruits = {"apple", "banana"}
table.insert(fruits, "pear")
table.insert(fruits, "pineapple")
end
We can also remove values from our table. For that we use table.remove
. The first argument is the table we want to remove something from, the second argument is the position we want to remove. So if we want to remove banana, we do the following:
function love.load()
fruits = {"apple", "banana"}
table.insert(fruits, "pear")
table.insert(fruits, "pineapple")
table.remove(fruits, 2)
end
When you run the game you’ll see that banana is no longer drawn, and that pear and pineapple have moved up.
When you remove a value from a table with table.remove
, all the following items in the table will move up. So what was on position 3 is now on position 2 in the table. And what was on position 4 is now on position 3.
You can also add or change the values inside the table directly. For example, we can change "apple"
into "tomato"
:
function love.load()
fruits = {"apple", "banana"}
table.insert(fruits, "pear")
table.insert(fruits, "pineapple")
table.remove(fruits, 2)
--The value of position 1 in the table becomes "tomato"
fruits[1] = "tomato"
end
ipairs
Back to the for-loops. There is actually another way, and an easier way to loop through the table. We can use an ipairs
loop.
function love.load()
fruits = {"apple", "banana"}
table.insert(fruits, "pear")
table.insert(fruits, "pineapple")
table.remove(fruits, 2)
fruits[1] = "tomato"
for i,v in ipairs(fruits) do
print(i, v)
end
--Output:
--1, "tomato"
--2, "pear"
--3, "pineapple"
end
This for-loop loops, or what we also call iterates, through all the values in the table. The variables i
tells us the position of the table, v
is the value of that position in the table. It’s basically a shorthand for fruits[i]
. For example, in the first iteration the values for the variables i
would be 1
and v
would be "apple"
. In the second iteration, i
and v
would be 2
and "pear"
respectively.
But how does it work? Why does the function ipairs
allow for this? That is for another time. For now all you need to know is that ipairs
is basically a shorthand for the following:
for i=1, #fruits do
v = fruits[i]
end
Let’s use ipairs
for drawing our tables.
function love.draw()
-- i and v are variables, so we can name them whatever we want
for i,frt in ipairs(fruits) do
love.graphics.print(frt, 100, 100 + 50 * i)
end
end
Summary
Tables are lists in which we can store values. We store these values when creating the table, with table.insert
, or with table_name[1] = "some_value"
. We can get the length of the table with #table_name
. With for-loops we can repeat a piece of code a number of times. We can also use for-loops to iterate through tables.
What is a game loop?
-
A game, fundamentally, is an infinite loop. During every iteration of that loop, we’re repeatedly performing the following set of steps:
- First, we’re processing input. That is to say, we’re constantly checking: has the user pressed a key on the keyboard, moved the joystick, moved/clicked the mouse, etc.?
- Second, we need to respond to that input from the previous step by updating anything in the game that depends on that input (i.e., tracking movement, detecting collisions, etc.).
- Third, we need to re-render anything that was updated in the previous step, so that the user can see visually on the screen that the game has changed and feel a sense of interactivity.
Photo taken from gameprogrammingpatterns.com/game-loop.html, where you can read more about game loops.
2D Coordinate System
-
In the context of 2D games, the most fundamental way of looking at the world is by using the 2D coordinate system.
-
Slightly different from the traditional coordinate system you might’ve used in math class, the 2D coordinate system we’re referring to here is a system in which objects have an X and Y coordinate (X, Y) and are drawn accordingly, with (0,0) being the top-left of the system. This means positive directions moving down and to the right, while negative directions move up and to the left.
Photo taken from rbwhitaker.wdfiles.com/local--files/monogame-introduction-to-2d-graphics/2DCoordinateSystem.png.
Game scope
- We are aiming to recreate “Pong,” a simple 2 player game in which one player has a paddle on the left side of the screen, the other player has a paddle on the right side of the screen, and the first player to score 10 times on their opponent wins. A player scores by getting the ball past the opponent’s paddle and into their “goal” (i.e., the edge of the screen).
Project Procedure
- First off, we’ll want to draw shapes to the screen (e.g., paddles and ball) so that the user can see the game.
- Next, we’ll want to control the 2D position of the paddles based on input, and implement collision detection between the paddles and ball so that each player can deflect the ball back toward their opponent.
- We’ll also need to implement collision detection between the ball and screen boundaries to keep the ball within the vertical bounds of the screen and to detect scoring events (outside horizontal bounds)
- At that point, we’ll want to add sound effects for when the ball hits paddles and walls, and for when a point is scored.
- Lastly, we’ll display the score on the screen so that the players don’t have to remember it during the game.
pong-0 (“The Day-0 Update”)
- At this point, you will want to have downloaded the demo code in order to follow along. Be sure to pay attention to the comments in the code!
- pong-0 simply prints “Hello Pong!” exactly in the center of the screen. This is not incredibly exciting, but it does showcase how to use LÖVE2D’s most important functions moving forward.
Important Functions
love.load()
- This function is used for initializing our game state at the very beginning of program execution. Whatever code we put here will be executed once at the very beginning of the program.
love.update(dt)
- This function is called by LÖVE at each frame of program
execution;
dt
(i.e., DeltaTime) will be the elapsed time in seconds since the last frame, and we can use this to scale any changes in our game for even behavior across frame rates.
- This function is called by LÖVE at each frame of program
execution;
love.draw()
- This function is also called at each frame by LÖVE. It is called after the update step completes so that we can draw things to the screen once they’ve changed.
- LÖVE2D expects these functions to be implemented in
main.lua
and calls them internally; if we don’t define them, it will still function, but our game will be fundamentally incomplete, at least ifupdate
ordraw
are missing! We’ll take a look at two more functions below: -
love.graphics.printf(text, x, y, [width], [align])
- Versatile print function that can align text left, right, or center on the screen
-
love.window.setMode(width, height, params)
- Used to initialize the window’s dimensions and to set parameters
like
vsync
(vertical sync), whether we’re fullscreen or not, and whether the window is resizeable after startup. We won’t be using this function past this example in favor of thepush
virtual resolution library, which has its own method like this, but it is useful to know if encountered in other code.
- Used to initialize the window’s dimensions and to set parameters
like
- Now, with these puzzle pieces in mind, you can see how we’re rendering “Hello Pong!” to the center of the screen:
Important code
-
We initialize our game by specifying in the
love.load()
function that our 1280x720 game window shouldn’t be fullscreen or resizable, but it should be synced to our monitor’s own refresh rate.WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720 function love.load() love.window.setMode(WINDOW_WIDTH, WINDOW_HEIGHT, { fullscreen = false, resizable = false, vsync = true }) end
-
Next, we overwrite
love.draw()
so that we can specify the text we’d like to render to the screen, in this case “Hello Pong!”, along with coordinates for where it should be drawn.function love.draw() love.graphics.printf( 'Hello Pong!', 0, WINDOW_HEIGHT / 2 - 6, WINDOW_WIDTH, 'center') end
pong-1 (“The Low-Res Update”)
- pong-1 exhibits the same behavior as pong-0, but with much blurrier text.
Important Functions
-
love.graphics.setDefaultFilter(min, mag)
- This function sets the texture scaling filter when minimizing
and magnifying textures and fonts; LÖVE’s default is bilinear,
which causes blurriness, but for our use cases we will typically
want nearest-neighbor filtering (
nearest
), which results in perfect pixel upscaling and downscaling, simulating a retro feel.
- This function sets the texture scaling filter when minimizing
and magnifying textures and fonts; LÖVE’s default is bilinear,
which causes blurriness, but for our use cases we will typically
want nearest-neighbor filtering (
love.keypressed(key)
- This is a LÖVE2D callback function that executes whenever we
press a key, assuming we’ve implemented this in
main.lua
, in the same vein aslove.load()
,love.update(dt)
, andlove.draw()
. It’ll allow us to receive input from the keyboard for our game.
- This is a LÖVE2D callback function that executes whenever we
press a key, assuming we’ve implemented this in
love.event.quit()
- This is a simple function that terminates the application upon execution.
Important Code
-
When you open pong-1, you’ll notice that we’ve begun using the
push
library we referred to earlier. You can “import” other files in yourmain.lua
file with therequire
keyword given that they are in the same directory. -
In addition, we’ve also added two new variables to the code:
push = require 'push' WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720 VIRTUAL_WIDTH = 432 VIRTUAL_HEIGHT = 243
This will allow us to think of our game in more low-res terms, by using the
push
library to treat our game as if it were on a 432x243 window, while actually rendering it at our desired 1280x720 window. With this change, we can see we’ve updated ourlove.load()
function accordingly:function love.load() love.graphics.setDefaultFilter('nearest', 'nearest') push:setupScreen(VIRTUAL_WIDTH, VIRTUAL_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT, { fullscreen = false, resizable = false, vsync = true }) end
-
We’ve also added a way to quit the game via user input, by using two of the functions discussed above:
function love.keypressed(key) if key == 'escape' then love.event.quit() end end
Including this code in
main.lua
will ensure that the program is always monitoring whether the user has pressed theescape
key on their keyboard, in which caselove.event.quit()
will be called to terminate the program. -
Lastly, we’ve made a small tweak to our
love.draw()
function so as to integrate thepush
library into the code.function love.draw() push:apply('start') love.graphics.printf('Hello Pong!', 0, VIRTUAL_HEIGHT / 2 - 6, VIRTUAL_WIDTH, 'center') push:apply('end') end
You’ll notice that our print statement remains unchanged, but we’ve wrapped it between
push:apply('start')
andpush:apply('end')
to ensure that its contents will be rendered at our desired virtual resolution. -
With these changes, you’ll notice that while we’re still printing “Hello Pong!” to the center of the screen, the text is now magnified and rendered at a lower resolution, despite our window size being the same as before.
pong-2 (“The Rectangle Update”)
- pong-2 produces a more complete, albeit static image of what our Pong program should look like.
Important Functions
-
love.graphics.newFont(path, size)
- This function loads a font file into memory at a specific path, setting it to a specific size, and storing it in an object we can use to globally change the currently active font that LÖVE2D is using to render text (functioning like a state machine).
-
love.graphics.setFont(font)
- This function sets LÖVE2D’s currently active font (of which
there can only be one at a time) to a passed-in
font
object that we can create usinglove.graphics.newFont
, per the above.
- This function sets LÖVE2D’s currently active font (of which
there can only be one at a time) to a passed-in
-
love.graphics.clear(r, g, b, a)
- This function wipes the entire screen with a color defined by an RGBA set, with each component ranging from 0-255.
-
love.graphics.rectangle(mode, x, y, width, height)
- Draws a rectangle onto the screen using whichever our active
color is (per
love.graphics.setColor
, which we don’t need to use in this particular project since most everything is white, the default LÖVE2D color). Themode
parameter can be set tofill
orline
, which results in a filled or outlined rectangle, respectively, and the other four parameters are its position and size dimensions. This is the cornerstone drawing function of the entirety of our Pong implementation!
- Draws a rectangle onto the screen using whichever our active
color is (per
Important Code
-
Alongside our
main.lua
file and thepush
library, you’ll find that we’ve added a font file to our project. -
On that note, you’ll find a small addition to our
love.load()
function:smallFont = love.graphics.newFont('font.ttf', 8) love.graphics.setFont(smallFont)
This will allow us to create a custom font object (based off the font file we’ve added to our project directory) that we can set as the active font in our game.
-
The only other changes to the code in this update can be found in the
love.draw()
function.love.graphics.clear(40/255, 45/255, 52/255, 255/255) love.graphics.printf('Hello Pong!', 0, 20, VIRTUAL_WIDTH, 'center') love.graphics.rectangle('fill', 10, 30, 5, 20) love.graphics.rectangle('fill', VIRTUAL_WIDTH - 10, VIRTUAL_HEIGHT - 50, 5, 20) love.graphics.rectangle('fill', VIRTUAL_WIDTH / 2 - 2, VIRTUAL_HEIGHT / 2 - 2, 4, 4)
As you can see, we are setting the background to a dark color, shifting “Hello Pong!” higher up on the screen, and drawing rectangles for the paddles and the ball. The paddles are positioned on opposing ends of the screen, and the ball in the center.
pong-3 (“The Paddle Update”)
- pong-3 adds interactivity to the Paddles by letting us move them up
and down using the
w
ands
keys for the left Paddle and the up and down keys for the right Paddle.
Important Functions
love.keyboard.isDown(key)
- This function returns true or false depending on whether the
specified key is currently held down; it differs from
love.keypressed(key)
in that this can be called arbitrarily and will continuously return true if the key is pressed down, whereaslove.keypressed(key)
will only fire its code once every time the key is initially pressed down. However, since we want to be able to move our paddles up and down by holding down the appropriate keys, we need a function to test for longer periods of input, hence the use oflove.keyboard.isDown(key)
.
- This function returns true or false depending on whether the
specified key is currently held down; it differs from
Important Code
-
You’ll notice we’ve added a new constant near the top of
main.lua
:PADDLE_SPEED = 200
This is an arbitrary value that we’ve chosen for our paddle speed. It will be scaled by DeltaTime, so it’ll be multiplied by how much time has passed (in terms of seconds) since the last frame, so that our paddle movement will remain consistent regardless of how quickly or slowly our computer is running.
-
You’ll also find some new variables in
love.load()
:scoreFont = love.graphics.newFont('font.ttf', 32) player1Score = 0 player2Score = 0 player1Y = 30 player2Y = VIRTUAL_HEIGHT - 50
In particular, we’ve created a new font object that is of larger size so that we can display each player’s score more visibly on the screen, and allocated two variables for the purpose of scorekeeping. The last two variables will keep track of each paddle’s vertical position, since the paddles will be able to move up and down.
-
Next, you’ll see that we’ve finally defined behavior for
love.update()
:function love.update(dt) if love.keyboard.isDown('w') then player1Y = player1Y + -PADDLE_SPEED * dt elseif love.keyboard.isDown('s') then player1Y = player1Y + PADDLE_SPEED * dt end if love.keyboard.isDown('up') then player2Y = player2Y + -PADDLE_SPEED * dt elseif love.keyboard.isDown('down') then player2Y = player2Y + PADDLE_SPEED * dt end end
Here, we’ve implemented a way for each player to move their paddle. Recall that our 2D coordinate system is centered at the top left of the screen. Therefore, in order for each paddle to move upwards, its Y position will need to be multiplied by negative velocity (and vice versa), which might seem counterintuitive at first glance, so be sure to take a moment to look at this carefully.
-
Lastly, in
love.draw()
you’ll see that we’ve added code for displaying the score on the screen:love.graphics.setFont(scoreFont) love.graphics.print(tostring(player1Score), VIRTUAL_WIDTH / 2 - 50, VIRTUAL_HEIGHT / 3) love.graphics.print(tostring(player2Score), VIRTUAL_WIDTH / 2 + 30, VIRTUAL_HEIGHT / 3)
First we set the active font to be the larger of the two we’ve created, and then we display each player’s score on their side of the screen.
pong-4 (“The Ball Update”)
- pong-4 adds motion to the Ball upon the user pressing
enter
.
Important Functions
math.randomseed(num)
- This function “seeds” the random number generator used by Lua
(
math.random
) with some values such that its randomness is dependent on that supplied value, allowing us to pass in different numbers each playthrough to guarantee non-consistency across different program executions (or uniformity if we want consistent behavior for testing).
- This function “seeds” the random number generator used by Lua
(
os.time()
- This is a Lua function that returns, in seconds, the time since 00:00:00 UTC, January 1, 1970, also known as Unix epoch time (en.wikipedia.ord/wiki/Unix_time)
math.random(min, max)
- This function returns a random number, dependent on the seeded
random number generator, between
min
andmax
, inclusive.
- This function returns a random number, dependent on the seeded
random number generator, between
math.min(num1, num2)
- Returns the lesser of the two numbers passed in.
math.max(num1, num2)
- Returns the greater of the two numbers passed in.
Important Code
-
You’ll find our first addition to the code at the top of
love.load()
:math.randomseed(os.time())
This seeds the random number generator, using the current time to ensure different random numbers each time our game is run. Beyond that, you’ll see a few new variables near the bottom of
love.load()
:ballX = VIRTUAL_WIDTH / 2 - 2 ballY = VIRTUAL_HEIGHT / 2 - 2 ballDX = math.random(2) == 1 and 100 or -100 ballDY = math.random(-50, 50) gameState = 'start'
ballX
andballY
will keep track of the ball position, whileballDX
andballDY
will keep track of the ball velocity.gameState
will serve as a rudimentary “state machine”, such that we’ll cycle it through the different states of our game (start, play, etc.) -
In
love.update()
, we tweak our code for paddle movement by wrapping it around themath.max()
andmath.min
functions to ensure that the paddles can’t move beyond the edges of the screen: -
We also add new code to ensure the ball can only move when we are in the “play” state:
if gameState == 'play' then ballX = ballX + ballDX * dt ballY = ballY + ballDY * dt end
-
Following this, in
love.keypressed(key)
, we add functionality to launch the game (thus transitioning from the “start” state to the “play” state) and implement ball movement mechanics:elseif key == 'enter' or key == 'return' then if gameState == 'start' then gameState = 'play' else gameState = 'start' ballX = VIRTUAL_WIDTH / 2 - 2 ballY = VIRTUAL_HEIGHT / 2 - 2 ballDX = math.random(2) == 1 and 100 or -100 ballDY = math.random(-50, 50) * 1.5 end end
Once in the “play state,” we start the ball’s position in the center of the screen and assign it a random starting velocity.
-
Lastly, we tweak our
love.draw()
function so that we can see the changes fromlove.update()
at each frame:if gameState == 'start' then love.graphics.printf('Hello Start State!', 0, 20, VIRTUAL_WIDTH, 'center') else love.graphics.printf('Hello Play State!', 0, 20, VIRTUAL_WIDTH, 'center') end love.graphics.rectangle('fill', 10, player1Y, 5, 20) love.graphics.rectangle('fill', VIRTUAL_WIDTH - 10, player2Y, 5, 20) love.graphics.rectangle('fill', ballX, ballY, 4, 4)
The only changes of note are the displaying of different messages depending on the game state, and updating the previous print statements to use the variables dynamically keeping track of position, rather than static values.
Code homework
For homework, you will be implementing a bouncy ball.
- Review our notes from class on tables
- Review our notes on collisions
- Implement a bouncing ball that bounces off the sides of your screen
- OPTIONAL: Recreate the bouncing DVD logo
Credits
- Copyright (c) 2022 Sheepolution
- Pong 0: MIT Open Courseware Colton Ogden & David J. Malan 2018
- Pong cabinet photo by Chris Rand
- pong74ls test, CC BY 3.0