Home

Categories

About

GD50 Lecture 02 - Flappy Bird

Posted on February 05, 2018 by Abhirath Mahipal
Tags  game-developmentnotes

This is a part of a series of notes. You can find notes to other lectures here Please feel free to bring to my attention mistakes, other interesting resources and feedback via the comments section. I’m all ears and will do my best to incorporate them into the blog post.


Resources


Optional Reading

These links were suggested by Colton during the lecture. They are purely optional.

  • How to Make an RPG
    He uses Lua with his own custom engine (similar to Love) to make a Role Playing Game. Colton mentioned that he got his hands dirty with Lua using it.

  • Game Programming Patterns
    Great general guide to learn game development concepts for large scale game development. Personal Note:- I’ve read a few of his blog posts and I can vouch that (Bob/Robert) Nystrom is awesome.

Please note that this lecture builds on a lot of foundation that was set in the previous lecture.

bird0 “The Day-0 Update”

love.graphics.newImage(path) - Loads an image from a file that we can later use to draw to screen.

He sets 512px and 288px as the virtual dimensions. He chose them as it worked well with the assets and they also have a 16:9 resolution. You are free to choose whatever you want after a few trials.

A filter was set (The filter decides what’s to be done when you upscale or downscale a texture). You can read more about them in my previous blog post.

Covers quite a few things that are very similar to the first lecture.

-- images we load into memory from files to later draw onto the screen
local background = love.graphics.newImage('background.png')
local ground = love.graphics.newImage('ground.png')

The keyword local binds the scope of the variable to the current file. background and ground cannot be accessed outside the file and it also avoid variable collisions. If another file happens to use the same name, we won’t run into the risk of changing the global variable with the same name.

love.graphics.draw() can be used to draw anything that is of type drawable. Images come under the type drawable. So keep in mind that background and ground can now be drawn to screen.

He recommended using aseprite to create sprites. Other popular options are Photoshop and Gimp.

bird1 “The Parallax Update”

Parallax can help giving an illusion of movement. Different frames of references move at different rates. For example you are travelling in a car. The mountains in the distant move rather slowly when compared to the rate at which the fences on the road move. You get a better feel of movement and distance as well due to this.

Two variables backgroundScroll and groundScroll are declared to keep track of how much the respective images have moved. The background mountain image and the ground image slowly move across the screen and wrap around the screen when a certain point is reached. These variables will be used to compare against those certain points.

-- speed at which we should scroll our images, scaled by dt
local BACKGROUND_SCROLL_SPEED = 30
local GROUND_SCROLL_SPEED = 60

Two variables to store the rate at which the background and the ground move. It’s a convention to set variable names to all caps if they are not going to manipulated i.e they are some kind of constants.

Now he shows the technique he uses to get the mountain and the ground to infinitely scroll across the screen. The actual image has two copies of the same mountain next to each other. So it creates the effect of wrapping around the screen. When a certain point is reached we just snap it back to the original position. Have a look at the picture below.

looping-point-concept

It makes logical sense to snap back the picture once the first mountain reaches the end of the screen (if we continue going forward and if the left corner of the screen touches the second mountain the image will no longer cover the entire screen).

However we needn’t be so pedantic while setting the looping point of the ground’s image. If you notice it has a pattern that keeps repeating itself anyway. We shouldn’t notice any difference even if we set an arbitrary value.

backgroundScroll = (backgroundScroll + 
                   BACKGROUND_SCROLL_SPEED * dt) 
                   % BACKGROUND_LOOPING_POINT

The code above shows how it’s achieved. We curb the image’s movement beyond a certain point by using % BACKGROUND_LOOPING_POINT on the probable future position of the image.

There are ways to save memory (using an image that has twice the width of the regular image doesn’t seem very efficient, does it?). You could use a regular image and render it twice to create the desired effect. Then once a certain point is reached snap them back to their original position. Since there’s only one image and both the drawables point to the image, memory requirements are lesser. This same technique can be applied even for rendering more copies of the same image (it’ll be easier to appreciate the savings in memory from texture sizes now. You can get the same effect without using an image that is 4 times as wide as the normal image).

He shows a YouTube video where they’ve hacked the camera of a few games. They can now see things outside the reach of a normal user. By seeing things beyond the normal boundaries they spot a lot of tricks used in game development to keep memory requirements low. For example:-

  • A game only had the face of mountain rendered. It looked gigantic but on rotating the camera one could see that it’s actually hallow. By not having to render those points and vertices a huge deal of memory is saved.
  • A monster is positioned in a shop in such a way that you can only see it’s face. On digging deeper they notice it doesn’t have legs but the user is oblivious to that fact.

In short games give the illusion of doing lots of grand things by using neat tricks.

Check resources for a few parallax sites.

bird2 “The Bird Update”

He creates a bird class. Recall from the previous lecture that it’s a convention to start the name of class with an upper case letter.

function Bird:init()
    -- load bird image from disk and assign its width and height
    self.image = love.graphics.newImage('bird.png')
    self.width = self.image:getWidth()
    self.height = self.image:getHeight()

    -- position bird in the middle of the screen
    self.x = VIRTUAL_WIDTH / 2 - (self.width / 2)
    self.y = VIRTUAL_HEIGHT / 2 - (self.height / 2)
end

self.image has a reference to an object of the class Image. So image:getWidth() is just a member function in the image class. The same applies to image:getHeight().

x and y (self.x and self.y respectively) coordinates are set so that the bird is exactly in the center. If you don’t understand the calculation used to determine it, please refer how Colton center aligns text in the last lecture.

The bird doesn’t move along the X axis at all during the game. It starts vertically and horizontally centered. It only moves along the Y axis during the game.

The Bird class also has a render method. Recall that delegating stuff to their respective classes can help reduce clutter in the main files, help us think more abstractly about the game and helps a lot with scaling.

bird3 “The Gravity Update”

Gravity’s forces an object to fall faster and faster (recall that it has an acceleration of 9.8 m/s^2). So the bird should fall faster and faster.

A Bird class is created and relevant functions refactored to it.

In Bird.lua he sets GRAVITY to 20. It’s a value that he found to work well. You can experiment and set any value. self.dy is the change in the Bird Y coordinate due to gravity.

function Bird:update(dt)
    -- apply gravity to velocity
    self.dy = self.dy + GRAVITY * dt

    -- apply current velocity to Y position
    self.y = self.y + self.dy 
end

Notice in the code snippet above how GRAVITY is added to dy again and again. So dy i.e the bird’s movement becomes larger and larger as time progresses thus imitating the actual behaviour of gravity.

bird4 “The Anti-Gravity Update”

Now we try to add the jumping action once the space key is pressed. Intuitively you know that it should move in the opposite direction of gravity. dy should be negative (so that the bird moves up) and it should move up slower and slower as time progresses. By changing the sign of dy from positive to negative we can make it move in the opposite direction. If -5 gets the bird up by 5 pixels, -3 will get the bird up by 3 pixels. We want to slowly reduce the rate at which it moves up and eventually it should start moving down again.

This is done by adding small positive value to dy over time. See lines 29 - 36 in Bird.lua and run through the changes on a piece of paper for better understanding.

He also designs a way to refactor input handling to their respective classes so as to avoid clutter.

-- main.lua

-- line 70
    love.keyboard.keysPressed = {}

--line 77
function love.keypressed(key)
    -- add to our table of keys pressed this frame
    love.keyboard.keysPressed[key] = true
    
    if key == 'escape' then
        love.event.quit()
    end
end


-- line 98 - 110
function love.update(dt)
    -- other stuff here

    -- reset input table
    love.keyboard.keysPressed = {}
end

Line 70 in main.lua creates a global table to keep track of all the keys pressed. In Lua everything (except the basic data types) are tables. So love.keyboard.keyPressed simply adds a new field to the table love.keyboard.

Line 77 shows that every time a key is pressed the relevant key is set to true in the global table. We reset the table every frame (line 98 - 110). We wouldn’t want keyPresses to stick.

Now that we have a global table which has a record of all the keys pressed in that very frame, we can use it to query keypresses. Every class can look for keypresses it cares about and we can thus eliminate a large number of if conditions in main.lua. Example below.

function Bird:update(dt)
    -- apply gravity to velocity
    self.dy = self.dy + GRAVITY * dt

    -- wasPressed in just a helper function which access the
    -- global keyPressed table we just created and returns
    -- true or false
    if love.keyboard.wasPressed('space') then
        self.dy = -5
    end

    -- apply current velocity to Y position
    self.y = self.y + self.dy
end

Notice how the Bird class can simply check if space was pressed or not every frame and act accordingly. main.lua only updates the global table and individual classes can use love.keyboard.wasPressed (a function defined by us) to check if a key they care about was pressed in the last frame or not.

A nice in-depth write up on predicting the difficulty of Flappy Bird.

bird5 “The Infinite Pipe Update”

We need to keep destroying pipes as they leave the view. One could argue that we only use pointers (references) to the image object for every pipe we create and they wouldn’t amount to much. Colton explains that given enough time, we’ll run out of memory (and our game will definitely use more memory than required) and crash.

If you notice the init function in Bird.lua, it’ll become obvious that for every bird we put on the screen, it loads an image rather than pointing to a preloaded image object. It wasn’t a necessary design decision because we’re absolutely certain that we won’t load more than one bird into memory. The decision to share the image amongst all the pipes becomes necessary because there are infinite pipes.

local PIPE_SCROLL = -60 is set so that we can use it to smoothly move the pipe from the left to the right (recall that -60 is multiplied by dt).

Have a look at the Pipe:init(), you will notice that the X coordinate is just set outside the screen. The pipe exists but is outside the user’s view (recall that pipes don’t appear as soon you start the game).

Colton has used a long pipe sprite. By giving it different Y coordinates you can make it look like they have different heights. Notice in the picture below that all pipes have the same length but appear to be different to the user.

pipe-different-heights

random.seed() is set in main.lua (it’s typicall a global thing which effects the random function as a whole hence we set it in the main file). A function to get the width of the pipe is created (will be helpful when we want to destroy pipes or check for scoring).

The Pipe:update() method is a single line function which updates it’s X coordinate. PIPE_SCROLL is the speed at which the pipe should scroll - self.x = self.x + PIPE_SCROLL * dt.

A table to keep track of the pipes currently in the screen is created in main.lua. We keep appending new pipe objects without specifying any key. It mimics the behaviour of a linked list.

A variable spawnTimer is initialised to keep track of the time elapsed since a new pipe was created. If it exceeds 2 seconds we create a new pipe and append it to the table.

function love.update(dt)
    -- line number 116 
    -- dt is the time passed since the last frame
    -- if we keep doing this every frame
    -- we can get the actual time elapsed
    spawnTimer = spawnTimer + dt

    -- spawn a new Pipe if the timer is past 2 seconds
    if spawnTimer > 2 then
        table.insert(pipes, Pipe())
        print('Added new pipe!')
        spawnTimer = 0
    end

Now we have a table which keep storing all the pipes created. We iterate through all the pipes stored in the table and check if the right edge of the pipe (left corner coordinate + the width of the sprite) has crossed the left edge of the screen or not. If it has, we simple delete the pipe (see lines 129 - 136 in main.lua).

Also remember to have the right render order. If the pipes are rendered before the background image, the background image is simply drawn over the pipes and pipes remain hidden from view.

Check resources and check out the link on table iteration in Lua.

bird6 “The Pipe Pair Update”

A new class for clubbing pipes together is created. It uses components (pipes) to build something more complex.

The earlier table that keeps track of pipe pairs is renamed appropriately to pipePairs.

The variable local lastY hold the Y coordinate of the last pipe. We use this value while generating the next pipe. We want pipes that have gaps that are reasonably close to each other so that the game is playable. For this we need some information about the last pipe.

Line 132 of main.lua -> math.max() and math.min() are used to sandwich the Y coordinate of the pipe such that they are at least 10 pixels below the top edge of the screen and are at least 90 pixels away from the bottom edge of the screen. math.random() is used to reduce or increase the Y coordinate by upto 20 pixels with respect to the Y coordinate of the last pipe.

Tweak the y value to vary the level of difficulty or to make the pipes look more natural.

Now the Pipe class takes a string and a Y coordinate. This makes it easier to use the PipePair class. We can pass in lower or upper to invert the pipe.

Check line 26 of PipePair.lua to see how the lower pipe is rendered using the Y coordinate of the upper pipe. PipePair also has a member variable called remove that acts as a flag. If marked as true it is deleted from our table that keeps track of pipePairs. Read lines 143 - 157 of main.lua to see how PipePairs are updated and deleted. Do not delete elements from a table while iterating through it. It’ll result in indices being skipped.

Changes to the Pipe Class

  • init takes in the orientation (lower or upper).
  • Use of parameters to invert the image and set the Y coordinate of the pipe depending on the orientation of the pipe. See line 39 in Pipe.lua. We scale upper pipes by -1. Scaling by -1 gives a mirror image (hence upside down) but the size remains the same. Scaling by 2 doubles the size. Also notice how the Y coordinate is taken care of by using an if condition.

Interesting Overflow Error
If we let the bird fall for a while below the screen, it’ll suddenly appear from the top edge of the screen. Recall how it falls faster and faster (dy becomes very large) and then all of a sudden dy overflows and becomes very negative (negative dy results in an upward movement), one fine frame it renders above the screen and then gravity starts to act and it then falls from the top.

Stuff like speed of generation of pipes, gap height, lastY value etc can be tweaked to give a different feel and vary the difficulty of the game.

bird7 “The Collision Update”

local scrolling = true in main.lua is used to toggle pause and scroll states. If the bird collides with any of the pipes, it’s set to false and also pauses further rendering.

We iterate over the table of pipes to check if the bird collides with any of them or not (lines 152 - 157 in main.lua).

We use Axis Aligned Bounding Boxes just like last time. You might be wondering that the bird isn’t a rectangle and won’t entirely fill in the rectangle formed by it’s X and Y coordinates. You are right about it.

function Bird:collides(pipe)
    -- the 2's are left and top offsets
    -- the 4's are right and bottom offsets
    -- both offsets are used to shrink the bounding box to give the player
    -- a little bit of leeway with the collision
    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

We give an offset of 2 & 4 (see the code snippet above) so that we don’t frustrate the users. If we go by the actual X and Y coordinate, the bird will be close to the pipe (but not touch it) and yet the user would lose the game as the bird doesn’t reach the corner of the box formed by the X and Y coordinates. This would really frustrate the user. In addition we give in a couple of extra pixels so that it actually looks like it’s colliding. Also by giving a few extra pixels of space we are being more liberal to the end user and might actually make it more enjoyable for him.

Colton briefly describes an alternative style of refactoring. Instead of giving the bird a collides method, you can create a generic function which takes in two drawables and checks if they collide or not. It’s more scalable.

It is interesting to note that at times you might not observe a pixel perfect collision. The bird might be too much into the pipe. The collision function isn’t faulty nor was it’s execution delayed, the bird probably has moved too much in a single frame and we check for collisions after it has made it’s move (remember that movement, checking for collisions etc happens once every frame).

bird8 “The State Machine Update”

He showed the possible different states. Check slide#30 for the same. Certain input keys and conditions trigger transition from one state to another.

He imports a class called StateMachine, a base StateMachine class along with few game specific StateMachine classes. Also a few fonts are initialised.

gStateMachine is declared in Main.lua. It is a table with some mapping to States (each state contains functions that are relevant to it). It is a convention to prefix global variables with a g so that it’s instantly recognisable that it’s changing some global state (even though it doesn’t tell which file the global actually resides in). Likewise m denotes that it’s a member field of a class.

Now the love.update(dt) function in main.lua only directly updates code that is common to all states (the mountains and the ground keep scrolling irrespective of which state the game is in). It doesn’t make sense to duplicate this across all states and therefore remains in main.lua. Everything else is delegated to the respective function that gStateMachine calls. Similar is the case of the love.draw().

The StateMachine class is taken from the book mentioned earlier as optional reading in the lecture. StateMachine is a generic class and also a wrapper that we use render, update (position, health, score etc), switch and exit states (mostly for clean up. Graphic objects need to be destroyed etc). This is the class that actually takes in states like TitleScreenState, PlayState etc during it’s initialisation. It can be thought of controlling or managing the states in the way of calling their respective exit, render and update logic whenever needed.

BaseState initiates empty methods which can be inherited. It saves us from writing some broilerplate code for each state. TitleScreenState for instance inherits BaseState so it has the exact function definitions of BaseState and then some changes and additions are made to it’s functions. I would like to repeat:- TitleScreenState has it’s own member functions for cleaning, rendering etc but it’s controlled by the StateMachine class. The StateMachine class just handles possible states.

It’s like having a group of kids with different abilities (the state classes) and a teacher who takes charge of them (the StateMachine class).

bird9 “The Score Update”

A new score state is now included. Line number 96 of main.lua shows the relevant function call as well.

A new member variable scored in introduced in PipePair.lua. If the bird goes past the right edge of the pipe it is set to true. The score is checked by iterating through all the pairs of pipes (lines 53 - 61 of state/PlayState.lua). We check if the X coordinate of the bird has crossed the X coordinate of the pipe’s right edge (add the width of the pipe to the X coordinate of the left edge).

 -- simple collision between bird and all pipes in pairs
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 it collides with a pipe, we use gStateMachine:change() to switch from the play state to the score state which displays the final score. If you recall gStateMachine:change() can take in optional parameters as well. We pass in the score. By writing the gStateMachine:change() function in this fashion, we can pass in local variables while switching to another state. This saves us from creating global variables just for sharing data between two states. If we had to share a variable across all states or many states it would at times make sense to make the variable global. However only the PlayState and ScoreState need the score variable (no other state uses it), so it is better to keep your code base functional and pass in stuff required by the state by the way of a function.

Similarly we switch to the score state if the bird touches the ground (lines 92 - 96 from state/PlayState.lua).

bird10 "The Countdown Update"

It’s just another state. A new class CountDownState is created and a new key is added to gStateMachine which maps to the relevant function (look closely - the inline function actually returns an object). He uses hugeFont (initialised earlier) to display 3..2..1.

A few fields are added to the CountDownState class. count is used to keep track of the seconds pending until play and timer keeps track of the quantum of time elapsed since the last decrement in count. COUNTDOWN_TIME is the time between each tick, it’s set to 0.75 because 1 second between each tick is a little long.

Recall how we kept track of time to spawn pipes (kept adding dt through every frame) until it reached 2 seconds. A similar approach is used. self.timer = self.timer % COUNTDOWN_TIME is executed once the sum of the accumulated dts crosses 0.75. % or modulo is used to preserve information if the timer happens to exceed 0.75 (we would lose this information if we just set self.timer to 0), it starts the next tick with the excess time from the last tick. This helps keep a smooth track of time (if excess time is taking during a tick, we deduct that much in the next tick).

When self.count touches 0, we use gStateMachine to switch to the play state.

The title screen now enters the countdown state instead of activating the play state directly.

bird11 “The Audio Update”

We now have a table of sounds. They all map to data types that hold audio. The sound effects were created using Bfxr. He also used a free soundtrack from FreeSound (Please be sure to check the licensing terms before using them). He also sets one of the audio files to loop infinitely. Flappy Bird is an infinite game and the background music shouldn’t stop abruptly.

Sound effects are added in the respective places. For instance sounds earned for a point scored is places just below the code responsible for updating the score. In line 80 of PlayState.lua he layers two sounds on top of each other for the desired effect (quite common in games and sound engineering). sounds['explosion'] is white noise and sounds['hurt'] is a downward or sine wave type of sound.

Check resources if you’re curious about sound wave types.

bird12 “The Mouse Update”

It’s left as an exercise to the viewer. Use the function love.mousepressed(x, y, button). Remember how keypresses were made global to relieve main.lua. A similar approach can be used for mouse clicks as well. Each class can take care of the input keys that they care about.

You can find the code for same in the GitHub repository though.

End