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.