[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.
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.
