[MUSIC PLAYING]
COLTON OGDEN: Hello, everybody.
Welcome to GD50 Lecture 2.
This is Breakout.
And interestingly, CS50 has a history with Breakout,
so I pulled this up today.
This is Pset3 in 2015, 2014.
It was an implementation of Breakout using the Stanford Portable
Library, which was a sort of Java library
that we were able to get C bindings for.
And so students were able to actually implement
a game what was at the time the CS50 appliance, which is a Linux distro.
But suffice to say that was--
oh, a funny story also.
I happened to also write the lasers for this implementation back in the day.
And I think that was one of the first bits of code
I got my hands dirty with when working with CS50.
So today in the context of Breakout, we'll
be talking about a few different things that we haven't talked about yet.
Sprite sheets being chief among them most likely.
At least the most visibly so.
So sprite sheets are simply a way of taking an image, a large image,
and rather than splitting it, rather than loading individual images
for all of your different things in the game,
whether it's your aliens or your paddles or whatnot, you can put everything
into one sheet and then just sort of index into that sheet using rectangles,
quads.
We'll talk about soon.
Which will allow you to just draw a subset of that image,
and therefore condense all of your artwork just into one piece, one file.
We'll be talking a little bit more about procedural generation
in the context of Breakout.
And in this case, we'll be laying out all the bricks in the game world
procedurally.
So having instead of the same set of colors, in this case,
the standard layout is to have a bunch of the same colored bricks row by row.
We'll actually implement a dynamic generation approach
and have a bunch of different cool layouts we'll see.
And it's actually quite simple to achieve pretty believable results.
We'll manage state a little bit better in this game.
So before we sort of had a couple of global variables
and we didn't really have the concept of a per state or a global state
that we were cleanly sort of sharing between all of our states
for our state machine.
But to avoid having sort of like a polluted global name space
and to just sort of keep things a little bit cleaner,
we'll end up taking all of the important variables for our code like, you know,
the player and any other entities.
The bricks, the ball.
And rather than keep them in our main [INAUDIBLE],,
we'll end up shifting them.
We'll sort of transfer them to and from the different states
via the state machine's enter method.
We'll actually have levels.
So a progression system.
So start at level one, go up.
And then with each level, we'll implement a scale
in terms of the generation of the bricks.
So we'll get higher tiered bricks and more points as a result.
We'll have a health system.
So hearts, in a similar fashion to Legend of Zelda.
Particle systems, which are a very important aesthetic component
to 2D games and 3D games.
Particle systems basically being a bunch of spawned images
that you sort of cluster, you put into a little spawner,
emit them in a certain way, and color them, perform math on them,
and get sort of believable effects like fire and smoke
and all these other things that would otherwise
be not easy to do using simple animation,
but trivial with a particle system.
We'll do a little bit more complicated collision detection with our paddle
and with our bricks than we did with Pong.
And then we'll also talk lastly about how
we can save data locally to our computer so that when we close the application
and run it again, we end up having a persistent high score rather
than just something that's volatile.
So first though, I would like to demo today's finished game.
So if anybody would like to demo from the audience, that would be nice.
Go ahead and come up.
I'll go ahead and cue it up for you.
What's your name?
JEREMY: Jeremy.
COLTON OGDEN: Jeremy.
Colton.
JEREMY: Nice to meet you, Colton.
COLTON OGDEN: Nice to meet you.
So we're going to go ahead and run Breakout here.
And so it uses the arrow keys.
So if you go ahead and press up and down,
you'll see you can move between the start and the high score screen.
So they're two separate screens.
So go ahead and--
here we have when you start, you can choose a paddle.
So rather than just the same old paddle every time, you get to select.
And as you can see here, he chose green.
So he gets the green paddle.
These bricks all procedure generated.
So if he runs the application, they'll be completely different.
And as is the classic formula, the ball moves
between the bricks and the paddle.
When it hits a brick, if it's of a certain color,
it'll either get destroyed-- in this case, if it's blue,
it's the base color brick.
So it's the lowest value.
And if it's higher than blue, it'll end up
going down a color depending on which color it is.
I believe it goes blue, green, red, purple, yellow.
So anything higher will get shifted down.
And then the player amasses points, as you can see top right.
Score.
And notice also the three hearts.
That will be the player's health.
So if he were to lose on purpose possibly,
we can see he gets another message that's saying press Enter to serve.
His hearts have gone down by one.
So now he's got two out of three health.
And so eventually if he were to by chance lose completely--
oh.
That's honestly the most fun part about Breakout
is just getting it caught in a bunch of stuff.
But you can see we go to a Game Over screen.
It shows your final score.
And then you can press Enter and it will--
oh.
I must have had a bug.
But that should take you back to the--
if in the event that you have a high score,
it'll take you to enter a high score.
And if you don't have a high score, it'll take you back to the Start menu.
So I made a couple of last minute changes.
Unfortunately I must have left something in there.
But that's Breakout in a nutshell.
Our goal today will be to implement basically all the functionality we saw.
Oh, we didn't take a look at the high score screen.
So let's take a look at that really quick as well.
So here at the title, you can see we have Start and High Scores.
Oh, man.
OK.
I must have screwed something up.
So I'm going want to go [INAUDIBLE] Breakout 12.
OK.
Sorry.
I apologize.
I'm going to fix that.
But it should show this menu here where you will have a list of all your names
that get loaded from a file and will output your score accordingly.
And in the event that you get a new high score,
you'll get to enter your name after that,
and then it will end up saving it to another file.
And when we get to that point I'll try and fix it
so that we can actually see what it looks like.
So let's go back to these slides here.
So this is the overall state, flow of our game.
So as you can see by me marking it out in a highlighted color,
we start off in the StartState.
And this is all stuff we've covered before.
Just the state machine.
It's a little bit more complicated than Flappy Bird.
We have eight states as opposed to I think it
was four or five in the last lecture.
And the arrows illustrate which states can move in between other states.
So as we saw, the StartState can move via the up and down arrows
in the HighScoreState.
It can move between the HighScoreState and back.
So when you go into the HighScoreState, press Escape,
go back to the StartState.
The StartState also has an arrow branching off
to the left going down to the PaddleSelectState
where we saw the user is able to select a paddle to use.
Once they've selected a paddle, we'll go to the ServeState.
They'll be able to serve the ball at their leisure.
And then it will go back and forth between the PlayState.
So if they end up taking damage, the ball
goes below the surface of the screen, they'll go back to the ServeState
again so they can reorient themselves.
If they're in the PlayState and they end up scoring,
clearing the whole entire set of bricks, they'll
actually get taken to the VictoryState.
And the VictoryState is where we increment the level
and we also regenerate the level.
And the VictoryState goes back to the ServeState,
and then we repeat that whole loop again.
In the PlayState if they are to get a Game Over,
they'll go to the GameOverState, it'll tell them their score,
and then they'll go to the EnterHighScoreState
depending on whether they have a high score.
And if not, as seen by the arrow that goes up and to the left,
they'll actually go back to the StartState.
And then the EnterHighScoreState will also go back to the HighScoreState
so that they can see once they've entered
their high score, their score relative to the other scores in the list.
So in Breakout0, which we're going to look at now,
we're going to do some very basic stuff.
So this is the Day 0 update as always.
I'm in Breakout0 right now.
Yes, I am.
So what we're going to do is we're going to look at first thing here, line 27.
So before what we were doing in our application
is having basically a lot of files at the top level and sort of losing track
of what we were doing potentially.
Especially as you start adding more and more files
and you've got like 50, 100 more files, that's
something that's obviously not maintainable.
So the solution there, just put them in folders
and then keep track of everything.
Keep them organized.
And that's a major thing that we're going to start doing.
And on top of that, we're also going to, in our code,
keep things a little bit more modular.
And that's why we have this file source slash dependencies,
which we'll take a look at in a second.
We've allocated a bunch of global tables here.
So we're taking the design decision of even
though I mentioned that we will be sort of taking a lot of the global variables
out of our application assets, we're going to keep
all of those in some global variables.
And we'll see in the future how we can maybe implement a resource manager
class that takes care of this for us.
But for now, for simplicity's sake, in love.load,
we're just going to have a few global tables that contain,
in this case, global fonts.
So by key, we can index small, medium, and large fonts,
which are just new fonts at different sizes.
8, 16, 32.
And we're using it.
We have a fonts folder now instead of just keeping it at the parent level.
We're going to set it to small.
We have global textures.
So background, main, arrows, hearts, particle.
So we have the background, which was the background of our screen.
Main has all of our bricks, paddles, the balls, et cetera.
Arrows are going to be for the paddle select screen.
The two left and right arrows.
Hearts are going to be for our health.
And then particle is a single, small, tiny little texture
that we'll use to spawn all the particles in our particle systems
later on as we get towards the end of the demonstration.
So this is push.
We're setting it up just like normal.
Nothing new there.
Except the virtual width, virtual height, and all that stuff,
those have been moved out, if we look into source in a constants file.
So this file here, instead of having all the constants in main,
it kind of makes sense just to take them out, put them
in a file called constants.lua, and we can sort manage all that.
We can know immediately when we're looking at capital window
width, window height, et cetera.
And these are all constants.
If you have a constants file, we just can more easily
track it rather than having to grab through all of our files
to try and figure out what we were looking at.
And the constants are used here in our set up screen as before.
And then another sounds global table, just as before.
We have a bunch of different sound effects.
I've separated the music from the sound effects
just so that we can see at a glance, oh, this is the music,
these are the sound effects.
Pretty straight forward.
We have a state machine, as always.
And we're just going to use a StartState for this demonstration.
Setting it to Start.
Love.resize, love.update.
These are all functions we've seen before.
Nothing too new.
Love.keypress.
We have a global input table.
So as in the case of Flappy Bird, we can index into that input table anywhere
in our application and call love.keyboard.wasPressed[key],
which allows us to take input exclusively from main and use it
in other modules.
Here we're drawing the--
so this is the actual rendering code.
And we're doing this in our love.draw as opposed to a specific state
because this is actually going to apply to all states.
We're always going to have this background.
So rather than duplicate it over and over again,
in this instance, this minor bit of code,
we're going to display the background behind all the states.
So all the states are going to render over this background
and make it seem a little more cohesive.
We're going to draw at 0, 0 without rotation.
And then this bit of math here, the virtual width
divided by, and then background width minus one, end up being a scale factor
so that we can always scale it to be our virtual width.
Because the texture by default is some amount smaller than our actual window
or our actual virtual width and height, but by dividing virtual width
by whatever the background width of that image is by one,
we'll get a scale factor because virtual width is larger than the image.
We'll get a scale factor on X and Y that equates to it completely stretching
to fill our virtual width and height.
And recall that these two parameters are the scale on the X and the Y.
So it's going to be some, like, one point something or two point something.
Whatever it takes to end up filling the screen.
And then lastly here, the new bit I implemented
is just a display of frames per second function, which I think
is kind of important generally, and it's very easy to do.
I don't recall, I don't think we talked about it yet, but just
love.timer.getFPS.
And then I just draw in the top left in green
so that we can see it throughout all of our iterations of the game, what
are frames per second are.
If you want to monitor without having to look through your terminal or anything
like that, just displaying at the top, it's
standard practice in a lot of games.
If you've gone to the debug console or whatnot
or sort of looked into some of the hacks,
you'll see that in a lot of places.
So I talked earlier about dependencies.lua.
So this ties in as well to our effort to sort of modularize everything,
keep everything organized.
Instead of requiring everything at the top of main,
let's just put it all in a file and then we'll
know at a glance what we're requiring and we don't have to look through main
and make main 100 lines, potentially a lot more than it needs to be.
So requiring push, requiring class.
Same as we've done before.
Require source.constants.
We have access to those.
Require StateMachine, and then BaseState and StartState.
So let's go ahead and take a look at our StartState.
So I put states in a subfolder of source.
This is another effort to sort of keep things modular.
In this particular project, we won't have a lot of nested folders of code,
but I decided to put the states in their own folder
just so easily you can get access to all your states.
So we'll look at StartState here on line 21.
So recall in the StartState, we just had Breakout in the center of the screen
and then we had Start Game and High Scores.
So the user was able to highlight which state he wanted to look at.
So we need to keep track of which one is highlighted.
So all this variable's purpose is just to keep track.
So one or two.
One being Play Game, and two being High Scores.
And then here if we press up and down, then we--
because there's only two options effectively,
you can just flip whatever highlight is with one or two.
If you have a list of options that's more than two,
you'll need to increment one until it gets to whatever
X is, your number of list options.
And then if you press down at that point,
you should flip back up to the top.
And the same holds true for whether you're at option one.
You should go flip, rotate to the bottom of your list
so that it looks as if you've gone all the way around.
And-- then we're just playing a sound here when we do that.
We have a love.keyboard.wasPressed[escape] call
here.
It's not global anymore because there are some states in our application
where we might want to press Escape to actually go backwards,
and we'll see that.
And so rendering here.
We render Breakout with a large font.
Now that we can access G fonts at large key in the center of the screen,
set medium font.
And then we're going to render our two text fields one after the other.
But if highlighted is equal to one, then we're
going to set it to some blue color, which is one of three--
255, 255, 255.
And then render it.
And then make sure to reset the color after that because recall
love 2D is sort of like a state machine in its own right,
where if you set the color to something, whatever you draw and render
after that, be it images or text, will adopt that color.
So having everything be 255, 255, 255, 255,
which is pure white, completely opaque, has
the effect of drawing everything completely opaque.
But if you don't do that, your images or whatnot that you draw afterwards
will be tinted or transparent, which you most of the time don't want.
But you might sometimes want that, and we'll actually
see that in the PaddleSelectState.
And same thing holds true here.
If highlighted is two, do the exact same thing.
And so if we run this application, which is mainly
just a subset of what we saw before, we can move up and down
between Start and High Scores.
But if we press Enter on any of them, nothing
happens because we have no event handlers actually taking care of that.
But we have the image scaled to the screen, we have Breakout in the middle,
and we have our two menu options there.
So Breakout1.
So this is where we start to dive a little bit into sprite sheets, which
is a major component of game development, 2D game
development that we'll be looking at in the future and in this application.
But a sprite sheet is just, ultimately, rather than have--
I don't know how many images there are on this sprite sheet here.
But however many of these files, just have
one file put them all together, and then using rectangles, define
where all the different sprites are.
And then when we want to draw, use those rectangles
and just tell love.graphics.draw, I want you
to draw this texture, this sprite sheet, but I want
you to draw just this section of it.
You'd pass it in a quad, which is just simply rectangle with height, X and Y.
And love 2D will know, OK, I'm going to draw the image, but only this bit.
And it has the effect of looking as if you're only drawing tiny little images
as opposed to one monstrous image.
And the functions that are relevant for us to look at
are love.graphics.newquad, which takes an X, Y, width, and a height.
And also a dimensions object, which you get from an image.
We'll see that.
And all that basically is, I believe, is just an X, Y, width and height as well.
Or just a width and a height, rather, from whatever image
you want to create quads for.
And then love.graphics.draw, we've already seen it,
but this is a different signature.
This has texture, quad, X, Y. Quad being the second argument.
And when it takes in this quad, it knows to only draw
that defined rectangle of image to the screen.
And so we'll go ahead and take a look now at Breakout1.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: The question was, are there any tools
so that we don't have to guess where the quad is when we're doing the sheet?
Yes, there are a lot of the time.
I looked and saw a couple, but I haven't tested them thoroughly myself.
For simpler examples like this, it's usually
easy enough to programmatically do it.
But yeah, when you get into having giant sprite atlases where
you have especially things that are not necessarily symmetrical
or rectangular looking even though they still need to be defined rectangularly,
it's often best to use a tool like that.
There are, I do believe, I just haven't used them.
I can bring it up in a future lecture so we can discuss.
Any other questions before we carry into Breakout1?
All right.
So I'm going to go ahead and open up the very first thing
we should look at on Breakout1.
In the source directory, we have a new file.
And from here on out, I'm going to assume,
we're going to always assume that when we introduce a new file,
we're going to include it in dependencies.lua.
And so in this case, all we need to do is just say require source/util.
And as you can see, we're also adding a PlayState to this demonstration.
But from here on out, I won't make mention of us actually
adding it to our project.
So util.lua is the module that contains the code
we're going to use to actually generate quads for a given sprite sheet.
And this function, all it does, is it takes an atlas or sprite sheet--
the names are synonymous.
You'll hear them both.
Or we pass it an Atlas, we pass it the width of the tile that we want
and the height of the tile that we want.
It's going to get the width and the height of the sheet here.
So every image has a function called get width and get height,
so we're just going to do that.
And specifically the sheet width and sheet height
are the width of the image divided by tile width and tile height.
So we know how many times we need to iterate over the sprite sheet
to generate a rectangle.
We're dividing it up based on the size of our tiles.
And then we just basically do a simple nested four loop here.
We start a counter and a sprite sheet.
This sprite sheet is going to be a table that holds all of our quads.
We just say for Y, get zero.
Sheet height minus one.
So starting at the top left, going down.
And starting at the top going down, and then x equals zero,
starting at the left going right.
At sprite sheet, sheet counter, which is one here because in lua,
tables are one indexed.
We're going to create a new quad at X times tile width, Y times tile width.
Give it the width and the height of our tile.
So just whatever we passed into our function signature.
Here it will often be in this case be 16 by 32
because that's the size of the bricks.
And then we pass in the last parameter that we saw in the slide,
which is atlas:getdimensions.
And then we just increment our sheet counter here.
And then at the end of this, when we're all done, we'll return this.
We'll have a table of quads that we can then
use that are in a sort of one, two, three, four, five, six, seven, eight.
Well, I should say, one, two, three, four, five, six, seven, eight top
left to bottom right of all the sprites in our sheet to make it
super easy to look at.
We have another function here.
Lua doesn't by default, have a slice function, but we are just adding to it.
Table.slice.
It takes the table a first, the first entry in the table that we want,
the last entry, and then the step between them.
Just like Pythons slice function, it just iterates over the
for loop, which is first till one.
So one by default. Until the last or until whatever this sort of number sign
is the size of a table, which I don't think we've introduced yet.
But basically, if we pass in last, it'll stop there, otherwise just
assume we want the whole entire table.
And then this comma here at the end, which has step or one,
you can pass in a step at the end of a for loop as a third argument,
and that will be however much increments or decrements the loop that you're in.
So by default, just one.
We go one, then we go to two, then we go to three.
But you can set it to negative one.
And so if you say four i gets three to one minus one,
you'll go three, two, one.
And you can't do normally a step, which is what we do here.
No, you can do a step, but you can't slice,
which is why we have here sliced at number of slice plus one gets table i,
and then eventually we return slice.
So it just returns just a segment of whatever table we're in.
And then the important function here that we're actually
going to use in our application, we're going to generate quads paddles.
And so this takes X and Y, 0 and 64.
And if we look back at our paddles here, we
can see that we have various different sizes.
So we have a small one, a medium one, a large one, and then a really large one.
So if we want to get every single paddle in our sprite sheet,
small, medium, large, giant, notice that we have four blocks
and within each of those blocks we have four different sizes.
So we can just iterate over this four times
and then just define whatever the size of this rect
is, that rect, that rect, and that rect.
And we'll see the math for it here.
If I go zero to three, for i, get zero till three.
We're going to go ahead because that will give us four.
So that's how many times we want to iterate over the sprite sheet
to get the separate quads.
We'll get the smallest one.
So quads counter.
We initialize counter to one.
Gets love.graphics, that new quad at X, Y, with the 32 and 16.
Oh, and X and Y default at 0 and 64 here because the note--
recall that these are all 16 tall here.
So we're starting Y at 64 so that we start right here.
And we're starting X at zero because it's on the left side.
So we'll do that.
We'll increment counter.
Get it at 32 wide by 16 tall.
Those are the actual dimensions of the smallest one.
The same exact logic applies for medium and for large.
Only that we're adding 32 and then we're making it size 64,
and then we're adding 96 to X at size 96 because they're getting wider,
but they're also offsetting more to the right.
And then the last bit is pretty much the same thing as before, except now we're
going Y plus 16 back to X because we've gone down a row in our sprite sheet
The paddle width at that point is 128, but still 16 pixels.
And then here at the bottom because we want to do this four times,
we want to go through the chunks are effectively 32 pixels
because we're going 16, 16, 16, 16.
We're going to add 32 to Y and then go to the next set of four paddles.
So this is how we're effectively getting all of the paddle sprites,
and they;re going to be stored one through X where I believe X is 16.
So we'll have 16 quads defined in our sprite sheet
thereafter that we can then return.
So I'm going to go back to main.lua now on line 64.
Here we have a new global table called gframes.
We'll be able to access this anywhere we want to draw stuff.
And it's just the same thing that we just saw.
Generate quads paddles, and we just pass it in our main texture.
And our main texture is this.
This is what our main texture looks like.
And then we're going to index it.
We're going to say it gets the key paddles,
because in that particular table was just the quads for our paddles.
So in the future, we just need to call love.graphics.drawtexture and then
index into gframes paddles at whatever paddle we want.
And that's how we can keep track of what we want to draw paddle wise.
And in this particular demo we have a new paddle class
because paddle is a thing in our game.
We can represent it as sort of a class or an object.
So we'll define a class for it.
Everything is pretty simple thus far.
Gets an X and a Y. Dx is zero with height.
Skin.
The skin is going to be what color it is.
We need to keep track of that.
And then the size, because size will be how we sort of offset
into our paddles, our quads, because the sizes are small, medium, large, giant.
One, two, three, four times four.
So one, two, three, four for the first set and then five,
six, seven, eight for the second set.
Those are all sort of by color.
So we can just multiply skin times--
or we can multiply whatever our size is by skin
and that will give us the current frame, the current quad that we want in order
to draw to the screen.
And then on line seven--
so this is keyboard input here.
Stuff that we've seen before.
If we're pressing left or right, then the paddles should move.
Dx should be set left or right.
We want to clamp it.
We saw this, we've seen this as well.
Clamp the input to the left and the right side of the screen.
If the dx is less than zero, do math.max and math.min otherwise
if we're moving to the right.
And then here, this is actually where we tie it all together
and we actually use the quads to draw something onto the screen.
So we're calling love.graphics.draw just our texture, our main texture.
And then gframes at paddles at our current size, which is two.
We want to by default have the medium size plus four times whatever
our skin is, minus one.
So if our skin is one, which is the blue skin, we won't add anything to it.
It'll just be four times zero.
But if we have the next one, it'll be two minus one,
so we'll end up adding four to that.
And because we're adding four to it times whatever that skin is,
it will just basically put us four quads in,
which is the next, the exact same paddle, but the next color.
And then lastly what we'll look at here is the PlayState.
So we had just the StartState before, but now we
want to actually test to make sure we can draw a paddle,
move it around the screen.
So we're going to implement a simple PlayState here.
So on line 20, we're just calling self.paddle gets paddle.
We're initializing a new paddle object.
And then we're keeping track of also this
is a simple, like, pause demonstration.
If self.paused, then-- actually yeah.
Did I say self.paused?
I did.
OK.
I just don't initialize it to anything.
I should have set self.paused to false here.
If self.paused, we're going to test to see whether we're pressing space,
and if we are, unpause it.
Otherwise, basically just do the same exact thing in reverse.
If we press space, pause the game, play a sound, et cetera.
Here on line 39, we're just going to call update on the paddle.
Which, just remember, test for left or right input.
Here we want to be able to escape the game,
so we're going to have a handler for escape.
Render the paddle on lines 47, which will do the love.graphics.draw
with a quad as we saw before, but it'll use
the skin and the size of that paddle to index into the quads tile sheet
appropriately.
And then here if we're paused, let's just draw
some text in the middle of the screen that just says Pause.
And we use the large font.
So we can go ahead and demo this now and see everything come together.
We have as before our StartState.
But if we press Enter, now we go to our PlayState
and we just have a paddle at the bottom of the screen.
It's size two, skin one.
Just the blue skin.
And we can move it left or right like that.
And if it hits the left side of the screen, it will stop.
And if it hits the right side of the screen, it will stop as well.
So we've made progress, but this is one of the fundamental things
I'd like to showcase today is just, like, using quads and categorizing
them, organizing them, and being able to draw
your assets from a large compiled image rather than keep track
of however many images it would take.
And you have to name all of them and sort them.
It would just be a big pain.
So yeah.
Definitely going forward when you have more than one sprite,
you want to sort of put it together in one sheet,
and that's how we can accomplish that.
But we don't have bricks, and this is probably
the other big main component of Breakout besides the paddle and the ball.
We want to have bricks that we can actually hit and aim for on the screen.
So this update will address that.
So let's go ahead and take a look at Breakout2 in main.lua.
I'm going to open it up here.
On line 66, you can see we have a new table in our gframes.
Because we had one just for paddles, we took out
just the paddles from our sprite sheet.
We're going to do the same thing for just the balls.
So we're going to look at-- if we look here,
we can see that the balls sort of come after all of the bricks here
and they're just laid out in eight pixels wide by eight
pixels tall increments here.
So four pixels to one brick, four balls to one brick,
two balls to one horizontally, and then two balls vertically.
And so what we'll end up doing is just a simple function in our util
that takes a look at that.
So let's go ahead and take a look at our util.lua, which we've made changes to.
And so what this is going to do is sort of do the same thing
that we did before.
It has to iterate.
So notice we have two rows of balls.
We have these four and we have these three.
So we want to iterate four times.
You want to find whatever the offset is here,
the X and Y. So it looks like three times 32 and then three times 16.
So 96 by--
I can't do math.
Whatever 16 times three is.
And then we'll end up 48.
And then we'll have--
which is what we do here.
So we have two iterations.
So a four loop that goes from zero to three.
So the top row, the four.
We'll set a counter to one here.
And notice also 96 and 48.
That's the X and the Y that we're setting.
That's where the offset is for the individual ball sprites.
Quads at counter gets--
and notice also quads is a table.
We're going to return this.
Quads at counter gets love.graphics.newquad at X, Y.
Eight pixels wide, eight pixels tall.
That's how large the balls are.
And then we're going to add eight to it because we're going to the right.
So this iteration just goes left to right.
And then here we're going to do basically X being set to 96
and then Y to 56.
And then because we were editing X directly in here,
we want to reset X back to 96, but then also add
the eight pixels so that we have the start for the next row
vertically, so at Y 56.
Do the exact same thing here, but only do it
three times because recall there is four balls on top
and then three balls on bottom.
And then return it at the very end.
And so now we have just an individual table.
We don't need to keep like one monstrous table of quads,
which I find sort of disorganized.
We can just have a table of frames for the paddles,
and the balls, and the bricks as we'll see.
Actually, I have it up here I think.
Maybe not.
So in ball-- oh, actually, hold on.
Sorry.
So we were looking at--
I skipped over this one on accident.
So the bounce update.
So everything I just said is relevant, but I accidentally
hit that right two times.
We want to go to the bounce update because this is slightly simpler.
So we were just talking about the ball, which is perfect.
So we're going to take the ball and then we're going to add that to the scene,
and we're just going to implement bouncing off the walls.
So actually, pretty identical to the code
we saw for Pong where you just detect whether the ball has
gone past the left, right, or top edge of the screen.
In this case, it will also allow us to go to the bottom of the screen
and we'll also implement colliding with the paddle
so then get a sense of the actual game play and what that feels like.
So everything is currently current.
So we're going to go--
after talking about the function to actually get the individual ball
quads out of the spreadsheet, we're going
to look at the ball class which is going to allow us to spawn them in our scene.
So a ball takes a width and height of eight.
No velocity.
But we're going to allow ourselves to initialize the ball with the skin,
and we'll see this later just as a cutesy little thing to you
use the actual individual sprites rather than just one constant sprite.
We're just going to give it a random number between one and seven
because there are seven quads.
And then we'll just use gframes balls and math dot random number
to get the actual ball spread that we want.
And so we have a simple collides function within ball
that would allow us to check to see whether we've collided with something
that has a X, Y width and a height.
So it's a simple A, B collision detection.
And then here we have reset.
Just resets it to the middle of the screen.
Update applies velocity.
Stuff we've already seen.
This is where we actually implement bouncing off the walls.
So if X is less than or equal to zero, greater than
or equal to virtual width minus eight, or less than or equal to zero,
this should be where we reverse the velocity.
In the case of it bouncing off the left side, we want to reverse the X velocity
but keep it going up.
If it hits the top, then we want to reverse
the Y velocity to keep it moving in whatever direction it was moving.
And same thing with the right hand wall.
And then play a wall hit sound.
And we're incorporating the sounds sort of as we go today
just because they're so simple.
And it's also kind of nice just to have a little bit of feedback
when you're actually endpoint of the game.
And the exact same code is here for drawing.
So we have main texture, but now we're using gframes balls,
and then we're indexing that at self.skin.
And recall that we just set self.skin in here.
So all we need to do to just make it random
is just wherever we create a new ball, just give it a math.random7,
and then that will index into that quads table
so we can draw a different ball texture each time.
And so let's go ahead and see--
oh, actually, no.
And one last thing we need to look at is the PlayState
has a little bit of new code as well.
We're going to spawn a ball, so this is where we do it here.
I'm not doing it random, but I could do it random here if I wanted to.
I could math.random7, and every time we boot up
the game it's going to be a different color because it's
going to be a different skin.
We need to update the ball.
So on line 50 we just update it like we do the paddle.
And then on line 52, we're just testing to see
whether it collides with the paddle because we're using just simple A,
A, B, B. If it collides with the paddle, we can assume it was coming down.
We can just reverse as delta Y.
Now, does anybody know what might be a current issue
with the current implementation of this function?
Particularly with this line.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: It will.
You're on the right track.
The answer was, if the ball is coming from the side,
it won't necessarily be bounced back up in the right Y direction.
If it's coming from the side, it will always, in this case,
be coming from up above.
So it always still be reversing in the right delta Y.
But what's going to happen if it comes in at an angle
and then isn't basically reset?
Like right now if it comes at an angle and it
gets caught-- let's say it's like below the top edge of the paddle.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: You're going to get an infinite collision
loop because we're not resetting it's position,
we're only updating its velocity.
If it comes in at the right angle from the side,
it's going to get stuck inside the paddle
and then it's going to cause a little bit of funky behavior.
I'll try and see if I can make that happen in my demonstration here.
But that's the gist of all of these updates.
So if we go to Start, we can see immediately we have a ball.
And when it hits the sides or the top, it bounces accordingly.
It hits the paddle.
So when it comes in from the top flush on the top, it flips the Y velocity.
Let's see if I can get it at an angle here.
There it is.
It'll get stuck.
And so whenever you sort of do A, A, B, B collision detection,
just remember to always reset the position of whatever it
is that collided that's moving so that it doesn't clip
and get stuck inside of something else over and over again.
Yes.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: The question is I'm always doing love space dot,
and as opposed to just running things from using
the complete path of whatever the file is, in order to do that-- so are you
on a Mac or a Windows machine?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: So on a Windows machine it is a little trickier,
but I've found a really nice sort of plug-in for VS Code.
So if you're VS Code, which is the editor that I use, it has plug-ins
and one of the plug-ins that you can download is for Love2D
and it has a config where if you just press Alt L,
it will run whatever directory you're currently in,
whatever project you're currently in.
It will call Love.
It adds it to your path for you.
So download the Love2D plug-in on VS code if you want that to work.
I'm on a Mac, so I can edit what's called my batch profile,
and alias Love to its complete path in my file system.
And you can do the same thing with--
I don't know how it would work with Windows in terms of aliasing,
but it's essentially the same thing as typing out the entire path to Love,
but only I'm changing it to another word.
I'm changing it to Love.
So I'm setting Love equals to application slash
love.app/content/resources et cetera.
So good question.
I would download on Windows.
I'm a big fan of VS Code and the Love2D plug-in.
I would recommend looking into that.
And I'm sure there are other plug-ins, and there's a page also
on the website--
I don't have a browser open at the moment.
But on the wiki, you can look at the Getting Started page.
I believe it's like love2d.com/wiki/gettingstarted.
They have a bunch of instructions for different operating
systems and different text editors that allow you to get sort
of a more efficient workflow going.
So any other questions?
All right.
So we did the bounce update.
Now we can finally edit the bricks.
Add in the bricks, I should say.
So these are pretty simple.
So we're going to take a look at it.
And right now we're not going to do any sort of fancy procedural generation,
we're just going to get some bricks on the screen.
Just some easy bricks.
Or rather, we will get some very basic procedural generation,
but not to the level that we'll see soon.
We'll see that very soon.
OK.
So I'm going to go into my main.lua here.
I'm going to go into the Breakout3.
And same thing that we did before on line 67,
we just have a new bricks table in our gframes.
And it just generate quads bricks.
We call from util.lua, so we can look at that really quick as well.
This one's actually really easy.
Sourceutil.lua.
Because they start at the very top of the screen,
we can assume that-- we could effectively
treat this whole thing as if it were just these
and just generate quads at a constant width and height
because, effectively, we only need a subset of the frames that's generating.
Because it's generating them this way, top to bottom, left to right,
we can just grab all the way up to here using
table.slice, which we saw before, and not
worry about indexing into any weird, like,
having any constants X and Y that we need to index with in order
to get an offset.
We can just do a very simple--
if we go down to line 57, generate quads bricks, it just does a table.slice.
And so within that, we're going to generate quads atlas 32,16.
So this is going to have the effect of dividing up our sprite sheet by 32
by 16 pieces.
It's going to generate all of these just fine,
but then it's going to have quads here, here, here, here, here
that don't line up with the quads that you see here
because it's just blindly assuming that all of the sprites in that sheet
are the same size because that's all we're doing.
We're just calling generate quads, which if you recall,
just generates a fixed size width and height throughout our entire atlas,
which is great for a lot of sheets that are symmetrical,
but there are cases where we have, like, for example here, where
our spreadsheet is asymmetrical.
We have paddles of differing sizes, we have
the balls which are eight by eight, we have the bricks,
we have the other power ups at the bottom.
But the generate quads bricks takes in that table
that we're generating, which is going to be a bunch of frames
that we don't want.
Many of them clipped, half clipped.
And then we're just going to take it from one to 21.
And when we do that, one to 21 is effectively-- that's how many of these
there are.
So 18 and then one, two, three.
So from one to 21, all of those.
That will be all the bricks.
We can throw away all the rest of the quads
and just blindly assume that they're all the same size.
So any questions on how quads or how any of these tables are working?
OK.
So we're going to go ahead.
We have a new class now, brick.lua.
So simple building blocks.
In brick.lua on line 30, we have a flag called in play.
self.inplay gets true.
And so we're just going to use this to render.
We're just going to say, if it's in play, render it.
If it's not, don't render it.
It's that simple.
That way we don't have to worry about object deallocation or anything fancy.
We have all of our bricks and whether it's in play
or not, render it or perform update logic.
And if it's not in play, just pretend it doesn't exist.
Just ignore it.
We're only going to have like 30 or I don't know how many,
13 max by four bricks in our scene at once, so worrying about freeing memory
isn't really an issue.
But if you have a million different things getting generated all the time,
having simple in play is false might not always
be viable because you need to store all that memory for all those objects.
So just a shortcut here, but not necessarily best
practice for very large games.
But certainly great and simple for small games.
On line 37, we define a function called brick hit.
And all this does is just play a sound effect and set in play to false.
And so all we're going to do is just check
to see whether there's a collision and then just call this hit function,
play a sound, and then just pretend it doesn't exist anymore.
And then render, all render does is if it's in play, check the in play flag,
draw main at bricks or using our bricks table here that we created.
And then we're going to start at one and then
we're going to index it based on our color minus one times four,
and then we're going to add it's tier.
So there are, if you recall, one, two, three, four,
five colors and four tiers.
And so what we're going to do is we're going to jump between the colors.
So we'll go value one, value two, value three, value four, value five.
That will be our first five or I guess six.
That will be our first six bricks.
And then we're going to go one, two, three-- or we're going to add,
we're going to have a tier basically.
It'll be one, two, three, or four.
And if it's at tier one, then we can just add--
basically to index into whatever tier we're on,
we just need to add tier minus one to whatever our index is.
So here if our tier is one, then we just want to render this block.
We don't want to go to the next one.
So we're just going to say tier minus one.
We're going to add--
so one minus one is zero.
So we're going at zero to this, get this.
But if tiers two, we'll add one, and two, and three.
And then we just multiply whatever brick we want by our color.
Multiply it by four to get an offset for whatever our actual color is.
So we take our color, figure out where on the sheet it is,
and then just add our tier to it in order to index
into our spreadsheet accordingly.
And so that's what the math here is doing.
And if we go back to our PlayState--
and I'm going to start moving a little bit faster
just so we can keep caught up.
But in our PlayState, one thing that we notice here,
we have a new class called level maker that we're seeing,
which was a function called createmap.
We're going to take out all the logic for generating our levels
and we're just going to put it in one place.
We're going to call that level maker.
Rather than in our different states that maybe
generate the bricks like the PlayState or I
guess it would be the ServeState, VictoryState,
I guess, rather than generating all the bricks
in that state within it's innate code, let's just make a level maker
and we can just say, OK, set bricks to levelmaker.createmap,
which will return a table of bricks.
Same-- excuse me-- logic as we saw before.
In this case, we're just going to iterate for k brick
in pairs of self.bricks.
If the brick's in play and it collides, if the ball collides with it,
then hit it, which will set it not into play.
So simple A, A, B, B.
And then lastly, we have our render logic here,
which is going to take that bricks table and just iterate over it.
And the last thing we should probably look at
is the actual level maker itself, which in this case is very simple,
but we'll see it gets a little bit more complicated later when we do it.
When we have a more elaborate procedural generation approach to our levels.
But right now, we're just going to say set two random variables here.
Number of rows and columns.
And then for every row or for basically every row and every column,
create a new brick.
And then there's some math here.
I'm going to kind of skim over it, but basically it
calculates where the brick is and then gives us
eight pixels of padding on either side.
And then based on how many it is, it needs to center all
the bricks and shift them by a certain amount to the left
and then start drawing all of them.
And that's essentially what this code does here.
So calculate the center.
I wrote it out in comments here, but I'm going
just kind of glaze over it for now.
But effectively, center all the bricks.
Basically calculate what offset on the X-axis you need to put all of them
so that they appear centered, and then you're going to draw them all out.
And then that's it for the level maker class.
So simply number of rows and columns, and then fill a table with bricks
but set their X equal to however much we need to center all
of them when they're all drawn out.
So we need to figure, we need to basically take in our number of columns
into account when we do that.
And then if we go into Breakout3 and run that, we have bricks.
They're getting collided with, and as soon as they get hit,
collided are in play on each of those bricks gets set to false
and they no longer get rendered.
And they no longer get updated in terms of collision.
Now, we still have the issue with the ball not getting reset.
We'll fix that.
That's an easy fix.
But we're coming a long way.
We have things moving at quite a pace.
I'm going to go ahead and move to the next bit of code here.
So this is another bit of code.
I'm going to sort of glaze over a little bit of the details here.
But at a high level what we need to do is it's one thing
to detect that we've collided with a brick,
but in Breakout, the ball bounces off of the brick
depending on which side it hits.
And we don't know this necessarily just based off of the collision.
We just know whether the collision is true or not.
We don't know where it came from and how much it collided with.
And then we're also going to fix our paddles so that rather than-- because
currently all it does is just negate whatever the Y velocity is,
but we want to add a little bit more variety
to how we end up sort of ricocheting the ball off the paddle when we play
so that we can sort of strategize a little bit,
give ourselves a little bit of game play.
So if we are moving to the right and we hit the right edge of the puddle
with the ball, it should probably go in a sharper direction.
Same thing with the left side.
And we can effectively do that by taking the middle,
figuring out how far away from the center it is,
and then just amplifying our delta X in the negative or positive direction
based off of that.
And that has the effect of causing that to happen.
So here we can see we have the ball sort of coming at the paddle,
and let's pretend that the paddle is moving to the left.
In this case, however far away the ball is from the center,
we want to scale that by some amount and then
end up making that our negative delta X, because that's effectively
how the game normally works.
If you move the paddle to the left or the right,
hit it on a corner or something, gives it that sharp angle.
And that's effectively what the sharp angle is.
It's just a strong delta X, and it gets amplified the larger this is.
So just basically take this, multiply it by some amount,
and then make it negative or positive on your dx.
That's your sort of paddle collision V2.
Brick collision is a little bit--
it's pretty simple, but it's a little bit more complicated.
Basically what we need to do is just check and see which edge of the ball
isn't inside the brick.
And so if the left edge of the-- and we can also sort of
simplify this a little bit.
If the left-- as you see here by the pseudocode--
if the left edge of the ball is outside the brick and the dx is positive,
then we can say, oh, we can basically assume
we've come in from the left side, so we should probably
go in the opposite Y direction on the left side.
Or sorry, we should go in the same Y direction, but negate our delta
X. Because we're coming in from the left,
the left side is outside the brick, so bounce it back.
And the same thing for the right edge.
And we only do this test, the left edge of the ball, if dx is positive.
Because if dx is negative, there's no way
the ball's colliding with the left side of our brick.
So we can shortcut that effectively.
We do the same exact logic here, just on the right edge
of the brick instead of the left edge.
And then if none of those hold true, we're
going to see if the top edge of the ball is above the top edge of the brick.
And if that's the case, we know that we've hit from the top.
We can trigger a top collision.
And if none of those have held true, we know
that we have had a collision of some kind,
we can just register a bottom collision.
And so this is a simple version of this sort
of way of doing Breakout collision.
It has a few faults when it comes to corners,
sometimes corners can be a little bit finicky,
but I would say it works 99% of the time.
For a much more robust and a better example, I would look at this URL
here because he also goes into a full sort of breakdown
of how he would implement arkanoid, which is the same thing effectively
as Breakout if you just want an alternative look at it.
But basically, his solution involved taking how much the X and the Y
differed on different points of the bricks relative to the ball.
And I believe he also kept the ball as an actual ball with a center point,
even though he rendered it as a rectangle.
So it's a little bit more robust.
I decided to implement it a simpler way, which I'll showcase,
which is the way that I demonstrated because it worked well
and it wasn't too much code to sort of look over.
But I do encourage you to take a look at that.
We're going to look at our PlayState now in Breakout4.
And in our PlayState, we're going to see--
sorry.
Line 65.
So this is the actual paddle code for influencing the ball's delta X.
So basically, if the ball.x is less than the paddle.x plus it's width divided
by two, so basically on the left side of the paddle,
and the paddle's delta X is less than zero, which means it's moving left--
because we don't really want to necessarily influence it if we're just
standing still--
we're going to do what I described earlier.
We're going to give it some scaler, like some start off value.
In this case, negative 50 is just sort of seeding this, giving it
some sort of initial value.
And then we're just going to subtract the ball's X from the middle point.
This being the middle point of the paddle
And then just multiply it by eight.
So whatever the difference is between the ball's X and the middle
of the paddle, multiply it by eight.
Add it to negative 50 and then negate that.
Also negate that whole value so that the whole entire value becomes negative.
And we, therefore, get a sharper delta X depending on which angle
it's coming at, and also how fast--
or not how fast, but whether or not we are moving left.
And it's the exact same thing on the right side.
Only because we're taking this math, this self.paddle.x
plus self.paddle.width divided by two minus the ball.x,
the ball.x isn't going to be greater than that point.
So this value is actually going to be negative.
So we're going to just make it positive with math.abs.
So absolute value.
Just a lua function.
So the absolute value of the difference between the ball's
X and the middle point times eight, add it to 50,
and that'll give us a positive value that scales depending on whether or not
we've hit the middle of the, we've hit the right edge of the paddle
and are moving to the right.
And so that's, in a nutshell, how we get that collision to work with the paddle
and how we can tweak delta X to be scaled a little bit more
than just a constant, you know, negative or whatever
it's current X was, but negative dy.
A little bit more complicated.
And then the actual collision code for the bricks
themselves is going to take place in a for loop here.
So if it's in play, if the ball collides with it, hit it.
So I added plus two.
So the gist of the math is if ball.x is less than brick.x
and the ball is moving to the right, self.ball.dx is greater than zero,
then flip it's X velocity.
So bounce it to the left.
That's what this check is.
But it plays a little bit rough with corners
because you could theoretically get into a position
where you come in at an angle and it's intersecting
with the paddle in two positions, both on top and the left
or on bottom and the left.
So in that case, adding two sort of prioritizes the Y being hit.
So it basically takes the check from the exposition of the ball to the X plus 2.
And so it ends up fixing the corners a little bit, but the gist of it
is just check to see if the ball.x is less than the brick.x.
And if it is and we've detected a collision, we can bounce it.
There are some subtle corner case bugs without adding this plus two,
so we add that.
And then flip the velocity here.
Oh, this shift here.
This is what we were talking about earlier with make sure
when you do a collision, shift whatever is
moving outside the boundaries of whatever you're colliding with.
So self.ball.x gets brick.x minus eight because the ball is eight pixels wide.
It should actually be self.ball.width for a better style,
but that's essentially what it translates out to.
Same thing for the right edge.
The plus six because it's on the right side.
So it's effectively the same thing as minus two if we're on the left side.
Just a sort of fixes corners, weird issues with corners.
But check in to see if basically the ball plus its height minus two
is greater than the brick plus X plus brick.width, which it means,
oh, we've collided with the right edge of the screen, of the brick.
And then if the Y is less than the brick.y,
then we've collided with the top of the brick,
and otherwise, we've collided with the bottom.
And with the top and the bottom, just do the same thing we did with delta X,
but do it with delta Y, but you're still resetting it.
So ball.y gets brick.y minus eight.
Ball.y gets brick.y plus 16 because the paddle or the individual bricks
are 16 pixels tall.
That's the gist of the collision detection.
And then if we actually-- oh, and one other thing that I ended up
putting here just to make it a little bit more interesting,
and this also ties into more complicated collision detection.
If your velocity is too fast, a lot of the time it'll skip through objects,
and then that causes a lot of problems with these collision detection
functions that normally are very sort of mathematically correct
and they work well.
They don't work well when it skips over what
you're trying to actually collide with.
So a solution to that, which was beyond the scope of this example but something
we're thinking about, is perhaps stepping backwards a certain amount
of time, a certain amount of pixels.
Perhaps maybe start at where you where your ball was on one particular,
on the last frame, and then just add its width and height to itself
until it collides with something, until it reaches whatever its current delta X
or delta Y plus its position is.
That's one way to do it.
Sort of just adding a bunch of invisible--
whatever you're colliding with or whatever you're using to collide--
add a bunch of invisible those to bridge the gap and check into if any of those
hold true for a collision.
A little bit more computationally expensive, but a lot more accurate
in terms of the physics.
And aside from that, everything is the same.
So if you look at the code in Breakout4--
and I'm going to go a little bit faster henceforth.
That's probably the meatiest part of the program.
We get collisions.
And then I'll try and get a strong angle so I can demo the--
that didn't work.
That actually gave a weaker angle.
So if you do this and you do it close to the center, it has the opposite effect.
But there you go.
That's a sharper angle.
So now you can actually influence the ball in a little bit more
of a personable way.
You know, not just have it be a flat delta Y gets negative--
or get negative delta Y effectively.
So any questions on sort of how the gist of all of that works?
OK.
Perfect.
So now we're going to get into a little bit more of some fun stuff.
We'll do a couple more examples, then we'll take a break.
So this is the hearts update.
So notice that the very top of the screen, as I've demonstrated in these
slides, we have just a few hearts.
One of them is empty.
We showed this earlier.
And then we have a game over screen, which is our final score.
So I'm going to go ahead and we're just going to look at the code a little bit
faster now since a lot of the stuff is fairly straightforward.
I'm going to go ahead and open up the--
I'm going to make sure I'm in the right folder first of all.
Breakout5.
And then in the--
so one other thing we're going to start doing is--
I mentioned this earlier.
And it's going to be it's going to hold true
for any of the sort of state transformations
that take place going forward.
Rather than keep global variables, we're going
to sort of do away with that idea outside of the asset tables
that we have just because those are kind of an exception
and they could reasonably be put into a separate class called the resource
manager.
We're going to start passing in what is basically our current app state,
or at least the variables that make sense.
And this is a common paradigm in web development with React as well.
But basically, everything that we need to be preserve state to state,
rather than just keeping global variables,
let's pass them between the states because the state machine allows
us to do that in the change function.
And then whatever that state is in it's enter function,
it'll have access to that and it can just
set those values to self dot whatever and use them.
But we no longer have global variables.
We're just saying, here.
Here's the values that are important for you to continue on.
And then that state will take its values and go to the next state
and say, oh, OK, here are the values that you need to function.
Like the serve, play, and all those states
that have the core game play involved will probably
need to maintain a reference to like the paddle,
and to the score, the amount of health we have.
But when we get to the end, for example, and then we no longer really
need a paddle, we no longer really need bricks or anything like that,
we just need to know what our high score is so that we can enter it
into our high score list, all we really need
to do is just pass in the high score state entry or just our high score,
and that's it.
So it encapsulates all of our data.
And at a glance, we can sort of see what we need to pass between the states
and what's going to be relevant at a glance as well.
It just clean things up quite a bit.
So that's what we're doing now on line 35.
And henceforth, we will do this in every state as we see,
but I'm going to sort of glaze over it in the future.
We have a ServeState now.
So a ServeState, this is very identical to what we did in Pong.
So we just wait for the user to press Space.
They can move around and then when they do press Enter,
basically the ball starts moving.
And then we change the PlayState here using
the current values that are necessary.
Paddle, bricks, health, score, and ball.
Those are basically the fundamental variables
that we need in order to keep track of our GameState.
So we have a ServeState, it will wait for us to press Enter.
And then our main.lua, we have a new hearts table.
And then on line 208, because we're going
to need the ability to render health and render our score across several states,
Play, Serve, Victory, Game Over--
actually not Game Over, but the three before that.
We don't want to duplicate those behaviors,
so I'm just calling a function called Render Health, which
just takes in whatever health is and then
we just set an X to virtual width minus 100.
And then for however many health we have, draw a heart
from the hearts sprite sheet, which I separated the hearts out
into a smaller image so you can just split them on like eight by eight
or whatever it is.
But just draw those and then add 11 to X,
and just keep going until we've drawn out however many hearts we have.
That will draw full hearts.
And then three minus health will give us however many health we're missing.
So if we took a point of damage, this is going to be equal to one.
So then it'll draw one empty heart after that or it'll draw two empty hearts.
So draw however many full hearts we have, then draw the empty hearts.
And those are two separate sprites that we get from the image.
And that will have the effect of drawing our health.
And then our score is simply, it takes a score variable that we pass into here.
And also note that the render health [INAUDIBLE]
and health variable and pass into it here.
And so in our PlayState, we are calling both of these functions on line 135.
Well, on line 135, we are calculating whether we
go below the edge of the screen, which is another important part of the game.
Obviously, we need to detect when we've lost health.
So it's as simple as this.
If it's greater than the virtual height, decrement health by one.
If it's equal to zero, change to Game Over.
Else change to the ServeState.
And note that we're passing in all these variables to and from our states.
The ones that are important.
Game Over just needs score, but Serve needs whatever
variables we were already using.
And then down here we're calling render score and render health,
and then the GameOverState is simply--
because it takes in score from the parameters list,
just wait for keyboard input to go back to the start and then render game over,
here's your score.
It's self.score, and then that's it.
Very simple.
Very simple state.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Sure.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: The question was, do any of these states
have access to their parent file?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Is everything in main.lua global functions?
Yes.
Functions that you declare.
Anything that's basically not specified as local that you define in main.lua
will be accessible anywhere in your application, including functions.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: You don't have to-- the question was,
do you have to declare as public?
No, there is no notion of public.
In lua, anything that does not have a local specifier
is assumed global, even if it's in a nested scope.
So you could have a for loop, you could have several nested for loops
and declare some variable without local, that variable can be accessed anywhere
above it or outside of it.
So it's pretty important to use local variables
when you're not explicitly allocating something as global
just to avoid the bug of for nested loops
and you have some variable name like hello and you use it somewhere else.
Good questions though.
So yeah.
We have a bunch of states now.
We have a GameOverState, a PlayState, we're
rendering our score, rendering our health.
If we go and take a look at Breakout5--
is it a different window?
There we go.
We can see hearts at the top.
Score zero.
Oh, and I forgot to mention the part where we actually add score now.
So the bricks themselves in their on hit, or I should say in the PlayState,
on line 81 when we detect a hit, we're just adding 10 to the score for now.
But later on, we'll do a calculation where
we take tier and color into consideration
and then perform arithmetic on that to get our total score for each ball hit.
But yeah, we have our health, we have our score.
And then once we take enough damage, we'll
end up going to the Game Over screen.
The Game Over screen will go back to our Start screen.
So making progress.
And then probably my favorite of the updates
before we take a short break is the pretty colors update.
So what this does is clearly we can have--
we've updated our level maker.
So rather than just having a bunch of very static bricks,
we end up doing a little bit more complicated procedural generation.
It's not complicated though.
Just in levelmaker.lua in Breakout6, we have a few different constants here.
So solid, alternate, skip, or none.
Actually, I don't think I use skip or none.
Just solid or alternate basically.
We have flags now.
So number of columns.
And we ensure that it's odd because even columns with generating patterns
leads to asymmetry.
So make sure the number of columns is odd.
Generate the highest tier and the highest color based on our level.
So in this case, we'll go no higher of a tier
than three because we have no higher tiers than three.
It goes zero, one, two, three.
And then whatever our level divided by five is,
and it would just take math.floor.
Math.floor takes in basically performing division and then truncating
the decimal point.
Well, not division.
It just literally truncates the decimal point off of a number.
So a level divided by five.
Whatever that is before the decimal point.
Level modular five plus three for the highest color.
So we'll cycle.
We'll go over and over again.
Go highest color one, two, three, four, five, and then we'll go to a new tier
with level divided by 5.
So basically, every five levels will increment in tier,
and then we'll start back at blue.
And then we go on, and on, and on like that for every number of rows.
So basically I have a few--
I'm going to sort of glaze over this a little bit
just because we're probably going to run short on time.
But we have basically two flags.
Whether we're skipping bricks in this row or alternating bricks color wise.
And if we do, we need to set a color for it and a tier.
And then we basically just say, you know,
the same sort of logic that we had before we generated random rows
and columns, but if we have the alternate flag on,
then as we can see in some of these photos here, here we have skip is true.
So the color for that row is set to the blue, but skip is true,
so every other brick is just going to skip that iteration of the loop.
Same thing here, only it's offset by one.
Same thing here.
So this is kind of a nice little pattern.
And in each of these cases--
actually not each of these cases.
Notice this third one, it also set alternate to true.
So it goes green, purple, green, purple, green, purple.
And so the logic there is if alternate is true, then
just flip the color every iteration.
If skip is true, don't generate a brick every other iteration, and so on
and so forth.
And then if you have solid or if you don't have alternate equals true,
then you have a solid brick like these blue ones.
And if you have alternate but no skip, you get this sort of pattern
where you have green, purple, green, purple.
You know, any random color.
And then also the number of columns is random.
So it can go--
here we have 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 but on this very bottom one,
we have that minus two it looks like because it can only go that wide.
And in those here too.
Smaller size.
That one there's no spacing.
So these are very simple concepts.
Like should we skip a block this iteration?
Should we alternate the colors?
And when you put them all together, it produces
things that look as if they were almost handcrafted.
Like this could be made by somebody.
Like, that looks like it was made by somebody.
Pretty much every iteration of this.
I mean, even that, that looks like a shape almost.
Its just very simple but the results are pretty awesome in my opinion.
And so that's just the gist behind what we're doing.
We're just setting flags and just saying, you know,
if we're skipping this turn and just every iteration,
every time we lay out a brick and we spawn a new brick on this row,
just do or don't.
Just make it's color--
pick two colors if we're alternating and then set
its color to whatever the off color is that we're alternating.
And if we're skipping and alternating, then
we're just doing whenever we're on a brick that we're actually laying
is when we change the color, the alternate color.
And so like I said, I won't go into too much detail.
Happy to talk about the generator after class.
But just because we're running short on time,
sort of going to wave my hands over it.
But that's it in a nutshell.
So any questions before we take a break for five minutes?
Yes?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: The question is, in an instance with this programming
if the ball were so fast that it we're actually inside the brick,
would it what?
AUDIENCE: Would it still bounce back?
COLTON OGDEN: Would it still bounce back?
The answer is no, it wouldn't.
This implementation doesn't take into consideration velocity
that goes too fast.
Mainly to-- for two reasons.
One, it's non-trivial to implement, and two,
it's an interesting thing to look at, and observe,
and be conscious of as you go forward in implementing your own games.
The current code, if it gets clipped inside of the brick,
it will have no edges that are peaking outside of the brick
and therefore, it will default to the final condition, which
is the last else clause, which puts it below the brick.
So it'll just go below the brick.
It'll almost be as if it came in from the underside and bounced out.
But like I alluded to earlier, if you want to implement something
like this yourself, you would have to slice up frame X and frame X plus one
into the size of the ball if the delta is so wide
that it either goes inside of a brick or it goes outside of a brick,
or if it skips a brick.
And this sort of solves that problem.
It solves both of those problems, but it's a little more
than we can cover in this example.
Any other questions?
All right.
Let's take five and get back to it.
All right.
And we're back.
So the next step is we have basically a layout dynamically
generated of interesting bricks now, but we haven't really
implemented scoring any of these.
We just have score gets score plus 10, which
isn't really particularly interesting.
So Breakout7 is what I call the tier update, which
should allow us to hit blocks that are a higher tier than just base blue.
And if they are of a higher color than base blue, they should go down a color.
So the hierarchy was, if we look back, blue
goes to green goes to red goes to purple goes to gold.
And if something is a higher tier, it goes
to the next color below it but at that same tier,
unless it happens to be like blue and gray, in which case
it'll go back to blue.
So how might we implement scoring based on this system?
What do we need?
What pieces do we need?
What pieces do we already have that we can use to make this happen?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: I'm sorry?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: So the answer was the brick index.
So yes, the brick skin and color are the pieces.
Yes.
So those are fields of brick.
So if we open up--
I'm going to go up to Breakout7.
And I'm going to start probably deferring a lot of this code
to future reading.
But in brick here, the tier and the color--
sorry, not skin, but skin is for the paddle.
But the brick has a tier and it has a color.
And so we need to perform some arithmetic on that here.
And that's essentially what lines 44 through 58 is.
So basically-- oh, I apologize.
That's not actually where the arithmetic is.
44, that does compute, but this is the bit of code
that computes how we can actually go backwards if we make a collision.
So if we collide with a brick and it's of a higher tier than one
and it's a higher color than blue, it should be brought back one step.
But if it happens to be blue, in which case self.color gets one
because blue is one, then it should just be removed from play
just like we've done before.
Only now, we're also taking in tier and color.
So we're decrementing tier based on what index we're at
and we're decrementing color.
And then this actually gets used in our PlayState.
If we go to line 81, which previously just
had self.score gets self.score plus one, there's a little bit of math here.
It's very simple though.
Just brick.tier times 200.
So make the tiers worth 100.
Plus brick.color times 25.
And so if tier is zero, if it's a base then we're
just not going to get that 200 bonus.
But the first tier, everything is going to be worth
25 times whatever its color is.
So one, two, three, four, five.
And then add 200 plus the brick.color for when
we get to the next set of bricks.
And so the result of this is--
I believe this is GUI Breakout7.
And then if we hit a brick--
since this one is blue, it should disappear.
And we're playing a new sound as well.
New, like, death sound just to make it clear.
But notice they change colors.
So that's all we're doing.
We're just taking their tier or their color
and just performing a simple decrement on it.
Looping back.
In the event that we go down a tier, we should loop back up
to the highest color of the lower tier.
So I'll let you look at the code for that
if you want to sort of get a more low level understanding of it,
but that's the sort of high level understanding.
The next big concept that I'd like to introduce you guys to
is a particle system.
And so particle systems are fairly omnipresent in video games,
I would say, because they make effects that
or otherwise difficult to do with simple sprite
editing achievable very easily and realistically.
Just like fire, for example.
Things that are very organic, and flowy, and have a lot going on
are often better represented with particle systems
than they are with simple sprite animation.
So does anybody know how we might be able to--
how a particle system might work underneath the hood?
I think I alluded to it previously.
Yes.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Yeah.
So what he said was in order to make fire,
for example, just spawn a bunch of particles
close to the center of wherever your fire is spawning and then outside of it
spawn fewer.
That is absolutely a way to get fire to work,
and also taking into consideration the travel of your particles.
For example, you might spawn a ton of fire particles really densely,
but then maybe they have some logic that makes them go upwards.
Maybe they have a negative delta Y and then some sort of acceleration
so they've sort of trail off.
And then maybe sort of how to get a more realistic fire
look, they travel sort of upwards and then fade away.
So the way fire works, sort of thinking of things
in terms of particles like that, you can achieve a lot of effects.
How might we implement, like, smoke, for example?
Same system.
So we could have maybe a timer in our particle effect,
or even a transition because in particle systems,
often you have the ability to transition colors between particles.
Let's say you start off red, go to yellow,
and then maybe your particle system transitions to gray or brown.
And then over time, your particles are going up, they're dissipating.
And they're also turning dark, they're turning brown,
it sort of gives you the illusion of fire.
And we won't be doing anything necessarily as
complex as this in our code here, but in Breakout8, we
will be using Love's sort of integrated particle system which is just
love.graphics.newparticlesystem.
And it takes in a texture because all particle systems need some sort
of texture as their foundation.
And then it needs the number of particles that it could maximally emit.
And so each individual particle system can emit up
to a certain instance of particles.
And in the number, and speed, and whatnot of all
those particles is ultimately the determining factor
for how you can get an illusion.
Back to last week's lecture, illusions, like, it's not fire, it's not smoke,
it's just a bunch of particles responding
with colors and acceleration and stuff.
But there's a lot of functions that particle system gives you in Love2D,
so I encourage you to look at that link just to explore some of them.
Love2d.org/wiki/particlesystem.
We'll be using a few of them.
Here I'm going to just briefly show you.
So each individual brick when it gets hit
is going to need a particle system of its own.
Because our goal is--
I'll run the code for you so you can see it.
So if you go to Breakout8 and then you run it,
we have a little bit of particles you saw there at the very end.
The blue you were probably able to see a little bit better.
And then one last time.
So it spawns a bunch of little particles.
So can anyone tell me how they think the particles are
behaving sort of in a nutshell?
What the logic is for the particles?
AUDIENCE: [INAUDIBLE] slightly random.
COLTON OGDEN: Yeah.
Slightly random.
And if you look at it, you'll also notice that they tend to go downwards.
So knowing that, we can probably just assume
that they have an acceleration that tends towards positive Y.
And that's essentially all we really need to do.
We spawn a bunch of particles outwards and then just set them--
they have all a lifetime.
They last for a certain amount of time.
And then they fade between two colors.
In this case, we fade from red to transparent or whatever color it is.
And then after the lifetimes elapsed, it has the overall effect
of sort of this glimmering, gravity based effect,
but it's really just a bunch of particles that are
set to spawn in different directions.
Apologize for that.
So we'll take a look.
It's going to be in our brick class here in Breakout8.
So we're going to go to brick.
We have a bunch of colors that we're storing here.
So if you notice, the particle systems adopt the color of whatever brick
they're hitting just so that it stays sort of congruent
with what we're looking at.
So we're just storing a bunch of colors here.
And I wouldn't worry too much about this.
These are just colors from the sprite palette
that we used with our sprite art.
There's specific colors that are only used in that sprite.
And having a palette, generally speaking,
allows your art to look a little bit more cohesive
when you're doing sprite art as opposed to just picking colors willy nilly.
If you say, oh, I'm going to only use 16 or 32 colors for this palette,
you'll sort of have a more cohesive look and also
a very retro look because often hardware was
limited to a certain amount of colors back in the day for older systems.
So it's nice to--
as an aside-- and we'll look at it next week as well.
Looking at when you're doing your own sprite art, try to use fewer colors
and then that will give you-- it also makes it easier for you.
You don't have to spend time choosing I want to have this shade of green.
I wonder if it looks good.
If you only have two shades of green or semi shades of green to choose from,
that's all you've got.
You have to make do with it what you can.
So what we're doing here is we're storing five colors from our palette.
We're going to use this.
And then when we trigger our--
so right here we're initializing a particle system.
So psystem gets love.graphics.newparticlesystem.
And then these are a few functions.
So feel free to look in the wiki for how these functions actually behave.
But lifetime acceleration and area spread
just are sort of the properties that influence
the way our particle systems behave.
And so using whatever our current color is,
we're going to set our psystem's colors using setcolors function.
We're going to set it between two colors.
Color with 55 times tier alpha and color with zero alpha.
So the higher the tier, the brighter the particles,
but they'll always fade to zero alpha, if that makes sense.
And then we'll just emit 64.
And this is all in the hit function.
So all we've basically done is just add this particle system trigger in our hit
function, and it has the result of the behavior that we saw earlier.
So any questions on particle systems or how we use them?
So level 9 is the progression update.
So the purpose of this update is to allow us to go from level one
to two to three to four and start get more interesting level
generation that way.
The gist of this is in our--
so if you look at our StartState--
so all we need to really do to store a level is just to store a number.
And then where do we increment the number?
Or when do we increment the number I should say?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Exactly.
So we increment the level.
We go to the next level when all of the bricks
are in play have gotten there in play flag set to false.
So we have no pricks that are in play effectively.
So in our StartState--
so let's go ahead and look at Breakout9.
So StartState.
We're passing in level gets one here.
We're just going to start off.
When we're going to StartState, we're just going to pass level equals one.
And then henceforth, anytime we do any state changes from play to serve
and to victory, as we'll see, victory being our new,
oh, you cleared this level.
Here's the next level.
We're just going to pass the level between them.
And then in PlayState, the important bit of code here is on line 204.
So this is just a function called checkVictory,
which is exactly as James said.
We're going to iterate over the entire table and just say if it's in play,
return false because we're not in victory
if we have any bricks that are in play.
But return true if we didn't meet that condition.
And so this is just a simple way for us to check whether or not
we are in a victory.
And so on line 88 of the same file in our PlayState,
we're just checking to say, hey, if self.checkVictory after we do any brick
hit--
because that's when we've just set a brick to in play is false--
just check victory.
And if so, play a new sound like a happy sound
that we've done a victory, and then just pass everything
into the new VictoryState that we have here.
And the VictoryState is simply a sort of just a message state.
So all it does is just renders everything as before,
but it just says your current level complete.
Self.level complete.
And then press Enter to serve and it'll go back to the ServeState
as soon as that happens.
And then here is where the actual progression happens.
When we go to the ServeState, we have our level but we want to add one to it.
So all we need to do when we trigger a transition into our next state, just
increment level by one here, and also create
a new map because bricks needs to get restarted because we have a new level.
Self.level plus one.
And that'll have the effect of, oh, we've gone from level one to two
to three to four et cetera when we go between PlayState to the VictoryState
back to the ServeState.
So any questions on how any of this works?
Yes.
AUDIENCE: Do you have to worry about garbage collection for any
of the bricks at all?
Or is that handled by the Love engine somehow?
COLTON OGDEN: Garbage collection is handled by Love.
Yes.
Yeah.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Yes.
Because the question was, do you have to worry about garbage collection
when we are sort of clearing away the bricks and adding new bricks?
The self.bricks table, this table here, it's
getting assigned to a brand new table from levelmap.createmap.
When there are no references to an existing table,
lua's garbage collector will trigger at whatever
interval it's set to trigger and clear up all that for you dynamically.
Just like the same way that Java works.
Almost identical.
Any other questions?
All right.
So we have progression.
In the sake of speed, I won't demo.
It also takes a while just because we have to clear an entire level then
get to the next level.
But that's how the behavior works.
The next sort of iteration of this is high scores.
And I will test to make sure whether or not this is actually working.
I know I changed some stuff.
Yeah.
So high score.
Let's debug for a second.
So HighScoreState line 38 in Breakout10.
So HighScoreState.
And then the issue was [INAUDIBLE] to index field high scores.
A nil value.
OK.
So that means that--
OK.
I think I might know the issue, but it's because I
transitioned to a new user that doesn't have a saved file active on this.
The way that will transition, therefore, into love.file system, which
is Breakout10's main new thing that it introduces-- so writing files
to your file system is done [INAUDIBLE] with love.filesystem.
And there's a few things.
So Love automatically gives you a directory, a save directory
that's pretty much hard coded.
There are a few exceptions as to how to not use that directory,
but it assumes that you're always using that directory.
And with very few exceptions will you always use that folder.
It's like app data local on Windows, and application support,
and the name of your application on Mac.
But it's a subfolder that Love has read and write access to
for files on your file system.
You can check whether it exists with love.filesystem.exists at some path.
You can write to that path with some data, that data being a string value.
And then love.filesystem.lines is an iterator,
which will allow you to look over any of the data that's
in a file at a given location.
Yes.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Yeah.
AUDIENCE: Does this work if you [INAUDIBLE]
COLTON OGDEN: It should.
We can pull that up now actually and see.
Because I know on their Love2D--
so file system.
So the question was he ported his--
when you port your Love app to the iPhone,
will it have the same sort of behavior if you're--
on an iPhone, will it have the same sort of save directory behavior?
And it looks like it's not officially on here.
I know that there is an iOS port for Love2D,
or the ability to send it to Love2D.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: I have to imagine yes.
It probably has some sort of--
I'm not entirely familiar with how iOS handles sort of local storage,
but I'm assuming that just in the way that it's been abstracted for desktops
and for Android, it's also abstracted for iOS.
Haven't tested it myself.
I would experiment and see actually maybe with this code.
See if you can maybe get it working with persistent high scores.
I know that iOS does typically let you store a small amount of data per app
in some location, a fixed location, but I'm not
entirely sure what that is offhand.
I can look into it more and come up with a--
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Yeah.
I mean, not from firsthand because I don't have an Android,
but it has official Android support.
So I'm guessing it does, but I haven't tested it.
I have not tested it manually on Android to verify that.
But yes.
I believe-- because in the prior directory we were looking at when it
showed--
oh, it's actually up here.
This path here.
This data/user/0/love2d.android.
file save.
That looks to me like it's the official sort of path
that data is stored on an Android device for application.
So I haven't tested it myself.
But if you have an Android and you're curious or maybe an emulator,
give it a shot and see if it works.
Oh, and it even says here, there are various save locations.
And if they don't work, you can see what the actual location
is with this function here.
The love.filesystem.get save directory.
That may work on iOS as well, so I'd be curious to hear about
whether that actually works on that.
Yeah.
So that's the gist.
Using the love.filesystem abstraction lets us read and write files.
We can then just paste or we can just save whatever data
we want anywhere within that directory.
We can just create files in there and then use those to store our,
you know, sort of game worlds, or character profiles, or whatnot.
How would we maybe go about implementing sort of like a high score list?
So I'll look.
There's a picture here.
So we have 10 scores.
We'll assume that's fixed.
Each of the scores has a name, and then each of the scores has an actual score.
So all we really need to do is just store ultimately the names
and then the scores.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: So we'll use an array.
Their response was we'll use an array as sorted by that score.
Yeah.
Essentially that's exactly it.
We're just going to keep a score table and each table
is going to have a sub table.
And each of those entries, one through ten,
is going to have a name and a score.
And then once we're done with our application,
we'll just use love.filesystem.write.
We'll have to convert all of those into a string
because we can't just take a table and then spit that out into a file.
We have to actually make it into some form that we can save
and then reload back in somehow.
What would be the most efficient way, do you think, or a way we can do this?
Probably just a new line separated list.
The way that I've done it in this application
is just names, and then new line, score, new line, name, new line, score.
10, so 20 rows.
And that gets the job done.
Assuming that you don't tamper with the file, then everything should work.
And you can write additional code as well
to say, oh, if there is a score that's all
garbled, we don't have enough scores, then probably
should render it accordingly.
My code does something similar to this, but not entirely.
The relevant code-- and I'm going to sort of just glaze over it.
If we're looking at-- this is Breakout11, right?
Yeah.
Oh, no, this is Breakout10.
So in Breakout10, we have to load all the high scores
in main.lua, which is here.
So set identity to Breakout or create a folder called Breakout
that we can save and read files to and from.
If it doesn't exist, then just create them.
In this case, I'm just seeding CTO my initials.
And then I times 1,000.
So 10,000 down to 1,000.
Just very simple data.
Writing into a file called breakout.lst.
It can be whatever you want.
All we're doing is reading lines from the data, or from the file.
And then this is if it doesn't exist.
And then if it does exist, then we're going to iterate over it
with love.filesystem.line, which will take a file
and then just split it on new lines basically and give you
an iterator over all those lines.
So it can just say, OK, if it's a name, which
means that if it's one or three or five or seven in the list,
then set the name to--
and we're using string.sub just in case they
write some long name or some long name gets-- they can't do it
through our game, but if it gets written to the file as some long name,
it should get truncated to three characters
so we can display it appropriately.
And then otherwise if we're not on a name line,
if we're on, like, an odd line or even line,
we should consider that a score and just use to number.
Because we're using string data and if we try to assign,
do any sort of comparisons numerically on the string data,
which we will have to do to compare high scores,
it's not going to work because it's going to see that there's strings.
So we use to number here.
Just a simple Lua function.
And then that's it.
And then we just return scores.
And then I'll sort out what's causing the issue,
and then push that to the repo ASAP.
But that has the effect of us being able to actually load all of our high scores
and display them at the start of the game.
It doesn't take care of being able to actually input our score.
And so we can do this with Breakout11, which you can see if you run the repo.
And you can test just to assign your initial score to some value like 10,000
or 20,000, and then just lose on purpose and you
get a sense of how it actually works.
But essentially, it's just arcade style.
You know, you had only three characters you could input your name.
So does anybody have any idea as to how we are sort of storing this,
or can pitch an idea?
So we have three characters and we want to--
ideally if we're, let's say I want to go to C on the first one.
Let's say I pressed up twice so I get to C. How is it going from A to C?
You could just say, you could just render
I want to render the character A, the character A, the character A,
but how is it going to know when you want to go to B, or C, or D.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: The pitch was, you could create
a table with all of the characters and iterate through it.
You absolutely could do that.
It's a little bit bulky.
That might be what--
actually, that's probably not how arcade systems did it back in the day.
Because the way that we're going to do it here in Breakout11 is I
added a new state called EnterHighScoreState.
And if you recall, CS50 teaches this.
But all sort of characters at the end of the day are just numbers.
ASCI.
In this case, 65, if you recall, is capital A.
So all we need to do is just draw out whatever that character
cast to a string is, or character.
And we do that simply down here in the draw function.
If we do string.char, at char is three.
All that has the effect of doing is just taking that number
and then converting it to a character.
So all we need to do then is what?
When we want to go from A to B, B to C, C to D.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Exactly.
But then what happens if we're at A and we want to go down?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: We would.
So if we're at A, then if we press downward and we want to go to Z,
the logic is in here.
But one we've incremented our code, if it's greater than 90, which is Z,
then we should set it back to 65.
We'll loop back to A.
And same thing here.
If we press down and we're at A, we've got to go back up to Z,
so we just set it to 90.
So simple loop back logic.
And we just draw it, we highlight.
And then once we've done that, the user presses Enter.
We transition to the HighScoreState, actually,
because this state should only trigger if they entered a new high score.
Which means that we need to check in the VictoryState, or not the VictoryState,
but rather in the GameOverState whether or not
their score is higher than any of the stores in some sort of,
quote unquote, global scores table.
And then how do we think we're passing the scores back and forth now?
Does anybody recall how we're keeping track of app state?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Yep.
In the change function.
So all we need to do is keep track of-- load
our high scores at the beginning of the game,
pass them all the way down the line.
And then finally-- and we can also load them in our EnterHighScoreState,
but we need to keep track of what our high scores are in the GameOverState
so that we know, oh, I've got a high score.
Let's instead of transitioning back to the StartState, let's transition
to the EnterHighScoreState so the user can add their high score to the list.
And then once they've entered their high score, which is here,
we'll just write it to this file again.
Compile a score string, which takes name and score of our scores.
We take whatever score that we were at that's--
we look through our scores table backwards
and when we find a score that's lower than ours,
we just keep track of that index until we get to one that's higher than ours.
In which case the one plus one, that index plus one
is what we should then overwrite.
And so we shift all the other ones below accordingly.
And we do that in this class if curious.
And so I'm just going to breeze through the last couple.
The paddle select update is just kind of a fluffy state
that lets us add a element of sort of, like, user selection to our game.
In our PaddleSelectState here, we transition immediately.
Instead of going to the [INAUDIBLE] PlayState now,
we're going to go from Start to Paddle Select when we hit Start Game.
So we're going to go to--
and then the Paddle Select class itself.
CurrentPaddle gets one, and then all it essentially
is is us drawing two arrows here.
And so if we're at number one-- in this case, I think we're at number three--
then both of these arrows will be completely opaque.
But if we're on the left or the right edge, they should darken to say to us,
oh, we can't move left or right anymore because we're
at either index one or four or five, and there's only that many colors.
And then render whatever that color variable
is using the quads table that we had before of the different tables.
And then just instructions.
And then from there is where we'll end up transitioning to the ServeState
rather than going to the ServeState from the StartState.
And all the code in that is here.
We have sound effects playing.
And then making sure that we also play a different sound
effect based upon whether they're at the left or the right edge.
If they're on the left edge and they try to go left,
it should play like a sound that sort of sounds a little rougher
to let them know that they can't go left,
and the opposite for the right edge.
And then once that's all done, once they press Enter on whatever paddle
they want, they're going to get the paddle,
we're going to instantiate a paddle, pass that into the ServeState,
and we're going to take currentPaddle from the state, which
is whatever value they got by scrolling between all the different paddles.
And then the last update, which is my favorite part of most every lecture
I think is the music update.
And all that really is is just music set play in main.lua,
and then set looping to true, and then we have a game.
And this is our Paddle Select.
So notice the arrows are semi-opaque on the left and the right.
It's kind of hard to hear, but when I press right now
it's kind of like there's a bit of a rougher sound.
We choose red.
We go to level one and we transition to the ServeState
from the PaddleSelectState, and then we just play the game as normal.
And that's basically all there is to it.
And there is a couple of features we didn't have time to really go over
like making sure we recover HP if a certain amount of points
have been elapsed, but I encourage you to look
into that when you trigger a hit.
There's some logic in the PlayState to say, oh,
if they've gone over a current recovery threshold,
let's add one heart to the player, you know, just keep them playing.
Just to reward them for their high score.
Next time we'll cover a few concepts.
So basic shaders.
Shaders are like little programs you can run in your graphics card
and do fancy effects, but we won't go into too much detail.
Anonymous functions.
We've seen a lot of anonymous functions in Lua in the context of Love.
They're just functions without a name, and you can just
use them as function arguments and do all sorts of cool stuff with them.
We'll use them for callbacks next week when we do things
like tweening, which is taking some value
and making it interpolate over time to some other thing.
Because right now we've basically just been updating things based on velocity,
but we haven't really done anything based on time.
So we'll take a look at that in more detail
next week with a library called timer, which is really fantastic.
Lets you time things and then chain things together.
We'll be covering the game Match Three if familiar.
It's basically Candy Crush.
We'll be using a different tile set, but it's the same idea.
And we'll have to calculate how to actually find out
whether we've gotten a match in the grid, our tile grid,
and then shift the blocks accordingly and do all the other logic, add score.
And then basically since it's so fundamental to Candy Crush and games
of its nature, we will have to cover how to sort of generate
these maps procedurally to have tiles that are laid out in a dynamic way,
and also in a way that doesn't start off with any matches
because then that wouldn't make any sense because the matches have
to resolve.
And then we'll take a little time if we have the time next week
to talk about sprite art again and palettes.
And maybe I'll show you guys how to sort of convert images from one
palette to another in, like, a program that I use, Aseprite,
but you can do this in any sort of large photo editing software.
And then assignment two is a couple of extensions to Breakout.
So if you noticed in the sheet there were a few little sprites here
at the bottom--
so get rid of the quad outlines.
So these little things down here are, I'm assuming,
they're meant to be power ups.
They look like power ups.
But the goal of the pset is to implement a power up.
And a power up is going to be such that when you grab it,
you'll get two additional balls, or however many you want actually,
that will spawn in addition to your one and detect collisions on their own.
So you'll have several and they'll score points for you.
And, of course, only when the last ball comes below the surface of the screen
should you trigger a Game Over.
And then I want you to add-- and this will also
be more detailed than the spec-- but I would
like you to add growing and shrinking to the paddle.
So currently, we have like four different sizes of paddle,
but we're not using them.
So it would be nice if when we gain enough points or we lose points, or not
points, but lives rather, we increase or decrease the size of the paddle
accordingly just to introduce another level of challenge
and or lack of challenge.
And then finally, one last part which is in the sprite sheet as well,
there's a key block here and a key power up here.
So sort of let the power up come, pick the power up with your paddle.
And then only when you have that power up should you
be able to break the block with a key.
And you should take this into consideration
when generating your levels as well.
So you'll have to also get your hands dirty with the level maker.
But all in all, that was Breakout.
So I'll see you guys next time.
Thank you.