COLTON OGDEN: In Pong 3, we
looked at how to move the paddles,
so we talked about delta
time in particular,
which is probably one of the most
important concepts with love.update
in order to actually get a functional
game loop with behavior that
depends on user input and AI.
In this example, we're
going to look at the ball.
And on the ball, we're actually going
to use random numbers to give it
a random velocity, and we're going
to explore game state a little bit,
as well.
So this screenshot here
kind of exemplifies
the latter point, which is state.
So far, we've mentioned it
a little bit-- game state--
and that just means
that whether the game
is in the beginning, where
nobody's pressing buttons
and is waiting for the first user
input, whether we're playing the game
and the ball is in motion.
Maybe the ball has gone
past one of the bounds
and so player one scored, and
it's players two's turn to serve,
so then we go into the serve state.
Or maybe player two scored
10 points on player one,
and so now it's time to get
into the game over state,
where the game is finished.
In this case, we're just going
to implement the start state,
so it'll wait for user
input to press Enter,
and then we'll go into the play state,
where the ball will move independently.
And in both of those
states, we'll actually
allow the paddles to move, as well.
Some important functions for pong4.
So math.randomseed is very important.
So this essentially seeds
the random number generator.
So random numbers are not really
random in the technical sense.
They are what's called pseudorandom.
So there's a function
that uses some fancy math
and ends up along a fairly
normal distribution,
calculates or creates some numbers
that appear to us as random,
but to the computer, they
are very much not random.
It's impossible, actually, to
have just truly random numbers
generated from a computer.
You can use atmospheric
noise and other ways
to create much more
actually random numbers,
but even that's probably
not technically random.
But that feeds into math.random
the function down below.
Now, the thing about what I just said
is that if we use the random number
generator without what's called seeding
the random number generator, which
means give it an initial starting point
that will make it different every time,
then every time we run our game,
because it's just an algorithm that's
essentially calculating
random numbers, it's
going to actually be the same random
numbers every single time, which
means our application's going
to run the exact same way,
even though we're saying that we should
have random numbers being generated.
So math.randomseed-- we need to
give that a number that is different
every time we run our application.
And to do that, we're going
to use what's called os.time.
So os.time is an interesting function
because it gives us the time in seconds
since basically 00:00:00
UTC, January 1, 1970.
This is Unix epoch time.
This is a very famous
thing that had to do
with the beginning of the Unix operating
system, or its creation, back in 1970.
And this is a very large number.
It's different every single
time you run your application.
It actually might be in milliseconds.
It might be in seconds.
But it's a very big number, and
it's different every single time
you run your application.
A couple of other functions--
math.min and math.max.
So the former gives you just
the smaller of two numbers,
and the latter gives you
the larger of two numbers.
And this is important,
as we're going to see,
in clamping our paddles such
that they don't actually
go above the top of the screen and
below the bottom of the screen, which
was an issue that we realized
that we had last time.
So that is it for pong4's slides.
I'm going to go over
to my text editor here,
and we're going to start
implementing some of these features.
So why don't we pluck off
the first easier thing,
which is making sure that
the paddles don't actually
go above and below the
edges of the screen.
Like I said, we can use
math.min and math.max here
to give us the lesser and
greater of two numbers.
Well, let's take going up as an example.
So player one and player
two-- if they're going up,
I want to make sure that their y
velocity-- their y values, rather,
doesn't actually go above 0, so into
the negatives, because remember,
everything is drawn relative
to its top-left corner.
So if we make sure that y
can't be any less than 0,
well, then it won't go any less
than the very top of the screen.
So what we can do is we can
say, give us math.max(0,
player1Y + -PADDLE SPEED * dt).
And we can do that here, as well,
if we say math.max for player 2 of 0
and the addition of
negative paddle speed.
If I run this just as a test--
oop.
I need to also make sure that my
clear is set to divided by 255.
255, divided by 255, and divided by 55.
If you see this in any Lua code
that you're experiencing thus far,
it's because the older version
of Lua had regular RGB values
and the newer version has
floating-point values.
So by dividing by 255,
remember you can stop that.
So if I go up and I'm trying
to press up right now.
I'm holding down, and it's not going
above the top edge of the screen, which
means that it is clamping the
y value to be no less than 0,
which is what we want.
And we could do it for
the left side, as well,
and we'll test that
on the next iteration.
So I'm going to go
back up here and we're
going to do the opposite of math.max,
so we're going to say math.min.
So what we essentially
want is for our y value
to be no greater than the bottom
of the screen minus the size
of the paddle, which is 20 pixels.
So I can say give us the
lesser of the VIRTUAL_HEIGHT
- 20 and player1Y with an
added PADDLE_SPEED times dt.
I'll do that down here, as well.
We'll say math.min(VIRTUAL HEIGHT -
20, player2Y + PADDLE SPEED * dt).
If I save that and I run it,
and I try to go to the bottom,
you'll see that, again, I
can't go to the bottom now.
And the same behavior on the left
side of the screen-- the paddles
are trying to move beyond the
edges and they are indeed clamped.
So you've successfully
solved that issue.
Now, the next thing that
I would like to get to
is getting our ball moving on its own.
So far, what we've been doing is
we've been waiting for user input.
When we are holding the
keyboard down, a specific key--
Up, W, S, and Down--
then we essentially add PADDLE_SPEED
times dt to our y value--
negative or positive,
depending on the key.
What we want to do is essentially
decide on a velocity for our ball.
And in Pong, it was
actually common for the ball
to have different velocities
depending on how you hit the ball,
and also randomly.
And this is where math.random
ends up coming in.
And remember, the very first
thing that we need to do
is actually seed our
random number generator.
So we do this with math.randomseed.
And again, we need a
value to actually seed it.
We need to actually give it
a starting value upon which
to base the random number generation.
So to do that, we just give it os.time.
This, in and of itself, isn't
going to actually do anything.
This really is tied to the
use of math.random, which
we're going to start using, as well.
So I'm going to give us
some starting values here.
So why don't we, first of all, decide
on keeping some values for the ball's x
and y position?
So currently, I'm going to go to
where the ball is being rendered.
And we see here now we
have some hardcoded values.
We have VIRTUAL_WIDTH / 2 - 2 and
VIRTUAL_HEIGHT / 2 - 2, which again,
shifts the ball a little bit up and
to the left relative to the center
of the screen, because
the ball-- remember,
everything gets drawn relative
to its top-left corner.
So what I'm going to do is
I'm just going to delete this.
I'm going to say the ballX and ballY.
And we're going to
define those ourselves.
So we don't actually have
those as variables yet.
So what I'm going to say is
ballX = VIRTUAL_WIDTH / 2
- 2, and ballY = VIRTUAL_HEIGHT / 2 - 2.
If I rerun this, it again looks like
it's running exactly as it should.
The ball is still being rendered
right in the middle of the screen.
Now, in addition to the x and
the y, we need a velocity.
We need our ball to
essentially update every frame
and add some amount to the x
and the y, which is actually
going to allow it to move across
the screen, a velocity value, which
we've been sort of setting ourselves
with keyboard input on the paddles.
And for this, we're going
to set it here up above.
So we'll say ballDX--
D is short for delta of X.
So we're going to say ballDX,
and let's just say I
want it to be random.
Let's say maybe I want it to go
left sometimes and right sometimes.
And let's just say
100 pixels per second.
So I want to choose randomly
between one or the other.
So what I can do is I
can say math.random(2).
And what this is going
to do is essentially
flip a coin, so it's going to us a
value between 0 and 2, noninclusive.
And actually, no, I believe it's
going to give us either one or two.
So what I can say is
math.random(2) == 1--
this is just going to be
a coin flip at this point.
This is going to be one or the other.
It could be 1, it could be 0, or 2.
But what I'm going to do is I'm going
to say math.random(2) == 1 and -100
or 100.
Now, this is a weird thing
about Lua, because you
might have been familiar or expecting
me to use the ternary operator, which
is this.
So in C and JavaScript, you can do this
thing called ? something:something,
and it takes a Boolean
expression at the very beginning.
So what this will do is if this
is equal to 1, this will be true.
We'll get this value.
And if this is false,
we'll get this value.
But in Lua, we actually
don't have this syntax.
We have "and" and "or."
The way that it works is
that "and" will essentially
take whatever first false value it
can find, or if not, the second value.
And if it finds a false value, it's
going to then calculate the "or",
so it'll have this or 100.
And "or" operates in such
that it takes the first truthy
value that it finds, actually.
So in that case, it'll take the 100.
So by virtue of the way that
those two logical operators
work, the construction of "and"
with "or" gives us a, in fact,
ternary sort of functioning operator.
So we'll do that.
We'll say ballDX should be
equal to negative 100 or 100,
depending on this coin flip.
And then for the ballDY,
let's just say that I
want it to be between
negative 50 and 50,
so that's essentially
what that's going to do.
So math.random can take in a single
number, or it can take in two numbers
and it'll give you a
value between that range.
So between negative 50 and 50 will
end up actually getting somewhere
in that range.
So you can assume, then, that
the ball will either go up
or sometimes it'll go down.
And by combining a random
x velocity of negative 100
or positive 100 with
a negative y velocity,
you can get all sorts
of different angles
upon which the ball will spawn
at the application's beginning.
So in addition, remember again,
we're going into state, as well.
So what I need to do is create
as some sort of variable
to keep track of the state.
What I can do is I can
see gameState = 'start'.
Let's just say we're going to have
two states in this application,
start and play.
So I'm going to do that.
And an update, then,
is where we're going
to have a little bit more work here.
So let's just say if gameState ==
'play' then ballX = ballX plus ballDX.
And remember, again, we need to
multiply it by delta time such
that the movement is interpolated
across different frame rates.
So we're going to do that.
We're going to say ballX = ballX plus
ballDX * dt, and ballY = ballY plus
ballDY * dt.
And remember, it can be adding
either positive or a negative number
in this situation.
Now, just as an illustration,
as a test, let's go ahead
and change this to play and run it.
And we do indeed see the ball moving
towards the bottom left of the screen.
If I rerun this-- let's try that again.
We see the ball moving
towards the upper left.
So in both situations, it
got a DX of negative 100,
the difference being that the ballDY
was first some positive amount, maybe 30
or 20, and in the former amount, it was
some lesser amount, like negative 10,
negative 15, something like that.
Could be more or less, just
ballparking it visually.
So now let's go ahead and get back to
actually creating two separate states.
So what I want to do is I want to
hit Enter at the very beginning.
I want to essentially have it be like
it's waiting for me to press Enter
before it actually serves the ball.
I'm going to go ahead and
add something down here
in the love.keypressed
function, because remember,
love.keypressed will wait for
one single keypress as opposed
to continuous input.
So if I say elseif--
some auto-formatting there.
elseif key == 'enter' or
key == 'return' then--
because Macs, actually, their default
value is Return for the Enter key.
On Windows, it'll be Enter.
If it's the case that we pressed Enter
or Return, I essentially want to say,
if gameState == 'start' then gameState
= 'play' elseif gameState == 'play' then
gameState = 'start'.
So this really doesn't do a
whole lot, in and of itself.
But now we can essentially say, up
here, because we have gameState checking
to see if it's equal to
play, it'll actually not
start the ball moving until
we press Enter the first time.
So if I do this and then it's
waiting, if I press Enter, boom,
the ball is moving
towards the upper left.
So that's great and everything.
It would be nice if we have a
little bit more visual input.
And it would be nice, too, if I could
restart the ball over and over again.
So in order to do that,
what I can do instead
is in play, instead of just
changing it back to start,
I can do essentially what I did
up here at the very beginning,
where I initialized the ball's values.
I'll just say ballX,
and ballY, and ballDX,
and ballDY get assigned back
to their default values.
So I'll do something like that.
You might think that this
might be a good place
to put a function so you could
call this in multiple places
and save a few lines of code.
Totally could do that, as well.
So if I do this, if I
hit Enter, you'll see
that the ball does get reset back
to its initial starting point
every time I press Enter.
Lastly what I want to
do is I want to reflect
the state at the top of the screen, such
that it's very clear and apparent to me
what I'm waiting to do.
So I can do that here.
I can say, if gameState
== 'start' then--
let's just indent this, not like that.
elseif gameState == 'play' then--
and then I want to change this to
we'll just say Hello Start State.
And I'm going to copy this.
And I'm going to say Hello
Play State, just like that.
And we'll do that.
So now it Says Hello Start
State at the very top.
If I press Enter, it does
say Hello Play State.
If I press Enter again, we go back to
the start state, then if I press Enter,
we go back to the play state there.
So join me in the next section, pong5.
We ended up adding quite a lot
of features in this section.
In the next section, we're going to
do just a little bit of refactoring.
We're going to dive a little bit
into Object-Oriented Programming,
or OOP, as we start to break
out some of these variables
that we have all over the
place and put them together
into some nice, clean packages
where we can represent,
for example, the paddle as an object
and the ball as another object.
So see you again in pong5.
