COLTON OGDEN: Hey, guys.
Thank you very much 8 coming.
My name is Colton Ogden.
I work full time with CS50.
And I'm going to talk to you guys
about a framework and language that I
recently started using in
hobbyist's game development
that I really started to like.
If I want you to follow along, I have
the set of slides I'll be using today
which have a lot of important
links at cs50.ly/lua.
Two of these links here are
getting LOVE 2D itself which
is the game framework we'll be using.
It has a download link and
then a wiki page for getting
started which has a few tutorial
pages and stuff just to provide you
that on-ramp David was talking about.
And then I also set up a GitHub repo.
So you guys can follow along.
I'll be running a bunch of demos today.
And the nice thing I
like about this repo
is I set up several
exercise folders as well.
So if you guys want to try and implement
a lot of the things we'll be talking
about today, you can do so on your own.
And I've written out like to-do's with
hints and stuff to get you started.
So let's talk about first what Lua is.
So Lua is the language
we'll be using today.
It's a early 90s language
invented in Brazil.
It means moon in Portuguese.
And it's a very light,
small language that's
focused around this data
structure called a table.
Which if you're familiar with Python,
it's very similar to a dictionary.
If you're familiar with JavaScript, it's
very similar to a JavaScript object.
And that's basically most of what
you do in Lua revolves around the use
of this table data structure.
Lua was invented in the 90s
mostly to provide a glue
layer for larger applications.
So back in the 90s and even in the
80s, game code bases were essentially
written in compiled
languages like C or C++,
which could be very monolithic and
compile times for such could take
upwards of several hours.
Rather than rely on
editing the C or C++ code,
designers at the time would much rather
write in a dynamic programming language
like Lua where you can have the game
code exposed certain methods like
drawing to the screen et cetera that
Lua then called dynamically and not have
to recompile your application.
And Lua was very popular
in the video game industry
because it could provide this
glue layer for large code bases.
And if you use JavaScript at
all, it's very similar to that.
You'll see some similarities.
And the nice thing about
Lua is its main purpose--
well, one of its main purposes was
also used as a config language.
So it's excellent at storing
data as well as code.
And the two can sort
of be paired together.
So let's go ahead and look at a
syntax of Lua that I've set here.
If you're in the GitHub repo,
it's at the parent level.
It's called lua_demo.lua. demo Lua I'll
just be going over some of the syntax
here.
So to create a variable,
it's very simple.
It's just some variable name,
gets, and then some data type.
Variables are dynamically
typed, so you don't
have to provide-- you don't have
to say int or string or whatnot.
You can just assign
it whatever you want.
There's two different
types of variables.
Global variables have no
specifier in front of them.
As opposed to local
variables-- like here a local
some variable-- and that just means
that it's local to that scope.
So if you're in the middle of
like a for loop or a while loop,
that variable is only going to
be accessible within that loop.
And if you don't specify local,
you can use that variable anywhere
in your application.
This is how you define
a function in Lua.
So like in JavaScript,
you say function and then
some name with its arguments,
its body, and then it
ends with an end keyword,
which is different than a lot
of other languages.
If you're familiar with Bash, it's
sort of similar in that spirit.
This is how you would call a function.
Just the name of the function,
and then whatever arguments
you want to pass to it.
In this case, hello and
world are two variables.
And we're concatenating them
with this dot dot operator.
That just means take two
strings and put them together.
And then we have if
statements which take
the form of if some condition
then, which is a Lua
keyword, and then execute this body.
Else execute this body and then end
like we did up above with the function.
Loops are similar to other
languages with the while form.
While some condition, do this body.
So in this case, we're setting i to 10.
While it's greater than
zero, decrement it.
I get's i minus 1,
print it and then end.
And then a for loop is
similar to other languages
as well like Python and C. For j gets
10 until it equals 1, subtract 1,
and then and at the end [INAUDIBLE].
And this negative 1
is actually optional.
If you don't specify it by
default it goes up by 1.
But since in this case
we're doing a 10 to 1,
we need to specify that
it's a negative step of 1.
And then a repeat until--
less common, but this
is the Lua equivalent
of a do while loop like
you would see in C.
And then here, lastly, we'll have
a demonstration of what a table is
and how to use it.
It's very simple if you especially
if you use JavaScript or Python.
An empty table takes the
form of two curly brackets.
To access fields in
that table, you simply
say that table's name dot
sum field get some value.
So in this case, person.name
equals my name, age, and height.
And then there's two ways
of accessing the fields.
Once you've specified them,
you can say that table
with square brackets and then a
string like you would do in Python.
Or you can access it with a dot
like you would do in JavaScript.
So it's kind of like a
combination between the two.
And then something that's specific
to Lua but similar to Python
is in order to iterate through a
table to get all the keys and values,
you would do for key, value
in a function called pairs--
which gives you all the pairs
of key values in that table.
And then you have access
to those within the loop,
and you can do whatever
you want with those.
So any questions on Lua syntax thus far?
OK, cool.
So that's Lua.
It's a very, very lightweight language.
I think it's like something
like 60 kilobyte runtime.
It's very small, very fast.
LOVE 2D is what we're going to actually
be using to do our game development.
So this exposes all of the functions
that we need to draw things
to the screen, to take keyboard input.
It's got some math functions
and audio, some physics stuff.
It's free, it's very
lightweight, and it's
portable to all major
desktops and mobile devices.
So you can write once and then
run it on any computer that
supports this runtime.
And the main thing that I really
think is great for LOVE 2D
is because it's so light and
easy to rapidly develop in,
it's great for prototyping.
Whether or not you want
to ship your game in LOVE
or like port it to unity
or something, I think
it's really great and really fast
and easy to write it in LOVE.
So before we get into what
we're going to be doing today
in terms of the actual
implementation, I thought
I'd talk about some
game concepts at large
so we can understand
how a game really works.
A game is simply an application
that's constantly running.
It's in a while loop effectively.
And it's running something
that we call a game loop, which
is a simplified three step process.
The first step is process input.
So while true, we need to say as the
user pressed a key on the keyboard
or moved their mouse or
done anything like that,
if so we need to keep track
of that because that's
going to have an effect on how we
interact with the game like how
it's going to update.
The next stage of the
game is the update phase
and this takes into consideration
everything in our game--
all the characters and items and the AI.
Whatever you've implemented needs
to update during this phase.
It needs to get to its next stage.
And sometimes this clock,
it represents the fact
that this can happen
multiple times depending
on how many frames have passed
since the last rendered frame
depending on how fast your computer is.
And then the render phase takes all
these updates into consideration
and actually draws
everything to the screen
in whatever order and whatever style
that you've determined it needs to.
Specifically with 2D
games, one other thing
we need to take into
consideration is the 2D coordinate
system which is very simple.
As we learned in geometry
many years ago, x-axis--
to the right, positive;
to the left, negative.
Y-axis is inverted from
Cartesian geometry.
I believe it is positive
up, negative down.
But in this case, it's going to
be positive down, negative up.
It can vary depending on
which platform you're using.
I think unity starts bottom left.
But in LOVE 2D, it
starts at the top left.
So (0, 0)'s origin--
top left.
Anything that's positive on the
x-axis is going to move to the right.
If it's positive on the y-axis is
going to move down and vise versa.
And so this is how we
draw things to the screen.
Everything in our game needs to have an
x,y coordinates so that it can be drawn
appropriately, and we'll
see this very soon.
So I going to open up a
very basic LOVE 2D demo.
If we go to LOVE demo in the GitHub repo
and we go to main.lua in that folder.
Main.lua is a file that LOVE 2D needs
to see in order to run the application.
Right here, I'm going to sort of
wave my hands at some of this stuff,
but I will go into detail on some
of the things that are important.
In this case, we're using a
virtual resolution library.
So we don't need to worry
too much about what this is.
But essentially this
will allow us to pretend
like we're drawing to NES era screen
while rendering our game in 1080p
or 720p or whatever resolution we want.
So here I have three
variables-- sprite, x, and y.
The sprite is going to be the actual
graphic we draw to the screen.
X and y are going to be its position.
We have our virtual
width and virtual height
that we want to draw
the screen at to make
it look like it's an old game
like a retro style NES game--
423 by 243 happens to be similar,
but 16 by 9 resolution of that era's
resolution.
And then speed is a variable we're going
to use to move our sprite once it's
drawn to the screen.
So the first core LOVE 2D function that
we're going to look at is love.load.
This is an initialization
function that takes place
at the very beginning
of your application.
This is where you set up
all the things that you
need to get going before the
game loop actually begins.
So in this case, we're setting
a filter for nearest-nearest
which just means whenever we
draw texture don't blur it.
Just make sure it's drawn
as it looks on the image.
We're initialising sprite to a new
image-- love.graphics.newimage which
is a LOVE 2D function that will look
into that directory was specified
for that image and it's going
to set it to that image.
We're setting x and y to roughly halfway
in the middle of the screen taking
into consideration the
sprite's dimensions.
And then we're setting up our
screen with the push library
with a virtual width and virtual
height but the 640 by 480
is the actual window width and height
that we're going to be rendering to.
So even though it's any less resolution,
it's going to be a 640 by 480 window.
So that information isn't
necessarily important.
The important thing
is we know that we're
getting a sprite,
assigning it an x and y,
and then we're drawing it to the screen.
So the next important LOVE 2D
function that we'll be looking at
is the update function-- love.update.
And it takes in a parameter called
dt, this is short for delta time.
And this is the number in
seconds as a floating point that
have elapsed since the last frame.
One frame generally in 60 frames
per second is about 17 milliseconds.
So that's roughly how much time will
have passed since the last frame.
And any time you need to
update anything in your game,
you'll do that in this
function, but you'll
multiply whatever you
need by delta time so
that this occurs independent of whether
or not your computer has slowed down.
And then love.keypressed
is the input phase.
So that was effectively the update
phase of the game loop we saw earlier.
The input phase for the sake of
this demonstration, this program
is love.keypressed key.
So this function basically
looks at whatever key
you've pressed on the keyboard,
it gets that passed in.
And then you can do any
conditional logic you want here.
So if he happens to be
left for example, we're
going to set x equal to x minus speed.
We're going to subtract 10 from it.
We're going to move 10 to the left.
If key is right move to the right 10.
And same thing with up and
down, but on the y-axis.
So we're going to move that
sprite on the right axis
based on what key we've selected.
And if the key is
escape, then we can call
this function called
love.event.quit which
will actually quit the application.
And then here at the bottom--
push apply start and we don't
need to worry too much about that.
That will just start and
end our virtual resolution.
The important thing to look at is
this love.graphics.draw function.
Which takes in a drawable
object, in this case sprite,
and then an x and a y.
And it'll always draw the sprite at
the coordinates that you pass in.
And if we run this program--
let's see, what am I going to?
LOVE demo.
We get this on the screen.
So it's loading the Mario graphic that's
in graphics/mario.png and if I hit left
and right, up and down on the keyboard,
it moves 10 pixels in that direction.
So very basic but most
of the building blocks
that we need for like the beginning
of our game are effectively in place.
So does anybody have any
questions on how that works?
AUDIENCE: I do.
What is speed measured in?
COLTON OGDEN: In this
case, it's just pixels.
So we're saying speed equals 10 and then
when we only modify the sprite's (x, y)
value here.
It's literally just subtracting
and adding pixels to it.
AUDIENCE: In this case, if
you just hold down the button,
would it register each frame?
COLTON OGDEN: It does not.
So in this particular
function, this is just
a callback function that registers
one time when you hit it.
And we'll actually see
another function that
love offers that allows you
to test for continuous input.
Any other questions?
AUDIENCE: How can we
use a switch statement?
COLTON OGDEN: I don't believe Lua
actually has a switch statement.
There are other clean ways we could
probably do it with like a table.
We could set a table to and
just reference it like that.
But also for simplicity,
it's really easy
to see what's going on here
just for an early demonstration.
So any other question?
All right.
So our goal today is going to
be a little more complicated.
We're going to be implementing the very
basics of what makes Super Mario Bros.
We don't have enough time to
really implement the whole game.
But we will be doing a lot of the core
will be having him jump and animate.
We'll have a world rendering blocks.
We won't have enough time
for AI and creatures.
But once we see how
Mario works, it's sort of
easy to guess how we
might implement that.
So today's scope, just
to reiterate-- we'll
have a tile-based map to
hearken back to the NES era
where everything was drawn with tiles.
We'll control Mario with keyboard.
He'll run around and jump.
He'll animate appropriately.
He'll have collision detection
so he can't fall to the ground,
and he can't like walk through
pipes and stuff like that.
And we'll have a very simple procedural
map generation system which to which
sounds semi complicated, but
it's actually really easy.
And I really like procedural generation.
And we'll have very basic sound
effects for jumping, hitting
blocks and stuff like that.
Goal number zero was getting
a window up and running.
We finished that.
That's easy.
Goal number one, we want to
render a screen full of tiles.
And so the first thing I think we
should talk about is sprite sheets.
So a sprite sheet is
basically just a sheet
a large image that
contains a bunch of smaller
images and we index into this image--
we basically take little
chunks out of this
to render individual
pieces of our game at once.
And this is how things
were done in the NES.
And this is all things are still
done today often with 2D games.
We'll have to pair this spreadsheet
with what's called a tile map.
And so a tile map is the data
representation of our map.
So if we were to assign a value
to each tile in the game--
like, if we wanted a block to be
number one and a pipe to be number x.
We need to be able to index that.
So when we draw to the screen, we can
say we can look at this big sheet,
not render the whole thing--
just render a tiny chunk.
And that's effectively what we'll
be able to do with a tile map.
So we're going to assign an
integer value to each tile
that we want, and then our game is
going to take that into consideration
and draw just a chunk
of that sprite sheet.
And this is what just the
raw tiles might look like.
If we were to envision like maybe
zero is no tile and one is a brick.
Like this is just a bunch of emptiness.
And then this is a bunch
of bricks on the ground.
And then this might be what that
looks like in the actual game.
But it's just as simple
fundamentally as having a map here
and just writing a function that
can loop over this and then draw
each tile where it belongs.
So we're going to look at a demo here
of mario-1 if you're in the GitHub repo.
And this is exactly what I
just showed you in this slides.
But this is an actual
application, and all it's doing
is it's doing what I just described.
Where this is 14 tiles
tall of just emptiness
followed by 14 tiles of bricks.
And it has the illusion we're starting
to see that this looks like some world
even though we're destroying little
graphics over and over again.
And this is how the NES and old
hardware used to work back in the day.
They were really good
at drawing little tiles,
and they took advantage of that fact.
So we're going to go
ahead and go into mario-1
and just look at the main really fast.
And we'll see that we have a
new line here-- require map.
So map is a file that we've
written ourselves in map.lua.
And we don't need to pay attention to
some of the stuff-- this boilerplate
code here.
This effectively lets us abstract
out what a map is in code.
It's called object oriented
programming if familiar.
And it basically lets us think
about maps in terms of code
rather than an abstract concept.
And we can call functions map get
tile, map set tile, et cetera.
And sort of build
layer by layer our game
and think about it in
more high level terms.
So we have a couple of constants here.
Tile brick gets one, tile empty is 29.
And these are the indices--
if we look back here
at the sprite sheet.
So lua is one indexed.
So starting here, this
is tile number one.
If we go 29 over, we'll see that
this is emptiness right here.
So all this is saying is the tiles that
we need to get started are 1 in 29.
The first tile and the 29th tile--
the brick and empty space--
to get started drawing.
In our map create function, this just
basically sets our map object up.
We're going to set a few fields here.
So we need a reference
to our sprite sheet.
So graphic/tiles.png, so fundamentally
the same function we used before to get
Mario.
We want to keep track of our
tile width and tile height.
We want to keep track of the
width and height of our map, which
would be important.
And then we want to keep track
of the actual tiles themselves.
This table right here,
this empty table, we're
going to fill this with the ones and
29s that we need to represent our game
world, or game map.
And this function here, generate quads.
So LOVE 2D's way of taking
a piece of a sprite sheet
is with a object called a quad.
A quad is just a simple rectangle.
You specify an x and
y, a width and height.
And it'll know to take
a chunk of that sprite
when you draw it, instead
of drawing the sprite.
And so I've written a
utility function in util.lua.
It's a little bit much,
but all it does is
loop through the entire sheet
given its width and height
and divide it up into those
quads and store them in a table.
So we can index that table later.
So if we go back down here, this
line is unimportant, but just related
to the actual--
it's Lua syntax for getting
this to be an object.
This is the important bit of code here.
We're going to iterate through that
empty table given a width and height--
we're going to iterate through our
width and height with that empty table
and fill it.
In this case, we're going to
start at the top, y gets one.
And then we're going to go all
the way down given the height,
and then we're going
to start at x 1-- it's
a nested for loop essentially
like Mario if you're
familiar with the Mario pset in CS50.
We're starting at the top left and
then we're just going row by row,
but we're going to set each
tile using a function that I've
written called set tile--
x,y to tile empty.
And tile empty, recall,
we defined as 29.
So this is going to
fill our map with 29.
It's going to be just an empty map.
And then this bit of code here is the
same thing, but it starts halfway down.
So we're starting y at
map height divided by 2,
but it's going to fill the bottom half
with tile brick, which is number one.
And then these functions here I
referenced earlier get tile and set
tile, they just taken the x and y and
because our table is one dimensional,
it needs to convert two dimensional
x and y into one dimensional.
So that's just the math you need to--
given a table-- insert and take out
a tile at a particular x, y location.
And then the important
part of map here, the thing
that actually draws it to the screen
is a function called map render.
This loops using a nested for loop--
y gets 1, x 1 every line will
love.graphics.draw the sprite sheet
but also taughtself.tilesprites,
which I showed you earlier,
is the table full of quads.
All those little quads that
represent the individual tiles,
it's going to index that at--
we're going to get the tile there.
So if at x and y tile
is equal to a brick,
it's going to look up the brick quad
in self and draw the brick quad.
And if it's empty, it's
just going to draw nothing.
It's going to draw the empty
pit of that sprite sheet.
And then we need to multiply
our x minus 1 and y minus 1
because we're one indexed
but we still draw zero index.
We're going to multiply
that by the tile width
and that'll have the effect of drawing
that sprite at a particular x, y times
16 in this case.
So that's all the code that
we really need to draw.
And then in main, we have two--
first of all, what we're
doing is we're creating
the map-- map equals map create.
And then down here in
the actual draw function,
we're using a function called
love.graphics.clear given a color.
In this case, this is just
the Mario blue sky color.
108, 140, 55 rgb, width 255 alpha.
So it's completely opaque.
And then we're calling map render,
and the map render function
takes care of all the drawing.
So most of what we've done
is offloaded to the map file.
We're just calling that
in our main.lua file.
So any questions on how this works?
We have a pretty basic scaffolding here.
It's not terribly interesting, we're
just trying some tiles to the screen.
Our first step into getting it to
actually look visually interesting,
I think, is getting it to scroll.
Because games of that era--
Super Mario Bros.-- were
called to 2D side scrollers.
Your job is to go through
the level, reach the end.
And in order to do that,
we have to have some sort
of notion of a camera in our game.
We have to look at where we are
and move and shift our scene.
So we'll effectively have some--
it's not going to be
technically a camera.
In our code, we're going to
pretend like it's a camera.
We're going to keep track of some
sort of top left corner of where
we might have our camera.
And then we want to scroll
our x and y on that camera
whenever we want to move
in a particular direction.
And the way that we actually
accomplish this in LOVE 2D
is a function called
love.graphics.translate,
and it takes an x and y.
So this has the effect simply of
translating literally everything we're
drawing in the window.
And this makes it look like we're moving
but we're not actually moving yet.
So if we call
love.graphics.translate x 100,
we're going to shift 100
to the right and that's
going to have the effect of
actually having moved backwards.
So if you want to make it look like
we're moving forwards given an offset,
we need to actually translate
it to the negative direction.
And that'll make us look like
we're moving forward to the level.
So let's go ahead and look at mario-2,
I'm going to run the example here.
And we see that we have a very simple
scrolling background now, instead
of a static background.
And all this is doing is just
calling love.graphics.translate
on some constantly updating x value.
And I'll show you here in mario-2.
If we go to map, something
that we fundamentally
changed is now our
update function in map
is modifying some value that
we've called self.cam x.
We're doing self.cam x equals self.cam x
plus delta time times the scroll speed.
Recall, delta time was the value that
LOVE is going to create every frame.
It's going to be the
amount of seconds that
have elapsed since the last
frame of rendered output.
And see up here, we have initialized cam
x to zero and cam y to negative three.
So this has the effect
of cumulatively adding
to self.cam x times scroll speed.
And scroll speed also
we've defined up here
to be 62, which is some
arbitrary value that I found
was a nice rate of movement.
And that's effectively
going to be multiplied
by some 0.0 something value
that's number of seconds
since the last frame of about.
If we go back to main, we
have made one other change
and that's that we actually have
to update the map with that update
function.
So all your updates must be funneled
through this love.update dt function.
So in here, all we do is
we call a map update dt,
and the dt when we get from love.update
update will get passed into to there
and our map will update.
The camera is constantly be
moving to the right, self.cam x.
And we can see here that cam x gets
used in the actual translate function--
love.graphics.translate.
Math.floor is simply a function
that converts a floating point
number to its lowest
integer form, which is just
important for a very pixelated games.
We want things to be
rendered in discrete--
basically, if you're rendering to a
virtual resolution-- so in this case,
we have in any case look to our game.
We're essentially rendering to
a virtual texture to do this.
This Isn't necessarily
important but just
for a low level understanding
of how it works.
If you render to a
texture something that
is low resolution but at a
sort of a point between 0--
something that's not discrete like
a floating point value like 10.5.
It's going to artifact really strange.
It's going to look like it's got extra
lines and be weird and pixelated.
So we just want to make sure any
value that gets passed into to here
is an integer value.
And that's all that
that essentially does.
It floors it, and then
we're also rounding it
by adding 0.5 before we do that.
And then notice that we we're
passing in the negative camera
x, not the positive one.
Because if we do the
positive one, it's going
to look like we're going backwards.
So we do the negative one because
the scene is shifting negative,
so it's making us look like we're
going forward if that makes sense.
Does anybody have any questions
on how this code works?
So goal number three, we have something
kind of cool up on the screen.
But we have a lot farther to go.
We want to actually have input.
So we've got like the
update phase working.
We've got the render
phase working, but we
don't have the input phase working yet.
It's not interactive.
So to do that we're going to
use a couple of functions.
We've seen one.
We saw love.keypressed
key which is simply
a callback function that executes
immediately upon you pressing a key,
but only does it that moment
and only does it one time.
The other function that we need
to use is love.keyboard.isDown key
which will return true whenever that
key is being pressed down constantly.
And then that's how we
get constant movement.
Because if we just press
a key, we want Mario
to keep moving even though
we're holding the key down.
We want to keep pressing the
key down for him to keep moving.
We're going look at a
demo of mario-3 here.
I'm going to go ahead and run it first.
So we have basically the same
scene that we saw before,
but if I press the keys on the keyboard,
we start moving right, left, up, down,
any direction we want.
We can also now see that our
map is actually half filled
with tiles and half filled the space.
This is the beginning of our game.
We have something interactive.
This is how we get started with
more complicated interactivity.
So let's go ahead and look
at our code in mario-3.
If we pull up the map file, the
fundamental update that we've made here
is we've modified map.update
to take in keyboard input now.
So we see if love.keyboard.isDown left
then we're going to modify our cam x.
And if we're pressing right,
we're going to increment it.
The same thing for up and
down on the y-axis, our cam y,
decremented or incremented.
The math.max, math.min--
these are effectively
ways for us to constrain our
movement to the maps boundaries.
We don't need to worry
about it in too much depth,
but math.max returns the
greater of two values.
Math.min returns the
lesser of two values.
And we're making the greater
and lesser of those values
the boundaries of our map
top, down, left, and right.
That's all that's fundamentally changed.
And because of that, now when we
press keyboard input, rather than
it doing autonomously, we can see
our map move in any direction.
So any questions on how this works?
OK, cool.
So next goal.
Our maps a little bland at the moment.
It's just a bunch of tiles.
But it'll be really cool
to start seeing the things
that we know from Mario like pipes and
clouds and bushes and stuff like that.
Just go ahead and pull up mario-4
and see what that looks like.
So here we have the
exact same base of code,
but now we're rendering a
lot more interesting output.
We have gaps in our level.
We have bushes.
We have those question
mark blocks and pipes.
We have a whole bunch of stuff.
It's starting actually
look like Super Mario
Bros., which I think is pretty cool.
And to do that, I'm going to
go ahead and pull up mario-4.
And what's changed about this code
here is our create function for map
where we've nationalized all the
data in the map, now we're doing--
and we don't need to spend
too much time on this
because it's kind of
an optional segment.
But this is an example of very,
very basic procedural generation.
We're saying if math.random 20, which
turns a random value within the range
1 to 20, if it's equal to 1,
which means we have a 5% chance,
then start a cloud at
some point in our height
that's above the middle of the map.
And then set these six
tiles, which together
form the cloud because if we
look at the example again,
these are all individual tiles.
And every tile in the
game is 16 by 16 pixels,
but this pipe is not 16 pixels wide.
And this cloud is not 16
pixels tall by 16 pixels wide.
These are actually
comprised of multiple pieces
and to get it to look like we
have a pipe or a cloud or a bush,
when we create one of these things, we
have to lay out several pieces together
in a way that they all fit together.
Fun fact, the NES was pretty
constrained and the developers actually
used the same sprites for
the Bush and the cloud.
We're not doing that in this case, but
this is how you got by back in the day.
They would pass this gray scale image--
it wouldn't even be
blue back in the day.
It would be like shades of gray.
And they would pass it into the NES
hardware with a particular palette,
and it would make it green.
They could make it whatever
color they wanted to
and that's a basic asset recycling.
And so what we're
defectively doing is just
choosing a random chance like every--
I should also say we're doing this
on a line by line level of our map.
So we have to go through our
entire map and generate it.
So the best way to do it is to--
at least, the best way I found was
to do it in vertical scan lines.
So starting at the left, we have no map.
If we go down, we have a chance to
generate ground and/or maybe a pipe
or a cloud or a bush.
So do it, go to the next
line, keep doing it.
Do we want to generate a cloud?
Maybe, let's generate a cloud.
If we do generate a cloud, we need
to lay out the pieces of the cloud,
and then keep going.
And then sometimes we
can say, optionally, hey,
maybe we want to break in
look in the ground here.
So don't actually draw any
ground tiles, just keep going.
Just go all the way down and go the
next one, draw ground, draw ground--
oh, don't draw ground here.
So we have these obstacles we're
generating as well as a result of this.
And here in the code, we have, here,
this is clouds and this is pipes.
5% chance to generate pipe.
10% chance to generate to a bush.
So you tweak these values as you
think makes for a good algorithm.
If you want to see more bushes increase
the percent chance per math.random
that you're looking for one.
And then that will give
you a higher chance.
Like if we made this
five, for example, then
we have a 20% chance to generate a
bush, not a 10-- or a gap, sorry.
In this case, it's a gap.
We have a 20% chance.
So we'll have a lot more gaps
in our map if we do that.
And this is all we're doing, we're
just going in vertical scan lines
throughout our--
we're going for y gets one and
then we're going all the way down y
and then increment x and then
increment x, go all the way down.
And that's effectively how
we're generating our map.
Yeah.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: You can
do a couple of things.
I think I took that into consideration.
One way that I can think
of off the top of my head,
keep track of some sort of flag in
your code that says if gap created
and set that to true
when you start a gap.
And you can tailor it
to however you want.
But let's say you want all
your gaps to be two wide,
if gap created equal true then on the
next one, you'll test for that you say,
oh, it's true.
So on the last scan when it
created a gap, create another gap
but then set it to false.
And then on the next loop,
it's going to be false.
You could then set gap just created
so that you don't like accidentally
redo another gap and
make it like too wide.
But there's a bunch of
other ways you could do it
but, yeah, I think a flag would
probably make sense there.
Yes?
AUDIENCE: When are we going to
ever have clouds to lighting?
It seems like the way it's
set up because a cloud is
greater than a vertical scan line.
It seems like you could on the
very next scan have another cloud.
Or maybe you have seen
clouds to lighting?
COLTON OGDEN: I think I might
have set that in the code.
I'm not sure.
It's been a little while
since I looked at this.
It could just be the random chance.
I ran it several times and I might
just have not stumbled upon that.
That's something that you could
also keep track of what the flag.
Just say cloud just created.
And then because a cloud spans
three vertical scan lines,
you could say is true, is true,
or keep a counter in place.
Do something like that.
At that point, probably
start having a table
with all of the different
structures that you want just
for organizational purpose.
Maybe the table would be called just
created and you can index it-- cloud,
pipe, bush, et cetera--
and then do it that way.
That's probably the
cleaner way to do it.
Any questions on how this is
being implemented at the moment?
Cool.
All right, so things are
going to look pretty spicy.
But we don't have a character that
we can control and that's important.
So the next step is going
to be actually adding Mario.
And Mario fundamentally
isn't a whole lot
different in terms of getting
him started drawing on the screen
than it was with our sprite
sheet, with our tiles.
We have another sprite sheet.
This is Mario's sprite sheet here.
And these are going to be quads
just like we did with the tiles.
Except in this case, he is
two tiles tall by one tile
wide so he stands out a little bit more.
And so we're going to need
a new object, a new class
called an animation so that--
because when he moves, for example,
his frame changes.
It's not going to be
just one frame that's
constantly being drawn to the screen.
Depending on what he's doing, whether
he's jumping or walking or running,
et cetera--
that's going to change.
And so we're going to need
some construct for that.
But just to get him started drawing to
the screen, it's a lot of the pieces
that we've already used.
So I'm going to go ahead
and run mario-5 here.
And we see Mario now drawn to the
screen, and in this limited example,
he can also move his
head left and right.
So we can see this shows us that we
have him now flipping his sprite.
So it doesn't make sense
for us to have a sprite
for him facing both directions.
We can easily just flip it based
on what direction he's facing
and then save ourselves half the frames.
AUDIENCE: You stepped
on a cloud up there.
COLTON OGDEN: Oh, there we go.
See, exactly.
I guess I haven't taken
it into consideration.
We'll do it again.
So now we can see it spawning
different levels every time I do it.
[INAUDIBLE] He's standing
above the ground.
No collision detection yet.
So I haven't taken that into
consideration for this example.
But if I were to go back and
fix it, which I probably should,
I would have some sort of table of
the different pieces I've created
and just keep track of whether they
were created on the previous x scan line
depending on how wide the structure is.
So if you go into mario-5, we have
a new a class called player.lua.
And this is going to contain all of
the information we need for our player.
He's got an x and y.
He's got a width and height,
an x offset and a y offset.
And these are important
because we need to tell
LOVE to scale our sprite on the x-axis,
depending on which direction he's
facing to get it to flip.
In 2D, to get a texture
to flip on any given axis,
you need to scale it
negatively on that axis.
And the origin of that scale needs
to be in the middle of that sprite.
In this case, since Mario is 16
pixels wide by 32 pixels tall,
the center of origin should be (8, 16).
We're going to need a reference to the
map so that he can interact with it.
We're going to give him
a texture, mario-1.png.
Frames of animation, just this
table, a current frame, state.
The state is going to be very important,
and we'll talk about as state machines
in just a moment.
This is going to be how we get Mario
to do different things based on input
into transition between those things.
A direction for him to face so that we
can say if you're facing to the right
flip your sprite otherwise
render it normally.
And then a delta x and delta
y, which is the standard way
of representing velocity on
your x and y-axis in animation.
So we're going to initialize x and y.
We're going to put him right
above the middle of the map
and then about 10 tiles to the right.
And then give him just
one frame for now.
And this is the quad that
I was referencing earlier.
To make a quad, just
love.graphics.newquad, the x and the y
in that sprite sheet you want to start
the quad at and then the dimensions.
In this case, Mario is 16
pixels wide by 32 pixels tall.
So at the very beginning
at 16 pixels and 32 pixels,
create the quad and it needs
the texture dimensions as well,
which you can use with to get
dimensions function there.
Set his current frame.
There's only one frame in this
.frame so Lua is one index.
So this .frame is one.
And then this is an important part here.
This behaviors stable.
This is going to be where we put all
of our states that Mario can be in.
And this is one thing I really
like about Lua as a language
and things like JavaScript as well.
Having anonymous functions
makes it just very easy
to package together
bits of code that you
might need to swap on a given basis.
In this case, this table--
we're saying at index idle, we're
just going to store a function.
Literally a function that's
unnamed, an anonymous function.
And it's an update function.
It takes in a dt.
And then we want this behavior.
And then we're going to call this idle
function in our code in just a second.
But we can see it's as
simple as right now, if we
pressed left, his direction is left.
And if we press right,
his direction is right.
This will be used in just a moment.
So in the update function,
we're referencing
this behavior's table we just created.
We're saying in
self.behaviors at our state--
and his state, recall, we set to idle.
We're going to just
call it like a function.
Just index the table and
then call it passing in dt.
And then this, we can just swap
in and out depending on his state.
Super simple.
We need to scale x in
our render function
because this is how we're going
to get them to actually flip.
If his direction is right,
his scale x is normal.
But if it's not right, so it's left.
It's going to be negative one.
And so if we scale anything by negative
one, it's going to flip it in 2D.
So love.graphics.draw, the texture,
the frame, put him at the x and y
that we need.
Zero rotation and then scale x here.
And this will have the
effect of whether--
because this is going
to run every frame,
if he's right based on
what we've input up here
because the update function calls
this update anonymous function.
He's going to be looking to
the left or to the right.
And so that that has the
effect, one more time,
when I press left--
direction set to right.
Direction is set to left.
Direction set to right,
direction set to left.
So you have the beginnings
of Mario on the screen.
So any questions as to how this works?
All right.
So it's not all that
interesting, he's stationary.
So now we're going to add movement
to him and this is a crucial step
in to actually
accomplishing what we want.
So how are we going to move Mario?
In this case, it's
actually can be very easy--
delta x and delta y is
probably more than we even
need to worry about here because
it's not going to change.
But in 2D game development, it's
common for delta x and delta y
to sort of be a function over time
rather than some discrete value.
So when you move Mario, you might
want him to start moving slowly
and then move faster.
And that's where your
velocity is actually relevant.
In our examples here,
we're not going to do that.
It's just going to be
one concrete value.
So his movement is going to be the same.
But to get a certain feel in your
2D game to get it to feel right,
you're going to need to tune the
graph of your movement accordingly.
And so we're just going to give
him a positive dx and a negative dx
and that's going to manipulate his
actual x and y value per frame.
And we'll see that here in mario-6.
So I'm going to go
ahead and run mario-6.
He's in the middle of the pipe
because there is no collision yet,
but if we move him left and right.
MC Hammer style, he's moving.
And he's not animating yet, but we
have effectively the very beginnings
of the core gameplay of Mario.
He's moving through a level, and we see
the environment and no collision yet,
but we'll get to that.
Let's go ahead and actually see how
we have this working in mario-6.
If you go to player, we see that
we've changed the idle behavior
function that we created earlier.
In this case, if keyboard is down
left, direction is left as before,
but we're going to see his
delta x to negative 80.
And this is some value that I found
was a good speed for him to move.
And then same thing for keyboard is
right, we'll say his delta to eight.
Yes?
AUDIENCE: Do you need the
delta x on this [INAUDIBLE]..
COLTON OGDEN: You don't, no.
Because actually, if I go back--
that one was because we
were just moving the camera.
I believe we've changed
it now so that the--
in map, I believe cam x now gets
the Mario's current position.
Yeah, so here we have our update
function has changed such that--
and I apologize for skimming
over some of these details just
for the sake of time.
But cam x now is actually
going to get self.player x.
It's going to get the cent--
math.min, it's going to basically
ensure that we don't center on him such
that we get clipped out of bounds.
That's what the math.min are
taking into consideration.
But now it's a function not
of some constant 62 value,
but it's actually his x and y value.
If we go back to player y, that's
all that we really needed to change.
Now because we have the
camera following him,
our delta x and delta y are
changing based on input.
We have the result of him simply moving.
The functions are in place such that--
notice that even moving to the left
here, the camera is not
going any farther than this
because this would just all be
blackness, and that's not what we want.
We want to confine it, and that's
the way that the actual game works.
We want to make sure that we confine
the camera always to what's visible.
And so you just need to put constraints
on where your camera can look.
All right, any more questions
on how we got that working?
AUDIENCE: Sorry, the
constraints on the camera,
that's something you [INAUDIBLE].
COLTON OGDEN: So if we go to map.lua
right here, camera x is going to--
this bit of code is going
to ensure that we don't go.
It's going to return the greater of 0 so
that we don't go any farther left less
than 0 or whatever this value here,
which is the player's coordinates.
And also math.min so that we don't
go past the right edge as well.
AUDIENCE: So that is your logic?
COLTON OGDEN: Yes, yes.
That's the logic.
This bit of code simultaneously
tracks Mario and prevents the camera
from clipping out of bounds.
OK, any further questions?
All right.
So he's not animating
so that's a core feature
and that's something that will
make it look nice and polished.
So we're going to start
implementing basic animation.
And to do this, we need to
start talking about state.
State machines, in general,
are a very clean way
of modeling transitions for
almost anything in your game,
but typically for entities
like characters and enemies.
We can see here these boxes
are all separate states.
So standing, for
example, that's a state.
Jumping is a state, ducking
is a state, diving is a state.
This isn't exclusive to Mario, this is
some other example, but the gist of it
remains true.
If we're standing, these
arrows model the sort
of transitions between
states and the conditions
that satisfy those transitions.
If we're standing, and we press B,
we should be in the jumping state.
And so if we're jumping, and
we press down, in this example,
we should go into what they
determined to be the diving state.
If we're standing in the press
down, we should be ducking.
And if we're ducking and release
down, we should be standing.
And this actually ties into our
behaviors table that we saw earlier,
and I'll show you right now.
So if we go to mario-7,
and we run mario-7.
And we move him, we now see that
he's got his running animation.
And not only that, but if
we let go of left or right,
he goes back to his idle animation.
Notice that it's a separate
frame of animation.
We actually have two states in use now.
If we go to mario-7's code, what
we fundamentally changed here is
our behavior table now has two states--
idle and walking.
And ideally, you would probably
engineer your code in such a way
that you separate out your states
[INAUDIBLE] different files.
It depends on the length of your code.
But for the sake of being able to
show how we've built things up,
I'm sort of putting
everything into one file.
But just know that there are cleaner and
better ways to engineer code like this.
Idle is changed now.
So if you're idle and you
press left, for example.
You want to start walking to the left.
Direction gets set left,
we know that already.
Delta x gets set to negative,
and we've separated-- instead of,
having a hard coded value, we've now
made it a constant walking speed.
So we can change it.
Delta x gets that value.
State now needs to change.
We're no longer in the idle
state, we're now walking.
So set the state to walking.
And then we have now
a set of animations.
So an animation-- if you
look at animation.lua,
it's a new file we've created.
I won't go into too much
detail, but conceptually
what an animation is,
is a table of frames--
all your quads for a given
animation-- and the interval
of time between those frames.
You keep track of how much
time has passed with a timer
that you set to zero.
And if every frame you add to that,
if we've surpassed the interval,
then we know that we need to go
to the next frame of animation.
And then we need to reset our
timer and just keep track of this
over and over again each frame.
And we do all of that here.
And it even has a logic in
here as well to make sure
that if we, for example, enough time
has passed between our last frame
that two frames of animation have
elapsed, we should take care of that.
And all that logic is contained therein.
I won't go into too much detail on
how it works for the sake of time.
But this is fundamentally how
we're getting Mario to animate.
We're giving him an animation where
we're assigning a name to an animation,
giving it a set of frames
in that sprite sheet,
and a time interval that
elapses between those frames,
and then just keeping track.
How much time has passed?
Has 0.05 seconds elapsed?
If so, go to the next frame, reset
your timer and rinse and repeat.
And also make sure loop
back to the beginning
of that because there's only
four or five frames of animation,
you need to constantly
cycle through that.
And that's what gives him
the appearance of walking.
We can see here in our animations table.
This is a new table.
We have idle, which is animation
create gets a texture and then
a set of frames.
Recall these are just quads
because the texture needs to know
what quads to render to the screen.
Idle's just one frame,
that's him standing still.
Walking is four frames.
And these are just
the particular offsets
because the sprite sheet
is a little strange in
that Mario has different sizes depending
on what frame is being rendered,
so I had to manually figure
out what those numbers are.
Fortunately, you only
have to do it one time.
But notice that I also have
interval gets 0.07 seconds,
and this happens to be the
length of time between each
of Mario's running
frames that I found makes
it look like he's running
close to the original game.
And we need to make sure that animation
gets set to animations index idle
and that we get the current
frame from that animation.
And every time we render, we want to
ask whatever his current animation is
for its current frame.
And that gets stored in the animation
class that it should be before.
So if we're back to our behaviors table,
when a state gets changed to walking,
the actual walking
animation needs to restart.
It doesn't need to be caught in the
middle because if we stop him walking
and go back to idle.
And then we restart him
walking at a weird frame,
it might not be clear in this
example, but for certain animations,
you want to restart the animation
every time that state gets changed.
Especially if they like start by moving
their arms up or something obvious.
And the opposite happens for the
right direction, but it's same logic.
Notice that we have his
walking statement here.
If we're pressing to the
left, move right and down.
And then it's effectively
the same thing--
idle and walking--
that we've done before,
but now they're just separate states.
And given his state, he can just call
this and update it really easily,
really cleanly.
You don't have to keep
track of hey, is he walking?
If he is do this.
If Mario is doing this--
the nice thing about
states is let's say,
you are in a game
where you have like a--
let's say you're walking
but you also have a weapon.
And the weapon has its own states.
Like, it can be firing.
It can be smoking.
I can be all this stuff.
You don't have to keep track
of if player is jumping
and player just shot--
you can just have two states,
weapon state and character state.
And then just modify both
those states simultaneously
and then that will simplify the whole
process of updating your character.
So we were updating
the animation in here
as well, because that
needs to update the timer.
We need to keep track of
how much time has passed,
so that we can add it to the timer.
And then if the interval
has been surpassed then
go to next frame animation
and then possibly loop
back, get the current frame and
then manipulate his x value.
And then here we can see if we're
just drawing self.currentframe.
Whatever his current animation is, it's
always going to get that current frame.
So any questions on how this works?
OK.
So he can walk, he's got two states.
And those two states, we can swap in and
out really easily and pretty cleanly.
Our next goal is what Mario's
probably the most iconic thing to do
is which is jumping.
And so jumping-- up to
this point, we've only
been manipulating our x-axis, left
and right, for moving to the level.
But jumping relies on the
y-axis being manipulated.
So to get someone to jump, you
give him an initial y velocity
of something really high
right like 400 or something.
He's in a ship into the air, but
as soon as he starts jumping,
we don't want to keep going forever.
We need him to sort of come down.
So what we need to do is apply
gravity to his y velocity.
We'll start his y
velocity at 400, but we're
going to add a negative
400 to be specific
because recall y is negative going up.
If is y is negative 400,
we need that to decrease
and eventually become positive.
So that he comes up and then eventually
comes back down to the ground.
So we'll see how we do that in mario-8.
Let's go ahead and load that up.
So here we have the same thing that
we saw before, we can move around,
but if I press spacebar--
he jumps.
And notice also he's got another frame
of animation which is as simple as just
one new animation object that we created
earlier just with that one frame.
And when he's in that
state, he can still move.
So when he's in the jumping
state, we know OK, you still
need a test for input.
And notice, there's no collisions
so I can still walk over the tiles,
but we'll be taking care
of that very shortly.
But, I mean, this is starting to look
a lot like Super Mario Bros., right?
Like, we've got most of
the pieces here in place.
So we're going to go ahead
and look at mario-8 now
to see how we implemented this.
So we have the idle state
and the walking state.
These have both been modified
because now when we're walking
and when we're idle, we want to be
able to jump because you can jump out
of both those states.
So in the idle state, if we
press the spacebar, his dy--
now we're modifying his dy-- it's going
to equal to negative jump velocity.
Jump velocity happens to
be a value I set to 400.
So his y velocity should
be set to negative 400.
In So here, we have
delta y negative jump
velocity, set his state to jumping.
And this will have the effect of on the
next frame jumping is going to update.
So it's going to go to this function
instead of the other function
that was called which was walking.
We're going to set his
state to jumping and then
we're going to set his animation
to a jumping animation.
We've added a jumping animation here.
It's just one frame just
like the idle animation.
Very simple and happens to be 88
pixels to the right at zero pixel
on the y-axis--
same dimensions.
And we essentially do
the same exact thing
when he's walking because the
behavior is fundamentally the same.
Jumping-- we can see that
we can still move here.
So when jumping gets updated,
when we're in the jumping state
and this is being called, we
can move left and right still,
which you can in the regular game.
And as delta x is set to-- it's just
same as walking speed, hasn't changed.
This is the part that lets
us actually prevent him
from shooting up into the sky forever.
This .dy gets this .dy
plus this .map.gravity.
And if we go to map, we can see
that we have a value gravity here
which is set to 15.
And this is just some value
you can tweak depending
on how fast you want--
either the less this is,
the more it's going to feel
like he's jumping on the moon.
Or the more it's going to feel like
he's on some really dense place.
But 15 felt like the number
that was very close to how
he jumps in the actual game.
And so this gets added to his dy.
And then this will be affect
of he's going to go up,
but he's going to go up less
and less, and eventually he's
going to come back down.
And then when it comes back
down, and he gets roughly
to the middle, which is
what this takes care of.
So if his y value is about-- this is
our very primitive collision detection.
It's not collision
detection, but it makes it
seem as if we have collision detection.
If it's equal to about roughly halfway
down the map minus Mario's height,
set his dy to zero, make him idle,
and then set his animation to idle.
So that's all we essentially
need to get him to jump.
So just gravity, y velocity,
and just modify the states
that we already have to get it to work.
And notice that we don't have to mess
with a bunch of if statements and messy
conditions to get this to work.
We just have discrete
blocks of logic that
work as a very simple state machine.
So any questions on how this work?
So our next goal--
he hasn't been able to interact
with any of the environment yet.
No collision detection, but
this is our first introduction
to collision detection here.
And to get this to work, what
we need to do is using our map,
we need to check the pixels
of where Mario's head is
at any point in which
he's jumping and say
is there a tile there that is
collidable that we should hit.
If so, set y velocity
to instantly be zero.
Gravity will then every frame
add to that and make him go down.
And maybe trigger some
sort of visual feedback,
so we know that we hit the tile.
And that's all we really have to do.
And then another thing
that we have to do
is Mario can be potentially
between two bricks at once.
In that case, we need to
check not only the brick that
happens to be to the left of him, but
also the brick to the right of him
so that they both get triggered, which
is how it works in the original game.
And all we need to do
effectively is look for whatever
brick exists at his origin point,
his top left point, and the brick
to the right based on his width.
So 16 pixels to the right.
So let's go ahead look at mario-9
and see what this looks like.
See if I can generate a level that has
two bricks right next to each other
before we--
well, maybe not.
But if I do that, we get some
very basic collision detection.
All we're doing is saying when
were jumping is there a brick
right where our head is?
If there is then gravity gets
zero, and also manipulate the--
in this case, we're also changing
this tile in our map which is maybe,
I think, it's 26.
The ID for the yellow
one, set that to like 27.
And then next time the map gets
rendered, 27 or whatever value
it is happens to be the dark version.
And that gives us the
appearance of having
modified our game map in some way.
So we can go ahead, that's
conceptually how it works.
If you look at mario-9, we can see--
and this is where it's necessary
to have a reference to map here,
which is why we've had
this the whole time.
Now, we actually need to look
in our map based on our input
and see what we're doing to the
game map and see whether we're
colliding with a given tile.
So what's changed is
in our update function,
I haven't added this
as a style decision.
I didn't add this to any of the states.
It's in his update phase.
It could be in his jumping
state, and that's up to you.
But just for the sake of
separating it out for now
so we can see it on its own,
it's an update function.
If his delta y is less than
zero, which means he's going up,
then we're going to use a function
called tile at his x and y values.
So it's going be his top left.
If it's not equal to
empty, we've hit something.
That's his origin, but also, his
width minus 1 plus his width minus 1.
So both tiles right above him
instantly set his dy to zero.
So now he's going to freeze in the
air and then gravity is going to apply
and he's going to fall.
We're going to just change that
block to a different block.
So what we're doing is we're
saying, hey, if the tile at (x, y)
is equal to that tile question, which
is like 26 or something like that,
then we're going to set that
tile to tile question dark.
And then we don't need to worry
about refresh sprite batch, that's
just a performance consideration.
If self.map tile at x and this is
the same thing, but for the tile
that's to the right and above him.
We're going to set that
to tile question dark.
And this has the effect
of it still will make
his dy set to zero even
if he hits a tile that's
not that question that
yellow question mark.
But he'll hit it, but
it won't change it.
If, for example, he jumps
and hits the brown one,
it still collides, but
it doesn't actually
perform any sort of
changes on the game map.
And that will just set the tile.
And then map if we go to tile at--
tile at is a very simple function.
All it does effectively is just divide
the x-coordinate that we pass in by 16
and add one to it.
And then the same thing for y divided
by [INAUDIBLE] and add one to it.
So very simple.
And that has the effect of let's say
we're at 48 or whatever pixels wide,
and we divide that by 16, it'll
be 3 because it floors it.
All right, and we add one
because Lua is one indexed
not zero indexed with its tables.
So
AUDIENCE: Doesn't that
mean you could hit a cloud?
COLTON OGDEN: In this case, yes.
I think it could.
But in the next example,
it gets-- actually,
I don't think so because I believe the
way that I have set the generation,
the clouds all spawn of
Mario's max jump height.
But we do get rid of that problem
because in the next example,
we'll see we actually
specify a table of tiles
that we can collide with explicitly.
Because we need to start getting
into pipes and the ground and stuff
like that so.
But yeah, theoretically if my
generation algorithm were messed up,
we could be hitting clouds right now.
But step by step, we build up
towards our ideal implementation.
Any further questions?
Yeah.
AUDIENCE: [INAUDIBLE]
COLTON OGDEN: What do you mean?
AUDIENCE: Like when he's jumping
[INAUDIBLE] is there [INAUDIBLE]
COLTON OGDEN: I believe I have it set so
that it's one pixel because in the game
I was noticing when running it that you
can see his fist go above the tiles.
Notice that you can see his fist hit it.
So I have it set so like I
think it's one or two pixels
the max of his bounding box.
But you do it to taste.
Do it right at the number.
Do it at one or two
pixels, it's up to you.
But that's just the way that they ended
up designing it in the original game
so I did it like that too.
But conceptually, you do it at
the limits of whatever you want,
and then you tweak it as you want.
So any more questions?
All right, goal 10.
Now, we need to implement as I
alluded to just a moment ago collision
two other tiles so now.
We can hit blocks but
when we're walking,
we're not actually testing
the ground below us
we're just walking
and setting the x-axis
and not performing any
collision below us.
We want to do that so we
can fall through gaps,
but we also want to prevent
him from walking into pipes.
And so that he can also walk on top
of blocks and other things like that.
So we're going to go ahead
and look at mario-10.
We'll see what this looks like.
So he happened to spawn in the
middle of a pipe, so bad example.
[LAUGHTER]
All right.
Whoops, and my jumping is not ideal.
But see now he can go on
top of blocks like that.
I can fall as you saw earlier.
I'll do it one more time.
You can fall down.
And I don't have it
set so you can restart.
And you can also not walk
through pipes anymore,
and you can jump on top of them.
So now we sort of have
the bare bone requirements
of what it means like walk
through Super Mario Bros. world.
And we're actually almost done
for what we have time for today.
I have a couple of one
more example after this,
but we'll look at mario-10 just
to see how we implemented this.
And as I alluded to earlier, I
believe it is in player here.
Oh no, it's in the map
coll-- right, there's
a collides function that I added.
So this map collides,
it takes in the tile,
and we're going to be running this
function for-- when we move left
and right we're going to be looking
at the two tiles at Mario's height,
left and right.
It takes in a tile.
We have this table here
called collidables.
And this just contains all the tiles
that we want to check for collision on.
So any bricks, question mark blocks,
pipes, that's fundamentally all we
need to look for.
There's no clouds in
there and then because of
that if we happen to intersect with
a cloud, he's not going to stop.
He's just going to go right
through it because collide
is going to return false.
Because using this
function here, we're going
to iterate through
that collidables table.
It's going to look through all of those.
If it happens to match with
that tile, return true.
Otherwise, we return false.
And we'll see this if we
go to our code over here.
This is part of our
previous jump code, but we
see if map collides with whatever
tile is [INAUDIBLE] above us,
we've replaced our previous code
with this collides function.
And then in our actual
state machine code up here,
when we're walking for example.
We need to do all of our
regular processing as before.
If we press space, start jumping.
And if we move left and right, set
our direction and our walking speed.
But we also need to check right
collision and check left collision.
And we can probably get rid
of check right collision.
If we're moving to the
right or to the left,
we can probably just
put it up above here.
But the functions work
as such; if we go down
to check left collision, if
our delta x is less than zero--
if we're testing that
map collides function,
we're basically looking at the
tile to our left and our y values.
So at our head level and at our bottom
level so we're Mario's torso is.
Because he's two tiles tall, he needs
to check both tiles to his left.
There could be, theoretically, a
block that's here but not here.
Even though that doesn't normally occur.
But we're going to check to
see if there is a tile there.
And if so, delta x needs
to get set to zero.
And then we're going to basically
look to see how many tiles he
would have moved to the left and then
push him back that number of pixels.
However many pixels he would have
moved in that direction, move him back.
And same thing with check right--
if his delta x is greater than 0, then
he needs to instantly get that set--
Or if his delta x is
greater than 0, and he's
got two tiles, or one or two
tiles, or one or the other,
do the same thing but to the right.
You're going to move him to the
left instead of to the right
if that makes sense.
So you're shifting him based
on the number of pixels that he
would have moved in that direction.
And that has the effect of allowing
him to not collide with the map.
And we'll look at it one more time.
Oh, this is perfect.
This is what I was
talking about earlier.
If there are two blocks, recall
we need to trigger both of them.
So if we hit spacebar
underneath both of them,
they both get activated
instead of just one.
And we can fall to the ground.
It's effectively pushing
him however many pixels
he would have moved in that
direction to the right.
And then we can jump on
top of blocks as well.
So that's effectively it when
he's moving left and right.
And if he's falling that's
another important consideration.
This bit of code here gets applied
after our velocity gets changed.
If both tiles below--
because he could be
theoretically between two tiles,
so we need to check the tile below
him and also the tile to the right.
If they collide with him
then set the dy to zero,
so that his velocity now is at zero.
State to idle, animation to
idle, and then basically tweak
his value so that he's not--
set his y value back
the number of pixels
he would have gone past
whatever tile he collided with.
Because based on his
velocity, we could update him
he could be like seven pixels
below a tile in the map,
we want him back up 7 pixels
that he's even with the tile grid
if that makes sense.
So any questions on how that works?
Sorry, that was loud.
Last goal and this was probably the
easiest step of the presentation.
We're going to add just some basic
sounds just to make our application
look all the more polished.
So with this, we're going to
use a couple of functions.
We're going to use the function called
love.audio.newSource, which just takes
in a path to a particular audio source.
In this case sounds/samplse.mp3,
and we can pass the key static.
And that just means if we pass in
static, store that sound in memory
constantly, don't stream it from disk.
And this is perfect
for small sound files
because depending on your computer that
may or may not trigger a lag spike.
And then we'll use the function
play on a given sound object
and that will just
start playing the audio.
And we'll see in a second,
we can set a call function
called set looping to true.
So, for example, for music,
we don't want the music
to stop after it plays once.
We want it to loop over and over again.
So we'll go ahead and take
a look now at mario-11 11.
This may be the most
gratifying example so far.
[MUSIC PLAYING - "SUPER MARIO BROS.
THEME"]
Notice that also we're triggering
a jump sound whenever he jump.
And when we hit the block,
it plays a coin sound.
But if we hit it again,
it doesn't play it.
It plays like a little thud sound.
And that's effectively
the gist of the game.
There's no death sound
unfortunately, but that's
how far we'll go with Mario today.
But we'll go ahead and look
at how we implemented this.
We store the music in our map with
music gets love.audio.newSource
music/overworld.mp3.
And all we do is start playing this here
at the bottom of our create function,
this .music set looping to true
as well, and this .musicplay.
Because recall we do want the music
to constantly go over and over again,
even if they surpass the
duration of the sound file.
And then player.lua has
a new sounds table here,
which just has jump, hit,
and coin, and then there
are just new sources as shown there.
And they're all passed in the static
flag because these are all small files,
they can be stored in memory without
too many repercussions-- jump, hit,
and coin.
And these are called here in
the idle state, for example,
and in all states when
we transition to jumping,
we should play this .soundsJump Soundz
play which is simply just plays his
jump sound.
Same thing for in the walking statement.
If we go to the part where we collide
with tiles here, the question blocks,
we can see that we're keeping two
variables -- play coin and play hit--
because depending on which
kind of tile block we've hit
we may want to play the coin
sound or the thud sound.
If we hit a tile question
block, the yellow one,
and we make a transition to the dark
one, we should hit do play coin.
Otherwise, play hit.
We've collided with something, but it's
not a yellow block so just play a hit.
And then here down below,
we see if play coin is
set to true, then self.soundsCoin play.
Otherwise, self.soundsHit play.
And that's all there
really is the audio,
it's probably the easiest
part of using LOVE 2D.
So other things to do that we
didn't have time for unfortunately
would be like adding
enemies to the game.
So something similar to
Mario in spirit, but that
has its own logic being applied in
the update function for example.
And you can give enemies their own
states, idle, walking, et cetera.
Maybe they're going after Mario,
maybe they're running away.
An end flagpole, something
that you collide with that
triggers a transition to another level.
A timer so that you can add a
sense of urgency to the game.
Power ups, text transitions,
lives, worlds, even
if you want to be grandiose, and
then warp pipes and a whole lot more.
So that's the nice thing is
you guys have a foundation now.
You can pretty much implement anything
with the tools that you show today.
So I'll stick around for
questions, but thanks a lot
for coming and best of luck.
Thank you.
[APPLAUSE]
