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]