Lecture 1: Flappy Bird
Today’s Topics
- Images (Sprites)
- How can we load images from memory to our game and draw them on the screen?
- Infinite Scrolling
- How can we make our game map appear to scroll infinitely from left to right without using all our memory?
- “Games Are Illusions”
- We’ll see how camera trickery is often the crucial piece for bringing games to life.
- Procedural Generation
- Going hand in hand with infinite scrolling, we’ll learn how to use procedural generation to draw additional sprites (such as the pipes in Flappy Bird) to the screen as our game map scrolls from left to right.
- State Machines
- Last week we used a rudimentary “state machine” for pong, which was really just a string variable and a few
if
statements in ourlove.update()
function. This week we’ll see how we can actually use a state machine class to allow us to transition in and out of different states more cleanly, and abstract this logic away from ourmain.lua
file and into separate classes.
- Last week we used a rudimentary “state machine” for pong, which was really just a string variable and a few
- Mouse Input
- Last week we worked with keyboard input for pong, and this week we’ll see how to process mouse input for Flappy!
- Music
- Similarly to how we added sound effects to our game last week, we’ll see how to add music to our game this week and ensure that it loops during game execution.
Downloading demo code
Optional readings for the week
- How to Make an RPG, by Dan Schuller
- Game Programming Patterns, by Robert Nystrom
bird0 (“The Day-0 Update”)
- At this point, you will want to have downloaded the demo code in order to follow along.
- bird0 simply draws two images to the screen- a foreground and a background.
Important Functions
love.graphics.newImage(path)
- This function loads an image from a graphics file (JPEG, PNG, GIF, etc.), storing it in an object that we can draw to the screen.
love.graphics.draw(drawable, x, y)
- This function draws an image to the screen at a given
x
andy
on our 2D Coordinate System. Recall that last week we usedlove.graphics.rectangle()
to draw our paddles and ball. This time, we’ll want to draw actual images, so we’ll be using this function.
- This function draws an image to the screen at a given
- We also use many of the functions we used last week:
love.load()
andlove.draw()
are particularly important for bird0, as they are what allow us to initialize our starting game state and draw our images, respectively.
- We are again making use of the
push
virtual resolution library, which can be found below:- github.com/Ulydev/push (helpful documentation can be found in README.md)
Important Code
- You should be able to recognize most of the code in this update from last week. As mentioned, at the top of
main.lua
we are requiring thepush
library, so in addition to initializing variables for our window width and height we also initialize variables for our virtual resolution width and height.- We also define two new variables, which marks a small departure from what we did last week:
local background = love.graphics.newImage('background.png') local ground = love.graphics.newImage('ground.png')
Here we are defining two variables local to this file that contain image objects for our background and our ground. This will come into play shortly.
- We also define two new variables, which marks a small departure from what we did last week:
- Our use of
love.load()
andlove.resize()
should also look familiar from last week. In the former we are again using nearest-neighbor filtering, setting the title of our window, and initializing our virtual resolution. Defining the latter allows us to resize our window. - We again define
love.keypressed(key)
so that if the user presses theescape
key, we quit the game. - Finally, take a look at
love.draw()
:function love.draw() push:start() love.graphics.draw(background, 0, 0) love.graphics.draw(ground, 0, VIRTUAL_HEIGHT - 16) push:finish() end
As we did last week, we make sure to wrap all our rendering logic inside of
push:start()
andpush:finish()
so that thepush
library can treat it with respect to our virtual resolution. The rendering logic, for now, consists of merely drawing our background and group. We draw the background at the top of the screen, and the ground at the bottom.
bird1 (“The Parallax Update”)
- bird1 allows us to “scroll” the background and foreground along the screen in order to simulate motion.
What is Parallax Scrolling?
- Parallax Scrolling is an important concept in game development. It refers to the illusion of movement given two frames of reference that are moving at different rates.
- For a real life example, consider riding in a car and looking out the window. Suppose you’re driving near mountains and there are railings on the side of the road. You might notice that the railings may appear to be moving much more quickly in your frame of vision than the mountains.
- We want to simulate this behavior in our game with the ground and background to give the appearance of motion.
Important Code
- In the code, we are using local variables to keep track of how much each image has scrolled (in order to know at which position to draw them) as well as how quickly each image is scrolling (so that the images can move at different speeds in order to produce Parallax Scrolling):
local background = love.graphics.newImage('background.png') local backgroundScroll = 0 local ground = love.graphics.newImage('ground.png') local groundScroll = 0 local BACKGROUND_SCROLL_SPEED = 30 local GROUND_SCROLL_SPEED = 60 local BACKGROUND_LOOPING_POINT = 413
You may be wondering why
local BACKGROUND_LOOPING_POINT = 413
. In short, whenever we “loop” the scrolling of our background image, we need to make sure we choose a point in the image where the loop effect won’t be noticeable. Here, we’ve chosen a point halfway through the width of the background image that looks exactly like the beginning of the background image. If this still seems confusing, be sure to run bird1 and use a different value for this variable (e.g., setlocal BACKGROUND_LOOPING_POINT = 270
) so that you can observe the difference. - We now loop the scrolling effect in
love.update()
so that we can continue reusing each image for as long as the game goes on:function love.update(dt) backgroundScroll = (backgroundScroll + BACKGROUND_SCROLL_SPEED * dt) % BACKGROUND_LOOPING_POINT groundScroll = (groundScroll + GROUND_SCROLL_SPEED * dt) % VIRTUAL_WIDTH end
Notice how we’re updating our scroll position variables for each image by adding the corresponding scroll speed variable (scaled by DeltaTime to ensure consistent movement regardless of our computer’s framerate). The looping occurs by taking the modulo of the scroll position by our looping point. In the case of our ground image, we don’t bother choosing a looping point within the image since it looks so similar at every point, so the looping point is just the width of our virtual resolution.
- Lastly, we update
love.draw()
to use the appropriate variables rather than static coordinates.function love.draw() push:start() love.graphics.draw(background, -backgroundScroll, 0) love.graphics.draw(ground, -groundScroll, VIRTUAL_HEIGHT - 16) push:finish() end
In order to “loop” each image, we are rendering a small portion of it at a time and shifting it left each frame, until we reach the looping point and reset it back to (0, 0). This means that as the scrolling takes place, the portions of the images that we’ve already seen will be to the left of the screen at a negative
x
coordinate until they are shifted back right to recommence the looping effect.
bird2 (“The Bird Update”)
- bird2 adds a Bird sprite to our game and renders it in the center of the screen.
Important Code
- We create a Bird class and give it functionality to initialize and render itself.
Bird = Class{} function Bird:init() self.image = love.graphics.newImage('bird.png') self.width = self.image:getWidth() self.height = self.image:getHeight() self.x = VIRTUAL_WIDTH / 2 - (self.width / 2) self.y = VIRTUAL_HEIGHT / 2 - (self.height / 2) end function Bird:render() love.graphics.draw(self.image, self.x, self.y) end
You can see that we create the bird image object from
bird.png
, which is our sprite’s image file which is now included in the project directory, along withclass.lua
. - In
main.lua
, we only need to add a few lines to instantiate our bird object:Class = require 'class' require 'Bird' local bird = Bird()
- And, inside
love.draw()
, we can call our bird object’srender
method to draw it to the screen:bird:render()
bird3 (“The Gravity Update”)
- bird3 introduces gravity into our game, which causes our Bird to fall off the screen almost immediately.
Important Code
- The meat and bones of this update will exist in our Bird class, to which we add an arbitrary gravity attribute and an
update
method, which we’ll then call inmain.lua
’slove.update(dt)
function.local GRAVITY = 20 function Bird:update(dt) self.dy = self.dy + GRAVITY * dt self.y = self.y + self.dy end
The trick is to manipulate the bird’s position using DeltaTime, which we discussed in last week’s lecture, applying our gravity value to the Bird’s velocity and subsequently applying the updated velocity to the Bird’s position.
bird4 (“The Anti-Gravity Update”)
- bird4 allows our Bird to jump and thus remain airborne.
Important Code
- Our approach will be to toggle the
dy
attribute between a negative and positive value in order to simulate jumping/flapping and falling.- Recall that our 2D Coordinate System has its origin at the top-left corner of the screen, so a positive
dy
value will cause our Bird to fall, while a negativedy
value will cause our Bird to rise (since the Bird’sy
value is higher at the bottom of the screen and lower at the top of the screen).
- Recall that our 2D Coordinate System has its origin at the top-left corner of the screen, so a positive
- Of special note is also the creation of a global input table in
main.lua
whose purpose is to allow us to check whether particular keys have been pressed by the user without overloading the defaultlove.keypressed(key)
function we’ve already defined inmain.lua
. As a result, we can check inBird.lua
whether the user has pressed the space bar without interfering with our check inmain.lua
for if the user has pressedesc
to quit the game. - We’ll create our global input table in
love.load()
:love.keyboard.keysPressed = {}
- Next, we’ll update it in
love.keypressed(key)
so that whenever a key is pressed, we can add it to the table:function love.keypressed(key) love.keyboard.keysPressed[key] = true if key == 'escape' then love.event.quit() end end
- At this point we can define a function that returns
true
if a given key has been pressed, andfalse
otherwise:function love.keyboard.wasPressed(key) if love.keyboard.keysPressed[key] then return true else return false end end
It’s worth noting that the above function can be equivalently defined as:
function love.keyboard.wasPressed(key) return love.keyboard.keysPressed[key] end
- With this input table, we can now add our jumping functionality to our Bird class such that when the user presses
space
by setting the bird’sdy
to an arbitrary negative value:function Bird:update(dt) self.dy = self.dy + GRAVITY * dt if love.keyboard.wasPressed('space') then self.dy = -5 end self.y = self.y + self.dy end
- Finally, we make sure to clear the table after each frame in
love.update()
:love.keyboard.keysPressed = {}
bird5 (“The Infinite Pipe Update”)
- bird5 adds the Pipe sprite to our game, rendering it an “infinite” number of times.
Important Code
- Unsurprisingly, we create our Pipe sprite by modeling it with a Pipe class.
- Inside this class, we create an
init
method that spawns a pipe at a random vertical position at the rightmost edge and within the lower quarter of the screen. We also create anupdate
method that “scrolls” the Pipe to the left of the screen based on its previous position, a negative scroll value, and itsdt
. Lastly, we include functionality for rendering the Pipe to the screen:Pipe = Class{} local PIPE_IMAGE = love.graphics.newImage('pipe.png') local PIPE_SCROLL = -60 function Pipe:init() self.x = VIRTUAL_WIDTH self.y = math.random(VIRTUAL_HEIGHT / 4, VIRTUAL_HEIGHT - 10) self.width = PIPE_IMAGE:getWidth() end function Pipe:update(dt) self.x = self.x + PIPE_SCROLL * dt end function Pipe:render() love.graphics.draw(PIPE_IMAGE, math.floor(self.x + 0.5), math.floor(self.y)) end
It’s notable to mention that we are never creating multiple Pipe sprites. Rather, we are always re-rendering the same Pipe sprite we created in line 15. This helps us save memory while accomplishing the same goal.
- We also make use of a
spawnTimer
value which we create inmain.lua
order to determine how often to spawn a new Pipe on the screen:local spawnTimer = 0
- And then, in
love.update()
:spawnTimer = spawnTimer + dt if spawnTimer > 2 then table.insert(pipes, Pipe()) print('Added new pipe!') spawnTimer = 0 end ... for k, pipe in pairs(pipes) do pipe:update(dt) if pipe.x < -pipe.width then table.remove(pipes, k) end end
We spawn a new pipe every two seconds, and remove it from the table once it’s no longer visible past the left edge of the screen.
- Finally, in
love.draw()
, we call therender
function of each pipe currently in the table:for k, pipe in pairs(pipes) do pipe:render() end
bird6 (“The PipePair Update”)
-
bird6 spawns the Pipe sprites in “pairs”, with one Pipe facing up and the other facing down.
Important Code
- Previously, in bird5, we were only worried about spawning individual Pipes, so having a simple Pipe class sufficed. However, now that we want to spawn pairs of Pipes, it makes sense to create a PipePair class to tackle this problem.
PipePair = Class{} local GAP_HEIGHT = 90 function PipePair:init(y) self.x = VIRTUAL_WIDTH + 32 self.y = y self.pipes = { ['upper'] = Pipe('top', self.y), ['lower'] = Pipe('bottom', self.y + PIPE_HEIGHT + GAP_HEIGHT) } self.remove = false end function PipePair:update(dt) if self.x > -PIPE_WIDTH then self.x = self.x - PIPE_SPEED * dt self.pipes['lower'].x = self.x self.pipes['upper'].x = self.x else self.remove = true end end function PipePair:render() for k, pipe in pairs(self.pipes) do pipe:render() end end
- The result of having a PipePair class is that we replace a lot of our previous Pipe logic in
main.lua
with analogous PipePair logic. The implications of this are that the flow of ourmain.lua
file need not change drastically, but with the caveat that now we need to accommodate for PipePairs rather than individual Pipes. - In our case, we can mimic the Pipe class to an extent, as long as we provide logic for ensuring a reasonable gap height between the Pipes, as well as accurate
y
values for our sprites, since we will be mirroring the top Pipe. Be sure to read carefully through the changes inmain.lua
, paying close attention to the comments!
bird7 (“The Collision Update”)
- bird7 introduces collision detection, pausing the game when a collision occurs.
Important Code
- The pausing of the game is handled by toggling a local boolean variable
scrolling
(checked inlove.update(dt)
) upon collision detection. - The collision detection itself is handled within our Bird class (it’s essentially the same AABB Collision Detection algorithm we saw last week), but with some leeway in order to give the user a little more leniency.
function Bird:collides(pipe) if (self.x + 2) + (self.width - 4) >= pipe.x and self.x + 2 <= pipe.x + PIPE_WIDTH then if (self.y + 2) + (self.height - 4) >= pipe.y and self.y + 2 <= pipe.y + PIPE_HEIGHT then return true end end return false end
bird8 (“The State Machine Update”)
-
bird8 modularizes our code as a State Machine:
While our eventual goal is the above, this update lays the foundations with the following states: BaseState, TitleScreenState, and PlayState.
Important Code
- We manage all our game states using an overarching StateMachine module, which handles the logic for initializing and transitioning between them.
- The TitleScreenState will transition to the PlayState via keyboard input. The BaseState is a skeleton for the other states- it defines empty methods and passes them on via inheritance.
- Of particular note in
main.lua
is the creation of ourgStateMachine
table to hold function calls to our different states:... require 'StateMachine' require 'states/BaseState' require 'states/PlayState' require 'states/TitleScreenState' ... function love.load() ... gStateMachine = StateMachine { ['title'] = function() return TitleScreenState() end, ['play'] = function() return PlayState() end, ['score'] = function() return ScoreState() end } gStateMachine:change('title') ... end
By representing our game states as modules, we vastly simplify the logic in our
main.lua
file. Now, each major part of our code will be in its own module, and each state can be accessed through our global state machine table. - Be sure to read carefully through the new modules, paying close attention to the comments, to understand how our
main.lua
has been simplified so cleanly! You should see that the PlayState should contain much of the logic previously inmain.lua
.
bird9 (“The Score Update”)
- bird9 introduces a new state, ScoreState, to help keep track of the score.
Important Code
- Once a collision is detected, the PlayState will transition to the ScoreState, which will display the user’s final score and transition back to the PlayState if the “enter” key is pressed. Note our addition to the
PlayState:update()
function to implement this transition logic:for k, pair in pairs(self.pipePairs) do for l, pipe in pairs(pair.pipes) do if self.bird:collides(pipe) then gStateMachine:change('score', { score = self.score }) end end end if self.bird.y > VIRTUAL_HEIGHT - 15 then gStateMachine:change('score', { score = self.score }) end
- The score itself is also tracked in
PlayState:update()
by incrementing a score counter each time the bird flies successfully through a PipePair.for k, pair in pairs(self.pipePairs) do if not pair.scored then if pair.x + PIPE_WIDTH < self.bird.x then self.score = self.score + 1 pair.scored = true end end pair:update(dt) end
- Logic for displaying the score to the screen during the PlayState is added to the
PlayState:render()
function:function PlayState:render() ... love.graphics.setFont(flappyFont) love.graphics.print('Score: ' .. tostring(self.score), 8, 8) ... end
- The ScoreState itself is implemented as an additional module with logical implementations for the empty methods in BaseState:
ScoreState = Class{__includes = BaseState} function ScoreState:enter(params) self.score = params.score end function ScoreState:update(dt) if love.keyboard.wasPressed('enter') or love.keyboard.wasPressed('return') then gStateMachine:change('play') end end function ScoreState:render() love.graphics.setFont(flappyFont) love.graphics.printf('Oof! You lost!', 0, 64, VIRTUAL_WIDTH, 'center') love.graphics.setFont(mediumFont) love.graphics.printf('Score: ' .. tostring(self.score), 0, 100, VIRTUAL_WIDTH, 'center') love.graphics.printf('Press Enter to Play Again!', 0, 160, VIRTUAL_WIDTH, 'center') end
bird10 (“The Countdown Update”)
- bird10 introduces yet another state, CountdownState, whose purpose is to give the user time to get ready before being thrust into the game.
Important Code
- First, we add CountdownState to our global state machine in
main.lua
:... require 'states/CountdownState' ... function love.load() ... gStateMachine = StateMachine { ... ['countdown'] = function() return CountdownState() end, ... } ... end
- The CountdownState is implemented as another module and it merely displays a 3-second countdown on the screen before play begins:
CountdownState = Class{__includes = BaseState} COUNTDOWN_TIME = 0.75 function CountdownState:init() self.count = 3 self.timer = 0 end function CountdownState:update(dt) self.timer = self.timer + dt if self.timer > COUNTDOWN_TIME then self.timer = self.timer % COUNTDOWN_TIME self.count = self.count - 1 if self.count == 0 then gStateMachine:change('play') end end end function CountdownState:render() love.graphics.setFont(hugeFont) love.graphics.printf(tostring(self.count), 0, 120, VIRTUAL_WIDTH, 'center') end
- As such, we modify our code in
TitleScreenState.lua
such that TitleScreenState transitions to CountdownState rather than directly to PlayState. - Then, in
CountdownState.lua
we transition to PlayState once the countdown reaches 0. - In
PlayState.lua
, we ensure that upon collision, we transition to ScoreState. - Finally, in
ScoreState.lua
, we transition back to CountdownState on keyboard input.
bird11 (“The Audio Update”)
- bird11 adds some music and sound effects to the game
Important Code
- We initialize a sounds table in
love.load()
, taking care to include the sound files we reference in our project directory, then set themusic
sound to loop indefinitely and begin playing it:sounds = { ['jump'] = love.audio.newSource('jump.wav', 'static'), ['explosion'] = love.audio.newSource('explosion.wav', 'static'), ['hurt'] = love.audio.newSource('hurt.wav', 'static'), ['score'] = love.audio.newSource('score.wav', 'static'), ['music'] = love.audio.newSource('marios_way.mp3', 'static') } sounds['music']:setLooping(true) sounds['music']:play()
- Lastly, we play the remaining sound effects in the PlayState module (jumps, score increases, collisions, etc.).
bird12 (“The Mouse Update”)
- bird12 adds mouse interactivity to the game in order to more closely resemble the original Flappy Bird iOS game.
- This update is left as an at-home exercise, but do take note of the following important function:
love.mousepressed(x, y, button)
- This function is a callback fired by LÖVE2D every time a mouse button is pressed; it also gives us the
(x, y)
of where the mouse cursor was at the time of the button press.
- This function is a callback fired by LÖVE2D every time a mouse button is pressed; it also gives us the
- If stuck on this exercise, feel free to open up bird12 to take a look at the implementation details. Be sure to read the comments carefully!