Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 45

[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: Could get stuck at the
bottom. 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.

You might also like