Placeholder Image

字幕列表 影片播放

  • [MUSIC PLAYING]

  • COLTON OGDEN: Hey, good evening.

  • Welcome to GD50 lecture four.

  • This is Super Mario Bros.

  • As seen on the slides here, though, we're

  • not using the actual Super Mario Bros. sprite sheet.

  • This is sort of like a rip off.

  • But I found a really awesome sprite sheet

  • that has all the basic tiles that we need to get this thing working.

  • There's a link in the distro as to where you can find it online.

  • I had a lot of fun playing with it.

  • So hopefully, maybe if you're curious, you

  • can use some of the sprites in there to go off and do your own thing.

  • But Super Mario Bros.-- the actual game which this lecture and assignment are

  • based off of-- is the game shown here.

  • I think everybody knows what it is.

  • It's probably the most famous game of all time.

  • But this game came out in 1985--

  • sort of revolutionized the gaming industry.

  • It was the game that brought the gaming industry from a crash

  • in the '70s thanks to a lot of poor game making policies

  • and companies and low QA standards.

  • It basically took the gaming crash of the late '70s, early '80s

  • and brought games really back to the forefront of people's consciousness.

  • This and games like Legend of Zelda and a lot of other NES titles

  • made Nintendo basically the dominator of the video games

  • industry in the '80s and '90s.

  • And even today, with games like Breath of the Wild,

  • they're still doing their thing.

  • This is Super Mario Bros.

  • It's a 2D platformer.

  • And what this basically means is you control Mario, who's a plumber.

  • He goes around, walks sort of looking at him from the side.

  • He walks left to right.

  • He can jump up and down.

  • He's affected by gravity.

  • He can hit blocks.

  • He can jump on enemies.

  • He can go down pipes, and there's a bunch of levels.

  • It was, for its time, quite a complicated game,

  • and it spawned numerous offshoots and rip offs and other good quality

  • platformers.

  • While we talk about Super Mario Bros. today,

  • some of the topics we'll actually be talking about are tile maps--

  • so how we can take basically a series of numbers--

  • tile IDs-- and turn that into a game world.

  • As you can see here, the game is broken up into blocks of 16 by 16 tiles.

  • You can see the bricks and the question mark blocks,

  • and the pipes are even all composed of simple tiles.

  • And they map to IDs.

  • And when you take a 2D table or array and you just iterate over all of it

  • and render the appropriate tile at the appropriate x, y,

  • you get the appearance of existing in some game world,

  • even though it's just composed of a bunch of tiny little blocks.

  • 2D animation is something we'll talk about.

  • So far, we haven't really done any animation

  • at all in terms of at least characters.

  • We'll do that with Mario.

  • He'll have-- our version of Mario, an alien--

  • when he's moving, he'll have two frames of animation.

  • The frames of animation-- that's sort of like a flip book

  • if you've ever used one, where you can see individual pictures.

  • And when you display them rapidly back to back,

  • you get the appearance of animation.

  • We'll be talking about that.

  • Procedural level generation-- we'll be making all of our levels

  • generate randomly.

  • So every time we play the game from the beginning,

  • everything will be completely different.

  • We don't have to hard code a finite set of levels.

  • Everything will be dynamic and also interesting, in my opinion.

  • We'll talk about the basics of platformer physics

  • and how we can apply that to our game world

  • here, because we are just using a table of tiles,

  • each with an x, y that's hard coded in the game world space.

  • All we have to really do is take an x, y of Mario, for example,

  • and then just divide that by the tile size.

  • And then we get basically what tile that is

  • in our array at that point in the world.

  • And so it's really easy to do arbitrary collision detection based

  • on what direction you're going and not have to iterate over

  • every single tile in your world.

  • Because it's just a simple mathematical operation to get the exact tile

  • given two coordinates, since the world is in a fixed space.

  • We'll have a little snail in our game that

  • walks around and does a couple of random animations

  • and will go after the player, sort of like a little basic intro to AI.

  • And then lastly, we'll touch on things like power ups and game objects

  • and how we might be able to influence Mario and pick those up

  • and that sort of thing.

  • So first though, a demo.

  • So if anybody is willing to come up on stage

  • to test out my implementation of Mario, that would be awesome.

  • James?

  • I'm going to go ahead into here, so we should be all ready to go.

  • So as soon as you're ready, go ahead and hit Return there,

  • and you should be up and running.

  • So as a part of having random levels--

  • so currently, we have a green alien.

  • The blocks have a random chance in this case to spawn a gem.

  • And so once they do, you can pick the gem up.

  • Either they have a gem or they don't.

  • You can pick it up, and you get 100 points.

  • As we can see, the world is sort of shifting

  • based on where James's avatar is, so it tracks the character.

  • We have some notion of a camera.

  • You're getting unlucky with the blocks so far.

  • So you can fall down through the spaces, so you probably want to avoid that.

  • But if you want to demonstrate doing it--

  • so in that case, we collided with the two blocks below it.

  • The one on the right had the gem.

  • So go ahead and just fall down so we can demonstrate.

  • So when we fall down, we detect whether the player

  • has gone below the world limit, and then we start him back

  • at the beginning of the game.

  • You can press Enter, it should regenerate a brand new world.

  • Notice how we have random holes in the ground.

  • We have random tiles.

  • We have random toppers for them.

  • All the blocks are random.

  • We have snails now.

  • They're sort of chasing after James.

  • He can jump on top of them.

  • There's a lot of little moving pieces here, but a lot of them

  • are actually pretty simple.

  • And I'll show you very shortly.

  • JAMES: Should I stop?

  • COLTON OGDEN: Yeah, sure.

  • That would be a great point.

  • So thanks, James.

  • Appreciate it.

  • Currently, there is no notion of a level ending.

  • That's part of the piece that actually will

  • spawn an object that the player can interact with to just sort of retrigger

  • a new level, basically.

  • But the whole engine behind this basic platformer is there, and it all works.

  • And so our goal is seen here.

  • Our goal in this lecture is to demonstrate

  • how we can get things like a character that moves around on a screen,

  • and a camera that tracks their position, and tiles that are randomized.

  • And maybe there are pillars in the ground, holes in the ground.

  • All of this, again--

  • at least the tiles-- are stored as just numbers.

  • So all we really need to do is perform a transformation on a series of numbers.

  • Maybe 1 is equal to a tile being there, 0 is equal to empty space.

  • And so just by looking at it, we'll see we go column by column.

  • We can say, oh, maybe there's a chance to not spawn any tiles along the y

  • column on this x of the world map.

  • Or on this particular y, maybe instead of spawning the ground level,

  • we spawn a couple above it and down so that we get a pillar

  • and so on and so forth.

  • And it's just this summation of these randomizations

  • equals a nice little variety of game levels.

  • So the first thing we should talk about really is what a tile map is.

  • And what I've alluded to so far is you can really think of a tile map

  • as being effectively a 2D array or a table of numbers.

  • And it's a little more complicated than that depending on how complex

  • your platformer is, because some numbers are equal to tiles that are solid

  • or not.

  • So you should be able to check whether a tile is collidable,

  • meaning that the player or whatever entity you want to check for

  • can actually collide with it or

  • Not.

  • So obviously, we don't want to trigger a collision on empty tiles.

  • We want the player to move freely through those.

  • But if they run up against a wall or if gravity is affecting them,

  • and they hit tiles below them or above them, we want to detect a collision

  • and then stop them based on which direction they're moving.

  • And depending on how complicated you get with your platformer, maybe

  • you have animated tiles, for instance.

  • So if a tile's animated, it will display a different frame of animation

  • based on what timer you're on.

  • Really, the sky's the limit.

  • In this case, we'll be fairly simple.

  • Our tiles will mostly just be numbers with a couple of other traits, which

  • we'll see later on.

  • And this is just an example here of a very simple map-- just a colored

  • background.

  • We have our character, and then we can sort of visualize all of those tiles

  • as being just for the sake of theory 0s or 1s.

  • So tiles0-- so I'll actually get into a little bit of implementation

  • here as to how we can get drawing some very simple tiles.

  • So if you're looking at the distro, in tiles0

  • is going to be where we start off here.

  • And I'm going to go ahead and run tiles0 so we can see what that looks like.

  • So this is just tiles0.

  • It's a much simpler program than what we just saw, but all we're doing here

  • is just a color in the background and then tiles.

  • Off the gate, anybody have any ideas as to what the first step would

  • be if we wanted to implement this?

  • AUDIENCE: Just put the tiles in a loop, draw them, and then have a background?

  • COLTON OGDEN: So put the tiles in a loop, draw them,

  • and then have a background.

  • Yes.

  • So basically, if this is main.lua in our tiles0,

  • first thing we're going to need is a tiles table to store our--

  • we're not going to be storing just flat numbers.

  • We'll be storing little mini tables that have a number in them and ID,

  • so we can say tile.ID if we have a 2D table.

  • Here, we have an empty table.

  • We're going to populate that.

  • If we're going to draw our tiles, we are going to need a sprite of some kind.

  • And what I did was I just chopped out a little segment here.

  • So this is tiles.png.

  • It's just literally one tile from the main sprite

  • sheet that comes with the distro.

  • And then on the right side is just transparent so that we can offset--

  • maybe tile ID 1 is equal to solid block, and then tile ID 2 is equal to empty.

  • And so if we recall generate quads, we can split up a sprite sheet

  • into however many quads we want to.

  • Let's say this is 16 tiles tall--

  • each tile-- and then the whole thing are two tiles wide.

  • So it needs to be split into two separate tiles.

  • We'll just generate quads, and then we'll have, recall, a table.

  • Each of the indexes of that table will be

  • a quad that maps to one of these tiles.

  • So number 1 will be this tile here, number 2

  • will be the transparent bit over here, and then

  • that's how effectively our IDs are going to map

  • into what gets drawn onto the screen.

  • The ID is the index into our quad table.

  • So going back into tiles0, we have here just a map width and height.

  • We're just going to say generate a map 20 by 20.

  • RBG-- we're just going to make it random,

  • so we're going to clear the screen with a random color.

  • And then this is the quads = GenerateQuads.

  • And notice that we're passing in tile size here.

  • It's good practice just to make your tile size a constant.

  • So our tile size in this entire lecture--

  • they're all going to be 16 by 16.

  • And so since they're symmetrical, we just pass in tile size TILE_SIZE.

  • And then here is where we actually end up spawning the map--

  • so nested for loop.

  • y gets 1 to map height, x gets 1 to map width.

  • Remember, we have to insert a blank table into the base table that's

  • going to act as our current row.

  • And then in that row, we're going to add a small at tiles y,

  • because y is going to be up here-- our current row and ID.

  • And so what we're doing here is if y is less than 5--

  • meaning we'll just set an arbitrary point for the ground, basically.

  • If it's less than 5 tiles from the top, then just make it the sky.

  • And so sky-- up here on line 24, 25-- we just

  • set two tile IDs, as I said before.

  • Sky is 2, so it's going to be on the right side of the sheet.

  • And then ground is one.

  • It's going to be the very first quad generated in the sheet.

  • So if y is less than 5, that ID should be equal to sky else

  • it should be equal to ground.

  • And so down here is where that comes into play.

  • We're going to clear the screen with our random color.

  • We're going to iterate over the loop, as James said.

  • We're going to get the tile at tiles y x,

  • and then we're just going to draw the sheet and the quads at that tiles ID.

  • And then recall, since tables are 1 indexed but coordinates are 0 indexed,

  • we take the x and the y, subtract 1 from them,

  • and then we just multiply them by tile size.

  • And that has the effect of drawing each of those tiles

  • at their respective point in the world and making

  • it seem as if we have this world-- this bunch of bricks

  • with a random background every time.

  • Which isn't all that interesting, but just a little bit more variety.

  • And so that's the very basic gist behind it.

  • I mean, it's essentially almost the same thing as what we did in match three,

  • where we just mapped the individual tiles that

  • were in the grid to indexes in the tile sheet based on the color and variety.

  • Only this time, they're always going to be in the exact same place,

  • so we don't have to worry about whether their x and y are

  • different from their grid y and grid x.

  • We're not maintaining a reference to those.

  • And so that's static tiles.

  • Does anybody have any questions about how we just

  • draw static tiles to the screen?

  • Pretty basic stuff.

  • The whole name behind side scrolling game

  • is that the tiles scroll based on what we're doing in the game.

  • It can be an auto scroller, in which case

  • maybe you're an airplane that's sort of going

  • through a level that's scrolling automatically,

  • and you're shooting things.

  • And you're not really in control of where you go.

  • Or it can be like Mario, where you control an avatar.

  • And you can walk around and jump and stuff,

  • and the camera will always be fixed on you.

  • And so the scrolling is just relative to where your character's x and y are.

  • So I'm going to show you guys an example of how we can

  • get scrolling implemented in our game.

  • And to do that, the function that we're really going to be looking--

  • at a new function--

  • is love.graphics.translate(x, y).

  • And so what that does is effectively just translates

  • Love2D's coordinate system so that whenever we draw something,

  • it gets automatically shifted by x, y.

  • And so that has the effect of the everything being sort of skewed

  • based on the x, y that we pass it.

  • And so if we maintain a reference to where the character is,

  • we can just shift where everything gets drawn on the screen.

  • And that will have the effect of it being a camera, but it's not.

  • All we're doing is just shifting the coordinate system

  • based on some offset--

  • x being in this case where the players is effectively.

  • AUDIENCE: So it changes the whole coordinate system?

  • COLTON OGDEN: It does.

  • It shifts everything in the coordinate system that you draw by the x and y.

  • And so that will basically affect what's getting rendered

  • into the active window at that time.

  • So I'm going to go ahead and pull up tiles1 here

  • so we can see how this works.

  • Let me go ahead and first run the program.

  • So if we're going into tiles1 in the distro,

  • currently it looks almost identical.

  • But I can move it if I just press left or right.

  • And so we can see here, this is where the 2D array of tiles

  • gets cut off here.

  • And then it also cuts off, because we're only generating a 20 by 20 level.

  • It also gets cut off at the very right side as well.

  • And these are details you would normally hide from the user

  • by just clamping the x between 0 and the right side

  • of the map minus VIRTUAL_WIDTH.

  • And that will have the effect of whenever you get to this point,

  • it won't let you go right anymore, and same thing for the left side.

  • Well, all we're doing right now-- we're not

  • doing it based on the character at all.

  • We're just using keyboard input.

  • So let's go ahead into tiles1.

  • And so the important thing that we're going to look at is--

  • as I just alluded to, we're calling love.graphics.translate

  • on some value called cameraScroll.

  • It has to be a negative value, because if we're moving to the right

  • up here or to the left--

  • if we're moving to the left, camera scroll basically is going to decrement,

  • so it's going to get less.

  • So we can say the camera scroll when we're

  • going left is going to be 0 or less if we're starting at 0,

  • or it's going to decrement.

  • If we press right, camera roll should increase.

  • If we want the appearance of moving to the right or moving to the left,

  • you actually have to translate by the opposite direction.

  • Because if we look at this, and if we call love.graphics.translate positive,

  • all of this is going to get moved to the right.

  • So it's going to have the appearance of us moving left.

  • And if we translate it to the left by a negative amount,

  • it's going to have the appearance of us moving right.

  • So if our scroll is positive and we want to move to the right,

  • we actually have to translate by a negative amount.

  • And so that's why I'm calling negative math.floor(cameraScroll).

  • Does anybody know why we're calling math.floor on cameraScroll

  • instead of just calling negative camera scroll?

  • Does anybody remember what math.floor does?

  • So math.floor will return the--

  • it'll basically truncate the number down to the lowest integer.

  • It will basically take off the floating point value.

  • Because we're rendering to a virtual resolution

  • with push, if we basically offset the translation by a fractional amount,

  • you'll get artifacting.

  • Because it's taking your window and just condensing

  • your image onto a virtual canvas, you'll get weird blur and stuff like that.

  • So whenever you draw something and you have a fractional number for something,

  • and you're drawing it to a virtual canvas that's been magnified

  • or it's being condensed, just make sure to math.floor it

  • so you don't get any weird blur artifacting.

  • If you take this out and experiment around, or even if in the distro

  • you take it out of the player's position, you'll see the player

  • will get weird blurry artifacting and stuff like that.

  • So that's why that's there, in case you're curious.

  • And so all we're doing here-- we're just saying

  • if it's equal to left, scroll the camera left, scroll it right,

  • or basically decrement our camera scroll and then increment or camera scroll.

  • And then just use the negative version of that here.

  • You could also just assign camera scroll equal to positive

  • when you move left and negative when you move right,

  • and then you could give it the regular camera scroll here.

  • But it's sort of mentally flipped in terms of this part.

  • So I just made the decision to decrement it here when we're pressing left,

  • because we're going less on the x and then more on the x when we press right.

  • Does this make sense?

  • Anybody have question?

  • AUDIENCE: I have a question.

  • Is there a corresponding function in JavaScript

  • and in other languages like this where you can shift a whole coordinate?

  • COLTON OGDEN: Is there an equivalent function in JavaScript

  • where you can shift the whole coordinate system?

  • Not in base JavaScript, probably.

  • I'm not too familiar with CSS.

  • There might be a CSS function that does it.

  • In a lot of 2D game engines, yes, I would say.

  • And a lot of actual 2D game engines will have a camera object, which

  • sort of encapsulates this behavior.

  • Love2D doesn't have a camera, so this is sort

  • of why we're doing this-- is because it's kind of a lower level game

  • framework, Love2D.

  • It doesn't really give you as many things

  • right out the gate, which makes it great for teaching these concepts.

  • But a more robust solution like Unity or Phaser

  • or a lot of other game frameworks is that they'll just have a camera object.

  • And you just basically give that your x, and then you just move that.

  • You basically tell that to track the player--

  • like camera.trackPlayer or trackEntityPlayer--

  • and that'll have the same effect.

  • It's a little bit more abstract.

  • It's a higher level than what we're doing,

  • but it's the same exact principle underlying.

  • So any other questions as to how this works?

  • All right, cool.

  • So that's all we're effectively doing.

  • We're just getting a camera scroll, decrementing it and incrementing it.

  • And then just every frame, we're translating everything

  • before we draw everything.

  • You have to do the translation before you draw, because everything that you

  • draw after the translation gets affected by the new coordinate system change.

  • So that's scrolling.

  • Let's get to actually talking about drawing a person-- an avatar--

  • more than just a set of tiles, since that's what the game revolves around.

  • If we look at character0, this would be our first example here.

  • This is just going to be a very simple example--

  • charactr0.

  • You guys probably know how this works already.

  • All we're doing is just drawing a sprite to the screen--

  • so just love.graphics.draw.

  • We're getting quads from a tile sheet.

  • I believe it's in the slides.

  • The actual sheet is here.

  • So we have this little guy--

  • several frames of animation.

  • It's 16 wide, 20 tall, and we just take a quad.

  • We split it up into quads first.

  • So we know that it's 16 wide by 20 tall, so we just generate quads

  • on this image by 16 and 20.

  • And then in this example, all we're doing

  • is taking the first frame, which is quads1, and just drawing that.

  • As you can see here, we have a bunch of different things.

  • We have like a crouching state, and we'll

  • get to more about animations in a little bit.

  • But here, we have him climbing up a ladder.

  • But you can see all these different frames.

  • We'll end up showing how you can play them back to back

  • and get different animations.

  • But for the sake of this basic example, all we're doing

  • is just rendering the very first frame.

  • And we can see that--

  • let me make sure I'm in the right file, which I am--

  • we are getting the character sheet here on line 43 and 44.

  • And then we have to give him an x, So characterX, characterY.

  • In this case, we're just setting him above tile 7,

  • so we do 7 minus 1 times TILE_SIZE because tiles are 1 indexed

  • but coordinates are 0 indexed.

  • And then we just subtract the height so that he's

  • right above the tile instead of right at the tile.

  • And then down here, we do a love.graphics.draw

  • on, as I said before, just characterQuads 1.

  • Just a very basic hard coded example.

  • Any questions at all as to how this works?

  • So now let's say we want him to move.

  • What do we need?

  • What's the next step if we just wanted him to move?

  • AUDIENCE: Give him an x and y?

  • COLTON OGDEN: Yes, give him an x and y.

  • So yes, so he does have an x and y already.

  • So if you look at-- am I in the right one?

  • So if you go to character0, this is character0 still.

  • We have given him an x and y already, but there needs to be another step.

  • What's the other step involved?

  • So if we wanted to move, we need to check for keyboard input.

  • And then we need to take his x--

  • we're just going to move him on the x-axis for now.

  • We basically need to take his characterX variable up here,

  • and we need to modify that.

  • We can basically do the same thing that we did down here in love.update.

  • Previously, it was on the coordinate system--

  • love.graphics.translate.

  • We modified the camera scroll.

  • We set that equal to scroll speed times delta time.

  • We subtracted or added it.

  • In this case, what we're doing is we have a new constant called

  • CHARACTER_MOVE_SPEED, and we're just doing that exact same operation

  • but on characterX instead of cameraScroll.

  • So the end result of that is that we have the character here.

  • And then we can move him left or right, and he go off screen.

  • Now, there's a couple of things wrong.

  • What's wrong?

  • What are some of the things that are wrong with the scene right now?

  • AUDIENCE: The camera should move with him.

  • COLTON OGDEN: Camera should move with them.

  • AUDIENCE: No animation.

  • COLTON OGDEN: Does not have animation.

  • Those are probably the two real things that are wrong.

  • So camera does not track him, which is an important thing.

  • Obviously, we want to be able to maintain a reference to our character,

  • unless we're at the left edge of the screen.

  • If we're at the left edge of the screen, this is actually OK.

  • And that's part of the distro-- is we clamp the x so

  • that it doesn't go past the left edge.

  • But if we're beyond the middle and not to the right edge of the screen,

  • it should be moving along with him and vice versa.

  • And then he needs to animate, so his sprite

  • needs to change every certain number of seconds whether he's moving.

  • And it has to be only when he's moving, right?

  • If he's standing still, you can have an idle animation.

  • Some characters will tap their foot and do stuff like that.

  • But let's say for the sake of this example we want him just

  • to stand still when he's idle.

  • And we want him to have an actual animation when he's moving.

  • We need to take care of these two pieces--

  • three pieces if you count the idle animation part.

  • So let's go into character2 and take care of the first part, which

  • is tracking him.

  • So let me go into character2.

  • Let's run it first so we can see what it looks like.

  • So now the camera is basically affixed to the player.

  • In this example, we don't take care of the left edge issue.

  • In the distro, that's fixed.

  • But we have the basic side scrolling mechanic-- take a character,

  • follow him.

  • How do you think that we're accomplishing this?

  • Yes.

  • AUDIENCE: Translate the drawing against the characterX?

  • COLTON OGDEN: Yes, exactly.

  • And it can't be exactly the characterX though,

  • because if it is, then the character is going to be on the left edge, right?

  • So we need to offset our x that we translate by.

  • We need to basically translate by his x minus half the screen space

  • plus half the character width.

  • And that will have the effect of translating it but always keeping

  • that offset half a screen width away from the player, if that makes sense.

  • And so what we're doing is in character-- this is character2, right--

  • character2, we're still doing the same thing we did.

  • Actually, that's the wrong file.

  • We are modifying characterX here.

  • So same thing we did before--

  • multiply the move speed by delta time and either add or subtract it

  • if we're pressing left or right.

  • But also, here-- reintroducing camera scroll.

  • And we're setting it to, like I said, characterX minus VIRTUAL_WIDTH divided

  • by 2, half the screen, and then positive offset

  • of his width divided by 2 so that he's perfectly right in the center.

  • Because remember, characters' coordinates

  • are set by their left, not their center.

  • And then we just do what we did before.

  • We translate the scene based on cameraScroll,

  • and we render him at characterX characterY using math.floor

  • to prevent him from being at a fractional point in our world space

  • and then it being blurry and artifacted.

  • And that's sort of it in terms of how we can get tracking over character.

  • And if you wanted to track along the y-axis,

  • you could do the exact same thing.

  • Maintain a cameraScroll x and a cameraScroll y--

  • so keep them separated.

  • And then you would just translate here.

  • So we're passing in 0, because we don't want

  • to track along the y-axis necessarily.

  • But all you would need to do is pass in your y cameraScroll.

  • And then you could do it based on characterY

  • and whether or not they're above the ground

  • or past a certain point in the sky.

  • So any questions at all as to how the camera tracking is working here?

  • All right.

  • So we took care of one issue, which was the lack of tracking.

  • But there was one other issue, which was he's not animated.

  • All he's doing is just moving sort of like M.C. Hammer--

  • or is it M.C. Usher?

  • M.C. Hammer?

  • I forget.

  • He's doing that.

  • He's not doing anything.

  • We need to actually animate him so that he looks like he has some life to him

  • and that you can also differentiate importantly

  • between two separate states.

  • He can be idle, he's not moving, and he can be moving.

  • So we should have some sort of visual feedback

  • as to what's currently going on.

  • So anybody know how we can go about implementing

  • an animation for our character?

  • What are the pieces that we'll need?

  • AUDIENCE: I guess if he's moving right, then

  • call a function and a render that looks through some images?

  • COLTON OGDEN: Yes.

  • So if he's moving right, then have a function

  • that sort of loops through some images.

  • That is effectively what we will be doing.

  • We have a class called Animation, which I've introduced here.

  • And all it basically does is keep track of-- you

  • pass it in a table, which has the frames of the sheet

  • that you want to animate over.

  • So we can just pass in--

  • let's go ahead and take a look here.

  • And I referenced the slide earlier, but all of these

  • are 1, 2, 3, 4, 5, 6, 7, 8, 9, 10-- however many there are,

  • you just pass into the animation.

  • Let's say he's on a ladder.

  • So let's say this is 1, 2, 3, 4, 5, 6, and 7.

  • You say, the frames are going to be 6 and 7,

  • so those will just loop left to right, starting back

  • at the beginning when it's finished And then you give it an interval.

  • So say I want the animation to happen this fast in terms of seconds,

  • so I want it that maybe happened every 0.2 seconds.

  • And so that will have the effect of every 0.2 seconds,

  • it'll keep track of a timer.

  • So have we gone over 0.2 seconds?

  • Start at 0 and then add delta time to it every time.

  • If we have, increment what our current frame of animation is.

  • So our current frame is this one, and then 0.2 seconds elapses,

  • it's going to be this one.

  • And then 0.2 second elapses, and we need to loop back to the beginning.

  • So we'll end up using modulus to take care of that

  • as we can see in the Animation class.

  • Basically, that's all done here.

  • So if we have more than one frame of animation, recall it gets a def here.

  • So we get frames, we get an interval, get a timer that's initialized to 0,

  • and then get a current frame.

  • We'll say the current frame is 1.

  • And then as long as we have more than one frame,

  • there's no point in looping over or trying to animate

  • any animation that only has one frame.

  • And we can, of course, have animations that only have one frame.

  • Idle is only one frame of animation, as we saw here.

  • That's only one frame.

  • We don't need to do any sort of logic to say, oh,

  • what's the next frame, because there's only one frame.

  • But if we were to look at character3, we can see two frames there.

  • And then that's just one, frame he's idle.

  • And when we move left, he moves in that direction.

  • Anybody recall how we can get him-- because obviously,

  • we saw the spreadsheet just a second ago,

  • and there was only one direction that the sprites were facing--

  • how we can get him to look that way, even though there's

  • no sprites for him to look that way?

  • AUDIENCE: Flip it on the axis.

  • COLTON OGDEN: Flip it, so love.graphics.draw.

  • Recall you can pass in a negative scale factor on whatever axis you want,

  • and that'll have the result of flipping it along that axis.

  • That's all we're doing.

  • So this is the default frame, so we're just drawing it.

  • And then we have to keep a reference to whatever direction he's facing.

  • And if his direction is equal to right, we'll

  • just draw that frame and then loop and process the animation.

  • If he's facing left, draw it, but also perform a negative 1 transformation

  • on the x-axis.

  • And just like that, we have that working.

  • So all we're doing--

  • just keep a timer.

  • And then when the timer goes over our interval, just increment the frame.

  • And then use modulus to loop back over it--

  • back to starting at 1.

  • And that's all done here on this line 28.

  • And so you can look in there a little more

  • if you want to get a handle on how the math works,

  • but it is just a simple sequence of iterating over a collection of frames

  • based on a timer.

  • And that has the effect-- just like a flip book, as I said earlier--

  • of our character having an animation and having some life.

  • So any questions as to how this animation class works?

  • AUDIENCE: The render is in the Animate class?

  • COLTON OGDEN: So no.

  • The render is not in the animate class.

  • So the render is--

  • I realize I didn't show any actual main here.

  • We have two animations here, which was just the idle one,

  • so we're just passing in one frame.

  • We're going to give it an interval of 1.

  • It's not going to really matter, but just for the sake of consistency,

  • we're giving it an interval of 1-- arbitrary.

  • And maybe we want to change his animation later.

  • So by having an interval here, we won't forget to add one later.

  • Moving animation-- recall 10 and 11.

  • So it's toward the end of the sheet--

  • the two walking frames.

  • Interval here is 0.2 seconds.

  • We need a current animation to render him,

  • and then we keep a reference to whatever direction he's looking at.

  • So if he's looking to the right, we're going

  • to reference this in the love.graphics.draw at the bottom.

  • And that's what we're going to use to perform the sprite flipping

  • along the x-axis.

  • Maintain a reference to that.

  • And then down here, the part that we actually reference the animation

  • is on line 150 if you're looking at character1.

  • Or is it character2?

  • Sorry, character3.

  • If you're looking at line 150 in character3,

  • we're using currentAnimation:getCurrentFrame().

  • So the class will actually just tell you whatever the current frame of animation

  • is, because it keeps a reference to what frame it is based on the timer

  • and how much has elapsed.

  • AUDIENCE: So the class is generating a different frame real time

  • and plugging it in there.

  • COLTON OGDEN: Yeah.

  • It's maintaining a reference to whatever the current frame is,

  • and it's in the table of frames that it got when you gave it

  • the definition up at the top here--

  • lines 51 to 58 where we create the two animations.

  • Basically, it maintains a reference to which index in this frame table

  • we're at.

  • So if 0.2 seconds has elapsed, we start at 1, and then we go to 2.

  • And then we'll go back to 1.

  • And so it'll just basically return frames, index.

  • And frames index 1 is 10, frames index 2 is 11.

  • And so the function is getCurrentFrame.

  • So characterQuads, currentAnimation, getCurrentFrame.

  • And then here, because we're performing an origin transformation--

  • so that's another thing to consider when you're flipping sprites.

  • When you flip a sprite, it actually flips along

  • whatever its default origin is.

  • And the default origin of any sprite is its top left corner here.

  • So if you flip something along its x-axis,

  • it'll appear here instead of just flipping in place.

  • So you actually have to set the origin to its center

  • when you do any sort of in place flipping of a sprite.

  • So you'll notice in the code when you're looking

  • at it that we have plus CHARACTER_WIDTH divided by 2 and plus CHARACTER_HEIGHT

  • divided by 2 on these two here.

  • So we shift where it gets drawn, and then we shift its origin

  • offsets which are here on line 160.

  • So if you look at love.graphics.draw, you'll

  • see it has a lot of optional arguments.

  • And these two at the bottom are the origin offset arguments.

  • And so these only really come into play when

  • you do some kind of flipping of a sprite on an axis

  • and you want graphical consistency not to have it flip one way or the other.

  • Sometimes that's the effect you're looking for, but in this case,

  • it's not.

  • We want him to literally stay in the exact same place.

  • So to flip a sprite in the exact same place,

  • you need a set its origin to its center, not its top left.

  • Does that makes sense?

  • OK.

  • And also here, 0 is the rotation here.

  • So it's sort of required if you're going to add

  • this many arguments to the function.

  • But we're testing if direction is equal to left, we want to flip by negative 1

  • on the x, else just give it 1.

  • So 1 just means default transformation, so no flipping.

  • And then we don't flip on the y at all, so that will always be 1.

  • And so that's in a nutshell how you can get your character to animate and also

  • stay in place when you animate it.

  • So any questions as to how animations or the origin offsets or any of that work?

  • OK.

  • So we did talk about animations.

  • The last thing we'll talk about for the character is jumping.

  • So if you recall from Flappy Bird, how can we get our character to jump?

  • What are some of the pieces we need?

  • AUDIENCE: Key press, and then the y goes up.

  • And then we have to have gravity.

  • COLTON OGDEN: Yep.

  • So key press is one thing we need, so check for space

  • is going to be the default key.

  • y goes up, and then check for gravity.

  • So not only do we need y, but we also need delta y.

  • We need velocity, because gravity is a transformation on velocity, not

  • strictly on position.

  • So if we go back to character4, this is sort

  • of a hackish way of implementing gravity,

  • because we haven't actually incorporated tile collisions.

  • And I'll defer most of the implementation for that

  • as to the distro, and I'll go over with you guys.

  • But right now, we have the exact same thing we had before,

  • where we have tile scrolling.

  • But if I press space bar, I go up, and then he comes down.

  • And notice that he has an animation as well.

  • He has a different frame.

  • So if he's jumping, he's got a little jump frame.

  • So that means now we have three animations.

  • We have an idle animation, we have a moving animation,

  • and then we have a jump animation.

  • So effectively, we have three states as well--

  • idle state, moving state, and jumping state.

  • Four states, actually.

  • And also, I noticed a slight bug here where if you're still in the air,

  • his frame doesn't change.

  • So it actually probably should stay to that frame,

  • even if he's standing still.

  • But I guess it doesn't matter too much.

  • We also interpret it as a feature.

  • But he's got a couple of states when he's in the air.

  • There should be two states here.

  • One is jumping state, and one is falling state.

  • And do we know why the two being different is an important thing?

  • So if we think about Super Mario Bros.

  • and we think about the differences between jumping and falling,

  • what are some of the things that change based on whether Mario is jumping

  • or whether he's falling?

  • How does he interact differently with the environment, I should say?

  • So if unfamiliar, Mario-- when he jumps, he can actually hit blocks.

  • So if he's below a block and he hits a block that

  • has some sort of behavior in it, it will trigger whatever is in that block,

  • whether it's a coin or whether it's to destroy the block.

  • And if he's falling, recall if he lands on top of an enemy like a goomba,

  • he'll destroy the enemy.

  • And so we need to distinguish between these two states.

  • Because when he's jumping, he's not able to--

  • when he's actually going up, he can't attack the enemy.

  • And likewise, when he's falling down, he can't destroy the block.

  • So even though he's jumping up in the air

  • and the gravity is applying a transformation

  • and it all looks like one state, there's actually two important changes

  • in his state that are relevant.

  • And that's something that we'll need to pay attention to,

  • and it's in the distro.

  • He has a falling state and a jumping state.

  • Even though they share the same animation,

  • they have different behavior.

  • So let's go ahead and look at the character4 distro here.

  • So what I've done here is I've added a delta y for the character.

  • So just like in Flappy Bird when we press space

  • and we made our delta y go up to negative 50--

  • so instantly shot up pretty high, because that was getting

  • applied every frame.

  • Same thing here.

  • Once we press space, we're going to change delta y to negative 50

  • if we go down to right here.

  • So if the key is equal to space, I have it in the love.keyPressed function.

  • Since we're doing all this in main.lua just for illustration,

  • things are a little simple.

  • If key is equal to space and his delta y is

  • equal to 0, what would happen if we didn't

  • check to see if delta y was equal to 0?

  • AUDIENCE: We double jump in the air.

  • COLTON OGDEN: Yep.

  • We'd be able to jump infinitely, so we have to do a check for that.

  • We set his dy to JUMP_VELOCITY.

  • JUMP_VELOCITY is a constant up top on line 29, which is negative 200.

  • And then gravity is equal to 7.

  • And so what we do is we set it to negative 200-- his delta y--

  • as soon as he jumps.

  • And then every frame down in update, we basically

  • increment his delta y by gravity.

  • And then we increment his y by delta y times delta time.

  • And so it'll have the effect of when he's in the air

  • and he's got a negative velocity, it'll actually

  • start becoming positive and positive until it is positive,

  • and then he falls back to the ground.

  • And then the hack that I was referring to earlier--

  • since we don't have collision detection implemented in this example yet--

  • is we're just basically checking to see whether he has

  • gone below what we set the map's floor.

  • And if he has, then set his position, first of all, to be above that tile

  • here on line 133.

  • And then set his delta y equal to 0.

  • And that will allow us then to hit space again,

  • because his delta y will be equal to 0.

  • AUDIENCE: So I didn't see [INAUDIBLE] on it.

  • Looks like there's always gravity, then.

  • COLTON OGDEN: There is always gravity, something

  • I realized shortly before lecture.

  • But all you would really have to do is, I think, if character dy--

  • Yeah.

  • You could easily take that out of there-- just an if statement around it.

  • AUDIENCE: It's just a waste of resources, right?

  • COLTON OGDEN: It is.

  • I mean, it's not expensive, because all you're doing

  • is incrementing a variable by a certain amount.

  • If anything, if you're introducing an if condition every frame,

  • which is probably the same if not actually more.

  • I think a branch is more CPU than just an assignment.

  • I'm not entirely sure about that.

  • AUDIENCE: Interesting.

  • COLTON OGDEN: Yeah.

  • In this case, it doesn't really have any side effects.

  • But it's a good thing to notice.

  • But now notice that we can just sort of walk along the floor here,

  • because there's no collision detection.

  • We'll talk about how we implement a collision detection soon.

  • So one thing that we'll start talking on-- and we'll take a break fairly

  • soon--

  • is procedural level generation.

  • So I am a big fan of procedural level generation,

  • and platformer levels are actually fairly easy--

  • at least in a simple sense--

  • to procedurally generate.

  • And so like with match three, all we basically

  • did was just loop through our grid and just

  • say, oh, get a random color and a random variety.

  • And then with the assignment, it was a little bit more complicated,

  • where you actually had to check to see whether you were on level one.

  • And then if you weren't, then your variety

  • should be maybe a certain amount depending on how far along you've

  • progressed in the game.

  • With a platformer level, we have to think

  • about how we can take that grid of tile IDs and think about it mathematically.

  • How can we get the results of a level, but make it different every time--

  • introduce some variation, right?

  • And so the solution that I found that makes the most sense

  • is going column by column.

  • So here, we just have a bunch of-- this is just

  • a very simple perfect screenshot to illustrate a very simple way

  • of generating the level.

  • But recall, if we just think about these tiles here-- these empty spaces--

  • being a 0 and these being a 1, it's sort of almost like binary in this case.

  • We could just fill the entire thing with 0 first, just assume empty space.

  • And then we could just column by column go down and just have a chance

  • every column.

  • OK, do I want to generate a ground here?

  • If I do, start at the ground level and then just

  • generate earth tiles all the way down.

  • And then go to the next x position, do the same thing, do the same thing.

  • And then maybe every column of the world that you're generating,

  • you also have a chance to generate a pillar like this.

  • So if generate pillar is true, then I want to spawn--

  • instead of starting the ground here, I want to start it here.

  • And then maybe you have a flag that says,

  • OK, not only do I want to generate pillars,

  • I also want to generate chasms--

  • just empty space, obstacles for the player.

  • Because if he falls down-- it goes below the world space--

  • it should be game over.

  • So in that case, you just say if generate chasm--

  • make math.random 10 or whatever it is--

  • then just go to the next x.

  • Don't even do anything.

  • And that will have the result of generating a chasm.

  • And so little piece by piece--

  • doing small things like that has the net effect

  • of generating a lot of visually interesting, dynamic,

  • and random levels.

  • You never know what to expect.

  • And this is a very basic example.

  • You could go infinitely far with it.

  • However many ideas you have in terms of how

  • to create obstacles and interesting levels and scenery for the player--

  • you could absolutely implement that.

  • AUDIENCE: How do you handle if there's a platform to jump on?

  • You have to have that consistency.

  • COLTON OGDEN: Yeah.

  • So if it's a platform, it depends on how you want to implement platforms.

  • And actually, I did a seminar on Super Mario Bros.,

  • and we did platforms as tiles.

  • In this case, we'll have blocks that are actually

  • what we've denoted as game objects-- which

  • are a little bit different than tiles.

  • Because they can have arbitrary sizes, and they don't necessarily

  • have to be affixed to the world grid.

  • But if you were to treat a platform that was, let's say,

  • two tiles wide as tiles, all you would do

  • is just basically have a flag up here that's like, generate platform

  • equals true or whatever--

  • AUDIENCE: And then turn it off after--

  • COLTON OGDEN: Turn it off after however many iterations.

  • You also need the size of it.

  • You'll need a flag that's like platform width equals however many,

  • and so you'll just keep a counter.

  • It's like current platform tile equals 1, 2, 3.

  • And if it's equal to width, then you don't generate it any more.

  • And that has the effect of potentially colliding with pillars

  • if you don't account for that.

  • So you can also in your logic say, if I'm generating a platform right now,

  • don't generate a pillar.

  • But you could generate a chasm, because the chasm

  • doesn't interfere with your platform.

  • If you don't have platforms as tiles-- if they're different objects--

  • then you don't have to do it during the actual world generation phase.

  • You can just test.

  • You can just create a game object that's a platform.

  • Depending on how complicated your algorithm is,

  • maybe make sure that it's not next to a pillar when you generate it.

  • And you could just do that by getting the tile here and then looking

  • at the next four tiles--

  • something like that.

  • We don't do platforms in this example, but it's something

  • that you could pretty easily do with tiles.

  • And slightly more difficult but also still fairly easy to

  • do with game objects, which is included in the distro

  • and which we'll touch on in a little bit.

  • Let's see, we're at level--

  • oh, another couple of things that I wanted to show before we

  • actually start getting into the code for how to generate levels.

  • This is the sprite sheet for this whole project, which is a really cool sprite

  • sheet that I found online.

  • It's in the spirit of platformers like Mario,

  • and it's got a nice little mockup here on the right.

  • So I encourage you to take a look at that

  • and just maybe get some inspiration and see all the different cool stuff.

  • Tinker around with it if you want to.

  • But as you can see here, there's a couple of pretty prominent things.

  • We have a ton of tiles.

  • These are all tiles here--

  • different tiles and variations.

  • And then we have a ton of these toppers here.

  • And so what really helps this whole demonstration

  • of generating these levels is the fact that we have

  • so much visual content to work with.

  • And so here, again, are the tiles.

  • Here are the toppers.

  • And then when you take the two together and then you also

  • have these random backgrounds--

  • these are toppers here, the top of the tiles here.

  • It's incredibly easy to just have a sheer abundance of visual variety

  • and interesting things in your game levels without even--

  • and the algorithms here are very simple.

  • All we're doing is just checking to generate pillars and columns.

  • I know.

  • I thought it was really cool and helps illustrate the importance and power

  • of this whole procedural approach to creating the levels for this.

  • And there's actually not that many games that take advantage, I think,

  • of procedural level generation in the platformer genre.

  • Plenty of games like Minecraft and Terraria--

  • Terraria is a great platformer that is an example of that.

  • But I don't think I've seen a really good Super Mario Bros.

  • game that does something like that.

  • Let's see.

  • What time is it?

  • 6:23.

  • Let's take a five minute.

  • And then as soon as we get back from that,

  • we'll start going into how we actually can implement the procedural level

  • generation in more detail.

  • All right, welcome back.

  • This is lecture four.

  • And before we took a break, we were talking

  • about procedural level generation in the context of platformer levels.

  • So recall, here are just a few examples that I took pretty quickly of my code.

  • And you can see they have different backgrounds, different tiles.

  • Sometimes we have chasms, sometimes we have pillars.

  • We'll be talking about a few ways to do the tile version of that,

  • because there's two levels here.

  • In the distro, we'll see there are also things like bushes, for example.

  • We can see in the top middle there the purple--

  • well, I guess those little purple cacti.

  • And the one right below that, there is a pillar with a yellow fern on it.

  • Those are separate objects from the tiles--

  • game objects.

  • But the actual tiles themselves we'll dig in here a little bit as to how

  • to get those generated.

  • So the first thing we want to look at, level0, is just some flat levels-- so

  • just basically what we've already done.

  • So I'm going to go ahead and go into level0.

  • And then if we see here, we have a simple flat level,

  • just like we did before.

  • Now the tiles are different.

  • And if I press R, they're randomly generating every time.

  • So you can get a sense of just how visually diverse this generation looks.

  • Oh, I think that might have been a bug earlier.

  • I'm not sure.

  • Haven't seen that yet.

  • But we can see here, I'm pressing R.

  • All I'm doing is taking the array of tiles that we have,

  • and I'm assigning it a tile set and a topper set

  • in the case of the scope of this generation.

  • So recall that the topper is just the top layer sprite,

  • and the tile set is the tiles underneath.

  • Anybody want to just suggest how I'm rendering the topper versus the tiles

  • and what's going on there?

  • AUDIENCE: You're just pulling it from part of the sheet, right?

  • COLTON OGDEN: Yes.

  • Yeah, in a nutshell, I'm just pulling the toppers

  • from a different part of the sheet.

  • Any idea how I'm storing information-- what's being stored here

  • to get it to render like this?

  • AUDIENCE: Maybe you just need to store the position of the topper

  • and know that everything else is below that.

  • COLTON OGDEN: Yes.

  • So you could store the position of the topper

  • and know that everything else is below that.

  • That would work for a flat level.

  • I don't think that would be reliable for a level that has pillars on it,

  • because the pillars are a higher elevation than the ground.

  • And then there's also chasms and stuff like that.

  • So what's going on here actually is we're

  • storing a flag in the tile that says whether or not it has a topper on it.

  • And if it has a topper, then we render not only the tile,

  • but as soon as we render the tile, we also render the topper.

  • And I won't go too deep into the code here.

  • But what we're doing to get all these different tile sets and topper sets

  • too is we have to take all of these tile sets-- these collections of tiles--

  • and divide them up, right?

  • We have to know that if we want to render the entire level in tile

  • set one, then we should basically take this into its own sheet--

  • its own table-- this into its own table, this into its own table, going

  • left to right actually.

  • And we have basically four way nested loop.

  • So we go every set on the x by every set on the y.

  • And then within each of those, we want to look for every tile along the x

  • and every tile along the y therein and split up

  • the tile sets so that we can index into the individual quads.

  • So in the actual code, I won't go too deeply into it here.

  • But I'll show you where it is if you're curious to look into how we do that.

  • It's in Mario in Source, util.lua, which is recall where we before

  • stored our generateQuads function, which does a simple split on a tile

  • sheet along its x and y based on whatever width and height you pass in.

  • We have in here also a generateTileSets function, which takes in the quads

  • from a generateQuads table.

  • So we first generate quads on all of this or all of this.

  • So we have every single frame of this divided by 16, which is--

  • I don't know how many that is.

  • 6 by 5 times 10 by 5, 10 by 4--

  • that many quads, so thousands of quads, I think, if not hundreds.

  • This I'm pretty sure is thousands of quads.

  • And then we take that and then divide it using

  • the number of sets along the x-axis, sets on the y-axis,

  • and then the size of each tile set along the x and size along the y.

  • We basically divide it using a four way nested loop here.

  • We basically just divide it up.

  • And then instead of doing a generateQuads

  • along the entirety of the picture, we just

  • basically do a 2D slice of that quad table we get back

  • from the first generateQuads call.

  • So I encourage you to look in here and experiment with that.

  • You don't need to necessarily know how it works for the assignment.

  • But that's how we can basically take a giant sheet like this

  • and easily integrate it into our code.

  • We can just swap in and out whatever active tile

  • sheet we want to work with, assuming that everything

  • is cleanly laid out like this, which is on the part of you or your artist.

  • You want to make sure that everything is conducive to programmatic organization.

  • Had things been scattered around in a very awkward way--

  • maybe things were zig zagged or there were weird spaces or something like

  • that--

  • we wouldn't be able to do something as clean as what we did here

  • in util.lua with just 63 minus 20 lines of code

  • by getting each individual tile set.

  • So that's an important consideration if you're

  • looking at creating assets for your project

  • and you want to do some programmatic hot swapping of your tile sets.

  • Let's make sure we're in the right--

  • we're not in the right example here, so we're

  • going to go into level0 into main.

  • And we have constants now for all of our tile sets and what the height is

  • and how many they are wide by tall.

  • We do it here.

  • We get our regular quads from our tiles and toppers,

  • so these are just literally every single tile within that big tile

  • sheet put in one table.

  • And then we just divided up into tile sets

  • and topper sets here with generateTileSets function.

  • And then we get a random tile set and a random topper set here--

  • math.random, number of tile sets, number of topper sets.

  • And then at the very bottom also, we have a generateLevel function--

  • 223-- which is going to be built upon in the next two examples.

  • Level0 is just a flat level, so it's actually

  • exactly what we saw before, which was just if y is less than 7,

  • ID should be equal to sky or ground.

  • And then this part is actually what I was

  • alluding to before with the topper, because recall

  • we need to store a flag in a tile to render a topper or not.

  • And it should be whatever the top tile is in the level on the ground.

  • In this very simple flat thing, we can always

  • assume it'll be the same y level.

  • In this case, if it's equal to 7, then topper

  • should be true, otherwise, false.

  • So every tile along y 7 is going to have topper equals true.

  • And this comes into play up here.

  • If we do love.graphics.draw(tilesheet), we have not only just tile.ID as we did

  • before, but we have tile sets indexed into tileset now.

  • So remember, tileset got a random value between 1

  • and however many tile sets we had that we spliced out of our massive tile

  • sheet.

  • Now, we just index into that, and then we index into tile.ID.

  • And tile.ID will then be whatever our ID is but relative to that sheet, not

  • the whole entire sprite at once.

  • And the same thing for topper.

  • We have a topper set, we index into topper

  • sets here at the topper set that we got, and then that's

  • where we'll have the collection of tiles that form that particular set.

  • And so the two are completely separate.

  • They can be one random color tile with one random topper, but it's consistent.

  • It's global.

  • We have one topper set and one tile set that are active at any one time.

  • And if we press R, which I did up here, then we

  • just reset them to random on line 139 and 140.

  • Tile set gets a new random number, topper set gets a new random number.

  • It has the effect of--

  • we can just walk around and then generate random sets.

  • So pretty simple.

  • And recall again, topper is--

  • because the tile that we're standing on is y seven, topper equals true.

  • So in that case, that particular top layer is always going to have a topper.

  • And it gives us a nice little bit of visual variety,

  • because it actually makes quite a bit of a difference having

  • a topper versus no topper.

  • And you can also just not have a topper and consider

  • that a permutation of the toppers times tiles, like procedural algorithm.

  • That's flat levels.

  • Does anybody have any questions as to how this works

  • or anything that we're doing here?

  • OK.

  • So things are a little flat, a little boring.

  • The next step will be actually introducing one of the things

  • that we can see here in our little collection of sample levels,

  • like this pillar right here in the very middle.

  • Does anybody recall how we go about spawning a pillar as opposed

  • to just flat land?

  • AUDIENCE: For that column, just put some more dirt down or more tiles down.

  • COLTON OGDEN: Yep.

  • So for that column, just put more tiles down instead of just the ground level.

  • That's exactly what we're going to do.

  • So I'm going to go ahead and open up level1 and main,

  • and I'll run the example here as well just so you can see it looks like.

  • So here we have quite a few.

  • And notice we haven't implement collision,

  • so we're still walking through them.

  • But they're just random.

  • Their random amount is up to taste, really.

  • Right here it's pretty common, so it might be worth

  • lowering the amount a little bit.

  • If you wanted to, you could also maybe have a flag that says, spawn pillar,

  • and maybe you want a pillar width.

  • You could have anywhere between one and three tiles.

  • And if its width is greater than 1, then just loop over a few times

  • and just draw that same height a few times as opposed to just one time,

  • and then set the flag back to false.

  • A lot of things you can do with it.

  • And also, they're a little tall here.

  • For the main distro, I ended up making them a little shorter.

  • But we'll see how we do this in the code here.

  • It's going to mostly be down in our generateLevel function.

  • So what we're doing here-- go ahead and hide that--

  • is we have basically this code here--

  • line 227 to 236.

  • So all we're doing here is just filling our entire thing with just sky.

  • We're just setting the entire thing to empty.

  • And now we have a fully populated 2D array.

  • All we need to do in order to change a tile-- we

  • don't have to worry about insertions or adding too many tiles to our array.

  • All we can do now is just directly change whatever tile exists there.

  • So all we need to do is starting on line 239,

  • we're going to start doing the column by column iteration over our entire level

  • and deciding whether we should generate pillars or not.

  • And we're always going to generate ground.

  • So here's the flag spawnPillar.

  • And if it's equal to 1, this is going to basically be assigned to spawnPillar.

  • So math.random(5)==1.

  • We have a 1 in 5 chance of spawning a pillar.

  • If we just want a pillar, then pillar gets equal to 4 from 4 to 6--

  • so y gets 4 to 6 effectively--

  • tiles at pillar x, ID ground.

  • And then here's where we set the topper, recall,

  • because now pillars can be the top most tile on the surface.

  • But they're above the ground level.

  • So we just basically say, when we're generating a pillar,

  • if pillar is equal to 4-- which is the very first tile that we start at--

  • then set topper equal to true here.

  • Otherwise, set it to false.

  • So that's how we can get pillars to also have toppers and then in this case,

  • we're not generating any chasms yet.

  • So all we're going to do--

  • once we've generated a pillar on that particular column,

  • we'll just say ground gets 7 until the map height--

  • so towards the very bottom of the screen.

  • And then we'll just set it to ground.

  • And then topper-- in this case, we're going to make sure

  • that we're not spawning a pillar.

  • Because if we don't check this, then it'll

  • also spawn a topper where the pillar meets the ground,

  • and it'll look a little bit silly.

  • And then we also want to check that ground is equal to 7.

  • And so all together, that has the effect of this behavior.

  • And so if we didn't check for that spawnPillar,

  • we'd have a topper right below our feet here too,

  • which looks graphically strange.

  • And also, you can see--

  • emergently, we're getting double width pillars.

  • And that's just kind of a natural byproduct

  • of a lot of these randomizations.

  • A lot of these procedural algorithms--

  • they'll generate outcomes that you might not necessarily

  • have anticipated, which is kind of a cool thing.

  • You didn't necessarily program it to have pillars that were two tiles wide,

  • but just the nature of randomization-- that's just what you get.

  • And that's another exciting thing about procedural level generation

  • is that it can surprise even the person that wrote the algorithm.

  • It's really cool, and it saves you work having to create levels.

  • So that was pillared levels.

  • Chasm levels-- who can tell me how we can do chasm levels?

  • AUDIENCE: You just skip a column.

  • COLTON OGDEN: Yep.

  • you skip a column.

  • So at the very beginning, all we can just basically say

  • is, do I want to generate a chasm here?

  • If I do, just skip.

  • Go to the next iteration of the loop.

  • And so we'll take a look at that.

  • As simple as it is, because Lua doesn't have the notion of continue--

  • this will be a refresher, because I believe

  • this was in one of the assignments--

  • it has a goto statement.

  • So basically, same code as before, starting column by column.

  • x equals one until map width.

  • We have a 1 in 7 chance--

  • just arbitrary.

  • And this should ideally--

  • if you're engineering an entire large game or application,

  • this would be called SPAWN_CHASM_CHANCE probably,

  • and just set that to seven somewhere.

  • But we're just setting it to 7 here--

  • just a static magic number, but magic numbers are generally bad.

  • Goto continue-- and so continue is here at the very bottom of the loop

  • here, which is this for x = 1, mapWidth.

  • So it will have the effect of skipping straight to x equals 2 if this at 1,

  • for example.

  • A lot of languages just simply have continue.

  • Lua does not have continue, so this is a community established tradition

  • for implementing continue-like behavior in Lua.

  • You create a label via double colon with a name and then a double colon,

  • and then you just goto it.

  • And so that's as simple as it is for generating chasms.

  • And so if we go to level2 and run that, we get chasms.

  • And so now we've got a little bit of interesting visual variety.

  • It's not spawning a ton of chasms in this example.

  • It spawned one so far.

  • There's another one.

  • And then sometimes just emergently, you can get two.

  • See, there we go.

  • We get some interesting obstacles as a result.

  • It almost looks as if someone intentionally did that--

  • almost.

  • I would probably, like I said, shrink the pillar size a little bit.

  • It's a little tall.

  • That's that.

  • That's basic procedural-- in the context of platformers,

  • that's the mental model for how we can start

  • thinking about generating obstacles.

  • And there's a lot of different directions you could go.

  • Let's say maybe you wanted to generate pyramids.

  • I mean, it's a common thing in Mario.

  • There will be steps, [INAUDIBLE] set for it.

  • The same implementation would basically happen here.

  • It would be a little bit different, because you're

  • doing it on a column by column basis.

  • But you'd effectively just maintain a reference

  • to something like step height, and then you

  • would say generate stairs is true here.

  • And then you would just set step height to 1.

  • So then you add a tile here.

  • You would go from ground level up until step height, generate a tile,

  • go the next one, and then increment step height to 2.

  • And then do from ground until step height--

  • tiles go up.

  • So 1 and then 2 and 3 until you've gone to stairs width, in which case

  • you stop generating stairs.

  • That's this principle behind how you could

  • do something a little more complicated.

  • Or pyramids-- same exact thing, pyramid width.

  • And then you just go until pyramid width equals--

  • or we're at pyramid width divided by 2, make it go up.

  • And if we're higher than that, make it go down.

  • And then you have the effect of the pyramid approach.

  • Yeah.

  • AUDIENCE: Where are you putting the column

  • generation if it's a [INAUDIBLE].

  • It's not in the play state.

  • COLTON OGDEN: In this case, it's all in main.lua.

  • But in the distro, it's going to be in levelmaker.lua.

  • So we've broken out all of this functionality

  • into just how we did Breakout.

  • We had the same sort of thing-- level maker--

  • and it just has levelMaker.generate.

  • And then you give it a width and a height,

  • and it will generate an entire level for you.

  • AUDIENCE: An entire level, but it has to continuously--

  • oh, you generate it all at once?

  • It doesn't generate as you walk?

  • COLTON OGDEN: The question is, does it generate continuously or all at once?

  • It just generates all at once.

  • So you could implement a--

  • if you wanted to do an infinite runner, the way you would do that is you

  • would break up your level into chunks.

  • And with infinite runners, usually you can only move in one direction.

  • So as you go right, your levels that you've generated before-- they get

  • discarded, so you avoid memory overconsumption.

  • What you would do is you would just generate a chunk--

  • maybe a 100 by 20 level.

  • And then you would go through that, through that.

  • And then when you get to level end minus maybe like five tiles or 10 tiles,

  • you would generate another one, append it, put it to the right,

  • and then you would just go from the left to the right.

  • And you probably would need some sort of semi-fancy code

  • to splice them together once you've generated them.

  • Alternatively, you could just always pad your--

  • no, you probably wouldn't want to pad.

  • I would probably just splice them end to end and then get rid of x equals 1 100

  • or however many on the left once it's gone past the left edge of the screen.

  • In this case, to summarize, it's all static.

  • But you could very easily--

  • not easily, but you could very well make it an infinite runner.

  • Yeah.

  • AUDIENCE: So we're rendering the entire level, but we just can't see it all?

  • COLTON OGDEN: The question is, are we rendering the entire level,

  • but we just can't see it all?

  • The answer is yes.

  • Currently, in this implementation, we're just rendering the entire level--

  • so tile by tile is getting drawn to the screen.

  • For small examples like this, it's not a concern.

  • But for a large level-- like if we did a Terraria level, for example.

  • Terraria's thousands and thousands of tiles wide by probably

  • 1,000 or more tiles tall--

  • you want to render only a chunk, only what you can visibly see.

  • And for that, you could use your camera offset and then just render

  • from one tile to the left and above that to one tile below the bottom edge

  • of the camera and to the right of it.

  • Just render that subset of tiles.

  • So you just need a for loop to iterate over a small section.

  • AUDIENCE: So you can kind of make an array

  • of what the map's going to look like and then

  • just render only slices of the array that you can see.

  • Is that right?

  • If you put a multi-dimensional array and then you just go through it

  • and render as you go-- is that the thought?

  • COLTON OGDEN: Question was, you just have

  • a multi-dimensional array of tiles for your level,

  • and then you just render it as you go.

  • The answer is yes.

  • You would have your overall tiles--

  • your big 2D array of 100 by 20 or however many thousands of tiles.

  • And then based on wherever your camera is rendering,

  • it's just a for loop within that just of a nested amount.

  • So maybe your player is at x 30 plus 6 tiles.

  • So you would just render from 30 tiles to maybe 45 tiles

  • on x and maybe 10 to 20 on the y--

  • just that chunk.

  • And it's just relative to where your camera is.

  • You're always rendering just a small little--

  • basically, it is effectively a camera at that point.

  • It's rendering a chunk of the tiles to the screen.

  • AUDIENCE: But in this code, it's not.

  • COLTON OGDEN: In this code, no.

  • The levels here are--

  • it's sufficiently complicated to introduce.

  • I mean, it's not too complicated to introduce.

  • It's pretty easy.

  • But the consumption-- the processing here--

  • is very light, because the levels are fairly small.

  • And even if we did have really large levels,

  • it's sufficiently small to not have to worry about it.

  • But if we did get to a point where your levels were 1,000 tiles or more,

  • and then maybe those tiles have additional,

  • you just want to squeeze all the performance possible out

  • of your application.

  • You could look into just rendering a subset.

  • It's fairly simple to introduce but just not something

  • that we included in this assignment.

  • Any other questions as to how this sort of thing works?

  • OK.

  • So far, we've talked about procedural level generation.

  • We've talked about animation and rendering and all that stuff.

  • We haven't really talked about how to do tile collision.

  • And we won't go into a terrible amount of detail,

  • because the code is a little lengthy.

  • It'll be part of your assignment to read over it and understand it,

  • but it's in the TileMap class that we have.

  • Basically, the whole gist is that because we're

  • on a 2D tile array that's fixed, it'll always be at 0, 0, at least

  • in the model that we've currently implemented.

  • We can just convert coordinates to tiles and then

  • just check to see whether or not the tiles at whatever that is

  • are solid or not.

  • Let's say we wanted to look at the top of our character in this case.

  • So if we have our character here.

  • For the sake of illustration, I put him between two tiles above him

  • just to show why we need to do this the way that we are doing it.

  • But you take the point here--

  • his very top left, so player.x and then player.y,

  • which is effectively their version of 0, 0.

  • And then player.x plus player.width minus--

  • we do a minus one for a lot of collisions

  • so that he can walk between blocks and stuff like that.

  • Because if you don't basically give him slightly less than the amount--

  • because he's 16 pixels wide, and the tiles are 16 pixels wide--

  • if he's between two blocks and he wanted to fall down,

  • he just won't fall down, because it's still detecting a collision.

  • Because if he's on the hole here--

  • let's say this is the hole, and these are the tiles here.

  • The x plus the width--

  • it'll trigger a collision on this tile and this tile still.

  • So basically, you need to minimize his collision box by one pixel

  • to fit through 16 pixel gaps essentially is what it boils down to.

  • But the gist behind collision--

  • in this case, this would only apply when he's

  • jumping, because this is the only time at which he can really

  • collide with tiles that are above him.

  • You would test for whatever block falls on this pixel

  • and whatever block falls on this pixel.

  • And if either of them are solid, you trigger collision.

  • And if not, then there's no collision at all.

  • So if he's right here, for example--

  • right directly beneath a tile-- it's only going to check one tile.

  • This point and this point are both going to fall on this tile.

  • But the reason that we want to check for both points here and here

  • is in the event that he is beneath two separate tiles,

  • because now this point's going to check this tile,

  • and this one's going to check this tile.

  • We can't just check this tile, because if we only check this tile

  • and there was no tile here but there was a tile here,

  • him jumping would still not trigger a collision.

  • It would think that it was only looking here and not here.

  • So for every collision on every side we do of him effectively,

  • we need to check both corners of that edge effectively.

  • So when he's jumping, we turn this point--

  • this x, y-- into a tile by just dividing it by tile size.

  • So we can say, player.x divided by tile size plus one.

  • That's going to equal whatever tile this is on the x.

  • And then same thing for the y--

  • we just divide the y by tile size, and then we add 1 to it.

  • And that will allow us to get the exact tile.

  • If we use those x, y that we get from that operation,

  • we get the exact tile at that y, x index in our tile's 2D array effectively.

  • So we do that for jump.

  • We check both corners of the top of his head.

  • We do the same thing for the bottom, only at that time,

  • we're checking x, and then y plus height,

  • and then x plus width, y plus height.

  • And then if we're doing the left edge, what are we checking?

  • AUDIENCE: The bottom left and top left?

  • COLTON OGDEN: We are.

  • So that will be x0, y, and then x0, y plus height.

  • And then if it's the right edge, same thing.

  • We check x plus width y, and then we check x plus width y plus height.

  • And so that's the gist behind collision detection in the distro here.

  • And you can see it in Mario if we go to TileMap.

  • Point to tile-- this is effectively where it happens.

  • On line 32, we're basically returning--

  • this bit of code here--

  • 28 to 30-- is a check.

  • Because we can jump over the map edge, we

  • won't be able to check at tile y divided by TILE_SIZE

  • plus 1, x divided by TILE_SIZE plus one, because those will be nil.

  • Those won't exist, because he'll literally

  • be outside the map boundaries.

  • Same thing if he goes below it or he goes beyond the left or right edge.

  • So that's all this code is here.

  • It just makes sure that if we do go beyond the map boundaries,

  • we return nil.

  • So that way, we can check nil rather than getting a tile index error.

  • And then on line 32 is the operation that I just mentioned,

  • which was we take the y--

  • so this x and y that we pass in are going to be the player's actual x, y.

  • When we pass those in, we're just going to get the tile at self.tiles,

  • and then effectively y divided by TILE_SIZE taken down to an integer,

  • and then add 1.

  • Because recall, tables are 1 indexed, but the coordinates are 0 indexed.

  • So this will result in a 0 indexed outcome, so we want to add 1 to it.

  • Same thing for here-- math.floor(x) divided by TILE_SIZE plus 1.

  • So effectively, points to tiles.

  • And then we'll just get a tile from that.

  • And the tile-- we can just check, hey, is that tile solid or not?

  • If it is, trigger collision.

  • So that's the gist behind being able to do it in a platformer

  • where everything is fixed.

  • That's sort of like a shortcut we can take.

  • Because now, what's the nice thing about this?

  • What jumps out as being a super nice thing about this algorithm,

  • imagining that we have, let's say, 10,000 tiles in our game world.

  • So if you look and see, all we're doing is

  • we're just doing a simple mathematical operation

  • on what his x and y is, right?

  • What's the alternative to this?

  • If we were doing this via AABB, for example,

  • we'd have to iterate over every single tile, right?

  • AUDIENCE: Can you summarize?

  • To avoid iterating over everything on the screen,

  • you just check the column that he's in and the column tile?

  • COLTON OGDEN: Yep.

  • So the gist is he's got an x and a y.

  • The x and the y are going to be in world coordinates, so his x could be 67

  • and his y could be 38 or something like that.

  • They don't map evenly to tiles.

  • But if we divide those by whatever the tile size is in our world--

  • 16-- that's going to be the exact tile.

  • We also have to add 1 to it, because the tables in lua are 1 indexed.

  • But we can index our self.tiles at the x, y

  • that we get from that-- the dividing by 16.

  • And that will be the exact tile that he's colliding with.

  • We don't have to basically have a collection of tiles

  • that we iterate over and check whether they

  • collide with the player using AABB collision detection

  • like we've done before.

  • Because recall, in Breakout, we had the bricks, right?

  • They all had their own x, y, but they weren't on a grid.

  • They weren't fixed.

  • So we had to actually take them and do an AABB.

  • We had to iterate over them and perform AABB on them,

  • because there's no deterministic way to just index at them really quickly.

  • It's the same thing with arrays versus linked lists.

  • Because arrays-- you can calculate how far some value is given an index.

  • You have instant access to it.

  • It's an order of one operation as opposed to a linked list.

  • If you want to try and get to a particular value,

  • you have to iterate through the entire thing until you find it.

  • AUDIENCE: Can you just look for the column that you might be landing on?

  • COLTON OGDEN: You're getting the exact tile at whatever your x divided by 16--

  • or whatever your tile size is--

  • and your y divided by 16 is.

  • And you're doing it, recall, for two different points

  • depending on what you're looking for.

  • If you're looking for the tiles that are above your character,

  • you're going to be doing it for this point.

  • So whatever this value is-- his base x, y--

  • whatever that is divided by 16 and then whatever that is divided by 16.

  • And then that'll get you whatever tiles are directly above him.

  • It will intersect with whatever tile intersects with this point

  • and whatever tile intersects with this point.

  • Same thing with here and here if we're looking on the left,

  • here, here if we're looking on the bottom,

  • and here and here if we're looking on the right side.

  • And we check for collision after he's already

  • moved so that these points will be intersected with potential blocks.

  • And that's how we can check whether it's a collision or not.

  • We do this when he moves and is in some sort of movement state.

  • AUDIENCE: So you're still doing collision detection

  • with his actual coordinates, but you're just

  • narrowing what you're character width--

  • COLTON OGDEN: Yep.

  • we're turning it from iterating over every single tile

  • to an instant operation, because we can just mathematically get

  • the exact tiles that he's at without having

  • to worry about where he is in the map.

  • It's just instant access.

  • And this only works because we know the tiles are always

  • fixed in the exact same locations.

  • They're always starting at 0, 0.

  • They're always going to be TILE_SIZE.

  • Things get a little more complicated when

  • we introduce game objects, which have their own independent x, y.

  • And for those, you do have to iterate over.

  • You have basically a collection of game objects or a collection of entities.

  • Let's say we have snails in the game world.

  • The snails aren't going to be at some fixed location every time.

  • They can move continuously.

  • So for those, we have to actually keep them all in a container

  • and then loop through them and say, has my player collided with any of these?

  • If he has, then trigger a collision with that snail--

  • kill it or kill the player if he's in a walking state or a jump state.

  • And if he's in a falling state, then they

  • should die, because he's colliding with them from the top.

  • And you narrow down what collision you check for, as you can see at the bottom

  • here.

  • Tile collision-- when you're looking above your character,

  • you're only testing that when you're in the jumping state,

  • because it's the only time you need to.

  • So that's the only point at which you'll collide with tiles that are above you.

  • When you're in the falling state is when you'll check for tiles below you.

  • And then you can interact with tiles to your side

  • when you're in either the jumping, falling, or moving state,

  • so you should check for left and right tiles in all three of those states.

  • AUDIENCE: Shouldn't you always test for beneath you in case you get a chasm?

  • COLTON OGDEN: In case what?

  • AUDIENCE: In case you get a chasm.

  • COLTON OGDEN: In case you get chasm, yes.

  • You're correct.

  • This should actually be tested only when in the player

  • falling state and player walking state, yes.

  • So the question was, shouldn't you be testing for tiles beneath you

  • when you're walking?

  • And yes-- not just falling, but walking as well.

  • This one only jumping, this one falling and walking,

  • and this one for jumping, falling, and moving.

  • Does that make sense--

  • how we can take the x, y and sort of turn that into a tile

  • by just dividing it by 16?

  • And do note the plus 1 as well, because our tiles in our self.tiles

  • are 1 indexed.

  • And so when we divide x, y by tile size, we're

  • going to get a 0 indexed coordinate.

  • If our x is at 14, we're within the first tile.

  • But if we divide that by 16, we're going to get zero.

  • So we need to add 1 to that so that we get the first tile in the array

  • still, which will be whatever that tile is.

  • So that's how the collision works.

  • It's all implemented in TileMap here.

  • And basically every state that the player is in, which is in StatesEntity,

  • and then player falling, idle, jump, and walking--

  • these are all states that perform this check.

  • They basically do all the logic that's here

  • at the bottom, which is testing in the player

  • jumping state, falling state, and moving state for left or right collision.

  • And then in the falling state, we check for collision below us.

  • And then in jumping state, we check for collision above us.

  • That's all done within the states themselves.

  • But the actual transformation from pixels to tiles--

  • that's just a function that we call from TileMap.

  • It's just a utility function.

  • AUDIENCE: What's the function called again?

  • COLTON OGDEN: It's called pointToTile.

  • So if you're in TileMap on line 27--

  • pointToTile(x, y).

  • And the first little bit here is just the bit

  • that lets you basically go outside the map bounds

  • without getting a tile index error.

  • So if it's just outside the tile limits, less than 0, or greater than width,

  • just return nil.

  • And so you can do a check on nil to check

  • to see whether TileMap pointToTile is equal to nil

  • or not when you do the collision.

  • And if it is, then just don't do anything probably.

  • But assuming that you're within the tile boundaries, on line 32

  • is where you do that transformation-- the math.floor, recall,

  • because we want to get integer values for these.

  • We don't want to get fractional numbers, because you

  • can't index these tiles as fractional numbers, although I'm not sure.

  • I think you might be able to in Lua generally-- index a tile

  • by a fractional number.

  • But in this case, we just want integers.

  • So we call math.floor on y divided by TILE_SIZE plus 1,

  • y divided by TILE_SIZE then add 1 to that,

  • and then we do the same thing for the x.

  • So that's the operation.

  • And then wherever we want to check for whatever tiles we want to collide for,

  • we just call pointToTile on those x and y coordinates.

  • That's the backbone behind all the tile-based collision in the game

  • effectively.

  • Any questions as to how this works?

  • Yes.

  • AUDIENCE: So you're only detecting the collision of corners

  • and not the edge itself?

  • COLTON OGDEN: Correct, because you don't really need to check for the edge

  • if you're taking into consideration the top and bottom corner,

  • unless your entity is sufficiently tall that they need

  • to check for more than three tiles.

  • In this case, our entity is not more than two tiles tall,

  • so we only need to check for his top left, bottom left.

  • If we're doing a left collision, top right, bottom right.

  • If we're doing a right collision, his top left, top right for top and bottom

  • left, bottom right for bottom.

  • If you had an entity that was eight tiles tall,

  • you need to check every single tile along his right side, which

  • just means you need to iterate over his entire height divided by tile size.

  • And then just offset the y that you're checking for each of those tiles.

  • Does that makes sense?

  • OK, cool.

  • All right.

  • I alluded to this briefly by mentioning state.

  • I don't know if I alluded so much to the fact that we're using entities.

  • But in this distro, we're introduced to the concept of entities.

  • An entity can be almost anything you want it to be.

  • In this distro, we're considering entities

  • to basically be anything that's living or sentient moving around--

  • in this case, the player or snails.

  • Those are entities, and then they just are subsets of entity.

  • An entity is a very abstract thing.

  • You'll see it in a lot of game engines and a lot of discussions

  • about how to organize your game and how to engineer it.

  • Unity is probably the most prominent adopter

  • of what's called the entity component system, whereby

  • you have everything in your game.

  • Every single thing in your game is an entity,

  • and then every entity is comprised of components.

  • And these components ultimately drive your behavior.

  • It's sort of like if you're familiar with composition over inheritance.

  • If you've heard of this as a software engineering thing,

  • that's effectively the same paradigm.

  • Rather than inherit a bunch of different things to be your--

  • let's say you have a base monster class.

  • And then you have a goblin that's a subset of monster,

  • so it inherits from monster.

  • And then you have a goblin warlord who inherits from goblin,

  • and then you have an ancient goblin warlord that inherits from that.

  • Rather than have this nested tree of inheritance,

  • you adopt composition, which means you take a base container,

  • and then you fill it with different components that represent

  • what the behavior of your object is.

  • So if you have an entity--

  • let's say you give it a monster component.

  • And then maybe you also give it an ancient component,

  • so it's an ancient monster.

  • And maybe you give it a goblin component,

  • so then it's an ancient monster goblin.

  • And then you give it a warlord component,

  • so it's an ancient goblin monster warlord.

  • So it has all the pieces that make it what

  • it is without you having to create this crazy chain of inheritance.

  • That's effectively what the model of an entity component system

  • is versus standard inheritance-- using that to drive

  • the model of your problem.

  • In this case, we're not going into crazy entity components.

  • But I wanted to bring it up, because Unity,

  • which we'll be covering in a few weeks, is entirely component-based.

  • Everything you write in Unity is a component.

  • And entities, whether they're in an entity component system or not,

  • form the backbone of most large games.

  • Most games that have some complexity to them

  • model most of the pieces within them as entities that have behaviors

  • and do things.

  • And so in this case, entities are snails and our player.

  • And then separate from the tiles--

  • when we do collision for that--

  • we want to also check collision on every entity with the player.

  • So we make sure that the player's collided with the snail in this case,

  • because that's the only other entities that they can be.

  • But you can have an arbitrary number of enemies if you want to.

  • If you collide with an entity-- so just a for loop.

  • So for entity in pairs of entities, check collision.

  • If you're in the jump state, then die.

  • If you're in the fall state, kill it, et cetera.

  • When you're doing most of your entity to entity interaction stuff,

  • that's generally how you'll model it.

  • You'll just iterate over everything and then just collide everything.

  • Depending on what collides with what, you'll

  • just collide everything with everything else and process interactions that way.

  • That's effectively how we do it.

  • We have in the--

  • I believe it's in GameLevel.

  • This maintains a reference to a table full of entities,

  • a table full of objects.

  • Objects can be-- we'll talk about that in a second--

  • gems, and blocks, and bushes, and stuff like that, and then a tile map.

  • For every entity, we just update it.

  • And then for every object, we update it.

  • And then for every object in objects, we render it as well.

  • And then we render every entity.

  • This is just sort of basic how you would take a game world,

  • populate it, and then process and update it.

  • Just containers, tables that maintain a bunch of references to everything,

  • and then just update them.

  • The actual interaction takes place in the--

  • because they're dependent on what state we're in.

  • If you look at all the different states for the player in the states slash

  • entity folder, you'll see, for example, on line 62 of the player falling state,

  • we're iterating over every object in the level.objects.

  • And notice the player has a reference to its level

  • so that it can access everything within it.

  • And then within that level, all the objects are stored.

  • So all it needs to do is just say, if the object collides

  • the player and the object is solid, then set our dy to zero,

  • et cetera, et cetera.

  • All this code's actually pretty easy to read through,

  • so I would encourage you to take a look at it

  • and just understand how all the collision and stuff is

  • working between the player, the objects, the blocks, and things like that--

  • things like blocks are solid, things like bushes are not solid.

  • But that's the gist.

  • Have a collection of objects or entities.

  • And then depending on what state you're in, collide with some.

  • And then depending on the state, maybe that kills you,

  • maybe that kills the enemy, maybe nothing happens,

  • maybe you become invincible.

  • Maybe you collide with a power-up game object,

  • and that power-up triggers your self.player.invincible is true.

  • And then if self.player.invincible is true,

  • then maybe you render him with a rainbow animation.

  • And then in any of the functions where he would collide and die with an enemy,

  • he no longer dies, he just kills them.

  • So that's sort of the gist behind how you would interact with objects

  • and how to process it.

  • Game objects are different.

  • Like I said earlier, these are examples of some of the objects here we can see.

  • The gems on the bottom left there are all in the distro.

  • If you hit a block-- and if we have a few minutes,

  • I'll show you really quickly how that works--

  • if you hit a block, you'll have a chance to spawn a gem.

  • If you collect the gen-- which means if you collide with that game object--

  • increment your score 100.

  • These are all other objects that I didn't have any time to implement.

  • But just off the gate, just as a mental exercise,

  • how do you think we could implement a ladder?

  • Yeah.

  • AUDIENCE: You would just have a climb state.

  • And if the player is touching a ladder and presses a certain key,

  • they would enter the climb state, and that would cause them to go up.

  • COLTON OGDEN: Correct.

  • So what Tony said was if they go onto a ladder,

  • they should go into a climb state.

  • And depending on whether they're in a climb state

  • or not, if they press a button, they should go up or down.

  • And then you would check.

  • If they're at the top of the ladder, get off the climb state,

  • go into a walk state.

  • Or if they're at the bottom of the ladder, go into a walk state.

  • And that's just another game object that you just collide with,

  • and then it's a new state for you.

  • Yes.

  • AUDIENCE: You may actually want it in a fall state,

  • because that way you could have a ladder that doesn't actually go anywhere,

  • it just gives you height.

  • But you could use that to jump over wide gaps.

  • COLTON OGDEN: What Tony said was you could

  • have the ability to jump off a ladder.

  • Is that what you Said Yeah.

  • The ability to jump off a ladder so that you can then use it as an obstacle.

  • That's absolutely true.

  • Actually, in the mock up that we saw up here, it's super hard to see.

  • I'll see if I can maybe zoom in on it here.

  • Mario, Mario, graphics, and then it's called full sheet.

  • The whole entire sheet that I used for this lecture is called fullsheet.png.

  • I don't know what that is.

  • So if you zoom in really high here, we can see effectively

  • what you were alluding to--

  • right here, this little rope thing.

  • I'm guessing for the sake of this mock up that's

  • what they were trying to illustrate.

  • But you have a game object that lets you go into a climb state.

  • Whether it's a ladder or whether it's a rope,

  • just add a new state for the player.

  • If they're in that climb state, then we have this new animation

  • which we saw in the sheet earlier, which was their back or their front.

  • And then they just climb up it and just update it

  • if they're moving up or down the ladder.

  • And then just give them the ability to jump off.

  • And then when you get to the top or bottom, just get off.

  • And you could think a lot of the same thing with a lot of these obstacles,

  • like the spikes here.

  • If you're jumping and you hit it, you should probably die.

  • And so you would check for if the object.ID maybe is equal to spikes

  • or whether object.lethal equals true.

  • Same thing with this one.

  • And then some obstacles are completely cosmetic, like this mushroom here.

  • In the case of the distro, bushes and mushrooms and cacti

  • and all those sorts of things are just completely cosmetic,

  • so you can walk through them.

  • They don't trigger collision, but they're rendered as game objects.

  • They're not part of the tile grid.

  • They don't get processed in the same way as tiles.

  • They're not stored in the y, x.

  • So that's effectively how we can start thinking about objects

  • and how to give them behavior.

  • Part of the assignment is going to be adding a flag.

  • So this flag is in the sprite sheet.

  • So what you'll do--

  • and I'll touch on this at the end of the lecture here.

  • We're getting close to it.

  • These keys here actually at the bottom right--

  • so part of the assignment will be to--

  • it's actually right here.

  • So I'll go over it really quickly.

  • Ensure the player always starts above solid land.

  • So in this case, when James came up here and ran, you ran the first example.

  • The very first time that we spawned the game,

  • it generated a chasm right where the player spawned at x1.

  • And so he just falls to his death if that happens.

  • Just right off the gate, anybody have any ideas

  • as to what we could do to check to see if we're at solid land,

  • assuming that the player's default start is at x1?

  • AUDIENCE: At that tile, check if it's solid.

  • If not, then just move it there over x until it's true.

  • COLTON OGDEN: Yeah.

  • What we probably want to do is look all the way down the column,

  • because we start towards the top.

  • If we find that there's no tiles down there-- it's just pure chasm--

  • we probably want to shift the player.

  • And then random keys and locks--

  • let me open up LevelMaker so we can see what you'll be interacting with,

  • because most of what you'll be doing actually is in LevelMaker.

  • It does a lot of what we did before with just math.random,

  • and then it will insert into objects.

  • So objects is a table here.

  • It will insert a game object depending on some logic.

  • So in this case, if we're generating a pillar,

  • we have a chance to generate a bush on the pillar.

  • So if math.random(8) is 1, in this case, we're already generating a pillar.

  • So we have an additional chance that's on top of the chance

  • to generated a pillar--

  • so basically, I think it's a 1 in 64 chance on that particular iteration

  • to generate a pillar with a bush.

  • You just add a new game object to objects.

  • In this case, this is the constructor for a game object.

  • You give it an x, y, width, height, and then a frame.

  • And then the frame is relative to whatever quad table matches the texture

  • string here.

  • So bushes is the texture, and so whatever quad in bushes

  • you want to give it--

  • in this case, we just gave it a random frame from that.

  • And then a lot of the same logic applies to other parts.

  • This is another part where we generate bushes just on flat land.

  • We have a chance to generate a block--

  • 1 in 10 chance this is a jump block.

  • So here we have texture, x, y, width, height, frame.

  • Notice that we have collidable is true, and this

  • is how we can test to see whether a tile is collidable or not.

  • Hit is false, meaning that we haven't hit it yet.

  • And if we have hit it, then we do this code basically-- onCollide gets called.

  • You can see where this gets called in the collision code for--

  • if we look in Player.

  • Player has check left collisions, check right collisions,

  • and check object collisions.

  • It doesn't have check up and down collisions.

  • There is a corner case for both of those such

  • that the logic had to be duplicated.

  • I forget exactly why.

  • But you basically get a list of objects that you check for.

  • Oh, the reason why is because when you get

  • the collided objects when you're in the jump state,

  • you trigger the onCollide function.

  • So let's go to PlayerJumpState.

  • If we're in the jump state, this is where we would basically

  • check to see if we've gotten any objects that collide with the player.

  • If it's solid, call its onCollide function, object.onCollide, and then

  • we just pass in the object itself, basically.

  • And so if you go back to LevelMaker, that's

  • where we write the onCollide function.

  • We write the onCollide function within the game object here.

  • So we just give it an onCollide, remember,

  • because functions are first class citizens.

  • We can just say onCollide gets function obj,

  • where obj is going to be this object.

  • If it wasn't hit already, one in five chance to spawn a gem--

  • so going to create a gem.

  • It's got all the same stuff in it.

  • In this case, it has its own function called onConsume.

  • onConsume takes a player and an object.

  • And then this is all arbitrary, by the way.

  • You can create whatever functions you want.

  • These are callback functions, effectively.

  • We're just going to play the pickup sound and then add 100 to our score.

  • And then here, in the event that we did get a gem,

  • we tween it over the course of 0.1 seconds.

  • We tween its y to be from below the block to up above,

  • so it has an upwards animation, effectively.

  • And then we have another sound that plays.

  • But that's effectively how we're spawning game objects.

  • Game objects have textures, x, y, width, height, and then

  • you can give them callback functions that you then

  • execute wherever it's relevant to you.

  • In this case, you'll only really need to worry about onCollide,

  • because the assignment is create random keys and locks.

  • They have to be the same color, but you can choose them at random.

  • If the player collides with the key, then he

  • should probably get some flag that's like key obtained is true

  • or something like that.

  • And then you go to the block that spawns in the level,

  • so you should spawn a block with that same color.

  • And then on collide, you should unlock it, so get rid of the block

  • and then spawn a new game object--

  • the flag.

  • And then that flag will have its own on collide.

  • And when you collide with the flag, restart the level.

  • And that's effectively the gist behind the problem set.

  • So it probably shouldn't take--

  • I would say probably maybe 40 or 50 lines of code probably should do it.

  • AUDIENCE: That game object--

  • was that a class?

  • It's not a table.

  • What is that?

  • COLTON OGDEN: It is a class.

  • There's a GameObject class.

  • A game object is basically-- and I realize I didn't touch on it too much.

  • In the context of this distro, you could almost think of it as an entity.

  • In this case, what I've done is I've differentiated

  • between living things and non-living things

  • as being entities versus game objects, which is a semi-arbitrary distinction.

  • But for a small project like this, it makes sense.

  • For a large project, I would probably create everything as an entity

  • and then give different kinds of entities

  • their own behavior and their own components,

  • sort of like how you do with an entity component system.

  • AUDIENCE: Are there two ways to create a class in Lua,

  • one with the curly brackets and one with regular parentheses?

  • COLTON OGDEN: There is, actually.

  • So the question was, is there more than one way to create a class in Lua

  • whether it's parentheses or curly brackets?

  • Yes.

  • I don't think I've ever actually talked about this.

  • Let's go back to LevelMaker.

  • If you instantiate a class and that class

  • takes in just a table as its only argument, you can just pass in this.

  • This effectively is that argument table.

  • You don't need parentheses.

  • It's effectively doing this--

  • same thing, only you don't need the parentheses.

  • AUDIENCE: And then that's a table that's being passed in?

  • COLTON OGDEN: Correct.

  • It's just an alternative form of instantiation

  • on things that only take a table as their argument for when

  • they get instantiated.

  • AUDIENCE: And you can only have one table in that case?

  • COLTON OGDEN: Correct, yes.

  • AUDIENCE: Wouldn't it be easier to create a new class which

  • would have its own set of game objects so you

  • would create a gem which would be helper dot gem, which

  • would in turn create a game object?

  • COLTON OGDEN: Can you say that one more time?

  • AUDIENCE: It's kind of hard to explain.

  • Wouldn't it be easier to create another class which

  • would have your gem and everything, and your gem in that class

  • would be a game object?

  • But in this class, when you wanted a gem,

  • you would say local gem equals helper class dot gem.

  • COLTON OGDEN: The question was, wouldn't it

  • be easier to create a helper class that would allow you to instantiate gems?

  • Possibly.

  • I think if you were going to design this a little bit more robustly,

  • and if this were going to be a larger game,

  • then you would just create a subclass for blocks, gems, et cetera.

  • To shrink the number of files that we had in the distro and to sort

  • of consolidate everything together and put all of the level code together

  • in one spot, I decided to just create GameObject as an abstract class that

  • you could then just create your own behavior for within the actual

  • constructor-- which is this bit here, which is just the table--

  • and then allow you to override the onCollide and onConsume functions.

  • You can actually give it whatever functions you want.

  • You could give this some arbitrary named function and then test for it later.

  • This is almost like an obscure way of inheritance.

  • But I think if I were to engineer this with the goal of making it a really

  • large game, I would just subclass.

  • I would just create a class for gem, a class for block, a class for bush,

  • et cetera, et cetera.

  • It wasn't strictly necessary for this example,

  • so we ended up keeping everything a little bit

  • more abstract in a sense-- a little bit more general purpose.

  • But yeah, you could definitely create classes for those.

  • And if you were in an entity component system,

  • you could have a consumable component.

  • And then that consumable component would then

  • allow you to give it some sort of behavior that affects the player when

  • the player consumes that object.

  • In this case, a gem is a consumable, so you would just

  • give it a consumable component with a texture of the gem

  • and then give it a callback function that

  • just increments the player's score.

  • You could probably put that in 10 or 15 lines of code.

  • It would be pretty easy.

  • And then blocks would be a spawner component,

  • so they have a chance to spawn.

  • And then you would pass in that spawner component a gem maybe,

  • so it would have a chance to spawn the gem that you passed into the spawner

  • component-- and then also a solid component to say, oh, this is solid.

  • So if I hit it, I should trigger a collision

  • and not be able to walk through it.

  • So you just layer on these components.

  • I would encourage you to think about this way of composing

  • your objects a little bit, particularly as we get towards Unity, which

  • makes a lot of use of this concept.

  • In short, yes.

  • I think that's pretty much everything.

  • Let me just go ahead.

  • We're running out of time here, but like I said, one more time--

  • make sure the player starts above solid land, random color key and lock,

  • and then make sure that when you get the key, you can unlock the lock,

  • and that spawns the goal.

  • So this is all something you can just add to the LevelMaker class,

  • and it will all work with your game level that way.

  • And then you touch the goal flag, then respawn the level.

  • So today, we talked about Super Mario Bros.

  • The other big Nintendo game of that era--

  • arguably one of the greatest of all time-- is Zelda.

  • So we'll be talking about a very simple Legend of Zelda game,

  • where we just have a random dungeon that we can go through,

  • a top down perspective, fight simple monsters,

  • open chests, blow up walls-- that sort of thing.

  • We'll talk about triggers and events.

  • And then we'll talk about hurt boxes, inventory,

  • a very simple GUI for opening up a menu, and then world states

  • so that we can see which doors have been blasted open so that they

  • render appropriately and whatnot.

  • And that's it for Mario.

  • Thanks a lot for coming.

  • I'll see you guys next time.

[MUSIC PLAYING]

字幕與單字

單字即點即查 點擊單字可以查詢單字解釋

B1 中級

超級馬里奧兄弟編程教程--CS50的遊戲開發入門課程 (Super Mario Bros Programming Tutorial - CS50's Intro to Game Development)

  • 0 0
    林宜悉 發佈於 2021 年 01 月 14 日
影片單字