SPEAKER: So this is Mari0, the tiles update, and so the goal of this update
is a little bit simpler, a little bit more distilled in
that we're just going to have a clear background and then a tile floor
that we can then eventually start adding on to,
including our avatar and other bits of the world, like pillars and clouds
and so forth.
But the very first thing we should get on the screen is just some tiles drawn.
So this is the sprite sheet we're going to use
to end up creating our game world.
So you'll notice that it's pretty simple.
It just has a few blocks.
It's got the bushes we looked at-- the bush and the cloud, rather.
It has a flagpole, which you won't actually get to in the track
but is going to be part of the assignment, including some flag images
there.
You don't have to necessarily use all of the images, just the flagpole and one
of the flag images.
We have the two pieces of the pillar.
Notice that it's actually split into two chunks of this texture.
So this is actually uniform in that these are all 16 by 16 tiles just
put together, packed together uniformly in a grid.
And we're going to look at how to actually split this out
very soon in this exact section, even.
But all the pieces are here.
And this is only a four by four tile, so everything is very compact.
The first thing that we need to look at in order to get our world up
and running is the idea of a tile map.
Thus far, we've really only taken a look at drawing rectangles to the screen,
but we want to start drawing textures instead.
And we're going to look at love.graphics.draw to do that.
But we, of course, need to have some way to store our world for it to be--
so we can render it to the screen, we need to have a way of representing it.
And to do that, the way that I like to represent, it is through a 1 to 1
mapping, especially for this kind of simple illustration,
as numbers, something that looks very similar to this.
So you can even look at this and visually see sort of how it maps.
Now, this is a mockup.
This isn't really how it's going to be stored.
But you can see, if you imagine that the zeros here are empty space
and the ones are bricks, well, it's actually laid out identically
to what you just saw in the screenshot before, where the zeros really
are the sky, the ones really are the bricks.
And now the numbers here aren't going to match up evenly, and especially
as you get into more sophisticated game where the tiles might have
other metadata associated with them, this model
is a little bit too simple for maybe a real world use case.
But for this example, it actually works perfectly well.
So that's it in terms of just the getting started,
the sort of mental framework we need to lay to get started with the code
itself.
Let's actually dive into the code.
I'm going to go over here to my Games folder, create a new folder altogether.
It's going to be called Mario, in which I'm
going to create Mari0 when I drag that over to VS Code,
and then I'm going to maximize this.
Now we're going to put a few pieces together, so a lot of this
is going to look probably pretty similar.
I'm going to create a new file.
It's going to be main.lua Inside of here,
I'm going to have just a few functions that were important to us before--
love.load, love,update, which we actually aren't going to use in this
iteration, and love.draw.
No love.keypress just yet.
What I want to have happen, again, we want WINDOW_WIDTH, probably,
for 1280, WINDOW_HEIGHT for 720.
We're going to go right to using push this time,
instead of using the normal resolution.
So I'm going to upfront say virtual width should be equal to 432,
and the virtual height should be equal to 243, just like that.
And I know I'm going to use push, so I'm going to say,
push is equal to require push.
I'm going to actually go back out here to Pong,
and I'm just going to copy both the class and the push.lua
from that last example, go back over to Main, go to my Mari0,
and then just paste those right in.
So we have class and push already built into the project.
Going back to VS Code, so now this will work.
I'm also going to include class.
Class is equal to require of class.
Now, what I'm going to want to have happen
is go straight to object-oriented programming
this time, as opposed to really sort of create
a bunch of data structures and variables outside and muddy up main.lua.
What I want to have happen is I'm already
going to want to think about my world in terms of a map object.
I want to sort of conceptualize all that it means to store a map and render it
and so forth inside of a class so I don't have to worry about that in main.
I can just say, for example, map is equal to a map.
And maybe I can go down to love that draw
and just call map render like this.
And this is a good way to sort of mock up your flow
as you're going along with an object-oriented code base up front.
If you know the objects that you want front,
you can write them before you implement them,
and then sort of backwards retrofit your code by creating those classes.
So up here, I know I'm going to need a map class.
So I'm just going to say, require map.
I'm going to go ahead and create a new file called map.lua.
And in here, I can say function map--
or rather, first thing I should do is say map is equal to class,
because it is a class.
I'm going to say function map init--
whoops-- function map update, and function map render.
And we will have an update applied in later iterations of this track.
But for now, we're just going to use primarily render and init.
So I'm going to go back to Main, and then I'm going to go over here
and I'm going to say--
well, actually the next thing that we need to do
is, in love.load, I should get the screen setup.
So I'm going to say push setup screen.
It just takes in our WINDOW_WIDTH-- or rather, it takes our VIRTUAL_WIDTH,
VIRTUAL_HEIGHT followed by our WINDOW_WIDTH and WINDOW_HEIGHT.
And again, it takes in an object full screen.
We're going to make that false, resizable equal to false,
and vsync equal to true.
Down here in love.draw, remember, I need to push apply start and push apply end
around the things that I want to draw.
So I'm going to go ahead and also I'm going to say love.graphics.print
hello, world, just as a sanity check.
And then one other thing I want to do is clear the screen
with that sort of Mario blue that we saw back here.
So in order to do that, remember, we have love.graphics.clear.
So I'm going to say, love.graphics.clear,
and then I'm going to say 108 over 255, 140 over 255, 255 over 255,
and 255 over 255.
And these last two you can actually just write 1
because that it will equate to the same exact thing.
So let's go ahead--
I think everything is well enough in order to at least give it a test run.
So why don't we go ahead and do that?
I'm going to run that.
And we do indeed see that screen being cleared in that blue color.
We see the text, hello, world, up at the very top left.
It is blurry, so what I want to do next is--
remember, we have the ability to say love.graphics.setDefaultFilter
of nearest and nearest, which gets rid of the pic--
or it pixelates everything in that.
It doesn't interpolate the pixels being rendered, so it solves that problem.
So it's a little maybe difficult to see because of the background color,
but we do indeed have the hello, world text here nice and crisp.
Cool.
So I'm going to quit that.
I'm going to go back over here.
Now what I want to have happen is get into the flow
of chopping up that sprite sheet.
So we need that sprite sheet, first off.
I'm actually going to cheat a little bit.
I'm going to go into my sort of pre-baked code
here, where I really have Mari0.
And I'm going to go into graphics, and I'm just
going to copy this sprite sheet.
I'm going to go over to games, Mario, Mari0.
I'm going to create a new graphics directory and then paste that right in.
So now we have this sprite sheet that I showed earlier in the slides.
It's the exact same file.
And one thing that I want to also talk about before we really do that is there
is going to be a mapping.
So let's find that again.
If we look at this sprite sheet here, we're
going to chop this up into individual components, like I said before.
And we want associated number with each of these individual tiles.
So the most sort of intuitive way to do this, and the way we will do this,
is to just go down from the top, work our way to the right, so top to bottom,
left to right.
So we'll end up saying this is 1, this is 2, this is 3.
This empty space is 4, and then we'll go back down to the next line
where this is 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16.
And so we're going to need to chop up these individual tiles.
And to do that, I want to introduce you to the love.graphics.quad function.
Now, first, for something like chopping up a file,
It makes a little bit more sense to put that in something like a utility class
because this is something we could use in a bunch of different places,
potentially, for working on a larger code base.
So what I like to do is, for something that's a little bit more utility-based,
I'll create something called util.lua or something similarly.
Capitalized or not, that's kind of up to you.
But in here, I essentially just want a function called generateQuads.
And generateQuads is a pretty simple function.
Should take what's called an atlas--
so you can think of sprite sheets and texture atlases
as is being sort of the same thing.
In some environments, the atlas will have other connotations
in that it has some sort of stored information, usually JSON information,
where you don't have to necessarily rely on things being grid-based.
But for the sake of, I guess, convention,
we'll use the term atlas here.
So it's going to take in an atlas, which is going to be the path to the texture.
Actually, in this case, it's going to be the actual texture data.
And then we're going to say that it should have a tile width and a tile
height as well that it needs to know about in order
to chop this up sort of uniformly.
So I'll figure out the sheet width.
So the sheetWidth is going to be atlas getWidth divided by tileWidth.
So the getWidth is going to be in pixels, and so will the tileWidth.
And this sheetWidth is essentially just going
to be how many tiles wide is our sheet.
And we're really the same thing for the tileHeight by sheetHeight.
Now, you'll notice I'm using the word local here,
which we talked about before in another section.
Local just means that these variables are not
accessible from anywhere outside of this function.
So we won't be able to have sheetWidth and sheetHeight accessed
in our main.lua, for example, which we wouldn't want.
We usually don't want to muddy up what's called your namespace,
sort of all the variables accessible to you at any given point in time
by making too many variables global.
That's why globals are generally said to be a bad thing in programming.
In game development, it's actually quite common
to see a lot of global variables.
But try to mitigate that as much as you can
because especially with very generically named variables,
it can come to bite you down the line.
Now what I'm going to do is I'm going to say--
I'm going to have a sheet counter.
This is where our quads sort of--
our numbering is going to begin at 1.
And this isn't strictly necessary with another way
to do this, which I can talk about very briefly.
But we're going to use it here just for illustration.
And I'm also creating this thing called quads,
and this quads is a table that's going to store all the quads we're
going to actually create.
Now, a quad is literally just a rectangle,
and it represents a chunk of a texture, basically.
And we're going to create this table full of quads,
and we're using it like an array.
Now previously, you saw tables being used like dictionaries
or like hash tables, key value pairs.
In this case, we're literally using it like a C array.
We're just filling it with data.
It's more akin to, really, a Python list because we don't have to, upfront,
say how big the array should be.
But we are literally just storing information or data chunk after chunk.
We're not associating keys to values.
Really, we are, in the sense that we're associating 1, 2, 3, 4,
5 to those values numerically.
And the interesting thing, too, about Lua is that everything is 1 indexed.
It's not 0 indexed, at least by norm and by default.
You can explicitly index things at 0, but the norm very much
is in Lua to start things at 1.
And if you use some of the default sort of array
generating functions, or creating or appending functions,
you'll find that it does start things at 1 if you don't specify an index.
So we're going to iterate through our texture, going from top to bottom.
I'm going to say for y is equal to 0 from the sheetHeight minus 1.
So this is going from 0 to--
in this case it would be 3.
And we're starting this at 0 because we want to actually specify
the quad in pixels on our sprite sheet, and the pixel specification is 0 based.
So pixel space is 0 indexed, but array indices are 1 indexed.
So this is kind of a game you have to play to sort of balance
this out a little bit when you're dealing with Lua.
So we'll say for x is equal to 0, sheetWidth minus 1 do.
And you'll notice that this is a double nested for loop,
much like you might have seen in Mario, for example, fittingly because we
are literally programming Mario.
Now what I'm going to do is I'm going to say quads at sheetCounter
is going to be equal to-- and this is just indexing into the table
that we declared already.
So I'm going to say love.graphics.newQuad.
Now, this is a love.graphics function, which expects--
as you see here, VS Code is kindly telling me what arguments it expects,
which is super convenient.
So it expects an x, a y, a width, and a height, and then
as well the dimensions of our texture atlas, which we can actually
get through a shorthand function call.
So I'm going to say that this should be set to x times tileWidth and y times
tileHeight.
So again, this is using x and y 0 based.
We need to figure this out.
We need to basically find where that little sub part of our texture begins,
coordinate wise, on our texture.
So if we're starting at 0, let's say our tile width is 16 pixels
and the tile height is 16 pixels.
If we're at y0, y0--
or x0, then we're just going to start at 0,0.
And the width, of course, is going to be tileWidth and tileHeight,
so getting to be a little bit of a long function
call, tileWidth and tileHeight, and atlas colon getDimensions.
Don't necessarily need to worry about atlas getDimensions.
Think of this really as sort of a necessary requisite
for using this function.
It just essentially expects the dimensions of the texture following
the declaration of the quad, and for reasons that are related to how quads
are stored at the C++ level in Love.
But if we're starting at x0, y0, or x1, y1, really,
our first tile up at the top left, it needs
to start at 0,0 because that's where the coordinate space is based,
and grab 16 pixels down into the right, which is where the tile width and tile
height come in.
And if we're, let's say, starting at x2, y0, then this becomes 1 times
tile height, which means--
or x1 times tile height--
x1 times tile width, which means it starts at 16
and grabs the 16 pixels and 16 pixels.
So it's essentially offset, depending on what our counter is in our loop here.
So this is how we end up iterating throughout our loop.
And remember, it's nested, so we iterate through our columns
because this inner loop is on the x-axis.
So we go x1, 2, 3, and then we loop back to y
where y is 1, which means we end up going back down to here.
So y will be 1, x will be 0, so 0, 1, 2, 3.
Then y is 2 here, so 0, 1, 2, 3, and y is 3, 0, 1, 2, 3.
So that's essentially how we're iterating through our sprite sheet
to create all the quads.
Now we've assembled this quads table.
So at the very end, what I want to do is return it.
So now we can call this from within anywhere in our program,
as long as we require util, which is what I going to do right here.
And actually, what we're going to do is use this NMAP,
so I need to make sure that this is done sort of upfront
before map actually gets called.
Now I want to actually go through the process of creating the map, which
is a little bit more involved.
So what I need to do here is say self.spriteSheet
because we need a sprite sheet to reference
to actually map the tiles to quads.
self.spriteSheet is equal to love.graphics.newImage.
So this is a new function.
This function is really just a way of pointing to a texture file
and loading it into memory so it creates a texture object.
I think it literally is a texture object.
And then it just takes a path.
So I can just say graphics/spriteSheet.png,
which is great.
And then now that's just the texture data itself.
Now, we haven't chopped it up yet so the goal of that
is going to be to use that generateQuads function that we just wrote ourselves.
But first I want to say self.tileWidth is equal to 16.
self.tileHeight is equal to 16.
self.mapWidth-- and this is where mapWidth and tileWidth are a little bit
different.
So the tile width here is literally just pixel size.
I know that all these tiles are 16 by 16 pixels.
The map width is going to be how many tiles wide and tall
our map actually is.
So in this case, let's just choose an arbitrary amount.
Let's say the map is going to be 30 tiles wide
and the height is going to be 28, let's just say, arbitrarily.
And then I'm going to have the actual tile map, so the numbers
that I alluded to before, this information,
because I'm going to essentially iterate over this and then
draw the right texture with a right quad down the line.
But I need to store of this in the map as well.
This is really the foundational data that comprises our map.
So I'm going to go back here, and what I want to do
is say self.tileSprites is equal to generateQuads.
So sprites, quads, we're kind of using them
sort of synonymously in this context.
But it's going to take in our self.spriteSheet.
And remember that takes in the tile width and tile height.
So I can say 16, 16 here.
I can say self.tileWidth, self.tileHeight.
That might be a little bit more best practice to do that.
So we're going to do that.
And then we actually need to populate our tile map with those numbers.
Now, over here we can see that we're using zeros and ones just as a mockup.
But if we look at our actual tile sheet, we can see here that 1 is the brick.
We know that.
That's the important tile.
And then if we go over here, 2, 3, 4, we see that 4 is actually
an empty tile, which is great.
We can use that to represent the sky because if it just draws
no information, if it just draws blank pixels,
it'll be the equivalent of no tiles actually existing there.
So I'm actually going to go up here to the very top.
I'm going to define a couple of constants.
I'm going to say, TILE_BRICK is equal to 1,
and let's just say TILE_EMPTY is equal to 4.
OK, perfect.
And then we'll say, for y is equal to 1 self.mapHeight
do, and what this is going to do is it's going to iterate top
to bottom throughout the entire table.
Or really, the table is empty now, but it's
going to iterate through the entire map that we have decided on our dimensions.
And what we can do is we can say for x is
equal to 1 self.mapWidth, so remember, columns and rows at this point.
We'll do self setTIle x, y, TILE_EMPTY.
And we haven't written this function yet, so why don't we actually do that.
So let's say function map setTile, which takes in an x, a y,
and we'll say a tile.
So in this case, the tiles are literally just
numbers, very simple way of representing tile information.
And for this, let's just say self.tiles--
remember, that's our data structure, sort of our array for storing our tile
information-- self.tiles at--
and this is an interesting way of storing information
in a sort of compact way for tile maps.
And you can do this in multiple different ways.
But this is essentially storing 2D map representation
in a one-dimensional way.
So we'll say y minus 1 times self.mapWidth plus x.
And so by virtue of that, we're essentially
saying, wherever our y is minus 1 because everything is 1 indexed.
So for example, if this is our first row and y is equal to 1, well,
it's going to equate to 0 here times the self.mapWidth
because that'll essentially get us--
if we're looking at our array, if we go back over here--
so if we're looking at this, have a two-dimensional map here.
But really, we have a one-dimensional map
that's representing a two-dimensional map space.
If y is equal to 1 and we subtract 1 and we add x,
then we're essentially getting anything within this top row.
But if, let's say, y is equal to 2, we're going to end up becoming 1.
So y will get subtracted to 1 times the map width, which
is this, which will put us right here.
And then we add x to that.
Well, really it puts us right here, and then we
add x to that, which-- let's say x is 1.
We get this tile.
x is 3, we get this tile, so on and so forth.
So y gets multiplied by the map width.
And you add x to that, and that allows you to index
into your one-dimensional array as if it were a two-dimensional array,
essentially.
Now we're going to write another function for getTile,
and we're just going to get an x and a y that goes into that.
We're going to return self.tiles at y minus times self.mapWidth plus x.
So essentially, the same thing, but all we're doing in this case
is just returning that same value.
So what we're doing here is just essentially filling
the map with empty tiles.
Now what I want to do is not just have purely empty tiles.
I want to also have some actual physical tiles at the bottom
of the screen, some bricks.
So what I can say is, for y is equal to self.mapHeight divided by 2 do.
So we're starting halfway down the map in this case.
And then I'm going to say, for x is equal to 1 until self.mapWidth do.
Or actually, for y is equal to mapWidth divided by 2,
and then I need to make this go until self.mapHeight.
So for x is equal to 1 until self.mapWidth do,
self setTile x, y tile, and I think I called it BRICK.
Yep, TILE_BRICK.
So this starts halfway down the map, populates with bricks.
So we're starting at map height divided by 2, not 0.
So we started at 1, rather.
We started at 1 here, and then we ended up going all the way down
to the mat height.
We could have made this mapHeight divided by 2 if we want to.
Would be totally viable.
But we ended up going all the way down, essentially populating it
with some default data, the TILE_EMPTY.
And then we went from the halfway mark downwards.
So we started at self.mapHeight divided by 2 going down until self.mapHeight.
And this is, by the way, how for loops work in Lua.
You have a 4 something equal something, comma, and then the end of that.
So if you have y equal to 1, you can say until self.mapHeight divided by 2,
and it increments sort of implicitly.
So it knows that that should be plus 1.
Now, you could specify a negative just like this,
if you wanted to do a backwards crawl through your for loop.
But in this case, since we're going forwards,
we can leave that off altogether.
So it's a little bit of a different way of using for loops,
but this is the Lua sort of syntactical way to do that.
So that's how we initialize our map.
So we set our tile at x, y to be TILE_EMPTY, which is essentially 4.
And then we set it from halfway down to TILE_BRICK, which is 1.
And again, that maps to how we have laid out our texture.
Now, if we go to map render, we can use a similar loop.
So if I say, for y is equal to 1 until self.mapHeight do, and then we're
going to say, for x is equal to 1 self.mapWidth do.
And then what I want to do is say love.graphics.draw,
which is a new function, so this just draws a texture.
We'll say self.spriteSheet.
So we're drawing the sprite sheet, but here's the important thing.
After we actually draw the sprite sheet and we're dealing with quads,
we need to specify what quad we want to draw.
So what I can do is say, self.tileSprites tile sprites
at self getTile x, y.
And then that will end up giving us the actual value of the quad,
whether it's 1 or 4, into self.tileSprites,
which is mapping to the quad that we chopped up
from the texture information.
So we have this sprite sheet, we have the quad.
Now we need to specify the x, y.
So I'm going to go ahead and just do an Enter here.
So in order to do that correctly, now remember,
our x and y are 1 indexed here.
So we're starting y at 1, x at 1 just because of the way
that tables generally work in Lua.
But pixels are not 1 indexed.
They're 0 indexed.
So to do that, all we need to do is say x minus 1 times self.tileWidth, and y
minus 1 times self.tileWidth.
And this, essentially, is drawing everything out on a grid.
So it's figuring out what our x, y is, offsetting it by,
in this case, self.tileWidth and self.tileHeight, this should be,
actually.
They're the same in this case, so it doesn't matter.
But those are 16 pixels, so we're essentially drawing a 16-pixel grid.
We're subtracting 1 from our table index so that it maps to are 0-positioned
coordinate system.
Now I'm just going to go ahead and give it one last look.
That should be just about everything.
So let's go over to main.lua.
We're creating our map here.
We're drawing our map here.
And we have this text.
I'm going to get rid of this text right here, actually.
I'm going to go ahead and save that, run this, make sure it works.
And then I have an end error in main.lua,
so I'm going to go back to map line 33, make
sure this is working appropriately.
So self.tiles-- oh, OK.
I made a mistake right here.
This needs to be equals tile, like so.
OK, perfect.
Let's go ahead and rerun that one more time.
And then I have another issue here.
So bad argument to draw.
Quad expected got nil, map 46.
So that just means that it found a nil object
where it should have expected a quad, so that just
means I have a slight little bug here.
So self.tileSprites, let's just make sure that we're actually getting that.
So self.spriteSheet, self.tileWidth self.tileHeight.
I think I found the issue.
It looks like I forgot to increment the sheet counter inside of util.lua,
the generateQuads function, so I'm just going to say,
sheetCounter equals sheetCounter plus 1.
So essentially, it was never incrementing.
So all I was doing was resetting the first quad to whatever
the next quad was in the loop here.
So there's only one quad in the sheet, ever.
So I'm going to go back to main.lua.
Let's try running this one more time.
And we do indeed have it working, so that feels very good.
So apologies for that.
But yeah, there is the sort of tile level up and running.
And it works just like we saw in the screenshot.
That was a lot of work for Mari0, and that's
just there's a lot of scaffolding required to actually get this working.
The next few were going to be a little bit on the shorter side,
so stick with me soon for Mario 1, where we actually
get our map scrolling, which is a very important part of sort
of side-scrolling platformers, by the very name of it.
So see you soon in Mario 1.