COLTON OGDEN: Hi, everybody.
My name is Colton Ogden, and
this is GD50 lecture one,
and today we'll be covering Flappy Bird.
So last year or last week,
sorry, we covered Pong,
which was just basic shapes and colors.
Today we'll actually
be diving into sprites.
As we can see here, we've
got some pipes, and a bird,
and we're covering a few other
concepts such as gravity, and more.
Today, the topics that we'll be covering
are in a nutshell, images and sprites,
as I just said.
So loading images from
memory from our hard drive,
and actually drawing them to the
screen instead of just rectangles,
and whatnot.
We'll be covering infinite scrolling.
So seeing things like-- and
if you've played the game,
pipes are infinitely
going from right to left.
How to actually get that going
infinitely so that we're not
using up also infinite memory.
We'll be discussing how games,
and in the similar vein,
are illusions in the sense that a lot
of the perceived vastness and perceived
complexity of games is often
just due to camera trickery,
and more because of limited hardware.
We'll be covering
procedural generation, which
ties also into infinite scrolling.
Procedure generation is a topic that
I am actually very interested in,
and will be touching on it throughout
the course in several locations.
But in the context of today's lecture,
we'll be using it for the pipes
because the pipes, they spawn
from right to left in Flappy Bird
as you're infinitely
going through the level,
but they can spawn at
various heights, and the gaps
are shifting as a result of that,
therefore creating this infinite level.
We'll be talking more in
detail on state machines.
So last week we covered state
machines in a very abstract sense.
We used just basically a string as a
variable, and then used if conditions.
Today we'll be actually
using a state machine
class replete with
various methods that allow
us to transition in and
out of these states very
cleanly, and allow us to
break out all of this logic
that we previously had in our
update and render functions,
and then put them separately
into their own state classes.
And then lastly, we'll also
be touching on mouse input.
And a point that I
forgot to mention here,
whoops, is also we'll be talking
about music, which is just basically
sound, which we did last week.
But we'll add that as a polishing touch.
If you guys want to
download the demo code.
We have a repo up right now
on GitHub/games50/fiftybirds.
It's our take on Flappy Bird.
A couple of things, I've
been asked a couple of times
whether we have reading
materials for the course.
And there are no formal
reading materials,
but there are a couple of
resources that I really
enjoyed reading, especially as I was
getting more into Lua and Love2D.
They are two books.
One is an online book.
Actually, they're both online
books, but the latter of which
has a physical form as well.
The first of these is How to Make
an RPG by Dan Schoeller, which
is actually completely written in Lua.
He uses a custom game engine
very similar to Love2D,
but it's handwritten by him.
But a lot of the same ideas apply,
and it's a great opportunity.
It's how I cut my teeth
on Lua, and I would
encourage you to take a look at that if
that's something you're interested in,
or if you like RPGs.
And then also Game Programming
Patterns by Robert Nystrom
is a very great general
purpose game development
book that talks about a lot of the sort
of more abstract high level concepts
with large scale game development.
But beyond that, no formal reading.
Those aren't from reading either.
Those are just if
you're curious, and you
want to read some resources
that I found very interesting.
Feel free to do so.
Today's goal is to implement
what looks like this.
This is our version of Flappy Bird.
We didn't use the same exact
sprites for copyright purposes,
but we note that we have a bird
in the middle of the screen.
This bird, on click, or on
spacebar, will jump up and down,
and your goal is to prevent
the bird from touching
either the pipes or the ground itself.
Every time you make it past a pair
of pipes, you will score a point.
As soon as you touch a pipe or
hit the ground, the game is over,
and that's that.
So today we'll be covering--
I'll be doing a little
bit more live coding.
So the very first example
that I want to cover
is the day zero update for Flappy Bird.
And a important function
that is going to be probably
the most noticeable, the most
visibly obvious function we'll
be using throughout this lecture,
is love.graphics.newImage,
which takes a path.
This function, all it does is
load a image file from your disk.
You specify it as a string, and
you can then use it as an object,
and draw anywhere you
want at an xy coordinate,
and we'll see this in practice here.
So I'm going to go ahead.
If you're looking in the repo, all
of these examples are covered--
0 through 12.
I'm going to start from scratch
in a new folder that I've created.
I'm going to create a brand
new main.lua, completely fresh.
And the first thing I
want to do is because we
are going to use a virtual resolution
just like we did last week,
so that we have a more
rhetoric, I'm going
to go ahead and require
the push library.
So push equals require.
Push just like that.
I've pre put push.lua
into this directory.
It'll just load by default
in the same directory--
the current working directory of
your script when you run Love.
The next thing I'm going to do,
I'm going to define some constants.
So window with should be 1280, and
then window height is going to be 720.
Those are our physical
window dimensions,
but then we also need a virtual width,
and we're going to use 512 by 288.
This is a resolution that I found
worked pretty well for the assets we'll
be using today, but you can make this
most anything you want to as long
as it's somewhere in that range.
It is a 16 by 9 resolution as
well, so that it fits comfortably
on modern wide screen 16 by 9 monitors.
What we're going to do is the
first goal that we have today
is to draw two images to the screen.
We want a foreground and a
background because notice,
if we go back to the slides, we
can see in the very background
we have a hill landscape, and then
on the bottom we have a ground.
The two of those are going to
eventually scroll at different rates.
It's going to be called
parallax scrolling, but just
for our very first example,
we want something very basic.
I just want to draw two
images to the screen.
So we're going to go ahead and do
that here by setting a local variable.
Remember, local means that it's
just defined to the scope that
it's in, rather than being global, which
means we cannot access this variable
outside of this file.
Local background gets
love.graphics.newImage,
the function that we just talked about.
Let me go ahead and hide this inspector
here so we can have more room code.
And then it's just
going to take a string.
So background.png.
And I realize I actually didn't
include those files in the directory.
So I'm going to need to do that as well.
Same thing for the ground,
exact same function,
love.graphics.newImage
except ground.png.
And before I forget, let's go
ahead, and do that right now.
I have the files here.
So ground and background.
We're going to copy those from the
distro repo into my bird0 directory
that I'm currently
developing in right now.
And as soon as we're done with
that, we're going to go ahead,
and we're going to
define love.load, which
is the function Love2D calls at the
beginning of your program execution.
In there, because we don't want
these images to look blurry
when they get loaded
and upscaled, we want
to go ahead and set our default
filter to nearest on min and mag,
which means on upscale and downscale,
apply nearest neighbor filtering, which
means no blurriness, no
interpolation of the pixels.
And then one thing that is
just a small little touch,
love.window.setTitle fifty
bird because it's GD50,
and then we're going to go
ahead and set up our screen here
with our virtual width, virtual
height, window width, window height.
It's getting a bit long, and
then it takes in the table.
Recall tables, just
take in keys like so.
Unlike in Python where you might use
a colon, we use an equal sign in Love,
or in Lua, I should say.
Resizable to true, and that is
the end of our load function.
Now, does anybody recall
how if we want to resize--
so notice I set resizable to true.
Do we know how we can send a message
to push to resize our screen for us?
So Love2D to defines a
function called love.resize,
which takes in a width and a height.
And in there, all we're going to
do is defer that call to push.
Recall the exact same function on push.
It takes a width and a
height, and that will
take care of dynamically rescaling
the canvas it uses internally.
It renders to a texture, and it's
going to render to the texture
that we set as the virtual
width and the virtual height,
and it's going to scale
it to fit our screen.
And it needs to know our
physical screen dimensions
so that it can actually properly scale
that internal canvas appropriately.
Does anybody remember the function
that we use to get input from the user?
So function.love.keyPressed,
recall, takes in a key.
Love is going to call this
automatically every time we press a key,
and that's going to be--
and we're going to have access to that
key, and we can do any sort of logic
that we want on that
key, using that key,
and we're just going
to call love.event.quit
because I don't like to press
Command Q or click the red x.
I just want to hit
escape, be done with it.
And then what's our render-- what's
Love's render function called?
it's called love.draw.
So call love.draw.
And then because we're
using push, does anybody
remember what we need to
actually do to get push to render
our screen to a virtual resolution?
So recall that there's
actually two ways we can do it.
We can call push start and push finish,
which we didn't cover last week.
Or we can call-- and that's actually
the new de facto way to do it.
Or we can do push apply start, which
is the deprecated way to do it.
But starting from here on out,
we're going to call push start
and push finish.
And then last, we have our images.
We've allocated them as objects up here.
We have a background and a ground.
All we need to do now is
just draw them to the screen.
So this is a new function.
Or it's actually not a new function.
It is a new function, actually.
Love.graphics.rectangle is what we used
last week for all of the draw calls.
In this case, we want
to draw an image object,
a texture object that we have in memory.
So we're going to call
love.graphics.draw,
and it takes a drawable,
which means anything
that Love has defined as
something that can be drawn.
In this case, images are drawables.
They can be drawn, and they can be drawn
at any given position that you specify.
So if you wanted to draw
it at the top left corner,
we would just say
love.graphics.drawBackground at 0,0,
and it has that effect.
And we're going to the exact
same thing with our ground.
The only difference being
that obviously, we don't
want to draw at the top of corner.
We want to draw at the
bottom of the screen.
So we just call virtual
height minus 16, which
happens to be the height of our image.
So if you run this--
I'm going to go ahead, and make
sure I'm in the right directory.
I'm not in the right directory, so I'm
going to go into a directory I wrote,
fifty bird scratch.
Go into bird0.
And if I run this, I should
theoretically have just two images
layered on top of each
other, which I do not.
So we just make sure that it gets saved.
Remember to always save
your work, and there you go.
So all we're doing now, it looks
infinitely better than last week
already, but it's very simple,
very few lines of code.
All the effort that
we've put into it has
been in our sprite
editor of choice, and you
can use most any application you
want to do this sort of stuff.
I use a program called
Aseprite, I like a lot,
but you could do this in Gimp, which
is free, you could do it in Photoshop,
you could do it in Microsoft
Paint if you wanted to.
Godspeed if you do.
But yeah, so that's as simple as it
is just to draw images to the screen.
So we've already made quite a lot of
progress in a very short period of time
in terms of the visual
aspect of our game.
But it's not interesting to look at
beyond the initial sort of honeymoon
period of now we have
colors on the screen.
We want to actually get scrolling
because the game, recall, is
a scrolling game.
And actually, would anybody
be willing to volunteer
to come up, and play Flappy Bird just
so we can see it live on the stage?
David, you want to come up and play?
Does someone volunteer?
Stephen, you want to come up and play?
STEPHEN: Sure.
COLTON OGDEN: Thank
you for volunteering.
I guarantee you're better
at this game than I am.
I'm going to go ahead and cd into
bird12 in the directory, which is
the final version of the game complete.
So I'm going to go ahead and hit Enter.
[VIDEO GAME MUSIC PLAYING]
So already, we can see
the parallax scrolling
that I referred to
before, which is the floor
and the background are
scrolling at different rates,
and we'll see this very
shortly in the next example.
We have a prompt.
We have text.
We've already used this
before with the font.
So go ahead, if you press Enter,
you're going to get a count down.
So space is to jump.
[BEEPING]
So we have our bird jumping
in the middle of the screen.
We have a score at the top.
The goal is to avoid hitting the pipes.
[CRASH]
OK, a score of one.
You want to try again?
Go ahead.
[BEEPING]
So it keeps track of his
position, and every time
he gets past the right edge of
a pair of pipes, as you can see,
that's when he gets a point.
So if you recall from last
week, what do we think is--
what's detecting the collision?
If we remember last
week, what's the term?
Anybody remember?
Axis aligned bound aabb collision
detection, axis-aligned bounding box.
It's the same thing that we did with
Pong, except now we're doing it--
we have graphics, but it's
the same exact concept.
We're just using rectangles.
And when one rectangle overlaps with
another rectangle, we trigger death.
So one last iteration I
think, and then we'll.
[CRASH]
We'll let you try one more time.
Go ahead.
I'll give it a shot.
I'm going to lose on purpose.
OK, here we go.
To be unfair, I got plenty of
practice when I was developing this,
but we'll see if that
actually holds true here.
So notice also, the pipes--
the procedure generation
that I-- oh, I lost.
Three points.
Let me explain a little bit more,
while I do one more iteration.
But the pipes themselves,
every time we start,
they're spawning at
a different location.
This is proceeded generation in
pretty much the most simplest
way possible, and notice that
the pipes are shifting gradually.
So this is sort of the
make up of our level,
and it's just generating bit by
bit do to some simple algorithm
that we have that just says hey,
spawn another pipe here, shift it
by some amount.
And this very simple approach allows us
to have an infinite level over and over
again, and it's very efficient.
We only ever have as many pipes on
the screen, and as we'll see soon--
we'll only have as many pipes in
memory as we can see on the screen
at one time, despite the fact that
this level could theoretically
go on infinitely, and so
it's very cost efficient.
So bird1 is the example.
It's the parallax update.
So parallax scrolling is
an important concept in 2D
and also 3D, but 2D game development.
It refers to the illusion of movement
given two frames of reference
that are moving at different rates.
So if you're driving on the highway,
and you see a fence next to you,
and you see mountains
in the distance, you're
observing parallax scroll by
seeing how fast the fence moves
relative to the mountains.
The mountains are going to move a
lot more slowly than the fences right
next to you.
And we accomplish the same
exact illusion in our game
by using this sort of
graphical illusion.
And so I'm going to go
ahead in my directory
here, in bird1, which
is an unpopulated--
it's populated with the contents of
bird0, the complete contents of bird0.
The version that you'll see
will have all of the code,
but I'm going to go ahead, and if
we run bird0 in that directory.
So I think right now I'm
still in the full distro.
So let me go ahead, go into
fifty bird scratch again.
Whoops, where am I?
And then I'm going to go
into bird1, and run it,
and I get the exact same
image that we had last time.
So everything is there from before,
just two images, nothing moving,
no parallax that we can observe.
I'm going to go ahead,
and start implementing
the basics of this parallax.
So if I go ahead in my main.
So I'm going to go down here to
where we have our background.
So we need a couple of new things.
So along with our
background image, we need
to keep track of how much
it's scrolled because we're
going to need to start drawing
this image to the screen,
but if we're going to
scroll it, that means
that we need to shift its x offset.
Instead of drawing it at
0,0, if we want it to scroll,
we have to draw it at some
negative value instead.
Over time, this will have the
effect of it moving right to left.
So I'm going to go
ahead, and keep track--
I'm going to use a variable
to keep track of the scroll
now, for both of these
images, and we're just
going to call them backgroundScroll
and groundScroll, and set them to 0.
So this is going to have
the effect of no x offset.
So I could use this variable right
now in this draw call down here,
which I'm actually going to do.
I'm going to go ahead, and go to--
I'm just going to find
out if that is correct.
I'm going to go ahead and set
that to negative backgroundScroll.
And here, I'm going to set
this to negative groundScroll.
So this is not going
to change anything yet.
It's going to be the exact same
thing because they're both 0.
They were 0 before, but we're
going to change them over time.
And in order to do this, I'm going
to go ahead, and go into up here.
One thing before we do that actually,
we need to set a speed for this.
This is going to happen
over time, but since they
need to occur at different
rates, the background
needs to go at a slower
rate than the foreground
so that we do get this parallax effect,
we need to separate speed variables.
Generally, the norm for something
that is not going to change
is to write it in caps with underscores.
This is constant notation.
This is frequently seen in
most programming languages.
We'll use it here.
I'm going to set a variable
called BACKGROUND_SCROLL_SPEED,
and I'm just going to set that to 30.
I'm going to do the same
thing, GROUND_SCROLL_SPEED.
Does this need to be higher or lower
than the BACKGROUND_SCROLL_SPEED?
The ground is going to move--
so the background needs to move
slower than the ground does.
So this is going to be higher.
So we're just going to set it to 60.
You can set it to whatever you want
to get the effect that you want,
but this will already
be quite noticeable.
The ground is going to move
twice as fast as the background.
And so what we're going
to do also is if we
just-- so what's going to happen if we
just let our image scroll infinitely?
What's going to happen
at a certain point?
AUDIENCE: Run out of image?
COLTON OGDEN: It's
going run at an image.
So how do we fix this problem?
AUDIENCE: Loop it.
COLTON OGDEN: Loop it, exactly.
So we're going to go ahead,
and set a looping point.
So another constant
background looping point,
and we're going to set this
to 413, which you kind of have
to look at your image, and determine--
you sort of have to set your images up,
if you want to achieve this effect,
by having them be a looping image.
So have either two copies of the exact
same thing that's your screen width,
or just copy the same
chunk over, and over again.
There's many ways to do it.
In this case, the looping point
of the image of our background
is 413 on the x-axis.
So we're going to set that to 413.
And then we're going to go ahead--
the next step is we actually have
to start changing the value values.
So in our update
function, which is where
this is going to happen,
I'm going to go ahead,
and define love.update, which
recall, Love2D will call for you,
but you must define it yourself.
I'm going to go ahead, and
set backgroundScroll too.
So what this is going
to do, backgroundScroll
gets backgroundScroll to itself plus the
speed we set before times delta time.
So it stays frame rate independent.
That will have the effect of
adding the speed to our image,
but we need to reset it.
We need to actually perform the reset.
And to do that, we'll just be
using Modulus, which recall
from languages like C, simply divides--
basically, sets that value to
the remainder of that division.
So in this case-- so
10 modulo 5 would be 0,
but 10 modulo 9 would be 1,
effectively, because we have 0
left over once we divide 10 by 5.
We have 1 left over
once we divide 10 by 9.
So I apologize if that
concept is not new.
But we're going to do the same
exact thing for our ground,
only we're going to modulo by
our virtual width in this case.
I did not set a looping point.
I do in later examples, but
our ground image is very--
it's consistent enough
such that you don't even
notice it when it loops without
just using the virtual with.
So we're just going to use the
virtual width in that case.
It's very patterned, and very small.
And aside from that, we already
have the background scrolls here
in our draw functions.
So when we run this code,
we should theoretically
have scrolling background.
AUDIENCE: So does it even just
have to be twice the width
or something so they don't run out?
COLTON OGDEN: They do, at
least twice the width, yes.
There's ways you could
effectively tile your image,
and do it that way to save
memory on texture size.
If you have maybe something that's
a quarter of the screen size
that you want to loop
over, and over again, you
don't want to have
that as one big image,
you'll just draw four copies of
that image to fill your screen,
and then to shift all of them.
Maybe five, actually,
so you have a little bit
beyond the edge of the screen, and
then just put all of them back to 0.
AUDIENCE: So the bottom
one, the ground is--
you wouldn't know if you just restarted
showing the image with the larger
background.
You wouldn't have to worry about
the mountain getting cut in half
when you replaced the right--
COLTON OGDEN: Exactly.
So we could actually--
I could show you right now
what that will look like.
So if we just take out
the looping point here,
or we set it to some value that's
completely inaccurate like 270,
and then we run it, after
a while it should just cut.
Yeah, right there.
AUDIENCE: So are you
drawing it twice, really?
Like one after another one
when it runs out or something?
COLTON OGDEN: No, the image is so wide
that it always will fill the screen,
even after it's been set back to-- even
after it's gone past the looping point.
I forget how large the texture is.
It is 1157 pixels wide.
So it's more than
twice the screen width.
Actually, I think it is
exactly twice the screen width.
No, it's not exactly
twice the screen width,
but it's more than
twice the screen width
so that when the amount--
the 413 pixels has elapsed,
it's still plenty past the right edge
of the screen, and the looping part,
it'll be the exact same
appearance on the texture,
but it's completely been
shifted back to the right.
The 0,0 of our image is now
at 0,0 in our screen space.
AUDIENCE: So the looping is
just reloading [INAUDIBLE]??
COLTON OGDEN: Your image is here,
moving, and then just instantly
back to the beginning,
and then moving back to--
the setting it back to 0
or technically, how many
pixels it's gone past the edge of
the screen because using modulo.
AUDIENCE: So it's just one image.
It's like you just instantaneously
flip it at the right time.
COLTON OGDEN: Yeah.
It's a translation.
It's an instant translation.
It takes place over one frame.
So you don't notice it.
Your human eye can't see it because
it literally happens in one frame.
The image data is the exact
same at those two points
because we have a texture.
We've pre-created a texture
that has the exact same data
so that you have that effect.
You have to have a texture that
allows you to do this, or smartly
draw four of the same images.
Keep track of all four of them--
or actually, eight of them-- so
you can move them to the left,
and then shift them
all back to the right.
AUDIENCE: I assume when
we do Super Mario Bros.,
we're going to have multiple images
that get stacked one after another.
COLTON OGDEN: When we
get to Super Mario Bros.,
we'll be talking about a concept
called tile mapping, which
is where we take a sprite sheet,
and then you basically chop it up
into pieces, have a map that is
basically numerical so that a brick is
value one, and then you look through
this giant two dimensional array
that you have, and then go
over it, iterate over it,
and then draw a tile at an offset
based on your index into that map.
So it's a little bit more complicated,
and actually a lot more memory
efficient, but slightly
different implementation.
OK, so we have parallax scrolling now.
I want to take a moment to--
because we've touched on--
this is a very introductory way of
demonstrating that games are illusions,
by using parallax scrolling.
All we've done, really, is just set two
things to scroll at different rates,
and this has made us feel like
we have depth in our scene,
but all we're doing, we
have two images, we're
scrolling them at different rates.
But this is a common
theme in game development,
is trying to devise a scene
that maybe is very elaborate,
but doing it on very resource
intensive devices like your iPhone,
or like an old console
like the Nintendo 64.
These illusions are all over
the place, and a YouTube channel
that I recently found
that I really like is
it's called-- the name of the channel is
She Says, but the actual show that they
have is called Boundary Break.
And what they do is
they take a camera that
goes beyond what the game
developers allowed it to do,
which they basically hack the game
camera so you can see in places where
you weren't supposed
to see before, and you
can see a lot of really cool trickery.
I'm about to show you a
couple of video clips,
but here's the YouTube URL if you're
curious to see the exact video.
It's about a 33 minute video.
It's on Zelda, Ocarina
of Time for the N64.
And I extracted a couple of
particularly noteworthy clips
that I thought were kind of
interesting, and also humorous.
I'm going to go ahead
and show the clip now.
So if we could dim the lights,
I'll go ahead and start.
This is the first example.
SPEAKER 1: OK, so there's a
lot to talk about with the shop
owners in the Ocarina of Time.
So I'm going to just condense
it down to the most interesting,
and the first one we're
going to talk about
is the bizarre shop owner in Hyrule.
Now, in Majora's Mask, the very same
character is actually shown with legs,
but in Ocarina of Time,
he did not have those.
In fact, he looks extremely
hilarious without his legs.
COLTON OGDEN: So this is a--
does anybody have an instinct as to
why they might have done this this way?
AUDIENCE: You're not
going to see it anyway.
COLTON OGDEN: Exactly, and beyond
that, also just saving on memory.
Not having to load a character model--
the vertices and textures associated
with it--
on such a memory constrained
device like the N64.
I forget how much memory it had.
Like four megabytes of memory,
I think less than that.
And so they are obviously cutting
however many corners they could.
In this case, by literally
using the illusion
of looking-- not the illusion, but just
the fact that you only could see over
the counter, and giving you the
illusion that there's a fully living,
talking shop keeper there,
but it's just half a model.
And other example here is more
to show how Ocarina of Time
used the N64's limited
memory to give you
the sense of being in a very large level
when you might not actually have been.
So if we could dim the lights one
time, we'll go ahead and show this.
SPEAKER 1: So this one was
apparently a hot suggestion,
[CHUCKLES]
which is free camera on Death Mountain,
including our friend, Big Goran.
The smoke halo looks sort of
weird against the black sky,
and here you can see Nintendo fooled us.
It's not full mountain, only the
cliff face is actually rendered,
and that's the path leading
towards the Fire Temple.
And if we zoom out, we can see
the scale of the whole mop.
Bigger than I thought
it would be actually.
The battle music's not quite fitting
for an epic panning shot, though.
COLTON OGDEN: Same idea here,
really, just limited memory space.
So let's load you know as
much as we could possibly ever
see from the perspective
of the camera of Link,
and it's actually very similar
to how, I guess, people
create stages in real life to
make you feel as if you're in a--
when you go to a play, feel
like you're actually in a scene.
But they've clearly cut as many
corners as possible, but it works.
In the game, you can't tell, and
that's very common in game development.
If you're trying to achieve
a particularly grand effect,
it's something to think
about is how can I
make it seem like I'm doing
something, but I'm actually not.
How can I make it seem like I'm a
bird flying through an infinite series
of levels, but I'm actually not.
We have a lot of more of
that to show coming up soon.
So far we have our
background, but we don't
have the title character of our
game, and in this case, fifty bird.
So I'm going to go ahead, and illustrate
how we can get a bird actually
rendering on the screen.
So I'm going to go ahead into my bird2
directory here, that I've created.
And note again, bird2 in your
directory if you've loaded the code,
is going to have the
complete implementation.
But in main, I'm going
to do a couple of things.
So actually, the first
thing I'm going to do,
we're going to-- notice
that I've included--
actually, I haven't
included the class file.
So I'm going to do that right now.
So in bird1-- sorry, I'm going to
take from bird3, the class.lua.
I'm going to go ahead, and put
it into bird2 because we're
going to make a bird class.
Recall, from last week,
a class is just a way
of taking several variables that we
might once have had disparate from one
another, putting them
together in a package,
putting functions associated
with those variables
together so that we can call--
we can sort of think of our game
world more abstractly, and more
compartmentalized, and cleaner.
So I'm going to go ahead-- and now
I have in bird2, the class.lua.
That's just the library we're using
to get classes in Love2D in Lua.
I'm going to go ahead, and I'm
going to create a new file.
This one's called bird.lua.
So remember, the trend is for classes,
capitalize them to differentiate them
from functions and variables.
This one I'm going to go ahead, and just
go ahead and use my cheat sheet here.
My sheets are sticking together.
OK, so this bird class is
actually fairly simple.
Recall that all we have
to do to create a class
is just use the class library,
the capital C with the brackets
there to initialize it.
We're going to go ahead and
define our init function.
So every class has an init function,
which initializes the object
that it's going to refer to later.
In this case, we're going
to need a few things.
So we're going to need
an image for our bird
because we want it to
draw to the screen,
and so what we need to do,
same thing that we did before--
love.graphics.newImage.
I'm going to go ahead,
and hide this really fast.
And then bird.png.
Simple, easy.
We want the width in
the height of our bird.
So I'm going to go
ahead, and set that too.
So every image has a set
of functions associated
with it that Love implements for us.
The image that we get back
from love.graphics.newImage
is itself, sort of a class, which
has a function called getwidth.
So this will allow us to achieve
the with, dynamically, of whatever
class we--
whatever image file we happen to
allocate, and create an object from.
And then we're going to go ahead
and set our x and y because recall,
we have to draw it somewhere.
We want to draw our bird in
the middle of the screen.
So we're going to go ahead,
and just calculate this
based on our virtual width.
So we're going to do
VIRTUAL_WIDTH divided by 2.
So it's halfway in the
middle of the screen,
but since it draws from
the top left corner,
we want to shift it to the left.
So we're going to use
our width that we just--
instant error-- we just
initialized from the image data,
and then we're going to do
a self.width divided by 2.
So we're going to divide the width by
2, shift that to the left on our x-axis.
That's going to put us in
the middle, horizontally.
Vertically, it's the exact same thing,
except reusing height instead of width.
And that's pretty much it,
except for one the last bit here.
We want to be able to render
our bird, pretty important.
So we're going to do love.graphics.draw
our image, and then at self.x, self.y.
And so this is all we
really need just to get
a very simple sprite onto the screen.
Now, it's not going to do anything
because this sort of lives
in a vacuum at the moment.
What we need to do is
in our main file, we're
going to require bird, which is
going to actually put it into our--
allow us to use it in our code.
We're going to create
a local bird variable.
We're just going to call it bird.
We're going to, after that, simply
render to the screen like that,
and if all is done and well, and
if I'm in the right directory--
it did not work.
Make sure you save your work, again.
Oh, I did not require class.
My bad.
So also, we need to do this since
we added that to our directory.
And I did not include
the bird.png as well.
So I'm going to go ahead, and do that.
I'm going to borrow that
from the next directory.
That should be all we need to
do, and attempt to call method.
Render a null value.
Interesting.
Did I not save bird?
I did not save bird.
There we go.
We did it.
So not particularly interesting,
but we're making steps.
Remember to save your work.
As we can see, I do not.
But we're making progress.
We have our entity that we will control.
Visually, we're getting very close, but
a lot of important details are missing.
What should be the
next step, do we think?
AUDIENCE: Let's get him
jumping and falling.
COLTON OGDEN: Exactly, and
we'll do that with the help
of a notion that's common in
platformers, and a lot of games,
actually, but gravity.
How do we think we can simulate gravity
in the context of 2D game development?
AUDIENCE: Just by default,
falling at a constant rate.
COLTON OGDEN: We could
do that, certainly,
and that's effectively
what we will be doing.
We'll be using something
that we used last week, which
was velocity, delta y, and applying
that velocity to our birds y,
frame by frame, and that will
give it the illusion of falling.
Now, falling at a constant rate isn't
accurate to what gravity actually does.
What we want to do, probably, is
some gravity over and over again.
Increment our gravity by
some sort of constant value
so that just like in real life,
things fall faster, and faster,
and then we want to add
that to our y value.
So I'm going to go ahead, and start
implementing that now in bird3.
Wrong repo.
So bird3, we have everything
that we had from before,
except now, I'm going to go ahead, and
in main.lua, in our update function,
this is where we're
actually going to want
to perform the update logic for
making the velocity apply to the bird.
We're going to defer
that to the bird class.
We're going to assume that we have a
method called update in our bird class,
which we're going to implement
shortly, and that's actually
all we need to do in our main class.
And the beauty of having classes
that you can delegate all this work
to, your main file, though it's still
getting quite large-- it's 108 lines--
it's not 200 , 300, 400, thousands
of lines of code because we're able
to break out this code, and
encapsulate it elsewhere.
So I'm going to remember
to save it this time,
and then I'm going to go
into the bird.lua file
in that directory, which
is the same with comments
because I loaded it from the
official repo, the same bird code
that we wrote before.
I'm going to go ahead,
and do a couple of things.
So the first thing that I'm
going to do is define a constant.
So I mentioned gravity before.
Gravity is going to be a constant
value just like it is in real life.
I'm going to define it to 20.
It's just some arbitrary value.
This is a value that
I decided felt right,
but you can tune this however you want.
There's no right or wrong way to do it.
The less the gravity is,
the slower it'll fall,
and the more you'll feel like you're
sort of in outer space, or on the moon,
or whatnot.
We're going to also
go ahead, and define--
recall that we need
some way to keep track
of how our how our bird is falling.
We want a velocity, a y velocity.
This is going to update
our position each frame,
and it's going to make it
feel like we're falling.
So we're going to set our
initial velocity to 0.
The bird's just going
to be in the middle.
It's not going be falling yet.
What we don't want to do
is apply this velocity.
So remember, in our main file we
assumed that we had an update function,
but we haven't actually
implemented it yet.
So we're going to do that right now.
We're going to say birdupdate dt.
We're going to pass it in the same
dt that we use in our main file,
and we're going to go ahead,
and just say our velocity
is equal to our current velocity
plus gravity times delta time.
We're just going to scale
gravity by delta time.
So it will move the
same amount no matter
whether we're running at 10 frames
per second or 60 frames per second.
And then we're going to go ahead--
we have a velocity, but it's not
actually changing our y value.
The y value is what ultimately
moves us on the screen.
So we need to apply our
new delta y to our y.
So we're going to go
ahead, and just do that.
Self.y gets self.y way
plus self dot delta y, dy.
And so if I go back into bird3,
assuming I saved everything,
we should just fall straight
to the screen, which we do.
Not terribly useful, but notice
it's slightly hard to tell, maybe,
but it does move faster, and faster,
frame by frame because that delta
y is increasing as well as our
y, and that delta y is getting
applied to our y, frame by frame.
I'll do it one more time.
It's just funny to look at.
All right, so we have basic gravity.
Super basic computation.
Just keep track of some
gravity constant, delta y,
increase that, and apply that to
your y, and that gives you gravity.
But Flappy Bird can jump.
So we need to find a
way to defy gravity.
So we're going to do
the-- in bird4, we're
going to call this the
anti-gravity update,
and we're going to talk about how
we can actually get that going.
So I found this diagram,
which I felt was pretty apt,
and it also covers a few of the other
concepts we're talking about today.
But see here, this gravity, that's the
constant we had just defined before,
the 20 or whatever,
and this gets applied
at whatever value you want it to be.
This gets applied frame
by frame to your y.
What we want is this.
This vector here is jump velocity.
We want some value to counteract this
gravity that we've been accumulating.
So how do we think we
can go about doing this?
We can set gravity to some, perhaps,
negative value, a high value.
And that will have the
effect of frame by frame,
if we go from some positive value,
which is taking us down on the y-axis,
and we go to a negative
value, frame by frame,
it's going to say-- let's say
that we start at negative 5.
We set it's velocity to
negative 5 it's going
to set y to negative-- it's going
to set it to plus negative 5 pixels
plus negative 4.9 pixels, 4.8 pixels.
It's going to shoot us up pretty
fast in a series of pixels,
but since we're applying gravity
frame by frame, this value
that we set before, 20, it's
going to have the effect--
20 times delta time.
So it gets, effectively, divided by 60.
It's going to counteract this again.
So we're going to shoot up
pretty fast, but gravity
is going to start taking
hold immediately after,
and we're going to start getting
the effect of our bird jumping,
and then falling down to the ground.
A couple of other things
that this diagram shows,
which I thought were
pretty cool, this pipe gap
distance here, something that
we'll be talking about pretty
shortly because this needs to be
defined so that we can offset our pipes.
Pipe separation, it's another
thing we'll be talking about.
And also pipe width, which
is just an intrinsic value
characteristic of the pipe
sprite we'll be using,
but I thought it was very apt.
NYU did a nice article,
if you want to look
at this, about exploring game space.
They computationally determined
what would make a Flappy Bird
level difficult or not, and rated
Flappy Bird levels that were dynamically
generated based on some sort of scale.
So if you're curious,
it's in the slides,
but I thought it was a cool find as
I was putting together this lecture.
So what we need to do is then, simply,
add some negative value to gravity.
Negative sort of anti-gravity.
So we're going to go ahead do that.
So in bird4 of the little
mini repo that I have here,
we're going to go ahead in main, first.
One thing that we want to do
is because another part of this
is taking input from the
user, being able to jump,
we want to be able to detect
whether they've pressed space.
But if we want to detect input for
every single entity that we ever--
in an instance like this,
it's not terribly important,
but let's say we have 20 or 30
different kinds of entities,
and they all have their
own input handling,
we don't want to clog our
main with that, necessarily.
So we can dedicate that--
delegate that, I should say,
to another section of the code.
In this case, we can sort
of put our birds input
handling together with
our bird class, right,
and expound upon the model of the class,
taking control of the code and data
for that particular object in our scene.
So what we're going to
do is in our love.load,
I'm going to go ahead,
and do something here.
I'm going to go ahead and
set love.keyboard.keysPressed
equals a table.
And what I'm doing is just adding
onto a table that Love defined.
It's called love.keyboard.
I'm adding my own value
into it called keysPressed,
and I'm assigning it to an empty table.
So what we're going to do--
this is now part of what Love
gives us as part of its SDK,
but it's something that
we've created ourselves,
and you can do this because
in Lua, basically everything
beyond basic variables are just
tables, and you can manipulate tables
however you want.
In this case, love.keyboard is a table.
I'm just adding a new
key called keysPressed,
and I'm assigning it to
an empty table of my own.
And we're going to see how this
is actually used in just a moment.
So I'm going to go ahead in
our keyPressed function here--
this function is called every time
a user presses a key in the game,
but I'm going to use it.
Because it does that, I can go ahead,
and just do something like this.
Love.keyboard.keysPressed key gets true,
and what that means is in this table
that we've just defined, we've
created ourselves, anytime the user
presses any key, because
love.keyPressed gets called for you,
we can safely rest assured that this
is going to get populated no matter
what key they press
because it's just something
that Love2D takes care for you.
But it's not getting stored until now.
Now we're actually going
to keep track of it
in our own table for reasons that
will become apparent very shortly.
The next part of this code is
defining a custom function.
So the impetus for this is Love
defines a couple of functions.
It defines a function
called love.keyboard.isdown,
which takes in some
key value, and you can
use it to test for continuous input,
which we did in the last lecture.
We were saying hey, if up is
down right now, or down is down,
then we need to update our
y velocity accordingly.
But it doesn't have a mechanism
like this for let's say,
we want in some file
other than main, to check
for if a key was just pressed one time.
It has this function,
love.keyPressed, which takes a key,
and that will trigger it, but we can't
access this outside of this function
because if we define this
function in bird.lua,
it's going to overwrite
this implementation.
And we don't necessarily want to
have to worry about other files
overwriting these functions
because who knows--
if you're on a team,
especially, who knows
who's overwritten love.keyPressed,
and what module in,
and what order does it get loaded in,
and what functions actually valid.
We're going to take care of this
problem by giving ourselves the ability
to test for whether a key has
been pressed on the last frame
by implementing a function that we are
also adding to the keyboard namespace,
the keyboard table
ourselves, called wasPressed.
And it's going to take a
key, and all it's going to do
is check that table
that we created before.
It's going to say if
love.keyboard.keysPressed key, then
return true, else return false.
And you could actually just return
love.keyboard.keysPressed key,
and it will be the exact same thing.
And so what this has
the effect of doing is
saying, OK, because on the
update, which we're about to see--
actually, I should probably do that
before so this all gets tied together.
At the end of love.update, we're
going to do one last thing,
and that's reset that table because
we want to just check frame by frame.
So we have a table, a global
table, that we've created
to check for whether a key is pressed.
We have a callback function that Love2D
gives us that allows us to do that.
So every time a key gets
pressed, we're going
to just add that key to that
table, and set it to true.
Now, we can just simply query that table
anytime we want to with this function
that we've created called
love.keyboard.wasPressed key, which
means on the last frame,
was that key pressed?
Basically return whether
it's true or false.
Now, the only problem is
we're not flushing it.
We're not ever setting that to false.
That's has the effect of if we just
press all the keys on our keyboard,
those will always be true until
we re-initialize the table
to some empty value,
which is what we do here.
On the update, which takes place
after all inputs been detected,
we're going to just set that
table to an empty table again.
And on the next frame, it's going
to-- whatever keys we pressed, those
will get set to true, and then we can
just query that table here as needed,
and any update henceforth.
So does anybody have any questions
as to how this is operating?
And so the ultimate
driving factor for us
as to why we want to
do this, why we want
to put in the work to keep track
of this global input table,
is so that we can actually query input,
single key input based in other files
outside of main.lua
because currently, all
we can do to check for single
key presses is look in main.lua,
but that's not what we want to do.
We're going to go ahead and go to our
bird.lua, and in our update function,
this is where we actually
get to use our efforts,
and say if
love.keyboard.wasPressed space,
which is the key that we want
to actually allow us to jump,
go ahead and set self dy to--
what should we set self.dy
to when we press spacebar?
Should it be a positive
or a negative value?
AUDIENCE: Negative.
COLTON OGDEN: A negative value.
We'll set it to negative
5, and we should probably
define this as an
anti-gravity constant up here.
But just for the sake of speed,
we'll say self.dy gets negative 5.
And I did say that, right?
I did say that.
I'm going to go ahead,
and go into bird4.
Go ahead and run this example.
And look at that, we're jumping.
But we can still fall
through the ground,
and we don't have any real game play.
But we've come a long ways.
Now we've taken single key
input that we otherwise
didn't have the ability
to do in Love2D, and we've
made it possible by just keeping
track of our global input state,
and flushing it every update.
So does anybody have any
questions as to how that works?
OK.
So the other big major visual
component of Flappy Bird
are these pipes that we
see here on the screen.
We have two pipes there, but the
screen is filled with infinite pipes.
So does anybody have any instinct
as to how we can implement this?
Well, we'll see before long, but
suffice to say we'll need a new sprite.
We'll need some sort
of way of keeping track
of when to spawn them because they'd
sort of spawn after a period of time,
and that will be our gap.
And then what will happen if we
just let it spawn forever and ever?
AUDIENCE: You have to
destroy them as they go?
COLTON OGDEN: We do
because if we don't do
that, after a certain period of
time, we're allocating memory
for each of these pipes.
Not a ton of memory, just essentially,
an x, a y, a width, and a height.
But because they all
reference the same--
they will reference the same sprite
image, but given enough time,
eventually you're going to
allocate a certain number of bytes
that will exceed your computer's memory,
or the amount of allocated memory,
and you'll either hang
infinitely, or crash.
And so we want to destroy
them as they go as well.
So we're going to go ahead, and
look at the final live coded example
just because from here on out,
it's going to be a little bit much.
I'm going to go ahead,
and go to main.lua first.
So just get my notes in order.
The first thing that we want to do--
oh, I'm actually in the wrong repo too.
I apologize.
I was in the distro repo.
I want to be in the scratch repo.
So I'm going to go ahead, go into
main, and I'm going to require pipe.
Now, we don't have a pipe yet, but
this is a perfect example of how
we can keep abstracting our game.
We have a bird class, but
we should also probably
have a pipe class because a pipe is
a distinct type of entity in our game
world.
We can model it as a unit,
we can give it functions,
we can give it data, and think about
it in terms of it being a pipe,
not being a set of xy,
width, height, et cetera.
Whatever data you want
to ascribe to it, we
can abstract that out, and think
in more abstract terms, which will
allow us to scale a little bit better.
So we're going to go ahead, and
assume that we have a pipe class.
I'm going to go ahead, and add
it to our folder here right now.
So do a new file,
pipe.lua, and I'm going
to go ahead, and reference to
notes here for just a second.
So the pipe class is
actually quite simple,
just like the bird class was initially.
We don't need to keep
track of a lot of data,
but we do want to keep
track of a few things.
So the bird-- there's only
ever going to be one bird out
at once, but with the pipes, we're
going to be spawning them over,
and over again.
And so if we allocate them--
for each pipe that we instantiate,
if we allocate a new image,
this is probably not
super efficient, right?
We're using the same exact data.
We have a bunch of pipes.
We only really need one sprite.
So outside of the init
function-- so just below where
we're declaring that
pipe is a class, we're
going to go ahead and create
a local variable that is still
scoped to this file, but
there's only ever going
to be one copy of this object.
We're going to go ahead and call it--
say that we have
pipe.png in this folder,
and this is separated
out from the functions
that we're going to be
defining in here, but this
has the effect of creating a
semi global graphics object,
even though it's contained
within this class file.
It's not accessible
outside of this class file
because we don't need it to be.
But it's also not being instantiated
every single time because recall,
if we look at bird.lua
here, we're just setting it
as self.image gets
love.graphics.newImage bird.png.
This will have the effect of
allocating a new image every time
we create a bird object.
But we only ever create one bird object,
so it's not really an important design
consideration for us to say, maybe we
should create a semi global image up
here.
It's not important in this context.
Probably good style to do so anyway
for larger projects, but just
a consideration for here.
Not really something
we need to worry about.
But yes, definitely
try to take an asset,
and reference it rather than allocate
it as many times as possible.
We want our pipes to scroll.
So we need some sort of value.
Just like we did with
the backgrounds, we
need some value that keeps track of
whether these pipes are scrolling,
and it can be a constant value.
We're going to directly call
it negative 60 this time,
and not negate it when we add
it to our position later on.
So PIPE_SCROLL negative 60, we can just
add it directly to our x, or to our--
yeah, in this case,
just to our x, and it
will have the-- times
delta time, of course,
and that will have
the effect of shifting
it left because it's a negative number.
We'll define the init function here.
So pipe init.
Within the init function, we're
going to do a couple of things.
So it's x.
Where should the x be?
What should the x be set to, let's
say, if we want the pipe to spawn
beyond the right edge of the screen?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Virtual
width, and you could also
say virtual width plus some
number if you wanted to.
Because it's set to
0,0, it's going to have
the-- you won't see it on the
frame that it gets instantiated,
but yes, virtual width or virtual
width plus some constant value, or some
value that you've
allocated ahead of time.
We'll just set it to virtual width.
So as soon as the pipe gets
initialized, it will be invisible,
but it's going to be right on
the right edge of the screen.
What about our y value?
First of all, let's take a look at what
the image looks like so we can see.
It's going to be in our--
I don't think I have the
actual image in that directory.
So I'm going to come here.
I'm going to grab the pipe.
This is what the pipe looks like.
Let's see if I can
expand it a little bit.
So it's kind of tall.
Where should we probably
place it if we wanted
it to look similar to Flappy Bird?
Probably towards the
lower end of the screen.
We can get fancy with it
too, and we can even maybe
make it randomized
just like Flappy Bird.
So we'll go ahead, and do that.
I'm going to go ahead, and copy this,
and put it into our scratch folder
here.
Back in the init function, I'm going
to go ahead, and set self.y too.
Because we want to talk
about procedural generation,
this will be sort of our first
foray into how we randomize this.
We'll be using the function
that we used last week,
and this is a ubiquitous function.
You'll see this everywhere in any
framework or game engine you use--
math.random.
We want it to be the
lower half of the screen.
So let's say virtual height divided
by 4 is the upper bound, and maybe
virtual height minus
10 as the upper bound.
So that will have the effect of setting
it to roughly a quarter of the screen.
Sorry, virtual height divided by 4
is towards the top end of the screen,
and then virtual height minus 10
is the lower end of the screen.
So it's actually going to cover anywhere
from the first quarter below that, down
to about 10 pixels from the bottom.
AUDIENCE: Do you have to set the
random seed in this [INAUDIBLE]
or do you do it main?
COLTON OGDEN: I do it in main.
So in this file, I am not sure if
I did it for this demonstration.
It is definitely set in the repo.
I don't think I set it in
this example, but yes, you
would set the random seed here if
you wanted it to run every time.
Sorry.
And the question was should we set
the random seed in the bird file,
or should we set it in main.lua?
Typically, you want to set it at
the top level of your application.
So we're going to set it in--
we're going to go ahead,
and set it in main.
And the function itself is here, and I
think it's starting in bird6, onwards.
So it will be--
did I not set it?
I may not have set the random
seed until later in the repo.
Let's check bird12.
So yes, math.randomseed,
and then seed by os.time,
as we used last week in class.
I'll set it here.
Probably, we'll only run it
once, but it'll have the effect.
Now we can run it several times just
to see the difference in the pipes.
Let's go back to our pipe.lua here,
and we have the x, we have the y.
So those are set accordingly.
We also want to set the width.
Does anybody recall what the function
is to get a width of a graphics object
and the syntax for that?
So we have our image
up here, pipe image.
It's love.graphic.newImage pipe.png.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Exactly.
So we're going to go ahead, and set
this to PIPE_IMAGE colon getWidth,
and that will become our new-- that will
allow us to store our width for when
we will use it later.
And then we need a few other functions.
So the pipe will spawn, but it
won't move because we haven't
applied any sort of scrolling to it.
We have the scrolling
variable up on line five,
but we need to actually
apply it to our pipe.
So we're going to go ahead,
and create an update function.
And then in that update
function, very similar to what
we've seen before already,
PIPE_SCROLL times delta time.
And then lastly, we
want to render our pipe.
So we're going to go ahead, and call a
function that we've seen already today,
love.graphics.draw We're going
to use the pipe image up above,
and then we're going to
go ahead, and use self.x,
and self.y, and that's
all we need for our pipe.
And let me make sure that
that's all we really need.
So in main.lua-- we've got to go back
to main.lua too because we actually
have to start spawning pipes.
So let's go ahead, and go to--
let me pull up my code
here one more time.
In main-- so on line 59--
sorry, you won't see it.
You'll see it in line 59
in the actual distro code,
but for me, it's going
to be slightly different.
We're going to go ahead,
and create a new table
to keep track of all the pipes
that we want to spawn because we
need a way to store them in memory.
We can't just set one variable to--
basically, almost like a dynamic array
in this case, or a linked list rather.
We're going to use this
table just to hold them.
We're not going to give them keys.
We're just going to
insert them like we would
do with just a linked list
like in Python, for example.
We're going to go ahead,
and what do we need
to do if we want to have them spawn
after a certain period of time?
Probably want to have
some sort of timer.
We want to keep track of
how much time has passed,
and maybe have some
sort of amount of time
that's our trigger to spawn of a pipe.
Let's say maybe 2 seconds.
So if we set a timer to 0,
it's just start just at 0,
but we can add to this frame by frame.
We can just increase this timer by delta
time, whatever that is, frame by frame.
It'll be about 1/60 of a second.
So after 60 frames have
passed, we'll get one second.
After 120 frames have passed,
we'll have two seconds.
At that point, we can then decide OK,
now it's time to spawn a new pipe.
Let's go ahead and do that.
So I'm going to go ahead,
and in our update function,
we want to handle the actual
increasing of this timer.
So it's as simple as--
and I make sure that I
called it spawnTimer.
No, I just called it timer.
Let's go ahead, and call it spawnTimer.
Be a little more specific
about what we want here.
So our spawnTimer.
And then we're going to
go ahead in our update,
and set spawnTimer equal to
spawnTimer plus delta time.
And then what we need
to do is then check
is our spawn timer
greater than-- because it
keeps track of time in seconds,
delta time will give you
a fractional amount in seconds.
So it will be at 0.013,
or something like that.
We want to keep track of whether
spawnTimer has gone past two, right?
So if spawnTimer is greater than
2, we want to add a new pipe.
Does anybody remember the function
for how to add to a table in Lua?
So it's table.insert.
So table.insert will take in a table.
So in this case, we want the pipes
table that we allocated before.
And then we're going to
put in a new pipe object.
This is how you instantiate an
object for call, parentheses.
That will have the
effect of now our pipes
table is going to-- every time we call
this, it's going to get a new index.
So it's going to start at 1.
Lua tables are indexed at 1.
The first time it
happens, index1 is going
to be equal to a new pipe
object, which is going to start
its xy at the edge of the screen.
Then index2 will be the exact
same thing, a new pipe that's
at the edge of the screen, and
so on, and so forth every time we
call table.insert.
Once our spawn Timer has exceeded
2, if we want this to not
spawn a pipe every frame here after,
which would quickly clog up our world,
we want to reset our spawn timer to 0.
So this will have the effect of now,
it's going to wait another 2 seconds,
and then this condition
will be true again,
and then we can add a
new pipe to the scene.
Let's go ahead and look at--
we're going to need to add
a new set of logic here.
Actually, I'm going to put all
of this above the bird.update,
and then below that, I'm
going to go ahead, and do--
I'm not sure if we've
covered this already.
I don't think we have, but if
we want to iterate over a table,
there's a function that
Lua gives you called pairs.
It will give you all the
key value pairs of a table
that you can then use while
you're iterating over it.
Similar to enumerate
in Python, if familiar,
except this will actually give you
the keys rather than just the indices.
So we can do for k, pipe in pairs
of pipes do some body of code,
and then we have access to the
key and the pipe within this.
We can just iterate over it, and use it.
So the first thing we want to do
is we want to update our pipe.
So for each pipe, update it.
Give it the delta time
of the current frame.
And then what was the
other important feature?
So this will have the
effect of scrolling it now.
It's going to get its
x shifted, but what
was the other important thing we needed
to do with every pipe in our scene?
AUDIENCE: When x is less than
0, we have to [INAUDIBLE]
COLTON OGDEN: Yes.
That is exactly true.
So what we're going to do
is if pipe.x is less than--
so if we did less than 0,
what do we think would happen?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: We would see it
instantly disappear because they're
based on the top left coordinates.
So what we need to do is
keep track of its width.
So what we'll do is
we'll just say if pipe.x
is less than negative pipe.width,
which will allow the pipe
to go all the way past
the edge of the screen,
we'll call a function called
table.remove, which takes a table,
in this case, pipes,
and then it takes a key.
And the key we have access
to up above on line 124.
We can just say k, and
that will have the effect
of removing that pipe from the scene.
And then as soon as that's
done, we're good to go.
The last thing that we
need to do is currently,
we're not actually drawing
the pipe to the screen.
So down below in our render function,
we're going to go ahead, and up above--
before we do the ground,
because if we do it normally--
if we do it after we render
the ground, the pipes
are going look like they're just
kind of layered on top of the ground.
We want it to look as if they're
sticking out from the ground.
So what we want to do is
have a correct render layer,
a render draw order to the screen.
We draw the background, we draw
the pipes, then we draw the ground,
and this will have the effect of
looking as if the pipes are sticking out
of the ground.
So what we'll do is we'll do the
exact same thing we just did up
above by saying for k, pipe
in pairs of pipes do pipe,
and then the render function
that we defined in pipe.
And this will have the
effect of iterating
through all the pipes in
our scene every draw call,
and drawing them before it draws the
ground, and before it draws the bird,
and that should be all that we
need to illustrate this example.
Let me make sure everything is saved.
I'm going to go ahead,
and go into bird5.
If I did everything correctly, this
should, after a certain period of time,
drop pipes to the screen that are
scrolling, and they're randomized.
Their y value is getting
set to some value
between the top quarter of the screen.
So starting about right where
Flappy Bird is right now, down
about 10 pixels above the width
of the screen, which actually,
that looks like 10 pixels above.
So that's a slight bug.
It should probably be something
along the lines of 30 or 40.
We won't encounter that
in the final distro
because they're not
set to spawn that low,
but you can see how this is sort of
the beginning of our procedural level
generation system, and we have most
all the components of our scene.
Now, normally in Flappy
Bird, we have two pipes.
We have a pipe that's above,
and then a pipe that's below,
and they're in pairs.
In the next example, we're actually
going to start illustrating this.
We're going to have pairs of
pipes that are joined together,
which scroll together.
That once you fly through
them, you score a point.
But for now, we have
all the pieces that we
need in order to have the basic
visual sense of the game completed.
We're going to take like a five minute
break now, and then once we come back,
we'll actually dive into how we can
get pairs of pipes into our scene,
and start getting into scoring, and
some other fun things like music.
All right, welcome back.
So the next part--
so before we establish the
bird, the background, the pipes,
we have all the visual aspects
of our game ready to go.
The next important piece of
the puzzle to really solve
is how can we start scoring
our game, and also how can we
get the pipes matching the way that
they're implemented in the actual game?
Which, recall, they're normally
in pairs, as illustrated here.
And we also see on the right-hand
side, as we've covered already so far,
we have the spawn zone for
our pipes, and on the left,
we have what I've labeled
the dead zone, where
pipes are de-instantiated
once they've gone
past the negative width of themselves.
But pipes come in
pairs, they get shifted,
and once the bird flies
between these gaps,
is ultimately when
they've scored a point.
And so we need a way to pair pipes
together, and define this logic for how
can we tell whether the
bird has gone past the gap,
and whether or not the pipes
have been de-instantiated.
So we're going to go ahead,
and I'm going to probably
stop live coding for the
rest of the demonstrations
because they're going to be
a little bit more complex.
But I believe my code
editor is over here.
I'm going to go ahead, and open up--
oh, this is my other editor.
So in the base repo now, we're going to
go ahead, and look at the full example.
So in bird6, which is
the pipe pair update--
our current subfolder
that we're looking at--
we're going to start in main.
So on line 33 in main.
We can see that we're
acquiring pipe pair, which
is a new class we're defining.
We're taking the pipe
that we had before,
and we're creating a
new composite class.
So we're going to take a class that
encapsulates two pipes together,
a pair of pipes, and we're going to use
this to think about our problem more
abstractly than we already are.
And this layering of abstractions
is a very important concept
in computer science, generally
speaking, but especially
in games where you might have objects
that are composites of objects that
are composites of objects, and
these abstract hierarchies are
sort of what keeps
programmers sane when dealing
with such large levels of-- when
you have thousands of lines of code,
it's sort of the only way you
can really make sense of it.
So on line 65, if we look--
now, instead of a table that's called
pipes, we've renamed it to pipe pairs.
We're no longer going to store
individual pipes in our scene.
We're going to take these
pipe pairs that we're
going to create, and store them in
our table as well as individual units.
On line 71, we need a variable
to keep track of the--
we're calling it last y.
The purpose of this
variable is so that we
can keep track of where the last
set of pipes spawned their gap.
Because if we made our
gaps completely random,
it will have the effect of not
looking continuous for one,
and also potentially
being impossible to beat.
We want some sort of
smooth contour to our gaps
so that we can fly
through them reasonably,
and that it looks as if it was
almost pre-made, and smooth.
So we're going to keep track
of a variable called last y.
We're going to start it off
at negative pipe height.
So up past the top of the screen, plus
some sort of value between 1 and 80
and 20.
It's going to be roughly
towards the top of the screen.
And this is important
because the last y is
going to be-- we're going to
end up flipping our sprite.
A flip on the y-axis has the
result of the sprite looking
as if it's gone its whole height
above where its actual y is,
and we'll see in more
detail, shortly, why
this ends up working the way it does.
We're going to go down to line
132, and in our condition,
if our spawnTimer is greater
than 2, what we're going to do
is this is where we
spawned our pipes before,
but now we're spawning pairs of pipes.
So we're going to set
a local variable y.
It's going to be--
this is the clamp operation
that we talked about last week
using math.max and math.min to
apply some sort of operation.
In this case, we're going to add
a random value between negative 20
and 20 to whatever our
last y value was, which
is going to shift the gap effectively
by negative 20 or 20 pixels.
We're going to clamp it between
negative pipe height plus 10.
So about 10 pixels from
the top of the screen,
and then we're going to set the upper
bound to virtual height minus 90
minus pipe height.
And this minus pipe light is only
because we're doing a flip operation
on our y-axis for our sprite.
I'll go into it in a
little bit more detail
to try to make it clear
as to why we're doing it,
and maybe I'll take out some
codes to illustrate what it looks
like without that operation applied.
But basically, it has the effect
of 90 pixels from the bottom
is where the gap could spawn.
So basically, the pipe
at the very bottom.
Recall that this gap is where--
this value is where the gap
itself begins, not necessarily
where the pipe starts.
It'll be between negative
pipe height plus 10.
Basically, effectively, between 10
pixels from the top of the screen,
between 90 pixels from
the bottom of the screen,
and then we're going to apply a
random permutation of this value.
We're going to add some value
between negative 20 and 20,
and that will give us a contour,
and it'll be a randomized contour.
Line 136, we have pipe pairs.
Table insert into that instead of pipes,
and we're just adding a new pipe pair,
and we're setting it to the value y.
And then the pipe pair
takes in a y value,
and that will be where
the start of the gap is.
And what this will have
the effect of doing
is it's going to flip
a sprite above the gap
so that we have a pipe right above
where the gap starts, and then it's
going to draw another pipe unflipped
about 90 pixels below that,
and that will be how it
puts the two together.
Line 144 is a loop that just updates
our pairs instead of our pipes.
So all we've done here is just renamed
it from pipe to pair, and instead
of pipes, we're using pipe pairs.
We are doing the same exact
thing here on line 153.
We've done for k pair
in pairs of pipe pairs.
And then line 150--
sorry, line 175 is where we are--
sorry, 170 is where we are rendering
each pair instead of each pipe.
And so if we open up pipe
pair here, we can take a look
at this class from scratch.
So it's a new class.
We're going to set our
gap height to 90 pixels,
and so this is just some
arbitrary value that I
felt was a pretty fair
value in terms of size,
but you can tune this
to whatever you want.
You could set this to--
if you want to be really cruel, you
could set it to something like 50.
Or if you wanted to be really
generous to the player,
you could set it to
something like 150, and make
it fairly easy for them to get through.
Or as part of the assignment,
you could randomize it
so that it varies pair
by pair, and you get more
of an organic looking obstacle course.
It's still shifted by
negative 20 to 20 pixels,
but now your gap varies, and you
can also randomize the shift amount
if you wanted to as well.
Let's say you wanted--
maybe you want the gaps
to be up to 40 pixels
difference instead of
20 pixels difference
on negative and positive value.
You could easily do that as well.
On line 18 we're just
setting our x to just like
we did before, virtual width plus 32.
So we're setting it to the--
actually, before we just
set it to virtual width.
Now we're setting it to
virtual width plus 32.
Both are pretty much equal.
This will just give it
a little bit of a delay
before it ends up going onto the
screen, but you can effectively just
do this, virtual width.
On the next line, 24, this is
where we sort of bundled together
the pipes that we're going to end
up actually rendering and updating
to the screen.
Instead of having just one
pipe, a pipe pair is two pipes.
We can easily put this
together in a table.
So we'll just create self.pipes.
We'll set it to a table that
has two keys, upper and lower,
and the upper pipe is just a pipe.
And notice one thing is
different about pipe.
Now, before, it took no arguments.
It was just a regular pipe.
Pipes had their own logic.
They set their own x and y.
They didn't need any sort of
parametrization beyond that.
It was all taken care
of for them randomly.
Now, they take a string.
So this top string means that
this would be a top pipe.
So that means that if
this pipe is a top pipe,
there's probably going to be
logic in pipe that now checks
to see whether it's top or bottom.
If it's top, then we need
to render it upside down.
We need to flip it along the y-axis, and
then we're going to set it to self.y.
And recall that we set self.y--
we passed in self.y in main.
Actually, I'm not sure
if I touched on that.
Let's go back to main here.
So if we go to--
I need to figure out where I
actually instantiate the pipes.
Here on line 136, after
we've calculated where
we want the gap to be
for this pipe pair,
we're going to go ahead, and insert
into pipe pairs, a pipe pair at y.
Y was the calculation between-- we
basically took the last y value,
the last gap that we instantiated, and
then shifted it by some negative 20
to 20 pixels randomly, and made
sure it didn't go above or beyond--
above or below the edges of the screen.
Back in pipe pair, we're going
to go ahead, and look at line 30.
I'm sorry, actually let's take a
look a little bit more closely here
at line 26.
So upper gets top and self.y.
That's where the gap is, and the sprite
is going to be flipped upon that value.
The lower value is going
to be a shift of that.
So the lower sprite needs to spawn
below the top pipe by the gap amount
so that the two are top
to bottom, but there
needs to be that space
between the two of them.
So we need to take that pipe, shift
it down, and then draw the next pipe.
So we're going to take self.y
plus pipe height plus gap height,
and that'll have the effect--
remember, gap height was 90 pixels.
The pipe height is a result
of flipping the y-axis,
and having to shift it
down the actual position.
So if we go back to line 30.
This is an interesting
illustration of what
happens when you edit a table while
you're iterating over a table,
and I'll show you this
in detail shortly.
But basically, on line 30, we're
setting a flag called remove to false.
And what this is going to do is before
we were just destroying the objects.
Whenever it got past the edge of
the screen, we just destroyed it.
But if we're iterating
over a table of values,
let's say a table of pipe pairs, when
you do a removal in most programming
language-- in Lua, when you do a removal
of a table value, and it's not indexed,
or it's non-keyed, which means that
it's indexed by numerical indices,
this will shift every other value down.
And so when you're
iterating it, and you shift
everything down, the value you
are currently manipulating,
let's say it's equal to 1,
if you remove that value,
you shift everything
beyond it down by 1.
But then you're going
to increment up to 2,
and you're skipping over what was
previously just 2, and is now 1.
So you're effectively skipping
over one of your entries,
and it has buggy behavior
in a lot of scenarios.
In this case, it causes the
graphics to glitch a little bit
because it doesn't apply a
pixel shift on one frame,
and so whenever a pipe gets removed--
and I can actually show this visually,
the first pipe left after
that pipe gets removed
ends up moving a little
bit to the right,
and so you get weird pipes shifting
to the left of the bird on each frame.
So whenever you edit
a table in place, make
sure not to delete while
you're iterating over it.
It's going to cause buggy behavior.
And like I said, I'll illustrate
this for you very shortly.
On line 36, we are
performing the update logic.
Now, a pipe pair has two pipes, each
with their own render components,
and their own positions.
We're using the code that
we wrote before for pipe,
and we're going to try to
expand upon it a little bit.
So we still wanted to defer a lot
of that code to the pipe class,
and we want to update the
pipes based on whether--
We want to still keep track of their
own x, and their render functions,
and so we're going to see,
basically, if our pipe pair
x is greater than negative pipe width,
which is the same exact logic that we
were using before.
Set our own x to that
minus pipe speed times
delta time, which is the same
operation we were doing before.
But we are also editing the x of
our self.pipes lower and upper,
and this will allow us to-- on line 46--
render the pipes just
as we were doing before
because they're getting their x values
updated just as they were before.
So we're effectively deferring
the render phase to our pipes,
and not really needing to add any
additional logic for that in our code.
We've made changes to
pipe.lua as well, so I'm
going to go ahead,
and open up pipe here.
And we've set the height and
width of it as contents here.
So pipe height gets
288, and then happens
to be about the size of the screen.
Pipe width gets 70.
On 31, we're sitting
self.orientation gets orientation.
Notice our init function,
which was previously just
empty, it took no parameters, now takes
an orientation, and it takes a y value.
The orientation is going to allow
us to ask, basically, is our code
a top or a bottom pipe?
And if it's top pipe, we need to
flip it, draw it, and shift it.
If it's a bottom pipe, we're
just going to draw it normal,
and not perform any sort of fancy
sprite flipping or anything like that.
Down here in the render function
is where this actually happens.
So on line 39, we're drawing
the pipe image as usual
at x, but at y, because
when you flip a sprite
it ends up completely flipping the y--
it basically performs a mirror on it,
but it not at 0,0.
It basically shifts it
up by pipe height amount.
We need to keep track of that, and
draw it at self.y plus pipe height.
Because if we draw it at just self.y,
because it's going to be mirrored,
and it's going to get shifted
by pipe height amount,
it's going to be beyond
the top edge of the screen.
We need to account for
that, account for the fact
that we're flipping it on the
y-axis, and bring it down.
AUDIENCE: Where's the
code where you flip it?
COLTON OGDEN: The question is
where is the code where we flip it.
So that's actually here on this line.
On this condition, we're saying if
self.orientation is equal to top,
then we want to--
so the parameters here, I'll
comment this just for clarification.
AUDIENCE: So the draw
function has a flip function?
COLTON OGDEN: It does,
I'll show you here.
So this 0, we've added
a few new parameters
to our love.graphics.drawfunction.
Zero is rotation.
We're not going to rotate it at all.
This is the scale on the x-axis.
So x scale, and this is
the scale on the y-axis.
So if we apply a scale
operation of one, it's
the same thing as doing no scale at all.
It's going to draw it on the x-axis.
It's just going draw it normally.
But if it's top, if this pipe has
been set to an orientation of top,
we're going to set the
scale to negative 1.
When you said a sprite--
its scale factor to negative 1, it
flips it along that axis, effectively.
And so that's how you get mirroring.
Most engines that allow you to apply
scale operations to 2D textures or 2D
sprites, a negative operation on an
axis will mirror it on that axis,
and so that's what we're doing here.
So we're mirroring it
if it's a top pipe,
and we're also shifting
its draw location
as well because when we
mirror it, it's going to--
at 0,0 it's going to do
the same-- it's going
to basically draw the same exact
thing, but mirrored on the y-axis.
So it's going to need--
if we want to draw at a
given location, flipped--
still draw it at 0,0,
but have it be flipped,
we need to account for that flip, and
shift it downwards if that makes sense.
So that's essentially all
that's involved there.
And I think that's pretty
much all of the code.
So we have our pipes now
that are being flipped.
If its a top pipe, it's
going to get drawn shifted.
It's going to have its other
pipe shifted down by that amount,
and its y-axis is going to be
increased by the gap height
so that it gets drawn 90
pixels, however many pixels you
want to set below that pipe.
So we're going to go
into demonstrate this.
Go up to fifty bird, the actual repo
now, and the actual distro code.
I'm going to go into bird6,
and I'm going to run it.
And now we have pipes that
are actually rendering.
But we're missing a couple
of important things.
Foremost, among them being that now
we don't have collision detection yet.
So we can just fly through
this course infinitely,
but notice that they're being shifted
by a random value between negative 20
and 20 pixels.
It looks more or less like it's being
generated with some sort of goal
in mind.
It's not haphazard.
It's not all over the
place, but you could easily
find ways to tweak this such
that maybe the gap height is
some value between 60 and 120.
And so you have easy and difficult
pipes, or maybe you have--
I think I'm so far below the screen
that I can't even get back up anymore.
Oh, OK, that's a physics error.
When your value gets to a certain point,
I think that's actually what it's doing
is it's actually overflowing the
value, and setting it to a negative.
Or underflowing it, and
setting it to a negative value,
and then incrementing it
because it's gotten so large.
But you could easily modulate parameters
such as the width between the pipes
as you saw in the diagram
before, or the height,
or even the speed at
which they move, and find
ways to tune it to make game play
that actually works for whatever goal
you have in mind-- making
it easier or more difficult.
And that's actually a topic that
they talked about in that article
that I linked to before,
where they generated levels
programmatically, and then
tested them programmatically
to determine what makes level in
Flappy Bird difficult or easy.
And so basically, those
are the parameters
you need to way as you're
thinking of procedural generation.
And procedural generation,
ultimately, is just
taking values that you
construct your scene with,
and just finding ways to just
manipulate them randomly.
Math.random some value, and that's how
you make random levels in a nutshell.
Making good random levels
is another question.
AUDIENCE: This guy made a lot
of money doing very little.
COLTON OGDEN: He did, he did.
There was a big controversy
around this game back in 2013.
AUDIENCE: He had like a
nervous breakdown, too, right?
COLTON OGDEN: I don't know
if I read too much into that,
but I was doing a little bit of
research, and was reading about some
of that stuff.
I've got to give him
props for banking on that.
Now we have pipe pairs.
That's arguably the most
complex part of the program
because going forward now,
as we get into collision,
and some more concepts,
collision is actually something
that we touched on last week, and
it's all basically the same stuff.
So if we go into bird7, the next
iteration of our application.
I'm going to go ahead,
and open up main.lua,
and then we're going to go to line 74.
And in order to test collision, we
don't have scoring in place yet,
but we need some way to determine
oh, we collided with a pipe.
We need some sort of feedback.
So what we're going to do is I've just
decided we should just pause the game.
So once we collide with a pipe,
let's just pause instantly
so we know immediately, oh,
we collided with a pipe.
So I'm going to set some
variable called scrolling
at the top of the
program in main to true.
We're scrolling.
We're going to start
scrolling, but when I
don't want to scroll any more,
when I want to pause the game,
this should get set to false.
So on line 120, if scrolling,
then do all of this update logic
that we did before.
And then at the very end of that,
we're resetting our input table.
So we can still take
input, but no updates
will take place if
scrolling is set to false.
All of this stuff is within this if--
excuse me, if scrolling, then.
So very simple.
We'll just encapsulate it
all within some variable
that we can turn on and off.
And then on 152, within
that chunk of code
that is being contained
within that if condition,
we're just doing a
very simple iteration.
For each pipe, this should
be for l pair in pairs of--
oh no, sorry.
For every pipe in the pairs of--
it's a nested for loop in this case.
So basically, within the loop that looks
over every single pair, to update it,
we're doing another loop that's looping
through with the pipes in that pair.
So it's only a loop of two iterations
with the upper and the lower pipe.
We could just also say if
bird collides with upper--
basically, if pair.upper or
pair.lower, or pair.pipes
to upper pair.pipes.lower,
but this is a little cleaner.
It's more scalable.
We can add more pipes if we want
to, even though it wouldn't happen.
But for every pipe in pair.pipes,
we have a function here that we
haven't defined yet
called bird:collides.
So if bird collides pipe--
so it takes in a pipe.
So it's going to return a true
or false value, we know that--
set scrolling to false.
So we collide, scrolling
set to false, update logic
is going to get shut off completely.
So we're going to have the
effect of pausing the game.
We're going to go into
bird.lua right now,
and we're going to actually
see how we implement this,
and it's going look very familiar
to what we did last week.
So in bird.lua, this function
here, from 29 down to 45,
it's just an aabb collision
detection test that we did last week.
We're just checking to
make sure any edges are--
right edge, make sure
that is to the left
of the right edge of the second box.
Bottom edge of box one should be--
or bottom edge of box one should
be above bottom edge or top
edge of box two.
If all of these things hold
true, then return true.
Else, return false.
That means we have a collision.
And notice that I've shifted everything
here by a couple of constant values.
Does anybody have any
instinct as to why I'm
saying self.x plus 2 instead of
just self.x self.width minus 4?
Why we're checking for that
offset for the bird in this case
when it is compared with the pipe.
AUDIENCE: It's half
the size of the bird?
COLTON OGDEN: It's not quite half.
It's a few pixels smaller.
Do we know why we want to do this?
We're basically shrinking the box.
Why would we want to shrink the box?
AUDIENCE: There's a gap between
the actual drawing [INAUDIBLE]
COLTON OGDEN: So not quite.
So there isn't an actual
gap between the drawing.
It's more a question of how much
we want to frustrate our users.
If we're pixel perfect colliding with
the pipes, there's no give and take.
It's like you collide, and
even if-- it might even
look as if you're not even
colliding with the pipe,
and you're still getting a collision.
Your users are thinking,
well, that's not fair.
That's really harsh.
We're shrinking our box so that
even if they're just a pixel off,
they'll still get a
little bit of leeway,
and it'll be a little bit less
strict in terms of the collision,
and this is a very common thing in games
when you have characters whose sprites
may not necessarily fill the entire
box that you've allocated for them,
even though you're doing box collision.
Just give your users a couple
of pixels deep, however many
you want, and they can
overlap with whatever
they're colliding with just
a tiny bit before it actually
triggers a true on the collision, and
it makes your game feel more forgiving,
and then also more fun
as a result of that.
So that's why we have--
instead of testing directly on x0
of that box, we're testing x plus 2,
and then self.width minus
4 because when we shift,
we add width to a plus
2 value, we need minus 4
so that we get 2 off the
right edge, and same thing
goes for the height, and the y value.
And so this just performs
aabb collision detection.
Expects a pipe, which means
that we need to ensure
that the pipe has an x and a y, a
width and a height, which it does.
Actually, just a constant here.
We're just checking pipe
width and pipe height.
We probably shouldn't do that.
It should be pipe.width,
pipe.height in that case,
because then this couldn't
necessarily just be a pipe.
It could be anything in our scene
that has a xy, a width, and a height.
It could be a general purpose collision.
And actually, something you
could also do if you wanted to is
just write a function called
collides that takes in two things
that you know have
bounding boxes, and will
allow you to perform collision
detection on anything
in your scene between any two entities.
That would be a more
scalable way, I guess,
of dealing with it, rather
than necessarily having it
specifically defined as birds
and pipes being the colliders.
But in this case, this is
the only thing we're really
colliding with, except for the ground.
But when you collide with the
ground, all you need to do
is just check to see whether
your y position plus your height
has gone below the edge of the screen.
So any questions as to how that?
AUDIENCE: Why did you add 2n and
subtract 4 instead of just subtract 2?
COLTON OGDEN: So the question
was why did we add 2 and subtract
4 instead of just subtract 2?
Because when you add a--
because we're doing self.x
plus 2, basically we're
shifting the whole box,
essentially, here in this part.
So self.x plus 2 brings
the beginning of the box
that we're colliding with
2 pixels to the right.
But if we just do 2 pixels minus
2, then the box's right edge
is still the right edge of the box.
We want it to be shifted
inwards by 2 pixels.
Because we've shifted at the start of
our box, the x position, 2 pixels over,
we need to shift it 4
pixels inwards because that
will have the effect of our box
being 2 pixels into the right edge.
Does that make sense?
OK, so I think that's
everything for bird7.
We're going to go ahead,
and run bird7 now.
And recall, if we hit a pipe,
we should instantly pause.
So bouncing, bouncing, bouncing.
I'm going to go through
one pair of pipes here,
and then I'm going to
hit this one on purpose.
Oh, we paused.
And notice that we had
a little bit of leeway.
We got a couple of pixels there
just to give us-- in case we
accidentally-- and also, it takes
into consideration you could move,
because of your velocity,
a couple of pixels
beyond necessarily the strict hard
edge of what you're colliding with
based on how many frames of passed.
Basically, essentially what your
velocity is, and what your position is.
In this case, I think it
looks like we're actually
three or four pixels above the
edge because our velocity was
so high because we jumped, but as
soon as it detected the collision, as
soon as we were on that frame
where our position was such
that we did trigger true
for our collision detection,
it paused the game.
Looping was set to false.
We no longer ran any update
logic, and this is our basic way
of getting feedback about that.
However, it's not particularly
compelling, gameplay wise,
and so we want to get into scoring.
Before we get to scoring though,
and also associated with that,
different states of our game.
So if we get into
scoring, clearly we want
to have a screen that tells us when
we lost, and what our score was.
We should also probably
have a title screen
because we're just jumping
right into the gameplay.
We want a screen that lets
us play through the game,
and as we'll see in a little
bit, a screen that also gives us
some time, once we start
the game, to count down.
Sort of say, oh, three, two,
one, go, rather than just oh,
go, and oh, I don't know what I'm doing.
I'm bewildered.
So this is a diagram that
sort of models the state
flow that we're going to be using
in our program here, our game.
We're going to assume that we start
on some sort of title screen state.
So going left to right.
A title screen state will
transition to the count down state,
and then we can define, however
we want, those transitions to be.
In this case, let's
just say we press Enter.
The title screen state
goes to countdown state.
Once countdown state has--
once the transition has triggered for
that, we should go to the play state.
And then once the transition
triggers for play state,
we're going to go down
to the score state.
And then score state should go back
into countdown state, and this models
our entire application's flow--
top to bottom, left to
right, chronologically.
So let's go ahead, and
take a look at some code
as to how we're going
to accomplish this.
Last week, I alluded to taking us--
and actually earlier in lecture, us
going from this string based approach,
to keeping track of our state with if
conditions, to a class
based approach, and that's
what we're going to illustrate today.
So I'm going to go
ahead, and open up bird8.
And in bird8, I'm going to go
ahead, and start with main.
So in main, on line 36, we're acquiring
a new class called state machine,
and a few other classes that we're
defining called bay state, play
state, and title screen state.
And these are the components
of our state machine,
and they've now, instead of being just
blocks of code in our update function,
they're separate blocks,
separate modules that
have their own logic, their
own update and render logic,
and we'll see that very shortly.
On line 78, if we go down here--
separate from that, I'm also
instantiating a bunch of fonts.
We did this last week.
So love.graphics.newfont takes
in a font file and then a size.
I've created a few different fonts here
because we have a few different ways
of giving feedback to the user.
We want a small font for
displaying press Enter
to a start, or something like that.
We want a medium font to display
the name of the game, perhaps.
Or, I think actually, Flappy
Font is responsible for that.
Medium font, I think, was for score.
Huge font for our countdown.
We want a big font right
in the middle of screen
that says three, two,
one, and then we start.
And then we're just going to start off
by setting it to Flappy Font, which
is going to be our title font.
So nothing really new, but the
beginning of our UI, so to speak.
On line 92, this is
new, and actually this
is a demonstration of a
type of naming convention
you'll see often in game code bases.
We haven't used it yet, but we
will start using it in the future.
We prefix a global variable
with a lower case g.
This lets you know when you're
digging through a bunch of files
that oh, this is a global variable.
OK, so I should probably know it's
probably not defined in this module.
Maybe it is, but I know it's global.
Other things you might
see are lower case
m for member, which means that this is a
member function, or a field of a class,
and you can instantly
see at a glance, and know
OK, if I want to find
the definition for this,
it looks like it's a member function.
So it's probably in this
class, here at some line.
You can easily find it.
And so in future
lectures, we'll be using
more of this lowercase g for global
variables that we use module to module.
In this case, we're
instantiating a StateMachine.
So we're using the class that we
will take a look at in a second.
The StateMachine takes
in a table with keys
that map to functions that
will return our states.
So we can just call change some value,
and it'll have in our state machine,
it will basically reference
that key in this table here,
and it'll call that
function based on-- it'll
basically set the current state of
that StateMachine to whatever state
gets returned by the
function at that key.
So in this case, change is going to
trigger return new TitleScreenState,
and we're going to get--
the StateMachine is
going to be set to the
title screen, effectively,
and we'll take a look at what the
title screen looks like momentarily.
On line 96, yeah, we're
changing to title screen.
On line 134, notice that we
don't really have much update
logic in this application anymore.
We're still updating the
scrolls because this is
behavior we want across all our states.
No matter what state we're
in, we want to make sure
that our background and our ground
scroll so that we have movement.
We don't need to duplicate
this behavior state to state.
This is a global feature of our game.
So we're just keeping track of
it here just as we would before,
but anything else in our
game that need to be updated
can now be deferred to
our StateMachine class.
And when we call gStateMachine
update delta time,
it's going to look and see
what's our current state,
and it's going to update that state.
And that's going to basically
be that chunk, that if chunk
do this logic that we were
doing from before last week when
we had a more primitive StateMachine.
Line 46, same exact thing.
Between the background and the ground,
because those will always render scene
to scene, we want to render
our current active state
using our StateMachine render function.
And so let's go ahead, and just look
briefly at our state machine library.
It's a very simple code.
It's actually taken from the book I
alluded to earlier in the lecture--
How to Make an RPG.
They give you this state machine,
which really cleanly, I think,
handles state transition.
Basically, it takes an init,
and then a series of states.
It has an empty class, or empty table.
So all of these are just empty.
If there is no-- this
is a thing you can do
in Lua, which just lets you
initialize a variable if it's not
given a value in your function.
So self.states gets
states or some value.
Which means that if states is equal to
a false value, it's equal to nothing,
just set it to this empty table.
So it's just a shorthand for instead
of saying if states equals nothing,
then set states to empty table.
Self.current is just an
empty class or empty state.
So this is basically what a state
is, it's just a set of methods--
a render, update, enter,
and exit function.
That's a state, and then you
define all of the behavior
in each of these functions, and that
compiles your state more or less.
Our change function takes in a name,
and then also some optional parameters
that we can use to enter that state.
When we change the state, or call
the exit function of whatever state
we're in.
So exit that state.
Maybe your function needs you
to de-allocate some memory.
Set the current equal
to taking that name,
and then call whatever functions there.
So it's going to return.
In that case, we saw earlier, it's going
to return a new title screen state.
So that's going to be what current is.
With self.current, we're going
to then enter that state machine.
So we're going to call the Enter
function that we defined there
with whatever enter parameters we
pass into change, which are optional.
And then here, StateMachineUpdate just
updates whatever the current state is,
and render updates whatever
the current state is as well.
And so I'm going to start
going a little bit quickly
just because we're
running short on time.
BaseState, all it does is
just implements empty methods
so that you can just
inherit this state, and you
can choose which methods
you want to define
without throwing any errors because it
blindly will call all these functions,
not checking to see whether
they're actually implemented.
And so this is a way for
you to just quickly avoid
a lot of boilerplate code, essentially.
The TitleScreenState here, this is
your way of with the class library,
just including everything
that belongs to BaseState.
So inheriting, if you're
familiar with other languages
that use inheritance--
take an object, copy
everything from that object or
that class, put it into this one,
and then add new stuff to it.
That's basically what inheritance is.
We're inheriting from BaseState so it
has all the functions BaseState has,
and then on top of that, we're
defining an update function.
So if we press Enter or Return,
change the global state machine
to the play state.
And then for the render, we're
just going to render fifty bird,
and press Enter halfway in
the middle of the screen.
And then the PlayState,
essentially to some--
basically, what the PlayState is is all
of the code that we ran before, only
now we're just putting it
in the update function here,
and the render function here, and
making bird, pipe pairs, timer,
and lastY member fields
of this state object.
So we'll go ahead, and
run this really fast.
This is our title screen state.
So at the very beginning, we
change to title screen state.
All it does is render, and
then the scrolling behavior
is throughout all classes, all states.
So we'll see that no matter what.
Once you press Enter,
it'll trigger change
to play, which will return
a play state, and then
now we're back where we were before,
and we're seeing the difference now
in having a couple of different states.
So quickly, I'll go
through the score update.
So this is a little bit more
complicated than the last example.
But to summarize, in bird--
sorry, we're in bird9.
So in bird9, if we go here,
we're going to go to main.
So notice that in main, down
where we define our StateMachine,
we're going to go ahead, and also
note that we require a new score
state because now we want
to display a score screen.
Down on line 96, score gets a function
where we return a score state object.
So now we can change to score,
and it will return that state,
and we can define all the
behavior within ScoreState
that we need to display a score.
In PipePair, we have a new
variable called self.scored.
Set it to true or false.
We're going to set it
to true if the bird has
gone past the right edge
of the pair of pipes.
That will have the effect of us
scoring a point, effectively,
because all we need to do is
just make sure the birds got
past that pair of pipes
because otherwise it
will have collided with it.
If it does go past it, set it to true,
and then add a point to our score.
And in our play state, we can
see that we've added a point.
So if we go to our play
state, 26 is where we actually
keep track of our score.
Self.score gets 0 in our play state.
We're going to go ahead,
and go down to line 56.
So for every pair, if
it's not been scored yet--
because we don't need to calculate
this if it's already been scored.
We should ignore it in terms of
scoring once it's been scored.
If the x plus width is
less than our bird.x,
meaning our bird is beyond the
right edge of the pair of pipes,
increment our score, and
set that pair to true.
We will then thereafter, because
of this condition, ignore it,
and we're also going
to increment our score.
So it's going to be kept track of.
On 83, notice that if we're
colliding with a pipe,
we should transition
to our score state now.
And we're also passing in score
gets self.score as a table
because remember, we can pass in
parameters when we call change,
and this will be passed into
our enter function in our state,
and then score is going
to equal self.score.
We'll have access to the
score within that score state.
We don't have to keep track
of it as a global variable
to see it in both locations.
And on 93, the same exact thing.
This is collision to check whether we've
collided with the bottom of the screen.
If our y is greater than virtual height
minus 15, do the exact same thing.
Transition to the score state,
and pass it in our current score.
So another death condition.
And then 104, we're just
going to set Flappy Font,
and then we're going to render our score
at the top left of the screen at 8,8,
and that will have that effect.
And so lastly, here, our
scores state is pretty simple.
All it is is we're going to get--
from those parameters we passed in
by a change, self.score
equals params.score.
We're going to, when we
press Enter, go back to play,
and then we're going to render
'you lost', and the score,
which we have access to-- self.score,
and then press Enter to play again,
changing fonts along the way.
And so if we go back to
bird9, and we run this,
notice that now we have
a score in the top left.
And I'm going to get
one point, and then die.
Then we go to our score screen now.
Remember, we passed score
into it from our play state.
We passed it in as
parameters, and then we
can press Enter again, go back to play
state, and when we fall to the ground,
we do it as well.
So we've just taken a look at
how to add scoring to our game,
but what if we want to
add a count down screen?
Maybe we want the users to be prompted
three, two, one before the actual game
starts throwing pipes at them.
Give them the time to
sort of get acclimated.
We're going to go ahead,
and take a look at how
we might do this using another state
very similar to the last example.
We're going to add a new
state called CountdownState,
which is shown here on line 38.
We're also going to, down
in our state machine,
add a new key, which returns one of the
new countdown states, just as before,
and then we're going to go ahead, and
take a look at our actual countdown
state here.
So in our CountdownState.lua, which is
in our states folder, as the others,
it inherits from BaseState.
We have initialized a
countdown time to 0.75.
This time in seconds.
One second is a little long,
so I made it 0.75 seconds.
We're going to initialize a
count to three, and a timer to 0.
The count's going to start.
It's going to use a timer once
the countdown time has elapsed,
right here, as this logic shows.
Increase the timer once the timer
has gone past countdown time.
We want to go ahead, and set it to--
we're going to modulo by countdown time.
So loop it back to 0 plus whatever
amount beyond the countdown time
we went so that we have
a smooth track of time.
We're going to set
self.count minus itself
by 1 so that we go three, two, one.
And then if our count is
0, which means that we've
gone all the way down
in our count, we're
going to go ahead and use our state
machine, and change to the play state.
And here, we're setting our
font to a font that we've set--
hugeFont-- and then
we're just twostring,
a little function that takes
a string, or takes a number,
converts to a string.
We're displaying self.count
at 0, 120, and then our--
it's printf.
So we're basically starting at 0,
y 120, virtual width alignment,
and then we're centering it.
So the one last piece of
that that we need to change
is in our title screen state, instead
of going straight to a play state
here on line 15, we're
going to a countdown state.
And what this has the effect
of doing, if we go into bird10,
is when we press Enter, notice
that we're going three, two, one,
then going into our play state.
Not just going straight
into the play state
as before, giving our user a little
bit of time to catch their breath.
And then if we die, we go to our
score state, but once we press Enter,
notice we're doing that as well.
So in our score state, we also are
changing to the countdown state.
So that was how to
make a countdown state.
Probably my favorite part of many of
these examples, and of this example
as well, is adding audio
to our application.
Music and sound effects, which
really ties everything together.
So we're going to go ahead
and take a look at this.
It's very simple.
Very similar to what we learned last
week, even when we just did Pong.
So in main.lua of bird11, which is
what we're going to look at now,
we're going to take a
look at a table of sounds
that we've initialized on line 88.
We've given them all keys.
Jump, explosion, hurt, score.
These are all sound effects that
I've generated with the BFX program
that we used last week, if you recall.
And then a music track
that I found online
on FreeSound, which is free to use.
The link is here, if curious.
It's just a nice, happy soundtrack
that I found for this game.
On line 99 to 100, we're going
to do one additional step
before we start the music.
We're going to set
looping on that to true
because in games that
are infinite like this,
we don't want our music to just
go, and then stop abruptly.
We want to have it loop.
Set looping to true.
I actually begin the play of that
music outside of any of our states
because it's going to be a global
music track, and then that's the music.
We also need sound effects.
If we look in our bird
file here line 45, which
is where we have the
logic for jumping, we're
also playing the jump sound
effect that we've generated.
Additionally, in our play
state, if we take a look there,
we can go ahead and see
in our states folder here.
Go to play state, and
take a look at line 58.
This is where we score a point.
So we should play our score
sound effects here, simply put.
And then the same
thing on line 80 to 81,
collide, the sound effect
here, which is we're actually
layering two sounds on top of each
other, which is a common thing
to do in sound design, and game design.
One sound, often, isn't all you need
to accomplish a particular effect.
So I have an explosion sound, which
is kind of a white noise effect,
and then a hurt sound effect, which
is sort of like a downward sine wave
type of sound.
It would be the exact
same here on 95 to 96.
Once we put all these pieces
together, we're going to run bird11.
[MUSIC PLAYING]
We get music.
[BEEPING]
We get a jump sound effect.
And when we score a point,
[DING]
we get another sound effect.
[CRASH]
And then if we hit a pipe,
notice that we have a sort of
[MIMICS NOISE]
and a white noise or an explosion
effect layered together.
So that sort of brings everything
together, creatively and artistically.
As an exercise to the
viewer, in bird12--
in the GitHub repo,
we have some code that
allows you to actually add
mouse clicks to the Flappy Bird
in order to make it a little bit
more like the actual game, which
was an iOS game.
So it relied on taps.
The function that you might want to
use is love.mousepressed x, y, button,
and I would encourage you to think
about how we took input, and made
it global in the context of a keyboard
in one of our earlier examples
so that we can call this was the mouse
just pressed in our bird.lua file,
as opposed to the main file.
And so next time, we're going to
be covering a few new concepts.
Or sprite sheets.
So taking a large file of images,
and taking out chunks of that
so we don't have to have
a million graphic files.
Procedural layouts.
This will be in the context
of the game Breakout.
So we want to lay out all the
bricks in our game, procedurally,
in sort of the same way
that we've procedurally
created a pipe level in this game.
We'll be talking about
separate levels, and having
them stored in memory as opposed
to just one continuous level.
We'll be talking about health.
We'll be talking about particle
systems, which is spawning little mini
graphics to accomplish various effects
that are otherwise difficult to capture
in simple sprite animation.
A little bit fancier collision
detection based on input
so that we can drive ball
behavior the way we want to,
and then also persistent save data.
How can we take a high
score, and not have
it refresh to 0 every time we run the
application, but rather save it to disk
so that every time you run the
program thereafter, we can see
what we've gotten scored in days past.
The first assignment, or other the
second assignment, assignment one,
is going to be a little bit more
complicated than last weeks,
but still fairly doable.
Make pipe gaps slightly random,
being the first component of this.
So before, a pipe gap was
set to a constant value.
Maybe make it some sort of random value.
Pipe intervals as well.
So we're spawning every two seconds.
Maybe we want to change
that up, make pipes
spawn a little differently,
a little more sporadically.
The more complicated
aspect of this assignment
is going to be awarding players a
medal based on their performance.
So have maybe a bronze, a
silver, and a gold medal--
an image that you display in the score
screen in addition to just their score
just to give them a little
bit of personal feedback,
and make them feel rewarded
for their effort, and make them
strive to get that last medal.
And then lastly, you'll implement a
pause feature, which we talked about
in class, so that when you press, for
example, the key p, the game will stop.
But unlike that example,
when we press p again,
the game should resume just
as it was in its prior state.
So that will be it for Flappy Bird.
I'll see you guys next time.
Thanks a lot.
