[MUSIC PLAYING]
DAVID MALAN: All right.
Welcome to Introduction
to Game Development.
My name is David Malan,
and this is Colton Ogden.
And this is a class that
assumes only a class like CS50,
which is the colleges and the extension
schools introduction to computer
science but more
generally we just assume
that you have prior programming
experience in most any language
and therefore have
some comfort with some
of the basic constructs of programming.
But we assume no background in Lua
or Lab 2D or any of the frameworks
that we'll be using in the class.
All of that lies ahead.
So, if you're like me, you probably
grew up with video games of some sort.
And when you maybe started programming,
the programming environments
were perhaps very text based, black and
white terminal window, and the like.
And maybe you did something graphical
with a language like scratch or Alice
or beyond, or if you're
in the world of the web,
you've made more graphical applications
of some sort, but still pretty static.
The sort of content comes on the screen,
then the content changes, and so forth.
And it's a little less obvious if
you're a little newer to programming
how you go about creating
some of those games
from yesteryear with
which you all grew up,
where there's a lot more animation,
there's a lot more asynchronicity,
lots of things happening
at the same time.
A lot of events happening,
and you all NOT--
not only want to capture
this interactivity,
but also want to respond
to events that are
happening, especially
if you have players
elsewhere next to you or online.
And so the way the
course will be structured
is through a narrative of these
various games, many of which
you might have played yourself.
But over the course of
the semester, we dive
into the context of each
of these games and look
at some of the underlying
principles, the constructs via which
they were built up, and really
use them as a point of departure
for talking about those
various capabilities
that you might integrate
into your own games.
And then punctuating the
semester, ultimately,
will be a number of milestones.
Some in the form of smaller assignments
that are meant to reinforce just some
of the more recent material and sort of
set you up for success when the course
is deeper and more hands on
projects. , Because indeed,
the project is where you'll build
or extend some of your own games.
And then the class itself will culminate
at the very end of the semester
with your very own final project,
an opportunity to propose,
to design, and implement a game
that somehow or other draws
upon the course's lessons.
So that when you walk out of
here in just a few months' time,
you've not only played
your fair share of games,
but have actually built
several of your own.
So without further ado,
allow me to turn things
over to Colton for a look and a
stroll through yesteryear's Pong.
COLTON OGDEN: Thanks, David.
I'm very excited to begin
teaching you guys this course
because game development
was actually what got me
into programming in the first place.
I remember back in 2006 or
2007 buying this book here,
3D Game Programming All In One, which
was a look through 3D game programming.
And it was a monolithic text
in the context of a game engine
that was popular in the
late 2000s called Torque.
It's not as in vogue these days, but
it at the time was pretty popular,
and it used a language
called TorqueScript.
And I remember reading through
this book and seeing all this code,
and I had never seen like
source code at all before,
or had ever been
introduced to programming.
And, frankly, I found
it quite intimidating,
because I was looking at all the
syntax that I didn't understand,
and I didn't know anything
about game development.
I had always played games growing
up and been fascinated by it,
but as I started getting more
comfortable with computers
and I started to get
more curious about it
and realized that it
was a major profession,
I started to dive a little deeper.
This was my first foray.
And after spending a little
bit of time away from it
after looking through the source
code for a TorqueScript, which
was rather arcane, a lot of
percent symbols and dollar signs
are the weird things that I just
hadn't gotten my mind around.
I went back to it, started to really
learn the basics of programming
and other languages, like
Python and c and c++,
and I grew to really like programming
and computer science a lot.
And here's just an image of what
Torque looked like at the time.
It was really the sort of
predecessor to Unity nowadays.
Although, in my opinion, Unity
does things a lot better.
It was more accessible, it uses
languages that are more in vogue
and popular and used by other
people already in other domains.
And so we'll be covering Unity
at the tail end of the course.
So we'll be covering
predominantly 2D game development.
But the topics that
we'll be covering today
as we get started in the context of
Pong are these bullet points here.
Lua, which will be the language
that we're using predominately
throughout the course, which is
a dynamic scripting language very
similar to Python and JavaScript.
We'll be covering LOVE 2D as our
primary game framework, which
is a runtime and a framework
which exposes all of its methods
for drawing, audio, input, etc.
via Lua, so that it's very easy
to write code very quickly,
but get very good results.
And the documentation for their
framework is superb, in my opinion.
Today we'll be talking about
a few just basic principles
as we get our feet wet
with game development.
Things like drawing
shapes, drawing text,
these are both very big
aspects of Pong, which
is just a very simple game
based on shapes and text
moving around the screen.
We'll be talking about Delta Time and
Velocity, which Delta Time is probably
arguably one of the
most important variables
that we keep track of in any game
framework or engine, which is just
the amount of time that's elapsed
since the last frame of execution
in our game, measured in LOVE 2D in
terms of seconds, fractions of seconds.
We'll be talking about
game state, because you
can have a state in your game.
You can be at the title screen, you
can be playing, you can be in a menu.
This will, obviously, be very important
because you want different update logic
and rendering logic depending
on what state you're in.
We'll be talking about basic
object oriented programming,
for those who might be
unfamiliar coming from C.
It's basically a way of encapsulating
our data, any of our game objects,
in such a way that the variables
that are relevant to them
are put together, along with functions
that will operate on that data.
So instead of having like 20
different variables for all
these different objects that you
have to keep track of in your code,
each individual object can keep
track of all its own information,
like its position, or anything
else that's relevant to it.
We'll be talking about hit
boxes today, predominantly,
in the context of box
collision, because we'll
be talking about Pong, which
is just paddles and a ball.
Those are all rectangles.
And they'll be colliding with
what's called axis aligned bound--
axis aligned bounding boxes, which makes
calculating whether two boxes collided
very simple, as opposed to
calculating rotated hit boxes,
which is a bit more complicated.
And then, lastly, we'll
polish off with sound effects,
because adding that polished
layer, in my opinion, is important
and it ties it all together and makes
it feel like a more cohesive whole.
So two important things that we'll
need to do when we're following along
with the examples, which I'll show
you a link to the repo in a moment,
is getting LOVE 2D installed.
It's a very simple process.
The first link here is
just a download link.
So it's available for all
major operating systems.
So Linux, Mac, and Windows.
And then the Getting
Started link down here below
will give you some tips
as to how to get started,
actually running it on your
machine on Mac, iAlias,
the actual runtime executable
within the app that it comes with.
So in my bash profiles that I
can easily just type love space
dot in any directory that
has a main dot Lua file,
and I can run it anywhere very simply.
And there are similar instructions
located on the page for other operating
systems.
And this is the repo here,
which has all of the source code
that we'll be using today.
And I've structured it in a
series of 13 different subrepos
so that you can follow along and we can
build upon Pong starting from scratch,
going all the way to a
fully implemented game.
So the first thing we'll
talk about is what Lua is.
We'll be using Lua for
about 75% of the course.
It's a very popular
dynamic scripting language.
Portuguese for moon, and it
was invented in the early 90s
as primarily a config language
and a runtime language
for compiled code bases to save time
on adding code to those code bases
and recompiling them.
A lot faster and a lot easier,
especially in the context
of the 90s when computers
were much slower, to expose
the core functionality of your
application to Lua so that you can just
run it dynamically and then interact
with your compiled code on the fly,
rather than having to
recompile and wait minutes,
potentially hours, just
to get some new behavior.
It's a language that's focused
around the concept of a table.
Almost everything in Lua aside
from basic variables, are tables.
A table is essentially a dictionary
in Python or an object in JavaScript.
Very similar.
Intent, for embedded use
in larger applications,
and the very nature
of Lua intended to be
used in the context of
these large applications
meant that it was perfect for
interacting with game engines.
Because game engines are a
perfect example of code bases
that are traditionally compiled
code for speed purposes.
But it can be very cumbersome to
have to add minor functionality,
and then recompile it and potentially
have your whole studio take hours.
So we'll be using Lua and a
compiled game framework, LOVE 2D,
to allow us to rapidly develop.
It's similar to JavaScript and Python.
A little bit more so to JavaScript.
And it's very excellent
because it was initially
intended as a config language,
and a-- just sort of a glue layer.
It's very good for storing data and
code together, almost one in the same.
So LOVE 2D is a fast 2D game develop--
development framework.
It's compiled in C++ and
it runs very efficiently.
Because it's so simple, despite the
fact that we're running it in Lua,
and as modules for
basically anything you
would need in the context
of 2D game development.
Only 2D game development officially,
although some people I know
are working on slight little 3D
experiments, but nothing official yet.
But it has graphics, keyboard
input, math, basically, anything
you could want in the context
of 2D game development.
It's completely free.
It's portable.
You can even run it on
mobile and also the web.
And it's excellent for prototyping,
even if you don't necessarily
want to publish a game in LOVE 2D,
it's great and easy and fast just
to whip something up in
LOVE 2D, and then port
that over to whatever
framework or engine
you might be using in the real world.
So before we get into looking
at some actual concrete code,
I think the most fundamental
thing we should take a look at
is what a game loop is.
So a game, fundamentally, is just an
infinite loop, like a while true or a
while one.
Only in this case, every iteration of
that loop we're doing a set of steps
back to back over and over again.
We're processing input so we're
seeing, has the user pressed
a key on the keyboard, have
they touched their joystick,
have they moved the
mouse, clicked the mouse.
If they have, we need to
feed that into our update.
We need to keep track of that, and
then change anything in our game state
that relies upon that input.
So we should move our paddles,
we should detect collision,
we should register all of this,
and then whatever has updated,
we want to rerender that.
We want to render it--
render where it's changed
so that we have the--
we see on our screen, visually,
that things have actually
changed in our game world
and we interact with it,
and we get a sense that
we're using something,
interacting with something dynamic.
And in the context of 2D games, the most
fundamental way of looking at the world
is via the 2D coordinate
system, which is just simply
as we learned in geometry in
high school, x and y-axis.
In this case, it's slightly different
than what we typically learn.
In high school, we tend to learn that
the xy origins, sort of bottom left,
y positive goes up, negative
goes down, positive x goes right,
and negative x goes left.
But in this case, we're actually
starting in the top left,
and then it goes y positive down,
y negative up, x positive right,
x negative left.
And everything that we
want to draw in our game
needs to have an x and
y-coordinate to draw in order
for it to be visually
seen on the screen.
So today's goal, we're going
to start a fairly low level
and work our way up through examples
today and in future classes.
Our first game is arguably
one of the simplest,
but also, one of the most
famous games of all time, Pong,
which was released in 1972.
And the gist of Pong
is you have a paddle
on the left side of the screen, a
paddle on the right side of the screen,
whoever scores 10 points by getting
the ball past their opponent's paddle
onto the edge of the screen, wins.
And so today in our lecture, the scope
is we want to, first and foremost,
draw shapes to the screen, because
that's how we get our ball and-- ball
and paddles rendering.
And those are just simply rectangles.
We want to control the 2D
position of these paddles,
because we want them to move up and
down and want the ball to also move.
We want to detect collision
between the paddles and the ball,
because that's how we get the
ball to deflect off the paddles,
and to deflect off the
ceiling and the floor.
And, also, how we detect whether it's
gone beyond the edges of the screen,
such that one player scores a point.
And then we want to add sound
effects for sort of a feedback
and sort of put ourselves into
the game a little bit more.
And then scorekeeping, because
ultimately the purpose of the game
is to beat your opponent,
so you want a way
to see who has scored 10 points first.
And so we're going to look through
a set of examples now in the repo.
If we look at Pong Zero, I've set this
to be called, The Day Zero Update.
It's a trend among many games to
have the games release major content
updates as the x update.
So just to be cute, I think we'll
call each individual example here,
The Something Update.
And so I'm going to
go into the Pong Zero
Repo of the directory, the GitHub repo.
And if we're looking at Pong
Zero here, we can see it
says here, The Day Zero Update.
I've commented everything
fairly heavily so that we can--
if you're reading the code, you can
sort of get a sense of what's going on.
At line 23, we're going to
start off by just declaring
a window width and a window height.
And these are just
constant variables that
will be accessible throughout
the rest of our application.
So I'm just setting 1280 by
720 as an arbitrary resolution.
It doesn't matter too much.
An important thing that we need
to look at here is that line 29,
we're using a function called
love.load, and I'm actually
going to go back to the slides here.
We're going to look at
a few functions, and I'm
going to go over them
and just sort of tell you
what they do before we look at
the code in too much detail.
So love.load is just a function
that-- given to us by LOVE, LOVE 2D,
and we overwrite it.
We give it behavior,
we tell it what to do.
And LOVE 2D is going to look
at it in our main.lua file.
If we're looking at Pong Zero, you'll
see it just has a main.lua file.
LOVE 2D expects just a main.lua
file, and will run the main.lua file,
and you can reference any other file
within the directory from that main.lua
file.
It's our bootstrap, effectively.
We're going to override
love.load with whatever
we want to execute at the very
beginning of our application.
It's just a startup function.
We can also define all that behavior
outside of the function above it,
but it's good practice to
find it within love.load
so that someone reading
your code will know,
OK, this is where all the
startup code takes place.
Love.update(dt) is a
very important function.
This function takes in
a variable called (dt).
Love passes it in a function.
You're going to overwrite
it with your own behavior,
and Love is going to execute this
every frame, passing it in delta time,
and you can use delta
time (dt) in that function
to change your application based
upon how much time has passed.
(dt) will always be a fraction
of a second, potentially more,
depending on how slow your computer is.
But, typically,
one-sixtieth of a second.
And you can scale anything
in your game by that amount
to get even behavior
across all frame rates.
Love.draw is the other
big function amongst--
between update and draw.
Two of the two, arguably,
most important functions.
Love.draw is the
function that we're going
to define that has all of our drawing
behavior, our rendering behavior in it.
And that's where we can draw our
paddles, we can draw our ball.
And then update is
where we can like change
the paddles position and so forth.
Two more important functions we'll
take a look at in the first example.
Love.graphics.printf is the LOVE 2D
analog of printf and C. The difference
being that this printf lets
us actually draw physically
onto the screen versus a console.
We give it a text as a string, and an
x and a y-coordinate and, optionally,
a width and an align, and
it'll will draw the text at xy,
but it will also take into
consideration the width,
and it'll also take in
consideration the align.
The with is how much to align it, and
the align is the mode of alignment.
So if we say x is zero width, our window
width, and then we say align center,
it's going to go between zero and
our window width and center align it.
So that'll have the effect
of center aligning our text.
But we can just as easily
say, right, and it will right
align it between those
two and have the effect
of rendering the screen--
rendering the text
along the right edge of the screen.
And then lastly, love.window.setmode
takes a width and a height
and some optional parameters.
Those parameters being things
like V sync and full screen,
and will actually set up our window
and get it rendering onto the screen.
And so if we go back to our
source code here, it-- at line 29,
we're overwriting love.load.
We're passing in love.window.setmode,
window width and window
height, which recall we defined
up above as 1280 by 720.
We're passing in a table.
This is the syntax for a
table, these curly brackets.
And the way that we define keys and
values is just with an equal sign
therein.
So full screen gets false, resizeable
gets false, V sync gets true.
So it's going to not
be full screen, it's
going to be a not resizeable
but it is going to be
synced to our monitor's refresh rate.
And that's where V sync is,
short for vertical sync.
And then on line 40, we're
overwriting love.draw,
and this has the love.graphics.printf
function, they're in,
and we're saying-- we're passing
in the string, hello, Pong.
We're starting it at x
zero, we're setting it
at y window height
divided by 2, minus 6.
Because the default font size
in LOVE 2D is 12 pixels tall.
So we're shifting it up
by six so it's perfectly
centered vertically in the screen.
And then we're setting the alignment
amount, the width, to window width
so that it's going to align it within
the entire width of our window.
And now we're setting
it to center alignment.
So it's going to be center
aligned within our entire window
starting at x zero.
And so if we go to Pong zero,
and then we actually run it,
it has the effect of doing this.
We're just rendering in our
default font, default size,
hello, Pong, right in
the middle of the screen.
So not a terribly
exciting example, but it
is showcasing the most
important functions of LOVE 2D,
so that we can get started with
slightly more interesting examples.
So, our first content update,
is the Low-Res Update.
So we're developing Pong
and Pong is an old game.
It doesn't look like the
example that we just looked
at where the font is fairly high res.
We want something that
looks a little more retro.
So what we want to do
is get our resolution
looking like it's from
a game released in 1972.
So what we're going to do is look at
a few more important functions here.
So Pong One has these functions.
So love.graphics.setDefaultFilter.
This function, the purpose
of that, is every time
we have a font or an
image in our application,
it's going to be applied
a filter by default.
So it's going to by
default a bilinear filter.
So what's going to
happen, the effect of that
is, basically, whenever we
magnify or downscale a texture,
it's going to think that--
it's going to assume
that we want it to be slightly blurred
so as to not look too pixilated.
Which is good in certain contexts.
For higher res 2D game development,
that's good, but as we're going to see,
that's not particularly good
in the context of retro games.
Retro games have a very 2D,
crisp, pixilated aesthetic,
and we want to preserve that.
And so this lets us
set a default filter.
We'll see that in usage shortly.
Another important-- very important
function which is the input phase
of our game loop that we saw earlier,
love.keypressed(key) is what's going
to allow us to start interacting
with that aspect of our game.
So love.keypressed(key) is a callback
function that LOVE expects in main.lua.
We're going to overwrite it.
It gets passed in a
key, and this function
gets called every time by LOVE
2D whenever we press a key.
It'll detect a key pressed,
and it will call this function.
Whatever we've defined
in here, it will call it,
and we can set it to
take in certain keys
and perform certain operations on
that input and it will get a string.
So if we say-- if we
press the escape key,
key is going to be equal to the
string escape in that function,
and we have access to that.
And another important
function, love.event.quit.
This has just a very simple effect
of quitting the application,
though we can call it in the code
as opposed to doing it ourselves.
And so here's an example of what
texture filtering looks like.
Point filtering is the
same as nearest neighbor
filtering, which is what
we're going to be using.
Bilinear filtering is shown on the
right, where it looks pretty blurry.
That's what LOVE 2D applies by
default to both fonts and to textures.
And we'll see that in an example.
I can actually run it
in two different styles.
So if you go to Pong One in
the repo, and then we run it,
we see here, hello,
Pong is now blown up.
And we'll look at some
more code, actually,
to see as to why it's blown up.
But if we go back to our
code, let me pull up Pong One.
Go to main.lua, and then I'm going
to explain this in just a second,
but let me comment this out and
we'll see the difference here.
You can see it looks a lot blurrier.
And that's the default texture
filtering taking place.
It applies, like I said, not only
to textures but also to fonts.
And that's not the aesthetic we want.
So let's look at Pong One in
detail, starting at the top.
On line 28, we're acquiring a library.
This is how you get a
library in your LOVE 2D
application, or your LOVE application.
Just equals require and
the name of the library.
Push is what we're going to be
using to take our 1280 by 720 window
and turn it into a virtual
resolution window at 432 by 243.
We can start to think of our game
in terms of a more low res feel,
and think about it in 432 by
243 pixels, but still render it
in a window that's arbitrarily sized.
In this case, we're preserving the
1280 by 720 window that we saw before.
If you go to our love.load function,
we see this being used on line 47.
Instead of
love.window.setmode, we're now
using push setup screen, the push
libraries setup screen function,
where it takes a virtual width, a
virtual height, our regular window
width and our window height, and
then the same table as before.
And this has the effect of
setting up a window that's
got our concrete
dimensions of 1280 by 720,
but a virtual resolution of 432 by 243.
And so now, when it renders,
as we'll see shortly, as--
well, as we already did see,
actually, it's magnified.
It has the effect of giving
us a lower resolution.
And in line--on line 58, if we look
at the love.keypressed function,
we've put in there, if the
key equals the string escape,
then love.event.quit.
So now we have input handling.
We've overridden love.keypressed(key),
which LOVE 2D is going to look
for in our application,
and then call as needed.
And then we're just looking in there.
If the key is escape,
then love.event.quit.
And if I run the application, I
can now press escape on my keyboard
and just quit it and not
have to command quit or click
the X on a Windows application.
And we've changed one more thing, also,
in the love.draw function on line 70.
We're using the push library now.
We need to-- it functions sort
of as a state machine in that we
set it to start rendering at
a virtual resolution with push
apply start, and then push apply
end, and then anything in between,
this is very similar actually
to how Open Go works.
We won't go into too much detail.
But this is very similar in spirit to
how much of Open Go programming works.
Push apply start, push apply end.
Between that, whatever we call, is going
to render at this virtual resolution.
And so we are calling the same
love.graphics.print(f) function,
hello Pong Zero, virtual high
divided by two minus six.
Same parameters.
And it has the effect of rendering
everything that's still got
the old aliasing going on the skin--
texture filtering going on as the
effect of giving us our magnified text.
So same text, same size, but now our
window of rendering is much smaller.
So--
Any questions so far on
how any of this works?
OK.
Awesome.
So we've gotten text right into the
screen, but we're nowhere close to Pong
yet, so the first big
thing, I think, that's
going to get us closer in
that direction is what we're
going to call the rectangle update.
So some important functions
that we should look at.
Love.graphics.newFont.
The default font, I believe, is Arial.
We don't want Arial in our
application, because we want something
that looks a little more retro.
We want something that
looks more relevant.
Love.graphics.newFont will
basically take a path to a font file
that we have in our folder, which
if you're in the Pong 2 folder,
you'll see a font.ttf file, and a size.
Because every font object
that we instantiate
needs to have a size, because
the font objects are immutable.
Once constructed, they cannot be changed
so they need to be allocated on a size
by size basis.
Love.graphics.setFont
will take whatever font
object we've acquired from this
function call, and we can set it here
and it'll set the active font
in LOVE 2D to be that font.
Love is a state machine
in the same sense
as before, in that it will have
an active font at any one time,
and whatever print functions you call,
will use the currently active font.
And that also applies to whatever color
you might want to render to the screen,
whatever.
If you have a font and you
want to maybe render it in red,
you need a set LOVE 2D's
active color to red as well.
Love.graphics.clear is a function
that takes an RGBA quadruple,
and will flush the screen in that color.
It just has a simple effect of
wiping the screen in that color.
Useful for drawing just
flat color backgrounds.
And then the last function, probably
the most important function,
is love.graphics.rectangle.
And this is the first
function that we'll
see that actually ends up drawing
something beyond text to the screen.
It takes it a mode, which can
be fill or line, an x and a y
and a width and a height, and it'll
draw a rectangle in that mode.
So either filled, so a filled
rectangle, or a line rectangle.
It'll take-- it'll draw it at
xy with the width and the height
that we pass in.
So let's go ahead and
take a look at Pong 2
where we can see this
actually implemented.
So Pong 2, we have our font.ttf
in there that I've included.
And then a main.lua.
And by the way, I forgot
to mention last example,
push the library that we required
is also just in the same directory.
And you can just do require as long as
the file is there within the directory,
it will just load it.
You don't have to specify .lua.
It assumes when you require some string,
that it follows with the .lua suffix.
So we're here looking at main.lua
in Pong 2, the rectangle update.
So on line 28 to 34,
it's all the same stuff.
We're acquiring push.
We have our width and height
virtually and physically.
In our love.load function,
we are on line 43
declaring small font to be
a love.graphics.newFont,
giving it the path font.ttf
because it's right there
in the same directory at size eight.
And this is going to create a font
object, small font, that we can then
set as the active font as needed.
So if we go down to line
78 in the same directory,
we see we're calling
love.graphics.clear,
and so we're passing it in a color.
I sampled some images
of Pong on Google Images
and saw a background gray that I liked,
so 40, 45, 52, RGB, and then 255 just
means completely opaque, so no
transparency, the alpha component.
And then we're doing
the same print.function
as we did before on line 81.
Below that on line 89 down
through 95, we're actually
calling love.graphics.rectangle.
And these are drawing the two
paddles and then the ball.
So, note, love.graphics.rectangle
fill mode,
because we want the paddles to be
completely filled, as is the ball.
We're giving it an xy of 1030
and a width height of 520.
And on 992, we're-- and that'll have
the effect of drawing it a little bit
shifted from the top left corner,
five pixels wide, 20 pixels tall.
On line 92, we're doing the
same thing, except we're
going virtual width minus 10.
So it's going to go to the right edge
of our screen, virtual width minus 10.
So 432 minus 10.
So 422.
And then virtual height minus 50.
So it's going to be slightly--
it's going to be slightly up
from the bottom of the screen.
So we have our top left paddle
and our bottom right paddle.
And then the ball is dead center.
So we're sitting-- doing another
graphics-- rectangle call.
Virtual width minus two divided by two.
So right in the middle,
minus two, because our ball
is going to be two pixels
wide by two pixels tall.
And same thing with
virtual height, minus two.
Our virtual height
divided by two, minus two.
So I'm going to go ahead and CD into
the Pong 2 directory and run it.
And that has the effect of we have
our new font here in the middle.
So it looks nice and retro, much more
so than the Arial font as before.
We have a rectangle here, five
pixels wide by 20 pixels tall.
A ball in the middle, which
is four pixels wide by four
pixels tall, and then a
paddle on the bottom right,
which is the same dimensions
as the paddle on the left.
So it looks very similar to Pong.
It's not interactive at all, but
we sort of getting the feel of what
we want our application to look like.
We have it mostly sketched out.
So any questions so far
as to how this works?
OK.
Awesome.
So Pong 3.
So currently we have no
interactivity with our application,
and we want to be able to
move the paddles around.
We don't want to just be looking
at an-- at an image the whole time.
So the paddle update is going
to solve this problem for us.
We're going to actually get our first
sort of-- beyond pressing escape
to quit the application,
we're going to get a sense
of interacting with it dynamically.
So the important function that we're
going to look at in this example
is love.keyboard.isDownsomekey.
And this is true-- this
is a Boolean function.
It just returns true or false depending
on whether the key that we pass in
as a string is currently
pressed down on this frame.
So it just returns true or false.
And so let's go ahead and
take a look at the demo.
We're going to go ahead
and pull up Pong 3.
The main.lua, they're in.
Notice that line 37 if you're looking
in Pong 3 we, have a new constant
we've defined called paddle
speed, which gets the value 200.
And this is just an arbitrary value
that I found was a good speed.
But this is how fast our
paddle is going to move.
We're going to scale it
by delta time, so we're
going to multiply it by how
many seconds have passed.
Typically, a fraction of a
second since the last frame.
And this is going to, therefore,
move the same distance
over time depending on
whether your computer is
running at 10 frames per
second or 60 frames per second.
So if we go down here to line 63,
I've also set up two new variables.
Player one score and player two score.
Those are both initialized at zero.
We're going to add in this example,
also, some rendering of the score.
And notice here on line
49, I've also added
a new font, which showcases how you need
to separate fonts based on their size
because they're immutable objects.
Score font gets love.graphics.newFont.
Same exact font file, but it's 32 pixels
large because the font-- or the score
when rendered in Pong is pretty
large in the middle of the screen.
And so we're creating-- we
have two different fonts now.
One for rendering our message,
one for rendering our score.
And it's just going to render these two
variables, player one score and player
two score.
And then we've also
initialized our y values
for the rectangles, the paddles
on the left and the right.
We need to keep track of their y
position, because paddles in Pong
can only move up or down.
So player 1Y gets the same
value it did before when
we initialized the rectangle,
when we drew it onto the screen.
It's going to start at
y 30, so pretty high up.
And player 2y is going to start
pretty low, virtual height
minus 50, which is 432 minus 50.
And so in love.update, which is
our first actual use of the update
function on line 75 with the (dt)
parameter that gets passed in.
Note, remember that LOVE 2D
will pass that in for us,
but we need to give it-- we need to
define the behavior inside of it.
We're using love.keyboard.isDown,
and we're passing in the string w
and s for this first block.
This first block here is
player one's movement.
So, traditionally, on
computer WASD is to move--
and this example, we're going to
allow ourselves to move both paddles,
so we're going to use w
and s for the left paddle,
and up or down for the right paddle.
So if love.keyboard.isDown
w, which means--
or we've currently pressing
the W key, player 1y
is going to get itself, plus negative
paddle speed times delta time.
So it's going to move up.
It's going to take negative paddle
speed, multiply it by delta time,
and add that onto our y
value, which will have
the effect of shifting our paddle up.
And it's the opposite for--
on line 82 for-- if
we're pressing the s key.
We need to increase the y by positive
paddle speed, because recall,
y-axis movement is--
up is negative, down is positive.
We're doing the exact same thing
with the paddle on the right
except we're using up and down as the
strings into love.keyboard.isDown.
And then down below here,
we are rendering in addition
to what we rendered before,
also, the score now.
So on line 125, note that we're calling
love.graphics.setFont, scoreFont,
because if we don't call this, it will
use just whatever the last font was,
which by default is the eight
pixel font because we set that
up top in our program.
We want to set it to the
score font, and then we
want to call love.graphics.print.
In this case, I'm just printing them
in concrete places not, using printf.
Virtual width divided by two minus 50.
So no matter how we
scale our window, it's
always going to be 50 pixels to the
left of the center of the window,
and the 30 pixels to the right
of the center of the window
if we're rendering the player two score.
And so if we go into Pong 3 and we
run it, it looks the same as before.
Note, that we do have a score now in
the middle of the screen, zero and zero.
But, more importantly, we can
move our paddles up and down.
But there's one problem, and that's I
can move beyond the edge of the screen,
which is not behavior that
we want in our application.
So we have some interactivity,
it's moving along,
but we still have a long
way to go, unfortunately.
Or fortunately.
So let's go ahead and
look at the ball update.
So we have paddles, they can move, they
can move beyond the edge of the screen,
but we don't have a ball that-- it
just sits in the middle of the screen.
And that's not what we're looking for.
We want to have a ball that we can
actually bounce between the paddles
so we can get an actual game
going beyond just moving paddles.
So a few important functions
we're going to look at.
We're going to get our
first look here at random.
So in games, random number
generation is a very common thing
so that we get unpredictability and
variability between different instances
of our game.
An important function
that just belongs to Lua.
It's not a LOVE 2D thing,
it's just a Lua thing.
Math.random seed numb.
So many of you have
probably heard of like seed,
like a random number
generator, seed, and that just
means a random number generator.
Because it's pseudo
random, it needs some sort
of starting value to base all
of its random numbers off of.
It takes a starting number, it
performs some mathematical operation
on that number to derive new
random values that we can then
use in our game engine.
But if we give it the same
number every single time,
it's just going to give us the same
random numbers every single time,
which means it's not
going to be random at all.
It's going to be very consistent.
So we need a way to seed
our random number generator,
give it a different
initial value or seed,
and we're going to do that with the
function math.randomseed somenumb.
OS.time is an important
function in the context of this,
because a very common way of getting
a different number every time
you run your application is passing
it in whatever the current time is
in seconds, because usually
it's a very large number that
is going to be different every single
time you run your game, no matter what.
Because it's based upon, in
the context of most engines,
in the context of Lua, what's called
Unix Epoch time, which is zero
zero UTC January 1st, 1970, which is
some huge number nine or 10 digits long
that changes every single second.
And then in order to actually
take advantage of all this,
we need a function to
get a random number,
and so we do that with math.random,
which takes a min and a max,
although you don't need
to technically pass a min,
it'll just implicitly deuse min
as one if you don't pass it a min.
And it'll return a value
inclusively within that range.
So if you say math.random
one, 50, it'll give us
a random inclusively between 1 and 50.
And if we just say math.random
50, it'll do the same exact thing.
It'll say-- it'll assume rmin is one
and give us a value between one and 50.
And then two important
mathematical functions
that are very basic but helpful in the
context of games almost everywhere,
it's just math.min, which
returns the lesser of two values,
and math.max which turns
the greater of two values.
And we'll see this in the context
of clamping values to some range.
So let's go ahead and take
a look at a demo here.
So I'm going to go ahead
and open up Pong 4.
And going to look at
main.lua they're in.
So here on line 47, we see we're calling
the math.random seedfunction as before.
And note that we're passing in
OS.time, another function call,
because OS.time is going to be different
every time we run our application.
So we're seeding our
application every time
we run it based on whatever the current
second is relative to zero, zero, zero,
zero, zero, zero, January 1st, 1970.
Which is going to be different
every single time we run.
Assuming we don't run it
within the same second.
And then if we go down to line 71
and seven-- or, sorry, 67 and 68,
we now have--
we're giving a starting
value to our ball,
because we want to actually
start manipulating our ball.
So we give it an X and a Y. So we're
setting it right to the center again
but, now, we're defining
a variable for it
instead of just rendering it statically
with our love.graphics.rectangle
function, because we want
this to change over time.
We want to start letting our
ball move around the screen.
So these x and y variables are
going to start changing now,
and they're going to change
relative to its current velocity.
And its velocity is going to be
stored in ball dx and ball dy.
dx and dy are common shorthands
for delta x and delta y,
which is how you represent velocity.
So what we're going to do, effectively,
is take whatever our delta x and delta
y are and add them onto
our ball frame by frame,
and that's going to have
the effect of updating
our ball's position by some value.
And separating the
delta x and the delta y
will allow us to have different angles,
different trajectories for our ball.
And then another thing that we're
also doing in this application,
we're starting with the
concept of a game state.
Because now we can have a starting
state, and what we're going to have
is a play state.
And so all we're going to
do here in this example
and in this application is
start state as a string.
In future examples, we're going to
use what's called a state machine
and actually separate out
different states into their own--
into their own modules.
But in the context of
this game, we're just
going to use a simple
string just to illustrate
how it works, and we're going to say
our first state, when we start the game,
should be the start
string in the start state.
And so here on line 86,
what we're going to do
is solve a problem that we had
in the last example, which was,
the paddles could move beyond the edges
of the screen, which is not behavior
that we should permit.
So we're going to call math.max
on zero, and the same operation
we were doing before, and that will
have the effect of returning whichever
of those two values is greater.
So if the value is--
if we're adding negative
paddle speed to our y value
and it goes into the
negative range, which
means it's beyond the
top edge of the screen,
zero is going to be the
greater of those values
and so it will always be zero
in that case. math.max returns
the greater of the two values.
So it'll have the effect of
clamping it such that it never
goes above the top edge.
The inverse is true
for line 96, where we
call math.min on
virtual height minus 20,
and player 1.y plus paddle
speed.delta-- times delta time.
And this will have the same
effect, it will return whichever
of these two values is lesser.
In which case, if we've gone above
virtual height minus 20, which
is down at the bottom of the screen
shifted by the size of our paddle,
it's going to set it to
virtual height minus 20.
So we never go below that point.
And we're doing the same thing
for player two, exact same logic.
And then if we're in
the play state, we're
going to actually update
our ball's position.
So we're in the-- if we're in the start
state, ball's not going to move at all.
But if we're in the play state, we
want ball x to equal ball x plus ball
x times delta time.
And note that there
is no shorthand in Lua
for adding the value to itself,
which is why we're calling ball
x equals ball x, plus
ball x times delta time,
instead of just saying ball x plus
equals ball x times delta time.
Just a language decision that they made.
But if we're in the
play state, this will
have the effect of scaling whatever
our current ball's velocity is and--
times delta time, so it
stays frame rate independent.
And then adding it to ball x
and ball y, which will shift it.
And we get this actually
working down here in line 170--
174.
We're now-- instead of just
rendering flat numbers to the screen,
we're actually using ball
x and ball y to render.
And if we're in the play
state, those will get updated.
But if we go back up to line 127, now,
we're in the love.keypressed function,
so we're starting on line 120.
Before we just had the if key
equals escape, then love.event.quit.
But now on line 127,
we're going to check
to see if the key is
equal to enter or return,
and then we're going to use that as
our way of just testing state changes.
So we're going to say if the
game state is equal to the start,
once you press entered, the game
state should be equal to play.
Otherwise, set it back to start.
And we set it back to start,
we're going to re-initialize our x
and y to be in the center,
virtual width divided by two
minus two, virtual height
divided by two minus two,
and we're going to give it an initial
random starting velocity again.
And note here, this
math.random two equal--
is equal to one, and
100, or negative 100.
It's just Lua's way of
doing a ternary operation.
So in C, you will often have like--
you would be something like
math.random two equals one,
and you would have a question
mark, 100 colon, negative 100.
It's the same exact
thing, but Lua doesn't
have that sort of shorthand for a
ternary operation, so we do it with
and and or.
We use logical operations
instead to do the same thing.
And note here we're also showcasing
that math.random can take
either one argument or two arguments.
In this case, we're
saying math.random two,
which means it will give us
a value between one and two.
So a 50-50.
And then if we do
negative 50-50, that means
we'll get a value between
negative 50 and 50.
So a range of 100, effectively.
And so what that has the effect of
doing, if we run our application,
we go into Pong 4.
We're in the start state
so now we're rendering--
if we're in the start state,
it's set to render that message.
If we press Enter, the ball gets
a random veloc-- it's actually
applying the velocity frame by frame.
It's updating in the update method.
If we press Enter again, it gets reset
and we're back in the start state.
So we do it again, it's
getting a random value.
Do it again, random value.
Random value.
So every time we're getting a
different random ball value.
But what happens if we try to actually
run it, or try to interact with it?
Nothing.
Goes straight through.
So we're missing a key
piece, even though we
have the core components of
our game engine implemented,
we don't have any concrete game
play, nothing's interacting.
And that's a major piece
that we need to look at.
And so the next--
before we actually start
doing that, though, we're
going to take a look at
the class update, Pong 5.
And so in order to get
into more of a-- in order
to scale our code more
effectively, we need
to start looking in terms of classes.
And instead of having an x and a y for
our ball, an x and a y for our paddle,
a delta x, a delta y for our ball,
all these different variables that
are sort of all over the place
starting to bloat our code,
before we get too crazy with
it, we should think about
how can we put this data altogether
so that we can just think in terms
of our paddles or our ball object.
And so we use what's called a class.
If unfamiliar, a class is simply a
way of taking all these variables that
we've been using thus far, but putting
them together in a container such that
we can just say, paddle.x or paddle--
you know, in this case, car.
If we have a function called drive car,
now we can just say, car.drive instead.
We don't have to have functions
that are separate from our values,
that-- we can put them all together.
We can ask what's our car's
current mileage instead
of having all these different
variables all over the place.
So the classes are
effectively blueprints.
Use it-- you define a class.
You say, OK, my car
class is going to have
a-- it's going to have
a mileage variable,
it's going to have a paint variable,
it's going to have a make and a model,
it's going to have all these things, and
it's going to maintain its own state.
It's going to maintain
all of that for us.
as seen here.
And, typically, these are
what are called fields.
And then we'll have methods as well.
Functions that, instead of being like
completely separate from this data,
a car now basically
owns its own functions.
It has its own method called
drive, or turn, or honk, et cetera,
and we don't need to
have a function called,
like, turn car, or honk car, et cetera.
And then this class is
effectively a blueprint.
Well, we'll see shortly
how to define a class,
but in order to actually have like one
paddle that has its own set of data,
and another paddle that has its own
set of data, we need to define--
we need to instantiate, create
objects from this class.
Basically, use this class as a
blueprint, but take it to a factory
and create concrete
cars from the blueprint.
And those are objects.
And so as seen here,
our paddles and ball
are perfect simple use
cases for doing this.
So let's go ahead and
take a look at Pong 5.
So in Pong 5, immediately, if you
look at the directory structure,
you can see that we've added
a ball.lua and a paddle.lua.
And it's tradition in most languages
that have object oriented programming,
as it's called, to capitalize class
names just so you can differentiate
classes, for example, from concrete
objects or variables or functions.
So if you go to our
main.lua, on line 35,
we're requiring a library,
called class, which
is what's going to allow us to
actually create these classes.
Because classes are not native--
they are in a sense
a native Lua feature,
but Lua's way of doing object oriented
programming is a little bit convoluted.
Some folks have kindly put together a
library that makes it a lot simpler,
and a lot more closely related to
other languages that do object oriented
programming more predominantly,
like Java or C#, or even Python,
allow us to use the keyword class in
a way that's very similar to those
libraries.
On line 39 and 43, we're acquiring
our own code, paddle and ball,
and we're going to take a
look at those right now so we
can see what a class looks like.
So I'm going to go ahead and open up a--
the ball file, ball.lua.
And we can see here
all we need to do just
to create a ball class is, using
our class library, ball gets class,
and then curly brackets like that.
And so now we have a class
object, a class table,
effectively, because
everything in Lua is a table.
But we can think about
it in terms of objects.
We have a class object
called ball, and then we
can start to define functions
that belong to this class.
So we're going to define what's called
a constructor, or an init function
in this case, an initializer.
And it's going to allow us to initialize
our ball with whatever we want.
In this case, we want to start
our ball off with an x and a y
and a width and a height.
And notice within here we
have a word called self.
Self and this are common words in
object oriented programming languages
that mean whatever object we're creating
with this class is going to be self.
So we'll see that--
we'll see that shortly.
Self.x gets x.
So whatever concrete object
we create using this call,
this init call, set its x to this x,
set its y to the y, set its width,
set its height.
That specific object.
Self.
And then we're doing the same
thing for delta y and delta x,
only that we are setting those two
random values just as we did before.
Self.dy, self.dx.
That belongs to whatever specific object
gets instantiated using this init call
as we'll see in the code.
We're defining just a reset
function here just to make it easy.
Before we had a--
several lines of code that set our
ball to the middle of the screen
and gave it a random velocity.
We're doing that now, and
this is a good way of sort
of refactoring out groups of logic.
We're creating a function called reset
that just does that all in one function
call, and we just call that within our
main function, condensing our code.
And then notice we have an
update and a render function now.
And we are going to call
these from our own update
and our own draw function
such that every object
that we want in our game, every
entity, and we'll build upon this game
by game in the future.
We'll just call update and render
on everything from our main.lua,
and defer all of that to each
individual class and objects
so we don't have to have a main.lua
that's like 800 lines of code.
We just break out all of the updates
that are pertinent to the ball here,
and all the render code that's
pertinent to the ball here,
call each individual
balls update and render,
and save ourselves a lot
of time in refactoring.
We're doing the same thing
if we look at paddle.
Paddle's a class, as well.
It gets the reason the class library.
Same exact sort of thing here,
xy with height, and a dy.
In this case, we're just initialising
that to zero so that we're not moving.
And then we're calling
the update function here.
So if our dy is less than zero, we're
using the math.max function as before,
with the top edge of the screen, and
then whatever our y plus our current dy
is, so delta y.
And then here, self.y gets
math.min, virtual height
minus self.height, self.y
plus self.dy times delta time.
So that's the clamping behavior that we
saw before with the paddles, only now,
we took it from main and we
put it in our update function
so each paddle calls its
update, and we take some lines
of code out of our main file.
And then it has its own render
function here, same as the paddle.
The render function for the paddle
and the render function for the ball
are effectively the same.
And so if we go to our main,
we-- we're acquiring the paddle
and we're acquiring ball
so that we can use them.
So if we go down to line 79, instead
of initializing our ball dx or ball dy,
ball x, ball y, paddle
y, player 1y, player 2y,
now, we have player one is
simply paddle 10, 35, 20.
And player two is a paddle,
virtual width minus 10,
virtual height minus 30, 520.
Ball is a ball, virtual width
divided by two minus two,
virtual height minus two,
minus two, [? five by ?] two
minus two, four and four.
So those paddles now have control over
their own x and y, their own width
and height, and the battle-- or the ball
has its own control over the xy width
and height.
And the self applies to this object.
This is whatever self was in our
constructor that we saw before.
So even now we can just
call-- so we can simply
say player 1.x player 1.width, player
1.y, and everything is contained.
We don't need a million
variables to keep track
of all the things going on in our game.
And this is going to be especially
important as we scale, and we have--
maybe we have 100 things
on the screen at one time.
We don't want 100
times x variables where
x is, however many properties that
thing has that we need to keep track of.
It's all the same logic
except, now, we're
calling player one update and player
two update in our update function,
instead of having all
that logic therein,
where they're moving and
then keeping track of whether
or not they're going past the top
and bottom edges of the screen.
And then if game state is play,
we're now just calling ball update.
And these are all getting
passed in delta time.
And then same thing here.
Instead of having all that
logic for restating the ball
as one block of code, we took
it out, we refactored it,
we put it into our ball class,
and now all we have to do
is just one line of code, ball or reset.
And then here down on line
169 in our draw function,
we just have player one render,
player two render, ball render.
And later on as we
scale and we make games
that have a lot more things on
the screen, a lot more entities,
we can just do these renders in a loop.
We can just say for each entity
in our screen, just render it.
For each entity in our
screen, just update it.
We can condense thousands--
hundreds of lines of code
into just a few lines of code
by deferring update logic
and rendering logic to
each individual entity,
thanks to object oriented programming.
And so that's how we're going
to refactor using classes.
So any questions on how
any of that works so far?
Cool.
This is a good point, I think,
to take a five minute break.
And once we come back, we'll talk
about how to look at frames per second.
All right.
So we're going to take a minute just
to look at something kind of small,
but often it's the case
where in games if we
want to make sure that we are performing
like our applications performing well,
we want to-- some way to
monitor our frames per second.
And so I figured I
would just take a second
to illustrate this quickly so that
we can use this in the future.
The two functions that are going
to be important for us here--
well, the first of these is just
a little small cosmetic addition
to the application.
It's just love.window.setTitle.
Title, so far our application,
I'm not entirely sure what the--
it says by default, I think it says--
what does it say-- untitled.
Yeah.
So that's not-- it's a layer of
lack of polish, more or less.
And it'd be nice just to
solve that problem quickly.
So we're going to call a function called
love.window.setTitle, some string,
which will solve that problem quickly.
We can make it look as if
we have that detail down.
And then the thing that's actually
going to let us determine whether or not
we are running well or
we're running very poorly
is a function called
love.timer.getframespersecond.getFPS,
which is something that LOVE
graciously gives us for free
and allows us to very easily
slap it wherever we want.
We can print it to the
console, or we can just
draw it straight to our application.
In this case, we're
going to do the latter.
So I'm going to go
ahead and go into LOVE--
or Pong 6 in our main.
If we go ahead and look at line--
where is it-- line 64.
love.window.setTitlePong,
just quick and easy.
Now, our window header
is set appropriately,
and if we go down to line
198, here I've decided
to sort of split out this
in a separate function,
called display FPS on line 198.
And the function is defined on line
207, so a function display FPS,
takes no parameters.
Its only goal is to just draw
our current FPS to the screen.
So we're going to set our
current font to a small font.
We're going to set our color-- so this
is what I alluded to before in that we
can set LOVE's rendering
color to some RGBA quadruple,
and anything that we draw beyond
that point will then be drawn at--
it'll be drawn into
whatever that color is.
So, in this case, we're giving it red of
zero, 255 on the green, zero blue, 255
fully opaque, which has the effect
of setting our color to just
completely green.
And then love.graphics.print,
our current FPS--
string, and then our current FPS
here, which is love.timer.getFPS.
But it's going to return that
as a number, and by default,
Lua does not allow you to
concatenate strings and numbers,
so we're going to concatenate
here with this ..operator,
which is the way of doing
string concatenation in Lua.
We're going to call the
two string function.
So we're going to take
in love.timer.FPS,
we're going to make
it a string, and then
we're going to concatenate it here.
And then we're going to call
love.graphics.print on that value,
and then we're going
to put it at 10, 10.
So shift it just a little bit from
the top left edge of the screen.
So that's going to have the effect
of the go to Pong 6, and we run it.
We can see now it starts
at zero and 52 because it
has to gather a few
frames of data before it
has a number we can actually use.
But we see there FPS at 60, and so our
game runs, otherwise, just the same.
Completely random.
A little bit broken, but
that's OK, we'll fix it up.
But currently we have
a problem, and that's
that our ball is just going
straight through our paddles.
So how can we fix this problem?
We need some way of detecting collision.
So in 2D games, generally, there's a
concept of aa bb collision detection.
And what this is is axis aligned
bounding box collision detection, which
means that we have bounding
boxes, just rectangles, quads,
which have an x and a y and a width
and a height which are nonrotated.
So they're completely
aligned with our axes.
They're completely
parallel perpendicular.
So the only way that we can get
this easy math, the aa bb collision
detection working is if we
have no rotation of our boxes.
They have to be completely aligned.
But if they are, we have
a very simple algorithm,
which is we're just making
sure that no edges of our boxes
are outside the opposite edges
of our-- of the other rectangle.
So if we have one rectangle-- and I'll
illustrate this on the screen here.
We have two rectangles.
If this top edge is
below this edge, we know
no matter what, they're not going
to inter-- they're not intersecting.
There is no way it can
because it's below here.
So no matter where it is on the
x and the y, if it's below here,
it's not a collision.
If this edge is on this
side of this rectangle,
we know, as well, there's no
way those two boxes can overlap.
And it applies to every edge as
long as it is the opposite edge.
So if this edge is below this one,
if this edge is above this one,
if this edge is on the right,
and this edge is on the left,
it means that no matter what,
those boxes aren't colliding.
So we can simply do four conditions.
We can say, if rec1.x is not greater
than rec 2.x, plus rec2.width,
and rec1.x plus rec1.width
is not less than rec2.x,
so if the two edges are not beyond their
opposite edges, same thing with the y,
and the y plus rec1.height, we
know that we have a collision.
We know that because we haven't
fulfilled any of those criteria.
But we know that if
that's not true, if the--
one of the edges is not beyond
the opposite edge, then it's--
we do have a collision.
So it is going to be true.
So we'll see that here in our code.
The go to Pongs 7, at line
113, we have a function
that we're calling called ball collides.
Our ball class has a
function called collides.
So let's go ahead and take
a look at our ball class.
And in this case, we've
defined our function such
that it takes in a
paddle parameter, so it's
going to compare against
another rectangle that
has an xy and a width and a height.
And we're saying that if rx is greater
than the paddle x, plus the paddle
width, which means if rx is
greater than the right edge.
So if our top left is greater than the--
or just our left-- is
greater than the right edge,
we know that we can't collide.
Same thing if it's greater
than the other rectangles,
self.x plus self.width.
No, sorry.
In that case, if the paddle's x is--
it basically the same operation
but from the paddle's perspective.
If the paddle is greater than
the rectangle on the right side,
if it's farther along the
right side past the right edge,
we know that there can be no collision.
It's just impossible.
Same thing with y.
If the y-- self.y, so this ball is y--
is greater than the paddle's
y, plus the paddle height.
So if it's below the edge
of the paddle, because we're
taking the height into consideration,
or if the paddle's y is greater
than this ball's y plus
self.height, then we
know that that also
can't be a collision.
But if that's not true,
then we need to return true.
And so if we go back to our
main-- no, that's the wrong main.
We go back to main.lua here.
We're calling ball.collides.
So if we're in our game state,
if we're in our-- sorry,
if we're in our play state, if
game state is equal to play,
if the ball collides with player one,
so player one is the left paddle.
So if there's a collision detected,
the ball.dx and dx is our x velocity.
So it's whatever direction
it's moving on the x-axis.
So it's going to be moving
to the left if it's gone--
if we detected a collision.
And it doesn't matter whether
it's moving left or right,
but we needed-- what we need to do
is set it to its negative value.
Because if it's moving
left and we said--
let's say it's moving left at its
negative 20 pixels and we set to 20,
the dx is now 20, it's going
to start moving to the right.
It's going to have the effect
of inverting its x velocity
and, therefore, reversing its direction.
But what we're also doing
here with times 1.03
is we're multiplying a little
bit just to speed up the game.
Because we don't want the game to into
perpetuity just have the same velocity.
It's not going to ramp
up the excitement.
We want to keep things going, we
want to get some momentum going,
so what we're going
to do is call ball.dx
equals its negative value times a scaler
that we've determined arbitrarily.
In this case, I've decided
it should be point--
1.03 so it'll increase
it by 3% every time.
And then in the event that we have a--
our ball-- because it's getting
added, its x velocity is getting
added each frame to
its position, we want
to make sure that it's not
like inside of our paddle.
Because it is possible that it could
shift a certain number of pixels
to the left, or to the right,
because the same operation applies.
Such that the two are sort
of like on top of each other.
We want to re-- we want to
shift it, we want to reset it.
So what we're going to do-- because
it'll detect another collision
immediately if that's the case.
If it, on the next frame,
it's within that paddle,
it's going to say that it's
still colliding with that paddle
so it's going to shift
its velocity again.
And it's going to have the effect
of it infinitely sort of bouncing
back and forth within the paddle.
We don't want that to happen.
So if we detect a collision,
we want to shift it.
We want to make sure it's completely
outside of the paddle's collision box.
So we're saying ball.x gets
player one.x, plus five.
Plus five because that's
the width of the paddle.
So that has the effect of just once
you detect a collision, negative set--
x velocity to negative,
and then instantly
shift it right on the right
edge of the left paddle.
And we're doing the same thing here.
If ball collides a play or two, we're
doing the-- we're negating or inverting
its x velocity.
And then-- this is the
same exact operation,
but since it's based on the--
the left top left corner,
we can't minus it by five,
that wouldn't make sense.
We're going to minus it by four
because that's the width of the ball.
So if we minused it by five, we
would have one pixel of space.
We plussed it by five on
this example, because we're
coming in from the right side.
We want to just make sure that it's
right on the right edge of the paddle,
so we're setting it to player one.x.
And in this case, we're
using the minus four
because that's the width of the ball.
So we want to shift it to the
left, the width of the ball,
and that will have the effect of the
right paddle, if there is a collision,
it'll just get shifted over,
and the right-- the ball
will be touching the paddle
right on their two edges.
Here on line 118, we're solving
the problem we had before of what
happens when the--
oh, sorry, that's actually
not what I was thinking of.
This is the-- if there's a collision,
then we want the ball's y velocity
to randomize every time.
So this has the effect of
when we're playing the game
and we've detected a collision
between the two paddles,
we don't want the same angle
back and forth every time
because then the game will just
infinitely take place the exact same--
the same angle will just keep
happening over and over again.
We don't that to happen.
We want some variability in terms of
how the ball bounces off the paddle.
So what this does is,
still within the condition,
if the ball collides
with player one, we're
going to, say, if the y velocity
of the ball is negative,
then we want to keep it going negative.
We still want the ball--
like if the ball's coming
at a sort of an upward angle
and it bounces off the paddle,
we want the x velocity to shift.
We want it to go to opposite direction,
but we want the ball to keep going up.
We don't want the ball to
like bounce back down, which
wouldn't make any sense.
We don't want to negate the y velocity.
So we're going to keep
the y velocity negative,
we're going to set it to a
negative value between 10 and 150.
And it's just arbitrary.
You can set that to whatever you want.
And then we're going to do the same
thing if the y velocity is positive.
We want the ball to--
we want the ball to go in the positive
direction if it's already coming down.
So we're doing the
exact same thing here.
It's the same logic in
the player two instance.
And then this was what I thought I
was looking at before for a second,
but this is how we fix the issue of the
upper and lower boundary of the screen.
Right.
Because it's one thing to solve
the fact that we have the paddles
now deflecting the ball,
but we don't want the ball
to infinitely go above the
top edge of the screen,
or the bottom edge of the screen.
So this is just a simple if condition.
We're just saying if the
ball's less than or equal to
zero, which means if the ball's
at the top edge of the screen,
just set it to zero,
so make sure it doesn't
go above the edge of the screen,
and then negate its wide velocity,
so it's instantly going
to start going downwards.
Yes.
AUDIENCE: This question
is about Pong 7, line 113.
Couldn't the shifting
of the balls dx and y
be done in the ball collides
function, if there is a collision?
COLTON OGDEN: The shifting of the
ball's function if ball collide-- no,
collides the ball--
the collides function
is a-- it just returns true or false.
So it would be--
I mean, I think you probably
could refactor it out that way,
but the purpose of
collides isn't to have
any sort of side effects like that.
Its only purpose is just
to return true or false.
Because we can do any-- we could
have any sort of behavior we want.
In a collides function, we may not
necessarily want to shift the ball
or do anything, we might
just want it return true
and print something to the console.
So, in terms of, I think, in
an engineering perspective,
it makes more sense just to have
a simple true or false function,
and then determine how you want
that to actually influence your game
state inside your main function,
or inside some other function.
OK.
And so, Yeah, we went down here.
The top edge of the screen, and
then bottom edge of the screen.
If the ball.y, it's same exact thing,
just the bottom edge of the screen.
If the ball.y is greater than or
equal to virtual height minus four,
and we're doing virtual
high minus four, why?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: Exactly.
So we want to make sure that
we write-- as soon as we--
the bottom edge of the ball
touches the bottom of the screen,
we want to detect a collision,
then we want to say ball.y, the--
gets virtual height minus four in
case it overshot the bottom edge
based on how much time has elapsed
and how much the velocity is,
you want to instantly put it
right up so that it's at the edge
so it's a clean bounce.
And then we want to negate
the y velocity just the same
as we did up above.
And so if we run our program
here, Pong 7, looks the same,
but now the ball's bouncing.
And note that it got a
neg-- it got a random-- it
looks like it's going
below the bottom edge
because the monitor is currently at
720 and that's the window resolution,
but it is bouncing off
the bottom edge as well.
And the angle, if you'll note, is
a little bit different every time,
because we are giving it
a random y velocity, a y--
yeah.
And then that's influencing--
oh, I messed up.
I wanted to illustrate
the speed increase.
It's going to take a little bit of time.
But every time it detects a collision,
it is going to be scaling its--
the x velocity by 1.03.
So it's going to make
it a little bit faster.
Now, currently, the y
angle's a bit steep,
so it's going to take
forever to illustrate that.
But we'll see that in a later example.
So we have the basics of our game.
But how are we keeping score?
What's the determining factor
for how we keep score in Pong?
Left or right.
As long as it goes past the left
or right edge of the screen.
So what do we need to thereby do?
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: We do
need a counter, and we
need to also monitor
whether the ball has
collided with the left or the
right boundary of the screen.
And then have that
increment that counter.
So we're going to go ahead
and take a look at Pong 8
to see how this is implemented.
We have here on line 88 and 89 some
counter variables, player one score,
player two score.
We've had those for a long
time, but we haven't used them.
We've only used them
to draw to the screen.
We're actually going to now increment
them, and show them as scorekeeping
variables in our code here.
I thought I had
implemented it in Pong 8,
but I think I might have left out
the actual incrementing of the score.
But this is the logic that's
pertinent to that example.
So if ball.x is less
than zero, which just
means if we've gone past
the left edge of the screen,
ignore serving player for now.
The important thing is now we
are doing player two score,
gets player twp score, plus one.
Just a simple increment.
And then we're resetting the ball.
Same thing for here.
If the ball.x is greater
than virtual width,
so pass the right edge of the screen,
and actually it could be ver--
if ball x plus four is
greater than virtual width,
then and it will have the same effect.
But, actually, no, because
we want to make sure
that we don't see the ball
at all when they score.
So, yeah, this is actually correct.
If ball.x is greater than virtual
width, then serving player gets two,
player one score is
player one score plus one.
And then we're going to reset the ball.
Serving player.
So now what we need to talk
about is the idea of serving.
So when we start up the game--
so let's go ahead and take a look at--
we're going to go to Pong now so
we're going to go straight to Pong 9,
and then we need to take a look
at what a state machine is.
So currently in the game, we've
talked about state a little bit.
We've had the start state, which
means the game is ready for us
to just press Enter and then the ball
will go off in a random direction.
And then we have the play state.
And the play state is set to our
paddles interacting with the ball,
and then keeping track
of score, basically.
A state machine is very important.
It's a ubiquitous concept
in game development.
It just means, how can we
monitor what state we're in
and what transitions take place between
those states to bring out new states.
And each individual
state has its own logic.
And by breaking out the logic
of these states separately,
we can scale our code much bigger
and not have monolithic code for--
this particular diagram is an example
of what you might have as a state
machine for a character like Mario where
you have a ducking state, a release
state which takes the down--
the in-- like the input of down.
So if we're releasing down,
it'll become standing.
So ducking state, the transition
is release the down key,
he becomes standing.
Standing key, press the down key.
He becomes ducking, these
are states and transitions.
These individual states are the
overall representation of his behavior
at large, basically.
And the same logic applies to our game.
We have a play state,
we have a serve state.
We want to have maybe a game over state.
If someone scores 10 points, then
it should say, oh, the winner is x.
And you can define any
arbitrary number of states, it--
which depends upon your model,
whatever game you want to develop.
For example, like Super
Mario has a title screen,
maybe your game has
like a high score state.
You want to display all the
high scores in your game
and we'll actually show
that in a lecture next week.
But this is what a state machine is.
It's just a-- it can be in any
one particular state at one time,
and the transitions are what allow
you to go in between your states.
And each state does have transitions
in and out of other states.
And we're going to use this in Pong 9.
So beyond illustrating
the score, we're going
to start keeping track of more than
just the start and the play state.
We're actually going to start
modeling the serve state.
And so let me go ahead and
illustrate what this looks like.
So if we're here, I just pressed Enter.
We started at the start state
as normal, but I pressed Enter
and now it says player one serve.
So we're actually serving.
And so if I press Enter
again as it instructs me,
player one is on the left, the
ball should move to the right.
Which it does.
So I'm going to go ahead and
lose on purpose as player two.
And now it's player two's serve.
So whichever character, whichever
player loses should get to serve again.
And so now if I press
Enter, note when we
were player one, the
ball moved to the right.
So for player two, the
ball moves to the left.
So we have now a little
bit more interaction.
We have different states.
We start off the game, and
then we serve, and we play.
So when the ball's live, when
we're actually doing this,
we're in the play state.
Now we're in the serve state.
So what's the transition between the--
sort of the play state
and the serve state?
What's the transition there?
We score a point.
So if we are looking at our state
diagram and we're in the play state,
the transition to the serve
state is x player scores a point.
And then if we're in the
serve state, the transition
is someone presses enter.
Enter key gets pressed.
And so that's how we want
to think about our games
if we have a bunch of different
sets of, sort of logic,
that we can sort of take out of our
game and think about conceptually,
it allows us to break our game up into
a bunch of different modes and states,
and not really get overburdened
by all these variables
that maybe need to keep track
of-- or what state are we in?
Like what are all these variables doing?
And we'll see how we can
break this out in a more
modular fashion in future weeks.
Note that right now,
currently all we're doing is
we're setting a state
variable to some string here,
and just doing if
conditions on it, which
works fantastically for small examples.
So if, like, for example, if we're in
the update function and game state--
see, over here we're saying if
the game state is set to serve,
then we're initializing
all of the variables.
And if the game state
is play, then we need
to actually perform our logic here.
So if the-- if we're in play,
this is going to get called,
each frame, and we're going to say,
if ball collides with player one,
do all this stuff.
And then this allows us to
sort of think of our game.
It's almost like having separate update
functions within our update function.
And we'll actually see how we
can take these out of one update
function in future weeks with
a actual state machine class
and implement things a
little bit more abstractly.
But suffice to say, now, whenever
we want to like, for example,
make a transition, if
someone scores here,
like if we're going to the
left side of the screen,
it's ball x is less than zero.
All we have to do is just
set this state to serve
and our update function is then
going to update appropriately.
So any questions on how sort
of state machines or the state
works here in the context of Pong?
Yes.
AUDIENCE: So the state machine
is the relationship to the state
or is the container of the states?
COLTON OGDEN: The state
machine is sort of a--
the overall conceptual look
at what your different states
are and their transitions, yes.
And in future weeks we're not
implementing a state machine
object or a class here.
But in future weeks, we will
see the state machine class
that manages transitions between
different states in a more
modular and clean fashion.
All we're doing here
is our state machine
is just if statements and saying if the
state is equal to this, then do this.
And then change the state to some value.
AUDIENCE: So the state
machine is a concept?
COLTON OGDEN: It is a concept.
Yeah.
But we will see an implementation of a
state machine as an object next week.
Any more questions?
OK.
Cool.
So currently we have scoring.
As we saw, the player one score
and player two score are getting
incremented now and, therefore,
getting rendered to the screen
whenever we go to the
left or the right edge.
So we're keeping track of score.
But what do we need now in
order for someone to win?
AUDIENCE: [INAUDIBLE].
COLTON OGDEN: Sorry?
AUDIENCE: [INAUDIBLE].
COLTON OGDEN: Yes it does.
Exactly.
So it's actually quite simple.
All we need to really do is
just an if statement, right?
If someone's score is equal to some
value, 10, then some player has won.
So if we look at player-- if you
look at Pong 10, we go to main,
and go to here, line
174, and also line 160,
we can see that all it
literally is is in our logic
from before, where we're just
testing to see whether the ball is
gone beyond the left or the right edge.
Because this is effectively where
you need to do your check anyway
to see whether someone scored a point.
So all we're doing is adding
logic to that part of the program
and saying after we
increment their score.
If it's equal to 10 we're setting
a value called the winning player.
We're going to set it to two.
So if the ball.x is less than zero,
then that means that player one got
scored on because it went pass
the left edge of the screen.
Therefore, player two
score should go up.
And, therefore, the winning player
should be, too, if the player two or--
two score is equal to 10.
And in this case, see, we're here,
we're setting a new state done,
and then if that's not the case, or
if their score is still less than 10,
we should still-- we should set it
back to serve, and then reset the ball.
And so if we go to our
update function, here--
actually, we're doing
it in our update phase.
So, currently, if it's the
done state, the ball gets reset
but no update is being applied
to the ball in that case.
We still have scores 10.
It'll still render the score,
so score whoever's got 10
and it'll show the other player's score.
And the actual logic that
applies here, whenever
we want to break out of that state, is
in our love.keypressed key function.
We see you're on line 227.
If game state is equal to done,
which we set it to before,
and this will only
execute if they've pressed
Enter or return, so
it's effectively waiting
for them to press Enter or Return.
You want to set game save back to serve.
We want to reset the ball.
We want to initialize
those scores back to zero.
So we're setting up a brand
new game, effectively.
If the winning player is one, then
we'll give serving player to two
so that they have the
advantage on the next game,
and then otherwise set it to one.
And if we go down to our render
function, so down to line 275,
if we're in the done state, then we
should render to the screen player
and then winning player,
because, remember,
we set winning player to one
or two, depending on whether--
depending on who won and
who scored the tenth point.
We'll say player one or two wins,
and we'll just render that and then
press Enter to restart after that.
And that's the logic for that.
And we can see this in playoffs.
If it's too slow, we might not
have to go through an entire run.
But I sped up the--
whoops-- I want to
actually get the ball back.
I set up the speed so it's--
we're in the serve state,
we're in the play state, it's
up-- the ball bounced back.
It's going to be a bit tedious, but
suffice to say, it's a big payoff.
Don't worry.
Should have set the
speed a little faster.
Almost there.
It's getting tense.
And player one wins.
So there we-- we're also setting the
font to a larger size, and in the code
I create a new font
object that's basically
between the small font and the
score font, which is a large font.
So 16 size font.
And so player one wins, and that's
really all it boils down to.
Just keeping track of your counter
and just making sure that when you do
hit 10 in your logic for
detecting the screen collisions,
that you set the state to done.
And if the state is done, then you
just need to monitor keyboard input
and see whenever someone presses enter.
Someone press Enter in
our love.keypressed,
it does the-- it has
the effect of setting
player-- it's player two serve because
player one won so that's only fair.
We're going to press Enter to serve
and then we begin a brand new game.
And that's simple.
So now we have an infinitely playable
game with a bunch of simple states.
We're missing a very important
detail, though, in my opinion,
and that's sound.
Currently, our game is just very--
it's great, the gameplay all works.
Everything is working fine, but
just missing a little polish.
And so what we're going
to do is we're going
to start adding audio to the
game, which is, in my opinion,
one of the more fun things to
add because it also means you're
close to the end of your project.
Love.audio.newsource is a function
we're going to look at here.
All this is going to do is take a
path and then optionally a type.
And this path is going
to be to a sound file,
and it's going to create an audio
object that you can play back
at any point in your application.
So what we're going to effectively do
is just whenever a collision happens,
depending on what type of collision it
is, we'll just play a particular sound.
And a program that I really
like to use for all of this,
and I-- what I encourage
you guys to download
if you want to start tinkering with
your own sounds for your project,
is a program called bfxr.
It's free on Windows and Mac.
I'm not sure if they have a Linux port.
They might have a Linux port of
a similar program called sfxr
which is what this is based off of.
But what this allows you to do is just
generate a bunch of random sounds.
And I can illustrate
that shortly for you.
If you would like to grab
it, it's on bfxr.net.
It's a super quick download and--
here, I'll actually-- I'll demonstrate
it just so we can see how it plays out.
So this is the interface.
Make sure I have some audio.
And then there is a lot
of different presets here.
So there's pickup slash
coin, laser slash shoot.
It's meant for sort of
like small games like this,
like implementing on the fly
prototype audio type stuff.
But you can see, it just implements--
I'll turn that down, it's a little loud.
It-- we have power-ups, for example.
So every time I click on this, it's
going to give us a random power-up so.
[COMPUTER SOUND EFFECTS]
And then randomized, you get
all sorts of weird nasty stuff.
And then-- the stuff that
we'll use is blip slash select.
Most of the things in our in like
interfaces and games like Pong you
just want simple sounds like this.
So I've already done the work
of generating a few sounds
that I thought fit pretty well.
I'll go ahead and show you
the code first in Pong 11.
If we go to-- if you'll see-- if
you look at the directory structure,
you'll see we have a sounds folder.
In the sounds folder, I've created
three sounds, paddle hit, which
is anytime the paddle hits the ball.
Score, which is when
any-- anytime the ball
goes past the left or the
right boundary of the screen.
And then wall hit, so
any time the ball touches
the top or the bottom of the screen.
And so the logic of this
is extremely simple.
All we need to do is whenever--
we already have it implemented,
so all we need to do is--
oh, first thing, I should say, and
this is a good illustration of a table.
And we'll start to see this
a lot more in the future.
We didn't really use tables
much in this lecture,
but the table is Lua's like sort
of be all, end all, data structure.
It's the dictionary-- Python
dictionary, JavaScript object.
It's an array, it's everything
that you need for anything
beyond simple variables in Lua.
It's what everything, even like classes
in other libraries are made out of.
In this case, we're just initializing
a table here called sounds,
and we're passing in three keys so it
takes-- it can take in key value pairs,
or you can just give it a list of values
and it will create indices for them
implicitly.
Here, we're just passing in like you
would do in Python or JavaScript.
Paddle hit, and note that it does
need these square brackets in order
to initialize key value pairs in a
table like-- in this format here.
Paddle hit gets love.audio.newsource.
And in this case, it
just takes in a path,
so sounds slash paddle head dot wave.
And we're giving it the key
word-- or the string static,
which is the type of asset it is--
it's stored as.
So you can have either static
or stream audio assets.
So if they're static,
they're loaded in memory
and they're kept in memory for
the execution of your program.
If they're stream, then they're
loaded on the fly as needed
by your game engine.
And streamed audio assets
can be helpful if you
have a huge game with a ton of sounds
and a long like large audio files.
You don't want to keep all
those in memory, necessarily,
because that could take up many,
many, many megs or gigs of audio.
And if you're sort of
loading assets on the fly,
if you have dynamic
loading in your game,
then that's another thing you
should take into consideration.
In this case, these are
very tiny sound files,
because they're like-- like
a fraction of a second long.
So we're just setting them all to
static so they get preserved in memory,
and we're just loading all
three of them into this table.
And if we want to refer to these later
on, all we need to do is sounds--
we can either reference
sounds.paddlehit, like that,
if we wanted to.
Because by default, Lua just gives you a
dot keyword, sort of the way JavaScript
does its objects, with the same
name as the key that you passed in,
or you can do it the Pythonic way,
which is, without the dot, sorry--
with just square brackets and now have
the same of-- those two are equivalent.
It won't work, though, if you
decided to put a space in your key,
it will, I believe, it will--
just won't work at all, but
it may inject an underscore.
I'll have to test it out and find out.
But, generally, it's not
best practice to use dot
when you already have the keys
lined up like this, anyway.
And what you can do
with strings that you
can't do with dot
using the dot notation,
is dynamically generate a
lookup of your table with you--
which you can do with
strings, which you-- yeah,
because you can't in a four
loop do four something in table,
and then table dot something,
that just won't work.
But you can do for
everything in your table
and then look up the key as that value,
that iterated value in your table.
We'll see examples of
that in future lectures.
But that's just something
to keep in mind.
So we have our table
here, a sounds table.
Oh, and-- and we have our sounds
ready, they're loaded in memory.
All we need to do now
is wherever we have
anything that, like any collision
in our code, we just do this.
It's as simple as the table.
At the key that we want,
colon, which is Lua's
way of calling a function
of a class or a table.
Colon play, and the play function
is part of the new source audio
object in LOVE that we
created in the table,
and that will just have the
effect of just playing it once.
You can set it to looping.
You can say the sounds paddle
hit, set looping to true,
and it will just infinitely
play over and over again,
which we wouldn't want
for a sound like this.
It would sound obnoxious.
But if you have a music
track, for example,
in a level, or something
like that, you would
want set looping to be true
so that when it finally ends,
your user isn't just
playing a game in silence.
So we're doing it with paddle hit,
we're doing it with wall hit as well.
So I've named them appropriately
so that it's easy to infer where
and for what purpose the
sound files are used.
So whenever they're at the upper
or lower boundaries, play the wall
hit sound, and then
whenever the ball reaches
the left or the right
edge of the screen,
just play the score sound effect.
And so if we play our game, and this
is always one of my more favorite parts
is playing the game with
audio because it just makes
such a difference, in my opinion.
We get sound effects.
It's a little thing, and it's very easy,
but it adds-- it adds so much flavor.
And then (explosion), and there we go.
And then our game is practically
implemented at this point.
There's just one more
example that I would
like to show you guys, a small
example, because all of the examples
thus far have had the resize
equal-- resizable equals false.
Sort of key in the push
setup screen initializer,
and in case you want to have a game
where you can resize your window,
all we need to do is call a
function called love.resize,
which takes a width and a height.
And what we're going to
end up doing with that,
specifically, for our use
case, because we're using push,
we're going to go to Pong 12.
And then if we go to main.lua
we see here on line 85,
I've changed resizable to equal
true now so that it will actually
allow us to resize the application.
If that's false, you won't
even be able to click and drag
the bottom corner of the screen,
it just won't let you do it.
And then all you have to do is
call love.resizewidthheight,
and then pass in push
resize with height.
Because push underneath the hood
takes a texture and renders to it,
and then upscaled it
to fill your window,
and so it needs to know what your
current window dimensions are so
that it can upscale it to
fit the right dimensions.
And push also adds
things like letterboxing,
which is convenient if you want to
maintain the exact same aspect ratio.
And in a game where maybe
you have the UI that's
driven by the size of your
application, this function
will be important because
then you can resize your--
you can resize and reposition
your UI elements appropriately.
Because if your game is small,
maybe you want certain parts of UI
to be invisible, or in
a different location
altogether, just so that you don't
take up a ton of screen space,
and just to accommodate all
possible users of your application.
But that has the effect now
of-- if we go into Pong 12
and then run it, actually, might not
even be able to use the but-- yeah,
I can just do this now.
I can resize it, and it'll maintain
the virtual width and height
that we set it to before, because
that's like first and foremost
what push will do and
it'll letterbox no matter
what size your application is to make
sure that it maintains that aspect
ratio.
So if you're beyond that aspect
ratio vertically or horizontally,
you'll get the appropriate
letterboxing for it.
So it's super convenient.
You don't have to worry about your users
getting super distorted aspect ratios,
because they are using some
sort of unforeseen resolution.
That will always maintain
it even if it's super tiny
because their monitor is super thin.
It will always maintain
the aspect ratio.
But that's pretty much
it for Pong, actually.
We have a complete game, start to
finish, and if you have any questions,
I'd be happy to answer them.
Any questions?
Cool.
All right.
Well, I'm excited to teach the
rest of this course to you guys.
We've only scratched the surface.
We have a lot more to cover.
Next week, we'll actually
be covering Flappy Bird
so we'll get some nice
colorful graphics, which
is a stark difference to our
black and white aesthetics today.
But that's it for Pong.
So thanks for coming.
