COLTON OGDEN: All right.
So with Pong 4, we talked
about random number generation
and seeding the random number generator.
We allowed our paddles to not move
beyond the top and the bottom edges
of the screen, and we applied a velocity
to our ball in the Update function,
which we talked about, to allow the
ball to move sort of randomly on its own
without any input.
And in addition, we also
talked about game states.
So having a Start state
and then a Play state,
so that we can have transitions
between different functional aspects
of our game.
Now, this update really
doesn't introduce
a lot of new visual difference.
You can see the screen looks
exactly the same, more or less.
The real big difference
here is we're going
to look at refactoring
our code base and talking
about object-oriented programming.
Because currently, the source code
is getting a little bit unwieldy.
We have a bunch of variables that are
tied to different sort of conceptual
objects in our scene--
the paddles and the ball--
but everything kind of feels like
it's out there in the ether, right?
Nothing is really compartmentalized
in a way that feels ideal.
So with this update, we're really
looking at just refactoring the code.
And in order to do that, the first
thing that we really need to talk about
is oriented programming.
And the first thing about
object-oriented programming,
the most foundational
piece, is the class.
So what is a class?
Well, so far you've been
dealing with variables.
You know, ints, strings,
chars, that sort of thing.
Very simple pieces of data
that you can group together.
And likely you've talked about structs.
And structs are very similar to
objects in most programming languages
in that they compartmentalize data in
a way that's semantically meaningful.
With classes, not only do you get
data that you can put together,
but also functions that are tied
to a group of data, essentially.
And you can kind of
look at it like this,
where you have a class diagram,
like a car blueprint, more or less,
where you say, a car
should have attributes,
like how much fuel is in its
tank, what its max speed is.
And some methods, like, for example,
to refuel for an individual car.
To set the speed of that car.
Maybe set it so that it's
driving at 60 miles an hour.
Get the speed of the car.
What is that specific
car's individual speed?
And then of course drive, which
means to go in the direction
once you have set the speed.
Now, this is all sort
of a conceptual look
at what a car should have intrinsically,
but it's not a specific car.
A car, once it's out
of the factory and it's
been manufactured using this class,
this blueprint, it becomes an object.
And classes and objects
are two terms that you'll
hear very frequently in the context
of OOP, object-oriented programming.
And in fact, the name is
object-oriented programming, not
class-oriented programming.
But classes and objects are
inextricably tied together.
The class is the blueprint, the object
is what actually comes off the assembly
line and functions within your code.
And you can have, by virtue
of that, many objects
that are derived from this blueprint.
You can have car 1, car 2,
car 3, where they each have
their own fuel, their own max speed,
their own color, their own make,
their own speed at that point in time.
And that is the beauty of it.
You can spawn six cars,
six paddles, but you only
need to have the code that actually
specifies what the object should
have one time in the class.
Therefore, we sort of
de-duplicate this information.
We only have one definition for
it as opposed to, let's say,
paddle 1's Y, paddle 2's Y, et cetera.
Now we just have to
specify what should a ball,
what should a paddle,
have as attributes?
So just some bullets
that I put together.
You can see blueprints
for what a class is.
They are a bundle of data and code.
And all of this sort of describes
essentially what I just talked about.
And we're going to take the concept
of the class and the object,
and we're going to tie it
into the paddles and the ball
specifically for this application.
So that's it for the slides.
Not a whole lot of
code and new functions
and that sort of thing that
we're going to talk about.
But we are going to
dive into refactoring,
and we're going to
specifically use a library.
So we're going to use this class
library by Matthias Richter.
It's a very simple way to allow us to
do object-oriented programming in Lua.
And it's possible normally
without this library,
but there's a lot of
ceremony and overhead
as opposed to using this
library that I thought
was pedagogically
meaningful for us to instead
focus on what it means to use classes
and objects, use a simpler library.
Let's not get involved
with the nitty-gritty way
that Lua at the lowest level
does object-oriented programming.
You can always look
into that if curious,
or even explore the class
library, but instead we're
going to use this class library.
And it's a really
clean, simple approach.
So the very first thing
that I want to do,
I'm actually going to create a new file.
I'm going to call this Paddle.lua.
And notice that I capitalized the
P. In object-oriented programming,
it's very much the norm,
and expected, that you
should capitalize the name of classes.
In this case, we're defining a class.
It's going to be called Paddle, so
make sure that it's capitalized.
I'm going to create another file,
and this one's going to be Ball.lua.
So first thing that we're going to
do, I'm going to work on the paddle
first and then the ball.
So I'm going to say paddle is equal to
class, with these curly brackets there.
And this is just the way the
class library expects to work.
What this essentially does is it
takes the class library from the--
first of all, we're going to
need to import that library.
But it takes that library and
it generates a new paddle object
that's going to have methods and
fields that we can describe ourselves
following this declaration.
Now in order to get
this to actually work,
you don't normally get
class working out the gate.
You obviously need this--
it's not a keyword that exists in Lua.
You need to import the library.
We're going to go into
main.lua, and right
where we have Push,
just alphabetically, I'm
going to say class equals require class.
Just like that.
And so now, what we'll
eventually end up doing
is saying require
paddle and require ball.
And because of the order in which we're
requiring these things, because class
is coming first,
followed by push, require
ball and paddle are going to have
access to this class variable.
So they are global variables.
And for what it's
worth, too, any variable
that you declare without
specifying the local keyword--
local class, for example,
equals require class--
is accessible anywhere
in your application.
But local just means that
it is local to that scope.
And you've probably talked about scope
in CS50 in the context of C and Python.
We haven't used local yet.
We will eventually get into doing that.
But for right now, any
variable without local
just means it can be accessed anywhere.
Which is why class is accessed
through the Paddle file,
even though it's separate
from the main.lua file.
So I'm going to go in here.
And I'm going to say each paddle--
or actually, rather, any
class that you define
needs to have a function
that creates the object, that
actually does the work of
assigning the right fields
and getting it ready to use.
So we need to define a function called
init, specifically, Paddle:init.
And this colon, again,
is very important.
The colon is what allows
objects to call methods.
So so far you've seen dot being
accessible and colon being used.
And colon just means
that if an object exists,
it will use a function that works
specifically on that object.
We've used dot.
You don't need to instantiate an object.
You can use a dot method
anywhere in your application.
Indeed, you can see that all of the love
methods, for example, are dot methods.
You don't have to create a
love object to use love.load.
It just is a function that exists
somewhere in a module called love,
specifically a table called love.
But if we're dealing
with objects, and we
want every object to call init
on itself and use its own data,
you need the colon.
It's very important.
So for the paddle, I'm going to say that
I want a couple of initializing fields.
I want an X, I want a Y, I want
a width, and I want a height.
So this will allow us to specify in our
main.lua, when we create a new paddle,
we give it those four fields.
And then it will take care
of actually storing that
here instead of having to store those
different fields in our main.lua file.
So we'll say Paddle:init.
And then this is another
very important thing
to know about
object-oriented programming.
You need this self keyword.
The self means this
particular object that we're
instantiating inside main.lua,
we're going to use itself.
We're going to refer to itself.
Self dot something means
it belongs to that object.
So if we create multiple
paddles, self, when
in the context of those
individual paddles,
will mean whatever that
paddle is, as opposed
to some global variable somewhere.
So self.x is going to be equal to
X, self.y is going to be equal to Y,
self.width is going
to be equal to width,
and self.height is going
to be equal to height.
So it's going to store
those values internally.
Now another thing that
we're going to need to do
is we're going to need
to update our paddle.
So in our main.lua, so far we've been
taking care of all that work ourselves.
Instead, we're going to defer
just a little bit of that
to our actual Paddle class.
So what we can do, again,
we're going to need access
to however much time has
passed since the last frame.
We can pass that from main.lua
down to our Paddle class
by just specifying this function.
Just take dt and making sure in
main.lua we feed dt into Paddle:update.
So what I'm going to do is I'm
going to say if a self.dy--
and that's another thing we're going to
need to also take care of, is self.dy.
So normally there is no velocity
unless we're pressing a key, right?
Dy is a variable, a value that we're
going to keep track of, that's going
to work inside of Paddle:update.
So Paddle:update will take care of
changing our actual Y value with
regards to dy.
So if dy is less than 0, then
what we're going to do-- remember,
if it's less than 0,
then that means it's
going towards the top of the screen.
And if it's positive, it's going
towards the bottom of the screen.
The y-axis is inverted from
what is commonly the case.
And we're going to say also
if dy is greater than 0--
because it can be the case that
dy is literally 0, which means
that we should not do anything, really.
We shouldn't move the paddle at all.
We're going to essentially copy
the code that's in main.lua.
So if I go over to main.lua
in our update function here,
you can see that we
have this player1Y is
equal to math.max of 0, and then
player1Y plus negative paddle speed
times delta time, we're
essentially going to have this.
We're going to have--
well, it's going to be
called paddle1, and then it's
going to be colon, update.
And then paddle2:update.
But we want to take the actual
functionality here in main.lua
and put that into the Paddle class.
So I'm just going to
copy this line of code.
I'm going to bring it
over here into Paddle.lua.
And I'm just going to paste it in there.
And same thing for the other case.
So I'm going to copy this,
bring this over into Paddle.lua.
And now we no longer have
this reference to player1Y.
It's instead just self,
specifically, self.y in this case.
So I'm going to do this.
I'm going to Command-D,
if you're in VS Code,
to select multiple occurrences
of the same variable.
I'm going to say self.y, just like that.
And no longer is it negative paddle
speed, or rather just paddle speed,
it's instead selft.dy.
So we'll just do self.dy, like that.
Now of course, we have to
set the dy, and for that we
need to check whether the keyboard
buttons are being pressed.
And we can do that from
within main.lua or in here.
We're going to do that in main.lua.
But we're going to go ahead and go back
here, and I can actually just do this.
We'll say paddle1.dy is equal
to negative paddle speed.
And then here we'll say paddle1.dy
is equal to paddle speed.
And then we'll say if it's the
case that none of those are true,
we'll say paddle1.dy is equal to 0.
Simple.
And we could do this
also in the Paddle class,
but we'll keep it in
here for simplicity.
And so same thing here.
Instead of using
player2Y, we're just going
to say paddle2.dy is equal
to negative paddle speed.
And here we're going to say
paddle2.dy is equal to paddle speed.
And then else, paddle2.dy is equal to 0.
So pretty simple,
pretty straightforward.
And then the last thing,
too, is we want to defer--
normally we've been rendering
our paddles inside of main.lua.
Inside of, yeah, inside
of main.lua here, just
explicitly by drawing rectangles.
Instead, what I would like to do is
say paddle1:render, and paddle2:render,
like this.
So what I can do is I'm just going to
copy this call as I had previously.
And we're going to define a new
function called render, just like this.
I'm going to paste that in there.
And now we're no longer using
hardcoded 10, player1Y, 5 and 20.
We're instead going to say self.x.
We're going to say self.y, self.width,
and self.height, just like so.
And remember, we're storing a
reference to these variables
through here from the init method.
We construct this in
main.lua, it gets stored,
and then you can reference
it in the later functions.
So for that, I can
actually get rid of this.
And now we have those being
stored just like that.
And just doing a quick look here,
make sure everything looks OK,
which I think it does.
And I think we're just about
ready to test to make sure
this is running smoothly.
So let's go ahead.
And global paddle 1 is not set.
So let me make sure-- oh, I actually
didn't instantiate the paddles.
Very important step.
So what I'm going to do is, right where
we have this player1Y and player2Y,
I'm instead going to
instantiate the paddles.
So let's say paddle1
is equal to a Paddle.
So I'm constructing it by just calling
the actual word Paddle, like we defined
here at the top of our Paddle class.
And I'm not calling the
init method explicitly,
but when you use these parentheses
here in what's called the constructor,
init gets called through the
class library sort of implicitly.
And this is syntax that you would see
in most programming languages that
are object-oriented, Python
among them, Java among them.
So I want to specify the different
attributes for the paddle.
So remember, it takes an XY
within a height, so we'll say 30,
and the Y will be also 30.
Actually, I think for
the X I had it set to 5.
So we'll say 5, and then actually 20.
And then we'll say it should be 5, 20.
So actually uniform dimensions there.
And then for paddle 2, its X is
going to be virtual width minus 20--
or minus 10, actually.
Virtual height minus 30.
And we'll say 5, 20, just like that.
So if I do this again, it's white.
So again I need to
reset the colors here.
So 40 divided by 255.
255, 255, and 255.
Save it, run it, boom.
There we go.
So I can indeed move--
oh it looks like I
have the keyboard-- it
looks like I messed up the
keyboard shortcuts there.
So let's go back over to
the update function here.
So if keyboard is up--
oh, I see.
Wait.
IsDown w, paddle1.dy
and then paddle2.dy.
So paddle 1 is set to 5, 20, 5, 20.
And then virtual width minus 10.
Virtual height minus 35, 20.
So let's go ahead and
see what's going on here.
So paddle1.dy is equal
to negative paddle speed.
Paddle2.dy is equal to paddle speed.
Then paddle gets set to--
oh I realized I'm looking
at Paddle right now,
and I used negative self.dy
when I added the velocity
to self.y if it's less than 0.
But actually, we should always be
just adding self.dy in this case.
And it will be negative depending
on how we've set it earlier.
So I'm just going to set
that to plus self.dy.
and let's run this just
to make sure it works.
And it works.
OK.
So I'm moving up and down.
Everything looks like
it's working great.
So, awesome.
Paddles work just like they did
before, but now they're in a class
as opposed to just being variables that
we're storing inside of our main.lua.
So now let's take a look at the ball.
So let's say I've got
the thing going on there.
Go to Ball.
So it's just going to behave in
a very similar way to the paddle.
So I'm just going to say Ball is
equal to class, with curly brackets.
I'm going to say that there should
be an init function that should
take in a XY width and height as well.
I'm going to define an
update and a render function.
And for the init, I'm just going to do
much like we did with the other class.
And these are similar enough such that
you would almost think they could be--
and this is more of an advanced topic.
But there's this thing called
inheritance, where you can actually
have a base class that you can
spawn children classes that
take all the attributes of the former
class and add some customized behavior.
And they're similar in
this case because they're
both rectangles, or quadrilaterals.
But we're not going to do that.
It's little bit too much legwork.
But you can absolutely
do that, depending
on the use case of your application.
And so again, we also are going to
need to have a self.dy, but also
a self.dx in this case.
And actually, we're
going to go back to main,
and we can just take these four
lines here with the comments.
I'm just going to delete them, because
we don't really need them anymore.
Because this is all going to get taken
care of inside of the Ball class.
So let's go ahead and I'm actually
just going to delete them altogether.
And we're going to say
that this is self.x.
And actually, I'm realizing,
oh, actually, that's fine.
We'll leave that.
We'll just say that.
And we'll do self.dy, dx, sorry,
is equal to math.random of 2
is equal to 1 and 100 or negative 100.
Again, ternary expression in that case.
Randomly between 100 or negative 100.
And the self.dy to initialize
the velocity there as well.
And I'm going to also create a function
called ball reset, because you'll
notice in main.lua, whenever we go
over to the restarting of the state
from play to start, we get
the ball going right back
in the center of the screen.
And it's kind of cumbersome to call
those four lines over and over again.
So what I'm going to do, is
I'm going to copy these lines
and just get rid of them.
And instead, I'm just going to
call ball reset, just like that.
And I'm going to copy those
over here into ball reset.
I'm going to change ball
x to self.x and self.y.
And then I'm going to call
this self.dx and self.dy.
Not xy, dy.
Just like that.
So now the ball can reset pretty easily.
And it's going to get set right
into the center of the screen.
So that's great.
And lastly, we also do
need to render the ball.
And update it, actually.
So this is simple enough.
Let's go over to main.
Let's grab the render
function from here.
So I'm just going to grab this.
Again, replace it with ball render.
And we're going to need to
instantiate a ball up here as well.
So I might as well just do that.
Where do I have that set up?
That is right here.
So we'll just say ball is
going to be equal to ball.
And the xy is going to be virtual
width divided by 2 minus 2.
Virtual height divided by 2 minus 2.
Five and five, cool.
So that instantiates the ball.
Let's copy the render
function over here.
And this, it is going to be set to fill.
We're going to say self.x self.y.
And not for four actually 4,4.
Yes, that works fine.
And then that should be it.
That should be good for that.
Let's run this, make sure it works.
And it does work.
So the ball is right in
the center of the screen.
Now, I hit Enter and
we change the state,
we notice that we don't have this
thing called ball.dx anymore in main,
because we haven't actually transitioned
the movement, the update portion
of our code into the ball class.
So what I'm going to do, is I'm going
to say, I'm going to copy these lines.
And I'm going to say ball
update of delta time.
I'm going to go back over
the ball and in update,
just going to paste those in.
Now it's not ball x and ball y anymore.
It's self.x.
So I'm going to copy that, self.x.
And it's not ball.dx, it's self.dx.
And we'll do the same
exact thing for ball y.
So self.y.
And this becomes ball or self.y.
rather.
Save that, run it,
start, and we do indeed
have the ball moving through the screen.
I can press Enter, and it restarts,
goes in random directions.
So everything really is
working the way that it
used to be working, except now we've
cleaned up our main function just
a little bit.
I mean, it's still quite long.
But instead of having a
bunch of lines of code
that do a bunch of different things
that are very sort of specific,
now we just say paddle 1
render, paddle 2 render.
Ball render.
It's much more readable.
In addition to cleaning up
the code in terms of volume,
we've cleaned it up in
terms of readability.
And that's arguably more important even.
So that's it for the
sort of the class update.
So in the next update,
pong7, we're actually
going to get to some interesting stuff.
Oh, actually, sorry, pong6, I
had my slides messed up there.
Pong6 is a very small update.
In this case, we're
going to talk about FPS.
So we're going to have a
little bit of code that
tells us at the very top
left, what our frame rate is.
Because this is important.
If you played a lot of games that
have a debug mode or the like,
you will see that they, it is
important to monitor your FPS
so you can make sure that your
computer can run your game.
Now, of course, this
game is very simple.
So this is a little bit less germane.
But this might be useful
for your own project.
So we'll take a little bit of
a breather from the hard stuff
and focus on the FPS update with pong6.
So see you then.
