Professional Documents
Culture Documents
Whoops, where am I?
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.
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.
Visually, we're getting very close, but a lot of important details are missing.
What should be the next step, do we think?
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.
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.
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.
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.
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.
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.
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.
[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.