[MUSIC PLAYING]
COLTON OGDEN: All right, welcome to GD50, Lecture 5.
Today's topic is The Legend of Zelda, as you can see on the screen there.
A very iconic game.
Last week we did Super Mario Bros, which is arguably the most iconic video game
of all time.
Legend of Zelda is a close contender.
Even now, just like with Mario, they're producing very wonderful games.
I think last year Breath of the Wild took all the, I forget which
ceremony it was, or award sort of show.
But it took like every award possible.
Just last year, the latest Zelda, the Breath of the Wild.
So it feels sort of apropos to talk about Legend of Zelda in that context
as well.
Here's a screenshot of the original Legend of Zelda.
It was an NES title, just like Super Mario Bros--
just like most games of its time, tile based.
You can sort of see how the surroundings are sort of layered tiles bit by bit.
The goal of the game overall was to sort of explore this open world, which
is kind of a first of its kind.
You controlled Link, shown at the very bottom there.
You had a sword, yo had bombs, you had arrows.
You want to go find rubies, you went through dungeons.
You slayed monsters and bosses, and then ultimately the goal
was to obtain the Triforce which is shown at the very top left and right
of the slide here.
Here's a screenshot, another screen shot of Legend of Zelda,
inside an actual dungeon where you can see at the very top,
there's got like a map layout.
Maps are sort of arranged in a grid.
And you could go in a room by room, whereby each room was the entire width
and height of the screen, looking for, solving puzzles, looking for items,
that sort of thing.
You can see there's a monster there.
You have hearts at the very top right.
So when you take damage, hearts sort of decrement.
Will see both of these, the hearts and the dungeon aspect of the game,
today in lecture, as well as the width and height of the room
being a dungeon tile.
And controlling an avatar.
But this is sort of a representative screenshot of what
the game looked like back in the day.
Some topics today that we'll be discussing in order
to implement sort of the foundation for what a game engine like this
might look like are things like top down perspective.
So as you can see, in this screenshot, we're
looking at Link from the very top in a bird's eye view, as opposed
to how we used to look at Mario, from the side, where
it was sort of like a side scroller.
Here we're actually looking at things from the top down.
So we have a view of the room in a sort of a different perspective.
There's not really a z axis, technically speaking.
But there sort of is.
You can implement things like gravity in a game engine like this
by jumping over holes and things like that.
But there's no gravity like there was in Super Mario,
where you walk left to right, you jump over gaps,
and it's very easy to see in that sense.
We'll be talking about infinite dungeon generation.
So in the context of today's example, we'll
see how we can go about implementing a dungeon that sort of you
can go through forever and ever, and how to sort of model that
and make it look as if you're traveling through a dungeon over and over again.
And going through different screens in different rooms that are all different.
But in reality, we'll see it's actually just an illusion like we've
seen before in prior lectures.
Well talk about hitboxes and hurtboxes, and what
the difference is between the two.
Hitboxes being rectangles on the screen that inflict damage
upon other entities in your game world, and hurtboxes
sort of being the rectangle that models where your player,
or where an entity in the game can be hurt by other hit boxes.
Well look at events.
So events are a way of broadcasting some key message that tells
the game world oh this thing happened.
And let me register a function to call when that event is processed.
So on some event, dispatch an event, and then
upon that event being received by whatever is listening for it,
perform this chunk of code.
It allows you to sort of decouple aspects of your game
engine a little bit, and makes for a little bit more readable code.
And allows you to do some interesting things with achievements,
for example, where you don't necessarily want to pull every single frame.
Oh, did I do this sort of arbitrary list of things this frame?
Rather, you can just process all of that with an event,
and have a listener and a function that every time you broadcast pick up coin,
maybe I have an achievement that's like oh,
how I picked up 50 coins in this level.
Your achievement callback function in your event can then look for that,
and in your game loop, you don't have to say every frame,
if a player has 850 coins, and you sort of
take out bloat that would exist in your sort of overall game loop that way.
And we'll see an example of how we use that in today's lecture.
We'll look at screen scrolling.
So a very iconic aspect of Legend of Zelda
is when you're going from one screen to another,
there is a sort of transition period as one screen loads and the other screen
goes away.
In hardware, in the NES, this was sort of the only way
you could actually render more than a screen width of tiles.
You actually had to dynamically load in tiles and sort
of overwrite tiles that existed before that in memory.
But today, we'll see how we can sort of create
the illusion of doing that by just drawing a room
and then having our main room, and then just sort of tweening over,
and then setting everything back to the origin at 0.0.
And it makes it look as if we're moving back and forth between all
these rooms, when in reality all we're doing is just doing a shift,
and then putting everything back to 0.0.
So I'll illustrate that on the screen, if I can.
And one of the last things we'll look at is data driven design.
In the context of a lot of types of games, particularly RPGs and action
games, it's often very valuable to be able to model all of your items,
entities, sort of abilities, anything you really
can, as data rather than logic, in order to make it easier for you to write one.
And two, for you to sort of allocate the design aspect of your game
to other people, not just programmers.
And have an engine that's very versatile.
And model and we'll take a look at how we implement
sort of a foundation for that later on.
But first I'd like to illustrate a demo for today's lecture,
a sort of implementation of Legend of Zelda.
Is there anybody that would like to come up and sort of demo this today
in class?
Awesome.
Thank you so much.
So whenever you're ready, go ahead and press the Return key there.
[MUSIC PLAYING]
And so we see on the screen, Legend of 50, which is our Legend of Zelda
you rip off.
So you press Enter.
You go into, this is the play state of the game.
So you control an avatar.
You can walk around.
He can interact with switches, as we saw here.
When he presses a switch, the doors open.
And once the doors are open, you can walk through them
and it does, as I alluded to before, it transitions the screen, one
entire screen height or width, depending on the direction, up above and below.
We see here now where we're pressing spacebar to actually swing our sword.
It's destroying the entities in the game space.
So there's a hitbox triggering when you press the space bar that collides
with other entities in the game world.
If that hitbox hits their hurtbox, then they are flagged as dead,
and they disappear from the game world.
And so this goes on ad infinitum.
It's just an infinite dungeon.
So he can go in-between as many floors as we want to.
All we're doing is every time we go through a doorway, spawn a new room,
delete the old one, and just keep going forever, and ever, and ever.
In a game like Legend of Zelda, it typically doesn't work like this.
There is a hard set number of rooms, and they all exist in sort of a 2D array.
And when you generate your dungeon, you sort of
have to take into consideration things like where do I put the keys?
Where do I put the boss?
Where do I put treasure?
That sort of thing.
We won't get too in-depth as to how we can implement a complex algorithm like.
But we'll touch on it a little bit, and talk about maybe some ideas
that we have.
And then lastly, if we just-- if we want to demo--
if we can see the top left, we have hearts there,
which is iconic sort of Zelda.
When we do take damage from an enemy, notice that we flicker a little bit.
So there's some rendering behavior triggering,
and we become invulnerable when we take damage.
And then lastly, when we finally take the last hit,
we get a little game over screen, using the Zelda font.
And that's the game in a nutshell.
We press Enter and loop back.
So pretty simple altogether, but there's a lot of pieces
here that we haven't really seen before.
So thank you very much for coming up to demo the game.
All right.
So that was a demo of Legend of 50, Legend of Zelda.
It has a lot of the pieces that Zelda has.
It's not as fully fleshed out, of course,
as a full game like Zelda, which would be monstrously large.
And there's a lot of things we need to factor in.
But the foundation is there.
We have the dungeon foundation upon which we can build an actual generator,
if we wanted to.
We have entities.
We have hit boxes.
We could easily model, because we have switches in the game,
we could model things like treasure chests that
open that have different states, just like the switch had different states.
And have different objects in our game space interact with other objects.
We have a lot here, and we'll talk about all of it today.
So here's a few screenshots just to show what we just looked at.
Our goal mainly is the second screen shot,
on the top right, which is the play state, which has
all of the little pieces we just saw.
But just to tie it all together, we do have a start state here,
and a Game Over state which is relevant in regard
to the hearts on the top left.
So the first thing that I'll talk about as we sort of get the engine built up
is, where do we go for getting our assets into the game?
And just like in prior lectures, prior to today,
we just have a sprite sheet, like we did with Mario,
where everything is laid out in a fairly even sequence of tile segments.
In this case, the segments are 16 by 16 pixels.
Here, I've overlaid a grid just to show that the picture is indeed perfectly 16
by 16.
And this is something that you should take into consideration.
And consciously do when you're building your game assets.
Make it easy to chop up into little pieces,
so you can then index into this sprite via some table of quads.
And then assign an ID to whatever object,
or tile you want to render to the screen.
And so we see if you look here, things like the doors,
they're not perfectly modeled by one tile.
And so when you do have things that are larger than 16 by 16,
it's not quite as easy as oh have an entity or an object with this frame ID,
and draw it to the screen.
You actually need to a little bit more complicated render logic.
So just offhand, if I wanted to draw a door,
does anybody have any suggestions as to how I would model that?
And or draw it to the screen?
So if we have a door that's not perfectly a 16 x 16 pixel tile,
offhand, what we need to do is just basically store all four
of those tiles in this case, or at least keep track of however many tiles
your object is.
And then just draw them based on some offset.
Have an xy that represents the top left of that object.
Place it in the right position, and instead of drawing just one tile,
we draw four tiles.
And then instead of having just one collision boss that's 16 by 16 tiles,
you can do a couple of things.
You can either check collision on all four of these tiles,
or at least all two of these door tiles, or model your own custom hit
box that then maybe that object that represents a doorway has control of.
So you can say doorway collides player, and then the doorway
has a height and width of however many pixels it is wide by tall here.
Just this door part.
And then, you are able to then build upon not just having one tile that
models an object or entity in your game space,
but now you have more artistic flexibility.
You can do things like have doorways that are more than one pixel wide.
And sort of have a more convincing gameworld as a result of that.
You'll see a lot of things like that in games like RPGs
where you have full houses that are obviously not just one tile.
You can model things as one tile if you want to,
but from an artistic perspective, and from just a game engine perspective,
it's a little bit easier to programmatically
be able to cut up all your assets into tiles and draw them as such.
So being able to build upon just the single sprite
and be able to do multiple sprites represent
an object is the key to things like houses, things like big trees,
things that are just more complicated than single tiles as we see here.
The character sprite sheet is, as you can see, a little bit more complicated.
So this is an example of a sprite sheet that has padding.
Sometimes you will get sprite sheets that aren't neatly divided
into even segments.
And for good reason.
Because sometimes you have tiles that are not perfectly
the width and height of whatever your game engine's tiles are.
In this case, the player is actually 20 pixels tall, and 16 pixels wide.
And on top of that, he's got different frames of animation.
We can see here at the bottom left, he's got a sword swinging animation.
The sword swing animation is actually stored in a 32 by 32 pixel frame,
because sometimes depending on which angle he's looking at,
his sprite can get a little larger or smaller.
So when you have a sprite with padding, what's sort of a way
that we can draw this to the screen reliably?
How do we take this into consideration?
How would we render a screen sprite with padding?
So let's say this sprite here, our character swinging a sword, is in a 32
by 32 pixel box.
Right?
This sort of box around here, this white space.
But he's only still maybe 16 pixels wide by 20 pixels tall.
Or maybe he is some amount similar to that.
In order to draw him to the screen, which you can look at in the code,
all we really need to do is assign that sprite an offset.
Just say, OK, this sprite's offset x and offset
y are some value that basically lets us draw the sprite
to a negative x and a negative y value.
And that'll shift the sprite up, such that it aligns perfectly
with wherever his xy are.
And you can take a look at the code to see exactly how that works.
Lastly the actual entities for our game world are different creatures.
And this is a sprite sheet-- this is a more ideal sprite where everything
is 16 by 16 pixels wide and tall.
And all we really did here was just divide the sprite sheet
using the regular utility function generate quads by 16 16.
We don't have to worry about separate widths and heights for everything,
and we can create animations very simply because of this.
As a side note, something that I like to do, when I'm parsing
or when I'm piecing apart a sprite sheet that's got a lot of individual frames,
particularly if I'm creating animations, or I just
need to know for a tile, a particular tile to draw to the screen.
It's kind of a pain to manually look through each and every tile, one
by one.
And say OK, this is one but OK.
But if I want to find out which one the slime is,
I got a count, OK how many pixels wide does this?
OK, and then it's times 4, it's 12.
And then it's on the fifth row, so 12 times 4 plus 1 will give me, OK.
So it's at index 49.
So I've spent a non-trivial amount of time sort of hand
calculating what all these do.
I wrote a simple Python script that will just go over a file,
and just add a digit to each individual quad,
to just show you at a glance what each sprite is.
And this is including the distro, so you can see what it looks like.
Saves a lot of time.
And I recommend trying, when you're sort of working with assets
and you find yourself doing something that takes a long time,
and it's sort of tedious and mechanical, maybe
try to find a way to sort of automate that, or at least
make it easier to do things at a glance.
In this case, just simply imposing a numerical grid solves that problem.
I don't have to spend a significant amount of time
figuring out which frames of animation the ghost facing left is.
I know instantly it's 67, 68 and 69, and allows me to just crank things
up that much faster.
So the first thing we'll take a look at is sort of top down perspective.
And we mostly talked about this before, earlier.
But all it really is is a tile map, which we've seen before.
The only difference is now, instead of looking at things from the side,
we're just looking at things from up above.
So what's probably the most obvious consideration
when designing a top down perspective versus a sort of side scrolling
point of view?
Looking at particularly at how the tiles are drawn?
What stands out?
So the thing that stands out to me is that we
have things like shadows on walls here.
We have also corners, and things altogether
are skewed such that they are almost like rotated slightly as if they're
simulating an angle of rotation relative to the camera facing from up above.
You can see this on the player, for example.
It looks like you're looking at him from backwards and up.
When you're modeling your assets that way, it's more convincing.
And Zelda has always done this, to make your assets look
as if they are slightly tilted, and that you're looking up above.
So when you're designing a top down game, just
for convincing the sake of being more convincing, try and emulate that.
The entities here, like the skeleton, and such,
are a little bit more straight on.
The bats and slimes and whatnot, even though they still
have a little bit of that appearance.
Like the spider it sort of looks as if it's from the top back.
But modeling your assets from a top down perspective, mainly the thing.
Pay attention to shadows and highlights, which
adds a lot in terms of convincing us that we're in this room with lighting.
And also make sure that you're doing things like corners and stuff
and making, it look as if things are slightly skewed rotation wise.
The first thing that we're going to look at in terms of the code in the distro
is Dungeon Generation.
So in Legend of Zelda, dungeons are fixed.
They're completely set in advance by the designers.
And in most games, this is actually the case.
In our example, and in a couple of other examples,
a primary example of them being that I can think of,
that I have in the slides, a famous game called The Binding of Isaac,
dungeons can also be generated.
So what's the sort of like the main unit of a dungeon?
At least in the context of Legend of Zelda?
If you had to distill what comprises a dungeon,
what's the most fundamental unit?
Tony.
A room.
Yes.
A room.
So we can almost look at this, if we picture it in terms of a 2D array,
right?
We have, assuming that this is like index 1.1
in Lua, 0.0 in other languages, going left to right, top to bottom.
Basically we have on or off, relative to each of these.
Off, on, off, off, off.
Each of the indexes in this 2D array holds a room.
And so the room has connections implicitly between the other rooms.
If you wanted to go, let's say, from this room here to the room
up above it, what's the offset, in terms of the 2D array?
How are we going from this room up to this room?
So we're just going up a y level, right?
So this is x level 3, y level 3.
If we wanted to go up to the next room, we need to load in.
If we're doing it the Zelda way, right?
And we're just going--
we're doing a transition between one room
to another, what we need to do is load in the room at this room,
minus one on the y, and then perform the transition.
And then set that to the current room, such
that now we know we're at y level 2, x level 3.
That's our dungeon.
In the context of 2D dungeons in The Legend of Zelda,
that's as simple as it really is.
You have a 2D grid of dungeon rooms.
Each room has its own collection of entities and objects
and connections to other rooms.
But really, all you do to fill a dungeon is fill an array in a smart way
such that there is no rooms that are, for example, left by themselves.
Notice that every room in the dungeon has at least one connection
to another room.
And that when you're maybe doing your algorithm
to create a convincing dungeon, let's say this room here has
a door on the right, that has a lock.
Right?
We want to make sure that the key isn't in that room.
Because if it is, we're never going to be
able to get to it, assuming that we come from another direction.
So when you're designing dungeons procedurally,
you want to take these sort of things into consideration.
And then for example, the boss room.
Let's say this is the boss room, the boss room
should have maybe a boss key, or something like that.
But the boss key should not be, obviously, in that room.
It should be somewhere maybe where there's a couple of rooms before it
that have a lock or a key, so that you know there's some sort of challenge
involved in your dungeon.
It's not just random as we've done before.
There has to be a little bit of sort of conscious design on behalf
of your algorithms.
Today we're doing things completely random
for illustration, just because a system like this is fairly robust and complex.
But with some effort, you could create a simple dungeon generator just using
those mechanics.
Just make sure that you have locked doors.
The locked doors can only open when you have a key.
Make sure the key exists in a place that's accessible,
and sort of create a chain of a control flow.
Model maybe via graph of some kind that represents your dungeon,
and the progression thereof.
So that's what a Zelda dungeon looks like.
That's what a 2D dungeon in this sort of perspective looks like.
And it looks-- it will look similar to this in other game engines.
It doesn't necessarily have to be perfectly modeled
as a screen width, screen height room going into another screen width, screen
wide room.
You can have arbitrarily complex rooms that have arbitrarily complex sizes
and shapes.
But you still need to make sure that the connections going out of the rooms,
like if you can still model left, right, up, down, if you want to.
You can model arbitrary numbers of connections between rooms.
Just make sure that you have puzzles that can be solved.
That's the main sort of obstacle in generating your dungeons.
Here's a game that I really like that uses the old Legend of Zelda formula
to very good effect now.
It's called The Binding of Isaac.
Notice already we can instantly see that it's top-down perspective.
It's the entire width and height of the room is the dungeon of--
the entire width and height of the screen is the dungeon room.
You have a map up here that shows you, OK, I'm in this room right here.
I can go up, I can go left.
I can go right, I can go down.
This room up here with the yellow crown, that's
going to be locked behind some door with a key.
So we need to have keys that spawn in any of these rooms that
are just blindly accessible.
And Isaac does things a little bit differently also
in that it generates keys and bombs randomly,
so that you can actually get--
you don't necessarily have to plant your keys in very specific locations,
if your algorithm is sufficiently accommodating and complex enough.
You can just at, the end of every room, have a chance to spawn a key randomly.
And if you're lucky, or if you're not lucky, and assuming
that the end of the dungeon doesn't exist behind your locked doors.
You have the opportunity to stall and lock those doors, or not.
And just go on through your dungeon as needed.
In this case, they don't lock the boss doors.
So you can go through the boss door regardless of whether you have a key.
So they've accommodated for this purely random approach,
and that just goes to illustrate how you can still take randomisation
using very complex principles, and produce games
that are extremely addicting and fun.
And you don't necessarily need to be super elaborate.
The first thing, OK.
So hitboxes and hurtboxes will be the next topic.
So I want to touch on, in the code, how we sort of do dungeon
generation in the game engine here.
So I'm going to open up--
there's a few files.
So doorway, notice in the source distro, there's a World folder.
We've sort of categorized all of the files that
have to do with world generation within that World folder.
We have a doorway file, we have a dungeon file, and we have a room file.
So the dungeon file models the dungeon.
All of the rooms that form our dungeon, just as a very high, level top
level, data structure.
It's actually very simple.
The room is, as we saw before, the individual unit of the dungeon.
So a dungeon is effectively like a table of rooms.
And then it holds the code for how we can transition between them.
But that's sort of how we can think about it.
When you a game level in general, in this case dungeon
is sort of our game level.
You can model the different subset aspects
of your level via some data structure.
In this case, we've decided to make it room.
But if you have just a platform or level,
maybe you have regions or zones, or anything.
Just little subsegments of your level that you can transition between.
It's useful to think in terms of that, because from an efficiency
and performance standpoint, you want to dynamically probably load
certain levels one at a time.
Certain aspects, certain components, sub areas of your level, one at a time,
rather than just the entire level at once,
because depending on how sufficiently complex and large your level is,
you can get to exercising your computer's memory constraints.
So don't want to do that.
So in this case, our algorithm is we have a dungeon, we have rooms.
We have one active room that's only visible at one time that's
loaded in memory.
And then whenever we transition between rooms,
we want to have another room that gets temporarily loaded.
So let's look at the dungeon class here.
Dungeon.lua.
So 14 self-taught rooms, empty table.
We're going to fill that with rooms, just room objects.
Self.currentroom.
Room, and we're going to pass it in the player.
So that the player has access to all the entities and objects therein,
and can do things like collision detection, which is very important.
And also so that the entities in the room can look at the player
and then decide what they want to do with their AI, which
you can model arbitrarily complex.
The whole init function of room.lua is going, we're going to show it here.
So in room.lua, in that same world folder,
we've instantiated the dungeon at a high level.
We know the dungeons, it's basically a table of rooms.
So what's a room look like?
Well a room, as we saw before, what are the pieces
that a room needs in order to function?
What does the room have to sort of keep control of?
AUDIENCE: Doors.
COLTON OGDEN: Doors.
Anything else?
So doors it has to keep track of, right?
Because when we touch a door, we should transition to the next room.
It should keep track of the player.
So the player can update.
It should keep track of objects in the room like switches,
as we saw in the example, such that they can update those.
And see has the player stepped on the switch?
If he has, open the doors.
And then entities, right?
It should keep track of all of the creatures in the room
so that they can update, they can interact with each other.
The player can hit them, or they can hit the player.
And you can model whatever interactions you want to.
So we can see here.
Oh, and another thing too.
Probably one of the more important visual aspects of it,
we need to also have a set of tiles that model what the room looks like.
It's a container.
So we're going to draw, we have a corner, a corner, a corner, a corner.
Walls on the sides.
Walls on the top and bottom.
And then a floor.
And so we need to obviously draw the room
before we draw all of the other things, and update all the other things.
So tiles, doorways, entities, and objects.
So we can see here.
You have tiles, soft-lit tiles against an empty table.
We have a function called Generate Walls And Floors.
Entities, empty table.
We have a function called Generate Entities.
And then objects equals empty table.
We have a function called Generate Objects.
And lastly, doorways.
And then here because it's not necessarily
as complex as we need for a entire function, we just have four doorways.
And this is static.
I've chosen in this example to have all the doorways always
be in the same place, and sort of behave in the same way.
But you could create a more complex system, whereby especially
if you have an algorithm that generates a dungeon dynamically,
and maybe your rooms aren't necessarily hard set.
Maybe you have a broom, right?
If we go back to our slides earlier.
And let's say we're looking at this room right here in the very center.
We can see it has a doorway on the left, a doorway up top,
and a doorway to the right.
But there's no doorway on the bottom.
And that looks like an arbitrary design decision on behalf of the designers.
But if we look at this room up top here, we
can see that it sort of behaves in the same way.
There's a doorway going into this room to the left.
There's doorway going to this room down below.
But it doesn't have a doorway up top and a doorway on the right,
because there are no rooms going between that room and those directions.
So when you have a 2D array of dungeon rooms,
and you want to model your doorways, it can be as simple
as is there room in that direction?
If there is, and you will have your data structures sort of
laid out in advance before you generate these doorways, if it does not exist,
don't make a door.
If it does exist, make a door.
And then make sure that when you transition
between from the right to the left, or from the bottom to the top,
that you go to the correct rooms at the correct indices in your 2D room array.
Does that make sense?
Anybody have any questions so far as to like how this works at a high level?
OK?
Awesome.
So these doorways here are all because of this dungeon
is completely random, and every time it always has doorways going top, bottom,
left and right.
We're always just going to put four doorways,
going top, bottom, left and right.
Note that they take a string denoting which
direction they are, and this will become important
later on in the doorway class, which we see here
is modeled as a separate class.
False here just means is the door open?
So by default, we're going to close the door.
And then self, so that we have access to the room from the doorway.
The room should have a reference to the player,
so that it can model interactions between the entities and the player,
as well as the objects and the player.
Notice that it has a render offset.
So if we look at the game here, the tiles
don't completely match up to the width and height of the screen.
And that's mainly a function of the virtual height
not mapping perfectly to 16 tiles, divided evenly by 16 tiles.
So what I've done is I've made the dungeon a little bit smaller.
And also the fact that the doorways take up a couple of extra tiles of padding
on their sides, it's like sort of blank space up here.
We've shifted everything inwards a little bit.
We've made the dungeon two tiles smaller width and height wise than the screen.
And then we've just rendered offset by a certain amount
such that it's completely centered.
We calculate how much padding exists between the fully rendered dungeon
and whatever blank space is left, and just shift by half of that amount,
and that's our render offset.
So that's why render offset is important in this case.
When you're trying to center anything, you typically
do it just by calculating an offset.
Calculate the width and height of whatever you're trying to draw,
calculate whatever your width and height of your screen is minus that,
divide it by 2, that's your render offset.
And then these two fields here are interesting. self.adjacentoffsetX,
self.adjectentoffsetY.
Does anybody have a guess as to what this is used for?
Yeah.
Drawing itself when it's the adjacent room.
Yes, exactly.
So drawing itself when it's the adjacent room.
So when you load the next room, you're going
to instantiate a room just like this.
But by default it's going to draw at 0.0.
However, if we want that room to draw not right where we are.
Obviously, if we're in the current room, we
don't want the next room drawing right on top
of where we are, because then it's just going to layer right
on top of the room we're in.
And we want to draw it--
if we're going to the right, we want to draw a screen width to the right.
If we're drawing it to the left, we want to draw it a screen width to the left.
And same thing on the y-axis.
A screen height above, or below it.
So adjacentoffsetX or y, we just add to when we draw the room.
And that'll have the effect of rendering it separately from the room
that we're currently in.
I can try and draw an illustration here.
So if we have our room here, this is our current room.
So self.currentroom.
And let's say we have a doorway here.
Right?
That's a doorway object, the player is here.
He collides with that object.
It's going to trigger a transition to a room up above.
So what we do is we load in a new room right away.
We always have a pointer called self.nextroom.
And this is all kept track of in the dungeod.lua file.
But self.nextroom by default is going to be nil.
But when we transition from the current room to the next room, right?
We should set that to something.
So self.currentroom is going to be the same.
But when we trigger this collision, self.nextroom,
this is going to be equal to just a new room.
And then we get which doorway we're in.
We figure it out.
We figure out which direction we're moving in, technically.
And then if we're moving up, then we just pass in a--
we said it's adjacent offset y in this case, to negative screen height.
If it's below, we set it to positive screen height.
If we go here, it's going to be negative screen with the adjacent x,
will be negative screen width.
And then positive screen width on the x if we're moving to the right.
And so this sort of adjacent we could see, if do xy,
it will be that's basically the adjacent offset.
And so we end up when we draw our transition going from bottom to
up in this case, we just tween the camera to this value.
So we have a camera, right?
This is our camera.
And it's going to be looking here by default. So camera x and camera y,
those are values in our code as well.
When we trigger this collision on the doorway,
and we have current room here, which is at 0.0,
and then we have this room here, which is at 0 plus our adjacent
offset y, which is negative screen height.
So it has the effect of making it negative screen height on the y.
Our camera x recall love.graphics.Translate is our camera.
So all we do is we just tween that.
We say, OK.
Here's our Cam x is going to be here.
And Cam y, it's getting a little bit messy.
I apologize.
But our camera x and y are here.
And then over time, we're going to tween that up into the next rooms
x and y, which is a just x plus adjacent offset x, y plus adjacent offset y.
And then once this camera has shifted from here up to here,
or whichever direction we're going, whether it's up, down, left or right.
Once we've completed that, we can normalize everything again back to 0.0
by doing self.currentroom equals, what do we need to do,
if we're going to put everything back to?
Let's say I want to make this room the new current room.
What do I need to do?
So we have current room, and we have next room.
If I want the current room to become the next room,
all I need to do is say self.currentroom equals self.nextroom, right?
And then once that happens, what happens to the adjacent
offset x and y of the next room?
They get set to 0, right?
I want to take this room that we've offset up here,
and I just want to make it the center of the game world again.
I want to just put it as 0.0, so we can do this exact same calculation
by just setting adjacent offset x or y to a negative or positive screen height
or screen width relative to 0.9.
So what I'm going to do is just set the adjacent offset
of x and y of the next room, which is now current room, to 0.
And it's going to draw it right back at 0.0.
And then camera x and camera y are also going to be set to 0.0.
And this is going to have the effect of looking as if we're going up
and staying there.
But in reality, we're just going up, and then
instantly shifting everything back to 0.0,
including the player, entities, and switches of that room.
So it's purely an illusion.
But it allows us to simulate this sort of infinite exploring a dungeon effect.
Does that make sense?
Does the overall flow of how this works makes sense?
OK.
So it's a little bit messy there.
Hopefully I was able to illustrate the overall algorithm for how
the infinite dungeon generator works.
Where did we leave off?
We were in the init function of the room.
So let's take a look at a few of the functions that comprise that.
So generate walls and floors.
This is very similar to what we've looked
at before with tile maps in Mario for example, where we just go from y to x.
And then we just pick a random ID, or our random ID.
Well, it is random ID for some of it.
But sometimes we need an explicit ID.
So remind me, what does the ID actually map to when
we're you drawing tiles to the screen?
If we want to give a tile an ID, what does that, what should that map to?
Yeah?
AUDIENCE: The frame in the sprite sheet.
COLTON OGDEN: The frame in the sprite sheet, that's correct.
It doesn't have to for all game engines, for all implementations.
But it's the easiest thing to do, is just to give your tile an ID
that you can then just draw, you can index
into your sprite sheet at that ID.
It's just very, very simple, lightweight clean approach to modeling.
And we can see here, ID gets ID.
After we've figured out what ID we want, now how
do we determine like let's say I want to draw--
let's say, for example, like this tile here.
Notice it's a corner tile.
What do I need to do to basically assign--
I'm going to die before I even have the chance to show you.
While I avoid enemies, if I wanted to draw that top left corner,
what am I sort of looking for?
I'm still taking damage anyway.
Relative to x and y, what am I looking for?
What x and y does that tile need, assuming
everything starts 1.1 on the top left, and goes down
to height and width of the overall dungeon?
What is the xy need to be of that top left corner?
It needs to be 1, right?
So if that's 1.1, basically the top left corner,
that ID should be equal to the corner sprite.
The top left corner sprite, specifically.
What about the top right corner sprite?
What should the x be?
Sorry?
AUDIENCE: Width.
COLTON OGDEN: Yes, width.
Exactly.
What's the y?
Still going to be 1.
What about the bottom left?
Width, comma height.
And then the bottom right?
AUDIENCE: But it's [INAUDIBLE]
COLTON OGDEN: Sorry, bottom left should be 1, comma height.
Bottom right should be width, height.
And so basically checking the position of each tile
is how we can infer its ID, which is what we're doing in the source code
here.
If x is 1 and y is 1, OK, top left corner.
And notice that we've put these into constants,
that we've put in the constant.lua, just for readability.
So we can instantly see OK, I should set ID tile top left
corner, not some arbitrary number that's our index into the tile sheet.
At maybe like 40 something, or whatever it is.
Bottom left is 1 and height.
Top right is width and 1, and then bottom right is width and height.
And we can see, it's readable.
We can sort of see at a glance what we're doing, where you're conditionally
generating all of our tiles.
If none of those are true, and then x is 1, what's that tile?
If x is 1, but it's not a corner?
It's a left hand wall.
And the same thing is if it's the width, it's a right hand wall.
And if y is 1, it's a top wall.
If y is height, it's a bottom wall.
And then if it's none of those, it's got to be a floor, exactly.
So that's basically how we generate the overall--
it's a very easy, simple generation algorithm.
What I've done is for randomization here,
we have actually a table of potential left walls, potential right walls,
potential top and bottom.
And then we just ascribe it a random value within there,
based on however large that is.
And that gives us variable, we can see it.
We have random-- every time I generate it.
So take note of maybe some of the tiles visually
that you can see there on the map.
Notice that they change.
Notice that the switch has also changed.
Notice that they changed again.
So everything is variable.
We have a little bit of visual variety.
This is what we did, essentially, with Mario.
We changed the tile set, and the topper set.
Only now we're just changing, we have only one tile sheet,
but several variations at the same tile they're in.
So all we need to do is pick a random variation of whatever tile.
And that involves us looking at the sprite sheet,
picking out which individual tiles, which separate tiles,
map to the specific type of tile, and then just picking a random value
from that table
That's how that works.
So a couple of things, I think, left to show relative
to the dungeon generation.
So 48, we have generate entities.
So entities are here.
We have their names, right?
Some of the entities we saw, skeletons, bats, slimes, ghosts, spiders.
It's a very lightweight function.
It's not like 15 or 20 lines, but we're generating
a bunch of different kinds of entities.
Because what we've done, we take, we basically
take a random type from that table.
We create an entity, and then using what we've
defined in a global table called Entity Defs,
we take out what sort of matters in terms
of generating each individual entity, the characteristics thereof,
and we just put it in simple data form.
This is sort of like the segue into data driven design for your game.
If you look at Entity Defs here, we can just see that all of our entities
are a sequence of data.
So up here we have player, on line 10.
Player gets a table.
We give him walk speed.
We have a table of animations.
So he's got a walk left animation, a walk right animation,
a walk down animation.
All of those have their frames, their interval, their texture, everything
is just clean data, right?
There's no logic here, really.
It's just flags, or values, simple things.
You could give anybody who has sort of the basic knowledge of what we're
doing this file, and maybe some textures, and say, hey,
I want you to design--
I want you to lay out basically all of that's
involved in what makes a skeleton render to the screen.
Create all the animations for them.
Give those animations their timing.
Give them the exact frames.
Maybe skeletons should have health, and maybe health should be equal to 10
on a skeleton, or something like that.
There's not really any programming going on here.
But we're describing everything in our game as data.
And that's important thing in complex games that have fairly complex systems,
but that can be modeled via some attributes.
You can just ascribe your entities and data, and then let your engine parse
this information, and then create your entities programmatically.
You shift the burden from the programmer to the designer, a little bit.
And you afford your design team, you and maybe other people
who aren't as comfortable with programming,
the ability to modify the game engine, add things to the game,
without having to go through the bulk of your engine code
and do anything too fancy.
In this case, it's simple.
All we're doing is just we're creating animations, and assigning a texture
to each of these individual things.
But I alluded to this last week.
You can have a file that maybe describes something like a goblin.
We'll see this at the end of a lecture, which has maybe like a flag
for is it flammable?
How much health does it have?
What are its animations?
What skills does it have?
What's its attack strength?
What's its defense?
Where does it spawn typically?
By putting all these attributes together and having your engine sort of parse
this, you can create, depending on how many fields you have,
you can create very complex potential list of things in your game.
Whether they're entities, whether they're weapons, whether they're items,
abilities.
Anything you want to.
Whether they're levels, even.
Based solely on just data.
So this is a holy grail of design and development
when you get into especially very complex games like RPGs,
where you can have skills that have particle effects
and do different damage to different things.
And you have entities that are-- maybe you want them some to set on fire,
some to be electrocuted.
Maybe some you don't.
Maybe some melt when they touch something.
Create a bunch of flags, create a function
that parses this, and generates entities as a result.
And you just allow yourself an incredible boost in productivity.
No longer do you need to create a spider class, a ghost class, a bat class.
It's all unnecessary.
All you need to do is define what sort of attributes does a bat have?
What attributes does a ghost have?
What attributes does a skeleton have?
And anybody can therefore mod your game as a result of this.
All they need to do is know what attributes a potential entity can have.
And your design team is all the more productive as a result.
So that's a spiel on why this is relevant.
We've implemented a very basic version of this
just for the sake of modeling animations.
But we don't need a separate class for all the different entities in our game
world.
We just need, basically, and in the context of this game,
what are their animations?
So that's entity defs, that's how it's working if we're
looking at room.lua at line 48.
All its doing is getting animations.
This is, by the way, your function that takes in these definitions
and creates your entities.
They should be looking for--
looking into that.
They pull that definition and then just parse out
each individual relevant piece of data.
And then just construct some relevant information
or attach a relevant flag to that entity that your game engine can then later
parse.
The flammable example, if you do an attack and that attack is of type fire,
and it collides with an entity, and that entity.flammable is true,
that should trigger some behavior.
But you don't need to do anything terribly complex.
And you can assign this to any arbitrary entity thereafter.
So just a simple way of getting very complex behavior for your game
objects, entities, whatever you want.
Modeling them as data rather than thinking about it in terms of classes.
So that's that.
Does anybody have any questions just like sort of how that works,
or why it's viable?
Or why it's useful?
OK.
Couple of last things we'll look at really fast in room.
So online 82.
Generate objects.
So we did this very, very similarly last week.
Where we just had a game object class.
Notice that it takes itself a definition for a switch.
That definition is in gameobjects.lua.
Switch.
Here's a little bit more sort of data modeling something like a switch.
It's got a type of switch, frame is 2 by default. So width and height of 16.
It's not solid.
Player can walk over it.
Its default state is upressed.
So when it gets spawned into the game, it's unpressed.
And it's got two potential states, unprecedented and pressed.
And each of those states have a frame.
So all we need to do to render it is say, what's its current state?
Render this state.frame.
And then now, we don't need to do anything--
that's basically all we need to do in order to render it at the right point.
Your game engine, therefore, needs to look at your object,
and look at its state and look at its potential states, index into that state
and then pull the frame from that.
But it forwards you sort of infinite flexibility.
You can now have maybe of a glowing state,
and maybe there's a frame in that sprite sheet that allows it to glow.
And you could just change it to glowing whenever you want,
and it'll just render appropriately.
So very flexible approach to modeling data.
149 in room.
So we're almost done with the dungeon here.
So 149 is our update function.
All it does basically is iterate through everything
and update it, including basically all the entities.
So for every entity if its health is less than or equal to 0, it's dead.
If it's dead, then don't render it, which we see down in render.
But if it's not dead, and self.player player collides with it,
and the player's not vulnerable, then we should damage the player,
go invulnerable.
And if the player's health is zero, change to game over.
So notice how readable this is.
When you model your entities like this as well, all you really need to do
is just check for certain flags or functions,
and you can do arbitrarily complex tests like this.
Because entities can be dead, or not.
Because entities can be invulnerable, or not.
And then because entities are all xy, with height based things in our game,
and collide just expects them to have that.
All we just do is play sound, damage the player.
What does damage do offhand?
What do you think it does?
AUDIENCE: I'll take a wild guess and say subtracts from your health.
COLTON OGDEN: Exactly.
Just take a wild guess and say it subtracts from health.
It does.
So entity damage just takes in a number, and subtracts that from health.
That's all it does.
Go invulnerable.
What does that do.
AUDIENCE: Means you've been taking damage for that many seconds?
COLTON OGDEN: It does.
It prevents you from taking damage for that many seconds.
All that really does is just set a flag, which I've alluded to quite a bit
already, an arbitrary flag that you can model via data or some other means.
Every entity has an invulnerable flag.
And if it's invulnerable, then the engine should look for that,
and change the rendering, and the mechanics of the entity accordingly.
And then lastly here, just simple if the player health is equal to 0,
which it might be after we take sufficient damage, change to Game Over.
That's all it does.
And then for every object, just as we do with every entity, update the object
and if the player collides with it, then trigger its On Collide function.
And this can be a function that you arbitrarily create,
depending on the needs of your object.
If you look up here, for example, every time we create a room,
we generate objects in the room.
We insert into objects a switch, which we saw before, in gameobjects.lua.
And takes an x and a y.
In this case, we just make sure that the x and y
is a random number between the top left of the map
and the bottom right of the map, within the padding that it has.
We get a reference to that object.
And then we define are On Collide function.
So what we do is we say if it's upressed,
so recall that we have a state in the game object definition.
If it's unpressed, change it to pressed.
It's all we really need to do.
And then notice here, for k, a doorway.
So for every doorway in doorways, self.doorways, set that doorway open
is true.
And then play a door sound.
So our On Collide function just interacts
with other things in the game world, and just very simple things
but has a pretty interesting sort of mechanic.
Like we have a--
I took damage off the bat there, because my spawning isn't perfect.
But it changed from-- notice that it changed its state from unpressed
to pressed.
Because it went from the one sprite to--
I'll go into another room so we can see that.
I'll take some damage on the way.
Go to another room.
Notice the sprite.
It sort of looks like it's unpressed, right?
Because the game object's default state is unpressed.
And then On Collide triggers when I go on top of it.
It changes state, which has an effect on what gets rendered.
And it's On Collide function is called, which
opened up every doorway in the room.
So simple like 15 lines of code.
But pretty compelling, interesting behavior for the sake of our dungeon.
Like, it adds a lot, as simple as that is.
Now we sort of feel like we're interacting
with our game world a little bit.
So that's how that works.
And then lastly, rendering on line 188.
Just go through all of our tiles, render all the tiles.
We've seen this before.
Render all the doorways, render all the objects,
render all the entities, if they're not dead.
Right?
If they're dead, then don't render them.
And then this bit here, we'll take a look at the end, which is stenciling.
So notice that when I walk through doors.
Well first of all, notice that I can't walk through doors if they're closed,
which is important.
So when you collide with your doorway objects, if doorway.Open is false,
it shouldn't trigger the room switch, right?
But if I open the doors they're now all set to recall.
Door.open is now equal to true.
So they've changed their renderings, and now they're rendering open doorways.
If I walk through it, notice that it looks as if the player walks
underneath the tiles.
But I'm drawing the tiles before the player.
And one approach that you might think to do
when sort of creating this believable appearance of walking
through a doorway, is say OK.
I'll just render the player after I render before I render the doorways.
Right?
So render all the doorways last.
But it doesn't quite work out, because the sprite actually starts right here.
So what does somebody think is going to happen if I were
to draw the doorway after the player?
AUDIENCE: Your default player disappears if you cross that line or [INAUDIBLE]
over that line.
COLTON OGDEN: Exactly.
The player would disappear as soon as he gets to this little bit of line right
here, which is not very convincing.
And actually I'll try right now to take away the stenciling,
so we can see what that looks like.
So if I just take away the stenciling here, and then I just render.
Can't type today.
So I'm going to go ahead.
Do that.
Well, first of all, the rendering order is such that now the doorways render
after before the player.
So he just walks right over them, right?
So I'm just walking, I was walking over the walls.
That's not compelling.
And if I were to do something as simple as change the rendering order.
So right now, the doorways render first.
I'll just render the doorways after the player.
So right here, I took that out, right?
No.
So right now, they render before the player.
I want to render them after the player.
And I'm going to just run it.
And then going to step on the switch.
And then, yep.
Notice we get some weird behavior, too.
Like sees his head is getting cut off?
And other entities as well, right?
In that case, I couldn't really tell.
But yeah.
Very weird rending behavior.
And what we do to fix that is we create what's called a stencil.
So basically I'll try and get a screenshot here I can show.
First let me fix the changes that I just made.
Right.
And then if I go back into the code here.
So a stencil is just-- and we'll see a slide on this in a little bit.
But a stencil is just basically any sort of arbitrary shape that want,
that you draw onto the screen.
It's invisible, but it determines whatever
gets drawn on top of that stencil, it determines whether or not
that thing gets rendered.
So I have a stencil going basically from here onwards to the next room.
So right about where the doorway hits the archway,
because that's where we want the player's head
to look like it disappears.
Stencil going here.
I have a stencil going right here.
And right here.
And on the right side as well.
And what that does is I've set the stencil
to say whatever passes through this stencil during the sort
of stencil testing period which is, well all we do is we just
draw the player during that time.
But basically if it's on the stencil, don't render it.
And so it what that has the effect of is we still
draw the doorways before the player.
So the player walks in and he's drawn above this part.
But as soon as he hits the stencil, he's not drawn.
It just-- all those, basically what it does
is it draws the character to the stencil, and not to the actual canvas.
And so we get if you want convincing layered, weird, visual effects
like that, stenciling is an approach.
OK.
And then that's all this.
So this stencil function, love.graphics.stencil,
takes in a function.
This is what's going to run during the actual stenciling process.
It's going to draw--
I draw four rectangles.
All those rectangles are just those archways.
Right?
Perfectly layered over them such that it goes into the next room.
And then we do.
Notice that it says Replace, and then 1.
So we replace any pixel that gets drawn to that stencil
with the stencil value of 1.
And then we only draw things that are less than 1 during the stencil test.
Which means anything that didn't get ascribed to value of 1, which
means that if the player went over the stencil,
got a pixel value of 1, that's going to be false.
He's not going to be drawn to the screen.
Yes?
AUDIENCE: So you are stenciling, if you created a different shape,
you could use that, for say, like a light system, some areas are dark,
some areas are light?
COLTON OGDEN: Good question was if you have a stencil, because you
can do with an arbitrary shape, you could
create lighting systems where some areas are dark, and some areas are light.
I want to say possibly.
The thing is, I'm not 100% sure whether stenciling allows you to do,
whether love 2D stenciling allows you to do arbitrary numbers.
I basically am not sure if it's on or off, or a gradient.
Typically, if I were to do our lighting system like that,
I would probably draw a faux lighting system.
One, you can use a lighting kit, like box
2D lights, which does really compelling, cool lights for you.
Or sort of a cruder way to do it, but possibly realistic,
would be to draw a shape that fits whatever you want to be your darkness,
and then render it at an opacity that's less than 100%.
So you'd have a room, and let's say maybe to sides of it
are kind of shadowy.
You draw black rectangles there, right?
But instead of drawing those black rectangles at 255 alpha,
you draw them at 200 or 150, or something like that.
And so you can still see what's underneath them,
but it looks as if they are a shadow.
And you can use gradient effects to do the same thing.
If you want to have shadow that's darker going lighter,
I believe you can draw rectangles with a gradient effect in Love 2D.
I'd have to look into a little bit more.
But you would look to do something like that.
Draw a gradient of shadow via some rectangle, or some arbitrary shape,
to simulate lighting in that case.
And you could accomplish something similar to that.
And there are a lot of other crazy cool ways I've seeing lightning done in 2D.
But that's probably offhand the simplest way that I could think of doing it.
Cool.
So that's stenciling, and that's basically it
for the dungeon generation, which is arguably the most important part.
And also we looked earlier at the sort of transition mechanic.
And we'll look at the transition mechanic a little bit more
in a little bit.
But let's take a five minute for right now, and then come back to that
and see some more stuff.
All right.
Welcome back to Lecture 5, Legend of Zelda.
So before the break, we talked about a bunch
of different things, dungeon generation being foremost among them.
Now we'll actually start talking about things like hitboxes and hurtboxes,
as we can see here on the screen.
So a hitbox is a rectangle, basically.
And it's how we've implemented it in the distro.
But we can see here there's a few different sort
of rectangles overlapping.
We have the green rectangles here.
These are hurtboxes.
These are where you can get hurt.
And then this is a hitbox.
This is where you can hit something.
And so games, like especially with fighting games,
in a lot of games of fairly complex interactions,
and complex entities that have weapons or particles,
things like that all do damage.
You'll see a lot of complicated overlapping,
and arrangement of these rectangles that sort of bring about
how things interact with each other in terms of collision,
and doing damage, and affecting other entities.
In this case, he's doing an attack that goes from the left to the right.
It's a low attack.
Only his foot, essentially, a little bit inwards.
But essentially his foot does damage, whereas the rest of him is vulnerable.
If someone were to come up to him from up above and attack him,
it would do some damage.
And there's Minecraft, and it shows you how the same sort of principle
applies to 3D games as well.
In this case, what you see there are all hurtboxes.
Those are all aware things can get hurt.
Even though those are few of those things are items,
so that only really affects whether or not the player collides with them
and picks them up.
But that's basically the difference between hitboxes and hurtboxes.
The distro I think I accidentally called the file hurtbox.
So when I push it up, it's going to be renamed hitbox,
because that's what we use it for.
But what is offhand in this game, what do we need a hitbox for?
AUDIENCE: [INAUDIBLE] attacks from the swoord.
COLTON OGDEN: Attacks with a sword.
And the reason that we need a hitbox for that is why?
AUDIENCE: To [INAUDIBLE] the hit.
COLTON OGDEN: Yes.
And why can't we just use the player's position x-y width and height?
AUDIENCE: Because it has a direction as well.
COLTON OGDEN: Because it has a direction as well, and also it needs--
that's essentially its hurtbox.
Right?
So those two reasons.
It has that direction that we need to sort of act as its reference point
for generating a hit box, to inflict damage on other entities.
And we need to use the player's main hitbox
and hurtbox that it already has to see if something
hit it, the player, maybe from another side, or something like that.
So let's go ahead and take a look here.
The main bit of code that deals with the hitbox, in this case,
is one going to be hurtbox.
Should be hitbox.
But we can see this is literally just a rectangle class.
xy, a width and a height.
self.x, self.y, self-taught, self.width, self.height equals all those things.
That's all you need for a hitbox.
Literally box, just all you need are those fields.
And then you can do simply collides.
Just like you do entity.
Entity collides hurtbox.
Yes, no, true or false.
Because recall, collides expects xy width or height.
And it's defined an entity.lua.
So if we go up to the player states.
So recall, last week we introduced the idea
of having the player maintain its own collection of states,
in a state machine.
As opposed to just the game world having a state machine that
influences whether we're at the Start screen,
the Play State screen, whatever we want to divide our game up into.
We have entities states as well.
Now one of those is the-- so we have the idle in the walk state.
Those are so very similar to last week's, where the idle state, they're
just standing still.
And the walking state they're moving and their animation changes accordingly.
The swing sword state is a new state, and what this does
is the player presses spacebar.
It triggers this swing sword state.
There's a new animation.
So we go into the swing sword animation, relative to which direction we're in.
And we get that direction and then we calculate the xy width and height
of whatever our hurtbox.
Should be hitbox, is going to be for the sword.
When the sword hits something.
So that hitbox, if it's facing the left, it's
going to be 8 pixels wide by 16 tall, which
is roughly the left side of the player.
And then we just calculate the x and y depending on which position we're in.
It should be roughly centered based on whatever direction the player is
looking at.
And so we then instantiate that hurtbox here.
We call it a self.swordhurtbox.
And then all we really need to do is what?
In our game loop to check to see if we've hit an entity?
We just need to loop over the entities in our room, the current room right?
Here.
So every entity in the dungeon's current room.entities.
If the entity collides with our sword hurtbox, sword hitbox,
then, we saw this method before, entity damage one.
And then hit enemy play.
That's as simple as it is.
And recall, in room.Lua, if an entity's health dropped below one, dropped to 0,
it would just trigger it to become dead.
Right?
And so that's all we need effectively.
And then we have some additional logic to make sure
that the animation only plays one time.
And then once it has played one time, change its state to idle.
And then we can press spacebar repeatedly within that same state,
just to keep swinging if we want to.
And it will just restart the animation, and restart
the hitbox being instantiated.
And so that's all we really need to do for that.
Now I have some lines of code here at the bottom of the sword swing
state.lua.
And player swords, swings swords state.
And what these do is sometimes it's useful
when you're programming to sort of see where your collision boxes are, right?
Because you don't necessarily know if everything's lined up perfectly.
When you're detecting collision between different entities,
you want to just check to see are the rectangles actually overlapping
when this triggers?
So all you need to do in order to visually see this at a glance,
and you may have seen this before, in other games or debug modes of games,
is just draw a line rectangles.
And then just give them the xy width and height that you want to look at, right?
So here, I'm going to go ahead and save this.
And then I'm going to run the--
I've basically uncommented it.
Because by default, I don't want it showing.
They're pink rectangles, I don't want them displayed on screen,
unless I want to do debugging.
So I'm going to uncomment them down here.
Notice it sets the color to 255, 0 255.
That's magenta.
I'm going to run this.
[MUSIC PLAYING]
Turn that down a little bit.
Nothing looks particularly different.
But when I swing my sword, notice that there's a little rectangle.
And the rectangles aren't 100% perfect necessarily.
They give the player little bit of an advantage.
Like up above, like his hitbox is barely anything.
Up above, in the actual animation, but in the collision,
I give it quite a wide berth.
And so that's how you can, at a glance, sort of
see whether your things are interacting appropriately.
There's lines of code.
I did the same thing for the other states, the entity, walking,
and idle states.
If you want to sort of look at those, and see not only the player
swinging the sword, but also the other entities, and the collision boxes they
have, and just to check whether or not they're overlapping appropriately,
or to change them, or do whatever you want with them.
But that's a nice way to do--
sometimes it's hard to debug collision if you
can't see exactly what's going on.
Because it's often just in terms of xy width and height, so on and so forth,
with offsets and such.
It can be kind of a pain and/or sort of difficult
to track down certain bugs that way.
So just draw to the screen.
You can apply that same logic to a lot of things in your game world
that may be hidden, but you want to see sort of visually just draw shapes
or draw different things for them.
So you can see what's going on.
So that's how the hitbox and hurtbox work for our player.
And you can easily just spawn more hitboxes for other entities
if you want to.
If you wanted to give them states, or maybe
you want to create projectiles that are offensive.
Maybe just create projectiles, and then shoot projectiles
in a specific direction, but assign a hitbox to that projectile,
if you want to.
Or just use the projectiles xy width and height,
and then you can calculate a collision that way.
Just treat it like a game object, or an entity.
It's up to you.
Or a separate class all together.
But that's hitboxes and hurtboxes in a nutshell.
Anybody have any questions as to how those sort of work?
OK, cool.
So let's go on to the next topic.
So events.
So I spoke of this earlier.
Events are just a nice way to sort of say when something
happens, do this block of code.
And you can do this anywhere.
And you can decouple it from like maybe two different objects
interact with each other.
But you don't want them to sort of pass references
back and forth between each other, and bloat your code.
Maybe you want the code for that check to happen not inside your main loop.
You want to sort of abstract it out to some sort of other function,
like instantiated events.
And then have your main rendering and update logic
be free of all this conditional stuff.
So you create event.
You say on some event, so maybe like on player walk,
and then you just update some, maybe a label
on the top right of the screen, that gives the player's xy.
And you just say that xy is equal to that player's xy.
I mean, aside from the fact that you could just literally
draw the player's xy.
But it's the same principle.
You can update some value somewhere separate from another entity.
And then just wait for an event, arbitrarily defined event,
that you then broadcast later.
So let's say I want a broadcast swing sword.
I have a swing sword event.
So whenever the player presses space, not only
do they do all the code that we saw before, but they swing sword.
And then we say when you swing sword, pass in the xy of wherever they swung
the sword.
And so then you can say OK, on swing sword, look at all the entities,
and see whether or not the hitbox at xy collides with them.
And you can sort of take out the logic from where you had it before, and put
it in some other centralized location.
If you want to.
More representative of this model is the idea of an achievement system,
where instead of every frame, and you can
have-- games have a ridiculous number of achievements.
Some games have like 1,000 achievements.
You don't want to put the test for every one of those 1,000
achievements necessarily inside of your update logic, right?
You can instead just broadcast an event for all
of the different things that influence whether those achievements are met.
So whether a player needs to get 100 kills in a game, or 100 coins.
Whether they need to jump off some ledge.
You just have events that model all these interactions.
Event on, pick-up coin, do this.
Increment some time, some counter that's stored somewhere else.
Event.on, kill creature.
Or kill creature triggers every time you,
literally a creature is sent from as false to dead as true.
Maybe whenever the player jumps, you do an on jump function.
And then you can test to see whether or not-- in that code,
you can test to see whether they jumped off that ledge.
And then if that happens, you have your achievement system,
but you don't have all these if statements,
and all these tests happening inside your sort of imperative game loop.
You can just pseudo asynchronously check for all of them,
given a sufficiently detailed event system in your game.
And so the library that we'll use just to show this a little bit
is in the knife library, that we saw before with timer.
Timer.on, timer.every, timer.tween.
In the knife library, there's a submodule
called Event, which allows you to call event.on.
Give it a name and a function.
So on player jump, off player jump.
You know?
Function.
If Player.Position is by some cliff, or some area that's relevant,
then call this block of code.
And increment, or set the achievement to true.
Event.dispatch.
Dispatch an event when something happens.
So if a player jumps, so when they press the spacebar, event.Dispatch jump.
And then just whatever values that your callback function needs.
Let's say you want to check to see what is on the cliff.
Well, if you want to check to see whether they jump off
that specific cliff, you need to check their xy position, presumably,
so you can just pass in optional parameters via event.dispatch.
You can say OK, dispatch the event that the player jumped,
and say that the player jumped at xy.
And so this function call back is going to have access to the xy.
It's going to say OK, on jump, I say I got,
I'm getting my callback called with player.x player.Y. And it happens to--
oh, it's not at the cliff.
So nothing happens.
Or it is the cliff, so the achievement, jump off cliff, unlocked is true.
And test now is not happening every single frame,
which otherwise would potentially.
And it's not blocking up your update logic for your player jumping.
Right?
The player, the jumping logic inside player, the player jumps state,
doesn't need to know--
doesn't need to ask whether or not we've jumped off a cliff.
That's something that we should just delegate to our achievement file,
or whatever you want.
And that's sort of the flexibility that events afford you.
So we use event.
I'll touch on this a little bit briefly here.
But basically, if we go to the player walk state on line 21.39, first of all
we're checking for input here.
Right?
We're just saying if we press the left, right, up, or down.
The direction is that walk left, walk right, so on and so forth.
And then what we do is we call entity walk state.update.
So this base walk state code that exists in entity, so that we can
let every single entity have this code.
All it does just check to see whether or not
the player or entity that it belongs to hit a wall.
And if it did, then set there bumped equals true.
And what it does is just a flag that says, OK, the player bumped a wall.
OK?
Why is that relevant?
Because then, after we call that, we can say, OK, if we bumped a wall,
and this only happens when the player walk state,
if we're looking to the left, temporarily adjust our position.
Because when it bumps you, it knocks you back out back into the world.
But we're going to re-bump our position into wherever we collided.
We're going to look at every doorway, and then
we're going to say if we collided with that doorway, and it's open,
then we're going to shift it to the center of the doorway
so that it doesn't look as if the player is walking through the wall.
We're going to set it to wherever the wall is,
or wherever the door is, the very center of the doorway.
And then we're going to call event.Dispatch shift,
and in that direction.
So shift left, shift right, shift up, and shift down.
And so all of these four different cases are going to say OK,
the player has hit a doorway.
I have a function somewhere.
Event.on, shift down, shift left, shift right, shift up.
That's going to trigger what?
The next room.
It's going to trigger us moving to the next room, and also the next room
spawning, and the camera sort of shifting position.
So the code for this is in dungeon.
So we're going to go ahead and look at dungeon.
So right here, on line 29 through 43, event.on,
shift left, shift right, shift up, shift down.
And what that does is we have another function called begin shifting,
which takes in an x and y.
Negative virtual width, virtual width, negative virtual height,
virtual height.
Does anybody recall why we need to pass those numbers in there?
If we're shifting left, and we're passing a negative virtual width,
what do you think that number is going to be?
So recall every room has an adjacent offset x and y
that renders that room with that offset basically added to it.
Its x and y.
So if we begin shifting to the left, negative virtual width
is going to be the x offset.
Because that room needs to be rendered a screen width to the left, right?
And so on for the right, virtual width.
It adds a virtual width to the x offset.
The adjacent x offset.
And then virtual height, negative and positive for shift up, and shift down.
So begin shifting, basically the gist of this is it's a tween operation.
So we start here, recall timer.tween, just takes a value and interpolates it.
So self, we have a camera and a camera y, right?
Going to equal shift x and shift y.
Eventually.
So shift x and shift y being the adjacent offset, that we're
going to need to shift the camera by.
So camera x and camera y, they both sort of zero, always.
When the room gets completely finalized.
So if the shift x is virtual width, negative virtual width,
cause we're shifting left, then it's going
to tween the camera going from right to left over the span of a second, right?
And then the player, the player x and player y,
those are basically set up here to the opposite end
of the room, the next room.
So if the player is going from the top of the room
to the bottom of the next room, it needs to get its x and y
put in that position, the bottom part of the next room.
Or the left part of the next room, if they're moving from the right, right?
Or the right part of they're moving from the left.
And the bottom part if they're moving up.
And the up part if they're moving down.
So that's all that does.
Puts them in the right position, on the next room.
Tweens both of them, the camera and the player,
because the player needs to keep moving to the next room.
And the camera needs to shift up.
When it's finished, we call finished shifting,
which is just a function that says basically everything to 0.
It sets the current room to the next room.
It sets the camera x and all that stuff all to 0,
and then all the entities and objects there, it all gets set to 0.
So they are offset to 0.
So that everything is basically right at 0.0
again, just like it was at the very beginning.
And then last but not least, when we get into the new room,
we start with the doors all open.
And then when we get into the room here, because this is all
done in the finished function, recall, this
will only happen after the tween is completed.
Every doorway is going to get set to false.
Open equals false.
So all the doors will close as soon as we get into the next room,
after we've performed the tween operation.
And then we'll play a door sound.
So that's the code involved in actually doing the shift operation.
So we were able to trigger it.
We don't have to necessarily do that code from the player walk state.
We just say if the players collide with the doorway,
then dispatch an event that is handled by dungeon.
Because dungeon has access to current room, next room, and all the stuff
that it needs to actually perform that transition.
So we have the two that are sort of decoupled but they work well
in tandem with one another.
So anybody have questions as to sort of how this works?
OK.
That's essentially the dungeon scrolling.
It's a tween operation.
It's a camera, a temporary room gets rendered.
We move to a temporary room, everything gets set back to 0.
And then it's just a wild loop of that sort of behavior, effectively.
And we use the event library to sort of clean up that whole aspect.
And sort of screen scrolling here, just another demonstration.
We went over it in detail, but this is a nice GIF
I found that sort of encapsulates what that means.
Stenciling is something that we saw earlier.
And if we look at this as a stencil, so we
can pretend that these are the circle of the heart,
and that rectangle are a stencil.
And then we want to draw this gray rectangle on top of those stencils.
We can use the stencil to mask out specific parts, which
would otherwise be pretty tough to do.
I mean, it's hard to necessarily, maybe individually
render pixels, or create a shape that sort of looks like this.
Much easier for a lot of very interesting visual effects
to use a stencil of arbitrary shape and size.
And you can use images, too.
You can use images of stencils, to create some pretty cool effects.
We went over it before, when I talked about the archway.
So I won't go over into much more detail.
But again, you can look at that in the, where was it exactly?
It was in the room function, room class, sorry.
Down here at the bottom.
So lines to 213 to 229 recall they're a stencil function.
The couple functions in particular that you need to use
are love.graphics.stencil which draws the stencils, and then
love.graphics.setstenciltest which compares
the stencil values to some value, and draws them
whether or not that condition is true.
So we have our function here, which draws four rectangles.
Those are all the archways.
It replaces any pixels that get drawn to those rectangles with the value 1.
The stencil value 1, so stencil value is just
like a hidden value that determines whether a image will get
drawn if it's being stenciled or not.
Set stencil test means that we're looking for anything less than 1.
And we'll draw that.
And because the player is going to be 1 if it's
on any of those stencil rectangles, it will not
draw the player if it overlaps with those stencil rectangles.
And so that's in general how it works.
And you can do, there's a lot different comparisons.
You can do greater than 1.
You can do iterative stenciling so that you can increment of value.
You can have simple values go between 0 and 255.
There's a lot of interesting effects you can get with that.
But the general use that we just saw is set the values to one,
and check to see whether they're less than 1, if you want to draw them.
If you want to not draw them.
Does anybody have questions how stenciling works,
and how these functions work?
Yeah?
AUDIENCE: So like having stat stencil cover up the door?
So when you draw the player, you won't actually
draw on the stencil, [INAUDIBLE].
COLTON OGDEN: Yes.
So it's like having the stencil rectangles go over the doorway,
so that the player will not get drawn when it's underneath them.
If I go back to a slide that has it.
The stencils are here.
Right?
I had to go through and figure out the exact xy that I needed for them.
And there is a--
what you can do is you can take that stencil function that
draws the rectangles, and you can just take it out of the stencil test,
and see.
You will actually be able to see where the exact rectangles are.
Because then it will just literally draw the rectangles.
But here, basically right between here and where it would be on the next room.
So that when we go from here over into the next room, it's still
stencils them.
Right here, the player is going to go over them.
It's going to set its stencil value to 1.
It's checking for anything less than 1 to render.
So it's going to fail the test, and it's going
to not draw that player at that point.
So it's effectively just masking out the player,
because we're assigning it to the right stencil value,
and we're doing the right comparison for it.
But yeah, that's effectively it.
Just using it to, because we were in a situation
where our tiles didn't cleanly lend themselves to drawing over
the player, which you could do.
You could draw the shadow part underneath the archway part,
and then just change the rendering order.
But the spreadsheet that we had didn't afford us that flexibility.
So we went with a little more interesting stenciling approach
to getting it done.
We looked at this earlier.
So game design via data, rather than a bunch of logic.
So the more you can sort of get towards modeling your anything in your game
like data, like this, the more flexible your engine is,
the easier it is for people to mod your game.
Which is hugely important.
And the easier it is for you to let designers
take charge of actually creating assets for your game.
So here we've created an arbitrary collection of data,
and you can get as complex with this as you want to.
The only thing that really matters is whether your game engine
supports those fields, and acts on those fields accordingly.
But once it does, I mean, the more things
you add, the more possibilities you have for adding new things.
So goblin, we have health, strength.
So strength should be accounted for in our game engine.
And this is, we're talking in just the general abstract sense now.
Let's say we're like making an RPG or something.
Or maybe this is part of our Zelda game.
If we got a little bit more detailed with it.
But goblin is going to be a creature that has 10 health.
It's got two strength, so these values will
get loaded when it's created as an entity,
and strength is some value that our engine
needs to apply to anything that it decides to attack.
Right?
You could do the same thing with defense.
You could create arbitrary things.
It's got a goblin texture.
So that's the sprite.
We've defined some animations here, which
you've already seen how to instantiate those in our game engine.
Maybe it starts off with a club weapon.
And you can also reference other sort of Lua files
that have these definitions, if you have maybe a weapons defs file.
You could index into weapons defs club, and then that club item
will maybe have a bunch of its own characteristics
that when your game engine is loading this goblin def,
it loads in the club def as well, and instantiates a bunch of qualities
onto that entity, or that item, that then have
behavior relevant to your game engine.
Aggressive is true.
So if something is entity.Aggressive, then maybe it's AI
will seek out the player.
Right?
And you can set it to false.
So now you can differentiate between entities that chase the player,
and entities that are sort of docile, and just do their own random thing.
Sleeps at night.
Maybe your game has a day/night cycle.
Maybe some entities stay awake, and some go into a sleep state, right?
So it will look and say OK, it's nighttime,
does the entity sleep at night?
If it does, OK.
Then entity change state, sleep, right?
And then during the day, when the day happens,
you'll do the same thing in reverse.
You'll say if the entity sleeps at night,
then you say entity.awake is true.
Or whatever it is.
Change state awake, or idle, or walking.
And then flammable.
Maybe you have weapons that shoot damage, or shoot fire.
Or maybe you have torches that shoot fire, or some--
maybe you're in a level that's like lava.
And you know, if you touch one of the tiles, it has a flammable effect.
And if it's flammable, may be your status equals flammable.
You take damage over time.
And you change your animation.
Maybe you'd get tinted red, or something like that.
But thinking about all these different possibilities,
like what your engine is capable of, you can create pretty much arbitrarily
complex data structures and entities, and items,
and whatever you want in your game, without having
to create a goblin class.
Or a skeleton class.
Or a arbitrarily complex creature class.
You just compose your creature with attributes and model
its behavior that way.
Composition over inheritance, which we talked about last week,
is very valuable in game design.
And that's what Unity's game engine sort of approaches.
Because entity is a purely entity component system,
whereby entities are modeled as collections of components
that each do something.
And we can sort of almost think of these as components, too.
Albeit in a much simpler sort of representation.
Does anybody have questions of this, or like why it's useful,
or anything like that?
OK.
So the last thing I wanted to talk about today, and I saw one or two students
were mentioning how they wished they knew, or wish we
talked about a little bit more how programming was done back in the day.
So I'm not an expert, necessarily, in programming 6502 assembly,
which is what the NES is written in.
There are a lot of people that are very good at it.
But there's a few links here.
So in the slides, if you wanted to look at the slides on the website,
Homebrew is actually quite a popular thing amongst certain communities
online.
And what it is essentially getting compilers
that will compile source code for some arbitrary processor, or development
environment from some arbitrary computer system.
Like the NES, which was a 8-bit microprocessor based machine
with its own set of interesting hardware.
It allowed you to compile the assembly for that
and actually run it via a program called an emulator.
And an emulator just allows you to run ROM images, or just arbitrary data
that maps to the machine instructions of some system
that the emulator has emulated via virtual machine.
And so if you wanted to look into it a little bit more in detail,
there's a couple of links here.
So NES.wiki has got a bunch of great links.
There's a Programming Guide that kind of goes
over the basics of how to do some 6502 assembly as well as with the NES,
and its particular hardware.
And then the compiler that most people use for this is called CC 65,
which is a 65 02 assembly compiler.
And so this is an example of some source code.
It's a little bit tough to read.
But I can't zoom in, I don't think.
But the gist of it is you have a lot of--
and here, actually I could pull it up online.
Le me try.
Super Mario disassembly.
So go here.
This is on Github.
So somebody decompiled, basically, or disassembled rather,
took the bytes that represent all the machine code and the ROM image of Super
Mario Bros and then just converted it back to assembly language,
and then added comments to it.
Because I won't go into too much detail about what assembly is,
and how it works, but essentially it's just
a layer directly above machine code where the keywords
map to essentially what are bytes in memory
that are actually program instructions.
And that influence the behavior of your CPU such that you get programs.
And that's what c is layered on top of, an in every language thereof,
thereafter.
So if I zoom in here, see if I can.
It's being a little slow.
But it's letting me--
so here we can see.
I think it's lagging because the file is just so large.
It's like 14,000 lines of code.
Because the thing about assembly is assembly is a very, very long.
Because there are so many--
you need to have, just like really slowly.
Zooming in infinitely.
There are just so many steps you need to do in order
to achieve the same thing that you get in a high level language like C,
or even languages higher than that.
Taking a lot of things for granted, especially things like loading
registers.
So every CPU has got a series of registers that can store values,
and then the CPU has logic that you can basically
say OK, if the value in register a is greater than or equal to some value,
then branch to this sequence of the assembly code.
And often you'll have to do things like and/or operations on byte level,
just on bytes.
Like right here, we can see we're ending the value 3 on whatever
is in register a.
Like I said, don't intend to get too much into what assembly is.
But for those curious as to how games were implemented in the '80s, and even
the early '90s, it was all in assembly language.
Because assembly language is you're literally
programming the CPU of whatever machine you're trying to program.
And that's how you get as much efficiency as possible.
If you know your computing, environment you know what your CPU is capable of.
The thing about compiling a language like C, or C++, or Java,
which is even higher of an abstraction than those,
is you're allowing sort of algorithms to do the work for you.
You're allowing algorithms to take your source code
and turn it into this, basically.
A layer lower than this, effectively.
This is an intermediary level that it does generate.
But suffice to say, in the '80s and '90s,
C compilers were not as good as humans were in creating games,
especially to do a lot of the tricky things that they needed to do,
in order to get them working efficiently on processors that were, at the time,
one to three megahertz in speed.
Which now we have like three gigahertz processors, and it's no big deal.
But that is effectively, that's sort of a window
into what it was like developing games in the '80s and '90s.
And then in the '90s, with like things like N64 Playstation 1,
going onto Playstation 2, and so forth, it was typically done in a language
like C or C++.
Sometimes with some variants.
Certain consoles, like the PS3 has a notoriously difficult graphics
processor to program.
So a lot of PS3 three teams needed to program in assembly,
even at the time, which was 2007, 2009.
But that's, in a nutshell, what it's like programming the 65 02.
So if you want, those links up above, it's
a pretty enlightening experience actually digging
into that sort of thing, and trying to make sense of what the assembly does.
It's quite difficult, and quite burdensome because of how long
these programs are, and just how minute each individual instruction is.
Like just checking, just loading a value into a register,
just to do a loop often will just be iteratively loading a value
into a register.
And then like performing some operation, or calculation, off of that.
And then branching to some other loop of code.
A lot of that can be condensed into like just a couple of lines of C,
or even fewer lines of Lua.
So a lot more of a burden but there is insight.
Definitely some insight into digging a little deeper
and sort of looking there.
So once again, those are the links on the NES dev wiki.
Wiki.NESdev.com if you're interested in looking at that.
In Assignment 5, like you did in Assignment 4,
you can create a game object that's consumable.
And feel free to use code from Assignment 4 to do this.
When like you define your on consume function,
if you're going to adopt the same model as the last assignment, then any ideas
as to what we need to do?
Probably just add the opposite of damage the entity, right?
We want to add hearts.
We want to add HP to it.
And you'll see in the distro that health is modeled as an even number,
because every one digit is half a heart.
Yes.
AUDIENCE: Can you give a negative number to [INAUDIBLE]??
COLTON OGDEN: You can.
And that will effectively be the same thing, too.
So.
The second part is including pots.
So there are pots in the sprite sheet.
So these should be solid.
So when the player interacts with them, he
should be bumped out of where he was.
Allow the player to lift them.
So there's an animation in the sprite sheet that will actually allow--
that shows the player lifting up the pot.
So you probably need a new state, player lift pot state.
Walk with pot state.
Whatever you want to define it as.
But you'll need basically to have those two in order to get this to work.
What's one thing that we'll need to do in order for the player
to lift the pot.
Any ideas?
Besides the rendering aspect of it, when the player walks around, for example,
what needs to happen?
If he's holding the pot.
AUDIENCE: He's slower.
COLTON OGDEN: You can make him slower.
Yeah, that's possible.
That's not required for the assignment.
More fundamental than that.
The pot needs to track the player's location, right?
It needs to be relative to the player's location.
So keep the pot probably above the player by some amount.
I won't be too picky as to how you implement that.
But the pot needs to track the player.
Which is this next point, actually.
And the walking animations, like I said, should
change while they're carrying it.
Allow the player, lastly, to throw the pot.
So when you throw the pot, basically turn the pot into a projectile.
If it hits one of the walls, break it.
You can either just despawn it instantly, or have some animation.
It's up to you.
But just, if it hits a wall, despawn it.
Trigger it to-- maybe just trigger it to some values, false.
Render is false, whatever you want to do.
If it hits an enemy, it should damage the enemy by one,
just like we've seen already.
And if it travels farther than four tiles,
in addition to also hitting one of the walls, then destroy it too.
So those are the main components for Assignment 5.
And the ways that which you achieve most of it's fairly flexible.
But yeah, that's Lecture 5, Legend of Zelda.
So thanks for coming.
I'll see you next time.