Transcripts
1. Course Introduction: Hey, everybody, Chris, here. And in this video, I've
got something really cool. I'm excited to
share with you all. It's my new GIDoGame
development course for creating a
survivor like game. So we've got a lot
of content in there. I mean, it's over 60 lessons and almost 12 hours of video. By far, it is the longest, most complete game development
course I've ever put out. So if you've ever thought about making your own fun
survivors Light game, that's exactly what
we're going to be doing. We'll build it up step by
step, right from scratch. We'll be using the
GidOEngine version 4.4, which you probably already
know is free and open source. So we'll code everything
in GD script. Which is the language
specifically made for the Gadot editor. It's beginner friendly,
it's easy to read, and it's tightly integrated
into the ado engine. So it works fantastically for doing game
prototypes like this. So whether you're new to game development or you've already done a bit of coding and you just really want to solidify your skills with a new
interesting project, I think this course is
going to be for you. We'll start simple,
getting comfortable with essentials like top
down character movement. Then we'll really
start to ramp it up, building out to some
genuinely complex concepts. You'll be putting
in character stats, leveling systems, item pickup we'll even build out
a spawning system that's going to position all of your enemies off screen so they can swarm the player
from all sides. So in a nutshell,
we'll be getting really hands on with the
Godos two D scene system. That's going to cover
everything else from projectiles to creating
polished user interfaces. By the end of the
course, you'll have a fully playable survivors like game prototype seen
in the background and all of the knowledge you need to make it into a bigger project. So what do you say?
Are you ready to turn those game ideas
into something real? Come join me in the
course, and let's build something pretty
incredible together.
2. Godot Project Setup: Hello, everybody, Chris
here, in this video, we'll get started
with the first step of our Survivors Like and GD Script project by
downloading the GdoEngine. If you don't already
know, Gadot is a free open source
game engine that has its own primary language, GD Script written
specifically for Gdo, which is what we're going to
be using to code the course. It's a scripting language. You can, of course,
use other languages in Gdo like C Sharp, which has strong built in support with a
Mnoversion of Gudo. Most of the tutorials you
find are going to be in GDScript because GD Script is
quick to get started with. It has really great integration
with the Editor EUI. It's easy to use, and
you don't have to compile your scripts every time you want to
make changes to it. So as far as simplicity, especially for small projects, GD Script has a
lot going for it. So looking at the Gado website, I'm going to be using the
current release version of Gado 4.4 0.1. Specifically going
to be downloading the non mono version of it, which means it does not have
C Sharp support because the GD Script only version does allow you to export to web. But if you use the Gado Mono, that feature is not supported. So Gadot 4.5 is already
coming down the pipeline, and there are a couple features that I think are pretty
cool coming out. But because we are
trying to keep this to beginners intermediate
level course, I don't want to overwhelm anybody with all those concepts. So let's go ahead and click
Download and grab 4.4 0.1, and we want the blue version
here. It's non Mono. This is going to be
for GD Script only. If you did want C sharp,
you can grab.net. We won't need that for this
version of the project. I might create a C Sharp
version of the course later if it makes sense.
We'll see how things go. So you're going to
want to save this install it and then
open the GADoEditor. So once you open it, you should be hit with the project manager, which will give you a list
of your current projects. You probably don't
have much here. There might be a few demos. But what we're going
to want to do is create our new project. So let's create a new project, and you can give this
any name you want. I'm going to call it script
Survivors subtle name, I know, because it's GD script, and it's a survivor style. So I'll put this in my
tutorials directory, which I have
specifically set up for these kind of long
form tutorials. So let's set that as
the current folder, and we should see something
like project name here and the path to
the full project. Because I made Script
survivors A one word, it didn't actually put
a separator down here. So I'll just use a dash as the default separator
for spaces. And we're going to want the
renderer in mobile mode. This is a pixel art course, so we really don't need the
Ford renderer for any reason, and we'll just go ahead
and create with that. So once your
project's booted up, you should see the
three D view port here. One of the cool things
about Gadot is that it has two completely separate workflows for two D and three D, but it does support
both of them. Unlike Unity, they're
completely separate entities. So it's not faking two
D in a three D system. It's actually completely
two D. And if you'll look over on the left
in this scene window, you'll see that there's well, really four types of nodes here, two D scenes, three D
scenes, user interface. And then if we click other node, there's the base
class here node, which has no 2d3d
components to it. So it's no position on anywhere, you would use that for
stuff like systems, background information
processing, not objects actually
in the game world. For everything that
is in the game world, generally, we're going to
be using a two D scene. So that's going to
be a blue node icon over here, just as
the difference. And then user interface
are the control nodes. So that is marked by green, anything that would be
an on screen button, floating combat text,
UI pop up windows, like when we're doing
rewards selection for what you can get
for leveling up. Those are all user
interface nodes. So, of course, as we
go, I'll explain more about what a node actually is. For right now, let's kind of cover the interface
a little more. So we have the file system in the bottom left hand corner. That's where you're
going to have all of your files in Gdo including any imported assets,
your SEN files, which is where you take
one or more nodes, and you save that to a scene, which you can create copies
of anywhere in your game. So it's basically a
template you can reuse. And then other Gdo
and non Gudo files like resources as well. So in the Viewport here, you
have your X, Y access here. One distinguishing
point is down is actually the positive
direction for the Y axis. So if you're talking
about moving upwards, you want to go negative Y, and if you want to move
down on the screen, you'd be talking about
a positive Y value. Right is positive X, and then left is negative X. So that will be
really relevant when we're moving our characters
around the screen. You want to make sure they move in the right direction and that you're getting the input for the right direction as well. If you look really closely
around the 1,200 pixel point, at least for me, there'll be the bounds of our
user interface. So the user interface is usually on its own
separate canvas layer. So it's a completely
separate thing from the two D
game world itself. You could think of the UI user interface as kind
of layered on top of the camera as it's
viewing the two D world. So whenever the camera moves, all the UI elements are
going to move as well. But the game world objects don't move with
the camera generally. So if you move the camera, the UI will stay in place
and everything under it, the characters, the enemies, those don't move
with the camera. And that's one of the biggest distinguishing
factors between the two D nodes and the user
interface control nodes. Anything that's a UI element
is a control node in Gadot. Over on the right, we
have the inspector which is used for defining any properties about your nodes. When you're writing scripts, you can give it a at export tag, which marks it as editable inside
of the inspector. That's really important. We'll be doing a lot of that. So you'll edit that over here. Of course, there's other Windows that have varying importance. I'll cover those in the
later videos as well. So in the next video, we'll
get set up with our project. We'll import all of the
art assets that we need, including audio,
music, art, fonts. And then there's also
a couple plugins I think would be extremely
useful for the project, the free to download and use. So we'll cover all that
in the second video.
3. Art, Sound, and Plugins ~ Import & Install: So in this video, we're
going to continue with our project setup by
getting all the assets and audio and plugins all set up into the projects that we're
good to go to start working. So over on PaySPN, I have all of the links to the assets
we need for the course, everything I used in the
pre recording project for script survivors. So if you follow the link up here at the top,
I'll zoom in on that. You can basically just go
to each of these links, and you only need the free
versions of each asset. I have to share it this
way to respect the no redistribution rule a
lot of these assets have, but you basically only
need to follow the links to each store page and
grab the free version. So, first off, Pixel Crawler has our character based model we'll use to set things up some
weapons and enemies. It's really core
to this tutorial, so that's what we actually
base a lot of the Alpha. So next up Raven fantasy icons. For UI elements, Crimson
Fantasy user interface. The font, I'm going
to be using m5x7. But honestly, you can
just pick whichever pixel art font you
think looks good. There's a lot on
itch dot IO as well. For some basic game
audio sound effects, Leo passes RPG
Essentials SFX free. And then for the
music, The Red Hearts prepared to Dev by Tall Beard is absolutely perfect
chip tune music for the style of
Survivors Light game. It actually works
really, really well. Okay, so you download
all the zip files, and then what you can do is right click in the file system, create a new folder for art. I'm going to put all
the visuals in here. And then right click, create
a new folder for audio, anything that's at Sound effects or music, I'm going to
be putting in there. And then to quickly open it
in Windows File Explore, or the equivalent if
you're on Max or Linux, is to right click and do
Open and File Manager. Okay. And then you just
want to extract all the files into
that art folder. So here we have the
Crimson fantasy GUI, we have the fantasy icons, and then the Pixel
crawler free pack. The only exception is that
for the fantasy icons, there's like three or four
different versions of it. I was just using the
full sprite sheets. These are easiest to work
with in Gadot because you can create a atlas
texture resource, and then just pick the icon
you want to select for that. For each of the items that you're going to be
assigning an icon to, I just find it much
easier to work that way. And also For Gadot, if it's importing
every icon separately, which is an option
with that pack, it does take a long time to actually load into the engine. So when you have your images packed into one file like this, it's just much more efficient. I think it's the way to
go. Aside from that, I think I was just bringing
in the entire directory. I don't change the file
names or anything like that. Typically, in Gadot,
you would go with snake casing for your
directory folder names, but I think it's just
simplest to just use what the artists provided
here and just go with that. So it shouldn't cause any
problems or anything. Just maybe a little
on optimized, but that's perfectly fine. And then for the audio
of the sound effects, if you open the zip, we'll
just come like this. And the music, I was only taking the three Red
Heearts box jump, which is the first music track. You can actually just select whichever music track you want to use for your game level, but I think this one works
really, really nicely. You know, let's just go ahead
and preview for a minute. Yeah, so absolutely killer track for survivor
style games, I think. So once you import everything,
you put it in your files. If you reopen the Gado window, you should see everything import into these folders properly, and then it should display
over here on the left. So you might get some
error messages when you're importing things
in the first time. You can just confirm that
the files are there, and that's the main
important thing here. I think these messages can
kind of be disregarded. Drop Stable doesn't even have anything to do
with that project. It might be related to the
caching on my computer. But what's important
is we have the files in here. You can
double click on them. You can see that it's
being recognized, like the music there and
sound effects over here. So it looks like we're good. So go ahead and clear that logo. So we need two plugins
for the course. One, I think it's just
a huge quality of life when you're navigating
the file system over here. It's called collapse
folders because you might notice that when you open a
bunch of folders like this, there's really not easy way to collapse or fold all of them back down so that you can see your full file system
here in one window. So what you want to do is
go to the Asset Lib up here at the top and then
search for collapse. So click on the search
bar type collapse. And then the one we
want over here is collapse folders.
So download that. Install it Okay, I'll say
installed successfully. Now we want to go to
Project in the top left, project settings and go over
to the plugins tab here. Check enabled, hit Close. And what that does is it creates this little icon over here
where you can collapse all, and you can have
all of the folders, go back to the root. Nothing's expanded, and it's much easier to navigate
the file system with that. I think that's just a
really handy button. It should be there in
Gadot basic, honestly. So the other one is a much
more involved plug in, and we're going to be using
it for state machines in Gadot It is also used
for behavior trees, but because the AI is very
simple and a survivor game, we're only going to be sticking
with state machines here. There's no need to make it more complex with a behavior tree. So we are looking for Limbo AI, and because I'm in 4.4, I'm going to be using
Limbo AI 4.4, of course. You might notice that
currently here, at least, there's no 4.5 out yet
because, well, I mean, 4.5 still in Dev, so it may work if you decide to use the Dv version or you may run into some issues. It probably will work, but, you know, there might
be a bug here or there. So, you know, another reason to stick to the
stable version of Gado. So let's download this, and this is going to be
our state machine plug in. We can install it. It'll take a little
longer because this is a pretty big plug in. I'm going to disregard
these messages once again. That might actually be happening because the project
name is the same, even though the
directory is different, so I'll just change the project name so that that
stuff can stop showing up. Hopefully, this will
fix. So I'll just change the name and give it the name GD script there.
Is it an extra tag? Let's close that I'll
clear the messages, and I'll just reload the
project while I'm ahead and I'll just see if
there's any issues when I reload the project. No, okay, it seems good. Okay. So if we check project project settings
and you go to Plugins, you'll notice Limbo AI
doesn't show up here. Limbo AI is actually
written in C plus plus, so that might be why
it doesn't show up as a cado plug in specifically. I imagine he did
that to maximize the code efficiency
since CplusPlus is faster than C Sharp
and GD script. But yeah, that's just how it is. So to confirm that
Limbo AI works, we can just quickly create a new TD scene here
in the top left. And then right click
here on the root node, the node two D, add a child node and look for just type in State. You should see Limbo state
pop up and BT state. As long as you see those classes
from Limbo AI appearing, then you know that the plugin
is installed correctly. So to recap, we should have an audio our sound effects
over five folders here, at least one audio track. And the art, we should have Crimson fantasy GUI,
Raven fantasy icons, and I'm only bringing in the
full sprite sheet folder, and then Pixel Crawler
for our main character, our background
props and enemies, kind of the main art asset for
the course series, really. And then a font. So I brought m5x7
as a dot TTF font. I think other types
may work as well, but probably best to just stick to the same one for consistency. And then we have add ons, collapse folders, and Limbo AIA. So that should be all
the external files we need to bring in
for this course. Everything else is going to
be GD script that we write, or scenes that we set up or
other Gadot files we create, as well, like resources
which can save to a file. So that's going to be
it for this video. Next, we're getting
started on creating our two D top down
game character and adding some basic movement
controls to the character, so it can move around
on the screen.
4. Top-Down Player Movement: So in this tutorial, we're
going to implement top down character movement for our main player character scene. But right before
we get into that, I do want to point
out that we can delete the demo folder from Limbo AI that automatically imports with the plug in itself. If you want to go through the demo or keep it there,
you're welcome to do so. This includes examples
on how to use Limbo AI, but I'll be showing
you on video for those steps for the state
machine specifically. So we don't actually need
that for our project. I'll remove that. Okay, and now we're ready to get started. So we want to start
off with a two D scene in the top left hand corner, so I'm going to click on
create root node two D scene. So that's going to give us
a base node two D. Now, the root of our character, generally speaking, we want
to be a character body two D, which is a type of node two D. So we can right click on our node two D and
go to change Type, and then we'll
type in character. We'll find character
body two D. Note that this inherits from
physics body two D, which is a collision object. Which is a type of node two D. So there's a
whole series of classes or object types that the character body
is inheriting from, and it acquires all of the traits of all of the
classes that come before it. So that means that anything
a physics body two D can do, a character body two
D can do as well. So let's hit change here to character body two D. And
as soon as you do that, there'll be this little
yellow triangle warning sign saying that the collision
object has no shape. So we need to right click
and add a child node, which is going to be a
collision shape two D, and I'll just zoom in on
the center 0.00 here. You won't see anything
yet because we haven't actually added a sprite. But let's start by clicking
on shape and the inspector on the right hand side and
doing new circle shape. So we might come back to edit
that and reduce its size, but first we need the sprite in order to really see
what we're working with. So let's take the root node and also rename it by
pressing F two. And I'm going to
just call it player. You could also call it
player character or player character body two D,
whatever makes sense to you. And then we're going
to save this scene, which is basically
the root scene and any of the child
nodes into one file, a dot TACN scene
file in our project. So if we hit Control S to save, so then we click in the
top right hand corner, this create a new folder button. And let's give it a name. I'll do characters,
I'll lower case, which is typically the Gadol
standard naming, snake case. So if you need
spaces, you would put underscore and then other
stuff, kind of like that. So let's do characters. And then in characters,
I'll create a folder for the player just
because I know in advance, there'll be a bunch of scripts, and then we can
combine those all on the same subdirectory. So I'll create a new folder. Player. And let's say Okay. So we have player dot
TSC and we're going to save that into this
directory or folder. So save the scene there. So in Godot, your project is essentially composed of
scenes within scenes. So you might have a
main gameplay scene. We have all the system setup, and under that, you might
load in the game world scene, which is the two D map and
the objects that should populate and inside
of that two D world, you would, of course,
have your player scene, the scenes for your enemies, your orcs, your
skeletons, et cetera. And so when your scene
is saved to a file, it's really easy to instantiate this because
it's just going to be basically taking the
saved data and loading a copy of it and putting
it in your scene. So really quickly, I could
actually demonstrate that. If we click on this new tab, create a new scene button. We can add a new
two D scene here. Let's call this the world. So I'll hit F two to rename it, and this will just be world. We can keep it as a standard
node two D. We don't really need it to have
anything other than a position on the screen. So let's hit Control
S to save this scene. I'll go out to the root and we can just save world dot
TSN inside of here. Okay, now we want to add a copy of our player
into the world. So what we can just do
is drag the player scene into the world hierarchy in the scene tab
at the top left. So you just drag player dot
TSN up here under World. And now there's a copy of the player inside
of the game world. So you know it's there because
you can hover over it. I'll say player
here. You can see the icon for
character body two D, and you can see the collision
shape that we defined. Now, there's no sprite yet. We'll add that
right now. So I'll hit Control us to
save the world. We'll go over to player.
I'll right click on player, add a child node we're
looking for sprite. So Sprite two D. There's also an animated Sprite two D node, and both can pretty much be
used for the same purposes. They set things up a
little differently. I would say for
player characters, it's actually easier to work
with Sprite two D and then define the properties
you need to animate on the animation player. We'll get more
into the animation of a sprite in the next video. So let's add a
Sprite two D node. And for right now,
we're going to want to load in a texture. When we're loading
in the texture here, we're not going to set
a single sprite image. We're actually going
to set a sprite sheet. So we get our sprite sheet
from the art directory. In the Pixel crawler
pack, we have, I believe it was entities, characters, body
A, and animations. So we'll just start
with the idol. So for this character, you get a down side and up sprite sheet. When you're doing top
down two D games, you can actually just flip
the side sprite sheet. From left to right, which
means you don't need to create a fourth sprite
sheet for left and right because you can just
reuse the same sprite sheet, and it works just as well. Saves you a bunch of time
to animate it that way. So we want Idle down to start just so we can actually see the character
we're looking at. So we'll load that in here. So this is a base
character sprite. So the idea here is that
you would be able to add clothes as an additional
sprite on top of it, make any customizations to it, but the animations at the
core stay the same as you would develop the
final character for basically different
styles and look. Okay, so a few things
probably stand out. First off, the character
obviously has no clothes. So this is a base
character sprite. The idea is that what you
would do is you would take additional clothes
sprites and add those on top of your base animation
for the final character, maybe as like a wizard's
hat or something like that. And the idea is you
can swap sprites out to finish up your
final character. But for the base animations, they're provided here
in the free pack, and you would just
build up from there. So we'll just be working
with this for the tutorial. The other two obvious
issues is, well, one, we have four copies
of the character showing on screen at once. So we want to make it
so only one shows. And the way we do that is we go to the animation tab here, and we change the H
frames to the number of sprites that are showing in
this horizontal sprite sheet. So I just change
H frames to four. And now it's only showing
the frame zero position, which is the first sprite
in this idle animation. The third issue is that it's very blurry
for the pixel art. So we can fix this across our entire project by going up to project project settings, and we want to scroll down to, I believe it's renderer. Nope, textures. So
rendering and then texture default texture filter linear here is more appropriate
if you're doing, I guess, things like vector art. But for pixel art, which
has to be Pixel Perfect, you want to change
this to nearest. And as soon as you do that,
it's going to basically show every pixel rendered crisply, as you would expect
from a pixel art game. And pretty much for the project, that's all you're going
to need to do to set it up for specifically
a pixel art format. Now, the character and the collision shape are a
little weird here right now. Generally, for top down games, you want the collision shape to go somewhere around
the character's feet. So I like to keep
my scenes still centered at 00 so that I would know basically the
exact position at which the character is represented in the game world and the offset wouldn't be
weird or anything like that. So what we actually
want to do here is offset not the base scene. We want to keep that at zero, zero, but the sprite node. So if you select the Sprite two D and you
hit W to go to move mode, you can left click and
hold and drag it up. If you want to kind of snap it to the Y axis, you
can hold Shift down, which kind of helps you keep it from going to the left
unless you go really far. And we just want
to drag this until the character's feet is
basically right around there. And then we want the collision shape to
be a representation of the character's feet area
because this is where the character would be allowed to collide with other objects. So it makes sense
that the position would be where the
characters actually standing in the
two D game world. Um, getting used to
the perspective of two D does take a little bit of getting used to because it's a little bit less obvious, I guess, than three D, where would just be more
like real life. So here we just have to give it the illusion that our characters actually standing right there, even though the characterprte
goes from here to here, this is the area where the character is
actually standing. So we can save that there. That should be good. Just for reference that was 4.12 pixels. I'm going to actually
just round that to four, just a bit of a
preference to kind of try to keep everything to
pixel perfect sizes. For a Pix art game, it probably won't be a big deal
and cause any issues, but, you know, just in case. So now if we click back
to our world scene, we'll see our character is actually standing
in the game world. And if we want to
play the world, we would hit this play
button over here, which plays the current
scene of the world. And we'll be able to
see the character very, very tiny in the top
left hand corner. It's actually almost
impossible to see that. So the next thing we probably
want is to add a camera. So let's right click on the
world, add a child node, do a camera two D. And although the world automatically
creates a default camera, even when you haven't
added a camera node, when you actually properly define a camera, you
can set the Zoom, which is really handy for Pixart because we're talking
about working with, like, 48 pixel by 32 pixel characters,
something like that. So we want to take the Zoom
and probably bump that up to about three or four
we can see the camera two D now is also centered
on the zero point rather than kind of being more of
this default canvas box here. We can actually see the
camera is this pink line. And if we run the scene now, our character should be centered and a lot bigger
and more visible. So if you want, you can
bump that up to a four. I think three is
pretty good for now. I'll change it later
if we need to. But as long as you can see the character, I
think you're good. One other thing when
we're playing the game, I think I want this to
be much more visible. So let's increase the
window size now so that we can design for a bigger screen size
more properly so that, you know, when we're
doing our previews, you guys can actually see
what's going on better. So let's go to project settings, and I'm going to take
the window display and change this to
something much bigger. So I think something like
1980 by 180 would be good. I mean, that depends on what your resolution for
your screen is. For me, that's going to
be kind of like this. Okay, I'll try this
from my screen. I'm working on a pretty big
extra white monitor here, so a little bit of
trial on error. I think that'll work good
because then I can still see the uh Of button over here. So play main scene pause and stop current running
project buttons, by the way. I would say, as
long as you can see that, then that'll be fine. So with our game Canvas, this big, I also probably
want to boost the Zoom. You can actually edit the properties while
the games running. So I can just take this
Zoom and change it to four. Let's tap back in,
and you can see that the size immediately updated. So if that's big enough for you, you can keep that or
we can go up to five. You know, maybe I'll actually
do a 5.0 Zoom there. Now for the moment, you guys have actually
been waiting for it. Let's get into the actual code. So we need to create
a script that extends character body two D and have that
attached to player. We can do all of that in one go by clicking on player
left click and then just click on the attach a new or existing script
to the selected node. And we're just going to call this the player dot gd script. And we are actually
not going to use the template character body
two D basic movement script. That is, um if I recall more for platformers because it
adds jumping and honestly, we can write it a lot better. So let's hit Create, and that's going to jump us
into the script editor here. So for right now,
I'll just keep it as the embedded script editor. But you can also click this button over here and
make it floating if you want to go full screen with
the script editor that can be handy if you're getting really into
GD Script coding. But here, this
script is going to be pretty quick and simple,
so no need, really. So let's go up to the top. Okay, so we can see that our script extends
character body two D. This means
that our script has the base class of
character body two D. Okay, so if we hover over the
extends character body two D, you can see the class hierarchy here going down from
character body two D, all the way to a
base Gadot object. This just means that
everything along the way, it'll inherit the abilities of each of those
different classes. So a character body TD
actually can do a lot. We're only going to need a
little bit of it, really. So just keep in mind that everything listed there,
each of these classes, which you can jump
into by clicking on their names if you
want to read up about them is something
that character body two D can do as well. So to read through all of
that would be a lot of documentation right now,
good to know about. Good to read up as
you go forward, but a bit overwhelming
for right now. So let's work on our class. We want to give it a class
name so that this script can be easily referenced
from other scripts by name by going to
the start of the line. And then pressing
Enter. Let's go up to the first line and type
in class name player. And now, we've defined this
script as the players script. So this is basically
a global symbol now in our project
we can look up and use just by referencing player typed exactly like
this with capitalization. You'll see as we go
forward in the course that that is extremely useful, especially for something
like a players script, which a lot of other classes are going to need to touch on. So let's work on the physics process
function for the character. So let's type in FU NC for
function and then underscore. And you'll see this
menu auto populate with the methods that are defined inside of
the parent classes. These are basically
functions we can override and add in code so that it actually
does something or changes what the base
class originally did. So we want physics process here. If you find physics
process and hit Enter, it'll give us the full
function definition here for the name here,
underscore physics process. Okay, so here we have the
name of our function. The underscore here
indicates that it is meant to be
private as and not accessed from other
scripts and only to be contained or encapsulated
from within this script. The Delta here is the
name of our parameter, and float is the type
of our parameter. In GD script, you do not have to actually type your functions. So if you want it,
you could remove this colon float and
just call it Delta. Now what is Delta? Well,
that would end up being whatever the collar decided
to pass into this function, which in this case,
basically has nothing to stop it from being anything
other than a float. You would pretty much
expect it to be a float. So in this case, it's
much better to actually declare its type with
colon and then float here. So I do highly recommend this. And then here also an
optional component of the function declaration. We have this arrow, which is
a dash next to the zero key. Okay, and then over
here on the right, we have the return
arrow pointing to the return type of void. Void means it has no return. You don't expect it
to actually return anything that you would use
inside of other functions. So to type that in manually, you would do hyphen and
then a graded and sign. And that's your return arrow
pointing to the return type. Okay, so, of course,
when we type this out, our function doesn't actually
have anything in it. You can have a
function do nothing by typing in pass lowercase, so that's a reserved
keyword there. But without that,
if I undo that, then you'll see this error pop
up at the bottom over here saying expected endantblock
after function declaration, meaning there should
be something in here after the
function is declared. And you can expand
this list here to see all the errors inside
of your current script, a pretty handy
little window there. So good if you need
to copy and paste, you can just select
all of it and copy. And you can paste that into any other application
you're using. Maybe you're doing a bug
report or something like that. Okay, so finally,
we can actually start writing some
function code. I know I'm going a bit slow, trying to keep it
accessible here, especially in the
earlier videos will definitely get a lot more
intense as the course goes on. So, first off, for
our physics process, we want the character to move, which means we need
a speed and we need a direction that the character
is going to move in. So somewhere we need to declare how fast our character moves. So we could declare it
right in the function. But actually, what I want
to do for right now is make an export
variable where we can actually change the speed
in the editor really easily by just going over here in the right and then type in
whatever speed we want. And we can change that
during gameplay, as well. So let's type in at
Export var speed and give it the type of float. So let's walk through
this slowly first. At Export here is a
keyword that declares it as being able to be
edited in the inspector. Okay, and then var
here means variable. Speed is the name
of the variable. Colon here defines the type, and then float is the type. So we can take this speed. Let's actually
write pass here for physics process and then
hit Control as to save it. And you'll see speed pops up here and the top right
for the player script. Make sure you have
the player node selected, and you'll
be able to see this. Now, you can also see
the default is 0.0. So that's the default
value for float. You probably for speed, actually want to assign
a real number to that. So I'm going to take
this and say equals, let's go with 100.0
hit Control S. Now the default value loads up here automatically because we
haven't set a custom value. If you set a custom
value by clicking here, let's say 150 and hit Enter
now the speed is 150. If we change the default to 200 here and hit Save
inside of the script, then you'll see that
the custom value still overrides the
default of 200. I can hit the reset button here to reset it to the
default, so now it's 200. Now I want the script
speed to be 100. I believe that's what I had
for the default project. We can always change
that later and feel free to actually set your players speed to whatever you're comfortable with.
That's completely. And that's how we can have our editor defined property
be used inside of the script. So export variables are really useful to make it
more accessible for editing the details of
how our script would edit without needing to know anything about how the
code actually works. Okay, so we have our speed. We can write physics
process now. Okay, so let's see
here. I'm going to first get the input direction. Okay, so for right
now, we want to declare a direction
variable, so var direction. And you'll see that I'm typing this inside
of the function. So this makes it a local
variable to the function, meaning as soon as the functions done, this is going
to be cleared. It's only used inside
of the function unless we return it as our return type. So it's temporary, basically. And we're going to set
this equal to input. Now know I'm capitaling
the I because this is a class name
that we're referencing. So the input class is you could think of
it as a global class. You can access them anywhere if you need to have it
do something for you. And we are going to be using the Import class
to get a vector. So a vector is going to
have X and a Y component, a vector two, rather. There's also vector
three for three D games, which adds in the Z axis. But for two D games, you only have X and a Y, which just by removing
that third axis makes two D games a bit simpler to
understand and work with. So we want to get
the negative X here. You can see the parameters that this function is defining, and there's actually
some extra ones over here like dead zone,
which we don't need. But the four we need
are negative X, positive X, negative
Y, and positive Y. So in one of the two
previous videos, I believe I did touch on this, but negative X and Gadot
is left on the screen. So we want input for left. Now, you can see
it populating with some predefined
action string names. You know, it's a string
name, by the way, because it has a ampersand
here in front of the string, which is quotations,
and then the actual name here,
ending in quotations. But these are all
default UI actions, and it's not a good idea
to actually use those. In the sense of controlling
your character because they already control the
user interface by default. So let's go up to
project settings. So project project
settings in the top left, Input map, and we
type in a new action. I'm going to type in
left and then click Add. So our left action, I'm going to hit Add event here, and I like using WASD keys. It's pretty standard
for a lot of games. So I'm going to
just type in A here where it says listening
for input and a rule. Automatically
assign it to the A, physical, or a Unicode key. So basically, A
on your keyboard. You can click Okay now. And now moving left
is assigned to A. And we want to do that for
up down and right as well. So type in right, add, and then we click over
here to assign the key, add event, hit D for right. Hit Okay. Now let's put up. Click add event, hit W for up, add an action, down, click Add event, S for down, and there is our movement input. So Kidob will automatically handle converting
those keypresses into the actual action
that we can use inside of the input class
G vector function column. So what we want to do here is put the names of those
actions we just typed. So we can do
quotation left here. And you'll see it actually
automatically populates, and you can just hit Enter
here to confirm it for negative X for positive
X, we want right. You can also see here it
just kind of showed up, so we can just double click it. Then the third one negative Y, we can type in down or find
it on the list. So down. And then for positive Y, oh, I actually mixed that up. Negative Y is up,
positive Y is down. So I'm going to add
positive Y down here, and then over here, I'm going to just erase
the down and type in up. So I'm pretty sure
that's right there. We'll definitely find out in
a minute once we play test. We want to take the
input direction, and if it's not zero,
then we want to move. So if it is zero, we just want to stop moving our
character altogether. So you can do that by
saying if direction. So this basically
already checks if the direction is equal
to vector zero or not. So if direction
means it's not zero, then we can take the
velocity of our character. Now, where did we get
the velocity property? Well, our classic stands
character body two D, and velocity is a property of the character body
two D. So if I was to right click on character body two D and go to Lookup symbol, can scroll down here
and see velocity pops up right here on
the list of properties. So any properties or methods, which is a function within
a class that belong to a parent class are going to be inherited and usable by
that inheriting class. So inside a player, we can take the velocity
and we can set that to we want the direction
times the speed. Now, you'll see this
Delta component here. This is the time between frames. We do not need to
assign it here for the particular movement
style we are using, which is move and slide. There is another one
move and collide, where you would pass and
the Delta when you call that function, if I
remember correctly. So it's a little
odd that, you know, the Delta is automatically
accounted for here, but that's just how it works. So any key press for
movement is pressed. We're going to
assign the velocity to the direction
times the speed. Then down here after that, we can do move and slide. Now, what would
happen right now? Actually, it would be
easier just to show. Let's hit play, and we haven't
select a main scene yet. So let's select the world
scene as our main scene. I'm just going to go up the directories here
and choose world. Double click. We
have a character. And if I press a movement
key and I let go, you'll see that it doesn't
stop our character. It'll just kind of keep sliding, which could be used for some
kind of lighting mechanic. But for regular movement, we actually want to add a se statement here as
the follow up to our I. So se and then make sure you
put the colon at the end. So that basically declares
what comes following as a block for the function to
continue executing code. And we want to
take the velocity. We want to reduce
that towards Serrel. Velocity equals vector two. So we're actually creating
a new vector two here, and we put parentheses in here. You can see that
the third type of constructing a vector
takes a X and a Y value. So inside the
parentheses, hit Enter. So this is going to separate the first parenthesis from
the second and makes it very clear to type the X logic here and then the Y
logic right beneath it. So we want to move toward
the velocity dot x to zero. And we're going to just do
that the current speed. So this basically means
as soon as we let go, we're dropping the speed
to zero immediately. There are a lot of different
ways you can do this. You can have it
gradually decrease over time if you use a larp function. But this is probably the most straightforward.
So we'll just go with that. Move toward. And then on
the second line here, we want to get the Y value
for the new velocity. So move toward the
current velocity dot Y, we're making it approach zero at the change or Delta of speed. So we save that there, and this means this is going
to take our current speed, which might be, let's say, up into the right at full speed, and we're going to drop it
by the full speed amount, meaning it's immediately
going to drop down to zero. We're approaching zero. So as soon as we let
go of our input keys, our character stops moving, is the end result of.
Actually count that. So to declare a comment,
if you want to do that, you put a hash tag and
then the description. So you can keep your comments inline if you like, generally. These days, I actually pull them out to the top of
the function like this, so Control X and then
Control V to paste it. And if you want this
text to show up in the automatically generated
documentation for your class, then you can do two ampersands. And save it, and then
that's going to add it to the player
class documentation. And we can access
that documentation by hitting Search help here. We search for player, our custom class, hit open, and you can see the
name of our properties, which in this case is just
referring to a variable that exists as part of
the class local. In this case, we could
just refer to as a variable that is a local state existing inside of the class rather than a local
variable of a function. So you can edit and read
from these as you need. In other programming languages, things like fields or properties are a lot
more clearly defined. But I think generally for most
basic GD Script functions, your property and variable
end up being the same thing. You wouldn't declare
something like a backing private field
like you would in C Sharp. I would say in most cases, though, because, you know, there would still be
some reasons for doing that to have a private backing
to your public property. Here you can see the method here and if we scroll down
the method descriptions. So physics process. As soon as we let go of
our movement keys, our character stops moving. So this is actually one of the coolest thing about
GD script is that it is essentially self
documentation generating. As long as you put the comments where they're supposed to go
with the double ampersands, you can comment a
field or a property. You can comment a method. And this is really helpful. It makes it easy
for other people to basically understand
your code without reading the entire script
from top to bottom. So you could actually do the
same thing with speed here. So two ampersands and
say the base movement, the base, the base
movement speed, of the player character. Control S save, and
then you can see our documentation
is still opened here in the script window. So if I click on Player,
you can see, oh, now, our speed property has a description which
can be easily read. So that's really handy. Keep that in mind. And now we
can go ahead and hit Play. So we're going to move
our character WASD. Now we will see as soon
as we stop Import, the character stops moving. Which is more what you would expect for a character moving on anything standard like
grass or stone or whatever, things that should have
friction, not ice. And that's the basic movement we'll be using for the rest
of the game, basically. Now, obviously, the
characters not animated. We don't have the
run animations. It's not facing the
direction as it should yet. Those are all things
we're going to cover in future videos going forward. One last thing I want to add into one last thing I want to
add in for this character, well, we're ahead of things, is to add in just one more
display name property. So I'm going to actually add a export for a var,
a display name. And this is going to
be a type string name. And we're going to
set that equal to ampersan whatever
default you want, you can just make it
player and hit controls. So for our player, we can see now the display
name string name property is editable inside
of the inspector. So I'm going to change
this and set it to Jaco, because we're going to make
our character by default, have a spear weapon. So I think that works. In survivor light games, generally, you would have multiple characters
you can select from. So having them all have a distinguished display
name makes sense. Now, real quick, there's also a name property
all nodes have. That refers to the
node name up here. But you can see things like
collision shape two D. You don't port spaces
and node names. So to use that as
your display name might be a little weird. Like, let's say
you add a subclass like fire dragoon, right? But you might make the
player node name here, Freragoon without this space, so you can have a distinguished
separate display name by having a custom
property like this. So what's that that to dragoon? Hit Control S. And that looks
acceptable for right now. Eventually, we'll
take this input and we'll move it out
to a separate script. We'll take stats like
character speed, and we'll also make
that a separate script. But this is a good start. So that's going to be
it for this video. I tried to slow down here
intentionally to cover all of the script terminology that we're going to be using
for GD script as, you know, the foundation
for the rest of the course, these are all things
that are going to keep coming up again and again. I hope that wasn't
too slow for anybody, but we are going to be
speeding up from here. So until the next
video, I'll see them.
5. Play Animations from Spritesheets: So as things are right now, our character can
technically move, but there's no animation
to the sprite whatsoever. So we want our character to animate when it runs idols and, of course, the death animation as well when we lose the game. Okay, so the first thing
we're going to do is actually change out
the sprite sheet here. If you look in the
Pixel Crawler pack, we can grab the Idol sprite
sheet for the night, or you can use the rogue or
the wizard if you prefer. So I'll grab this and
bring it over here. And these are basically
side facing sprites. You'll notice that
this character doesn't have a up or a down. A few advantages
to this, though. First off, if you're
only animating one direction and
then flipping it, which is what the standard
survivor, actually does. It's a lot easier
to build characters because you only need to
animate one direction. And secondly, you can
completely skip over animation trees and just focus
on an animation player and a state machine which
controls the logic for your character switching
between states like Idle and given those reasons, we're actually just
going to switch over to the night Idol
sprite sheet here. You could use the idol
pretty much interchangeably. You could later use, like,
the rogue sprite sheet, like right there or the
wizard one over here as well. So either of those are going
to work perfectly fine, and there's already
the setup for having three character
classes right there. But it was going
for more of a spear wielding knight to
start off with. So like a character's
named agroun, this spright kind
of fits it, and he will be able to throw spears. Okay, so we have our
sprite sheet set here. We want to turn this
into an idle animation. The He frames have
already been set up here, but we need an actual
animation player in order to do that. So right click on the player
node, add a child node. Look for animation
player. Add that in. So when you have the
animation player, the animation window will
be here at the bottom. You can click right here
if you still need it. And we want to go
to animation and then Nu and type in the
name of the animation. So we're starting with
the idle animation, so we'll just type in Idle. And we can add in
a property track. So we're going to
grab from the Sprite two D some of these properties, namely the texture.
So double click that. We're going to right
click Property Track, add in H frames, and then property
Track, Sprite two D. V frames. Then lastly, for right now, at least we
need the frame number itself. So add and frame as a property. So now we can take this Zoom and zoom in a bit on
our sprite sheet. First, I want to take the snapping time down here where it says 0.033 3 seconds
and change that to 0.1, meaning that we're
going to be working in 0.1 second intervals or
a tenth of a second. Then enable this snapping
to timeline cursor. Now you'll be able to
just kind of click here to the 0.1
second intervals, makes it a lot
easier to work with. You might have seen
on this bright sheet that there's four
frames of animation. So we're going to cycle through
those every 0.1 seconds, which means four times 0.1, we want a 0.4 second
long animation. So over here on the right side, type in 0.4, and then this thing that looks like arrows feeding
into each other. Animation looping,
you want to click that so that'll make
our animation loop. Now, in our animation timeline, there are no keyframes we
definitely want to set a keyframe at the start of the animation for all
of these properties. So right click insert key. If you left click here,
you'll see that the value for the texture is already preset to this night
idle animation. If it's not, then
you're going to want to change it to the
Idle sprite sheet. From here, you can drag and drop like that and put that there. So what this keyframe means
is that at the start of our animation at 0.0 seconds when this
animation starts playing, it's going to set the texture on our sprite to this value, which means that as soon
as the animation starts, we change out the sprite sheet. Now, H frames, we also want
to right click and Serta key. Now we already predefined
our value here as four in this bright two D. So you can see that
when you Serta key, it'll add whatever value
it's currently set to as the key frame point
on the animation timeline. And whenever these
keyframes hit, that means that the value
at that time is that value. Now, for our Pix art game, almost always we're going to be using discrete mode over
here on the far right. You can see these three dots. What that means is
that the value does not change until it hits
the key frame point. In something more
like a three D game, you might do
continuous animation, meaning that it'll gradually go between the
values of keyframes. So if you had something
like a position keyframe, then it will animate
the position between the starting
position of zero and the end position of 100 over time as this animation
timeline progresses. So a lot of animation in games
is actually just changing the values of properties on
your object as time goes on. Okay, so continuing to V frames, we can right click insert a key. You'll see that the value here, if we left click
on the keyframe is already set to one.
That's what we want. Once again, if we look at
the Idol sprite sheet, it has four H frames, horizontal frames, and one vertical frame in
terms of rows and columns. So that's why we have four for H frames and one for V frame. Not a lot of customization
we have to do here. We just have to make
sure that at the start, it's set to those values so that our animation
plays correctly. Now, what we actually
need to do is put in our four frames
of animation here. So right click on the first
frame and do Insert key. You'll see that for frame, it actually shows you a
preview of the sprite. It's going to show very helpful, but you can also
left click on it and see that's set
to value zero. Now we want to snap to 0.1. If you didn't already choose apply snapping to timeline
cursor, you can do that here. That'll make it much
easier to go to 0.1. I guess you can also
manually type in the value if you want to
jump in the timeline. I usually just snap with
the mouse cursor, though. Then right click on
the frame section, the frame row and do insert key. Now you'll get a line
that goes between these. That line means that there's no change in value
between here and here. It's just a continuation
of the same value. So we want to click here
and change that to one, and that'll mean that
now it's going to be playing the second
frame of animation. Since the Bright sheet is
like an array of values, it starts at zero, not at one. Generally, in programming,
that would be how it works. So one is actually the second
frame of animation and zero is actually the
first frame of animation. Let's right click on
0.2, insert a key, click on the key frame point, and then change
the value to two, and then go here to 0.3. Right, click insert key. Click here, change it to three. Now, you shouldn't see any lines going between any of
these frames anymore. We can go to the start here, hit play, and there is our
animation for the character. Now, once again,
because we're only doing left and right animation, we're only going to
need to do one idol, one death, and one
run animation here. And then we'll
control flipping of the character a different way. I'll show you
that later on. So a much simpler setup and a lot easier to add new characters in if you
want to do that. So let's actually duplicate this animation because a lot of this we're actually
going to reuse. We're just going to
change maybe the He frame value and the texture. And depending on how many
run frames there are, we may change the
frames here as well. But let's duplicate it
by clicking here on animation and do duplicate,
and let's do Run. Okay, now we want to
click on texture, and we want to drag and drop the run sprite
sheet and to here. We can see this has six
frames of animation. So what that means
is we need to change the H frames value here to six. We want to change the
animation duration to 0.6 because we're working on
six frames now, not four. And then we're going
to want to go to 0.4 seconds and add in the
fifth frame of animation. So insert key, click here, change the value to four for the fifth frame and
then go to 0.5 seconds. Right click here,
insert key, left, click on the keyframe, change
the value 4-5 and hit play. And there's our round animation. So duplicating saves
a lot of time. Okay, now let's add in the death animation while we're on a roll. So let's duplicate
and add in death. And then we want to
change the texture here, of course, to the
night's death sheet. So drag and drop the death sheet into the keyframe inspector. I can count here. There's
six frames. So let's click. Actually, we don't need to change anything else because we just duplicate it from
around. We can just hit Play. But you can see it's looping. So characters don't
usually do that. So we want to actually disable loop by clicking twice here. It'll make it so that the
animation loops back and forth. Like, it goes to the end
and then back to the start. That would be more useful for something like
a moving platform. We want to click
a second time to turn off looping altogether. So go to 0.0, hit play, and the animation should
stop here at the end. And then once the death
animation is done later, we just need to
do something like key a Game Over
for our character, and then that'll be that. Okay, so which animation is actually going to be playing
when we run the game? Well, let's hit
play and find out. Okay, so you can see the
idle animation is there, but it's actually not playing
any of the animation. Uh, we didn't actually set an animation to
Autoplay on Start. Our animations are
eventually going to be controlled with something
called state machine. That'll be what we get into
more in the next video. Let's click on
Animation player and change the animation here that we're looking at
from death to run. And we'll click over
here this little Autoplay on Load button. So click that hit play, and now you'll see
that regardless of if our character
is moving or not, it's going to be playing
the run animation. Note that it also doesn't flip
directions automatically. That'll be a simple script
we add in later as well. But we have three animations
on our character, and we have the
ability to switch between them by
calling them by name. In the next video, we'll
create the state machine with a move state that will be able to switch
between idle and Run.
6. Player State Machine: Okay, so our character has animation player and the ability to play the run animation, but we want to switch between idol and run animations
depending on if there is key input for
our character or not. So let's control this
with a state machine. So if you recall at the
start of the course, we added in Limbo AI, which is a state machine and behavior tree system that you can use in Gado out of the box, saves you a lot of work, not
having to do it manually. Going to be sticking to state
machines for the course. I think they are just a little bit easier
to work with going forward and much better for
simple setups like this. And this is, to be honest,
a very simple setup. So let's right click on the player at the
base at a child node. And we're going to be looking
for the keyword limbo. So you'll see here Limbo HSM standing for hierarchical
state machine. As in the root node here, the state machine itself is
going to control the states, the limbo states, and it's
going to be able to switch between them as we add
transitions between those states. So let's create the Limbo HSM. And then I'm going to right
click here and we're going to add our first state
add child node. So we want a limbo state
and just create that. So the hierarchy is the
root state machine, and all of the states are
what we can switch between. And each of these states
are going to execute different code depending on what state the character is in. For this course, we're
really only going to have a move state
and a death state. So very simple. But still important
because we want to know when our character should
switch to the death state, and the move state is going to be controlling the
idle and move animations. Technically, you could separate Idle and move into
their own states, but they're essentially
going to be executing the same code because whether the character is
idling or moving, it's still free movement
for the character. It's just a matter of which animation it's
going to be playing. And I don't think
that necessarily justifies a completely
separate state. So let's right
click on Limbo HSM, and I'm going to
attach a script to it. This will be called, let's
say, player underscore HSM. So it's the player
hierarchical state machine. Let's create that. And then up here at the top,
we're going to, of course, want to give it the
class name, player HSM. Generally speaking, as I'll
show in the file system here, if we search HSM,
whatever you put as the snake case
for the filename, you would also want to have the class name up here, but this, of course, has
capitalization and you skip the spaces because this
is a type symbol up here. It's a very good idea to keep this name and the script
name synchronized. So the player HSM is going
to help us to control the player character body through the states that
exist on the Limbo HSM. So we want to grab
the player and set it as the agent that
the state machine controls. Okay, so first for
our player HSM, we want a reference
to the player that our state machine
is going to be managing. So let's do at export var
player of type player. And if we've defined
the class name for our character player body script as player, this
should show up fine. And you can see in the inspector over here on the right
that it did show up. So if you want, you can click Assign and assign it to
the player right now. So that means our player
hierarchical state machine now has a reference
to the player. So what we want to do is
make sure that on Rady, that this script is going
to set up the state machine by initializing it with
the player as the agent. So let's create function, underscore Rady, and the return
type is going to be void. Pretty much always is for ready
functions because this is a default node function meant
for setting up your node, as in one of these nodes
over here on the left, something that exists inside of your scene after all of its
properties have been defined. So the ready function executes the code that when
the object is ready, it's going to do some
final setup steps. So very common for using before the object really
starts running in the scene, but doing the essential
final setup steps. While we're going
ahead with this, I'm also going to introduce
some new concepts as well. So first off, the
assert statement. So let's type in assert. So this means you pass
in some condition and expect it to
evaluate to true. So in this case, what we want to check is that the player has actually
been set in the inspector. Whenever you set a
export variable here, you're basically depending on the designer or whoever setting up the game project to
actually fill in these fields, and it's very easy to actually for to define one of these, especially if you have a
lot of scripts running around with these
export variables. They're very convenient for setting things up
in the inspector, but it's user error prone. So we can just assert that
the player is not null. Now you see I do exclamation
mark and then a equal sign. The exclamation mark means you invert whatever you're
trying to test. So if player equals null, which would actually be
two equal signs like that, then you would check
that the player is null. But if you do an exclamation
mark and an equal sign, then you're saying the player is not and whenever you
do an statement, you have the option
of also passing in an optional failure message. So what do you want it to
say to whoever's running the script when that
condition failed to be true? So in this case, I would
want something like player agent must
be set on the HSM. So hierarchical state machine. So I'm just putting
up here at the top, mentioning very
clearly that this is a hierarchical state machine. So if you ever have any
question about why HSM is, well, it's right up there so
let's show this in action. If I run the scene, it's not going to error
because we have set the player node as the reference on the export
variable for a player. But if I unset player
right here and we run it, you're going to get an
immediate error message. It's going to say
assertion failed. Player agent must
be set on the HSM. So the assert is
just a check to make sure that what you expect to
be true is actually true. So writing statements
against a condition is the general idea of testing inside of programming
in general. To go more advanced than this, you would write unit tests
or integration tests using a testing framework like GD Unit or GUT
Godot unit testing. That definitely goes
beyond the scope of this course series, but it's something to
look up if you have the time later on
because writing tests, make sure that your code
is actually written good, and it can be
helpful in the long. Catching bugs early. So
let's move on with that. We're only going to use these
little assert statements like this. You can
see how it works. If the condition fails,
then we get a message, and that lets us know that, oh, we forgot to set something that we should
have set something. So it's pretty handy, actually. So let's make sure
that in Limbo HSM, once again, we
assign the player. And then let's do the
setup of the HSM itself. So I'm going to actually
make this a separate function because this is going to do a few
different things. So let's create another
function down here, function. And when I say function, I really mean FU
and C, just say. Underscore set up HSM, and then the parentheses, the turn arrow and void
had an extra space there. So what we want to call on the state machine is initialize, and we need to pass in an agent. As I mentioned, the
agent in this case, is the player to
pass in the player, and then we need to
call set up HSM. Inside of the ready function. So underscore setup HSM. Okay, so to walk through this, I'm making it a private function with the
underscore here, meaning that we should
only be executing this inside of this
player HSM script. The initialize does all of the background setup
for the state machine, and it's going to make it operate with the
player as the agent. The other thing we need
to do for right here is set the initial state. So which state are we going
to be executing first? Let's also rename the Limbo
HSM node to player HSM. I like to also keep this kind of synchronized with the
script attached to it. So player HSM here. Equals player HSM inside
of the scene window. And then the Limbo state here, we'll call this the move state and then attach a
new script to it. Also an advantage
of if you rename it before you attach the
script, if you right, click on the state and then
attach the script now, you'll see that the name of the script also defaults
to the name of the node. So keeping those three
things synchronized up generally a good idea
keeps things organized. And we'll just save this
into the player folder by default for now, and let's inherit
from Limbo state. Create. Okay, so I'm going
to call this the move state, class name move state. We might actually re use
this for the enemy as well. Since they're all going
to be essentially using the same animation setup. So the move state,
we're going to need the names of
two animations, which are our idle animation and our move animation,
which is run. So I'm going to say
export for Idl anim, and we're going to set this
up type to a string name. And we're going to default
the value to Idle. Now, putting the ampersand here to make it a string
name is actually optional. When it's declared as
a string name here, the variable, it's going
to automatically convert. So it's optional if you
want to do this as Ah, so let's do export var. I'll just call it run
animation because that's what it's
going to be called by default in this project, and we'll do equals ampersand. Putting a couple comments here two hashtags makes the comments
show up in the inspector. So if I go over to the inspector and I hover over idle animation, you can see the
comments actually show up there automatically,
which is very handy. You can also see
how the property is defined as a string name with a default value of idle or
run for the and for this, we want to make sure by checking the animation player
that our names match up. So you can see I was doing a lowercase idol, lowercase run. It is casing sensitive when
you're using these strings. So make sure that it
matches here and here, whatever it shows up in the animation player
for idle and run. Okay, so I'm going
to right click on Limbo State name here
and look up the symbol. And you're going to see
that there's a bunch of special methods that
this node has enter, exit, and setup and also update. So these are state
specific methods. They do sound kind of similar. To what you would have on
a standard Gadot node. But the difference is
that these only execute when the state machine is
actually using this state. The state machine only executes the code of one of
the states at a time, and the other states basically just sit there passively until the state machine has
determined that it's time to transition into
a different state. So that means you can
write basically four sets of code and only one is
going to run at a time, and that's very handy for
keeping unnecessary stuff from happening while
your character should not be doing that stuff. Like a character should
not be attacking when the character is only running and there's no attack input. Therefore, you don't allow that code to execute by managing
it with a state machine. Okay, so we want to start off
with this enter function. The virtual keyword for
these functions here means that although there might be a default implementation
for the function, we can write our
own custom code, which completely changes what it does when these functions
actually execute. Let's look into the
move state script, and we're going to be doing
function underscore Enter. So what do we want to occur when the move state enters inside
of the state machine? So we want to take
the animation player, and we want to set its
animation to either idle or run depending on if the character
is moving or not moving. Okay, now for our move state, how are we going
to get a reference to the animation player? So the idea is that
for our agent, it's only going to have
one animation player, the animation player
of the player. There are several
ways you can actually get that reference inside
of your Move State script. I'll just try to show the one that off the top of my head, I think would be the
most practical here. So if we go to the player HSM, then we can export a reference
to the animation player. So let's do at export
var animation player, and then Colon animation player. Okay, a comment to make
it clear that this is the player's animation player, and then we assign
it in the inspector. Now, the reason I'm
doing it in this script rather than the children's
scripts, like the states, is I just want only one
exported reference here, and then we'll use
that in order to get the animation player
from our state scripts. So if we jump into the
move state script, I could just very quickly
get the reference that the player HSM
here at the top has by doing something like get parent and then
dot animation player. So that's really all
you'd have to do there. And now we can assign that to a local variable if we like. Other option would be we could
have a method which always gets this or we could
cache it here locally. So we just need to
take this value here and then assign it
to a local variable. So var, I'll say underscore
animation player, which is of type
animation player. Then it could be on Enter, but I'll change it
to setup later. We'll do Underscore
Animation player. Equals, Get parent. Yeah. You can see
it autocomplete, got rid of the
function name there. This should be your
full line here. On Enter right now,
it's going to assign the animation player to whatever the parents animation player is. So that makes it a little
easier than having to have export variable reference on
each of these state scripts. The state scripts
instead just depend on the parent having a reference
to the animation player, which I think is fair
because a player HSM is really going
to be controlling the animations on
the player anyway. So I think that the setup
makes sense in this case. Now, rather than on Enter, which will be called every time the character enters the state, I will implement
the setup function. Function, underscore
setup, and then autocomplete that returns void. We'll take this line and
put it up here like that. So the setup only runs once when the state
machine initializes. So basically, we
don't need to get the animation player every
time we enter the state, just one set setup. Now, what we do want to occur at Enter is that we play
the right animation. So right now, we don't
have the right way of determining the animation
because our code for that is actually trapped
in the player script with this direction being set
to a local variable. So we're going to
have to pull that out later for this
to fully work. But what we can do right now is do animation player dot play, and let's just say let's start
with the idle animation. Now, no, I'm putting
in the variable here, a reference to idle animation, which will resolve
to the string name. So this expects a string name, but we're not hard
typing this down here. We want to make sure
it's the variable so that we still have the option of changing it in the
inspector, if that makes sense. Generally, as a
rule, if you have something you want to
change, that's a string, you want to bring it out to
some kind of variable rather than hard typing it every single time you reference
it in the same script. Sometimes, if the message
is only going to ever be used once in that
one specific spot, it's perfectly fine to type the string your code into
your function code rather. That can be fine, too.
Okay, so it's going to play the idle animation as soon
as the move state starts. Now, to test that,
we want to turn off the auto run inside of
the animation player. Or maybe we don't actually. Let's stop the game
and run it as it is, and it should actually go into the idle animation
or so I thought. So let's try churning off
the auto play on load here. Now, without Autoplay on load, I think it should play nothing. And so we still have the problem of it looks like the state's not entering because the
animation isn't playing. So what we can do to prove that is to go back into
the script editor. And I'm going to
set a break point here by left clicking to the left of the line
number in the script. And this is going
to mean that when it hits this line of code, it's going to break and allow us to see what's
happening in the debuker. So I'm going to hit play, and it's not going to hit there, which means that the state never actually entered.
It's not active. So that means we need to
go back to the HSM script and make sure that
the move state is actually active at the
start of the game. So let's go into player HSM. And I think what I forgot
here was to actually call set active on the
player HSM itself. So we want to set active true. We initialized, which
runs the setup, but we haven't set it active, which actually makes the
state machine start running. So now if we hit
play, we will hit that Enter line on
the move state. So this proves that the move state is actually being recognized by the state machine. To go a little more detailed, you can look at the stack
trace in the bottom, and you can see, if
you read carefully, that the move state
enter function ran, and it ran after setup HSM ran, and you can actually click here and jump to
the line of code, which caused Enter to
run on the move state. See set Active true here, which calls the Enter on the
first state that gets ran. And this all occurred,
if you click at the bottom line on
ready setup HSM. So you can see, basically what
led to enter being called. So it was the ready function on the HSM called the setup HSM, which called set active and then set active made the
move state run. So now if you haven't already, you can uncheck this
breakpoint and you can click down here to continue or press F
12 on the keyboard, and you can see the idle
state is going to run. If you want to change
that to the run state, so we take run here and replace play Idle animation with play
run animation, hit play. And now you can see
that it's playing the run animation when
the state enters. So in the next video, we'll
focus on actually controlling the move state with player input to make sure we're playing the right animation at
the right time.
7. Player Input as Component: Our move state is playing the animation on the animation
player for our character, but right now it doesn't
have the code to know when to switch
to the run animation, when there's actual
player input. So to handle that, if we look at the player script, we can see that this is
where the direction is being acquired for the player, and we're doing it with a local variable for the direction. So this works for this
physics process function, but then we can't use the input direction
anywhere else but here. So we would want to bring
that out to some kind of property variable that can be accessed
by other scripts. Now, we could do something like VR direction here of vector two, and then change right here to set the local
variable direction. And that would probably
be the simplest option. Together. However, I think if we kept going down
this route and we put all of the input inside of the player
character body script, then we'd really be breaking
a separation of concerns. So the player
character body script is mostly about just
moving the player. If we add in all of the
input functionality, acquiring all the
players input into it, then now the script
is going to serve two or three or
even more purposes as we keep cluttering
up the player script. So I'm going to try
to bring this out to a separate node script that our player
script can still use, but we can also
reference on, let's say, our states in order to handle all the input for our
player character. So let's right click
on the player node, and I'm going to be
adding a child node. Let's type in node here. So this will be just a
basic node class, create. And I want to rename
this to be player input. Let's attach a script to it. Right click attach
script, player input. And we can give it
the class name, player input so
that it can easily be referenced in a
export variable. So now from our player script, we need to basically grab this, and I'm just going to control C, paste it over to player
input, so I can put it there. But where we want to
actually get the direction is inside of the input method. So right now in the
players script, physics process is executing every physics processing
frame of our game, which by default is something
like 60 times a second. We probably are not pressing
the keys 60 times a second, so that's a lot of
unnecessary get vector call and it probably wouldn't matter so much
for one player character. But in general, I
think it may be slightly better on
the player script to handle the direction input inside of the function
underscore input method. So function underscore
input, basically, whenever an input event
is sent to the game, then it will go
through this method and see if there's any
code for it to respond to. And the way that we check if the event is
actually relevant to us is we would basically check the action name on
the event itself. So we'll type in if, and I'm going to
make a new method if underscore is movement action. And we're going to pass in
the event to this function. Then we're going
to do some stuff. Otherwise, we're going to just pass and not
do anything yet. What is the I movement
action function? Typically, we're just guarding against other types of actions, and we only want to process the input if the action
is a movement action. So we'll say function
is movement action. Of type event, input event. And then this is going
to return a Boolean. So a boolean is a
true false value. Is it a movement action? True or false? So this is going to look
something like this. Return event that is action, and we're going to
actually just copy and paste these
names for right now. Okay, so we can see
here I'm repeating the same string here
that's up here. So generally, I would try to bring those out to its
own separate variable. But we could Yeah, why don't we kind of
do that right now? And then I'll show you one step we can go beyond that, even. So let's put a variable here, Var left action or actually, let's just say var left
is of type string name, and it's going to be equal
to this value right here. So let's Control C, copy this, paste it down here, and then I'm going to
select this again. I'm going to hit Control
R to find and replace, and I'm going to
replace it with left. Now, when you're
using this tool, it's going to replace what
you have currently selected. So I want to replace this with the left name, so I'm
going to hit Replace. Now, the second one, which
it's automatically going to jump to or maybe
it doesn't Uh, right here, we do want to keep as Ampersand Quotation left. So let's click Next match, and we'll find down
here we want to replace this with
left, so hit Replace. And that'll be
basically it there. We should probably
just take care of this error message
right now, too. So I'm going to declare
this far direction, and we'll worry about assigning this
properly in a minute. Okay, so that got rid
of the error message, you can save the
script for right now. So right now, I
movement action is returning whether the
action is the left action. So does the name
of the action on the event match the name left? Now, we have three
other actions, so we need to check three
other conditions here. So let's quickly bring this out. I'm going to select
this Control X to cut it, type in right. Go down here, create a
new variable var right, string name equals, and then
Control V to paste it in. So we have the variable assigned to the
string name right. And do the same thing
for up. So select this. Control X to cut, type in up. Go down here. Var up is of type string name, and equals Control V to paste it N. And just one more
time. Control X. Type in down, go
down here, down, string name equals,
Control V, paste it in. Okay, so now we have our four
variables which correspond to the actual string names that we're really passing
into methods like this. You want another fun trick, you could tab these
over like this, select right behind the
equal sign hit tab, behind the equal sine hit tab, behind the equal sign hit tab, behind the equal sine
hit tab, like that. And you can actually
line all of these up and it looks pretty
and all that. Might be a little more readable. Up to you if you like it
to have the tab or not. Okay, but now the
important thing is to finish this method, we need to check
the other actions. So we're going to do space. And then the straight
line character, which is shift and right under the backspace, the
vertical line. So the vertical
line, vertical line, and you have to hold Shift down to do that,
at least on my keyboard. So those two vertical
lines means or. I guess you could actually
type in or like that, and it would be the same. And we could just
try that if that actually is a little bit easier to read and do
event dot is action, and we want Right. Hold on. Write the
variable name. Good. So, it looks
like that works. So let's just do that
again or event I action up or event
is action down. Okay, so hopefully
that makes sense. We're checking if the
event is left action, right action, up
action or down action. Now, this line is kind
of going a little long. You can see this like
cutoff point vertically. So these are kind of just
general guidelines here. You can see that we
have 107 characters on this line. Not a problem. I mean, I think it's
pretty readable, but if you want to make your code go down
more vertically, then you can actually break
it up here by pressing Enter. Now, if I hit Control S here, it's going to give us an issue. It's going to be expected
expression after or. So it currently doesn't know we're trying to jump
down to the next lines. So what you actually
have to do is put a backslash right after the O, and now it knows that this
continues on the next line. So you can keep doing
that like this, Backslash Enter to
put a new line, and then backslash Enter
and put a new line. Now, I would argue this
looks pretty clean here. So we have our four conditions, and it's going to
check all of them, and as soon as one of
them evaluates true, it's going to return true. And if none of them are
true, then it's false. So if the action is
a movement action, then we're going to be able
to process our movement code. Okay, so this is where we
actually assign the vector. There's two places we're
going to use the input. One is for the facing direction and one is for the
movement direction. So let's get the
variable for the input. Var input is of type vector two, we're going to set that equal
to Import dot G vector. And just like before, we want left, right, up and down. Okay. And there's our input. So
whenever we press any key, we want to set the
directional input. So direction is just going to be equal to whatever
Imports value is. But with the facing direction, it's a little bit special. A character in a four
directional two D game cannot face vector 00 because that would be basically
looking at itself, not looking to the right, up left or down, which are the only
directions it can look. When we're assigning
the facing vector, we only want to do that if
the direction is not zero. So if input does not
equal vector 20, so for a vector two
direction of zero, that means X is
zero and Y is zero, remember, vectors have
X and Y component. So given that that
condition is set, then we can take facing
and set that to the input. And we'll get rid
of this little pass here because we don't need that. And then at the top here, var facing as of type vector two. We can just default
this to, let's say, vector two dot right,
which works perfectly. If our character is
always expected to face right by default on load, you could, of course, make
that left instead if you want. And then the direction here,
we want to replace this with vector two or if you want
to be a little more fancy, although this is the
default value as well, we could say colon equals
vector two dot zero. So this right here
actually means that we're inferring the type of the
variable from the assignment, and the assignment
is a vector two, which means that this
is automatically going to be assigned
to a vector two, which means you
don't have to type this vector two is
equal to vector two. That just looks kind of funny. So, likewise, you can actually just do that down here as well. And make it a little
bit less wordy, although both will work
perfectly fine here. The only downside
is if you change this vector two dot right
into something like a string, that this is also going to actually technically
be correct because now it's assigned to the string
of vector two dot right, and that means that
this is now a string. So, you do have to be a
little careful about that. I haven't actually seen
that be a problem. So I think in these cases, inferring the type
of the variable rather than declaring
the variable type outright is perfectly reasonable and fine and often a
little bit easier to read. So up to you, you can do both. If I'm looking at
things correctly, I think that is basically
our player script for now. We'll come back and add
signals to it a bit later. Okay, so in our player script, let's change things here. So instead of having
a local variable for direction and assigning the direction instead
of physics process, what we are going to
be doing is hitting at Export var input is
of type player input. And down here for
physics process, what we want to be doing is getting the current
direction from the input. So var direction, and I'll infer the type from
inport dot direction, going to be assigned to
the local direction, and then we use
that for the rest of our script by checking, Is the direction actually
a value that's not vector 20 is what
this line means here. And if so, we take the direction and multiply it by the speed. The difference now is that the input is managed by a
completely separate script. Our player script
is kept more clean, and we have a reusable player input component that
could be used for completely different
character designs that need the same
kind of player input, even if the character actually handles the mechanics of its movement completely
differently. So let's click on
the player script, and we want to make sure
that Import is assigned. So like before, what we
could do is put function, underscore ready, and assert that input is not null
and give it the message. Input must be assigned for the character body
to move properly, run the game, see that our
assert fails, which is good. Click on the player and assign
the input to player input, run it one more time, and
see that it actually works. And if you do WASD, you can see that
it's moving based on the input from our
player Import script.
8. Switch Animations with Signals: Right now our up down left right directional input is controlled by the
player input script, but we don't have the animations being controlled by the
direction on that script yet. So if we took a look at
the move state script, we could imagine that we would be able to just
grab a reference to the player input and get
parent on it just like before. This method, although it
works fine generally, it depends on the
player HSM script, having a reference to
all of these properties. If we change the
player hierarchical state machine script,
at any point, it may break how
this script works, and the script also has kind of a direct dependency on the parent having all
of these properties. So we wouldn't be
able to reuse this on a completely different state
machine that doesn't have, let's say, animation
player exposed. So what you can do as another option is
to use blackboards. So we look over here on the right when you
have the HSM selected, you can see Blackboard
plan is an option. So when you create
a Blackboard plan, it tells any child nodes in the state machine system what it is going
to have access to. And in this case, we want the facing direction
of the player input. So I'm going to create a new Blackboard plan
here on the player HSM, and then hit Manage. We can go over here
and add a variable, and we want to add
let's go with facing. So when we're talking
about the animations, what's more relevant to us is the direction the
character is trying to face rather than the direction that the character is moving in. Those would usually be the
same thing, but not always. So we'll have facing here, and then on the dropdown, vector two, so we can click Manage over here and
then add a variable. And we're going to look for
the direction vector two. So change type from
float to vector two. And then we can hit Okay, so in order to get this
direction too and have it be used across our different
state machine states, we want to go to binding, and then direction, we want to bind this to the
player Import script. But if we do that right now,
nothing's going to show up. So one of the limitations in
sight of the Lambo AI system is that to bind a property
onto the Blackboard plan, that property needs to
be exported variable. So if we go to player Input, we can take direction
here and just give it at export and then save. So you click on player Import, you'll see that the
direction shows up here. Technically, this
means that you could change the direction
before the game starts. It's going to immediately
be overwritten by this Iport function as
soon as you press a key. So there's not really
much of a downside. It's just that the direction has to be exposed for this to work. So on the HSM, we can now bind to the
player inport script, and we're going to be looking
for the direction property. So choose direction
and hit open. Okay. So if you read this now, the direction is now binded
to player Import direction, which means that
whatever we set on player Import direction is going to be mirrored onto
this Blackboard, and we'll be able to use that inside of our scripts
like the move state. So while we're at
it, we can also set the animation player
onto the Blackboard plan, and that will remove
the dependence between the state and the HSM. Let's manage and add a variable, I'll say animation player. And let's go to type
and choose Node path, I believe is what we want here. Okay. Click Okay. And we assign this to the
animation player. So now if we read
from the Blackboard for the property animation
underscore player, we should be able to get a
reference to animation player. Okay, so now if we
go to Move State, how this is going to
look is more like this. So underscore animation player is going to be equal
to Blackboard. So the Blackboard is
created at runtime, based on the Blackboard plan. So this is basically
the runtime object where we get the
variables we need. So we need to do
Blackboard dot, Get VR, get underscore var, and we're looking for
the property name. So, in this case, that's going to be animation
underscore player. The default will be null. We do want it to complain
if it can't find it because that means we messed up somewhere around our setup. Now, we can basically
remove this line now, and now our script
doesn't depend on the HSM script at all. It just depends
on the Blackboard being set up to have
an animation player, which will get an error message if it does not have animation
player in the Blackboard. So now for the direction, we just need to get that
on update, I suppose. So function, underscore update, and we're going to be getting the current
Blackboard variable. So that's going to
be VR direction. It's going to be equal to
Blackboard dot Get VR, and we're looking for direction. And we can't infer this
variable type because we don't know what Git VR is actually going
to return here. So let's declare it
as a vector two. Okay, so now we have
the direction property, and our script doesn't
even need to know anything about the
player input script. All needs to know is that the Blackboard has a
direction property, and we can use that
for our animations. So very simply, if
direction is not zero, that's short form
for that, then we're going to play the run animation
on the animation player. So underscore animation player, dot play, run animation, Ale, animation player,
dot play, idle animation. So let's go ahead and hit Play
now and see if this works. So if I WAS and D, then you can see that we get the animation movement
for our character. Now, there is a big flaw here, which is that the offset on
our sprites do not match. Now, that is just the nature of how the specific sprite
sheets were set up. I think the size of each
frame does not match up. So it creates an offset. Now, that would be something
you can either fix in a editor like A Sprite, if you are the artist, or if you just want
to make it work, for this case, we can do that
in the animation player. So in animation player, we can go to let's say let's
check the idle animation. And here we have it centered
here as we would expect. So when we go to the
run animation of Run, we just need to
make sure that this matches the idle animation. So we're going to
want to key frame the position on each
of our animations. So let's add a track,
property track. Let's see, the
Sprite two D because we're talking about
the sprite offset, not the player character offset. And then we want position here. So let's add a key. Right now it's at negative 14. I'm going to go to Idle. And then we're going to add
the same property in advance. So position two D and then
add the keyframe here. The reason I'm doing this is because the idle position
is already correct, so I don't want to have that change when I'm
setting up the run. While we're at it,
let's check Death. I think the death animation is actually at the right spot. So all we need to do is go here, add the track, sprite
two D, position. Okay, insert the key. Okay, and then that
should be good. So now we need to
change the value for the round to animation
specifically. So hit Q to go to
Selection Mode. Left click on the sprite. You should have this big box. You can see probably
where the offset came from here because there's so
much empty space up here. But anyway, so we just
need to hit W and then move this up to
right about there. Let me check the transform.
Yeah, negative 27 pixels. I think that's what I had and
the in the draft project. So let's keyframe
that. Let's hit Play and see if that's better. So WASD, it's almost there, but I think it needs to
be a few pixels higher. So let's W and
then move this up. Let's try 30, hit W. N,
it's still not there. Okay, let's move this up more. I'll look at the idle animation. Okay. And that's where his
feet are on the ground. So let's go back to run. Okay, and his feet
should be up here, two pixels under the bottom. Okay, and then
idle, and then run. Okay, I forgot to key frame it. So let me move that once again. I don't think we
need the X offset. Let's key frame that at
negative 30 pixels here. Okay, let's hit play
and test one more time. WASD. That's looking
correct to me. So we'll go with that. Let's
hit Sop on the player here. And just to be clear, as
a more long term fix, it would be better to
probably go in and fix the animation spike
sheets and just making sure that they
all have equal size, regardless of how much
movement there is on the animations so that you don't need to mess
with the position. Long term, this is a little
bit harder to manage, so it's better if
it's just fixed on the animation sheet directly. Okay, so for our characters
idling and movement, so the last thing we
really need to do for our characters basic
movement is to make it flip when we're moving to the left and then flip back when
we're moving to the right. That'll be the next video,
so I'll see you in the next.
9. Flipping a 2D Character: So we have our up down left right movement for
the character, and it can switch back to the
idle state. That's great. The thing that we need
to change here, though, is that our character
needs to be able to flip from right to left, depending on if we have
a left or right input. So we want to basically flip the sprite node
from left to right. Now, Sprite does directly
have a flip H here. We could certainly
just tell it in a custom script to flip the H based on whatever the
player input does. To go one step a little
bit removed from that. Instead, what we could do is
actually put a parent node which flips itself when the player input facing
direction changes, and then that will
automatically flip the sprite node because
the sprite node is a child of that parent
node two D. So if I click on player node
and I add a child, let's add a node two D, and then I'll call it flip two facing two D. Let's right
click it, attach a script. We'll put this in the
default location, create, and we'll make it class
name flip two facing two D. And this is going to flip the X scale on the transform, depending on if the character
is facing left or right. Okay, so what does
this basically mean? If I grab this bright two D and I pull it under flip
to facing two D, and then let's put this back
up where it was before, towards the top
of the hierarchy, I'll take flip to facing two D, and then over here on the
right, uncheck the link between X and Y because we're
only changing the X scale. Okay? And then type in
negative one for the X, and you'll see that the
character flips to the left. If we type in one to the X, the character faces the original direction,
which is to the right. Of course, you need
to be a little bit careful with the setup. If you put your weapons under
the flip to facing two Dno, then that means your
weapons are also going to be facing
left or right. Now, sometimes you want
that, and in other cases, you don't your projectile
launching system later on, you don't want it to flip based on what this node scale is. But if you were to, let's say, put a spear in the
character's hand, you probably would want
that sprite to face the right direction
when you're facing right or the left direction
when you're facing. So you just have to be careful about what nodes
you put under here, because anything that is
under flip to facing two D in the hierarchy is now going to flip whenever that
parent node flips. So just keep in mind what's parented to what, and
you should be fine. So inside of our flip
to facing two D script, we want to add a export var. And we're going to say input, which is of type player input. Now, for right now, being dependent on player input
means that this script is specifically a player's
script and not necessarily used by
the enemy scripts. For a script this
small, in this case, it might just be worth having
the direct reference to the player input because
that just gives you the simplest, most
practical setup. And then the only thing you
need to make sure is that the input is actually
assigned on the inspector. So we can do that
with the assert. Let's do a function
underscore already and then assert that input
is not null and say, in order to flip reference to the player input is required, once again, you can always
just go to the top hit play, and it's going to
hit your assertion, so that that test is good to go. Otherwise, at this point, we can just get the
signal access from player input and use that to flip this node whenever the
facing direction changes. If I recall, I haven't
put the signals in yet. So if we right click
on player input, look up symbol, we want to go to the top and
declare some signals. So a signal is like an event. If you're coming
from other languages like C Sharp, it is an event. And in this case,
what that means is something triggered inside
of your code and you want to notify
subscribing objects that something occurred and optionally pass some
contextual information or data to the receiving object. Okay, so let me go
ahead and declare one, and then I'll explain
it a little simpler. So signal is the keyword, and then we would
say facing changed, and we'd want to pass in the
direction it changed too, so we could say value or facing, and that is going to
be a type factor two. So we're declaring
the parameter. What we're sending to any receiving objects goes
in these parentheses here. So the signal is facing changed. So the event whenever the facing property in this
case, down here changes, we want to send out
a signal that lets any subscribing objects know that the facing
value has changed. So at that time, they can respond to it
in their own code. And what this means is that you don't have to be constantly checking the facing property of player input on every
single frame update. You just wait for the script to notify you that the value has actually changed to
something different. So you have that
meaningful information, and you only need to update like once every few seconds
because your player only changed its
movement direction once every 3 seconds or facing
direction in this case. In other words, you become
dependent on a signal triggering your other
code rather than constantly checking a value
for changes manually. So with our facing
property down here, we want to take it and add a setter so that when the
facing value changes, we actually emit the signal
whenever it changes. So if you add a
colon at the end, this allows you to declare
your setters and getters. You can just do one of them. You don't have to do
both. So in this case, we want to say set value. And note this is on a new line. So in this case, we really are declaring a function,
though, actually. And this just runs
whenever we set the value of this property
to something new. So we're changing the default of just setting the value and doing nothing else to setting it and doing some extra code. So when we set the value, we want to take facing
and set it to the value. So that would be what
it does by default. If you don't declare any
setter Getter functions, the Getter would just
be more like this, return facing, right? That's
what you would expect. You access the property by name, facing, and it would return
the value that it's set to. So if you don't need custom
code for the Getter, you don't need to
declare that because that's what it does by default, and this is what facing
would do by default. Now, if you want to do
something like emit a signal, then you do need the extra code. So now we want to say facing changed the name of
the signal dot emit, so we emit the signal, which means any object subscribing to it is now
going to be notified, but we want to pass in the
information of the facing. So we pass in facing. So we're taking this value, sending it through the signal, and then in our other scripts, we're going to receive
that information, and we can decide
what to do with it. Now, there's one other trick
I want to show here, though, which is that we
don't want facing change to get emitted whenever
we set the facing value. This might be getting set to the same value on every frame. I think that the is
movement action check actually guards against that because this would
only actually trigger when there's a real
movement action, which would be, in this case, like pressing the right key or letting go of the right key or the same for the
other directions. But it's always good to be more defensive
in the programming. Just assume that your
other assumptions aren't necessarily correct and
then to guard against it. So we're going to
put a guard here. If facing is already
equal to the value, then that would mean
that there's no change. So we just want to return here. We don't need to set facing because the value is
already assigned to facing. It's already correct, and we don't need to emit the signal, and we shouldn't emit the signal because the value
doesn't actually change. So we have this guard against bad sets to the facing property. But if that guard is passed, then we set the facing
and we emit the signal. So with all that, the signal
is now going to emit. And we can just go to our
flip to facing two D. And after we assert that
Import is not null, then we know it's set. So we can go to input
dot facing changed. You'll see the signal is actually referenced
by name here, and it has that little
Wi Fi symbol or signal. So we put input dot
facings changed. So now we're looking
at the signal name, and we want to connect to it. So dot connect. And what do
we want to connect to it? Well, that would be
a callable function that we set up here. To keep things, I think,
as simple as possible, we should create a custom
function at the bottom. So I'll say underscore
facing changed. It's going to still
error because there's no function with that name
inside of our script. So if I hit Enter
a couple of times, we'll say function,
underscore on facing changed. Now, what parameters is this
function going to take? Well, it's going
to take whatever was passed into the signal. So in this case, you'd be
looking at facing direction. So I'm going to say
P, underscore facing, P as in parameter, making it very clear
that this value is coming from the parameters that were passed
into the function. And then this is a vector two. We're not going to
return anything, so we're returning void. And now we can write
the code to respond to the signal emitting
facing changed. Okay, so when facing is changed, we want to take
the X direction of the facing and use that to determine if we should
face right or face left. So if p dot facing dot
X is a positive value, then what we want to
do is we want to take the scale X of this node two D, the flip to facing node, and we want to set it equal to the absolute value
of the scale dot X. When you take the
absolute value, you're making an always
positive number. So to face the
right, we're making right our positive direction. So we always want
to face the right whenever the input is
actually facing to the right. Now, we're not
going to say else. We're going to say else if P
facing X is less than zero, so we don't want that zero case because our characters
should not be able to face the 00 point on the
vector two that doesn't really exist as a facing direction for
this type of two D game. So we don't want to do
anything when that's the case. So that's why I'm checking if the value is less than zero, because less than zero would
actually be facing the left. So we do scaled dot X, and we're going to set
that equal to negative one times absolute
value scaled dot X. Okay, so this is going to
always give a positive number, and then we're always
going to make it negative. So there's probably a few different ways to
math this out here, but I think that
should work just fine. Okay, so we go to two D view. We click on flip
to facing two D, and we assign the player input. Okay, so that's where we're
going to get the signal. We're going to connect to it on Rudy back in our facing script. Okay, so if I hit play, let's do right movement. Our character faces the
right, and if I hit left, our character faces the left. If we do top left,
we're facing left. If we do down right,
we're facing down right. Just test all the
eight directions our character can move
in with keyboard input, and that looks good to me. So just to be a little
bit forward thinking, one thing we could do is grab this code and pull it out
to a different function, refactoring it into
a new function. So say function update, flip, and this will require
the facing direction. So P facing, vector two, and then I'm going to take this, cut it down here and on
facing changed actually going to call update flip P facing. Now, the reason for
doing this is so that I can also reuse it on ready. So on ready, we'll just say update flip Import dot facing. So when our script first loads, we want to set the facing
direction up immediately before any Import actually comes from our player because it might
be the case where we decide, let's say, in player input, we export this facing direction, and then we change
the default of vector dot right to vector two dot left because for whatever reason we
want our characters to face left by default. So now with this, we can be prepared for that
by automatically initializing with the
right flipping direction based on whatever
the input is saying. So now, this is a
public function. We could just call this from
another script if there was a non Import related case where we wanted to change the flip
direction of the character. For instance, maybe
during a cut scene, you want to cut out
all player input, and you want to
call this manually. So your character was
facing the right, and in the cut scene, you
want it to face the left. So you'd have the
option of just calling Update flip on the flip
to facing two D node. Okay, so definitely opens up
some possibilities there. And that is pretty much all
we're going to need to do for the WASD movement
of our player. It's looking good. I think all the concerns are
separated pretty
10. Grass and Dirt Tile Map Layers: Okay, so since our
characters movement and animation so basically playing
for both Idol and run, including the
flipping direction, I think now is a
good time to start putting in some background
so the game looks a little more pretty
rather than just having that default
Gadot gray background. So let's go ahead and
switch to our world scene. If you don't have that open, you can go to the bottom left here. Just search for world, and you should be able
to find world dottSCn if that's the same name
you were using like I was. And then in the world hierarchy, we want to right click
and add a new node, and we're looking for
a tile map layer. You can see, there was
another node called Tilemap, but that's been deprecated,
I believe, since 4.3, and since 14.41 and
about to go to 4.5, it's definitely
recommended that you switch to tilemap layers, which breaks up the tilemap into individual layer nodes rather than having every single layer. Contained inside
of one tile map. So let's add a tile map layer. And we're going to call
this something like ground, so I'll rename it to ground. And if you have it
selected out the bottom, you should have the
tilemap window. So open that up if
it's not open already. And then we need to find the
art assets from our project. So I'm going to go
into art and then Pixel crawler
environments tile sets, and we're going to be wanting to work with the floor tiles here. So for us to bring this on, we don't want to
be on the tilemap, but rather the tile set tab. Look in the top
right hand corner while you have the
tile map layer selected and you'll see tile set as a resource you
can assign here. You want to left click
and do new tile set, once you do that at the bottom, there'll be a new Tilset tab. So once you've
renamed it to ground, look at the top right
with it selected, and you should see a
field for tile set. So we want to click here and declare a new tilesetRsource. You do that at the bottom, there should be two tabs here
related to the tile map, which is tile map for actually selecting and
drawing this tile set, which is for setting
up your tiles, and then tile map for
drawing your tiles. So we want to go to Tile set. You should see a box over here that we can drag the
floor tiles into. So find floor tiles
inside of the project. So it's floors underscore
tiles dot PNG, and we'll just drag
and drop that in here. There'll be this little pop up, which asks if you want to automatically create
the tiles, we do. So we'll go ahead
and hit us here. And then you'll see
in the preview window that all of the tiles
have been automatically sliced into actual
tile resources that we can use and
paint from in the game. So the simplest way to draw would just be
to go to Tilemap and then select those tiles individually that
we wanted to place. Hit Q to make sure you
are in selection mode. And then down here in
the tilemap window, we want to be in paint tools, so that's D on the keyboard. And then you can left
click to draw the tile. You can go over one and left click and put
another tile down. You'll see that right now, the Tap is actually showing in front of the player.
That's not what we want. What you can do is just move
the ground to the top here, and that'll
automatically sort it, so the ground is
under the player. So that's the simplest
way to do that. So while selecting the tiles one at a time like
this will work, it's very, very tedious. So what we probably
want to do instead is create a terrain set based on these grass tiles and then use
that to automatically draw which tile we need depending on what is adjacent to that
tile, if anything at all. So if you expand tile
set in the top right, you can go down to terrain sets and then add a new element. So we're going to leave
it on match corners and sides and you expand
terrains and add an element. And we can just rename
this, let's say, grass terrain change the color to something a little bit
more grassy, like a green. Now at the bottom,
go to tile set, and then we're
going to do paint. We're going to select from
the property editor terrains. We're going to
select the terrain set as terrain set zero, and then the terrain
we're drawing is grass. So it should be pretty obvious which ones are
the grass tiles here. But in order for the tiles to build correctly
and automatically, we need to select the tiles that are supposed to be part of
this grass terrain. So you can actually just
left click and hold and drag a box around all of these grass
tiles, kind of like that. If it's highlighted, then
that's going to mean it's basically marked as
part of this terrain. And this is dark grass here. We could set that
up later. Okay. And then we may need to get
these ones down here as well. We'll see how that goes and
these light tiles there. Now, these bottom ones
look like they're more a dark grass. We can try to include
them and see how it goes, but they may not make
it in the final cut for the terrain that might be more
of a dark shade of grass. So we'll have to see how
that actually tiles. Okay, so we've selected all
of these tiles as grass. Now we want to go to
the top left one, and we want to mark
the corners and sides that are grass or are
adjacent to grass. So the simplest way to
think about it with this tile set is just if the area is
transparent, generally, you aren't going to mark it, and if it is solid grass
or near solid grass, then you are going to mark as being grass or grass adjacent. If I just left click
here and drag around, you're going to see it's
filling in the cursor, and we want these eight
squares that aren't in the bottom right hand
corner to be marked as grass because
this right here in the bottom right hand corner is saying that there's
something else down here. The tile to the
bottom right is not grass and this tile
is reflecting that. So it'll basically use
this tile when everything down to the right up left
and these three corners, are grass, grass adjacent, but the bottom right one is not. So the idea is you have one
for all of these scenarios. So in this case, you would
do these top six tiles, and this means that
if there's no grass underneath it or to the bottom
left or the bottom right, then this tile would get
automatically assigned. So we just want to keep doing this kind of thing
all the way around. So this one looks
like all the corners and sides except
the bottom left. And then this one
is the top four. This one is, I think, just like this one
up here again, the top eight to the right. And yes, it is possible to
have multiple tiles that have the same I believe they
call it a bit mask here. So if we go down to the bottom, you'll see that these are
all nine square tiles, for sure, because they're all
grass and grass adjacent. So you would actually fill
all of these in all the way. And then that will fill in everything inside of
these edge tiles. Okay, so we just kind of
need to keep doing this. So this one up here at
the top is going to be the middle top to bottom, and the right top to bottom. Here we have the eight sides and corners except the top left. This one's the
bottom right four. This one's everything
except top left. The bottom six tiles, everything except top right, the bottom left four, everything except top right, the left six, everything except bottom and
the top left four. We have our corner
pieces up here, and we have the grass
solid pieces down here. So to draw with it, we just go to Tilemap and
then select terrains, grass. And what we can just do is
grab this rectangle tool. Let me zoom out the
camera here a little bit. And we're going to zoom out with Middle mouse wheel
on the game map, and we can just
left click and drag a giant box into the background. And there is our
background grass. Now, you'll see that these
tiles were, in fact, darker. It was a little hard
to tell on my screen. So maybe in the tile set, these are also dark grass that it really looked
the same to me. So the solution to this
is just that we remove the dark tiles from the light tiles when we're
assigning the grass. So if I right click on any
of these bit mask pieces, then that's going to remove
it as part of the tile if we fully remove all the nine
sides, corners and center. Now if we redraw this again, those won't be an option when
it's filling in the tiles. So I'll just left click and
drag another box over it, and there we have
the light grass and only the light grass
for this entire scene. Now, in a survivor like game, your character may move
around the map a lot. So I'm actually going to
make this a very large area. We'll just go from here
to here just so that there's plenty of space for the character
to move around on. Now, you can see it
actually takes a while to fill that in because
that's a lot of tiles. Okay, I also want
to show if I hit E, to go to Race mode and
then D for the paint tool. Then we can left click
here, and you'll see that it automatically assigns the adjacent pieces based
on the tile in the center. So this tile has no tile, so it automatically
recognized, Oh, I need the corner
and side pieces that we assigned in the terrain. And I can just keep using
the eraser brush like this. And you'll see that
it's automatically generating the
sides and corners. It may have some limitations
like needing to go, like, a two by two and a line
in order to look right. It depends on the
tile set, really, because there's different
ways of building the tiles. But at the very least,
this is much, much, much faster than if
you had to manually assign all of these
side and corner tiles. Just letting the computer
figure it out for you based on the terrain
rules that you define, like we just did, is
a huge time saver. So we're going to fill this in. There's also the paint
bucket tool down here, so we can just click on this area where all the
tiles are the same. And oh, well, we also have
to E to remove the erasuod. And now we left click and
it all gets filled in. As you can see, it will select whatever region
you're looking at. So that could be
this entire thing. If you want to fill it in with a totally different
tile terrain, you could do that by defining a dirt and switching
to dirt down here and declaring it
down so this is a little more advanced than
I actually intended to go, but let's actually define another terrain with
this dirt over here, the kind of orangy dirt so that dirt can
transition into grass. So let's in the top right, declare a new terrain type, and I'm going to call this dirt. That brown is fine, I think. You might actually not
want to use, like, a brown on brown tiles so that it's easier to see where you've defined and where you haven't. But if it works for
you, that's fine. Whatever color you want to
assign is perfectly okay. Okay, so we're going
to be working with paint dirt in the
tile set window, and we're going
to do exactly the same thing with
these dirt tiles. So here, we're painting
the top left eight, not the bottom right, and
then the top six, like that. And I'll just keep
going around like this. This one's a little unclear, but I'm going to go with
the top right four, and we'll see if it
works correctly. If when you actually draw with your terrain,
it doesn't look, you might have missed one of
the bits here or there for, you know, whether it's on
or off or center or sides. So sometimes it does
take a little bit of trial and error to get right. But definitely worth
it in the long run. So just keep going around, you know, same shape. Get the corners and sides where it is dirt and mark it as dirt, and then just keep
going until you finish all the way around
in the circle. Okay, and then finish up with the bottom three
tiles down here. We'll get the light dirt and just skip over the
dark dirt for now. Okay, and that looks good. Okay, you know what?
I mentioned that I was going to handle
the transition from the dirt to the grass. But actually, with
this tile set, I don't think that's needed because these tiles
individually, they have kind of like transparency rather than
grass here next to the dirt. So you don't have to define the individual grass terrain as part of the dirt like this, where you would take these
other tiles and say, Oh, that's grass over there, and then dirt in the bottom left. Because it's
transparency over here, so that's actually not needed. But in other tile sets,
you might need to do that. Where if you know that like one corner is supposed to be grass, then you mark it as grass there. So having two types of terrain
for one corner or side. But here, I think
we only need one. Let me give this a shot and see how that's going to
look. So I'll go to Tilemap. We'll do dirt, and we'll
try to draw over it. I'm thinking what
we actually need. Okay, so yeah, here we
can see that it's marking the sides as
transparent. I'm wrong. So I'll try marking
these sides as grass, but I don't think
that's actually going to give us the expected result. I think what we
need to do is just have two separate layers for
this particular tile set. Okay. So I'm going to
try drawing with that. So tile map mode and then draw with dirt and
let's try that. It's still kind of
messed up. I'll just try a box. Yeah, still doesn't work. So Control Z undo a few times. And what we're actually
going to want to do is just paint with
dirt over the grass, or grass over the dirt depends on which layer
you want to be on top. So I'll right click on the
root, add a child node. Let's say tile map layer. I'll rename it to
be let's say dirt. I'll move this below
ground so that by default, it shows above the ground. So whatever's on bottom
is actually on top. Not sure why it's
set up that way. I just is. And then
we'll go to the dirt. Oh, and we want to reuse the
same tile set, by the way. So in ground, I'm going to right click on the tile
set and save it. This is on the top, right,
by the way. So save it. Locally into the project. So I can create a new
tile sets folder, and then I'll just say
ground tileset dot TRs save. Okay, now, if we click on dirt and then go
to the inspector, we can click the dropdown, Quickload, ground tile set. Okay, now with this
tilemap layer, we can use the same tile sets that we've already
previously defined. That's kind of how
resources work in general. It's very easy to
share them across multiple nodes or
multiple scenes, et cetera by sharing the
resources like that. Okay, so we're going to
draw with dirt here, and then I'm going to
draw with dirt over here, and you're just
going to see that the dirt just sits on top perfectly because we're
painting on different layers, so this just shows on top of
the background and because there's transparency over
here on these side edges, the grass basically
fills in the gaps. The other method I was
showing where all of the texture information like
the grass and the dirt is all in one tile sprite would be more where you set up the multiple
terrains in one tile, which is more
complicated, honestly. So I'll rename the
ground to grass. Because we're going to
be more specific there. So we have two tile map layers, and it's much easier
to manipulate them than tile map
would have been. Okay, let's see where we're at. We probably want to more, like, draw a dirt path. Okay, so in order for the
dirt to assign properly, we want to draw a
two by two line. So you want to be using the rectangle tool
or the keyboard. If you do just a one tile line, it's going to end up
kind of like this. Based on the tile
site, it's not sure how to fall on the
information here. There's no tile where
it has transparency on top and transparency on
bottom, so that won't work. You actually need to
do two pixel tall path like this and then
just drag your box, and that will work for that. So we could just kind of make
some paths on the level, kind of like this, you know, just drawing two tile wide paths wherever
you want them to go. And that's going to kind of make it a little
more interesting than just having
plain grass tile. Though it's completely up
to you how you want to, like, you know, organize this and make it
more interesting. So, like, if you've ever played, let's say, Sim City, that's exactly kind
of what I'm doing right here right now, like, creating the road system and basically putting
it wherever I want. Not going to fill any
residential zones in anywhere or
anything like that. But just having some more
tiles on the map is a bit more interesting than
literally just grass. Okay. So we'll see if that's
good. I'll hit Play. And yeah, our character kind of has something
to walk on now, so that's a little
bit more interesting.
11. Placing Trees as Map Props: And now I think it
would be a good idea to add in some props
to the game level. So we have some props right up here in the environment
folder as well. So there's some animated
ones and static ones. We could just start
with trees as the simplest prop we can
put into the game level. So let's grab uh, I don't know. We'll double click here and you can see the tree
models up there. So we might want to
grab these big trees, and we could just do that with a sample Sprite two D node. And we can put these
in super simple. All we need to do
really is go to the PNG file and drag
it into the game scene. It's going to
automatically create a Sprite two D up here. So just rename it to Tree then we're going to
take this texture, and we're not going to
draw the whole texture. We're going to draw
a region of it. So what we want to do is use a atlas texture so we can select the
region we're drawing. So right click here,
and I'm going to copy this image real quick because we're going to
click on the drop down, go to Atlas texture, which is going to unset
that texture temporarily. Expand this and then
right click and paste the texture we just
copied into the atlas. And there we have
it show up again. Now we want to
click Edit region. And then inside of here, we'll
go to Snap mode, Cid snap, Middle mouse wheel
in to zoom in, and we'll just grab
this tree like so, hit close, and we have
a tree in the scene. Okay, now, depending
on your game style, you may decide to add
collision shapes to your tree. You would do that the same way you would with, like,
a player character. So if we look at the
player character, you can see it as a
collision shape here and you can make it so that objects will collide with the trees. However, for this demo, we actually want to minimize
the amount of collisions that our enemies are going to be doing with any kind
of game character. If we have a lot of
collidable objects inside of the game and we don't
put in something like a navigation agent or use character body two D so they can move and slide along
the collision shape, then you're going to get
enemies getting stuck. So much simpler than that
would just be to skip over the collision and just let any character walk through
the tree, have it there. Strictly for visuals.
We have the tree here. I'm going to right
click on the scene, and we'll just create a new
node for all of the trees. So I'll say node two D, create, and we'll move this
out of the tree, so it should be underworld. And I could just
rename it to be props. I'll move the tree
inside of here. And then we can just duplicate this tree as many
times as we need. So I'll first right click and
save the branch as a scene. We'll put the tree in
a new folder, I think. I'll call it. Let's
go with Objects. Yeah, and I'll
just put it inside of here. So tree dot TSN. We can hit Save N if we
want to edit the tree, change it to a
different type of tree, then we can just
jump into the scene. So to do that, you just find this little icon here
and you open an editor, so you can edit this tree and
change it to whatever you need if you want to change all of the copies
of it in the world. Let me show an example of that. So we have the tree. I'm going to duplicate it a
bunch of times, and then I'll hit W to move
the copies around the scene. So select tree two, W, move it around the scene. Move mode is right here.
Select mode is here. So you need to select it
and then go to move mode. And now I want to
change the tree, all copies of the
tree across the game. So I just go to the tree scene, and then over here on the right, we'll expand the atlas
edit the region, and then let's select
this red one instead. So red tree, hit
close, hit save, and now back in the world, all the trees are that red
autumn leaf kind of tree. Now I'm going to go
back to the tree, and I'm going to undo that. So let's hit Control
Z a couple of times. We'll just go with the
standard green tree. And if you want two
types of trees, then just find the tree scene
in your hierarchy here. So you can just right click
here and duplicate it. Let's call it tree
autumn and hit Okay. Open up tree autumn so
that's double click. Let's zoom in on the tree, and I will take the texture
and edit the region, and we can just grab these
red trees from over here. And now if we go to world, we can put the autumn trees in, or we can put the
tree in just dragging the scene straight into the
parent scene, the world. If you get used to
that, you can pretty much populate the
scene pretty quickly. Now, one thing I do
want to change here, and maybe I'll need to
demonstrate to make that obvious is that when
it comes to Y sorting, the position and the tree scene
of the sprite is going to matter because we're going to Y sort from the center position. So wherever the character
touches the ground is generally where we're going to want to position the sprite. So up here in this style game because I did that with
the player, right? So we want to make
sure characters are sorting from where they stand. That also applies to objects. To demonstrate why
that's a problem, let's go back to the
main scene props. I'm going to go to ordering
here on the right. D Y Sort enabled in the
world, do the same thing. Go to ordering and
choose WYSort enabled. Okay. And now if we play and we move our character
in front of a tree, well, the cameras actually
not letting us do that. So let's add in a
follow cam so we can actually move around
the map a little bit more. I'm going to take
the player, right click, add a child node, remote transform two D, and then in the right, we want to assign it to the camera. We want to update the position and not the rotation and not
the scale, so uncheck those. Okay, now if we hit
play, so stop and play. The camera's going to
follow our player, and that's a basic
follow camera in Gudo. Okay, so we get to this object. If we're up here, you
can see that we're sorting behind the tree,
which we would expect. But if I go to the
halfway point, now the characters standing
basically in the tree. Visually speaking, we still
want that character to be behind the tree until the character gets
to about here. Otherwise, it just looks weird. So to fix that jump
into the tree scene, you can click here
to open an editor. And we do want this
sprite to be up here. But I think if you just
move the sprite like this, because it's the root node, it's not going to fix anything. So we'll go back to the world, and yeah, let's just hit
play. You'll see what I mean. We'll go over here, and
it's going to change the position where it
instances these trees, not the position
where it sorts from, right? You see the
problem still there? So we need to go back
to the tree scene, and I'm going to right click. I'm going to add a child node, a node to I'm going to right click on this and make
it the scene root. So this is the real
tree at the root, and then this is just
the sprite two D now. So when you instance an object
inside of another scene, it instances the root basically
at its center position. So this is the center of
the root parent two D node, and now we can
offset the sprite. So if I take this sprite, well, it's already actually offset, but you can just W and move it to where
you need it to be. Just remember that your
transform position is an offset from the root, and the root is where
it's actually going to be instanced at the 00
point right here. So that's why this
is needed to have a base no two D and then you
sprite nested beneath it. So we also want to do the
same thing with tree autumn. But you might
notice that because these objects are very similar, they're actually repeating
a bunch of steps. So what we can
actually do is have an inherited scene based on the summer green tree where we only replace
the srt image, but everything else is
the same for that tree. So I'm going to delete the autumn scene down
here. Autumn tree. Let's delete that. Oh, I got to delete it and
close the scene. So delete that and then up here, make sure you close this
as well. So don't save. And then in the
world, it may still have a copy of the scene because it doesn't
completely delete it. It's just that now there's no parent scene for this to go to. I'll cut that out as
well and save that. So we've moved all
the instances. Okay. And now on
the bottom left, we want to right click on Tree, do new inherited scene. So now we have unsaved scene
based on the parent scene. We can save this as tree autumn, and we want to put this into the objects folder.
So save that. And the only thing we're
changing here is the sprite. So hit Edit region and grab the sprite for the
summer tree, hit close. And if we go back
to the main scene, you'll see we'll be
able to instance the tree autumn again. It appears I actually messed up. So we need to make the
atlas texture unique. Otherwise, you end
up with this where it's replacing the trees. So now on the tree autumn scene, we want to take
this bright two D, and the first instinct that I had was actually to just edit the region and to change
it to this down here. But that's actually incorrect. And let me explain why. So, right here, this
is a texture resource, and resources get shared between any scene
that references it. And both our tree and our tree autumn are
referencing the same resource. So if we change its properties
like the atlas region, just like the edit region
here on the autumn scene, it's going to actually
update those same properties on the original
tree scene as well, because they're sharing
the same resource. So it's not a new copy
of the texture resource. It's literally the
same resource. So we want to right
click here and then make it unique in the autumn scene. And now we can change this
to this texture down here. So select the summer
tree and hit close. Hit Save. If we go over
to the green tree, you should see it still has the green tree sprite.
We can go over to world. Looks like a couple
of these trees kind of got unreferenced, so we might want to
delete those real quick. So I'm actually going to
take props and make it 00. And then I want to take the props node and
I want to lock it. So if you click up here, you can lock it, and yeah, that should keep it from moving. So if I hit W and I try to move the props node now,
it's not going to move. So one important thing to note is you have a parent
node two D here, props, all its
children's position is actually based relative
to the parent's position. So if you have a grouping
node like props, but you still need
it to be a node two, probably a good idea to lock it. So I would apply the
same thing if you have a enemies or projectiles
grouping node, which we'll probably
have later on. Now we can just add
our trees to props, so I'm just going to
pop a few in there. Okay, so if I take this tree that we just created and I hit W and try to move it up
past the cening point, then you'll notice
when it gets here, it's just going to disappear behind the grass and the dirt. So that is because
of why sorting. So what we're going
to need to do is in the top left
for grass and dirt, we're going to need
to select them. I think you can even select
them both at the same time. And then go to the
right hand side. We're looking for
ordering and take their Z index and set it to something like let's
say negative ten, any value that's negative. So this will make it so
that these are always behind anything that
has a Z index of zero. So if we select the
tree now and go to W and move it, now it's fixed. It's always going to show on
top of these other objects. So this is just about how
we're layering our scene. Some objects, you
want to sort with other objects on
the same Z index, like the character in the tree. But then other objects like the tile map should just
always be in the background for these ground tiles because the characters are
standing on the ground, so you should see the characters before
you see the ground. Okay. And now we
just kind of want to fill on the scene
with some more trees. You can just select
them and duplicate them and move them up. Make sure that you put them in the props node as the parent
just for sorting reasons. And then we'll just
kind of select these and duplicate them. You can duplicate more
than one at a time. So if you just kind of
want a certain pattern, then we can just kind of make a group of trees and then just
copy all of these around. You can even move these
into its own subgroup, like having like a
tree block parent, and then in that we'll have a bunch of
different trees and you can hide it
so you don't have so many tree nodes
populating props. If that helps you
out. Okay, so let's move some trees around
a little bit more, and I could just kind of
select all these ten trees. And I'm going to yeah, let's try the blocking
thing I mentioned. I'll just right click
and add a child node. Let's say node two
D, tree block. I'll grab all the trees, move it under the tree block. So just select them all. And now I could save this tree block scene to the project, and we're going to want to
save that Objects tree block. Now we can just duplicate
the tree block, control D W, move it around like
this and kind of just fill in the scene as we need with trees wherever
we want them to be. At some point, you
probably want to customize this a
little bit more, but I guess this is a quick way to kind of prototype the scene, and hopefully it
doesn't look too bad when we hit Play
and we zoomed in. But yeah, if I was doing
this tree by tree, this would take, you know, probably, like, 20
minutes or something. So if we do need to make
some customizations, like right here, if we zoom in, I see that this tree you know, kind of close to the road, maybe I want to edit that so I can hit Q and then
left click on this. We can see it's part
of Tree block three. We can right click
in the hierarchy and do Editable children. And then for this
one, specifically, we can now select the
trees and move them. So like this, we have kind of more of a
customized tree block. Okay, and we can hide that.
So now we're just looking at 15 tree blocks rather
than 150 trees. We can play, go into game mode, and just walk around
our game world. And from this Zoomed in, it's pretty hard to tell that, it's just copy pasting the same tree setup
like ten times, but it's a lot less boring
than just having grass, right? Like, we have the
path, we have grass. If we get some flowers
or other stuff in here, it might actually almost look
like a legit game level. Oh, and here's a
tree we can fix. So, you know, a little
bit of play testing. You can find stuff
like that. Let's zoom in and fix that one real quick. So this is part of
Tree block two. If you look on the
screen, you can actually see the name of the node that we're
hovering over. So this is the parent node, the one that the scene is
Enstus the tree block, not its child nodes which are contained inside
of that scene. To make this editable, right click on Tree Block
two, editable Children. Hit Q to go to Selection Mode, select this tree,
move it around, and we can grab
this one as well, move it out of the way, and just make those
little edits. And yeah, from being zoomed out, it looks like none of the
trees are in the road anymore. So that's probably
the bare minimum of what we need going on.
12. Adding Stumps & Fixing Y Sorting: So I was looking through
the pixel crawler pack for some flowers that would be able to be put around the scene to spruce
it up a little bit. Didn't really see that in the free version, unfortunately. But I guess we have
these stumps over here. We can try to use
that as a prop and just kind of fill in the gaps a little bit and make it
a little less dull. So we'll take this and create
a new stump scene from it. So I'm going to basically repeat the same
steps with the tree. I'll drag this to the scene. Let's go to texture. Right, click Copy. So copy that texture. And then on the drop down, click a new atlas texture, and then we're going to
paste in the atlas here. Like so. Okay, so you
should see the whole image. Now we want to edit region and select the stump that we
want to use in our scene. So Uh, I think this one has a
slightly less reddish stump, which looks closer
to our actual tree. So let's do that one. Hit save. Now I'm going to right click this and save it to a new scene. So we'll say stump dot TACN. Now, if you recall, we want to offset
this stump, as well. We may need to anyway. I think in this case, it does need to be
a few pixels down. So that means we need another
node two D in the root. So right click Add a child node, node two D, and then right
click and make this the root. Okay, so now this is just
a Sprite two D. Okay, so the stump texture
is a Sprite two D, and then the root is stump. So now we'll save
the stump scene. Take the sprite, hit W, move it up probably to right around there for
Y sorting purposes. Go back to the world.
Now we have the stump. Let's create some more stumps and create some more stumps
all the way around the scene. If you happen to accidentally select the grass or the dirt, what you can just do is
go up here to the top left and lock the
nodes like that. And now you shouldn't be able
to select them, I believe. So if we hit Q and select the stump or
around the stump, Okay. So now these nodes
shouldn't be able to be selectable by clicking
around on the map. Let's go down to the
stumps down here. They're not using this
right base scene. I'm actually going to just
select these and delete them. And let's get rid of the
node two D, as well. Okay, search the
project for stump. Move that into props like that. Okay, now we zoom out. We find this sump, hit W to move it, position
it where you want it. Controlled you to duplicate it. Okay, now it has the name stump to when it's parented as props, so that's more like
what we're looking for. And just put some stumps
around the scene, you know, just things that you can encounter that are more interesting to look
at than just grass. Of course, the pack
has other stuff like buildings for,
like, a crafting game. I didn't think those are
the most appropriate. And if you're really going
to put buildings in, I think you would start
to argue that you really need the collision shapes and possibly navigation agents to make it more believable. But we're keeping it as
simplistic as we can, 'cause we're just
doing, like, you know, a really simple
survivor like game. I think there's enough in
the course already that, you know, I don't
want to overwhelm anybody, but if at the end, you really want to understand the navigation agents
and stumps, let me know. I do have some videos on
YouTube for that sort of thing. If you look up Chris
tutorial's navigation agent, you should be able
to find those, if you want to get
started with that. But yeah, this looks
like a decent scene set up. Alright, play. Let's run around. Our scene. We got the stumps. Now, we can just walk through
it. That's okay. Now, here, this is
a little less okay. We can see that the sorting
on this tree is not correct. In fact, the tree is
not sorting at all. I think so I guess here
we need to go into the autumn tree scene
and try to fix that. Okay, so let's go to Local scene view over
here on the top left. We're looking at the
tree autumn scene. Click on the root. And maybe what we
need here is to check why sorting
enabled for this scene, and you might need to do that in the parent scene, as well. So ordering Y sort enabled. Note that my games
still running, so I can tab back and see immediately if
it worked or not. And currently, it
is not. So let me see that tree in the world. Is it actually part
of the parent? Oh, you know what it must
be? It's the Tree blocks. Okay, so we have to go
into the tree blocks, click on the Tree block
ordering YSort Enabled. Oh, yeah, when you create a
new level of the hierarchy, if you still want Y
sorting to be there, you have to assign YSordEnabled
at the parent level. So props has YSorting enabled,
but Tree Block didn't. Now Tree Block does. So if I tab back to the game,
is why sorting. Okay, so we want to be able
to walk over the stumps and see that we
can sort in front of the stump and
behind the stump. And for the most
part, that works. Now, I did also notice that there's a bit of an
issue with the trees. They do do the sorting, or at least it was doing
the sorting, hold on. Let me check with the tree here. Yeah, this one's sorting. But the position where it's
sorting from is pretty off. So what I actually
want to do is go into the base tree scene so that
it applies to both trees, and Make sure you're in local view here if you have the game running
in the top left. Select the Sprite two D node because we're controlling
the offset of the Sprite. Hit W and then move this down probably
somewhere like there. I'll tab back to the game now. And then you try
to walk like this, you should see that the Y
sorting is much better. So here we're
standing in front of the tree as we would expect. And then here we're
standing behind the tree. So that's a lot better.
You do have to get those offsets right or things
just won't look right. And that's looking
like most of what we need here for the level props. I might create a
stump group, as well, so I could just take
these 16 stumps, and let's give it a parent. So let's right click and
reparent to a new node. And then we'll do a node two D. So now all of these stumps
have been grouped under one node two D. We're going
to call that the stump block. We'll right click here,
save the stump block as its own scene into
the objects folder. And now we can just duplicate
our stump block, hit W, move it around our
screen, and just kind of, you know, put stumps
other places. So it's not exactly like
procedural generation here, but kind of a similar idea
where we're just trying to fill the mapping
with the pattern. Of the parameters, which are
basically just duplicating the positions of the stumps
and then offsetting them. So it looks a little different. And we can run around
a game one more time. We have the Weiss sorting
working pretty good here. Maybe for the stump, we want
to change it a couple of pixels so we can go
into the stump scene. And then let's go to
Local view and move the sprite two D offset
down a couple of pixels. So let's see how
that is right now. Uh, maybe I got the
direction mixed up. Actually, maybe we
need to do it more like this and then hit save. So we're moving this up. Okay,
that's behind the stump. This is in front of the stump. Okay. Yeah, that looks
a little bit right. So just the very bottom of the stump should be below
the 00 point of the parent. And we're going to walk around. Yeah, we'll just see
stumps and trees. It's not really needing to be
hugely varied here because the focus is just on the
combat and it's going to be leveling up and picking up grades and that
kind of thing. But having something in the background does make
it more interesting. So I just wanted to go
through all of that. You can, of course,
manually move some of these stumps out
of the way if they ended up on the road
same way as before. So let's go to the
world let's find that stump that was
on the road, okay? I'll hit Q, we'll select it. So we see Stump block three
over here on the left. Right click and do
Editable children. Then click on the stump, move it out of the way and do the same thing with any
other stumps you find. And here's one. Okay, so I'm going
to select this node, Stump block two. We'll
editable children. Then click on the
individual stump, W to move it, move it out of
the way, and that's done. Okay, we can also do that
with this tree up here. So that's part of
Tree block five. Let's make it editable. Select the tree, move it. Oh,
there's another one here. Interesting. I must have actually duplicated a tree
block in the same spot. Yeah, right. Let's move this
way over here. All right. Okay, I don't really see any
other trees on the road. There's one stump over here, so I'll try to fix
that one, too. W to move it, move
it out of the way. Make the parent editable
if you need to. And that's probably
going to be almost okay. One last one. So editable
children. So select it. Make the parent editable, select the individual node, move it out of the way.
And that's basically it. So collapse everything, so it's not cluttering
up your hierarchy. That's one of the reasons I made the prop node to begin with. Um, if you had, like,
flower textures or other doodads, maybe rocks, you'd probably put those on a separate rocks or
doodads layer up here and just position it on a Z index that's higher
than the grass and dirt. That would be a good way
to get started with that. Alternatively, they can be
their individual seen objects. I'm not sure about the
performance, though. I would assume if it's
part of the tile map, it's probably a lot
better than if they are, you know, their own
nodes in the game scene. But as long as you don't
have too crazily many, it probably isn't an Um, so that's going to be basically our tile mapping
portion of the course. And from here, we can
kind of just move on to setting up
weapons and enemies, gaining exp, leveling up, HP, all of those RPGsh rogue
like kind of features.
13. Spawn Projectiles with a Timer: Character can move
around. We have something of a game
world to play on. Next, what we need
to do is create an attack that's on a
timer that will create a projectile where we
can use that to attack enemies without needing any other key input
from the player. In survivor games, you typically end up accumulating a bunch
of different weapons, and they just automatically
trigger for you. The only thing you need to worry about is walking around on the screen and letting the weapons do the work
while avoiding enemies. And that's basically
the gameplay loop. So get started with that. Let's create our weapon
script and player. So in this two D scene, I'm going to right
click on the root. We're going to add a new child
node D, node two D here. Going to rename
this to be weapon, and let's right
click it and attach a script to be weapon dotgD. And this can go into the
player folder for right now. So we're going to have
this weapon script. We might even make it
weapon two D, honestly. So if we want to go
into the file system, we can rename that
to be weapon two D, if that makes more sense. Okay, so what this weapon two
D script is going to need to create the projectile
game objects that actually are the two D
representation of the weapon in the game world that can damage an enemy is we're
going to need a timer. So I'll say var
timer of type timer. Now, you can add
these directly in the scene inspector
over here on the right, like you would add other nodes. But I don't want to
have to do that for every single weapon
script I create. So to make that a little
bit quicker and easier, I'll say function
underscore ready. So when our script is ready, we'll just take
the timer variable and set that to a new timer. Well, add the timer as a child, so add child timer. That means that
this new timer is going to exist under the weapon. If we remove the weapon, we remove the timer as well. And then we want to connect
to the timer's signal. So the timer has a
time out signal, and we want to connect
that to a function, a callable function, so we
can say on time time out. And down here, we can go
function on timer time out. Now, this signal timeout
doesn't actually send any data, so we don't need
any parentheses. And then this
function is going to have a return type of void. So when the timer times out, we want to basically
cast our spell. We want to summon
the projectiles. So I think cast is okay
name for that function. So we'll just say cast, and we want to
pass the direction that our projectile is
going to be cast into. So if we're facing up, we're
going to cast upwards. So we can get a reference to the facing
direction for that, but we'll come back
to that in a second. So we'll have function cast, and this is going to take a
P direction of vector two, the direction we're
casting into, and we can return void for that. So in order for the timeout
signal to even be hit, we have to start
the timer first. So we'll say timer dot start. So the time that we want to
set up for how long until we create a projectile instance
is going to depend on the definition of each
projectile weapon. And later we'll have different
levels of the weapons, so we may actually
change the cool down as the weapon levels up so we can shoot more often would be one way we can
power up our character. For the weapon two D, we're
going to want to have that definition as
a resource that we can use so our weapon
knows how to operate. So let's do at export VR, and let's say definition
of type weapon definition, and we're going to
need to create that. So in the file system, let's expand outwards,
and then I'm going to create a new
folder for our weapons. So I'll right click
on resources and do Create New folder weapons. And inside of this folder, I'll just create a new resource. So right click New resource. And let's create it as a
base resource for right now. I'll say spear definition. Since our character is a ragont we'll give him a spear
as his default weapon. So I'll save it here, Script, we need to create our
new weapon definition. So let's right click on weapons, create a new script. Let's say weapon
definition for right now, and it's going to inherit from
resource and create that. So now we go into
weapon definition. Let's give it the class
name, weapon definition. And here's where
we're going to define the stats in the UI specifics like name for how our weapon is represented
in a data format, but not in the game world. That'll be more of
the projectile class, which extends from
node to D. So we do need quite a few here to kind of pull
everything together. I would say the weapon system is probably the most
complicated part, so I'm going to try to go
thoroughly through it. So first off, we may
want to set a name. So at export var name. So nodes by default have a name property, but
resources don't. So we can take advantage of that by defining a
custom string name. So that's going to
be the display name, how we represented in the UI. And then we could say at export var icon of
type texture two D. Later we'll add in custom stat blocks for
each weapon level, but that's too much
for this one video. So for right now, what we do need from here
may be something more like a get cool down function, and we may pass in a level here because we
don't actually know what level the weapon is
from the weapon definition. That is going to be set on the runtime weapon two
D object over here. Because this levels up in game. And then the definition is
more of an editor definition. It sets up how the weapon
operates at each level. So those are separate things. The level is on the weapon
to D. So we want to pass on the level that
it currently is at. So we could say, P level, and that's going
to be an integer. We will return a float the
cool down for the weapon. So for right now, I'm
going to make this simple and just do let's say
if you turn one point. Later, we can make it
more costum by actually resolving the cool down based
on the level we pass on. But just keeping it simple
here so that we can test that everything weapon
two D actually works. So in weapon two D, we want
to start the timer at, let's say, definition
dot Get Cool Down. And we'll just pass in let's say negative one here because we
mean to change it. So I'm putting a little
to do note here to remind myself to get
the actual level, just a note so that, you know, once we test this works, that we can keep going from there and fully
implement the system. Okay, we're also going to
want to be able to get the projectile instance that
we're trying to create. So let's just do a
function get scene. We can also pass in the level. This is going to
return a packed scene. Let's go ahead and create
that weapon scene, the projectile,
more specifically. So in two D view, let's go
ahead and add a new scene. We're going to create
a two D scene here. I'm going to rename
this to be Spear. Let's right click on
it, attach a script, and this is going to
be projectile dot GD. I probably want to
save this and oh, let's say the weapons folder. I think that makes
sense. And then open Create, that's cancel. Create is over here, and then we want to give it the
class name projectile, and we'll save spear
dot TSCN inside of weapons so that we can
actually see the projectile. We want to attach
a Sprite to it. Click on Spear, attach
a Sprite two D node, and then we're going to
grab the weapon Sprite. Which is going to be part of an atlas texture over
here on the top right. So do an atlas texture. And then let's quick
load from our project. I think it was 16 by 16. And then we need to
get our texture. So go to art in the bottom left. Go to Pixel crawler, go to weapons, wood. And then word dot PNG, we drop that into
the Atlas slot. Okay. And now we
can edit region, zoom in, and we want the
spear, which is right here. So let's drag around
that and hit Close. Okay, now we have
the spear sprite. So we can at least see
our projectile uppear in the game once it is
basically loaded in. We want to load in this scene. Let's go back to player, and I'm going to open
up the weapon script. Then I'm going to right click on weapon Definition
and Loup symbol. And for getting the scene, we're simply going to do
something like return load. And now we want to load
that spear scene in. So spit dot TSN. You can see that you
can just find it in the resources weapons spit dot TSEN and put that in there. So once again, this is
a temporary measure just to keep it simple so that we don't have to
fully implement it. So in our weapon two
D, when we cast, we're going to get that weapon
seen from the definition. So we want to say var, projectile is going to
be of type projectile, and we're going to set
that equal to definition Get SN and we'll say negative one here for
the level. You know what? Actually, we could just start declaring our actual
level of the weapon. So let's make that a level one. I'll say at export var level. This will be a type of type integer, and
it starts at one. Okay, so now we can just pass the level into these functions, get cool down and get seen. It doesn't do anything with it yet, but at least
we have it there. And, of course, when we
level up our weapon, we're just going to increment
the level here by one, and that'll change what
these functions give us back if we try to get the
cool down or get the scene. Okay, so we get the scene, and then we want to
do instantiate on it. Then we want to
add it as a child to our projectiles group
and the main scene. Where are we getting the node to actually spawn the
projectiles in? Let's go to the world
scene two D view, and I'm going to minimize this. Let's right click on World
and the top left and add a new node two D. I'm just going to rename
this projectiles. Now let's go to the top right, and I'm going to
use groups here. We're going to assign a group
to the projectiles node. So add a new group. I'll just do projectiles, lowercase, make it global, and I'll say the node where all projectiles are
contained within, Enter. Okay, now we can see in the projectiles node
has this group, and we can reference the node
by doing first in group. So let's go back to the script. It's the projectiles underscore. We could also declare that
as a constant if we want. So I would say constant
projectiles group, making constant all caps
the standard convention. It makes it clear that
this is a constant value that isn't going to change and differentiates itself from a regular variable like
level or definition, which can change
during gameplay. So let's set this
equal to projectiles lowercase, just like before. So we're going to want to
get this node and add it to that node. So let's cache it. As a local variable, let's say var
projectiles parent, and this is going to be a node. So to get that on ready, let's do it before the timer. Underscore projectiles
parent equals G node. But we have to get
the tree first. So get tree dot get
first node and group, and we want the
projectiles group, which is a string name. In most cases, strings and string names can be
used interchangeably. It converts between the types automatically in a lot
of cases in the back. We can declare it
as a string name here because that's what
it expects for this. So we get the first
node in the group. If that doesn't exist, we're probably going
to get an error in the console, which
is a good thing. So we just need
to make sure that our game levels actually have a projectiles
node like this. If you like, you could save this node into your
project and reuse it, right click Save branches scene and then maybe we'll
create a levels folder. For anything related to levels and put the projectiles
dot TSC in here. Then if you keep
reusing it from here, they'll always be in
this projectiles group, and you shouldn't
basically have it desync between multiple levels. So now you can reuse the
projectiles scene without worrying about the values changing between the
levels you use it on, so that could be
a little helpful. And now, once we've
created the projectile, we want to make sure
that that gets added as a child to the
projectiles parent. So underscore projectiles
parent Ad hild projectile. Like that. We can
get rid of this now. And then we just want
to make sure that the global position is equal to the weapon two
D's global position. So let's say projectile
global position is equal to global
position here. So we're taking the position
of the weapon two D node. And then we need to pass
it direction into cast. So let's just do
vector two dot, right? That's capitals.
A constant value. And now the script should be
able to run without error. Okay, so it has the scene. I knows what cooldown to set. And it knows to create a copy
of the projectile on top of the weapon two D node
whenever the timer elapses. So let's run this
and give it a shot. We can see get
cooldown and null. So we haven't actually set
the weapon definition. So we could say, assert that definition does
not equal null. So each weapon requires
a weapon definition set. So then we need to
go to the player. We need to find this
weapon two D node. I'll rename it weapon two D. Let's go to the
inspector top right, and then define our
weapon definition here. So new weapon
definition, expand this. We're going to possibly
want the icon and the name. So I'll call this spear. And let's get the icon
from the spear scene. So we can just take the same sprite and
copy it to the icon. So I will right click on
the texture. And copy. Now let's go to the player
scene where we have the weapon definition
set and just right click on icon and paste it in. Okay, so there's our UI
representation of the spear. So we hit Play, and we get the spear to instance
after every 1 second. Now our projectile
doesn't move yet. It has no code to make it move, but we have the time working for putting
the projectiles into the world at the
right time based on our weapon definition class. Of course, there's
a lot more to it. We haven't messed with stats or anything yet, but
this is a good start. So I'll get more into
this in the next
14. Move Projectiles in Launch Direction: So at this point,
we're ready to start putting enemies
into the game and then making our projectiles able to hit them moving
along the screen. Okay, so let's make it so
that our projectiles can move now in the direction
that they're launched in. I'm going to go over
to the spear scene, and this is the
spear projectile, the one that actually
instances onto the game world. Let's open up that script. And we're going to want to
put a launch function inside of here so that when our
projectile is instanced, another script can tell it what direction it's going to move in. So I'm going to put
in function launch, and we're going to
pass in a direction. So P underscore
direction vector two, and that's going to
be the direction that the projectiles
going to move in. So we want to take that
launch direction and make it local so that we can use
it in our process functions, process going to update
on every second and translate our projectile
node across the screen. So var direction is
going to be vector two, and we're going to set that
up right here in launch. So direction equals P direction. Now, once we launch
a projectile, we also want to queue it to free after a certain
period of time. Otherwise, projectiles
that never hit a target would exist in your game world
forever and would eventually cause
performance issues. So we do want to make sure
that objects free eventually. So we can do that by waiting a certain amount of
time. I'll say wait. So this basically means that it's going to come
back to the rest of the code that follows this line after the
condition is met. So we're going to say get tree. So we're getting the
current scene tree that this projectile node
exists inside of, and then we're going
to do create timer. And we give it a time. So
let's just say 1.0 seconds. Everything else you can
leave as the default. So this whole thing
means we're waiting for the timeout method on the timer that we create
using the scene tree, and we wait one seconds
for that timeout to occur. And then once that is done, we can just it quarter
on the projectile node. That's going to free the
projectile on anything under it. Now, a projectile
that hits targets, if it hits a target, it's probably going to remove
itself before this, but this is just a fallback so that if it just completely
misses all the enemies, it'll free itself eventually. Okay, and then we need the
projectile to actually move. So let's say function underscore process or physics process, actually, is probably a
little bit more appropriate. Let's do that. So usually
you use physics process for anything that involves moving physics objects
across the game. Now, this is a node two D. We're not adding anything like rigid body physics
to the projectile, but just so that it's movement synchronizes with everything
else in the game. If the physics process is set to 60 times per second or 60
movement periods per second, then this will move
60 times per second, just to keep it consistent
with everything else in the. Export var speed up here
at the top of type float. We'll add it to 100. So that'll just be the default
speed for the projectile. So things like the speed and the duration that
this projectile lasts in the game world are stats that I want to pull out to a separate stat script so
that we can define it per weapon and per level of each weapon in a much
more organized way. But for right now,
it's good enough, so we'll work on that later. So we want to say
translate here. Any node TD can translate
across the screen. It's movement without
any collision. And we're going to say
that we're moving in the direction times the
speed, times the Delta. Now, if you want to know what this calculates
when you're debugging, hitting a breakpoint,
then you might actually want to do
something more like this. Var move, which is a vector two is going to be equal
to this bit right here, Control X, paste that end there, and then you just
translate the move. So now if you were to hit
a breakpoint, right here, and we may actually be
able to do that just by hitting play because
physics process is going to trigger as soon as that
spirit gets created, then we can see at the bottom, we have the move value of
00 and Y is zero, zero, because we didn't launch the projectile with this function, so it doesn't actually
have the direction it defaults to vector two
dot zero for right now. Okay, so I'm going to
undo the breakpoint. You. Okay, so close the game. It actually brings up a good
point I want to touch on, which is that if the
direction is vector 20, then that probably
represents an issue because we always launch in a direction the direction the
characters facing, which should never be
vector two dot zero. So we might just expect that the launch function has
already been called before we put it into the
scene fully with Adhild. So let's just make that
function underscore ready. And we're going to assert that direction
does not equal vector 20. Call launch to set direction or projectile before
adding it child. So we want to so we want to require that we've
already set the direction with the launch
function and give a note that we should call launch before we call Add hild. So the ready occurs after okay. Now if I run, we should immediately hit the assert
as we would expect. So let's jump into the weapon two D script where we're adding the child of the
projectiles parent. So now we want to call
projectile dot launch in the P direction that we
pass into the cast function. So that it'll actually set
it to something proper. Now, we can see that down
here on vector timeout, we're using vector
two dot right. So I guess it would be
a good way to test. It should always move to
the right in this case. So the problem here
is really that our launch is trying to
do two separate things. I think it actually makes
more sense to pull this bit out to the ready function
since we're using that now. So I'll paste that
in there, Control X, this, control V, paste
it in. This makes sense. As soon as the
projectile is ready, we get it ready to free
after a set period of time. So this keeps the launch method
only handling one thing, which is basically to initialize the object once
it's been created. So this is almost certaintly
the better way to do it. Okay, now let's hit play. And we have our projectile
shooting to the right. You can also see they
only last 1 second, probably not long enough, but it's a good test to make sure that they
are actually freeing. So at this point,
there'll only ever be one or two of the spears in
the scene at any time. It shouldn't cause any
performance issues whatsoever.
15. Setting Up Enemies and Hitboxes: Our projectiles
move to the right. Now, let's create
an enemy so that our projectiles can actually
connect with it and hit it. So that's going to
mean we're setting up a hit box and a hurt
box in this video, and we'll worry about
stats later on, like HP. So in my project right now, I see at the bottom, there's
a few warnings here. These are just mentioning
that if you're not using a parameter, you
should underscore it. So I'm going to click on each of these and just give it a
little underscore there. So I'm not using Delta in
this player physics process. So I underscore the Delta. It implies that it's
not being used. And then here in another script, I'm going to do the same thing. And just for any
of those warnings you're getting, just repeat. You're not using the parameter, then underscore it to make it
indicate that you expect to not use the parameter and that that's not an accident
or a mistake. And we'll go ahead and
create our first enemy. So let's start with an Oc. I'm going to create a new
scene up here at the top, and we'll go to two
D scene on the left. I'll rename this to be Oc. We'll right, click it,
change the type to a character body two
D. And select that. Okay, then I want to
right click on the orc, add a child node. We'll do a sprite twoD here. Also, right click here and
add a child collision shape. And on the right,
we'll just make that a little circle shape, kind of like what you
did with the player. And let's controls, save the
scene inside of characters. I'll make a new
folder for enemies. So enemies, put that in there, and then save the orca TAC. Okay, now our orc needs
Bright, of course. So let's select this Brit node, and we'll go down
to the bottom left. We'll look for I believe it
was entities mobs orc crew, and we want to
grab one of these. So I was just using
the default orc so we can grab the idle sheet. I'll select this
Bright two D node, and then over on the
right, we're going to drag and drop that
idle sheet into here. Okay? Now, obviously, we have a similar setup to the player where we have a sprite sheet. We need to define how many frames H frames are from left to right,
which you can see is four. And we want to do that
with an animation player. So I'm going to click on Ok, add a child node.
Animation player. So select the animation player, go down to the bottom, do a new Idol animation. Okay, and we need to keyframe some of the
sprite properties. So let's add a track,
property track, sprite two D, and we're going to look for H frames.
So double click that. Okay, and then repeat the
steps for property track, sprite two D, V frames, property track,
sprite two D texture. And the bottom right, enable
snapping right here and then change the snap
time to 0.1 seconds. So one frame per
tenth of a second. And lastly, on the right, we want to change this to 0.4 seconds total duration
and enable looping. Okay. On the timeline here, go back to the start
of the animation, right click insert a key. So we have our H frames. We want to take that,
and in the top right, we change that to four. And then VFrames we
want to insert a key. We leave that as one up here. Right click insert a frame. So that's going to
be our first frame. Don't worry about how it shows all four frames at once that'll automatically fix in a second. And then we want
to right click and insert a key for our texture, which you can see
in the top right is already defaulting to
our animation here. So we haven't played
the animation yet, so it's still showing four
frames, but as soon as I play, it's going to show the first
frame of the idle animation, and the frame section down here updates automatically
to what we would expect. So now we want to
go to 0.1 seconds. It should snap 2.1
second intervals, and then right click and sort
of key for frame and then change that in the top
right to one up here. So that's the second
frame of animation. Then add in the
frame value of two, which is the third frame up here and the last
frame of value three, which represents the
fourth frame of animation. Okay, now, if we at play, we should have our
idle animation. And for the simple
survivor like character, we're only going to make
it face left and right. So we just need a
flipping node in these individual animations for whether it's facing
left and right. So because this is for a
simple survivor like game, we're only having it
face left and right. So we'll just need to add
a flipping node later on. And aside from
that, we just need our idle run and
death animations. And that's pretty much it unless a character needs a
special attack animation. So let's duplicate
the idle animation, and we'll put in
our run animation. For run animation, we want to
take the texture down here and change the value and the
top right to the run sheet. So take run and drag that
to the top right. Okay. And that is six
frames of animation. So we want to take H frames
here and change it to six. And then we want to add in
frame five and frame six here. And so the key change
this value and the top right to
four, insert a key, change this value to five, and then take the duration of the run animation and
make it 0.6 seconds, hit play, and we should
have our run animation. Now, I think it's
offset incorrectly, kind of like the idol to
the run of the player was. So we're going to need to do
that offset setting, too. So let's go to the idol. Let's move this character up. Make sure you're selecting
the sprite node specifically. Then hit W to go to move
mode and move the sprite up so that the 00 point is
centered on its feet down here. Wherever it's center of gravity where it touches
the ground should be. And then go to transform on the right and insert a keyframe. So I have negative 14
pixels as the Y position. Key frame that, hit Create and just have the
reset track. That's fine. And now we need to
keyframe the run as well. So for run, we're going
to go to the top right, and I'll make it Let's
try negative 27 pixels. Let's try negative 30 pixels. I figured that these are set up kind of the same way
that the player was, same artist and all that.
So let's keyframe this. And create. And we have
our run animation. Now, let's quickly
switch to Idol. Okay, so now we have our
run animation playing. Let's quickly switch
to Idle and it play. And it seems like
the pixels line up. We'll have to test that
end game, of course, if we're going to actually
show the Idle animation. In this case, though, we
may not actually ever show the Idle animation unless the enemy is ever going to stop running towards the player. But idea, at least with this
setup is going to be that the enemy is always
running towards the player as long as
the player exists. So we might not ever actually
see the idle animation. It would be up to you. Maybe
when the player is defeated, the characters just
start idling instead of moving around on the screen more because there's no
more player to fight. But that would just be like
an extra not really required. What we do need, though, is the death animation. So let's take run because I think the death
animation is six frames, and we're going
to duplicate this to make a death animation
so type in Death Enter. We're going to turn
off looping for this. Characters only die once. Then we want to take
the texture and change it to the death sprite
sheet in the bottom left. Drag that into the inspector
and let's hit play there. And then if we switch to idle, it seems like it lines up and
run seems like it lines up. Okay, so that's probably good. I guess the position on
the death is negative 30. Anyway, we have our Oc. We can just pop an orc
into the game world. So let's take orc here. And actually, we probably
want like enemies hierarchy, so I'll right click
insert a node. So node two D, we'll do enemies or could even
just be NPCs if we want. Maybe enemies is better. And I'm going to
drop an orc from the bottom left. Under enemies. So we can see it's not actually showing the animation yet. So I'm going to hit
Open and Editor, and we're going to see
what's up with that. We probably need to, like,
set a default animation. So I might take death
and change that to idle. Let's default it as idle. Okay? Hit play. Let's switch back
to the game world. Does that fix this? I guess not. Okay, so since
that's not working, we should go to the reset. Animation. So this is whenever you're not
playing an animation, it resets to these values. And we can see that
the He frames here, if we look in the top right
is actually set to one. So if we set that to four, and I guess now we hit
play on the reset, then this is what we should see if we go back
to the game world. So save your orc scene,
go to game world, and okay, now it's
looking correct. So I'm going to hit W
and move the c. Okay, and then let's just Control D, duplicate a few more cs. And then I think
the last thing we want to do here is to take enemies and enable on
ordering y sort enabled. Since this is apparent
for all the orcs, we do want the orcs to sort
with each other, as well. That might be necessary. So that might be
necessary to set there. Okay, so our spears can move, but they can't
actually hit anything. We haven't set up a hit box, and our orcs can exist, but they have no HRT box
to actually take damage. So HRT boxes and hit boxes and Gadot D are going
to be Area two Ds. If you're coming
from unity, it would be more like a trigger zone. Let's go to Spear
and we'll create a Area two D that we'll
make as our hit box. So I'll right click on the spear add child node, Area two D, and I'll rename this
quickly to be hit box two D. Let's right click
it, attach a script. Now, this is going to go
probably in more like the root. So let me see. I
guess putting it in the weapons folder makes sense
here and we'll hit open. So we'll create the script
at weaponlash Hit Box two D. Create give it the class
name Hit box two D. Okay, so in our HIT box script, we want to immediately connect to the signal that
the area already has, which is area entered. Because what we're
trying to check is if we enter the
area of a RT box. So if we do function underscore
ready, then on ready, we can do area
entered dot connect, and we're going to connect
it to on area entered, which is a function
we're about to set up. So create function
underscore on, area Entered. This is going to receive and
area two D is the parameter. So P area area two D and
return type of v Okay, so an area entered this area, which type of areas
do we care about? We care about
specifically hurt boxes. So if P area is a Hurt box two D and the
type doesn't exist yet, so it should be an era here. Then for right now, we
could say something like prints then for right now, we could say something
like Prance, which is going to print a
message to the console. Why don't we try it like this? Percent s and quotation marks means we're going to
replace this with a string. Hit percents. We could even say at position, percents, and then
we're going to replace those strings using
another percent sign. And we have three strings we need to replace
inside of that string. So we're going to
pass in an array of values we want to do
for the replacement. So the first one is going
to be, let's just say self. So we're going to
get the Hit box, which is going to hit
the P area Hurt box. And then we're going to
say that the position is the P area dot global position. So an Area two D is
still on node two D, so it still has a
global position. That'll work for
just picking up that our hit box is actually
hitting the HRT box. Okay, now we need to
make a RT box on the Oc. So let's go to the Oc and I'm going to right
click here and create an Agile node Area two D. Let's rename this
to be Hurt Box, and I'm going to
capitalize the B for box. Two D. Let's right click
it, attach a script. We're going to want
hurt Box two D. That can just go in maybe
characters for now. We're not going to really have
any destructible objects, at least in this demo. So you could argue, Okay, this belongs more
in objects folder, or maybe you're organizing
your scripts altogether in a scripts folder which
holds specifically scripts. Organization is
kind of up to you. This is just kind of
how I'm doing things. So the characters
slash Hurt Box two D, we'll create it there. Okay, now this script, we need to call it class name Hurt Box
two D. For right now, we won't change
anything else about it. We just want that
identifier tag, Hurt Box two D. Now, the Hurt box, you can see it needs a collision
shape as well. So let's right click and add a child node, collision shape. And then in the right, I'm going to create a
new circle shape. So we can define where can
this character be hit. So I might hit W and move it up. We can just make it
kind of big here. So if I make it bigger, kind of like that,
that would be fine. You might also prefer
a capsule shape. So if we select
capsule shape from you might actually prefer a
capsule shape here as well. So if we do a capsule shape, which is more like a humanoid, then we can select
capsule shape. And then over here,
we can just hit W, move it up here. This would be good if you want, all of the pixels
that show on screen for the k to actually
be damagable areas. So you could make that argument. And then we just want this to be a child
of the Hurt box tot. And then our orc also
needs a collision shape. So where can the feet
collide for the c? So that is going to make more sense as a
circle shape here. And then we look
at the orcs feet. Let's just shrink it down
here, something like that. So if we chose to
have collisions between our orcs and other
orcs or orcs and the player, other props and the object, then this is the shape that we would actually
be using for that. Now, if you want your
enemies to collide with other enemies and
players and stuff like that when they're moving
by using move and slide, that's something
a character body two D can do out of the box. But it is going to have a performance cost
associated with that, since it's actually calculating movement based on physics. If you want to get the maximum number of
characters on screen, you might consider using
translate instead, just like we did with the spear, which means it's just going
to move regardless of what's on screen in the direction
that it's trying to move. So keeping it as dumb, simple as possible, it's kind of up to you what you
want at the end of the day. And how many thousands of enemies you need
on screen at once. Okay, so before we wrap this up, I think the spear also needs a collision shape
for the hit box. So I'm going to right click
here, add a child node, collision shape two D. We
go over to the top right. Let's make this a circle shape, and then we'll take the circle
shape and move it up to the spear's tip and shrink
it, something like that. You might prefer it to
be a capsule shape. It kind of depends on how
picky you want to be. Capsule could
obviously be more of an oval or less
of a pure circle. Or maybe even just a rectangle. So if you want to go
with a rectangle, you could do, let's say, rectangle shape up here,
shrink the sides like this, bring this down, and
maybe like that. So using kind of the
simplest primitive shape here as possible for the hit box while
still trying to maintain the actual shape of the sprite where
it makes sense. So I think this
would be quite good. I don't think anyone would
really notice much that, you know, maybe this half of
the spear hasn't hit here. If you really
wanted to be picky, you could do it like this. And yeah, maybe that
would be a little better. Just a little bit
less chance for it to hit it might slide along the
edge over here to the left. Once again, one of those more, try it and get a feel of how it's working kind of things and see if it's right for
your game or not. Let's go ahead and
run the game now, and we'll try to get the orcs to hit with those projectiles. So let's see cannot
instantiate a null. Level. Okay. So right now, the projectile is not
set on the weapon two D anymore.
That's interesting. Let's go to the player, and
I'm going to take the weapon two D. We can see that the
weapon definition is set here. If I expand weapon two D, let's jump into
weapon definition. So we have the look up symbol. Maybe I moved the weapon.
Let's see. Where's spear? I accidentally moved spear from weapons spear to this
different folder. So let's move that
back to weapons. Okay, it will work fine
now. We'll hit Load. Okay, and then we
have the spears. So let's go to output. I'm going to move this
down a bit these orcs are actually being hit by the
spear, which is good. We can see the position
at which that occurred. The hit box information
isn't too useful, but at least we know what
type is hitting what type. If it was a completely
different type of area, it would say the class
name for it here. And that's pretty much working.
16. Dealing Damage to Enemies with Hitboxes and Stats: Hit boxes and hurt
boxes are working enough that we can pick up
the hit on the HRT box, which is going to be
the start of dealing damage to our orc enemies. So in this video, we need to set up the stats for the
orc and make sure that we're passing a damage on hit over to the orc that's
going to take damage. A big part of this
tutorial is going to be actually defining the
stats of the spear, the stats of the
orc, and setting up the interaction between
the two of them. So if we jump into the
projectile script, we want the projectile to
respond when the hit box hits the Hurt box by passing
in the damage of the projectile over to the
hurt box and hitting it. So I'm going to put up an at
export var for hit boxes. This is going to be an array
of hit box two D. Okay. Now, those I want to assign in the inspect So if I click
on Spear array two D, the array of hit
boxes is over here. We need to increase
its size to one, click a sign on
the first element, and then choose Hit box. So now we have a reference to the hit box for the projectile
inside of the script. It's made as an array because some weapons may actually
have two or more hit boxes. So we just make that an
option from the start here. And what we're going
to do is connect to each hit box signal on hit so that our projectile
can deal the damage. So let's right click on the
Hit box two D. Symbol here, do Lou symbol, and this will jump over to
the hit box script. So rather than
print that we hit, we actually want to
emit that we hit. So let's create a
signal hit up here. Whenever the hit box
recognizes a hit, then we'll pass in
the target that we hit or the hurt box
that we hit in this case. So we'll say Hurt box here is a type of rt box two D. Okay. And so rather than print here, going to cut that out, and I'm
going to say hit dot EMIT, and we're going to pass
that with the P area as a Hurt Box two D. So now if we go over to
the projectile script, we can just connect to
that signal on ready. So let's do that before
we do the await portion. First, we'll say assert
hit booxes dot empty. So we don't want
that to be true. So we could say
equals, equals false, and say needs at least one
hit box to hit a target. Now, this is a
little bit dependent on what your intent is. You could theoretically have a projectile that
doesn't need to hit. So this assert might not be
necessary, but for our game, we do want projectiles
to be able to deal damage to enemy targets. So we're going to leave
that assert in there. And so after our asserts, we can say four
box and hit boxes. We're going to say box
dot hit dot Connect, and we could say on hit box hit. So we're going to
connect that signal to a new function we're writing
down here at the bottom. So function on Hit Box
Hit is going to take a P Hurt box could do HRT underscore box if we want to be
more consistent. And that's going to
be of type Hurt Box two D and return void. Okay, so with the HRT box, we're going to call,
let's say, P Hurt box. Let's say dot DL damage, and the amount is
going to be based on our calculations right here. So where do we get the
damage to calculate? Well, let's just declare
final damage here. And we'll set that to
ten for right now. And this will be an integer. So all the damage and HP in the game will
be integer based. Just to keep it
simple, technically, you could do a fraction of a unit of damage, but
that's pretty confusing. Most games will
just use integers. Okay, so we'll say we deal
with final damage here, and we'll worry about
damage calculation later. So we could say, to do
Damage calculation. Okay, so now our Hurt box, we want to add that
deal damage method. Let's go over to Hurt Box. So I'll say function
deal damage, and we will pass on the
damage of an integer. Oh, we could put a return
type here right now, I'll just return it
as void because we are expecting the damage we deal to be the
damage it receives. We're calculating in
the projectile script rather than the Hurt box
script, in this case. Let's actually change it
to a Boolean return type. So I'll say try
deal damage here. The reason for that is that we'll make a
condition that it's possible that the stats
are impossible to be hit, so we'll have that managed here. So the final fail point of if
it's actually going to take the damage will be the
invincibility on the stats. So I guess that makes more sense from a box point of view. So we'll say, if not, a stat controller
dot can be hit, then we'll return false. Now, the stats are where
we're applying the damage to. It's going to manage the runtime
stats for the character. So we're going to need to
set that all up in a minute. So now would probably be the
right time to set that up. So up here at the top, we're going to want to at Export VR, a stack controller reference. So this will be of
type stack controller. So we'll create a
new script with. Let's go over to the org
scene in two D view, and we're going to right click
it and add a child node. So this is going to
be a regular node. It's not a position space. This is a stats manager, so it's all database. Therefore, it's a
node, not a node TD. And we'll call it
Stat Controller. All right click here. We're
going to attach a script. So the name of it will
just be Sat Controller, and we'll put it in the
character's folder because our main player
character will have a stack controller as well.
Let's create the script. I will put in the class name, and I'm going to control
VPastedn Stat controller because I copied that
from the other script. This manages the runtime
stats for an object. So we're going to
have stuff like VaraHP which is an integer, and we can set that to a
default amount maybe 100. We also have Var Max HP,
which can be the same. The idea will be that the HP can never be set above the MX HP. So if you want to just take
care of that right now, we'll put another cool
in here at the end, and I'll say set value. So HP is going to be equal to the men of the value
and the maxHP. Okay, so this right here
just limits it so that you can ever set the HP
above the maxHP. We might also want a variable
for is the object alive. So we'll say far live,
this will be a boolean. It's going to default to true. And then we need that function that we were getting
an error on. So if I check Hitbox, there's the can be hit function. We have to implement that. So let's go to Stat Controller. Function can be hit, which is going to
return a Boolean. So we can put whatever
conditions we want in here. For right now, we're going
to say, we'll return live. So if the character is
alive, it can be hit. Otherwise, it cannot be hit. So back over here,
if it's alive, then it will be able to be hit, but this is inversing
that with the k. So if it's not alive,
then we return false. Basically. Now, down here, we'll say return true because we assume
that at this point, it did do damage because
the conditions were met. We already passed
the Guard clause. So we want to take
the SAP controller, and we want to take its HP and we want to
set it to the new value. We might want to calculate
that right above it. And as a local variable. So new HP is going
to be equal to stat controller dot HP minus equals the damage or
just minus, actually. So the assignment is actually over here to the
left of the new HP. So we set the stat
controller's HP to the new HP, and we'll return true. We may also want to put in here like a signal
that the Rt box emits, which would also
be a hit signal. Okay, so I want to test
everything so far. I'm going to set a break
point over here on the left so that we
can hit that on Debug if the weapon manages to
damage the character. So let's hit Play and see
if it works at this point. I'm sure I'm missing
a couple of things. So we can see at the bottom
here that deal damage doesn't exist because it's
actually tri deal damage now. So we changed the name and
then we'll run this again. Do we hit the breakpoint?
We get to here. Stack controller can be hit, but we didn't actually
assign the stack controller to the RT box. So this might be another case
where we want to assert at the top that the SAT controller
actually has been set. So assert stat controller
does not equal null. HRT Box needs at controller
reference to effect. So the HRT Box needs a
reference to the SAT controller so it can change its
stats. So let's see. If we go to TD View, Local mode up here, and then we click on the RT Box, we'll just assign on the right the stack controller over here. In case we forget at any
point with any hurt box, we have the assert to have
a fallback to catch us. So let's run this one more time. We're going to hit that and you can see that
we've returned true. You can also see
at the bottom that the new HP has been set to 90. Our base HP for the
stat controller is 100, so 100 minus ten, we get 90, so we know that that's
actually updating correctly. And the next thing we'll do
at the start of Next video is going to be to set up
the orc state machine so that we can transition
from alive to dead once the HP drops to zero and then remove the
orc from the scene.
17. Death State When Enemy Reaches 0 HP: Now we want to set up a state
machine for our enemies. It's going to be
incredibly simple. There'll only be two
states off the bat, which are going to be the move
state or the death state, which clears it out of the game. So let's right click on
Oc add a child node. I'm going to say HSM here, and we're going to
grab the Limbo HSM. We could rename this
to be Enemy HSM, and then I'm going to right
click on it, attach a script. So this will go right into the enemies folder
slash EnemysHSM. We'll create that. Set
the top here. Enemy HSM. This is the state machine for
enemies inside of the Okay, so looking at the player HSM, it's going to be
really similar here. I think because we actually
switched to the Blackboard, that script no longer needs a reference to the
animation player directly, which would be correct if I look over here in the
Blackboard plan. There's no need for
the second reference. So I can actually just
cut that out right there. So we're going to
copy this code from player HSM over to
the enemy HSM script. So let's say that
instead of player, this is going to
be a NPC script, which could have its
stats set on it. So NPC of type NPC assert
that the NPC isn't null. So NPC agent must
be set on the HSM, and we'll initialize it
with the NPC script. So just kind of changing
the names here mostly. So the NPC script, let's go over to the
orc scene to D View. And we're going to
want to we click on OC, attach a script. We'll say this is NPC, and we'll put it in characters
not character slash Enemy, or it could be in
enemies up to you. We'll create this script
for the character body. And let's call this
className NPC. So the NPC will just have easy reference to
the stack controller. So that we don't
need to necessarily reference those two
things separately. Inside of the state machine, it's pretty convenient to access properties from the
agent because you always know the agent is going
to be there on any state machine
script once you've initialized it on
the state machine. So if we do export
VAR stats here, and then we'll say
stack controller, then we can reference that super easy inside of the
state machine, too. So over here on the right, we assign the stack controller, and now inside of the enemy HSM, the errors should go
away as long as we click on Enemy HSM.
Well, this isn't right. It's supposed to say NPC. So we just need to reboot
the Cadle project, go to Project, reload project. Okay, and that
should fix that up. So now we assign the
orc, which is the NPC, and we have a reference to the agent as well as the stats, and that should be
most of what we need for our state machine. Okay, so let's right
click on the enemy HSM. I'll add a child node. And let's do a Limbo state. I could rename this and say something like NPC move state. Well, right click attach
a script. Create. Okay, so next, let's create
a Chase state for the enemy. This will be where
the core logic for our character moving towards
the player is going to be, as well as playing
the run animation as soon as we have
it into the state. So I'll right click
on the enemy HSM. Let's add a child
node, a Limbo state. I'll rename it Chase State. And I'm going to right click
here, attach a script. So Chase state that probably
belongs to an enemy. I'll hit Create. Okay. Let's give it a
class name at the top, Chase State, and we'll
give it a comment. So this is going
to end up running towards the player wherever
it is in the game. For right now,
we're just going to have it play the animation. We'll set up a transition
from the Chase state to the death state and
just make sure that our character
dropping to zero HP makes the enemy alive
inside of the game. So we'll say function
underscore, Enter. When we enter, we want to
play the run animation. So we can even do function
underscore setup, and then we're going to
get that animation player. So far, underscore
animation player of type animation player, and we're going to get
that from the Blackboard, just like we did
with the player. So animation player equals
Blackboard dot get var. And we're looking for the end quotations
animation player, and we want to complain
if we don't find it. So we have the animation player. When we enter the state,
we play the animation. So we're going to say underscore animation player dot play, and we want to
play the animation we define up here at the top. Let's do at Export var. Let's say Chase animation. Sure. And that will
be a string name. I'll default it to run. And then we copy
this down to here. So we play the right animation
when we enter the state. Okay, and our Enemy HSM, we already initialized it. We already set it active. So it probably is going to work. So we can see if it runs now. Let's go into play Mode. Oh, we haven't put the animation player in the
Blackboard, of course. So it tells us right away
that there was a problem. That's good. Click on Enemy HSM, go over to the Blackboard plan, and let's do New
Blackboard Plan, manage it, add a variable
animation player. Choose the type of
node path. Hit Okay. And now an animation
player we want to assign the animation player of the or and now we can use it
inside of our states. Okay, hit Play again. And let's see if our orcs
are running. They all are. That's great. So we
need our death state, and we need to set
up our transition. So I'm going to right
click on Enemy HSM, add a child node, which is
going to be Limbo State. Rename this to Death State. Right click, attach a
script, hit Create. We'll call it Death
State at the top. So class name Death State. Okay, now in our death state, we also need to play
an animation very similar to what we did
in the Chase state. This is code that we're going
to kind of repeat here, but because we're only doing
a few states for our game, I think that doing
things like making a new base class is just a little bit too extra
at the moment. So I'm just going
to copy this over. To the death state.
Well, paste that in. And I could just
say the animation here that we want
for the death state is defaulted to death, and we get the animation
played the same way. But then we want to play
the animation over here. And because we're not
using a base state, we don't have to worry
about things like calling Super to make sure that the animation state
code actually runs because they're both going to use setup, and they're
both going to use Enter. If you declare a new
setup or Enter function, it actually overrides
whatever's in the base class if you forget to call
Super and by the way, you would call Super like this. Super just means that
whatever the parent class is, you run that at
this point in time. So you can see it's a
little bit tricky and confusing to use that kind
of direct inheritance. If you have like five or
ten different states that all are animation states because they all play in animation
in exactly the same way, then it becomes worth it
to kind of have that. But we're literally
only going to have Chase and death,
so it's overkill. But Tay should mention that that's another way of
doing things, as well. So on Inter, we
basically want to make our character invincible. So we may want to do something like agent dot stats dot LI, and we want to make sure
that this is set to false. So remember that when a stats
controller is not alive, then can be hit is
going to return false. So this will prevent
us from accidentally hitting a character that
is in its death state. They also do something like,
say, function finished, and we declare this function
so you can actually call it from the animation
or other places. And we're going to say that
the agent needs to u free. So agent Qu free. So this basically means
the route Aosne the org. We're going to free it and
everything inside of it. When we call the
finished method. So now we can go to two
D view animation player down here at the bottom. We're going to go to
the death animation. We zoom in on the timeline,
go to the end here. We'll do add Track, call method track
on the death state. And then we're going to
right click down here, insert a key, and we're going to look for that finished
method we just create. So, this means that when our
death animation is done, it's going to call finished finished on the death
state is going to free the agent as soon as the frame is done rendering
and everything else resolves. So usually you
would call quarter, not free, free
freeze immediately, and quarter waits for other operations to
properly resolve. So this would actually
reduce the amount of weird bugs you might
get when you qu fare. Okay, so the last
thing we need to do is jump into the enemy HSM. Let's create a new function
for setup transitions, which will return void. So we want to add
a transition from our chase state to death state when the
death event is emitted. So when you declare an
event here like this, this is just a string that our
system is going to respond to we could go up here and
create a constant death event. It's going to be a
string name equal to we'll just do
death lowercase, paste that down
there, and we can connect to the stats on
our agent like this. Let's say NPC dot stats
dot Alive changed. We haven't created this signal, but I'll just go ahead and
jump here, so connect. Underscore on NPC Alive changed. Okay, so I'm going
to copy this down to the bottom function paste
on NPC live changed, and this is going to
have the new value. So alive is a boolean. Could say P for parameter
if we like here. I usually like to
do that. And this is going to be returning
a type of void. So when the NPC is
no longer alive, then I want to emit
the death event. So if P alive equals false, then we are going to
dispatch the death event. Okay. And when we
add the transition, that means that
this transition is going to happen when
this death event occurs. Now, it's still giving us an error message because
we don't have the Chase state or the death state reference here at the top. So we can just say at
export var Chase state, which is a limbo state, and at export var death state, which is a limbo state. Now save the script
over in the inspector. We can assign the Chase state
and assign the death state. And now, once we call
setup transitions, it's going to handle
managing these transitions. I think we want to call
that before initialize, or perhaps right after. Let's do it right after. So
underscore setup transitions. We call that. We set
up our transitions. There shouldn't be
a need to create a death state to chase state because we don't have enemies coming back
to life currently, though, you may later
add that sort of thing. Certainly, that would
be a common theme, especially for, like,
undead enemies. Okay, so the last thing to
make this work really fast, go to the spear scene, and let's jump into the hit box, or is it the spear?
Right, the spear. And we want to take the final
damage and set this to 100, so it will be one hit
equals one orc defeated. Hit play, and let's see if
this all comes together. So it's giving us an error.
I haven't created live changed event on the NPC stats. Right click on NPC
symbol, go to Lou symbol. Right click on the
symbol, Lou symbol, right click on Stat
Controller, Lou symbol. Okay, and here's our stats.
Let's create the signal, so we could say
signal live changed. And this is going to pass in the new live value as a Boolean. We'll come back here and assign more signals, but for right now, I want to make sure
that this signal emits whenever a
live value changes. So I'm going to say set value. So first, we're going
to guard against if the value is the same as
the value being set to it. So if a live is equal, two equal signs to the
value will just return. Otherwise, a live equals value. And then finally, a live changed dot emit
with the new value. So where do we want
to calculate Alive? Well, a good place to do it
would be in this HP setter. So whenever the HP changes, we want to check if the
character is still alive. So that's just
going to be live is equal to HP is
greater than zero. Okay, so that's handled. Now we have the live changed event. This should all trigger when
the character hits zero HP. Let's stop the game
and re run it again and see if we hit
it with a weapon, like so, does the
character remove itself? Does it go to the death state? And we can see that
the answer is yes. So it takes 100 damage. HP hits zero. A live
gets set to false. The signal gets emitted. And then that means over on the enemy state machine that the transition triggers
because this signal hit. This patch on the enemy HSM
emits the internal event. So these state machine events
are separate from signals. These are just for
the state machine in Limbo AI signals in the
greater context of Gadot. So we get that
internal death event, and that makes us go from
Chase state to death state. Inside of death state, we play the animation death on Enter we make sure that this is set to
false here, though. In theory, you wouldn't
need to do this, but I don't think
it's going to hurt anything to make sure that if we're playing the death state, that the character
is no longer alive, I think that's fine. And then we have the
finished method, which we're actually just
calling from the animation. As you saw before, the
death animation down here calls it right at
the end of our animation, and that is how we
make our enemies become on live as soon
as they drop to zero HP. So I know there's a lot of
pieces coming together. I would recommend
for this part if you haven't already been downloading the individual part
snapshots that you do grab the one for
this video if you are, you know, running
into hiccups and see where maybe your
script is a little off. So what we're doing right here with signals is the
observer pattern. It's really useful when
you want one piece of code to respond to a change in a different
piece of your code. And something like C Sharp, this would be called events
and then event handling. But in Gadot it's
called signals. Essentially, they're
the same concept, though. So it's
observer pattern. If you want to look
that up and read more about it. So that's going
to be a wrap for this
18. Rotating and Aiming Projectiles with Weapon Loadout: Okay, so in this video, we're going to make
it so that our spear rotates to face the
right direction after instancing and that
we can actually launch the spear in the
correct direction as well. So how we're going to
need to set that up is inside of our weapon
two D script here. So in our survivor like game, we probably want to make
it so that a character can have multiple weapons
at the same time. Therefore, it actually
makes sense to have some kind of manager for
all of these weapons. So we can create a
weapon loadout script that's apparent to
this weapon two D, and we could use that as a sample way to
resolve things like the player import for our
weapons individually. So let's right click on the player node and
add a new child node. I'll call this node two D, and then we're going
to rename that to be weapons loadout. We're going to
immediately parent weapon two D to weapon loadout here, and I should rename weapon
two D to just be like spear weapon making it clear
what it's actually holding. And then let's click on Weapons
loadout and add a script. So this is going to be weapons loadout it can go in player, create and then at the top, we'll do class name
weapons loadout. So this is going to
be a manager script for all of the weapons that
a player has equipped. So here we could just grab a quick reference to
the player input. Okay, and now we can go to
the spear weapons script, and we're going to insist that whenever we
have a spear weapon, we have the dependency
that the parent is a weapons loadout that'll be
consistent across our game. So I think that
the setup is fine. So I'll do var
underscore loadout here inside of our
weapons two D script. And this will be of
type weapon loadout. So on ready, we're going to
want to get that loadout. So say, underscore loadout
equals get parent, and we should assert that
the loadout does not null. Parent of a weapon should
be weapon loadout. Okay, and it seems like I
might have set the class name. Is it weapons with S? Yes. Okay, so that's weapons loadout here,
not weapon loadout. Okay, so we have a reference to the parent loadout script. This does create a dependency, but it also makes it easy to
access things that we need. So, in case we ever decided we wanted to change
this pattern, I'll create a function down here at the bottom
for Get direction. So function Get direction is going to return a vector two. And in this case, we're going to use the loadout to get that. So return underscore loadout
dot input. Dot direction. And so this is how we
resolve the direction, but we can just always use the G direction function
in order to handle that. So then if we call, let's say, get direction in ten
different places, but we change the loadout. So we don't actually
use a loadout anymore or maybe the loadout
doesn't use an input, then we just have to go in
here once and change this. So maybe the loadout has a
direct reference to direction, so we just change it like that, and then it would work again. But I actually need to
change this a little bit because we're not going
to use the direction. We're going to use the facing
direction because remember, direction is the raw input. It can be vector dot zero, but we're never going
to cast a spear a down at the character's
feet in this style of game. So we actually want
Get facing here, and we'll take that
to Import dot facing. So we're really going to
resolve the direction that the character is
facing and instead. Okay, so let's take this Get
facing and where we cast, we'll just do cast to
get facing instead. And we move our
character around, you're going to
see that already, we can cast in the
right direction, and this should include
diagonals as well, like that. Okay, so that's half of what
we need done right there. Now, we also need to rotate
any projectiles we instance. So let's get that
rotation angle. So inside of cast, under where we create the pact scene
for the projectile, let's do VR rotate angle. And we're going to take
the get facing vector. We're going to convert
that to an angle here, and I think that's in radiance. If I right click
and do Lou symbol, we can see returns the
vector angle with respect to the positive X axis in radians. So that would be stuff like Pi divided by two kind of thing. Okay, but we want to rotate
it so that the spear or other projectiles are facing the right by default
as our base angle. So if it's facing
up and the sprite, we want to rotate
that Pi divided by two or like 90 degrees like this so we're going to add Hi, which is a constant
divided by two, and that'll make it so that it's facing the right by default. Withut mathing it
out, it's really more of a trial and
error thing here. But let's get that rotation. So we're going to after we instance the projectile and
we said it's global position, let's do projectile dot Rotate and this function takes radians. So we have our rotate angle. Really, I should rename
that to be rotate ads. So I'm going to double
collect that Control R, and we'll say rotate
ads for radiance, replace all, and that
looks a lot cleaner there. So save and run, and there we have our spears pointing the right direction. And this should also work with essentially any projectile. As long as those projectiles face upwards by
default, if they don't, then you can rotate this
sprite individually on the projectiles scene until they are facing the
intended direction. So there we have our aiming and rotations for
the projectiles. And to show one more thing,
if I let go of the direction, then our direction
is now vector 20, but we're still shooting
to the right because facing will never be set
to vector two dot zero. And that's why we use facing instead of the raw
direction input. Okay, that's a big
step done right there.
19. Adding Sound Effects and Music: Let's add some sound
effects into our game. First, I'm going to start by having a launch sound effect, and then maybe when the enemies
reach their death state, we have the option
of making them play a sound effect
there as well. I would probably stay away from on hit sounds because
there's going to be multiple enemies getting hit on every
single second, and that would
just end up being, I think, too many
sound effects at once. You wouldn't want to overwhelm
your player too much. So let's just start
with those two. So for the player spear weapon, let's say in the weapon to D, we'll add in a sound player. So at Export var audio
player of type or audio stream player two D.
We want it to be two D because these projectiles exist inside of the TD game world, so it makes sense to
affect the audio based on the spatial positioning of the camera and the
projectile and all of that. So we will take the audio player and we'll have to
put it under here. So to add audio player node, we're going to zoom in here and right click on Spear
weapon, add child node. So we want the
stream player two D, add that in under there. And we want to assign a stream audio that we can play here. So let's find out sound effects. I'll collapse all the folders. Let's go into audio. Let's see. What was I using?
I think we had, like, Blal sound effects. Okay, let me get my headset on here so that it's not coming
through the microphone. Okay, so inside a, I think
it was going with slash 04, so we could double
click on that, go to the top right and preview. Yeah, I think that's what I
was using in my demo project. So let's take the slash and put that into
the stream here. So now we can just tell
this audio stream player to play whenever we
launch a projectile. So let's go into stream weapon. And when we have the
projectile cast, we'll say that if the
audio player is not null, then we'll tell it to play
its current audio clip. So down here, after we instance the
projectile, we'll say, I audio player then we'll
say audio player do play, and that's basically
all we have to do. The reason we have this condition here is
that for some weapons, you may not actually want
it to play any audio, so we will just skip
that option here. Now, one thing I want to point out here is
that we are giving extra responsibilities
to this weapon script. So now the weapon two D is not only responsible for
instancing the projectiles, but also telling
the audio player to play when the
projectile is instanced hiring its own reference to the weapons loadout and
creating a timer, as well. So it's a few things
going on now, I don't think that
this is overwhelming and a problem for the game, but it is something
to be aware of. If you notice your scripts taking on too much
responsibility, then you might want to
consider breaking it into multiple nodes
with different scripts and having them work together as individual pieces rather
than one giant mega script. But we're not there
yet. The script is only 50 lines long. It's not really a problem. If you had maybe two
or 300 lines of code, it would probably
get to the point where you would need some
sub objects, I would think. But don't worry
about that too much. Most of the scripts in this
course are pretty simple. I just want you guys to
be aware of that going forward that a script can
become too monolithic, and that can be problematic
in the long run. We have a spear
weapon. We need to assign the audio player for
it to actually play audio. So assign and then
audio stream player. Now if we play, we should
get our sound effects. Okay, yeah, there
we go. Every time we instance a projectile. Okay, now we want to
play a sound effect when our orcs die. So let's go into the orc scene. All right, click on the orc, add a new audioStream
player here. If we want, I could
rename this to be a death audio player. We could say. Okay, so
now we would want to find a suitable
death sound effect. Playing around with
it a little bit, I think I think enemy
Death 01 is too much. Even flash oh two is a
little much as well. The thing is,
there's going to be enemies dropping like flies
in this kind of game. So we need something very subtle or maybe even none at all. I think I'm looking
for something more like the Step grass 03. Now, that might sound
a little weird, but I think anything
that's longer than that is just going to be too much to see how it goes, may even remove this altogether. So let's assign the Step grass oh three to the stream here, and to tell it to play that
sound effect on death, let's go to the
animation player. Down in the bottom left, we will select the
death animation. And we'll add a new
track, add track here, call method on
Death audio player, and we'll right click at the
start of this animation, insert key, and we want to play. So it's kind of like we're
writing a script when we're defining these
animation keyframes, except we have the timeline, so we can tell exactly when we want it to
play that animation, and it's very easy
to move it along to a different point
in time just by dragging it to a
different keyframe. So you could script all of this writing GD script code if you wanted to and just
skip the animation player. But obviously, animation
player kind of makes it a lot easier
to do it visually. Okay, so now if we play, we should get both
sound effects to play. So you can hear
when the orcs dive, it does play that
little sound effect. It's a little quiet,
but maybe that's okay. If we think the spears
too loud by comparison, we could even take the spears audio stream player and drop the volume
decibel here by a bit. And hit play and run
it one more time. You can even edit this
while the game's running. So we could just drop
this down. Okay. You can see it's
already way quieter. Sea, one of the nice
things about cado is you don't have to necessarily close the game to change
your properties. And let's hit an enemy. Okay. And the volume difference between those is a
little less pronounced. So it's probably good now. As a little bit of a
bonus for this video, if you want to add in music, let's go to the world, and let's add in a
audio strem player, audio stream player two
D, just the default note. So stream player here, and then pick this one,
Audiostream player. Okay, so with our
audioStream player, go over to the right,
and we're going to want this to be on autoplay. And I think to make it loop, we actually have to load it from the audio MP threes first. So let's close everything
in the bottom left again, go to Audio, and we have the box jump song from
the Th Red Hearts pack. So we'll bring that
into the top right. It's actually not P three. It's a dot OGG, but both formats for music
are supported in KDO, so it'll work fine. And then we want to take
this and check Loop on. But we might actually need to
do that in the bottom left. So double click the OGG here. Okay, and then
check looping here. And when it gets to the end, it should loop the whole
thing. Okay, so that's good. Okay, so the last thing
before we play the game, I'm going to take the volume DB and put this at negative 20 because I think it's
probably going to be quite loud, otherwise. So let's hit Play
and see how it goes. Okay. So we can hear the music, and
it's still pretty loud. So I'm going to take
this to negative 30. Maybe even negative 40. Yeah, that's probably a
bit more appropriate. So now we have the audio. We have the music, and we have a Enemy
Death sound effect. So that's kind of
the basics there for audio sound effects and
music inside of the game, and we can keep
going from there.
20. Combat System Singleton: Okay, so the basic
projectile damaging and combat kind of
works at the moment, but we don't really
know how much damage is being dealt to the enemies
in the context of the game. So one way we could do that
that would also visually enhance the gameplay would
be floating combat text. So whenever one of
these characters takes damage on the hurt box, we can communicate
with, let's say, a combat system to instance, floating combat text at the position they took damage every time they take damage. So this will mean
there'll be a lot of numbers floating
around the screen, but I think that works pretty
well for the style of game. It's very action based, and we do want
immediate feedback whenever we are
damaging enemies. And we also want to
see the benefits of leveling up our abilities, as well as our character
gets stronger. To create our system,
we're going to want to do that as a
Singleton object, meaning that there
is one reference to this class or this system
throughout the game. And if we set it as auto load, it will always be there
in the background, and it will be easily
referenceable by name. You can also implement Singleton pattern in
the script itself, but we'll do it the Gada way, which is a little bit simpler
and easier to understand. So at the top here, I'll
just create a new scene. We'll create it as other node, and this will be a base node. If you are two here, you can rename it to
be Combat system. Then let's right click on it, attach a script, Combat system. We can save that either
in the root or we can create a systems folder
to save that into, which would probably
be my preference, and I'll create that in there. Now we want to save this combat system into
the systems folder. So combat system
dot TSC save that. And now let's make it
a auto load so that this node script can be referenced across our game
just by using the name. So I'll go to project
project settings. Then we have Globals here. And we want to add a
new TSCN as a global. So click here, go to
Systems and then do TSCN. You can also see
that you can select a dot GD script as
the combat system, and what that would do is
create a new instance of the node script in your game as soon as
the game launches. But if you use the dot TSCN, then you get the advantage of being able to use
export variables because this is a very specific copy of the combat system. If I double click that here and add this as combat
system node name, then now we have this
combat system as a global Singleton
and side of our game. And the difference
between autoloading a dot GD script and
auto loading the SEN directly is that if
we do something like Export var spawn count, integer equal to ten, then we can set this
over here, right? But if you instance
a script that hasn't been defined as
a scene or a node yet, then you wouldn't have
any ability to actually customize properties
like that in advance. Now, I'm sure there's
ways to get around that, but that's one advantage
of using dot TSE. Okay, so we have
our combat system. We're going to want some kind
of report damage function. So I'll just kind
of draft that out. Here. Let's say function. Let's say report hit. I
think that makes sense. Let's say we have a
target that got hit, and we can say no
two D could just be anything in the game
that was able to be hit. Generally, that would
be like a character body or a static body. But we'll just use the base
class for flexibility. And then we have here
the amount of damage. So P damage is an integer. This will return void. And then in this combat system, we will instance our scene
for the floating combat text. So let's go up here to
the top and we can say at Export var floating
combat text scene. And this will be of
type packed scene. And down here on Report Hit for right now, we'll
just do a pass. Or if you want, you can even do a push error and say
not implemented. This would give you a little
bit more feedback if you ever forget to implement a script rather than
just it would run the function and it would pass so there wouldn't be any error, but it also wouldn't
do anything. So I think this is
a little better during development, actually. Now let's jump over to
our HRT Box two D script. So Control P, HRT Box two
D. And then inside of here, we can reference
the combat system. Okay, so combat system
dot report hit, and we want the object that the stat
controller is managing. So I don't think
the stat controller actually has a
reference to that yet. If we take a look at the
orc scene real quickly, let's click on the
Stat Controller. We can see there's
nothing over here in the right side to get
a reference to the Oc. We could just make that like a get parent kind of reference or we can make
it an export variable. If you want to make sure that the connection is more absolute, then I would go with at export. Because that would mean
that if for some reason you move the stat controller
down one level, like you parent it
to something else, then Get parent would no
longer return the orc. So if we use at export, we can make sure it
returns the c. We could say something like
body or character, maybe even object,
keeping it more generic. And then this can
just be the object this controller is
managing stats for, and I'll assign it in the
top right to the orc here. Now, if we go back
to the Hurt box, we can use the stat
controller dot object here, and then we can pass in
the damage P damage. Okay, so that'll give
the combat system everything we need to know about where to position the text and how much damage to
assign to the text.
21. Animating Floating Combat Text: Now we need the floating
combat text itself. So let's create a
new scene over here. Click plus and do
user interface. And we're going to
make the base node just be like floating
combat text. But the actual text we'll
assign as a child label. And this will help us out when we're using the animation player to animate because we don't want to change
the root position. We just want to change
the texts position relative to the root of the
floating Combat text control. So, right click here, add a
child node, look for label. And then let's right click on floating Combat text
Adhild animation player. Okay, I think that's
what we need here, so let's control S. And we can save this. Maybe
we create a folder for UI. This is kind of UI still. So let's create a new folder. I'll type in UI. Okay. And then floating combat text dot TSC and can go
right in there. So hit safe. Okay. And now we need a script attached
to our root here. I'll right click on
floating Combat text, attach a script, and we'll
create that in the UI folder, floating combat text dot gD. We're going to want
a reference to the label so we can
manipulate it here. So control var label label. So we'll assign the label here. And we want to create some
kind of initialized function. If you've used Gado, you
probably know that there is a function underscore
and knit here. That function is used for when a node is being
created in the scene. But in this case, we
need to wait until the object has actually
been created already. So that's why we need to create. So if you've used
Gadot for a bit, you might know that there's a function underscore
and IT function. So this is a overridable
function where it can run code before
all the properties have been set for your system. You might use it to pass in optional parameters to set
those properties when you are creating a node
from a function call rather than existing inside of a packed scene
like over here, which is what you usually do, you would just set
up the scene and then instance the scene
from a paced scene. So that's actually not
what we want to do. We're not trying to customize
the initialized function. We're trying to make the
floating combat text run when it is
already in the scene. So I just wanted to make
that distinction there. Okay, so what we want to do in the floating combat text script is have the option to set
the text on the label, and we'll just do
that directly with a kind of more of a
delegate function here with function set text. So we'll set the
text on the label, but we'll do it operating through the root node
floating combat text. So set text, and
we'll say P text, which is a string,
we'll return void, and we'll take
label dot text and set that equal to P text. So one reason to do this is
that our Okay, so, of course, we're going to want
to set this whenever we instance the floating
combat text scene. So you can give a
class name up here so that we can easily get
that set text method. So class name,
floating combat text, we'll hit Controls there. And now in our combat system, whenever we report a hit, we want to instance our
floating combat text scene. See, let's say Vara text is
of type floating combat text. And we get this by instancing the floating combat text scene. So enter and then instantiate. Okay. And then we want
to add that as a child. And now we don't want to
actually add the text as a child of the P target because when the P target gets
deleted from the scene, like the death animation plays, it would also remove
its child nodes like the floating combat text. So because we have
this global system, the combat system
to handle the text, I think we might be able
to get away with just doing add child on
the system itself, and then we add the text node. Now we need to set
the global position to the position of the P target. So text Global position equals p target dot
global position. And if this bit doesn't work or we run into any other issues, then we may get a reference to, like, a canvas layer, the actual UI and position in there under some parent kind of similar to what we were
doing with a projectile, so we just need a parent
to hold all of the text. But the position
should be wherever the enemy took damage
is the idea here. Now we want to take the text
and we want to set its text. So set text to a
string of P damage. So P damage is an integer. We convert it to a
string by saying STR and then wrapping the P
damage in the parenthesis. And then we set the text
to that string value. So if we set the combat system
floating text scene here, so now I'm looking at the Singleton scene,
the combat system, we can go over here
and we can quick load our floating combat text. So we have that reference to the packed scene over
here on the right, so it should be instantiate let's hit Play and
give it a shot. Okay, so we're going to
have the damage be hit. Okay, yeah, that's kind
of what we expect. And you see, very important when the orch gets
removed from the scene, the text does not remove itself. Okay, so now we just handle the animation for the
floating combat text. So we did that in the
animation player. Create a new animation
at the bottom. New. And let's see. We can
call it whatever we want. I'll go with float up, and then we need to add
some properties to animate. So add track property
track label, specifically the label because we want that offset position. So let's look for position here. Okay. Now, we can't see anything because there's no default
text on the label, so I'll click on the
label, and I'll just say DEF for default here. We may also want to
assign a theme with a new default font that fits our Pixar game
a little better. So we can do that by clicking
on floating Combat text. Going to theme on
the right side. Do new theme, expand this, select a default font, and I'll choose what we
already installed at the start of the
project five X seven, which you can see is
a nice Pixar font. So and then if we want to reuse this theme,
which we probably do. I'll right click and save
this to the project, and UI sounds fine. So we'll just say game theme. Dot TRS for now. Okay, if we close
that, you can see it's now referenced
by the file name. Okay, now, a couple of things. On the root node, the
floating combat text, we can see it basically
has this giant box. We don't actually need
it to be that big. So go to layout. And then instead of
Anchors preset full rect, let's change that to reset. So now the control is just
this little one point up here, and the label is its
own size as a child. Then another thing on the
floating combat text. I do want to take mouse
and change the filter to ignore just so that mouse clicks will still
go through on the screen, even if they're touching
this floating combat text, I want mouse clicks
to be ignored. So if there's any
UI on the screen, this won't interfere with
pressing the UI button. That's pretty important. And now we go back to our
animation player, and let's keyframe the position. So I'm going to zoom
in at the bottom. Let's right click
keyframe the position, and let's move it to wherever we want the
starting position to be. So the 00 is where our
enemy took damage. I think I just want to make sure that it starts
somewhere above that. So I'm going to w
to move the text. I'll move it up here, and then we'll keyframe
the position here. So go to layout on
the right hand side, and then go to transform
and keyframe the position. So I have it at negative
15 pixels on the Y. And then we can go to how
long do we want this to last? Maybe half a second. You can also enable snapping
if that helps. Okay, so 0.5 seconds. And then let's move it up here. So kind of like that
and then keyframe it again in the right hand side, just using the keyframe button. Okay, and now it's going to animate between
these two points. So if I go to the
start and I play, you can see it's going to
animate that position movement. We can change the full duration
to 0.5 seconds as well. And then if we like,
we could just have it ure at the end by doing add track
call method track on the floating
combat text root. And then right click here, insert and type in quarre. Okay, that'll make sure that the text cleans itself up after it is done in this scene without having to custom script
anything else, really. We can just call it
from the animation. Now, this would probably work, right, right now, but
it's really basic. Let's test it. Just
make sure it works. Okay, yeah, I didn't
play the animation. So that's because I
didn't set Autoplay here. So in the bottom right
of the animation player, Autoplay on load. Okay, now we hit Play again, and we're getting our animation. Okay, so that works at
a very basic level. It's not bad at all.
But while we're here, why not make it
more interesting? So I can add a track on the
property track of the label, and we'll do, let's say, scale. Okay, so this will
let us make it bigger or smaller
as time progresses. So let's key frame it
as it is at 00 time. Let's say about
0.15 seconds or I guess I'm snapping to 0.16 67. It doesn't really matter here. And you can change your
snapping in the bottom, right, by the way, if you want to have a more customizable
keyframe snapping. So let's go to label and
then transform scale. We will take its
size and bump it up. So maybe like 1.5
or maybe even two. We can start with that and
scale it down if we need. So I'll keyframe this. So now it's going to go
from 1.0 to 2.0 size. And then when it's fading out, we want it to go really small. So let's take it to, let's say, 0.1 size, and then key frame it. Okay. So now if we play our animation, it's
kind of like this. Maybe the scale is
a little bit too big on this second keyframe, so I'll just take it
to let's say 1.5. I think that's right. Hit play. Okay, that works pretty good. And we can also take the
position and offset it maybe to the right when it is reaching
that peak size point. So let's take the position
here with W move mode and move it off to the right or something
and then key frame it. Okay, and now we'll play again. So now we can see
how text pops out to the right and then
back to the top left. Another thing we
could do would be to change the color or
the Alpha over time. So let's add a track on
property track of the label, and we'll make this
self modulate. So it only affects this label, not any child nodes. So I'll self modulate
on the label. Let's right click insert
a key at the start. So this will be our standard
white with full Alpha. Let's go to 0.15 or wherever our key
frames are snapping here and insert a key. 0.0 1667 for me. And then let's change
the color to red or something because
this is for damage text. And then let's go
to the end here. Right click insert a key, and we'll change it to
let's say zero Alpha. Can it play. So now
it's kind of like that. I think it might make
more sense if we swap the first
keyframe and this one. So I'm going to drag
this on the timeline. I'll pull the left
over here to the 0.0 1667 point. This
goes over here. So now it'll start at
red and go to white. And we could have it end
with a white color as well. So I'll click here,
go to the value and change it to 255
for the green and blue. So it ends white though
fully transparent. If I play a few times here, it feels very gradient. I mean, it really is
because we're just traversing one color to another color over
a period of time. So gradient would be like
that from left to right. Like, it goes from red to white as it progresses
across the screen, except instead we're
doing it over time now. So kind of up to you
how you like it here. You could just change
this to be red, too. So it's just red all the time. The only thing that changes
is the transparency. Then you'd have to set
the third key frame point as well, kind of like that. Maybe we would want it to get
darker over time instead, so we could take the values here and make
it like a dark red, kind of like that and
see how that goes. Yeah, that looks
pretty good, I guess. Maybe the red is a
little too vibrant, so I might take the initial one and make it
kind of like that. And maybe it can be a little
brighter at the peak. Let's try, okay, darker to bright, and then
to dark again. So, that seems to work pretty
good. I kind of like it. This is purely a personal
preferences artistic thing. So really, whatever you
like, go with that. Okay, so let's play
and test it out, and we're going to
have our damage text as soon as we hit the enemies. And it looks to me like it
just moves way too fast. So maybe we need to double the duration of this animation. So if we click on
animation player, we can just move these
keyframes along, and this is one of the
huge advantages of using a keyframe animation
editor rather than, you know, hard coding it out. It's just easier
to iterate and to make small changes to adjust it to be where
you want it to be. So let's take these
keyframes for Kframe. Put that at the end here at 1.0. This should go to 1.0 as well. And you can drag a box
over multiple keyframes and move them all
at once like that. So I can drag a box here, and we move this to, we could try like 0.5
seconds, whatever. Okay. Now, that's pretty slow there. So maybe we want this to
be more like front loaded. Let's bring that over there. Hit play. Okay. I
mean, I'm not sure. We'll have to give it a shot and see what it actually
looks like on the screen. It seems like it takes a
little too long to fade out. So maybe this does belong more like there. A little
further along. And maybe 1.0
seconds is too long. I'll just kind of drag it there. So whatever this keyframe
point is 0.8 seconds, that'll be the
duration of the clip. Maybe the text is
also too small. So I think one way we can
kind of get an idea is to put a copy on the world
scene so that we can see it right
here as it is. Okay, so there's our Daf text. I think we can
actually write click here and do editable children, and then we can click on the animation player and just
test it and seeing here. If we switch to float
up, does that work? Yeah, kind of does. Okay. Yeah, this helps a
lot. So you can see really what it's going to look like in the
actual gameplay, because we have the
mock up scene here. Okay, so maybe let's
take the position here, and I'm going to move
this down a little bit. And it looks like
we can keyframe here as well, so that's good. So do this actually get
the 28 negative 31? Let's have it fade
out a little faster. So I've never actually tried
it this way, specifically. It seems like you can
keyframe on the label, but you can't adjust the animation player directly when you're editing
from this other scene. So there's a bit of
a trade off here. I might change the self
modulate on the first frame. And keyframe it there.
Okay, so that works. That updates the
animation player. And then I can jump into the
floating combat text scene. And I want to take
the modulate color and just move that over
to the left a lot more. So it'll turn red faster, but I didn't really like it when it was red the whole time. Okay, and then I'll go here and I'll add another key frame. And in this, I'll make
it full transparency. So it'll be visible on
the screen for longer. It won't fade out until close to the end of
the full animation. Let's play, and I'll
try that one more time. Maybe here I also want its
color to be white as well. So I'll do that. So just
have a flash of red. One last thing in
the bottom right, if you take the
interpolation mode here, you can change it to cubic. And I'm going to do
that for the position, the scale, and the self
modulate properties. So if you look
really closely here, it puts a bit of a curve on the left and right
sides for that. So if that's working like
other timeline editors, which I think it does, then that's going to mean
that it'll be a ease in and ease out curve rather than a linear transition for
the animation here. So let's test that here and
see if that's the case. That's what I would
expect coming from other things like
DaventiRsolt video editor. And it does seem to
have that easing here. So I'll change it back real
quick just to compare. We take that to linear. Yeah, now the speed's
just consistent. So I was writing my assumption. So that can give us a little
bit more of a oomph to the animation because it has its slow moments and
its fast movements now. If we hit play, we'll test
that out one more time here, and I think that works
right for right now. So I'll hit play.
We'll test it out. I feel like the animation
is a little long here. It just needs to have
that popping moment where we can clearly read it. Okay, so once
again, I'm going to shorten the animation
just a little bit here. Pulling all the
keyframes to 0.7 there. Animation ends at 0.7. Let's play one more
time and test it. I think I'll just wrap
it up after this. Okay, so the fading
is now very fast. Okay, and then for the
position when it's fading out, I want to move that as well. So let me drag this over
here so I can see it. Click on the label, pork
kind of maybe there. I'll keyframe the position, and then I'll drag this point
over back to the right. I think this will just make the animation a little more subtle. Okay. Yeah, so now
when it's fading out, it doesn't move quite as far. And I think that's a
little softer to look at. So we could go on like
this for a long time. I think you get the idea. Just
customize the animation to whatever you think would make it feel good in terms of color, scale, transparency,
positioning, et cetera. The main things for this
tutorial, though, code wise, are just making sure you have the combat system.
It's auto load. You tell it to
report the damage, and then on that report damage, you instance the combat text, and you add that not as a child of the target node
that took the damage, but as some systems node or just a persistent node that's never going to remove itself. And you remove the
floating combat text in its animation
by calling Q free. And that's basically the gist everything else is
more artistic detail.
22. Enemy Movement with Player Tracking in Godot 4: Projectiles already
have the ability to damage and slay an enemy, but our enemies don't actually
walk towards the player. So for orcs to move
towards the player, first, they need a
reference to the player. It would be a little
bit tricky to have a direct reference between
the orc and the player initially because they are not existing in the same
scene when the game starts. When we spawn enemies
into the game world, they're coming from
the packed scene. So they don't exist
here until they're actually put into the scene
by the spawning system. So what we could do instead
is have a reference resource, we could call it player context, where the player sets
itself on the resource, and then each orc is going
to reference that resource to pick up on the player target as soon as they spawn
into the game world. This would have
some extra benefits of being able to swap out, let's say, the player
target on all of the enemies by changing
out the resource once. We could even have
the combat system set the reference as soon as they spawn a copy of the orc
into the game scene. So the spawning system
could essentially choose which player each orc
is going to go after, and that can have some
added benefits over just making it a pure
single ten reference, which you certainly could do, in this case, for the player. But that would also tie you down strictly to more of a
single player game, which is fine for this course. But I'm going to try to
show how you can achieve it with the resource method. So in the bottom left
of our file system, going to collapse everything. And let's go into characters, and I'm going to right click
here, create a new script. Let's call this player context. And for inherits, let's
make it resource. Okay, create the new script and make sure you open it up in the script editor will give the class name player context. Okay, so this is just
setup to be essentially a shared reference to a specific
player inside the game. So what we need here is
going to be var player. And if we have a player class,
we'll make it extend that. So var player is of type player, and that might actually be
all we really need here. For that resource. So let's go to the
player script now, and inside of
player, I'm going to export a reference to context. So let's do a
Export var context, and I'll call this of
type player context, controls to save it. And we can see in the right now we have a slot for resource. So let's create
our new resource. And then I want
to right click on it and save it to the project. We'll save it into
let's say characters. We could put it in player
if we want, so why not? And then I'll call it player Context here dot TRS,
which is for resources. You might also want to
move player context at GD into the character
slash Player directory. Okay. And now our
orcs need a target. So we can go to the orc NPC. You can click over
here if you need to. We'll say at Export var target
is a type player context. If you needed to, you
could also make it like a target context if
you just need to go a step more generic for this, but they're only ever
going to have one target. So I guess making more
specific by calling it the player context
might be a little bit better here for
our specific case. But feel free to
make it a target context if you
want enemies to go after inanimate objects or
NPCs or something like that. And this is going to
hold the reference to the player target that
this NPC is chasing. So let's just set it
directly in the inspector. So click on OC and then
on the right hand side, we need to give it
the player context. So I'll click here. Quick
load, player context. And you'll notice
that even though the orc and the player are two completely
separate scenes, we can have them
reference the same resource and our project. The resource exists
on the file system, so we can reference
that inside of our individual packed scenes. So when they actually get added to the same game world scene, they'll both have
the same reference. So the player is going to set itself as the player
on the player context, and then everything else, the NPCs are going
to read from that. And that's how they're
going to relate to each other through this
context intermediary. So the NPC script isn't going to do anything
directly right here. We're going to have that
set up on the Chase state. But the last thing we need
to do before we get to the chase state is going
to be to go back to the player scene and
inside of player on ready. First, we want to
assert that the context doesn't null because it's
kind of needed for our game. So assert context so
the context resource needs to be set here
so the player can make itself the player
of the context. And then we're going to take context dot player equals self. Actually, this is
going to air out anyway as soon as we don't have a context because
this can't be null. So this assert might
not even be needed. We can just cut it there
and reduce the redundancy. So as long as we have this line and the
player loads first, the player on the
context is set, and then we load in
all the other NPCs, we can have them use
their hace state to move towards the player now. So inside of Ok, we're
going to go to Chase state. So in the Chase
state, we need to get that reference
to the player. So we can get that as a
reference to the orc, and then the orc has the
player context here. So there's a few steps
removed. So let's see. Maybe we'll say something like VR target, node two D here. So we have this local variable. And then whenever we enter, the Chase state we'll get a
reference to that target. You can even do it on setup
if you just want it to be a one time thing.
I'll put it in Enter. So every time we
enter the state, we'll get the new target
from the player context. So underscore target equals agent dot target
dot player here. Because we don't know what
the agent type is of NPC, we don't know that it
has the target and we don't know that the
target has a player. Okay, so this bit is a little
bit fragile because it basically makes the assumption
that the agent is a NPC, and it makes the assumption
that the target is set, and it makes it the assumption
that player is still the property on
that player context for getting the target from. So this line specifically
could break pretty easily if you change some of
your other classes around. You might want to just
set up some asserts here also to just kind of be
early warning signs. So we could say assert the agent dot target and maybe assert agent dot
target dot player. Let's change this from agent dot Target to agent
dot Target is an NPC. Okay, and I think
that's good enough. It doesn't cover all the cases, but if we were to change this class to something
else completely different, I think these two lines
would catch that. Let's hit Play and see if we're still good or if
we get an issue here. So agent was expected
to be an NPC. Maybe what I want is
agent Target as NPC. I might be just using
the bound keyword there. So here, I actually
want to check that the agent is an NPC. The agent dot Target
is the player context. So we make sure
that the agent is an NPC so we can
operate it on it. And we're getting no issues
now. So that's good. And the last thing we just
need to do is to put in the movement towards
that target location, and we're going to do that on
function underscore update. So let's implement
that. Okay, so we need to get the
target position. So var Target position. Okay, so that's
going to be assigned from underscore target
dot Global position. And then we have the
position of our agent. So var agent position. That's going to be equal to
agent dot Global position. So an agent can technically
be a regular node, I think, and that's why
it doesn't show up here. Savara agent position. The reason it can't
infer the type is because we don't know for sure that the agent has this
global position property. So we do need to declare
it there as a vector two. Okay, and then we want
to get the direction between these two locations. Savara direction's
going to be equal to the target position minus
the agent position. And then we normalize
this vector to get just a direction. So it normalizes the XY
value to a scale of 1.0. It won't have anything
like the distance anymore. It's just going to be
the peer direction, which would look a lot
like if you're doing input dot get vector
for the keyboard input. That would also be on a
scale 0-1 for both X and Y. Okay, now that we
have the direction, we just have to move in the
speed that we are assigning. So each of our
characters are going to have a speed in the game, and we're going
to get this speed from our stat controller. So I think one good way of setting it up would be
to use the Blackboard. So if I click on Enemy HSM, we can go over to
the Blackboard plan, and let's manage, and
we will add a variable. Let's see stats. This can be a node path, and then we're going to
assign the stats controller. So now we can use that in, let's say, our Chase script
and get the speed from it. So on setup, we want to
get that stats controller. So we could say var,
underscore stats is a stat controller, and we will say underscore stats equal Blackboard dot Git var, and we're looking for
the stats property. So from there, we want
to get the speed. So var speed down here
at the bottom again, which is a float is
going to be equal to underscore stats dot Get speed. Or if we want to make that
even more straightforward, we could just say dot speed. So now we need a runtime speed
for our character stats. So we go into the
stats controller, and let's create a
speed property here. So for speed, a float
is equal to 100.0. So this is basically
the run speed for the character at runtime. We haven't set up any
definition resources yet to control basically what
the starting stats of each character like the orc or lady will put in a
skeleton will be. So we're just building this
up one step at a time. So now in the Chase State,
we have our direction, we have our speed,
we have our Delta, so we just need to move the
character a certain amount. So when you're moving a
character body two D, there's at least two
ways you could do it. One would be to call
Move and slide, and that would be on the NPC. So this agent is
NPC thing up here, I think it would actually
be better if we did Var NPC of type NPC. And instead, we say, underscore NPC
equals agent as NPC. And then we assert that
this NPC is not null. So this ensures that
the agent is an NPC, but also we have a reference to the NPC so we can use
its class functions. So that makes it easier
to come down here and say stuff like NPC dot
MOV and slide. You can use this, and
this would also have the ability of sliding along any collisions
that the enemies find. But we're going to have our
enemies ignore collision for this game partially because it allows you to squeeze more
enemies onto the screen, and also partially
because it makes it simple but I'm actually going to do npc dot
translate here. So this allows you to move without taking collision
into consideration. So it has the downside of your characters aren't
going to be able to slide along edges like water's edge or a tree or things like that that
are getting in the way. But it also has the advantage of making the movement as
simple as possible, which means you can
squeeze more enemies onto the screen at one time. And I think that maybe
this arguably fits a vampire survivor style
a little bit more. So we're just going to make our enemies brain dead simple. So pc dot translate, we're just moving it in a
direction by an amount. So we do direction times
speed times Delta. Now, that would work fine, but you might want to
take that bit out to one more variable above
and save our move, which is of type vector two, and we'll set that
equal to this bit here. So we do the math outside
of the function call, and then we call it on the move vector two
once we have that. This also would
help with debugging because you can set a
breakpoint there and then it would show you
what the move value actually came out to
before you call translate. Whereas, otherwise, if you
just put this into there, you wouldn't know
what's going into the translate call unless
you jumped into here. So a little bit cleaner,
a little bit easier. Okay, so I'm going
to hit play, and we're going to see
where we're at. We have the orcs running
towards the player, and wherever the player
is on the screen, they're going to
try to go to that, which is exactly what
we would expect. Now, they do move
pretty fast, though, so I might go into
Stack Controller and take the default speed
to something like, let's say, 40.0, and at play. Okay, and I think that's a
little bit more appropriate. In most cases, the enemy
should move slower than the player so that the player at least has a chance
to avoid them. We can see that they're all stacking on top of each other. Okay, so we'll hit
play, and we'll see that the orcs are going to
run towards the player. These trees don't have
any collision setup, but even if they did, they would still run right
through the player body. Now, the player is still
colliding with the orcs as well. And that's because
the orcs still have a collision shape and body, and the player is moving
using move and slide. So it's kind of up to us where
we want to go from here. We could move all the orcs onto a different collision layer
using collision here. And I probably would
do that right now. Let's set up some
collision layers. So edit layer names. We'll say that one is world, two is player, three is enemy, four is projectile, and that should be good
for now. We'll close that. So we want to move our
enemies onto the enemy layer, and we'll turn off the
mask so that they're not actually looking to collide
with anything right now. So now if we hit
play, our player will be able to walk right
through the enemies, but would still collide
with anything else. So actually, I don't really like that they stack
up on each other. So even though
Translate would be the fastest function to
run here for Chase state. And this is definitely
the set of code lines that is going to be the
performance block for the project because you're
having hundreds of potentially even thousands of enemies trying to move at the same time using
this update script. And remember, almost every
enemy is going to be in the Chase state all the time until they're
actually defeated, and they're only
defeated for a second. So you're talking about everything being in
the Chase state at the same time update
runs at every frame. So this is the part where if you need to optimize
anything, this would be it. Let's add the collision
back in for the orcs. We'll have them collide
with themselves, and we'll see if that just
gives us a better result. The orcs all being on top of each other isn't really ideal. So we'll put that back in. So instead of doing
NPC dot translate, what we do is we say underscore NPC Velocity is going to be equal to this bit
without the Delta. So I'm going to get
rid of the Delta and we'll say new velocity. Okay. And then we set the old velocity to
the new velocity. And then instead of
calling NPC dot translate, we call npc dot move and slide. Once again, I want to
iterate this takes about twice as much processing
power from my tests. So if you could have
5,000 enemies on screen at the same time before
you get too bad of lag, now it will be about 2,500. But if you can have
the enemies space out, maybe you don't need 2,500
enemies on the screen at the same time because they all occupy a completely
different position. So maybe this is the way to go. So let's go to the
orch, and we'll make it so that it masks the enemy. It'll also collide
with the player here. So it's looking to the
player, the enemy. And let's also make it the
world, but not projectiles. Okay. And then we want
to go to the player now that we're actually working with different collision layers, and we'll set its
collision layer that's under collision
Object two D to player. And then we want it to
collide with the world. And actually, I think just that. Thinking about it
again, the orcs, maybe I don't want it to
collide with the player. I want the player
specifically to be able to walk through enemies so that
it can actually escape. So the enemies will collide with the
enemies and the world, and we'll hit play from there. We could even turn off
world at some point. It kind of depends on what
we want. Okay, so let's see. It looks like the enemies are not stacking on
top of each other, which is actually probably good. They won't occupy
exactly the same space. If we want them to be
even more spaced out, we would just increase the
size of that collision shape. Here, if I pause the game
real quick and we go to Editor visible collision shapes, and we can go back in here. Oh, we have to restart the
game for that, I guess. Okay, we'll restart, and then we'll be able to see
the collision shapes. So this one down here, that's the collision shape of the orc. The big one is the hit radius, so we can hit them up high, but they can only collide at the feet because that's how we defined the collision
shapes before. But yeah, with this setup,
they'll be a little bit more spaced out from each other.
And I think I'm liking.
23. Spawning Enemies on a Time Curve: You can see our enemies work. They'll have a little bit of
a collision with each other. Which, you know, after watching this is definitely
the way to go, rather than the
translate method. However, we still only have
four orcs at the same time. We want to be able to spawn
a whole bunch more dudes, and if we're going for
that survivor like style, we want them to
spawn off screen and then go towards the player. So almost in a circle, we need them to just
appear off screen and then just swarm the player
in the center of the screen. So we'll need a spawning
system in order to do that. Let's go to the world, and we'll add in a new
system node here. I'll right click Add
a child node as node, and then I'll rename
this to be spawning system or sponnorSystem,
if you prefer. Right click on it,
attach a script. So we'll call it
sponsorsystem dot gD and I'm going to save it
in the systems folder. And then for our spawner system, we're going to need
to declare a whole bunch of different properties that are going to be export bars so you can customize
them in the inspector. So first off, we need
the spawn margin, which is a float,
and I'll have it default to ten.
Forgot the far there. Okay, so the spawn margin is the minimum distance off the screen that the
characters can spawn. That is in pixels. I'll be named this
to be characters actually and characters
up there as well. We might have a world
where we spawn NPCs, not necessarily just enemies. So it helps to be more generic
when you're not sure about the final use for your
script because you might actually want it to be more flexible than just
doing enemies. Okay, next, we want
the spawn Extra range. Okay, so at Export
VR span extra range, I'll have this go to 100. Okay, so this is
the bonus distance that an enemy might spawn away from the edge of the screen in addition
to the span margin. So if your spawn margin is ten and your extra range is 100, then that means the
pixel distance away from the edge of the camera is
going to be ten to 110 pixels. Then we need the definitions for our enemies that
we're going to spawn. So this should be
basically a setup saying what can spawn
under what conditions. So let's say at export
var random definitions, which is array of
spawning definition. Okay, now we haven't
defined this class. It's going to be
a resource type. It's a definition we set up in the editor that we
use during gameplay. So a resource is the perfect
type for that kind of data. Okay, so that's going to give us an error
because we don't have the spawning definition
class name defined. Let's do that. Right now. I'll go into the root of our
project and the file system. Let's say, maybe this belongs in systems because it's part of the
spawning system. I'll right click,
create a folder, and we'll say spawning. And then inside of
here, I'll right click, create a new script, and I'll say here spawning
underscore definition, which is of type resource. Okay, create that and
then open up that script. Okay, and we need to
give a class name, spawning definition
here at the top, and we'll worry about setting
that up a little bit later. So to continue with
the spawning system, we need a parent for where we actually spawn our objects onto. So at Export var spawn parent
is a node two D reference, the parent for any object
spawned by the system. And then we want the frequency. So I'm going to make
that a curve so that we can control the timer. As time progresses by just using a curve and then
sampling from the curve. So rather than setting like
ten different time periods, after 30 seconds, spawn
every 0.5 seconds. And then after 60 seconds, spa every 0.4 seconds. We can just have it as a curve. And then, no matter
when it's spawning, it'll just get its next timer
amount from that curve. So if you're going to
have a lot of values, it's easier to
define a curve with a slope rather than
going one by one and being like if X then Y in like a more
of a dictionary style. So I'm going to say export
var span frequency, which is of type curve. Okay. We'll come back to
that in the inspector. Okay, the next thing
we're going to want is a runtime variable. I'm going to call it
var random span stats. And this is going to
be of type dictionary. I want to give the dictionary
key type and a value type. And the value type
is spawn stats. So this is going to correspond
the type of spawning with the stats for the spawning in a one to one ratio at runtime. So this is runtime specific, and this is editor specific. And the spawn stats
is going to keep track of when the
last time we actually spawned an object is and how many times we have
spawned using this system. So let's create our
spawn stats object. I'll right click in spawning
and create a new script. We'll call it spawn stats. Actually going to have
this extend ref counted. Now we can jump into span Stats, and up here, I'll give
it class name spatts. And the two things
I want to track here are the spawn count, which is an integer starts at zero and the last spawn time, which is a float, and
we'll start it at Enfinit. This will be the last time that the object was spawned out. Okay, so now in our
spawning system, we have this relation. So when we start up
the spawning system, we want to take the definitions
and we want to create a new span stats for
each of the definition. So this handles
the runtime data. This handles the editor details for our one to one setup here. So if we want to
get the span stats, we can just reference
the span definition, and it will immediately spit out the span stats that
corresponds with that. And that's in a nutshell
how dictionaries work. You have a key, and
it'll give you a value, and it's very
useful for relating two different objects together. So let's set up our
system ready function. So let's say function underscore ready is going to return void. And we want to say for definition
in random definitions, we want to take the
random spawn stats, and we want to assign the
definition key to our value, which is going to be
a new spawn stats. So spawn statts dot n Okay. And now we have one of these objects for
every definition, and they're linked
in a one to one of spawning definition as the key and span
stats as the value. So now it becomes
very easy to look up the spawn stats
for the definition. Okay, and let's say that the next spawn time is going to be equal to the
spawn frequency, which is the curve, and
we sample on the curve the elapsed time since
the game has started. So we need a few more
variables up here, actually. We're going to need
the var elapsed time, which is the time since
the spawning system has gone into the game world. We can create a timer variable. So VarspawTr is a float
of type zero, zero. Since this is going to
be an internal timer, it's fine to just
actually have a node manage its
own time tracking. So we'll try doing that with
a span system rather than creating actual timer
object into the game world. So the spawning system
is going to handle its own timing for
this instance. And then the spa
next spawn time is going to be basically how long we're waiting
for this next spa. Let's set up the
timer function here, function underscore
process, and that's going to be Delta as the
parameter, returns void. We're going to take the elapsed
time and add the Delta, which is the time since
the last process call. So elapsed time
plus equals Delta. And then we want to
take the spawn timer and plus equals
the Delta as well, since we're keeping track of how long since the
spawn occurred. And we'll say I spawn timer is greater or equal
to next spawn time, then we're going to
do a random spa. So random span here,
and then after that, we'll take the spawn timer. And rather than set
that directly to Zal, I'm going to minus equals the next spawn time because there might be a little
bit of remainder time, especially if we get to really
small span time intervals. So I think it's a little bit better to make it more
accurate like this. And then we'll say that
the next spawn time is going to be a sampling
from the frequency curve. So equals spawn
frequency dot sample, and we're sampling
the elapsed time. So basically on the XY curve, it's a graph where X is how long has elapsed since the
system entered the scene. And then Y is how long to
wait for the next span. So whatever the elapse time is, there'll be a new
value we can get from the curve in order to set
our next spawn time at. Okay, a few more lines, and let's do our
random spawn function, so far random span. Return type of void. So what we need to do is get our spawning definition
that we want to spawn. So that's going to be a
random selection based on a weighting of each of the definitions to determine which object is going to
be span or not spawn. Okay, I'm going to pass
here for right now because the logic for the
spawning is going to get kind of a bit
more complicated. I just want to make
it so that we can at least see it working in the
game world for right now. So back out in two D view, let's take the spawner
system and we'll assign this bon parent to the
enemies node. Okay? So like that, it
should be enemies. This bond frequency is
going to be a new curve. So let's go on this curve, and we can define values. So if you can zoom in
here, that'll help. The Min domain in the
Max domain is going to be basically how many seconds
into the game we have it. So you could say something like 300 seconds or 120 seconds. However long you think
that the level should be, that would be what you'd
want to make the max domain. So if you have a final time for your level and
then after that, the level ends, then that would be perfect for the
Max domain here. The X Y value, what we want to get whenever
we sample the X value. So how long should it
be between spawns? Maybe two as the
max value would be then we go up here to the curve and we left click
to set a point, and we just kind
of control that. So in general, at the
start of a level, you would want less
spawns than at the end. So if we kind of pull
this up here and say, maybe only every 1.5
seconds, a character spawns, and then we go over to
the right and we left click and we pull down
this keyframe point to something like this
where we can say every maybe not 0.2 seconds. Oh, maybe ever 0.2 seconds. So every 0.2 seconds,
an enemy can spawn. And we kind of have this
curve from here to here. You can also click
on the points, and there's a curve adjuster if you need to get a little
bit fancy with this. So right now, if we sampled it, let's say at 75 seconds
into the game level, that would mean
that the spawn time would be about 1.2 here. We can see that from where the
XY intersects on the line. So likewise, if we went
all the way to 225, then it would be
about half a second between enemy spawn frequency. So the difficulty gets
harder as the level goes on. And we can just adjust
this. You can even set a third point in the middle
if you need like this. Maybe you need it to flatten out for a while and
then get really hard. Control E to undo. And I think you
can right click on a node to remove
it from the graph. Most likely two
points is going to be good enough
because you can kind of control that with the
handles if you need to change this rate at which it gets harder. But
we could kind of do that. Now, I'm going to personally
take this way over here to the left
because in our testing, I don't really want to
test past 2 minutes. I just want to make it fast
paced so that I can kind of show how it's going
to work later on. But in your actual game, feel free to have a much longer more drawn
out spawn curve. So right now, after
120 seconds here, it's just going to be
flat for the rest. And that's fine
for me right now. Okay, now let's create a
spawning definition. Up here. We'll add one. We need to create a new resource for
the spawning definition. So new spawning definition, we'll right click and save that. Let's go into
characters, enemies. And I can put this in the
same folder where the orc is at because we're going
to call it orc spawning. Definition or just orc spawning. I think orc spawning is fine. Dot TR, yes, we save it. But of course, this doesn't have anything associated with it yet. So we need to add in what packed scene we're
actually going to instantiate whenever
we spawn the orc. Okay, so in spawning
definition, script. Let's add export
the vR the scene, which is a type of packed scene. And we can also add
an export var weight, which will be a float of 1.0. So we have the scene to
spawn and the amount of preference to
selecting this scene as the random spawn choice. So when we randomly select, we might have like three types, and they each have
a different weight. So if one has a weight of three and the other one
has a weight of one, then the one that has
a weight of three has three times the odds of being selected as the one that
has a weight of one. And that's just one way
we can make it a little bit more skewed in
the randomness. So it's not perfectly random. It's that each choice
has a weight to it, and the weight
determines the odds. In the scheme of the
random selection. So we click on
spawning system now. Ok spawning is now going
to have a scene reference, so we quick load our Ok scene. Okay, now that gives
us what we need. So for random
spawn, I'm going to make it as simple as
we can right now by getting the first definition in the list and then
instancing that. So far instance, which
will be a no two D is equal to the random
definitions at position zero. And we're going to
get the scene, and we're going to instantiate that. Okay, so we now have our Oc. That's the zero position
and the orc spawning. So we'll take that
and we'll add that to the random parent or
the spawn parent. So spawn parent dot Ad
child of the instance, we'll just have the instance go to position 00 for right now. We'll worry about its random
positioning later on. And then we can see that the
span management is already managed out here outside of
the random spawn function. So honestly, that might
actually be about all we need. For testing purposes, I'm
going to take this and make it like 10
seconds over here, just so we can really see the spawning ramp
up if it works, and we'll hit play.
So let's see. Do we have any
characters coming at 00? Oh, yeah, yeah, I can
see them right there. Okay, so here's our orcs. You can see it's spawning faster and faster as time goes on. So we already basically
hit the peak orc spawning, and our spears can just
cut right through them because our spear has
no max hits count, so it's very easy to win
this game currently. But you can see how the
curve makes it so we can easily ramp up the
spawning as time goes on, and that's just a cool
feature I really like a lot. It's much better
to work with than defining in like at 10 seconds, now the spawn rate is 1.0, and at 20 seconds, now it's 0.9. So now you can just all
values for all the time, basically be managed
by this curve, and it's just much easier
to kind of edit them. You lose precision,
but it becomes a lot simpler to kind of give you a general sense of what
you need for these values. And you can still,
you know, click and set absolute values. So in points here, you
can see that you can set the exact XY positions for those points if you need
to get really technical. But in most cases, like I said, just two points on a curve
is going to be good enough. So there's a lot to add into
the spawning system still, but this is a pretty good
start for what we need.
24. Spawn Off Screen with Calculated Offset: Okay, so we already have
our spawning working. But the problem is
that the characters are just spawning at
the 00 point where we actually want them to spawn off the camera wherever our
camera is looking at. So it always looks like they're coming from somewhere
really far away, but really, they're
just coming from a little bit away from
what we can see. And then they're
gonna swam the player rather than spawning
in the 00 point. So let's work on that. Okay, so in our
spanner system script, we're going to go back into
it down at random span. So right now random spawn
is not very random. Let's work on spawning
the enemy off the screen. So we need to get a new location for where we're
spawning the instance. So down here, let's add
in a var spawn position. So that's going to
be a vector two. And we're going to get that from Get spawn position off camera. So we want to generate a span position that's
off the camera. So that's going to
involve some math and the current viewport
of the camera itself. So let's create our function, Get span position, off camera. So the first thing we
need to do is to get the camera TD that's
currently active, what our actual human
player is looking at on screen so that we can stay outside
of that when we spawn. So our camera is going to
be equal to Get viewport. So basically, we're looking at what the player
can currently see, and we're going to get
the camera two D that the player is using
to view the viewport. So we have access to that camera maybe it even makes
sense to mark this as a camera two D. So we could have a guard
check if we like here, saying that if the
camera is null, which probably should never
happen, but in theory, if it could, then we want to push an error
that there's no camera, no camera two D
found in viewport. Maybe that could occur if
for some reason you're using a three D camera and there
is just no two D camera. I guess that would
make it trigger an error. So it's possible. Okay, so if that occurs, then there's a major
issue in the game. We'll just return
vector dot zero. That happens and we'll push the error and let the
developer figure out, Oh, why is there no
camera in the game. Okay, but assuming
we have a camera, we can actually do the math. So first, we want to get the center position
of the camera. Let's do VR camera
center is a vector two, and we want the
camera doc global position for that.
Yeah, just like that. And then we want the
size of the camera. So how big is the camera on the X dimension and
the Y dimension. So that should be
another vector two. So we'll say VR camera size of vector two is going to
be equal to vector two, and we get the viewport size. So get viewport dot size, and then we want to divide
that by the camera Zoom. Okay, so the reason for
that is that our camera, especially in a
two D Pixar game, if I click on camera
two D up here, and then we look over
here at the Zoom, then you can see that our Zoom may actually take the
viewport and scale it in four or five times so that we can view
our Pixar well. That's why we need to make sure that even if we're
looking at, like, let's say, 12 80 by 720 pixels, if we zoom in by
a factor of five, then our camera size is actually scaled down to those new
values based on the Zoom. So that's why we need to
make that adjustment. So next up, we have
the VR camera rect, which is a rect two D
type or rect two, rather. And this is a
rectangular shape that represents the bounds
of our camera. It's based on the size. So for the math here,
we're constructing a new rectangle by doing Rectangle two
and then parentheses. And our first parameter is camera center minus camera size. And then we multiply
that by 0.5. And then for the
second dimension, we just have camera size. So that gives us the
appropriate rectangle box that represents the
camera on screen. Next, we want to randomly
select an edge that we're going to be using to
spawn the enemies from. So our camera is a box, so it has four edges, and we can basically spawn
our enemies up above, down below, to the
left or to the right. So we just want to pick
one of those edges, and because we want
to even distribution, we'll just do it randomly.
And then there's no waiting. So, in essence, it
should just mean that they're coming from all
directions pretty consistently. So if we say var edge, and we're going to have
that be an integer value, and it's going to be equal
to Rand I for integer. We're going to give
it percent four? The percent sign here
means remainder, so it's the remainder
of dividing it by four, which means it'll either
be zero, one, two, or three, and those
represent our four edges. Okay, then we need an offset. How far away from the
edge do we want to actually spawn our enemy? We don't want them
to spawn directly on the edge because then it would be on screen visible when it's instancing
into the game world, and that would look
kind of funky. So let's say var offset, and that's going to be
a float I believe here, equal to span underscore
margin plus random float. Okay, so we're basically generating a random float
here and we're going to multiply that by
the span extra range. So random F can be 0-1 0.0. So a value of zero times our span
extra range is going to give us an extra spawn
distance of zero. And if it's 1.0, it'll give 100 because that's what
we have it set to the span extra
range property. And if we have
something in between, then we're going to get
some value of pixels 0-100. In addition to our span margin, giving us the total offset. Okay, so we'll say
var spa position, and that's going to be
assigned to a vector 20 here. Zero all caps the
constant, rather. Okay. Add a few more lines. And we'll say if the edge is
less than or equal to one, then we are treating that
as the top or bottom. Then let's take our edge and we'll do a match
statement with it so we can calculate the son position based on which edge
we're actually using. So let's say match edge, and default, we'll
push an error handled. Edge number, percent
s percent edge. So having that is just kind
of helpful if we forget to, for some reason, put
in one of the cases. Now we can actually match
the edge integer to a Enum value if we want to be a little bit more verbally descriptive here. So if we go up here to the top, I'm going to declare an
enum, say enum edge, and we're going to
have top as zero, bottom as one, left as
two, and right as three. Okay, so now, if we go down here and we match the
edge to those values, we could say edge dot
top, if I'm not wrong. Hold on a minute. Seems like there's a
bit of an issue here. Still, I need to put
equal sign here for the camera rect. Okay,
so that's going to work. Now we can come down
here to where we were doing the Enum matching, and we want to actually
put in the code here. So the spawn position
X for top is going to be equal to a random
float from a range. So we say Rnf underscore range, and we're going from camera rectangle dot position dot X to camera dot position dot X
plus camera dot size dot X. So for a rectangle tutti, the position X is always
going to be up here. So if you add the
size X to that, then you would get over here
on the right hand side. So if we're going to
be going for the top, we want to position somewhere
between here and here. So that's why we take
the random value of the position and then the
position plus the size. So that allows us to get somewhere between
this line right here. And then we need to do the
Y position, of course. So spawn position Y is going
to be equal to camera Rect. Dot position, dot, Y. Minus the offset. So we're subtracting the offset because when you
subtract a Y value, you're actually going
up on the screen. So we have the Y position, which is the top
of our rectangle. So, let me pull up
paint right here. The Y position is
up here, right? More specifically, it
would be the Y value that gets us to the 00
point on the rectangle. Then you take the offset, which is going to be some
random value up here. So our enemy is going to be kind of spawning in this area, roughly speaking, and
this is based on whatever the Y offset was randomly
generated to be. So we have our ten pixel margin, and then we have
110 pixels up here as our extra plus margin area, and then our enemies can spawn somewhere inside of
here for the top value. Okay, hopefully,
that makes sense. And then we just kind
of do the same thing for all the other edges. So edge dot bottom. Is going to be precisely
this line up here. And then the span position Y is going to be a
little different. So we want spawn
position dot Y equals camera rect dot position dot Y plus the camera rect
dot size dot Y. So that gets us the
bottom of the rectangle. Because now we're
adding the Y size, which pulls us down
to this bottom area. And then we just add
the offset to that. So plus offset and
note that we're adding the offset because we
want to go further down underneath the camera box, but on the top, we
subtract the offset. That's very important. Now we just do the same thing with
the left and the right. So edge dot left, which is our two enum. And that's going to be spawn
position dot X equals, camera rect dot position dot y minus the offset because we're going to the left on our box, so we want to
offset to the left. That's it for the
spa position X, and it's spa POS, not spa
position. I abbreviated it. So spa post Y is now
going to be equal to, and we take a random F range between the or camera rectangle, dot position dot Y. So that's going to be
the top of our box. And then the other side of the random is going to be
the bottom of our box. So that's going to be
camera wrecked dot position dot Y plus camera
wreck dot size dot Y. So when we add the size, once again, we're going
down to the bottom. So our span position vertically for left and right is somewhere
on this line right here. And then finally, edge
dot right all caps. It's going to have the
span position X of camera Rect dot position dot X plus camera we size
dot X plus the offset. So same thing. We
have a box over here and a box over here, and we want to
offset to the right or offset to the left if
we're spawning left or right. Let me double check
that. Okay, position X plus size dot X plus offset. Okay, that looks correct. Of course, we'll
test it in game. And then the span position Y should be the same
because we're still dealing with that vertical picking a random spot
between here and here and here and here for a for the left and
right generation. Okay, so that should basically
be our span position code. And then we want to
return the span position. So this function
is fine as it is. It's not too bad. If it
got a little bit bigger, I probably would just take
this out to another function. Honestly, we could
because I felt the need to put this
comment in the code. So if I wanted to, we
could say function, underscore, calculate
final spawn, which will return
in a vector two. And then we put that here instead of defaulting it to vector two and putting
all this down here. And we cut this to
our new functions, so we're refractoring it. And then we're
going to obviously require some parameters to go into this function
for it to work. So first off, we
need, let's say, camera rect, which
is a rectangle two, and then we need camera size, which is a vector two Control S. And then we want to return the
son position inside of here. So the var spawn position
is a type of vector two. We go down to the bottom and we return the spa
position out of here. Okay? Up here, we want to
pass in those parameters, so camera rect and camera size. And then we need this over here. Finally, we also
need the offset. So pass in the offset up here. Okay. And then we need
offset as a parameter. So offset, and I think that
was a vector two as well. Okay? So Control S actually
was it just a flow? Oh, it was a float. Okay.
So a one directional float. And once you save that one, most of these errors
should go away. So we need to match on the edge, and then we need to return. We could also put in
that edge parameter. So edge, which is
going to be of type. We could just actually
use the enum here. It should work fine
because integer can just auto assigned to
an enum as far as I know. So if we go up here and then
we pass in the edge, Okay, that's going to come
in here and it's going to work as a enum, which is kind of what we expect to match it
with down here. So to go through
enums a little bit, an Enum is basically a
list of named values that correspond with integer
position inside of this Enum. So the zero because this is the first is the zero position. So it's like the first
item in an array. You could almost think
of it like that. And then bottom is one, left
is two and right is three. I think some languages
allow you to assign custom integer
values to a Eenum. I don't know if
you can do that in GDcrip but that's
basically the idea. If you don't mess with it,
then zero is just top. One is just bottom, two is just left, and
three is just right. But what this gives
you the advantage of doing is that when you
create a function like this, instead of saying
zero, one, two, three, which is very obscure for what that actually
represents, what is a zero? You can just replace
that with edge dot top, which is the value of zero, but it's much clearer
to someone reading the code what that is actually
supposed to represent. Okay, let's see. So we have our Get spawn
position off camera, and we had that here
in random span. So now we actually need to set the position for the instance. So instance global position is going to be equal
to the spawn position. That part's easy. Let's hit Play and see if it
actually works. Cause that was quite
a bit of code. Okay, so our characters aren't
spawning on the screen. I see them coming
from off the screen. Let's see, or any coming from
the bottom and the right. I don't see any coming
from the top or the left, and that kind of concerns
me a little bit. So I'll see if I can find them. Okay. And I think what we
should do for testing that. I'm going to close the game
right now is go down here and we were missing them
on the top and the left. So let's add a breakpoint
here and a breakpoint here. So if, in fact,
characters are trying to spawn from the top and the
left, then those should hit. And then we can check the values that they're actually hitting. So I forgot to stop in the game, so it is going to just
automatically hit this breakpoint. Hit Play and see if we can
get one from the left. Okay, there I did see one
coming from the top, actually. Okay, and one is coming
from the left, supposedly. So it seems to be
kind of going, right. Maybe if I bump up the
spawn rate a bunch, it'll be easier to see for sure. So let's go to the world. Do we have the spanner system? I'm just going to take this right here. I'll
remember the value. So it was 0.66 and 1.5, and I'm just going to drop
this way down so we can get some very fast
spawning for right now. Let's hit Play, and
I'm going to test. So we'll just see
a bunch of guys spawning very fast or we should. And yeah, okay, that
looks mostly correct. Can we get a guy from the
bottom left over here? 'Cause I want to see
that occur, too. Okay, yeah, yeah,
finally, finally. We got a couple guys over there. Okay, so I think it
is working proper. If there's an issue, it's possible that the math was not double check
to be correct, but I think it's going
the way it's supposed to. If not, it's a
little hard to tell. But yeah, I do see
them coming from all directions. So I
think we're good here. This math would
be something very easy to get wrong, though, so I'll put it on screen once again if you need
to double check. So we have our spawn position. We're getting that from G
spawn position off camera. That's our function right here. So we get the camera center from the camera global position, the camera size, is
from the viewport size, and then you divide that
by the camera zoom. The camera rectangle
is constructed from the cameras center minus
the camera size times 0.5, and then the Y value for
that is just camera size. The edge is random integer, and you get the
remainder of four, which will give you
zero through three. The offset is span margin plus random F times
the span extra range. So this is somewhere 0-1
point with decimal points, and then we add the span extra range as some
percentage of that. Calculate the final span using all those properties we just
set up the camera rect, the camera size, the
offset, and the edge. So we take that into here. We match on the edge. So zero, one, two, and three. And then for the X value
on top and bottom, we do a random range
between the left side and the right side. Using that math. For the Y position, we take the Y position at the top and then subtract the offset
or for the bottom, we add the size to
get to the bottom, and then we add the offset. And then for left and right, we do the same thing in reverse. So for X, we do the left
side minus the offset, giving us further to the left. And then for the
right side, we add the size to the position
X to get the right side, and then we add the offset to go further to the
right from that. Then for the span position Y, we do a random range
between the position, which is the zero Y position, and then the position plus size, which gives us the bottom
corner position in the Y axis. And so that'll make it so that the left and right
spa is somewhere between the top of our screen vertically and the bottom
of our screen vertically. But the position is
offset off the screen, so we won't actually
see them spawning. And we returned
the span position so we can use that in
this other function, and that's pretty much
going to wrap that up.
25. Random Enemy Spawning with Weight & Time Conditions: Our offscreen spawning
is working, essentially. There's a couple of things
I want to clean up, and then we'll
proceed with adding an skeletal enemy so that we can randomly select
which enemy we're going to spawn based
on a waiting system. So let's close the game out. And first off, we can
see this label here, the floating combat
text instance. We don't need that
anymore because we're done editing
the animation. So I'm going to
delete that node. And then on the spawning system, for the demonstration, I took this way lower
than it was before. So over here on the first point, I wanted to bring that
back to the original, which was like, I think it was 0.66 seconds originally,
and then 1.5. I'm going to make that
0 seconds and then 1.5 seconds as the
spawning period. So we can actually just go
down here and type in 1.50. Hit Enter, if you want to
be precise with the setup, and you can see that
the point while selected shows 0 seconds, and then 1.5 seconds
as the spawn time. 0 seconds is the elapsed
time here, if you remember. I think these four work enemies, we could just remove
from the scene. So I'm going to delete them. So I'm going to select
them, hit hit Okay. You can hit play just to test that everything
still works, and we should get the enemies spawning off screen
coming towards us. We don't have you can
hit play if you want to double check and just make sure that the enemies
are still spawning. We just don't need
those initial enemies because the enemies are going to come after the game starts. We do want to give the
player a couple seconds to, you know, acquaint themselves with the controls and so forth. Okay, so for creating
the skeleton enemy, let's first clone the orc in me. So I'm going to search
for orc and the project. We go down to orc dot TCN. I'm going to right click
and clone it, so duplicate. I'll call it skeleton dot TCN. And then let's search
the project for that skeleton scene
we just created. Double click it. And
then we'll zoom in here. So I'm going to take
the sprite here and change it to the
skeleton sprite. So let's quick
load on the right, and we're going
to look for idol, and there should be
one for the skeleton. We may need to zoom in a bit. There should be one here
for a skeleton enemy. And then on the top
right, we can quick load, and you may have to zoom in
a little bit to find it, but type idle and we'll look for the skeleton sprite sheet. It may be a little
hard to see, but I'm going to select
this one right here. If you can't find it,
feel free to just navigate the file
system and find it. It's probably in the
skeleton base folder. Okay, now let's rename the
root node to skeleton. And we're going
to need to set up the animation player to make
sure that it is playing the animations of idle skeleton and then Ron skeleton and
skeleton death as well. So let's go to the
animation player. And then in the bottom
right, we basically need to change the texture
for each of these. So the reset texture will go to the top right and then quick load the Idle sprite
sheet we just selected. Right now it's
already pre selected, so we can select that. And you might want
to play and make sure it switches on reset to the skeleton and then change the texture for the
other animations as well. So Idle we're going to click on texture, go
to the top right. Quick load, choose the sprite
sheet for the third time. Okay, and hit Play. And then make sure that's
playing correctly. Looks good. Let's go to
the death animation. Select the texture here. And because there's a lot
of frames in the top right, it's a little hard to
actually see what's going on, even harder than
the idle animation. So I think it would be easier
to just navigate to the art and then find pixel
crawler free pack, entity, mobs, skeleton crew, base death, and then just drag
the death sheet into the top right like that. Okay, hit play and make sure that it is playing correctly. So it seems like there's eight
frames of animation here. So we do need to zoom in
a bit like this and make sure that H frames will set
in the top right to eight. Okay, then hit Play. Yep.
Okay, that looks correct. And now we need to add in the seventh and
eighth keyframes. So right click on frame at 0.6
seconds and do insert key, click on it, go
to the top right, change it to six, do
the same thing at 0.7. You can't snap there because we need to extend the animation. So make the animation
0.8 seconds long. Click on 0.7, S that
keyframe and change it to seven for the eighth position is what we want.
Okay, let's hit Play. Play one more time.
There's our animation. Now move the death
state finished to the end at 0.8
seconds like that, and there's our death animation. Okay, so we want
to run. Should be quicker. Click on texture. And then in the bottom left, where it says skeleton base, choose the run folder, the run sheet over to the top right. Drop that in. Hit play, and we can see it's
playing our run animation. It has the same age frames, so that's all we need to change. Okay, that's the basics
of our skeleton. We can go to the
stat controller. Aside from assigning stats, it's like run speed
and HP and so on for our skeleton and
a custom values for our orcs and CD that
is the basic setup there for our two characters. So let's go back to the world, and then we're going to have
the spawner system, right? So we have our random definitions
over here on the right. We want to add another
random definition, and then we're going
to create a new one. So new spawning definition. Let's say that the weight
for the skeleton is 0.5 so we'll get half as many
skeletons as we do orcs. And let's quick load the
skeleton scene, okay? And then we'll just check
the sponsor system script. So open that up, and we'll see
the random selection here. So right now it is not generating the
random selection scene. It's just selecting the
first one in the array. So we either need
a bit of code or a function in order to pick which scene we're
actually going to instance. So let's zoom in,
and I'm going to say var scene is a packed scene, and we're going
to want to select the random spawn from a list of randomly
spanable definitions. So we'll say random
definitions, save that. And then we need to create this select random
spawn function. So I will do function, Control V to paste it in. And then we're going to
say P definitions is an array of let's see what do they call
it spawning definition. And we're going to return
a single packed scene. So if P definitions is empty, then we can return null because there's nothing
to select from. Otherwise, let's
calculate from all of the total weights and then
randomly generate a value between zero and
the total weight to determine which scene
we're actually selecting. So that's random selection, but it's weighted
random selection. So let's say if our total weight is a float, starts at 0.0. And then for definition
or D and P definitions, we're going to take
the total weight, and we're going to increase
it by the definition weight. Okay? So we do that
for every definition. Now we have the total weight, and we need to generate a value between zero and
the total weight. So far, let's say random
value is a float. And we're going to take random F. So this is zero
to one point oh, all inclusive,
including decimal, and it's going to be
times the total weight. So that'll give
us a random value between zero and
the total weight. And then let's say V
checked weight flow is 0.0. So this is how much weight value we've tried to pass before
hitting the random value. Once we hit the random value, we'll return whichever scene or whichever definition
sne we're currently on. So for definition
in P definitions, we're going to say that
the checked weight plus equals definition
dot weight. And if the checked weight
is above the random value, then we'll return the scene. So return definition dot CM. Okay. And then if
we get to the end and we still haven't
hit anything, that's a issue, so
we can push a error. Okay, so here I'm
pushing an error. Was not able to
find a definition at weight value percents, where checked weight
total was percentS. And then I'm replacing
those percents with the random value and
the checked weight. So this kind of shows what we were checking,
where we reached, and it's indicating that
we didn't actually find a checked that was
above the random value, which should always
happen because the random value can only
go up to the total weight. So this should not occur, but if it does, we have
an error message. And then at the end
here, we can just say return null because it wasn't able to find
a proper definition. So this makes the most sense. It'll probably immediately
break something indicating that
something needs to be fixed in the math
at this function. Once again, I want to emphasize
this should never happen. If our math is correct. Okay, so we select the random spawn from
our list of definitions, and that handles the weight
and the random calculations. So if I run the game
now as things are, then we should be getting half as many
skeletons as we get orcs. But let's see. Do we
get both of them? Because we should
get both of them. So we have a few orcs spawning. Okay, so it actually
turns out the code was working perfectly fine
for select random spawn. I just forgot to use the scene. So we want to take
the scene here. And instead of random
definition zero instantiate, we just do sent instantiate. Okay, now we can go back into the random spawn and remove
that Prince debug message. Okay? And if we play,
it should work fine. It should actually
use the one we randomly selected rather than always the first
one in the array. So we should get half
as many skeletons as we get orcs, but
there's our skeleton. And as time goes on, you'll see a bunch more orcs
then you'll see skeletons because of the waiting.
Okay, so that's good. And that's basically
the idea there. Okay, so last thing
for this part, you may want to
delay how long until a enemy definition can
actually spawn into the game. For instance, at the start,
maybe you only have orcs, but then after a while,
you can add in skeletons. So we already have
the elapsed time. So if we say a starting
and ending time in terms of elapsed time when a
definition can be spawned, then we can control
when the orcs can spawn and when the
skeletons can spawn. So if I go into
spawning definition, we can add an export var here
or starting time a float, and this can be 0.0 by default. And then the ending time it'll
also default that to 0.0. So what we want to do
with these two is start creating like a can
be spawned method. So we could say
function can be spawn, which of course is going to
return a true false Boolean. And for this, we
actually want to pass in the elapsed time because
that's part of the condition. We have to check what time
it is in the game or time since the spawner system has actually been created, rather. And then we'll use
that to check if the elapsed time is within the starting time
or the ending time. So if P elapsed time is
before the staring time, then we'll return false. If ending time does
not equal 0.0, or rather, if ending time
is greater than 0.0, I think that's a little
bit easier to read. Then we want to check if the ending time has
actually been passed. So if the ending time is
zero, we're just ignoring it. So and P elapse time is
above the ending time, then we'll return false. Otherwise, we've passed all the conditions so we
can return true. So that's basically
all we need to do for a time based spawner, and we just check if
it can be spawned. And if it's not
able to be spawned, we remove it from the list that we consider for
random selection. You would do something
very similar. If you also want to, let's say, have a maximum number
that they can be spawned, you would just pass
in the number that has been spawned and
check if that is above or equal to
the maximum number that the spawning
definition would allow. That would be helpful
for something like a boss that can only spawn once. So you can just check, Oh, it can spawn one time. How
many times does it spawn? Is it one or greater
than don't spawn it. We'll see if we
come back to that, but this should be all
we need for writing now. So now, back in the
spawner system, we want to basically filter spawnb from the
random definitions. So I'm going to
create a function filter spawnb which
is going to take a P definitions array
of definitions, and that's weapon. That's
spawning definition. And then we will return
our filtered definitions. So return an array of
spawning definition. So we create our
filtered list here. Var filtered array of
spawning definitions, and that's going to be
equal to a empty array. And then for DF
and P definitions, we'll check if DEF
can be spawned. I guess we need the
elapsed time in here, but that's a local variable. So we can just pass it
in directly to this. So elapsed time, as in we
don't need to put it as a parameter to this
function because it's part of the state of the object, which is the spawning system. So if it can be spawned, then we'll add it to
the filtered list. So filtered dot pen definition. And then at the
end, we just return filtered so here in random span, we do var spanable and that's going to be
equal to filter spanable, which is going to take our random definitions
as the parameter. And then instead of
select random spawn from random definitions, we select random spawn from our filtered spanable variable. Okay, now that gives us the ability in the
spawner system, if we go over to the
spawning definitions, we can say starting
time, ending time. Let's just say orcs
are always active. But then for skeleton spawning, we can't spawn skeletons
until 15 seconds, but then once we can
spawn skeletons, we have a weight of two,
so we end up seeing a lot of skeletons as soon
as they're able to spawn. Then we can play. And if
all the math checks out, we should see nothing
but orcs initially. But then after 15 seconds, might be helpful to have, end game time or something to see
we'll see if we need that. Then after 15 seconds, we can see the skeletons are going to start
spawning because they are now allowed to be spanable because the starting time
of 15 seconds has elapsed. Okay, so now you can see the
skeletons start to come in. So we have our second
wave of enemies, and we could just make
the skeletons tougher. So the difficulty
progresses as game goes on because we add
in tougher enemies. And, of course, in
the spawning system, we increase the spawn
rate as well, like that. So you can kind of see
how that would work. That's the nature of
basically controlling the random spawning of
our spawning system. That's most of what we
actually need here. Good job keeping up
with everything so far. Okay, so we might
do a couple tweaks here and there later
on if we need, but for the most part,
that's our spawner system. So now we should
start working on fleshing out the combat
a little bit more and make our enemies actually
able to damage our players would be a good next video.
So let's go from there.
26. Player Hurtbox and Enemy Hitbox Setup: Part of our basic combat
mechanics is going to be setting up the ability for the
enemies to hit our player, which is going to
involve setting up a hit box for the enemies when
they run into the player, and we need to ensure that the player has a
Hurt box, as well. And we'll probably
add an invincibility time just to make sure we don't get too many hits when we get mobbed by an enemy
kind of like that. Okay, so on our player, we'll create our Hurt Box nodes. So I'll right click
on the player route, add a child node, and let's look for HRT Box
two D. Enter, add it in. We'll assign the
stat controller, which we haven't added
to our player yet, so we can right click on player, add a child node,
stat controller. Okay. And then we want
to do some assignments. So object for the SAT controller is going to be the player root. And then in HRT box, we want to assign
the stat controller. So our HRT box, you can
see by the warning sign, we also need a collision shape. So let's right click on it
and give it a child node. Collision shape two D. And
then in the top right, we'll make this probably
like a circle shape. Okay, so we get this
circle shape appearing. We want to shrink this
down to the size we want to be able to take
damage for our players. So depending on how
we want to do it, we might make it
slightly smaller or slightly bigger than
our collision shape. I'll go big for now, and then we can
shrink it if we need to for game balance or
the feel of things. So that already
gives us the ability to take damage on the player. Now we need to give the enemies the ability to deal
damage to the player. So on our orc scene, let's right click on Orc, add a child node,
hit box two D. Okay. And then this is going to
need a collision shape. So right click,
add a child node, collision shape two D, and
then in the top right, we'll make it a circle shape, and then we shrink
it to where we want the orc to be
able to hit targets. So I'll make it
slightly bigger than the main collision shape
for the actual collision. Then in our RT Box two D,
if we check the script, we can see that we
get the hit signal from the hit box
hitting a RT box. So to deal damage, we actually
need another script to deal damage on the signal
hit and being emitted. Before that, though, let's add the collision layers for our
hit box and our hurt boxes. So the hit box two D on the collision layer is going to be masking for
the player layer. So mask of layer two because
that's the player layer. For the layers which
this hip box exists on, we can just turn off layer one, or we could set layer three
for enemy if we want. But I'm probably
going to turn off monitorable because this
is a one way hip box. It only needs to detect. If it finds a hip box, it doesn't need
to be monitorable by other nodes inside of the
game, at least currently. So let's go to
play and I'm going to do the reverse for her
Box two D. Over here, we're going to turn
off monitoring, and we're going to
take the collision. We're going to make
the layer as player. That's very important layer two here, we can turn off the mask. Then inside of
here, I'm going to right click under Hit Box two D, and I'm going to
add in a new node, add child node. This
will be a node two D. And let's rename this to be something like collision attack. So when the orc collides with another enemy using
this hit Box two D, it's going to do all damage
as a collision damage. A lot of classic games
have this sort of thing where an enemy just runs into
you and you take damage. So we'll have to right click here and create a new script. So let's put collision attack in enemies. I think
that's fitting. Create. I'll rename it Class Name collision
attack up here at the top, Let's export a reference
to the Hit box. So Hit Box is of type Hit Box
two D. And then on ready, function under score
ready, returns void. We're going to take Hit box, and we're going to connect
to the hit signal. So it dot hit, dot connect, and we're
going to connect on hit. So now we need, let's say, the at export var damage here, which is going to be an integer. We can default it to ten. So let's get the
callback for the hit with on hit function
underscore on hit. Now you need to right
click on Hit and check the signal parameters. So we have the Hurt
box two D here. Okay, so that'll get us
access to the stats as well. So inside of collision
attack, let's jump back. So we have the P Hurt box, which is a type Hurt Box two D. This will return void, and then we're going
to hit the player. Okay, so on hit P
Hurt Box two D, we want to take the P HRT box, and we want to
tridal damage to it, and we're going to
pass on the damage. And then that's pretty much it there because we already
wrote that into the hurt box. So the Hurt box already
handles reporting. I already handles
setting the HP, which this will emit the signal for the HP changing
if we need that. And it does the calculation. It also does the guarding to make sure that we
can hit the target. So it just makes sense that
we use this logic where. As long as we have
collision attack, we assign the hit box up
here and we set our damage. We can see if that will work. So I'll just try running
into an enemy and see if our player gets
the damage above it. Okay. I just need to be
sure Oh, okay, yeah. There we go. Our player
got hit for ten. We know it's the player
and not the enemy because the spear
currently does 100. Now, currently, there
seems to be one issue, which is that enemy
that is already dead seems to still be
able to hit the player. It already took 100. I
went to Death State, and the collision attack
was still enabled. So we could handle
that a few ways. One would be that we just check the stat
controller and make sure that the stack controller on the orc the HP is above zero or
the orc is still alive. Otherwise, we just completely
ignore this attack. That would be one decent way. Another way would be we put
it into the death state, and then we disable
all the hit boxes as soon as our orc
is considered dead. I think that's maybe too much responsibility
for the death state. So I think it belongs
a little better here in the collision attack. So what we'll do is we'll export a reference to
our stat controller, export V stats stat controller. So what we'll say on it is, if not stats dot Live, then
we're going to return. Otherwise, we'll deal damage. So basically, while
the character is not alive, it cannot attack, but if it is alive,
then we'll consider the collision damage valid if
we do get that connection. So now if we go back
into play here, we'll be able to walk
at an enemy that had just been defeated and
not take damage from it. Okay, so I need to
time this right. Okay, I run into it and
we see that we hit this, but I forgot to set the stats. So it might be a good
time for an assert. So assert stats. So the stat needs
to be set so that we check if the
character is alive. And then this will
basically already require us to have the
hit box soda error, if we forget to set
that in the inspector. Okay, so close there.
Click on Collision attack. Let's assign the stats. Let's go into play. We'll do this one more time. Find an orc. And you can see
that we don't take damage if the orc had already been
defeated, one more time. Yep, seems to be
working just fine.
27. Player Invincibility Timer and Custom Stat Definitions: Okay, so last thing
for this video, we want to set up an
vulnerability period, specifically for our player. So in Stat Controller, we can go in here. So in SC controller, we can create an invulnerability timer. So let's say down here V Invincible time and we
can default this to 0.0. It'll be type float. So it's the time
after getting hit that the object
cannot be damaged. Okay, so with our invincible
time defaulted to zero, that's going to be
fine for our enemies. However, for a four hour player, we do want to have
our custom value for the Invincible time. And we probably also
want to be able to customize things like the HP, speed, Max HP, et cetera. So we're really getting
to the point where we need to set up some kind
of stats definition, where we can define what the starting stats for each
character is going to be. So if we could create
a new script for that, which is going to look a lot
like our weapons definition, where we're just
defining properties about the character
and its stat block. So in the character's
photo, alright, click Create a new script, and we'll call it Stat
underscore definition. This will extend from resource
and click Create, okay? And then search for
that script we just created Stat definition,
double click it. And we'll come up
here to the top. So class name stat definition. So this will define default
stats for a object type. So the stats we
want is going to be basically mirroring what we
have on the stats controller. So let's go into
stat controller, and I'll just actually
copy these over for right now into our
stat definition. I'm going to remove
all this setter code. Because we just want
to purely define export editable properties that basically don't run
any extra code. It's just pure data that is saved within the
GIDoEditor essentially. So we don't need alive, and we will want a default for the HP and fencibT
HP and MAX HP. So let's export all of these at export for MaxHP
at export for HP. Over here, we'll do
at export for speed and at export for
invincibility time. You might also want to
give them some comments. So MaxHP maximum
health for the object, HP, starting health for
the current object. Okay, and that will be that. So now we need a way for our stat definition to be loaded onto the stat controller. You could either put a load function in
the stat definition, which kind of turns
the resource into being a little bit more
than just peer data. Well, we could put it
on the stat controller and some kind of
initialized stats method where it knows about
the stat definition type, and then it can
load all the values from an object of that type. So I'm going to do it that way. And that's going to mean
we're going to need to create a new function
down here at the bottom. Since the stat controller
is going to need to call initialize on the
stat definition anyway, because this is the
node the node will handle when it is ready
to set itself up. So either way,
there would be some minor dependence on
the stat definition. So I'll create the
function, set up stats, and this will take
a P definition, which is a stat definition. Well, return type void and
let me add some lines here. So in setup stats,
we'll just mirror the definition onto this node. Max HP is going to be equal
to p definition MxHP. HP is going to be equal
to p definition dot HP. Let's see what else speed
is going to be equal to p definition dot speed, and invincible time is
going to be equal to P definition dot Invincible T. So now we need to
just do that on ready. So under invincible time, we'll add function
underscore ready, and we'll say set up stats
with our definition. So we need to export a definition on the node so
that we can actually do that. So up at the top, we'll do Export VR Stat definition
is a type stat definition. Okay. And then we just use
this as soon as we ready. So set up stats with
the stat definition. That's going to
load all the stats into our runtime value. And this lets us
customize the values for the base stats of our characters using something we can
set up in the editor. So if we click on
Stat controller, we go to the top right. I can create a new stat
definition here for our Oc. And let's say that the Mx
HP should be 40 and 40, and the speed is 25. So we save this now to the
folder where we have the orcs. Click on Stat
definition, save as, and we can put that in
the enemy's folder. So Ok Stats. Sounds like a perfect
name there. Save it. Okay, and that's how
Ok Stat definition. So when it loads, it's
going to load up with these values as soon as
the character is ready. Okay, so that's basically
that for the orc. Now, I also want to
copy this hit box setup over to the skeleton, as well. So I'm going to Control C, go over to skeleton,
Control V, paste it in. I might take the
collision attack, and I'm just going to boost
the damage to, let's say 25. Let's assign the stats
to the stat controller, and the collision shape
being the same works fine. Uses the same collision layers. It's stone in me trying
to hit the player. So everything else is
set up and just fine. And then we need to set
up the stat controller. So stat controller here,
go to the top or right. And create a new
stat definition. Let's say the skeleton has more like let's say more
like 50 health, 50 Health, a speed
of 40 is fine. So we'll right click Save As, and we'll say skeleton
stats dot TRS, save. Now we need to do
that for the player. Go to the player, click
on the stat Controller, go to the top right, do
a new stat definition. Now, here I want us at
the invincible time. Let's say a character
is invincible for 0.2 seconds after being hit. We'll give the
character a base speed of let's say 100 and
the Max HP has 100. I think I'll leave
that as the start. So right click on
stat definition. Save as, and we'll save
this in the Player folder. Playerstt dot TRS, hit Save. And now the characters should
each have different stats. Last thing we need to do is actually use the
invincibility timer. So on St Controller, we need to trigger Invincible
with a method call. So let's create a function
set Invincible here, which will return type void. And then we'll have
a local variable for if it's invincible or not. So variable invincible
is a boolean. This is going to
default to false. If true, the object
cannot take damage. Okay, so for can be hit, we want to say
that the character is alive and not invincible. So I'm using this with the keywords rather
than the logic symbol. I guess it makes it a little bit easier to read, in a sense. So if the character is
alive and not invincible, then the character can be hit. And we want to
invincible equals true, but we also want to
start a timer here. And we'll say that this
is equal to a new timer, equals timer dot new. And then we add that as
a child to this object, invincibility timer, and we'll
connect to its callback. So invincibility timer
dot timeout dot connect on Invincible Timeout. So Control see this and
paste it down here. Function on
invincibility timeout. This overture and void. And we'll say that
invincible equals false. Okay, so now we need
to start the timer. Invincibility time or start, and we'll say that the time
is the invincible time. That should handle the
logic of whether we can or can't hit because the character
is invincible or not. Okay, so for set invincible, we could check when HP is
set that if the HP dropped, then we want to trigger
the invincibility. But I think what might make
our intent clear is to create a take damage function in this definition, as well. So we could do something
like function, take damage, and let's
pass a a P damage t here. We ta and void, and
this is going to take HP and set it to P damage. And then we want to set the
character to invincible. We could even say if invincible
time is greater than 0.0, then we'll set invincible. On this object. And the HP
is actually going to minus equal the damage here because we want to take its current
value and update it. So this will still call the live equals HP is greater
than zero here. If you want it, you
could even make get live just a function
that checks this, which would be fine as well, rather than doing
this previous setup. But we already had that. It works fine.
We'll go with that. So now, whenever we deal damage, we really want to call take damage on the
stack controller. So let's find out where
we're dealing damage, like the projectile
might have it. Okay, so here we
can see everything goes back to the Rt box. So let's right click
and jump into this. And instead of setting
the HP directly, we're going to say stat
controller dot take damage, and we pass in the P damage. So we can cut away this line, and we actually don't need
to calculate this because that calculation
is automatically handled in the stat controller. So this kind of more
illustrates our point that if our stat
controller takes damage, it might be doing other
things like setting invincible on the character because our character
took damage. It's not even just that
we changed the HP value. It's specifically that we took damage from a damaged source. Okay, so let's hit Play and
see if it all comes together. We'll have to take damage
from a few enemies, and then we'll want to make sure that we can actually only take damage once every
quarter of a second. So if I shoot over here, we need a few more
enemies for this to work. It might even be easier if
I disable the projectiles. Okay, so I'm gonna
run into these guys. I should only see
damage occur once. Okay, 25. I think it is working. If I walk through all of these. I'm only seeing damage
come up once at a time. To VLC for sure, though, I need to turn off
the projectile. So I'm going to
temporarily delete the spear weapon from the scene over here. You don't
need to do that. I'm just demonstrating. Okay, so then I'm just going to go walk on to the enemies. Okay, so we only
get damaged once. Now, this collision
hit thing is only incurring on entering the area. So if we stand on
top of the enemy, we still only get hit once. Okay? You can see even though there's a bunch
of guys hitting us, we only get hit once every quarter of a
second or so, like that. And our characters already done, so we can't get hit more.
28. Implementing Periodic Damage for Enemy Hitboxes: Fix the issue where
HIP box can only damage once after another area. The HipxTD enters this hit box, but not when it
persists inside of it. We can set a timer inside
of our hip box to D to keep checking if the HIP box
is still inside of the area, and then we can keep dealing
damage to it periodically. Okay, so once we emit the hit, I can also add it to the
tract areas and the script. So let's say var targets, which is going to be an array of Hurt box two D, which
is currently nothing. And we'll say that
after hitting, we do targets
append, the P area. Okay, now we need to connect
to the area left signal. So area exited dot connect. And we're going to use the
callback on area exited. Okay. So function
on area exited. We get the P area. So we want to check that
if it's a HIT box two D. So the areas of
type area two D. Okay, so we check if P area is Hurt box two D, then
we'll continue. Now we'll check if
targets dot has P area, then we're going to
erase it from targets. Targetst erase the P area. Okay, so now the targets are
only going to exist while the target Hurt box persists
inside of our hit box area. We can check every period of time whether we need to
damage the enemy again. Okay, so we could do that
with something like, let's say we have
optional timer, so at Export var timer timer, and then we'll connect to
the time or timeout signal. So timer dot timeout dot Knect and we'll say on timer Timeout. Okay, so we'll put that
call back down here, function on time or TO. This will return void, and we
will try to hit that area. So on the time or timeout
for Target and targets we do hit dot a Mt Target. So if the objects are still in the area after they've
done their initial hit, then after every time or period,
they will get hit again. So we go to Ok and let's look
at our hit box two D. Okay, and maybe we want to
be a little bit more clear about what
this timer is for. So let's say this is
actually a damage timer, and I'll update the
property in the ready. Okay, so now for
our damage timer, we want to right click on
hipbox two D, add a timer. And we'll say for this that
we do like 0.25 seconds. This will be autosrt, and then we just need to
reference that in the Hip Box two D. I'll rename it to be damage timer so that the
names match up as well. So click on Hit Box two. Assign the damaged timer. Now that's going
to connect to the signal inside of the script. And for each object or target that is in the area
when the timer elapses, it's going to trigger that. And because this
is not one shot, it's automatically
going to loop. So every 0.25 seconds, it's going to keep
doing this and it's going to check the targets
every single time. So that's probably
good there. We just need the skeleton to
have the same thing. I'm going to control C copy this timer over to the skeleton. And we'll Control V paste
it onto the Hip box two D. Let's assign it in the Hit
box two D and the top right, assign the damaged
timer, save and run. Okay, and we can see that on
some of the hipbox two Ds specifically like the projectile the speed projectile,
it doesn't have this. So we need to be
clear that we say, If damaged timer, then we
connect to its signal. So this is optional,
and I wrote a comment there to be clear about
that optional timer to periodically emit hit signals
on the targets inside the hip box two D area after
a set time has elapsed. Okay, let's run again. And we'll just see if an orc can actually damage us twice. Well, that's gonna be a little hard with
the spears there. Let's disable the
spear, as well. So disable the spear weapon
or remove it temporarily. Let's go back into the scene. Okay, and let's find an orc
to damage us a few times. Okay, so here we have our k. Let's see how many times
we can take damage. Yep, we definitely can get hit multiple times by the same orc. And that's probably too fast, honestly. So let's
join that down. First, I'm gonna control Z to add the spear
weapon back in. Let's go to the orc and I'm going to set its
weight time to 0.5 seconds. And if it seems like
the player takes too much damage as multiple
enemies group up on him, you can still increase
the invincibility timer, so the maximum number of hits
per second would drop 5-4, depending on if you
make it at 0.2 or 0.25 seconds timer.
However you want to adjust
29. Player Defeat: Character runs out of HP, we do need to switch
to a death state, and part of that is going to be stopping the attacks
from proceeding, as well. And eventually, we'll put
in a game over screen, but let's just get it to get to the end of the death
state for right now. So over on the player scene, we have our move state. We want to put in a death state, and I think we can actually
just go to the org scene, and we might even just be
able to copy and reuse this. If I look into the
death state script, I see nothing about
it that would really be an issue except for maybe the quarter
here at the end. Maybe we just want to notify a game system that the
character has been defeated. So I'll just copy the script. We'll go over to player. I'm going to click over here and add a new Limbo state node. Let's say player defeated state. So player defeated state, right click, attach a script, and then we'll create that in the player folder.
I'll paste this in. We'll rename the
class name up here to player defeated state. Okay, let's check the name of the animation and make sure that's on the animation player. So animation player death, we have that there.
Okay, so that's fine. If we go into the death state, then we're going to want
to add a track called Method Track on our
player defeated state. And if we go to the end here, let's enable snapping in the bottom right
too, if that helps. So snap to 0.6, and then right click
and sort a key, and we'll call finished
on the script. Okay, so I'm tempted to use the already existing
combat system and then do something like report
defeated on the player. The defeat of the player is more of resetting the game or
announcing game over. So it's not really a function
of the combat system. So for the sake of
separation of concerns, I may actually just create a new node for handling
game management, and that might
eventually include saving and loading, as well. We'll see. So if we
go to the root of our project and systems, I
would want to create it here. I'll right click and
create a new let's say, script, and we'll call
it game manager dot gD. I think that works as a name. Okay. And in game manager, we'll leave it as a node
because we're going to make this an
Auto oad Singleton. Why don't we just use a
function report game over, so this could be called from anywhere as long as we
reference the game manager. And when we report game
over, let's just print, which is going to print
strings to the console, and we'll say, game
over player defeated. And later, we would replace
this with UI so that we can actually switch to a new game or see the game over
stats, et cetera. But this will at least
show that it's working. Okay, so we want to take
the game manager and create a scene so that we can edit export variables if
we ever need them. So I'm going to write
clicking Systems, create a new scene. This will be game
manager dot TSCn. I'm going to right click here, change its node type to a node. We'll drag the game
manager script onto it. Okay. And then we
want to save it, go to Project Settings. Inside of Globals,
we want to auto load the systems game
manager dot TCN ad, and now it's a globally referenceable game
manager symbol. So if we want to use that, we can just go back
to our player and the player defeated
state and finished. Instead of queuing free, we'll
leave the character there. And we'll say game manager
dot report game over, and that's all we need to do. One reason to not
remove the player from the scene is that as
things are right now, all of the enemies
are depending on the player's existence so that
they can continue moving. So as soon as you remove
the player from the scene, then all the enemy scripts
are going to start breaking. Now, if you want to
fully flesh that out and allow the character to be removed from the game scene, then you will just need to check on basically each run of, say, the c chase scene here. You would just need
to make sure that the target is still valid, and if it's not valid, then
you would just return. So that could be
something like if underscore target equals null, then you return, and then that'll just mean that
it just doesn't move at all. So, you know, for fun,
we could actually go to player and in the
player defeated state. We can actually add in
the agent dot q free, and we could just see
how that would work. So if I just add
in that check that Targets Not null for
the chase scene, is it going to break or
is it going to work here? So let's give it a shot. Let's see if our enemies can still continue functioning with no player on the screen. Okay, so we're going
to get defeated. I haven't actually set up the transition yet for player HSM. So in player HSM, let's create another
function, set up transitions. Okay. So this is going
to require us to add a transition from move
state to defeated state, and that'll be on
the event defeated. Okay, so let's define
those up here for the player HSM at
export var move state, which is a limbo state at
export var defeated state, which is a limbo state. And then we'll create a
constant for defeated, which is going to
be a string name equal to and quotations. Okay, now we need to update our variable names here
for add transition. So snake casing for
those variables. And we should only be able
to go from move to defeat it actually because
we don't have any resurrection
mechanic in the game. So once we defeated,
the games over, and that'll be fine. You could always
add a transition back if you have some
way of resurrecting. Now we want to call setup transitions
inside of setup HSM, set up transitions there, and we can handle
the transition by connecting to the
player stats signal. So after asserting that
the players not null, we want to connect to the
signal on the player. So that really is going to be the signal on the
stat controller. It could either
directly reference the SAT controller or connect to the stat controller through the player though it
doesn't have that setup yet. So I guess in this case, it would be a little bit more direct to just
reference an extra at Export VR stats of the
stat controller Okay, and then down here we'll say stats Alive changed dot Connect, and it'll be on Alive changed. So let's create that function
at the bottom and callback. And this is going to
have the P value, the new value of the live. The S return void. So if P value equals false, then we want to dispatch
the defeated event. Okay, so that will
handle triggering the transition from move to defeated state if we
are in the move state. If you end up adding a
bunch of other states, there's also the option
of doing any state here. So this will make it so that
if the characters defeated, then from any
state, you can just transition directly
to defeated state. That might actually
be preferable thinking long term because maybe you would add
on some kind of attack state where you play
a different animation. You'd still want to be able to immediately switch to defeated rather than just being
able to go from move to defeated when this
dispatch occurs. Otherwise, if you dispatch when the character is
not in the move state, then it may never go
to the death state. So I think this would generally be the
preferable way to do it. Okay, so now we need to assign everything in the inspector. So assign the stats controller, assign the move state, and assign the defeated
30. Testing and Enhancing Behavior on Player Defeat: To make it quicker, I'm going to click
on Stats Controller. Let's take the HP and set it
to ten, and I'll hit play. So if our character
gets hit once, we'll immediately switch
to the death state. Okay, so let's find an orc. Okay, there we go. Okay, so
we hit the defeated state. But we can see that the
agent doesn't have stats because we didn't require
that on the character. So if we just want
to be consistent, then yeah, we can add
in stats to the player. I think it's okay to reference
stats through the player because we would generally understand that the player
is going to have stats, especially in an
RPGs kind of game. Also, we can give rid of that
speed variable right here. Can see that the
player is still using the speed from the base script instead of the speed
from the stats. So yeah, let's get a reference
to at Export var stats, which is a stat controller. Okay. And then when
we have speed, we want to use stats dot speed. Okay? And let's copy this and replace it
down there as well. So consistently using it in our physics process
for the player. Okay? If we assign the
stats and the inspector, we can do that right there. And then if we look at the HSM, we no longer need a
direct reference to the stats because we already
have it through the player. So we can do
player.stats.ai changed, and we'll connect to the
live changed through that. So just being a little
bit more consistent about how we do things
across our scripts, though both options,
of course, work, whether you do it directly to the stat controller or
through the player. So let's go into the game mode and have a character
get defeated here. So the characters removed, and what's going to happen
is when new characters get created new NPCs, we assert that the target player needs to exist, but
it doesn't exist. Okay, so, actually,
we're just going to remove this assert
because we can have character spawn into the game world and
not have a target. So, if this is null, I think this might
actually still run. And then so there'll be no target because the
character's already dead. And then we come in here. When target is null, then
we don't do anything for the Chase state.
So I'll hit play. Okay, so there's that. The
enemies just kind of stop. Yeah. Okay, so you can see
that it says trying to assign previously
freed instance. So what we do need here
is one more check. We'll say I agent dot target dot player does not equal null,
then we'll assign it. Okay, that seems to be good now. No more errors.
Now, the characters are still running in place. So we may even put
in a idle state for the enemies if we want
it to be a little bit more believable because
they've finished their job, so now they're just
sitting around. So what we could actually
do here on Update is we dispatch an event. Let's say this is
the idle event. Okay, so we're going to dispatch Idol from the Chase
state, right? So now if we go to Ok Enemy HSM, we want to add a transition from the Chase state
to an idle state. So say add transition. Chase state to Idle state. And that'll be under
the event Idle. Okay, so we need a Idle state. So I'll do at Export
var Idle state, which is a Limbo state here. Let's right click Add in a
new child node Limbo state. Okay, and this is going to be really similar to
the death state. I'm going to go into
Death State and copy everything up here into
the new limbo state. So this is going to
be Idle state here. Right click, attach a
script, Idol State. And let's control
V paste that in just rename this Idle state. If I was going to
create more states, I'd probably start
thinking about creating a base animation
state at this point. But really, it's
not that much code. We're duplicating here, so
it's not really a big issue. And this is the simplest
way to handle it. At least short run speaking. Okay, so with the alpiPasted
for the Idol state, I just want to rename the
animation here to Idle. It already gets the animation
play from the Blackboard. We do want to cut this
bit out about setting the agent to no longer be
alive because that's not true. Basically just this.
We have an animation. We get the animation play
from the Blackboard, and when we enter, we
play the animation. And the animation
is idle over here. So now in Enemy HSM, we just want to assign
the idle state. Aside from that, this should
handle the transition. We do want to take
the transition to death state and change
this to any state. So whether the characters
idling or chasing, we can transition to death if the death event occurs.
That's important. And then we just want
to make sure that we copy the idle state
over to the skeleton. Paste it in here and assign
it in the enemy HSM. So assign on the
right, idle state. And aside from that,
it should be good. So I'm going to hit
play, and we'll see if we can get our
character defeated. Okay, so here we
go. Our character switched to the
idol and that's it. So they no longer need a target. They're no longer
chasing a target. They're just idling around
in their idle state.
31. Creating Pickup2D Scene: Now we're going to
move on to creating EXP pickups so that our player can gain experience
points and leveling up. So there's going to be quite a few steps
involved with this. The first is going to be to create our basic pickup script, a pickup collector script, and then making sure
that our character can actually acquire
those and then assign the XP boost to the
player's current experience. So let's start by creating the actual game two
D world pickup. So I'm going to create
a new note here. We'll use two D scene, and I'm going to
rename this to be pickup two D. All
right click on it, and let's change the
type to be a base of Area two D. And now if we click on the
attached script button, then we'll get a pickup
two D that extends Area two D. Let's
say Objects folder, and then I'll create a new
folder inside of here. We'll call it pickups. Then I can save the
pickup undersquare two d dot GD inside of
here, create the script. And at the top, we're
going to give it the class name pickup two D. We'll save this in
the Objects folder pickups. Pickup tdt TACN. So
save that there. So this is going to be a world physical node that represents an overall object
that can be picked up and applied to a character
in one way or another. So that might mean
directly applying it to the stats or in some
other circumstances, it might mean you
pick it up and you add it to a
character's inventory. We're not going to have an
inventory for the game, but we will have the
ability to add levels to the weapon loadout or create a new weapon loadout for
a new type of weapon. For the pickup two
D, we want to create a T function so that we can take the object off the game map and apply it to the characters. So let's say function Tr take, which is going to need a target. So P target is a no two D, and we want to return Boolean whether this was
successful or not. Okay, so I'm going
to push an error here which says it's
a virtual function, implement an extended script. Now, in Gudo 4.5, they're going to have
abstract classes, which makes setting this up more concrete because they add some extra keywords for it. But because we're on Gudo 4.4, which is the current
stable release, we'll just do it the old way, which just basically
means that we have to remember that when we extend our pickup two D class
that we need to manually override this with a new version of the function ourselves. With the same parameters
and return type. For TriTek, we're going
to return false because we're trying to take an object
on this abstract class, which is the pickup two D. So for the different
pickups in the game world, we want them to all have
their own custom way of applying to the player
or target node. So we'll need to implement a custom extending script
for each one of them, L one for EXP and then
one for a weapon pickup, or there might be
a upgrade pickup. So those are all going to
work a little differently, so they all need their
own tritak method. Okay, so we have our
pickup two D script here. Let's right click and
add a child node. We need a collision shape. So for the collision shape here, I'll just go to this
shape in the top right, and let's make it
a circle shape. So you probably also want to
default sprite here as well. So I'm going to right click
here, Add a child node, and we want to get a sprite. So for an object
that's just going to play one animation
over and over again, no switching or anything,
I think animated Sprite two D is a
better option there. For instance, if a coin is
spinning on the map over and over again and
you just want it to autoplay that one animation, then animated Sprite two
D is perfect for that. So we'll select that
and hit Create. Now, for the icons in the game, they may not even
really an animation. But if we're going to make
this the default template, I want to open that
up as a possibility. So for our animated Sprite, we go over to the
animation over here. We'll create a new Sprite frames resource and open that up. So we have a different animation window in the bottom left. So I could just rename
this to Idol, if I want. Let's go into art. We want to go into
Raven Fantasy icons, and the full Sprite sheet, let's look into this and see
if there's anything we want. So to access this properly
from this animation window, there's this icon Ad
frames from Sprite Sheet. So click on this. Then
go to Art fantasy icons, fall Sprite sheet, 16 by 16. Really, you could use a
bigger one if you want. Maybe I'll just
use the 32 by 32. So as a little bit more
resolution and open it up, and you can just select
the icons you want. Okay, now for this
to slice properly, we need the right
number of rows and columns across our
sprite sheet here. Or if you know the
size of each icon, I think you can just
go to the size over here and just put 32 30 Yeah. Okay, that's definitely
the faster way of doing. When you change one, it'll automatically
calculate the other. So we have 16 horizontal. Really, those are the columns, and then 128 vertical
are the rows. So rather than calculate that, we just know that each
one is 32 by 32. Okay? Now we can easily just pick
the icon we want from this. So you can use
anything you want as a EXP Gem I think in
the template project, I was using this one over here. So kind of just a blue gem.
We add that one frame. And this would autoplay if we just check
autoplay down there, but you can see that this doesn't even need
to play really. And it's probably slightly more performant if we
turn off autoplay. So it's just showing
this one frame and not trying to
even loop anything. Pure assumption on my part, but let's just have
that set there. And that'll be our default
spike for Pickup two D. So for the pickup
two D root node, we probably also want to set
up the collision layers. So I don't think we have a
specific layer for pickups. So let's add in a
new collision layer. Just make it layer
five and say pickups. Okay, close. And then
we want to, of course, put the pickup on layer five, and we'll turn off mask. We'll turn off
monitoring because this is going to be
picked up by something. It's not monitoring for pickups. It lets the player that's picking up the object
do the detection. So this is just monitor
a bowl with layer five. And that's the base
scene for our pickup
32. Implementing Experience Pickup System with Inheritance: What we can do in
the bottom left is search for pickup again. This should show
the pickups folder, and we want to right
click on Pickup two D and do an
inheriting scene. So that's at the top here,
new inherited scene, which means that we're creating a new scene
based on this scene, so it inherits all the same
defaults from our parent. So we create that new scene, and you'll see that some of
these nodes turn orange, which means that these are actually based off
of the parent scene. You can jump into the parent
by clicking right here. So you can have
scenes within scenes. And we want to rename the
root node to let's say EXP pickup two D. Because we want to be specific
about what we're doing. And let's save this
as a new scene. So XP pickup tod
dot TCN, save that. And then let's right click on the root node and
extend this script. Okay, so this is
going to give us our EXP Pickup two D script, which extends from pickup two D. And then we'll go up
here to the top and say class name EXP Pickup two D. So we want to
give a variable for how much EXP we're
actually going to pick up whenever the character uses
Tri take on the pickup. So at Export VR is
going to be a integer, and we'll default that to one. And now, if we look at
the pickup two D script, we want to take this
function and properly implement it in our
inheriting script. So I'm going to copy
this, and we're going to go over to
EXP Pickup two D. I'm going to paste it
and we're going to write our real tri take method. So the EXP pickup is going
to apply the experience to the target if the target
actually has experience. So let's say if ptarget
dot stats equals null, then we're going to
return false here. Otherwise, we can say var stats is equal to ptarget dot stats. And because we don't
know what this stats variable actually means in
the context of each script, let's assign it the stat
controller hype directly. So once the condition is met, that this is a target
that has a stats block, we can say stats dot experience
plus equals experience, and then we'll return true. So after we pick
up the experience, we don't want the pickup
to be usable again, so I'll say monitorable
equals false, and then we'll cue
free on the object. So when you cue free,
there's going to be some period of time before it's actually finally
removed from the scene. So we want to make sure that nothing else walking into it at that singular frame could accidentally pick up the
EXP pickup a second time. So I'm turning monitorable false here so that nothing
else can detect. Just as a safeguard, and then we remove it from the
scene completely. So this bit right
here, I probably actually want to reuse
across all my pickups. I'm going to cut this into pickup two D and then
say function remove, which will return void, and I'll just do
this wrap up step. So now, if I go
back to EXP pickup, I just call remove, which is
sourced from the base class. So this function is
already written in the base class, and
we can reuse it here. Then, like, yeah,
anytime we implement the trite if we
want to redo that, we can just do remove here. And for this to
work, we need to add in the experienced stats
to our stat control. So in the set controller, I'm going to create
that EXP property. Okay, so we're going to
have Vara experience and the soil default
to zero as an integer. Okay, and kind of in line
with the take damage, where we call the set invincible separately
from setting the HP, I might actually prefer to
not make a setter here. Rather to do function
ad experience, and we'll give it an amount. So P amount, it's an integer
and we'll return void here. So we want to take the
experience plus equals P amount. And then as a to do,
check for level up. And we can also say
prints experience here. So we'll get the total
experience as we're collecting items from
the map in the console. And then to update the
EXP pickup for that, I want to do stats dot add
experience experience. Okay, so we're being
a bit more explicit. So this function implies
anything related to experience that
needs to change on adding experience
is going to occur, which means we would check
for a level up and so on. And I guess this would
have some benefit. Like we could change the
experience value without automatically triggering
or checking for a level up if we had the
need for that later on. Instead of always doing
that, no matter what, like we did with the HP setter, where it always
updates your life. Both ways of doing things
are valid to an extent. It's kind of about how you want to express
your intent here. And do you want to hide the fact that setting
experience would also trigger a level up by putting it in a
experience setter, or do you want to
be more explicit with add experience function?
33. Setting Up Collector2D and Fixing Player Animations: To get the experience onto our stat controller
from the Pickup two D, we need our collector
two D node, which is going to go
around collecting pickups for our player. So let's go to the player scene. I'll right click on player. We add a child node. Let's look for a Area two
D. So this area two D, I'm going to rename it
over here to be collector two D. And then let's right
click attach a script. So the script will go it
could be in characters, so I'll actually put
it in characters. There might be a case
where we actually make an enemy able to
pick up objects, but I don't want it to be
completely player specific. So I'll put the script there. Let's create. Then
in the top left, we'll give it the
class name collector two D. What we care about for the
collector area two D is when a pickup
enters at space. So we'll say function underscore ready is going to return void, and we'll say area entered
dot connect on area entered. Then we'll create the
callback function underscore on area entered is going to receive a P area area two D as a parameter
and return void. So if P area is a pickup two D, then we will do P area dot
Tri take on our root node. So our root node is our player. We could export our
root node here at export object root is
node two D, I suppose. And then we'll send
that object root here. As the TriTek object, the one we want to pass through. Now, in our inspector, sign the player here, and that might actually
be all we need. So let's add a copy of our EXP pickup into
the game world. We'll put a few in, really.
So I'll right click on World. Let's create a node two D.
I'll rename this pickups, and then I'll add a couple
pickups onto the game world. So one, two, three, and then I'll put these all
onto the pickups parent. We can see that these
are quite large. That's fine for right
now, but we probably are going to scale them down
for the rest of our game. So before these
are going to work, I just remember that
we haven't set up the collider on the player. For collector two D, right
click at a child node, and we want collision
shape two D here. So on the right, add a new circle shape and shrink that to maybe
right around there, a little bigger than the
normal collision shape. Go to collector TD and
take the collision. We're going to mask on the
pickups layer and then turn off mask layer one world and
collision layer one world. This should be a
monitoring only script. And I did just
notice that because these bright frames
are different sizes for our animations, they are not lining up
at the center properly. Like, the idol looks fine, but run is maybe one
pixel to the right, and then death is a little
incorrect there as well. So one way we can fix this to make sure they're
all correct is to assign a position property
to each of the sprikeFrames, which it looks like I
actually already did here for the death position. So Idle has its own value. Let me see what happens if I actually remove those
position properties. So do that for death
Idol and run, okay? And then we'll remove it for the reset as well if it's there. Okay, so, yeah,
it does look like we need to customize that again. Okay. So let's
start with the run. I will position it
right here at negative 30 and keyframe that
onto that animation. We'll go to Idle. I'll
hit W and move that down. So it's like right about
there, negative 13 and keyframe it. Okay,
let's go to run. Let's go to Idle. It seems like Idle is
still one pixel low, so I'm going to move that up
to negative 14, keyframe it. I think that's what
we had before, but I'm going to take the
run and move it one to the left as well and key frame it. So now if we switch
between idle and run. Okay, that actually
looks correct now. Let's go to death
and move it over here to the right
and key frame it. So that's ten pixels
to the right. And now we switch to idle. Okay, death is one pixel
to the right too much. So that needs to go to a
negative nine, key frame it. So we have idle. Okay,
so we need to go two more pixels to the left for the death and key frame it. So that's an eight, and then
the idle is zero for X. Okay, all of these look
like they're lining up now. Okay. So here's
the actual values. Eight negative 14 for the death. Idol is zero negative 14. Run is negative one
and negative 30. And then the reset, that
should match the idol, which is zero, negative 14. So reset will take
down to negative 14. Negative 14 is right
there, Keyframe it. Okay, and that should fix
any animation problems we're having with
those sprite sheets. Okay, now back to the collector
two D. Make sure you turn off layer one and the mask
for layer one, as well. Okay, and now we can hit
play and see if we can actually pick up
those little icons. So we did get that one. Let's check the output. Did we get the experience? Yes, we did. We have one experience
there. We'll go down here. We get experience two. Now, this one experience three, so we can confirm
our pickups are working based on
our console output. And our character got
defeated in the background. But that's good
that's showing that that game over system was
actually working, as well.
34. Creating Enemy Drop Mechanics and Resizing Pickups: Our pickups. Now, we
want to make sure that enemies actually drop them
when they're defeated. We probably also quickly
want to resize the pickup two D because it's
double the size of our player at this point. So in EXP pickup two D, I can either change
the animated sprite to the 16 by 16 version, which is convenient because
we already have that, or I can take the node
and scale it down. So I can make it like 0.50 0.5. And then that would be one
way of making it shrink. But if we already know that size isn't going to be
consistent in our game, it might just be
making more sense to go into the base scene
of pick up two D, change the animated sprite. And then we add the same icon, but from the 16 by 16 pack. So go into art icons full
Sprite sheet, 16 by 16. Size it as 16 by 16 because this is the
smaller sprite sheet. And then we'll control
metal mouse reel, zoom in, find the sprite, add it, and remove the large one. So select zero and delete that, and then it'll go down to
frame zero. Save that. And then back in this scene,
you can save it as well, and it should be good
on the worldview. Okay, so now it's the
appropriate size. Okay, so let's go
into the orc scene, and we'll add the
ability to drop items. So in here, we want a
script that is going to handle items that we
can drop into the game, and we may want to randomly
select from a list of items, and we can weight them
kind of the same way we did with waiting
the enemy spawning. So I'll right click on the death state since this
is going to be directly tied to the death state in the sense that we
drop items on death, and I'll add a child node. So do a regular node here. Then we'll rename this to
be something like drops. Right click, add a script, and then over here,
we'll create that. So we'll give it the
class name drops. So we'll give it the
class name drops. Okay, and in our drop script, we're going to want basically a list of items we can drop. So let's say at Export VR, we'll go with drop definitions, and this will be an array
of drop definition. So kind of following the
same pattern from before, where we have a editor
defined resource, we can repeat that again
so in the file system, we're going to want
to go somewhere like the Objects pickups folder and right click and
create a new script. And I am going to call
this drop definition. And this will extend from
resource. Create that. Now we can open up
drop definition, give it the class
name drop definition. And we're going to want
at Export VR scene, which is a packed scene. So what are we dropping
into the game world, the actual item drop? So because it's a packed scene, it could really just be anything that has a node at its root. You could drop enemies
if you wanted to. So this packing just
makes it flexible. Okay, and then next
we want a weight. So Export var weight is afloat. We'll default that to 0.1, and that is a decent start. So we'll go into the
drop script again, and we have our array of drop definitions so we
can click on drops. And if you can't see the array up here and the
inspector over here, you can just go to Project
and reload the project. Okay, so now if you
check the inspector, it should show up there as long as you didn't
have any errors. So we'll create our
first drop definition. So new drop definition, and we can make this a EXP
gym with a weight of one, if we want. So quick load. And we're going to look for
the EXP pickup two D enter, and that sets that up. So our orc is just
only going to be able to drop the EXP Gems, and that's perfectly fine
for our basic enemy. So we want to function we can
call on our death state or our animation that would be able to generate the
drops in the game world. So we could say function
drop if you want, you could pass on a
number of times here, like P times is an integer, and that can default to one, and this will return void. So we could say four time in range zero to P times
incrementing by one each time. So this means we start at zero, we go up one time until
we reach P times. So if P times is one, this will run once
because it'll do zero, and then it'll hit
P times and stop. So we'll do the colon there. Okay, and then after
doing the loop, we need to generate
a drop each time. So it would probably
be best to calculate the total weight once and then
we use it inside of here. So let's generate the weight. So before we generate the drops, we'll say four DF and
drop definitions. We'll accumulate
the total weight, so we need a
variable above this. So far, total weight, there's a float
starts at zero, zero, and that's going to
be total weight plus equals DF dot weight.
Okay? So we have that. Now, inside of this four loop at the bottom, we're going
through it each time. We want to get our
selected weight between zero and
the total weight. So it's kind of repeating
the same logic as other randomization we've
done in this course. So say var selected weight is going to be equal to rand F, which is 00-1 0.0, and we'll multiply that
value by our total weight, which will give
us our selection. So now for each of the drops for definition in
drop definitions, we'll add the weight so far. So far, current weight, which is a float
and starts at zero, zero, we'll say that the current weight plus
equals the definition weight. Okay, so that gives us
what we're at so far. If this is greater than
the selected weight, then we want to return this drop or add it to a drops array. So if the current weight is greater or equal to
the selected weight, then this is the one we want
to append on to an array. So let's create array up
here above the four loop, and we'll say var drops is
an array of drop definition. Now, key thing here, this can have multiple copies of the same drop definition. It's an array, not something
else like a hash set. So we can have the same drop two or three times if needed,
which is important. Say drops dot append
the definition, and then we break out
of that four loop, and then it's going
to go again here. So once we get to where
the current weight is equal or greater than
the selected weight, we pick the drop, and
we loop again for each time that we need
to for this drop setup. So now we have all
the drops down here, so we want to instance
them into the game world. So let's say four drop in drops. We're going to create an
instance which is going to be equal to drop dot
seen dot Instantiate. And then we're going
to add that as a child to some parent inside
of our game world. So in world, we could just reuse this pickups
node if we want. So I can take pickups. Let's go to the node up
here on the top right, and I will add a new group. So we'll say pickups. I'll make this global. So for anything that's an item to
be picked up from the world, we'll add that in
as its node group. So when it's in a node group, it's very easily findable
inside of our script. And we can actually
get that pickups node as soon as our orc load, so we'll know if
there's a problem. So we'll say var pickups parent, and that's going
to be node two D, and we'll say
function underscore already. Let me zoom in here. Pickups parent is
going to be equal to Gtree dot Get first
Node in group, and we're looking
for that pickups so in the node groups over here in the
inspector node groups, you can see pickups
there is already one, and this is assigned
to this pickup node. So as long as the
pickups node exists in our current game world,
it'll work just fine. We could go a little
step further and pull this out to a
constant variable. So we'll say pickup
group here, all caps, and then constant pickup group
is a string name equal to, and I'll say ampersand, Control V, paste the string. So as long as you have a node in the pickups group
for this and you've assigned that node to the group inside of your game project and
it exists in the scene, then this will work
perfectly fine. You don't need to get
any direct references, which would be tricky
because the orc doesn't exist in the scene until
it's been spawned. So this is a decent way of getting that parent node
that we can parent it to. If we want to be sure
we can even say, like, assert pickups parent
and does not equal null, and then we'll say there must be a parent for items to drop. Okay, that's pretty
clear. Okay, so we instantiate each of the scenes and then
we want to add them as a child to
the pickups parent. So pickups parent AchildEstance. And the location we want the
drop to be in is going to be based on the
node two D. So we actually need to take drops and change it to a
node two D type. So then down here, we can say Instance global position is going to be equal to global
position from the script. Okay, so that means
back in our org scene. So now we want to
change this node type, I'm going to right click
and change the type, search for drops, and
you should see it here. Double click. Okay,
click on drops. Make sure in the
inspector, you still have your drop definition
for the EXPJM there. And we probably need
to move this out of the HSM nodes here. Since this is a node two D, we want it to be a child
of our main node two D, so I will bring this up
here as a child of work. And that should
probably be good. So we have the item or whatever we're dropping get instance
into the game world. It becomes a child of
the pickups group node, and then we give it the
position of this drops node, which drops is just a offset, if any, of the orcs position. We don't need to change
it, but we could just have a default as zero, zero
there. That's fine. And now we can either
call drops from the death animation
or the death state. So if I go to
animation player here and we open up the death
animation, let's see. Death state. So let's see. We're calling
finished death state. We could just put the drops as soon as the orc starts dying. I think that might be more
the way most games go, where you see the loop before they despond
from the scene. So at a track, call
method on the drops node. Then down here at the first
frame. Let's zoom in. All right, click Insert
a key. We do drops. You can see the parameters
work in animation player. So in the top right, we have our drop method name.
Have the argument. So the first argument and the
only argument is int value. So how many times do
we want to call drops? We could say 50 for fun, and let's hit Play and see
if that actually works. So this will put a lot of these EXP gems into the
scene if it's working, but at least we'll
definitely know if it works. So let's see what
happens. Okay, I think they all just
spawn on the same spot. So I'll go over that. And yeah, you can see we
picked up 50 gems.
35. Generating Random Item Drop Positions: Ly, when the orc drops
50 of these items, they're all stacked on top of each other, if I
check the output. So we got 50 XP there. And I think what
we'd want to do is that when the object spawns, we either give it
an offset or have it move away from the object. It's dropping from a little bit. So I think the quickest
way to do this would be to generate a random
offset position, and this will be a
certain number of pixels away from the character
in a random direction. So we can just add this
in for the drop script. The top, I'm going
to at export of our drop Max distance,
which will be a float. And let's go with ten
pixels by default. This is going to represent how far away from the drops node. That's this specific
node that we're working on that the
object can spawn at, and this is a maximum amount, so it can be 0-10
pixels effectively. So down here at the bottom,
I'll create a new function. I'll call it function, underscore, get span, offset. And this will pass
in a P max distance, which is a float, and we can default that to the
drop max distance. So we don't have to
pass a parameter. It can just be that by default. And then we want to return
to vector two as the offset. So to get a final offset,
we need the direction, and then the magnitude,
the magnitude is right here in
the P max distance. So let's say var direction is going to be an inferred
type from vector two, and we're going to
create a new vector two, so wrap it in parentheses
here, we need a X and a Y. So to get that, we
basically need to generate a random direction between
negative one and 1.0. So I will say rand F, and I think we need range
here because we want to go negative 1.0,
not zero to one. So rando F range on
negative 1.0 to 1.0. And then we need to do that
a second time so we can just copy this line for
the Y value down to here. And then I want to normalize the vector so that
it's a pure direction. Okay, so now we get our offset. So var offset is going to be equal to direction
times the P max distance. And then we return the offset,
and that's basically it. So we take Get
spawn offset here, and when we're setting
the global position, we're going to do global
position plus GitspawnOfset. The default is
drop Max distance, so we can just use that
as the default here. So you don't actually
need to pass in drop Max distance,
but you could. So it would just be the
value overriding itself. Either way, it'll be the same, but you have the option
of leaving it blank, and then that value
will just default down here. I think
that's all we need. So let's just go to
play mode and see if those 50 items actually drop
at different locations. So let's hit play,
and we'll go in here. I'll shoot an arrow
or spear at the orc. And we can see that they do all spawn in random directions. But I actually made a mistake. So we don't want to normalize
the vector because I want the percentage of the distance
to be able to go 0-100%. Okay, so because I normalized the vector for the direction, it's always going to actually give you the full
max distance here. Th will just be the
pure direction. But what I actually want is
when it takes that direction, it'll be some percentage
of the distance. And we might actually
want to call this more like a random vector. And then we'll go down to
offset down here and past that. So when it's taking the X, it'll be somewhere
0-100% of the distance on the X multiplied by the P max distance and
the same for the Y. So this will give
it kind of more of a random position inside
of an imaginary circle. If you were to create
a collision shape, it would kind of
look like that for the possible span locations. So let's go into play mode, and it should be more correct now when we drop all 50
items at the same time. So there you go. They're all randomly generated
inside of that circle. We're never going
to actually drop 50 items at the same time. So this kind of
looks kind of funky. But if there's like
two or three drops, I think it'll work just fine. So to kind of demonstrate, I'll go to animation player, and when we do death, I'll take the drops
function here and we'll change its argument value of how many drops to something
more like three. I think that's more realistic. Let's hit play, and then
we'll go into here. In most cases, I
think most enemies are just going to drop one item. But if we do decide
to drop three, then at least we can see that they have
different positions, and it's obvious to at
least see where they're at. Okay, so I want to wrap this up by just changing
the drops to one. Each or will only
drop one EXP up.
36. Implementing a Pickup Gravity Area: It's a little tedious
if your player has to walk over each and every
pickup that you want to grab, especially when you're
going to be dodging all these orc enemies that
span across the screen. So what we want to add in is going to be a
pickup gravity area. So on our player scene, rather than adding
the gravity area to our collector two D node, I'm going to create a
separate two D area node for gravity specifically. The reason for that is because the collision shape two D
here for actually picking up the items is going to be a different area than the one
that is applying gravity. So that would be kind of confusing with the
collector two D, having two area shapes here. So we're going to
create a new node. I'm going to right click on
player add a child node. This would be at area two D. We'll add it in
here at the bottom, and I'm going to rename it
to be say pickup gravity two D because this is specifically for items on the
map that can be picked up. Okay, so let's right click
on player add a child node. We're going to look
for area two D, and I could call
it something like gravity area two D. I thought about calling
it pickup gravity two D, but we want to make the script a little
more generic because there may be cases down the road where you actually
want the gravity to apply to something
that's not a pickup, and handling the pickup
specific gravity is just managed by
the collision shape. Handling the fact that
it is for targeting pickups is just managed
by the collision layers. So all you have to do to
make it target pickups is to choose mask five here
for the pickups layer, and let's turn off layer one for where it exists on and
make it monitoring only. Okay, so that's our setup. Then we just need in
the gravity area to apply it to all objects inside
of here that it detects. So inside of the gravity
area, we'll right click. Before we move on from
the inspector, though, if we look at this
gravity section, is a built in area
two D gravity, but the thing is that applies
to rigid body two Ds, which are full physics
objects in the game, and we don't want 1,000 rigid bodies floating
around the screen. So rather than using
the standard physics for controlling the
gravity on our pickups, we just want a simple script where we're just
going to make it pull into wherever the
player's center is. And not deal with rigid
body two Ds at all. So essentially, it's a
performance optimization. We don't need full physics
for each of our pickup items. They just exist to be
picked up by the player. So sucking them into the player directly is perfectly fine. So I'll right click
on Gravity Area two D and we'll attach a script. Of course, I'll call
this gravity area two D. Let's create
that class name. Of course, gravity
area two D. Okay, so while an object
is within the zone, we want to track it and consistently apply
a movement to it, basically translating
another node two D across the screen towards the center
point of this area two D, the gravity area two D. So
let's have a local variable. I'll call it targets, and this will be an array
of node two D is fine. On function ready, we want
to connect to the signal of this area two D. So
we're going to say area entered dot connect, underscore on area entered. We'll also connect to
the area exit area exited dot connect
on area exited. We need to create
those functions now. Function underscore
on area entered, and this is going to give us
area two D as a parameter. So P area area two D, we be trying void here. Okay, so what are we going
to do with the area here? We're going to move it towards the center point of our gravity area two
D. This is going to work fine with the
pickup because the pickup areas are at
their root in area two D. So anything under it is going to be automatically move towards the center so we do require it to have
that kind of setup. Otherwise, you would
need a reference to the parent node of the scene. Whatever is your base of your pickup scene is
the object or node, rather, that needs to move. So be aware of that.
Because we're doing all the pickups
specifically like this and pickups are
exclusive to layer five, it will work just fine as it is. If you want to be extra safe
and you want to limit it to only pickup scripts,
then you could, of course, check if the P area
is of type pickup two D. It doesn't seem to be
necessary as it is right now, but that's something
to be aware of. So I'll just put
a little comment up there about what I just said. Okay, and since the P
area pickups like that. I'll work fine as it is. So I will say targets
dot a pen, P area. Remember, P area area two
D is of type node two D, so that works fine up here. There's no need to be
more specific than that because we only need to access the node
two D functions. So on area exited, function, underscore on area exited. P area area two D, returns void, then
we're going to say targets dot erase P area. Now, to note about
the erase function, if the object you're
trying to erase from the array does not exist in the array, then
this will do nothing. If it does erase, then
it will remove it. So there's actually
not a need to check if it's already
in the array. Because if it doesn't
exist in the array, then this will already
just do nothing. So there's no need
to double check. Right. And then we need
a physics process. So let's say function
underscore physics process, and we'll say for
target in targets, we want to translate
it across the screen towards the center
gravity area point. We need a speed at the top at export far gravity
speed is a float. If I recall, this
is going to come out to pixels per second. So I'll try something like 50.0. Then we need to
calculate the direction between the target and our
gravity area center point. So far direction is going
to be equal to target dot global position dot
direction two global position. Okay? And then we just need
to calculate our move amount. So var move is going
to be equal to direction times gravity,
speed, times Delta. Since we're using translate, we need to factor
that in directly here for the final move vector. We can say this is
a factor two D, if that makes it more clear. And then we want to
take the target and translate it across the
screen by the move offset. This is essentially our
gravity script right there. The last thing I think
we need is to add a collision shape to
our gravity area. So right click on the
gravity area to D, add a collision shape to
D. Go to the top right, a new circle shape
and make this rather large so that we have the area where it's
actually going to apply gravity
towards the center. Okay, so I can play, and we'll go in here
and you can see that our objects get
sucked towards the center. So right now, the
gravity is linear. We may want to
give it a curve so that the closer it gets
to the center point, the faster it's actually going to animate that translation
towards the center. So if we go to gravity area
two D, and inside of here, I'll say at export, let's say effect curve of
type curve. We'll save that. Then in the inspector, let's edit this effect
curve, new curve. And then we want X or the
max domain here to be basically the distance
from the center. And then the value here is
going to be what percentage of the max speed you want to
apply based on that distance. So let's say that
the distance is like 100 here for
the max domain. The closer it is the closer
we want to get to 1.0. So let's take the first point
and move it up here to 1.0. I'll zoom in here
so you can see. And then I will just pull a
second point down like this. Okay. So if it gets
to 100 distance, it's going to be very
weak at that point. And that would be measured
in pixels, of course. So then with that curve, we want to go back
in here and adjust the move by sampling this curve. So let's get the curve modifier, and that's going to be equal
to effect curve dot sample. And we need to sample
with the distance from our target to the
gravity center. So vara distance is going to be target dot Global position, dot distance to the
global position. Okay, so we just
pass that distance into the curve
modifier sampling, and then we take
the curve modifier and multiply that by the Delta. So this will be some value
between 0.0 and one. And that should affect the
gravity based on this curve. The closer it is,
the stronger it is. And over here, it gets weaker. Feel free to customize
a curve if you need to change the shape of the
animation a little bit. Okay, so I'll hit play here and we'll kind
of test that out. It's a little hard
to see because maybe I made the max
distance here too big. So what you can do
is you can check the collision shape and
see what its radius is. So 27 pixels. So we're basically making a
curve for up to 100 pixels, but the max of this radius
is 54 pixels, 27 over here. Oh, really, no, no, no, 27 pixels because it's the center
to the outer point, right? Not the diameter, of course. Yeah, I might want to take the radius here and
just make it 30 pixels, a nice even number, and then we go to
the gravity area. And then let's take
the curve point here and move this to
30 pixels, as well. Okay, so that'll kind of be like our max distance on
this shape over here. It doesn't need to
be perfectly 30. Like, roughly here so we can see the shape a little better. Okay, now if I hit play
and we go over here, it should be a little
more accurate. Okay, now you can
see the gravity really ramping up
as it gets closer, so it's slow on the
outside, almost too slow. So I might actually pull
this up a little bit, and you can adjust the shape of the curve by pulling
the handles, whatever you need to do.
See, I kind of like that. We'll hit play one more
time just to test. And yeah, I think that's
right for gravity. So there you go.
Scaling gravity. The closer your object gets
to play, the faster it moves, and it'll suck up all the
pickup items or really whichever areas you happen to mark on the mask layer here. Which in this case, right
now is just pickups, but you could do that
with other areas.
37. Building a Player UI with HP and EXP Progress Bars: Okay, so although the main
combat is working in the game, we can pick up items
like EXP pickups, but we have no UI indication
of what's going on. We can't see our
character's level, our HP, our EXP bar. So we're going to
start by working on EXP bar and adding in
a level counter to, let's say, the top
left of the screen. And we're going to do
that on a new node. So I'm going to go up here to the middle section,
add a new scene. And we want this to
be a user interface. So I'm going to call
this player UI. Let's save it into the UI
folder. And save it in there. Then I'm going to click
on the player UI, and we'll add in, let's
say, a panel container. So this is essentially
going to be the background for
our main player UI. Now, you say that the player UI stretches across the
whole screen by default. So we want to go into
layout on the right and change Anchors preset from
full wrecked to top left. Okay, now, that'll
shrink the size here to basically nothing. And then we want to
do container sizing, expand on horizontal
fill and vertical. Then under our panel container, we want to right click
and add in a new, let's say, a V box container. A VBox container organizes
its children vertically. So if you put two items in here, the one that is directly
under it will go on top of the other in
terms of its visual layout. So let's right click
on the VBox container, and now I want a HBox container. So our HBox container, the first one, will
go left to right, and this will be on top of any other children
HBox container. So it helps us to
kind of lay things out almost like a
grid, but not exactly. Okay, and then as a child
for the HBox container, we want to add in a
texture progress bar so you can search Progress
and you'll find right here. This allows you to,
in the top right, set textures for
the under texture, the over texture, and
the progress texture. So as the values fill up, it will actually show more or less of the
progress texture, and then under it will
be the base texture, which shows in the
background always. Okay, and then in
addition to that, we want a label to
the right of this. So I'll right click on HBox
container, add a label. A basic label should
be fine here. We can always change it to
a rich text label later. Okay, and now for this
texture progress bar, we want to go find the
art for our progress bar. So let's minimize everything on the bottom right and
expand the art folder. Let's go to Crimson Fantasy GUI. And I think it was
under GI Sprite here. So in the top right for under, I'm going to create a new atlas texture and then expand that. And let's drag GI Sprite
to the atlas here. So we can see the full atlas, pretty cool Gothic art
style going on here. And we can select what we
want our undertexture to be. So the under texture
is going to be basically our bar where it
is completely unfilled. So if I edit region, I can zoom in here, and I'll select this,
but right here, like so, that can be our
under texture hit close. And then for the
label let's just say zero out of 100 HP for right
now, just as a mock up. So now it makes sense to
rename this HBox container, something like HP display. And let's duplicate it, and then I'll rename the
second one to be EXP display. Go to the texture progress bar. And in textures, we're going to select a different
one from Edit region. So let's see what do we
like here as a EXP bar? We could use this
basic one over here, but I think it might
actually be kind of helpful if we could
see different chunks. So with this one, we could
see out of seven how close we are to
actually leveling up. But you'll notice as soon as I change the value
here for this one, the top one automatically
updates as well because we're using a shared
atlas texture resource. So what we actually
have to do here, I'll close is right click on this texture and
then do make unique. So when you do that, this
texture will be unique. We need to go back to the
first one and fix it, right click and then
do make Unique, and we need to change the HP
one back to its HP texture. So over here, we'll
select that HP bargain, and now we can see that these
are two separate things. Okay, so now let's also do
the fill in for the top one. I'll go to progress
here, new Atlas texture, and then we can quick load from the drop down menu,
our GUI sprite. And now we want to edit region, and we want to grab the
filled in health bar here. Okay, so that'll look something
like that and hit Close. Now that we have
that texture set, if you go to, let's say, value here and you type in 50, you'll see that part of the
health bar is now filled up. But you also see that
with this setup, this may partially be because the heart is kind
of hiding things, but it doesn't really
seem like it's accurately reflecting 50% of
your total health here when the value is 50. So to get around that,
what I was doing is changing the men value to be something like negative 20 to kind of offset this
over to the right. So now you can see there's four visible health
chunks out of eight. So that 50 value makes a lot more sense when you have that negative 20 offset there. So you might just need to
kind of customize that. So obviously, when you
characterize zero health, it's going to show zero is the value which
will be over here. But what really matters
is like the 50 out of 100 here and the 100
out of 100 there. So just to make sure we do value 100 for fully filled
in health bar, and that looks good.
We can say 20. We can still see our health. And even at five or ten, well, I guess at five, we don't
have to see it necessarily. But like at ten, we can
still see a tiny sliver. I think, as it is right
now is pretty good. So we can actually just
leave that default as 50. And let's work on the
EXP display texture. Let's just right click on
the base under texture here, copy, and then right click on the empty
progress and paste it in. Then we want to right
click here and make this unique and change it
to the value we need. So Edit region, and we want to grab this filled in
version of that bar. Okay? And we can type in 50
for the value for testing. Okay, and then we can see that's halfway filled in.
So that's fine. We don't need to do
a negative min value on this version of the texture. There's a bit of an
issue here where our progress bar
for the XP is red, just like the one on top. So what we actually
want to do is so that this metal portion is
tinted a different color. So how we can do that is
by going over here to the progress tint and changing that to more like a
green, kind of like that. But you can see
that that actually tints the entire texture. So what we want to do is right click on the under texture, control copy, and then paste
it on the over texture. Okay, so now only the pixels that don't exist
on the under and over are going to be affected by the progress
texture change. So whatever color you
want the progress to be, you can just change it to that by just kind of
manipulating the tint here. Can change the
darkness, and that is probably good enough
for your XP bar, to be honest, as the
simplest solution without using a
shader or anything. So if we go to the label now, we want to change
this to say EXP. So zero out of 100 EXP. Okay, and then we
want to vertically center these texture
progress bars. I think you can select
both of these at the same time if you hold
Control down and left click and then go to layout container
sizing and change it from fill vertical to shrink center. Yeah. Okay. And then that
basically takes these, and it centers with respect
to the other elements like this label over here on the right. So that'll look good. Then we just want
to shrink the text. So what we might
want to do actually, is just go to the player UI
at the root here and change its theme if we quick load from the drop down
menu our game theme. Then it's going to have
that pixel art font, which is already by default,
a little bit smaller. Okay, last thing for
wrapping up our mockup, we probably want a
margin container as apparent to the
panel container. So right click on the roots, player UI, add a
margin container. So search margin, and then pull panel container under that. Then now in the
margin container, you can go to theme overrides
on the right constants, and you can customize a margin. So I'll go with five for
all direction margins. Okay, kind of like that. So that's probably a
sufficient margin, but I actually got
the ordering wrong. The panel container goes
under the player UI, and then the margin container goes under the panel container, and then the VBox
container goes under that. Okay, so the point is that we
want the panel container to stretch the background
image to the edges here, and then we want everything else to kind of be
contained within that. So that's why the
margin container has to be within the
panel container, so the panel
container can stretch out further than the
margin container. It's just a ordering
thing. So that's going to pretty decent. Let's put it into our world. So on our world scene, let's create a canvas
layer to put the UI under. So right click add a
Canvas layer node now. Okay, and the reason
you would have a canvas layer here is it gives you a separate rendering area from everything
else in the scene. So the UI is rendered completely independently
of the Toti game world. And that would be important
if you have, say, Toti game lights, and you don't want that to
affect the UI at all. You want those to be
rendered separately. So the lights for the
world are different from if you have any lights for the UI, that would
be a separate thing. And you just kind of
manage them separately. So I will now want to
add the player UI here, so filter your files, search for player Underscore UI, if that's what you
gave as the name, Dragon drop the scene
into the Canvas layer, and it's going to pop up right there in the top left of our UI. Before we hit Play, make sure player UI is under
the Canvas layer. That'll be important
so it doesn't also render in the game world, but rather as part
of the screen. So it's there in the top left. It's actually quite tiny. That's pretty hilarious,
if I'm honest. We can either upscale the UI here or we can make
it bigger in the base scene. So maybe what I just
want to do is take the canvas layer here and
we'll just transform, increase the scale
to, like, four X. You can zoom out. Look
for the purple lined box. You might actually
have to hide, like, the ground world for
that to actually see it. Okay, there. So we have
the purple box here. This is the full view port area. So your UI here is kind
of with respect to that. So at four times scale, I think that works pretty good. Now I'll show the dirt
and the grass again. We'll hit Play. And let's see if the UI is there
nicely on the top left. And I think that size
is actually pretty decent. I'm liking that a lot. We'll go with that for now, and then we'll work on
scripting it out.
38. Implementing a Stat Based UI System with Signals: So for this UI to actually work, we need to hook it up
to signals that are going to relate to our
character Sp block. So that means we need like EXP changed signal or a
HP changed signal. We want to make sure
we update that. We also have another
minor issue, which is that basically this UI is very big on the screen
area where it's at currently. So I want to quickly add in
a script where I can just make it so the Canvas layer will automatically show
when the game starts, but we can optionally just toggle off when we're an editor. So I don't want
to always have to manually turn on the visibility. What I'll do is
I'll right click on the Canvas layer over
here, attach a script. So this would just be
canvaslayer dot GD. Let's save that in the
UI folder Bin. Create. And I'll just say function
underscore ready. It's going to return void, and then we'll say
visible, equals true. And that is all
we need for that. So now we can hide it
when we're looking at our map view
here and hit Play. And it should still pop up
as soon as the game start. So now we can hide it
from our map view here, hit play, and it'll still show as soon
as the game starts. So in the top left
corner, there it is. So that will be helpful
while we're editing. Another way you could handle
this would be you have a completely separate scene for your Canvas UI when
you're editing that, then your world scene,
and you combine them together in
one parent scene. When you actually load up the game world,
that would be fine. And maybe that would be
the long term solution. But this is just a really
nice, quick and easy fix. So let's work on connecting our player UI to
the player stats. So let's jump into
the player UI scene. I'll right click on player UI. And attach a script. So the playi dot gD can extend Control
and inside of here. We'll just at an
export R and we'll say context of the
player context. Since we already
created that resource, it's super easy to use. And the inspector over
here to the top right, just quick load the
player context. And if you remember from
earlier on in the course, the player is going to
automatically set itself there, so we can just grab the
context dot player dot stats, and boom, we have a
reference to the stats. We just need signals in order to update the HP display
and the EXP display. Okay, so we can
look up the stats. Let's see, stat
controller dot GD. If we jump into that script,
we have a live changed. Let's create some
signals for HP changed, Max HP changed, and
experience changed. Okay, so signal Mx HP changed. And you can see that this is
a lot of stats that we're going to change with
a very similar type. We might want to pass the
object that was being changed, the old value, the new value, and the change amount. So we can actually create
a class to encapsulate all of for any of our stats
that are going to change. And this is a very stat
based game system, survivor like,
right, because you might have cool down reduction, you might have player
movement speed, HP, attack power, all
this kind of extra stuff. And those are all stats that
could be represented inside of one object that we
can pass into an event. It's also much easier
if you just have one parameter so that you send that parameter
and then they receive it as one piece
rather than three or four. So to show what I
mean, let's create a new script in our file system. So I'm going to
collapse everything, and then we'll go into let's say objects because
objects have stats. So I'll right click,
create a new script. So this is going to
be data that goes on a signal for whenever
a stat changed. So a pretty clear name would
be stat changed data dot gD. We create that. Okay, and
let's open up that script. So stat changed data. Class name, stat changed data. And this is actually going
to extend from ref counted, which is a object that when
it's no longer referenced, it will automatically be
deleted from the game. So references. And instead of extending no, this is actually going to
extend from ref counted, which is a Gadot object that keeps track of how many
objects are referencing it. And when no objects
reference it anymore, it'll basically queue
itself up for deletion, so you don't need to
manually free it. It'll just be picked up
and deleted by the engine. That, by the way,
is also the default for when you don't
extend from any class, that's the default
class as well. Okay, so our stat changed data, we want to give it an
initialization function, so function underscore a NIT. And when we create
an NIT for a object, we can pass on whatever
parameters we need. So I'm going to say P object, the object that we are
affecting with our stat change. So no TD, the P Nu value, which I guess will be an
integer for right now. There could be a float
stat, in which case, you could change this
type to a variant to support both
integer and float. But I don't think right now
there's actually going to be any floats going on in the game for stats like
experience and HP. We've made those a
concrete integer, which is probably
preferable anyway. So, the new value
we're passing in, and then we want the old value. And when we have the
new and the old value, we can automatically
calculate the change. So let's put in the state
properties for this object. I'll say var object is
going to be a node two D, the var new is going
to be an integer. The var old is going
to be an integer. The var change is
also an integer. So when we initialize
the function, we just take the
parameters and we set the local variables
to those values. So object is going to
be equal to P object. A new is going to
be equal to P Nu, old is going to be
equal to P old. Now that we have the
new and the old set, our change is going to be equal
to the new minus the old, I think that is unless
I have it reverse. Okay, so if the old value is
30 and the new value is 20, then the change is going
to be negative ten, which is what we want here. So yeah, new minus old. So essentially what we're
doing here is we're passing these four parameters into the signal as one St
changed data object. And then on the signal receiver,
the callback function, we can access everything
inside of St changed to theta just by doing St
changed data dot object, dot new dot dot CAGE. And we might not
need all of that, but we have it accessible. Also, if we ever add another
thing here like var Cookie, which is a texture tue
or something like that, then we don't need to add
this as an extra parameter. It's contained inside
of this class. So you can modify this object
while not having to change your callbacks or the
sending signal parameter because you're just
sending one object. Hopefully, that makes
sense. It's easier to kind of modify as you go. Anyway, we have our
stat changed data. So if we go back to our
stat controller script, now we can create our signals. So Max HP changed
is going to have the data of the
stat changed data. Okay, and then we just do signal for the other one signal, HP changed data, stat changed data, signal
experience changed. Data stat changed data. Now, the reason I would
put a signal for each of these is because on
the receiving script, we can choose which stats
we actually care about. So, for instance,
the EXP display over here only cares about
the experience changing. So we're only going to
connect to the experience changed signal and maybe a level changed signal as well to update the level label if that goes
in the same display area. So we have those signals.
We just need to emit them. So what I was doing
before with, like, the HP setter here, if you're just going to
emit the value changed, I think that's acceptable use of a set so we emit that the value changed and just kind of make it an
observable property. So I'll say here,
whenever we set the HP, we'll just say HP
changed dot am HP. Now, if we're getting set to HP 100 when it's already
100 over and over again, we might want to skip
that signal emission, so we can say if HP
equals the value, then we just return because we don't need to
update live here. We don't need to update the HP because it's the same value. So following that pattern, I'm going to basically do
the same thing with MAX HP. So we'll do colon.
Next line, set value. If Mx HP equals
value, we return. Otherwise, Max HP equals value, and we emit Max HP changed. Oh, but down here, I forgot this is actually not
just the at HP value here, but we actually need to create the stat changed data object. Data is equal to stat
changed data dot new. And we're going to pass in this object reference up here
for the stat controller. So object, and then we do the new value of HP and the old value, which
we need to get first. So before we set the new value
on HP, right before that, we do var Old equals HP, and now we put the old in here, and we pass the data to
the signal emission. So HP changed dot MMI with the data, and that's
all we need to do. And now it's going to have
those four data properties inside of that object
instead of just one, but it still looks
super clean because we're just sending one object. Okay, now in MX HP, we want to do that again. So we'll say var
Old equals Max HP, and then we'll do far data equals stat
changed data dot u. So do the object, the Max hP and the old, and then we emit the HP changed signal with the data,
and that's that. Okay, so following
the same pattern, once again, down
here in experience, colon set value,
we can still guard against if the experience is
the same value just in case. So if experience equals
value, we return. Otherwise, we get the old
old equals experience, and then experience
equals the value. And then Vara data is
equal to stat changed data dot w. Object experience old, and we emit experience
changed with the data. Okay? And I might add this. Oh, yeah, okay, forgot the D there
at the top for the signal. Control STA, and
that should be good.
39. Scripting Dynamic HP and EXP Displays for Player UI: So we have the signal setup, and we have our context
here and the player UI, which is going to allow us to grab those signals
from the stats block. But we don't really need to do that directly
in the player UI. We need to do that in the HP
display and the EXP display. So I want to create scripts for the HP display and the
EXP display down here. So I'll right click on HP
display, attach a script, and we can put that in UI, so hp display dot GD. Great. We can just make
this extend from container. We really don't care about
what type of container it is because you can create a
display using other types. Maybe you display your HB vertically rather
than horizontally. So you can use a
different base class, like container rather
than HBox container, and you can still then make the actual node in
HBox container. So just keeping things
flexible is the idea here. And I'll say class
name HP display. Okay, so in our HP display, we need a reference to the stats of the player controller. We could export the
player context and then directly get that here
for each of the displays. However, if we set
things up like that, for each of your
sub UI components, if each of them have an export property that you have to set, then it becomes very
hard to drag it into the scene to reuse it because then
you have to, like, right click on it and make it
editable and make sure you assign all of the individual properties
like player context. To every sub UI node directly. So another option, which
might be preferable, is to have the root player UI handle injecting
the player context or what's relevant from it, the stats controller into
each of its sub UI nodes. And then you only need
to make sure that the player UI knows
about its subcomponents, like the HP display
and the EXP display. But then you let the player
UI handle the heavy lifting. So how that would look like in the context of our HP display, would be that we need our
local stat controller. So if our underscore
stats stat controller. So we make this local, right? And then we'll create a setter
function so that we can set the stats controller
from the player UI. So say function set stats, and this is going to take a P
underscore stat controller. Which is the type
stat controller. I'll return void. I put the P underscore for parameter so that
it's very clear that this variable is a
parameter of the function. In case you have another
stat controller up here, there'll be no confusion between our local reference to the stat controller and
our function scope, P underscore stat controller. So we're going to use this set
stats function to do that. So do underscore stats is equal to P underscore
stat controller. Okay, so we have our
set stats method. It assigns the
stats to the stats, which is private here
because of the underscore. Hover, this is GD script, so it's not truly private. It's just marked that you should not access this from
outside the class, but you still can technically. So we have our
stats setter here. We want to connect
to the signal now. So this is the HP display. And I want to say stats
Hp hanged dot Connect, and we'll say on HP changed. And we can also say
underscore stats MxHphanged dot Connect. And this can just be on
underscore on Max HP changed. So technically, both signals
have the same parameters, so we could just use one
callback function if we wanted. But to keep it flexible, maybe there would be some code that we'd want to
do differently, depending on if it's
MaxHP or the HP changed, I'll just have two callbacks here, so function underscore. On Max HP changed, which is going to
have the P data, the stat changed data,
this overturn void. And what we want to
do is refresh LUI. So I'll just create a
refresh function here, and then we'll do the same
thing for on HP changed. So we can just copy this down here and then
remove the Max bit. So delete four times.
Okay, that's that. Now we need a refresh function. So we'll say function refresh. And what do we want to
do on the HP display? Whenever the HP changes? We want to update the slider value and we want
to update the text label. So we want to basically
get the percent healthy for percent health, and that is going to be a
float equal to let's say, underscore stats dot HP, divided by underscore
stats dot MAX HP. Okay, then with the
percent health, we can assign that to the
texture progress bar. So you can see now we need a reference to the
texture bar down here. So the only time we're going to be setting up the reference to the texture bar is
like the first time we're creating this
HP display scene. Maybe we save this to a
separate scene on our project, and the main scene that
we actually put in the world is the
player UI scene. But I think it's
perfectly reasonable to export the texture
bar and the label here. There's other ways of
grabbing these, as well. But that's the method I'll use. So I'll say at
export var progress is a texture progress bar. And then at export
var label is a label. So when we're refreshing, we can take the progress
and take the label, and we'll say progress dot
value equals percent health. So let me see. That should
be on a scale 0-100. And this calculation is
actually going to give you a 0.021 0.0. So we want to multiply
this by 100 to get an actual percent so times 100. And I'm not sure about the
order of operations here. So I'm just going to wrap
this part to make sure that this executes before
this with certainty. Okay, and then the progress
value is going to be our percent health
0-100 as a float. Then we need to set the
text value on the label. So label dot text is going
to be equal to the string. We could just say in
quotations HP, percents, out of with a forward
slash percents, and then we replace
those values with a percent sign and pass
it on an array of values. So the values we're going
to use are going to be the current HP and the MX HP. So in other words,
underscore stats dot HP and underscore
statts dot MAX HP. So if you want this
to be customizable, we can cut that out
with Control X and replace it with something
like health text template. Then up here at the
top, we could say at export bar Health text template
is going to be equal to, and then Control V paste that. This still has these percent
as symbols for replacing. So we still do Health
text template percent, and then the array of
the two values in order, which is underscore
stats dot HP, and then underscore
stats dot max HP. Okay, so that will
update the bar, and we'll update the text. So we also want to call this
whenever we set the stats. So I'll say refresh up here to make sure that
after we set the stats, we immediately update
that text, as well. We need a colon up here
to finish that function, and that is pretty much good. Now on the right hand
side for the script, we do need to assign the
progress and the label. So assign the texture progress
bar and assign the label. Make sure you're selecting
the ones under the HP bar. Now, that's going to be
good for the HP display. We almost want the same
script for the EXP display, but there's a
couple differences. So I will control A, Control C to copy
everything in this script. We'll go over to
the EXP display. Right click, attach script. Exp display dot gD
sounds good to me. We'll change the inherit type
here to container, though, and then just select everything and replace it with a
Control V pasting it in. Now we need to change the
class name to EXP display. Our HealthText template. We're going to select
that Control R to replace it with
EXP text template. This Health text template,
select the text. You can double click on it
to select the whole thing. And then do Control R. Down here, we're
going to replace Health text template
with EXP text template, replace all, and then we
want to change this HP right here to EXP as
our default template. The last thing we need to do
is change the connection of the signal to
experience changed. We can completely get rid
of the Max HP changed, and then replace this
function name right here on HP change so you can
select it, Control R, and then down here, we
want to replace that on EXP changed or experience changed, whatever
makes sense to you. We'll replace that.
Get rid of the on Max HP changed data here
to wrap this part up. Let's not update the
progress bar yet, so I will comment those out. So for right now, we'll zoom
in here and I'll replace the experience text template with underscorests
dot experience, and I'll put experience
over here again, too. So it'll say like one out
of one or two out of two, but at least we can
see it working. Now in the top right, make
sure you assign the progress. To the texture progress bar, the label to the label. Then we just need the HP
display and the EXP display to call their set stats
function from the player UI. So inside of the
player UI script, let's at ExportvRHP display
of type HP display at Export var EXP display
of type EXP display. And then function
underscore ready, we're going to call hp display dot set Stats to
context dot play dot stats, and we need to get that
twice so we can even say var stats of stat Controller, then we set that
equal to this bit. So I'm going to cut
that, go over here, equal sign, paste it in. So we set stats on the stats. We do the same thing
with EXP display XP display dot set Stats. And then we do stats. Last thing we need is to
assign the HP display and the EXP display and the inspector so that
we have that reference. So click on player UI, go to the top right,
assign the HP display. And assign EXP display. Now, really, the HP display and the EXP display can be
and probably should be their own self contained
scenes that we instance a copy of
in our player UI. So I'm going to right
click on HP display, and we're going to
save the branch as a scene inside of our UI, and then right click on EXP display and do
the same thing. Save branch a scene
inside of the UI. So now we can jump into these
and individually customize these little displays as we need in the context of our
greater player UI. So we have scenes within scenes, and then we take our
player UI scene, and we put that into
the game world, which is invisible
right now because the Canvas UI is hit.
But yeah, there you go. And if we at play, we'll see if anything
linked up properly. Okay, so set stats
and HP display is null because I forgot to
assign them in the inspector. You can see right here
these are null on ready, which means I forgot
that at export. Also because this is checking, we don't need any asserts
because it's kind of implied here that
this is required now. So jump into the player scene, click on the player UI, assign the HP display
and the EXP display. Anyway, now we can play,
and we'll test it again. So we have ten out of 100 HP. If I recall looking
at the stats, our Max HP was 100 and
our base HP was ten, which obviously we get hit here. You can see the HP
updates to zero. The EXP progress bar not working yet because
we need to set up our level E experience
requirements, which is going to create, like, a whole hierarchy
of leveling up. But let's try real quick, setting the HP to
50 on our player. So I'll go to our player scene, and then I'll look inside
the stat control here. And we'll set the HP
to 50. Let me see. If I do that right there,
can I tap back to the game? No, because the players
already removed, I think. But by hit play, we should see the HP bar update here with the right progress
value, but it does not. It shows the right HP value, but we have to fix the
HP display real quick. Okay, so in the HP display, let me see what
was going on here. The progress value is equal
to the percent health. Let me set a break point
here, and I'll hit Play, and we'll see what
is actually setting that so the percent
health is 0.0. Ah, right, because I'm
dividing with integers. So when we divide
by two integers, we're going to get an integer, which is going to
automatically go to the floor of zero. But
that's not what we want. We actually want it
to return a flow. So what we could just
do as a quick fix here is just say float on the Mx HP. So now this is going to be an integer dividing by a float, which will return a float. If we hit play, then we should see the
right value go there. So now we can see
percent health 50. If we hit play, the bar is
showing properly there. Minus the offsetting for
the value. So let me see. Did I offset the value
there, HP display? We have the texture progress
bar, and for some reason, it's up to 00 here, but I want this to be negative 20 so that when the value is 50, it properly shows in
the middle there. You can see right there this
is like four out of 8 bars. And the value over here is 50. I must have unset
that at some point, but the kind of hackish fix
for that is you just make the min value more out to the left so that 50
ends up in the middle. Because if this is
zero over here, then 50 actually ends up
as, like, the third bar. See, I make sure your men
value is negative 20 there, and that should work
for the HP display. Okay, so final test here, we hit play progress
bar being set to 50. I can undo that
breakpoint, hit play. And we can see our
health progress bar is correctly in the
middle, right there. Our HP is showing us 50
out of 50. Let's get hit. You can see our HP drops, the UI immediately
reflects that, including the progress bar. So all of our signals are
hooking up correctly. We're getting the
experience set there, and we just need to add in our leveling up definitions so that we can level
up our character. Before we completely finish with the video on our
set stats function, there is one flaw here, which is that if we ever reset
the stats with set stats, and one is already set here, we'll end up with
duplicate signals. So we want to say here, if Underscore stats is not
null before we call set Stats, then we want to disconnect
from the signals, so we can say underscore stats dot MxHphanged dot disconnect. Underscore on MAX HP changed. And same thing for
the regular HP. So we just copy that
and do SATS HP changed, and then on HP changed. This is more of just
making it robust. I never intend to set the SATS controller to something different, but just
in case you do, this will make sure
that it doesn't cause a horrible bug where you have two signals connected for two different
stat controllers, but on one HP display. Now, do the same thing kind of over on the EXP one as well. So if you look at
the EXP display, I'm just going to paste that in and we change
the signal name. So sats experience changed, and then on EXP changed here. Okay, so that's what
that should look like. Underscore SATs
experience changed disconnect on
experience changed. Okay, so that should be good for if you ever changed
the SATS controller. Just wanted to show that to be a little bit more complete.
40. Debugging and Optimizing UI and Pickup Scripts: I wanted to clear up a
few warnings and errors. I've been getting in the console
with the project so far. So if you've been copying
the code word for word, you may have run into
these same issues. So pickup two D
where it says you can't use monitorable
equals false, normally because it's blocked and you have to wait
for set deferred. So if you double click on that, it'll point out that in
the remove function, I was trying to set
monitorable to false. Well, actually just going
to remove the monitorable equals false here because
when you Qu free, it's removed at the end
of the current frame. When you call set deferred, it's removed at the end
of the current frame. So making it not monitorable at the same time you free it is not really going
to help anything. The point was to
make it so that if two objects try to take
it at the same time, that it would not pick up twice. However, what we could do if
you want to be extra safe. However, if you want to be
extra safe and pick up two D, let's give it a var picked. And this will be a boolean
that sets to false by default. So I'm just implying that
it's a boolean there. I mean, if you want to
declare it for sure, then you can do that.
So this is picked. And if we try to take when this is true, then we'll allow it. Otherwise, we'll return false. So what I'm going to do with Tr take is I'm going to make
this actually guard, and then we'll have a
separate take function, which will be what the
pickups actually implement, like the virtual function,
what they currently do. So I want to do
function, underscore T, and this is not going
to return anything, so that's a return type of void. And I'll take this push error
and bring that down here. So I'm going to
return true here now. I'm going to change this to be underscore T. So we call
the virtual function, and then we're going
to guard against picking up a second
time up here. So we'll say, I
picked, return false. Okay, so there's our guard. And otherwise, we get here. After we take, we do
picked equals true. And I'm putting that in here
so that we don't need to worry about that Boolean
and the inheriting classes. We just need to worry about implementing the take function. So I think this is
a better setup. It will work well
with ir quarter, and as long as we set picked
to true after calling we're sure that it won't pick up a second time because
we have this guard here. So now we just need to update
our other pickup scripts. So search pickup. I think we
only have EXP pickup so far. So I'll double click into that. We're going to rename
this to underscore Take the return type is void. And instead of
returning false here, if stats is null,
we'll push an error. So push error. Percent has no stats
stat controller. That's going to be
percent P target. So we replace this with
a node name there. And then we don't
return at the bottom. We just call remove.
And we need to make sure that we match the
parents signature. So I probably need to pass the P target to
the parent script. Okay, so let's go up. And yeah, we want to pass that P target. So P target we'll remove the underscore there because we're actually
using it now. And then in here we do underscore
P target is a node two D. Now that should clean up the errors in our
EXP Pickup two D. And just to verify,
we'll go into play mode. Okay, then the other
warnings, let's go to that. In most cases, these are
just unused parameters. So, in the case of something
like pickup two D, you see that P target is unused, so you just want to mark that
with underscore to make it so that it's marked as
being unused intentionally. Let's go to the next one here. Camera size was never used. So the camera size was used actually to create
the camera rect. So it's actually not
needed here because in this calculate
final spawn function, we used for the spawner system. The camera rect gets created, and then the size is
pulled from that. So we can actually remove camera size as a
parameter, cut that out. And then where we
call that up here, just remove camera size
as a parameter as well, and we simplify it
to three parameters. So that would be a
good case where that warning actually
kind of helped us. And then, next here, if you have an update that
doesn't use the Delta, which would be the case if you doing move and slide because move and slide doesn't
need to do the Delta, you set the velocity,
and then move and slide automatically
handles with the Delta. So we underscore the Delta
and that's it there. And this drop script,
the name drop here is shadowing
the function name. So we could change the name here to something
like drop instance. And then wherever we
use drop under it, like right here, we say
drop instance instead. You just want to make sure that if you use a variable locally, you don't have that variable
also declared outside of the scope in the class base, like the local variables. And you don't want it to have the same name as a
function name as well. Then in each of these
stat changed signals, the callbacks for that, I was just refreshing
whenever the value changes, and I don't actually need to use the P data for these
functions directly. So I would just
underscore both of those once more on the
EXP display script. Click there and underscore that, and that should get rid of all the current
warnings and errors. And I'll hit Play.
Okay, no errors for.
41. Leveling System with Experience and Stat Progression: Health and experience now, we need to add in some
kind of leveling mechanic, and we need to have
a leveling stat on our stats controller in order to manage
the leveling up of our player and possibly
other characters as well, but definitely for the player. So we're going to go into
the step controller, and I'm going to add in
a new variable here. We'll say is going to be
an integer equal to zero, and we'll have a similar setup to experience down here
where we'll just have a satur value where we'll say I level is equal to the
value we'll return, otherwise, we'll just repeat
the same pattern emitting the signal for level changed so that we can
respond to that in our UI. So we'll say var
old equals level, and we'll say level
equals value. We'll say var data
is equal to stat changed data dot u
object level old, and we'll emit a
level changed signal. With the data. Now, we need
to create that signal, of course, so copy level
changed up at the top. We'll create a signal
level changed data, stat changed data. So now we can subscribe
to that if we need it. Now we can fully
implement add experience. So whenever we add experience, we want to also check if we're
ready for the next level. I put that in here rather than the experience setter
because I don't want too many
random side effects by setting the experience, so I want to explicitly call
the ad experience method, which levels up if the
threshold is reached. Okay, so to do that, we're going to need to create a
level definition object. So let's create a
new script for that. Everything will go into
let's say characters. Right click here,
do a new script, and we will call it
level definition. So level definition dot gD. It's going to extend
from resource, create. Double click to open it up, and we'll give a class
name level definition. It'll extend resource,
and this will define a level up
for a character. So what we need here is going to be the
experience threshold. So at Export VR, we'll say experience needed. And this will be an integer. We could default to
something like ten. And we probably also want some stats that we're
going to level up. So for right now,
we'll just start with HP and keep it simple, but you would do
extra stats here later on as you work out
they became mechanics. So at Export var HP, we'll say that's an
integer, defaulted to ten. And we could say this is the
bonus HP for leveling up. So you hit this level, your
character gets this much HP. Now, how do we apply our stat
changes to the character? You could do it one
at a time manually, but a smarter way would be to create a method
to do it for you. So let's say function apply, and we will apply this onto a stats controller, let's say. So P stats of a stat controller. Then we'll return
void with this. So we want to basically
update the sets. So P stats dot HP plus
equals the HP amount. And before that, we also
want to increase the MX HP. So p stats dot MX
HP plus equals HP. It's important that you
do the MAX HP before the HP because the current
HP is capped at the MAX HP. So if a full health character
increases its HP by ten, it actually won't
gain the health unless the MAX HP updates first. So important ordering there. And that can be our basic
level definition for now. So in stat control, it
would be a decent place to define our level definitions. So I'll say at export VR
level up definitions. And this is an array
of level definition. Okay, so to check
for the level up, we need to get the next
level in the sequence. So let's make a function to get our next level if there is one. So down here at the bottom,
let's say function, underscore, get next level. Which will return a level
definition or null. And this is going to return the level up
definitions dot Git, and we're going to
get at index level. So the reason we're
getting at index level is because level is actually the
second place in the array. So if our character
is level one, we are currently
on the index zero, and we want to look at
the index plus one, which is level because
level starts at one. Okay, so to handle our add
experience leveling up, we first need to get the
next level definition for the next level. So like the level plus one, we need to get that level
definition up at the top. So we're going to at Export, not in a way, but actually
a dictionary here. So at Export VR level
definitions, we'll say. And this will be a dictionary of integers that are assigned
to level definition. So the integer is the level
we're trying to reach, and the level definition is the stat changes that
would have if it exists. So before we level up
or try to level up, we have to first see if there's
actually a level defined. So at the bottom, we'll create another private
function function underscore get next level, and this is going to return
a level definition or null. So we're going to return our
level definitions dot GET at the level plus one. Our current level plus one, we want to get that and
we want to return null. If we can't find it. So if we return null, then we'll skip leveling up. So in that experience,
let's do VR next is going to be a level definition
equal to get next level. So if next is null, we just return or you
could even push a warning. Actually, yeah, let's
push a warning. Character cannot level up
to percent a percent level plus one because that level
has no definition defined. So we could say string of
level plus one over here at the end and finish it with another parentheses.
Okay, that'll work. So, we take the level plus one. We're trying to level up to
level two if we're level one, and we're noting that we can't level up because
that's not defined. So you might want to
have that as a warning, or if you're sure you don't can just comment
that out or remove. I'll leave it in for now
so that we can at least see when we fail to set
a level definition. So the next level is null, so if next is null, then we'll return because
we can't level up. There's no next level defined. So we just skip altogether. And then we want to check if the next level
experience requirement is less than or equal to
our current experience. So we can put that as part
of a tri level function. So underscore tri level up, and we'll say on the next. So we come down here and
we'll have our function. Underscore. Try level up. Underscore because it's private, and we pass in the next
level level definition. Or actually, you know,
I feel like this should be contained within
that function, really. So this is going to return a boolean, and we'll
have this here. So we just get the next level
inside of this definition. And then we don't need
to pass in a parameter. We just try level
up after we add XP, right? It's just like this. So if we can't level
up, we return false. And then we want to check if the experience is greater or equal to next dot
experience needed. If that's the case,
then we'd level up. But I'm going to reverse that.
I'm going to say if it's less than the experience needed, then we return false. Okay, so we're out of
the if statement again. And then if we get to here, we've passed the guard checks, so we can say level
plus equals one. We'll subtract the experience needed from the
current experience. So experience minus
equals next level. Oh, sorry, next, dot
experience needed. And then we will apply the stats of the next level
to our current stats. So after subtracting the XP, we do next dot apply to self because we're working inside of the stat controller script, and then we'll return true because we successfully
leveled up. Now let me think about this.
I think I actually want the level to increase after the stats have
applied, like this. So if we're going to say, have a character
level up, and then we give them the choices
for the rewards. So picking an upgrade
at leveling up, which is a core mechanic
and our survivor like, we want to do that after the
base stats apply so that the UI will still update
basically at the same frame, but just before we level up and emit our
level changed signal, thus causing the UI to react and show us
our upgrade choices. I think that's important
ordering change there. Okay, so yeah, then the
level of change signal is going to get emitted up
here because of the setter. And that is basically
our leveling up. If for whatever
reason, you want to get the current
level definition, that might be useful
outside of the script. And this one might even be
useful outside of the script. So I'll take Get next level, and we'll remove
the underscore for pivting move that underscore
and try Next Level two. And let's add in a
function G current level. Definition or maybe just get
current levels clear enough. And then we'll return
level definition. So we will return the
level definitions dot GIT, and we want to return
the level or null. Okay. So those
might be useful for other scripts that want to look into what's going on
with the stat controller. So it makes some
sense to make those public. By not underscoring it. That's why. Okay, so we
have this stat property, and we can level up
whenever our EXP increases, which should be automatic
because this ad experience is going to get called from
our pickup, our EXP pickup. So everything is going to flow nicely if it's working good. Except now we need
to make sure that we're connecting to
the level stat and the level up signal or the
level changed signal, rather. And we're going to do that
inside of the EXP display. So we need to look in a project
for EXP and scroll down, find our scene xp display
dot tSCN, open that up. Okay, so now that our
leveling should be working, we need to go into
the EXP display, and we need to make
sure that it is showing how much EXP we
need for the next level. So let's go in here. And when we refresh, we want to get the
next level XP. We want to change this bit
right here to the Mx XP of the stats if the stats have a next level that
we can actually access. So you get rid of that bit and
say VarnNxt is going to be equal to underscore
stats dot next level. So we get the next level, and if there's no next level, then we'll say I next
is equal to null, then we want to change the
label dot text to be equal to, let's say, our max level string. Okay, so we need to
define that off here at export var max level string, and that'll just be
equal to something like max level in all caps. Okay, else, we're going to
set the text to our EXP, our current number out of
the next level number. So we've confirmed
that next is not null, so we can replace this bit
with next dot experience Ned. Now if I run the game, it
will probably just say Max level because we haven't defined any
level definitions. If we're at MAX level, then I also want to
take the progress bar and set that value to 100. So now if we run the
game, currently, that's going to give us an
error because I haven't set the progress,
so let's do that. So in our XB display, go over to the right and
assign the progress bar. Okay, now we can run, and we should see a
full progress bar. We're at MAX level. Okay, so picking up XB no longer matters. I can still good experience, but it's never going to
level up anymore. Because level definitions are assigned in the editor,
not during gameplay. Although you could do that, but that would be
pretty weird, right? Like just defining
end game that you can level up to now level 31, where before 30 was the cap, no, that's something
you do on the editor. 99.99% of the time. So we have the progress
value being set here. We're going to want
to get a percentage between our experience and
the next experience needed, and then we'll update
the progress value here for when we can level up. So our progress is afloat
and we'll set that equal to next dot experience needed divided by underscore
stats dot experience. Now remember, we're dividing
an integer by an integer, but we actually want
it to be a float. So we need to take
one of these and cast it as a float.
So say float. And then on stats
dot experience, we finish wrapping it
with our parentheses. So we do that, and this
should now give us somewhere between
a 0.0 and a 1.0. But this needs to be
basically converted into a percentage because
our progress bar by default has a
max value of 100, so we're looking at percentages. Either that, you need to scale
the max value down to one. Both are actually valid. So here we can just wrap this with another
parentheses and times it by 100 and that should
give us our actual progress. So we're still never going
to hit this because we need to define some
level up definitions. So let's go into the player, and we'll click on
the stat controller. We have our level
definitions here. So we can add some
dictionary key values. We want the next level of
two to be defined here. So add two and a new
level definition. Inside of here, we can just
say XB needed ten HP ten. That's fine for starting.
Add the key value pair. It's important that you
remember to hit this. Otherwise it won't be
in the dictionary. So we have our dictionary
with one key at level two and a level definition to
level up our character. Okay, one last little
tweak we need to do is to make sure that our player
defaults to level one. So let's go into the stat controller and
where we have level. We need to make
sure this is a one. So that way, when we check
for level plus one of two, we'll find that
level definition, and it should display
properly in the UI. So let's hit Play, and we'll
have our XB needed here. The progress bar didn't update, but let's see when
we get an EXP, that goes up, and it's
showing out of ten. And yes, simple error. I forgot to assign the progress to the progress dot value. So maybe it actually makes sense to change this
to a different name. We're shadowing a variable. I bet if I check
the errors here, you can see that yeah, I shadowed this with the
progress texture bar name. So let's say percent leveled
would be a much better name. And then we'll say
progress dot value equals percent leveled. And now I'm pretty
sure it should work. Let's Play. Go in there. We can see why EXP
didn't love up there, and now it's saying
one out of ten. That's interesting. So I
need to set a breakpoint here to see what this showing. And let's go into play mode. And we can see it's
saying 0.0 right now, but let's pick one
up and we get 1,000. Another simple logic mistake, I should be doing the
experience divided by the experience needed.
So we reverse this. I'll take the experience
needed and put this over here, and then I'll cut the
stats dot experience and put this over
here to the left. We could still make the float casting on the
right. That's fine. Okay, so let's play once again. Test that? So, okay, now it's showing 0% leveled up. We pick one item up.
We get 10% there, two, three, but it should
be going all the way. So I want to check the progress stop value here and see
why that's occurring. Okay, so here we
have 40% leveled up. So the percent leveled is right. So we might actually
need to check the UI outside of the game. So let's go into the
EXP display here. I'm going to test setting
the value to zero. Okay, that's correct.
100 is correct. It goes all the way
to the far right. Let's say 25 50, 75. Okay, that looks correct. So let's take the value to zero. All right, play. Let's
test that a little bit. Okay, remove the breakpoint. Go into play mode, and
let's do one, two, three. So that is 30%? Maybe it is correct, actually. Okay, so we get four out of
ten and then five out of ten. Okay, yeah, it was working fine. Let's get seven out of
ten. Let's actually just level of our character
to prove that it works. Okay, so we get nine out of ten, and then we have the
skeleton spawning. We can get one more,
ten out of ten. Okay, you saw we just
got ten HP here, then we went to zero out
of ten for the EXP needed. So the part about the next level should be returning
something, but it didn't. So the reason that occurred like that is that our experience
gets set to zero. It emits a signal, and then our level changed
doesn't connect here. So after the level is changed,
it doesn't refresh again. Okay, so the
simplest solution is just underscore stats
dot level changed, and we connect on level
changed to refresh again. So we can copy this up
here and disconnect it. So we'll create our
Callback function at the bottom function, underscore on level changed. We're not going to use the
data, so underscore P data. Stat changed data,
we return void, and we will just refresh. Okay, that way, after the
experience gets set to zero, right before we
increase our level, that will refresh the UI, but then the level changes
and we refresh the UI again. So let's show one more time that it's all working as intended. So I just will get enough
experience to level up here. So for and then, five, six, seven, eight, nine. Skillsins don't actually
drop anything yet. We'll fix that
soon. And then ten. Okay, so now it shows max level, but we also increased our HP. So that refreshed our UI twice. Once when we changed our stats, decreasing the experience and another time when we
increased our level, which now will show the
next level does not exist. So we show Max level at our
EXP bar as we would expect. So everything seems
to be working.
42. Setting Up XP Thresholds and Signal Connections for Level Up Rewards: Point now where we
would want to add in a rogue like upgrade system. So we level up our character when our character hits
a certain XP threshold, and then we get to
select from one of three options for how we want
to upgrade our character. And this is chosen
from a random list of available options. So for convenience's sake,
before we get into this, I want to change
the XP requirement, fitting Level two to one, just so we can trigger
it very quickly. So if I go to the player SAN, we'll open up player dot TCN, and then let's go to
the stat controller, I believe was where
we were defining it. And the level definition, we have the first level
here set at ten XB needed. Let's change that
to one real quick. And then maybe I'll add
in a couple extra levels. So we can do a Level three
here as the new key, and then let's do
a new definition. We'll just make
that one need ten. So we'll add that
key value pair. You can add as many
as you'd like here. So this is basically just a giant list of the
levels we're trying to set over here on
the left and the stat level up definition
over on the right. So level four, we
can make, I'd say, 20 XP needed, and it
boosts HP by ten. And just keep going
as you'd like. So level five, let's say
30 X P and L level ten. I'll just kind of flesh this
out so I don't really need to worry about it much
later. Let's do level seven. So new level definition. We'll say 50 XP, add key value pair, seven, and we'll do a new
level definition at 100 XP and add that in. And I think that should
be good mostly for our prototype having
seven different levels. Is definitely enough to
kind of test things. So we have those level definitions
set up on leveling up, it's going to emit
the level changed signal from our stat controller. So we want to connect
to that in our new UI, which is going to provide us the option of choosing one of three different items
or level up features. And we want the
game to be paused while our player is
making that choice. So let's create a new UI. I'll add a new
control node here, level up selection, I
think is a decent name, and we'll add a new
script to this. So level Uselection dotGD. We'll save that in the
UI folder, of course. So UI, and then open and create. Okay. Also, control as to save the scene next to
where the script is. So that's also on the UI folder.
I'll just save it there. Okay, now we'll kind of
start working on the script. I'll do class name
level up selection. Generally, unless something
is very specifically going to be a one time use script that nothing else
needs to reference, I'll almost always
create a class name, if that wasn't clear
by this point. Just having a referenceable name makes working between
scripts a lot easier. Okay, so the quickest way
we can get the reference to the player's stat
controller is going to be you guessed it. The
player context. So let's add export var, the context, which is going to be the
player context here. Control S to save that. And then in the inspector, we're going to quick load the one player
context of our game, and that basically gives
us the reference to the player and thus
the stat controller. Okay, and then in function
underscore ready, we want to go to the context dot player dot stats dot level Chang and connect to that
with on player level changed, which will be a new function
we create right below. So function underscore
on player level change. And this is going to have the
level change data object. So P data is what do they
call it Stats changed data. So we only want to actually provide the UI if the
levels actually going up. There may be some weird
circumstances where we decide that the level could
go down in the future. So to guard against that, we'll say if P data dot
change is greater than zero, that we know it's leveling up. Okay, so when the level changes, we only want to respond to it leveling up,
not leveling down. So we're going to say, if pdata dot change is less
than or equal to zero, then we're just going to
return here because it's not a condition we
want to handle. Otherwise, we're going
to level up the player. Now, for right now, I'm
only going to write the code to handle
leveling up one at a time, because in the context
of the current game, that's the only way
it would be possible. So I'm going to write at
least the prototype of the script to only handle
leveling up once at a time. If you for some reason picked
up a 1,000 EXP object, and then that would trigger
three or four levels up, that might be a condition
you want to handle later on. But I think that's a little bit unnecessary here because we're only going to be picking up
one XP at a time anyway. But for this script for
now, we're just going to be handling one
level up at a time. So let's create a function here. I'm going to call
it start selection. And then this function,
which I'll put above function start selection is going to provide us
with a three options. It's also going to show the UI. So we can just start
with a show call here. And then we also want to
pause the game so we can say get tree dots
paused, equals true. Okay, so since we're
pausing the game here, that means it's going to pause all scripts that are set to only be active when the game is not paused, which
is the default. So over on the right
here under node process, we need to change
the mode here to I'm thinking always so that it can always trigger stuff
inside of the game, regardless of if it
is paused or not. Because we also want
to be able to open the script when the
game is unpaused. So being able to call start selection during an
unpause game will be important. So I think making this
always actually makes sense. But we definitely want it to run while the game is paused. Later we'll need to come
in here and generate a list of rewards that we're
going to show to the player. We'll need to set up the
UI a bit for that first. And we also want to
make sure that this is tested so far so that
it's actually popping up. Let's right click
on the level up selection and add a couple
UI components to it. So I will start with, let's say, a panel container, and that'll let us see it when
it pops into the screen. Also, click on the root UI. And then we're going
to change, let's see, layout to top left and capreset over
here. So right there. I also want to make sure
that mouse does not stop the clicks
from going through, but we're actually going to
ignore it for right now. There will be things
we want to click on the UI such as buttons
to make the selection, but we don't want the UI root to block those mouse
and port signals. So let's see if we can get
this to pop up on leveling up. Let's go to the root and I'll add in the
level up selection. So level underscore up, and we'll put this
under the canvas layer. Let's temporarily show the UI. Okay. And we're going
to want that to actually be in the
center of the screen. So take the level up selection and go to layout
in the top right, and we're going to
center it over here. So center and if we zoom out, then this should theoretically be in the center of the screen. So let's go hit
Level two and game, and we'll see if that
is actually working. So So I'm going to go over here. The game did pause. I
don't see it showing up, but that is a good start. So at least we know that
it's triggering on Lavote. So I'm thinking what might
be causing the issue here is the Canvas layer
scale is set to four. Let's make that one like that. Okay, and then I want a
child for the Canvas layer, which I'll actually
use for the scaling. Let's right click
on Canvas layer and add in a control node here. So I'll take the other nodes, and I'll make the root. So this node will be the UI. Okay, we're also going to
need to take the UI control, the root here that
I just created. And we're going to take the anchors preset and
make that full rectangle. So now that's going
to stretch to the size of the viewpoint, we want to make sure
that mouse is set to ignore so that this
doesn't block UI signals. Okay, and then if we have the level up selection
here set to the center, we can actually see that's the real center
of the viewpoint. So here's the root
UI control node, and then this is in the center. So if I hit Play now and
we go and pick up a XP, then this would
show in the center, though it was
showing bit default. So I got to hide it first. And now let's hit Play. We go here, and you see it pops up as well as
pausing the game. Okay, so that script's working. We need to resize our
HPUI up here again. So I'm going to actually
click on the UI here and I'm going to increase
its scale directly. It might just be the most
appropriate to scale the individual
components rather than applying a global
scale to everything. Then we can just size our play up selection
appropriately. So another advantage of having the UI here is that if we
want to apply a theme to everything under is that
we can just go to theme in the bottom right and then
quick load the game theme. So now everything will be
using that Pixar font, even if we don't assign it directly to the
level up selection. So sometimes you do want fine grained control over
the individual themes. If you want a custom
theme for, say, the level up selection, then you would just create a
new one down here. And I think at the
lowest levels, it overrides whatever
is above it. So you can basically have
a hierarchy of themes like that because not everything needs to override every
single property as well. You can have a main theme, and then for very custom stuff, like how a slider bar looks,
you can make that like a sub
43. Building the Level Up UI Layout and Animated Text: We're going to work
on building out our UI template for the
level up selection. So we already have
a panel container. I think we should
stretch this out, so I'm going to pull on the bottom right hand corner
and move this a bit bigger. We're going to need
a much bigger UI to actually show our selection. Now we're going
to need a header. Maybe we show a graphic.
Like, congratulations. You leveled up
something like that. Then we need three
column displays. Those will be VBox containers, which will show our options. So that'll be probably
like a title text, a icon, a description, and then a selection button. And once the selection
button is pressed, then the level up selection
will process the selection, apply it to the player, close the UI, and
resume gameplay. So let's right click on
our panel container, and I'm going to add
a margin container as the next level,
add a child node. So we're looking for
margin container. And then in theme
overrides on the right, I'm going to take
these margins and set them each to five
pixels for now. That'll just make it
look a little nicer so everything's not as
clumped up together. Let's right click on the
margin container and now add a VBox container. So we can organize
everything else vertically. You can already see
the margin container, kind of spreading the edges
a little bit to the sides. We want to add to this
probably our title text. So we can add this as
a rich text label. Alright, click Add a
child Rich Text label. The reason for
making it rich text instead of a regular
label is that rich texts, you can do things like BB code, which is good if
you want to apply a rainbow effect or waving
the text up and down. So it's just more powerful
than a regular text label. I'm sure it
technically has, like, a resource cost
associated with it, but a few text animating on screen is not
going to lag you. So let's put it in here
level up as the text. We can see it's not
really popping down here. So we need to take fit content
over and check it to true. Now our text is going to
show up in the center there. We also want to
center this text, so we'll change the horizontal
alignment here to center. Okay, now it's centered.
Let's put an icon beneath this so we can show a
graphic four hour level up. So I'm going to right click on VBox container,
add a child node. Let's say texture rack. Add that and we can
select a texture. So any graphic we want to use
from our project will work. Let's see if we can
quick load something. So if I search, let's say 64 by 64 to get the highest
resolution texture, let's grab from those icons, one that we want to use and
then scale it up even more. So I actually wanted to use this as part of
an atlas texture. So if you did select it, right
click it, then go to Copy. And in the dropdown up
here, do atlas texture. Click on atlas texture, and then right click
paste the atlas end. Now we can select which icon
we want to use from this. So just anything you
think looks good, there's a lot of options. And this is the 64 by 64 texture sprite sheet
from the icons Pack. That was an obvious, so one of the three art assets we
have for the project. So something like
these twin swords or the shining sword
could work pretty good. Anything that would be like
a glowing human figure, kind of implying,
Oh, I level up. I got super strong, like, for instance, specifically
like this, or in the base project,
I used this one, which is more of a lightning,
but it could also work. Okay, so this time, I'll
go with the one where it looks like a giant beam of light rather than the lightning. And I'll hit close. So we have our icon showing in
the texture rect. The stretching isn't
really how we want it, so we want to change
the stretch mode to keep aspect centered, I believe is the
best option here. And that'll work pretty okay, I think, following
our level up icon, let's add in a text block, which we'll say to the player, we want to make one
of a few selections. So let's right click
on VBux's Container, add another rich text label in and then we'll do
something similar. Check fit to content. And then for the
text, we'll say, pick one of the level
up options below. Since we do intend for the
Pixar font to show up here in our actual display rather than go over to
the world every time, to check how it will look
with the actual game theme, let's go to the base UI
node level up selection, and just assign the
theme here as well. So in the inspector,
quick load the theme, and then that will
update our text. So now you can see that
with this different font, the text actually
fits on one line. That's a big difference.
Let's click on the Rich text label two,
and let's center that. Okay, next, let's
take this title and make it a little
more interesting by adding a rainbow and a
wave effect with the BB code. So check BB code enabled over here and go to the start of
the text on the top left. We're going to put in square
brackets for BB code, rainbow and then space FREQ for frequency equals 0.5
space SAT equals one. That's the saturation.
Space value equals one, and then end the
square brackets, and you'll get a rainbow
effect over here on the left. Now, to make sure
that that ends, you go to the end of the text, and then you do square
brackets Rainbow. And there we have
our rainbow effect animating in the viewport. And we can also change
the settings here. So if you want it
to be less vibrant, try taking the
saturation to 0.5, and now you have a more tame
rainbow showing up there. I kind of like that,
so I'll keep it. Now, let's also add
in the wave effect. So hit home again to go to
the start of our textbox, or you could just click
up there on the top left. And then do another
square brackets, and we're going to be
typing in WA VE for wave space AMP for the
amplitude equals 50. That's how high it
goes between waves. And then space frequency
or FREQ equals two. That's how often it's
going to do a wave, and then end it with the
right square bracket. Okay, and then you want
to go to the end after the rainbow up here and
do square brackets wave. That just means that if you
add extra text after that, it's not going to have the
wave effect because we ended the effect
in our text block. So Control Z, we can see our text animating there.
That's pretty neat. We might want the amplitude
to go down a little bit, so I'm going to shrink it to 30. So if we watch our animation, we can see that
the text animates pretty much to the bottom or
the top of our label area. I think if we have an
amplitude of 50 like we did before it even gets cut
off. So that's not ideal. If it's too much
going up or down, we can add some padding to this specific label by
going to theme overrides. And then we want to go
to styles normal and do a new style box empty
because there's no background. Expand that, go to
content margins, and then we can make
the top and the bottom, a few pixels offset. So I'm going to try
making the top negative three and the bottom
negative three. And then what we can do is add a content margin here
to the top and bottom. So I'll make it like a two pixel on the top and two
at the bottom. Okay, and that is going to
basically stretch our box to be a little bit bigger than it would have
been by default. So that's another fix if
you didn't want to just add a whole bunch of new
lines in the text box. I guess that would also be
another quicker option. But a little bit less
precise, as well. So, finally, for this, if we'd like the
text to be bigger, we can go to theme overrides and customize the
font sizes here. So I think we want
normal font size, and let's boost it to 24. Okay, that one actually
looks a little blurry. Let's try 32, right? Okay, so I think for
this specific font because it's Pixar there are very specific scale numbers that you have to set
the pixel size to. So if I said it's
something like 22, I guess it's not like a multiple of eight or 16, so it's
not going to work. So let's do 32 instead to have that there so that
it renders correctly. These overrides, by the
way, if you want it, you could also spend time and customize it and the
themes specifically. But if it's just going to
be a one off like this, it might just be
quicker to assign your custom values to
this header specifically. Only if you're going to have
a lot of UI across the game and you want to customize
it in a centralized place, then doing a lot of
theming makes sense. Rather than just
using it for fonts, you can also customize the font sizes or
add custom header
44. Designing Reward Selection UI and Final Adjustments: Alright, so now
we need to create our vertical selection boxes. So that's going to be a sub UI. I'll go create a
new node up here. This will be, let's say,
it's a user interface, but I'll right click here and change its type to
a panel container. We'll just make the root
here a panel container. And this will be a full size by default, expanding
everywhere. Let's rename it to
be Reward selection. And I'm going to save
it in the UI folder. Okay, now let's go
to Lavop selection, and I'm going to put three
of those in the hierarchy. So search for reward in the project and drop
one of those onto VBox. I'll duplicate it a
couple more times. And you'll see that the
immediate problem is that there are three, but
they're all vertical. So we need another
HBox container to make them go horizontal. So right click on VBox container and add a child node
HBox container, and put the three
reward selections under the HBox container. Now you should see if you
click on the HBox container is that they each have a offset
position from each other. But right now they don't
stretch across the screen. So we need to go to
reward selection, and it's going to be
layout, container sizing. We want expand on horizontal. And while we're
at it, let's just do expand vertical as well. Control as to save, go back to the
level up selection. And now we can see that they're kind of in a column sense. They each take even spacing of the space
from left to right, and it fits our main
panel container. So that's more like
what we're looking for. In reward selection,
let's right click on the top and add a node. So this is going to
be a Rich Text label in the Rch text label,
let's put the text. We'll say square bracket name, and I will check Fit content. We can zoom in here to see it. So for the reward selection, I'll click on Reward selection, and let's take the anchor preset and let's take the anchor
preset and do top left, okay? And then under here, we need
a margin container. Okay. So if we look here,
so if we look here, there's no spacing between our labels and the
edge of this panel. So let's add a margin container under the reward selection. Right click Add child
margin container, and then make the Rich text
label a child of that. The margin container will do
theme overrides constants, and let's say maybe three
pixel for the left, top, right, and bottom margins
over here on the right. Now let's take the
root reward selection, and I'm going to change
its anchor preset to, let's say, top left. But let's also take
the reward selection and stretch it out
a little bit so that we can at least see what
we're working with here. We don't need it to stretch the entire screen because
that's misleading, but we do want to be able to
see our vertical content. And we can go to
level up selection to see how that's
going to look at any point after the resizing
and this UI container. So so far, it looks good. We want to center
that title, though. So I'm going to click
on Rich text label, and let's do horizontal
alignment Center. Also, for the vertical container sizing here and the control, I want to do Shrink begin. So that'll make sure that it doesn't expand any
further than it needs to. If we go to level up selection, we should see our
centered labels here. Okay, now we need our
icon placeholder. So I'll right click
on Margin Container, add a texture rect, and then we can select an
icon from our project. So let's do new Atlas texture. And here, I'm going to Quickload an atlas icon from
the 16 by 16 pack. Let's do dit region and
select a placeholder. So something for leveling up, something like this
looks pretty okay. And I'll hit close.
Now we need to make sure that this is
actually organized properly, so we're going to
need a VBox container under the margin container. So right click on the
margin container, add a child nailed VBox container and make the title and the texture a child of that. Okay, now it'll
organize properly. We need to make sure that
this keeps its aspect ratio, though, so it
doesn't look weird. So in the inspector for textuct change the stretch
mode to keep aspect. Also should be keep
aspect centered. Okay. There. That's
looking more appropriate. We may also want to go to layout and set a
minimum icon size. So here, I'm going to take
the custom minimum size, and we'll say something
like 48 by 48. Okay, that will kind
of expand that. 32 by 32 is fine. So that just means if your
icon is smaller than 32 by 32, it'll automatically
scale up to that size. Okay, let's add a
description text, so I can control C
the rich text label and then click on VBox container,
control V, paste it in. And we will put in square
brackets description here. Obviously, we can see that that is pretty big for this area, so we may need to work
on our font sizes later. And then, lastly, we want a
button to select the option. So I'm going to click on VBox
container, add a button. So here we're probably
just going to want a regular button. Which does allow you
to have an icon, so we can quick
load a test icon, same way as we did before, at less texture, expand it. Quick load from the 16
by 16 sprite sheet. Do Edit region, and
pick an icon out. So maybe something like the sword with a level
up symbol makes sense. Okay. Now we can see
our button as an icon. And then in the
textbox, we could say choose indicating what
we're doing here. Now if we go over to our
level up selection window, we can see our three parts
of the UI show up down here, we have three buttons that we'll hook up to choose from later on. We have our animated title. And that is more or less what our template
is going to look like. It's just a matter of providing the options for selection, hooking them up to our player, and then upgrading our player
once they make a selection. It's just going
to be a matter of populating the list
with what options are available and then
applying that to the player once the player
makes their selection. Okay, so you might
notice there's a little issue where
sometimes the edge of our icons kind of overlap with possibly
the neighboring icons. So to fix that, we can go
to Reward selection and check filter clip.
For the button. So when you do that, if you go over here and you
zoom in and out, that should fix that
issue right there so you don't see any more
overlapping pixels. Also, for the level of
selection in the world, it might be technically
centered here. But you can see that
it's going to the down and to the right from
that center point. So it's actually not going to
appear centered on screen. To edit that, I think it
would be a good idea to take our Canvas layer and
right click save it as a new scene
into our UI project. So I'll say gameplay UI Canvas as the name, and
then we'll open it up. Okay, now, if we look into Hue, we can much more clearly
see where our objects are positioned on the screen and their size relative
to the viewpoint, rather than having the
cluttered world view. So we can see, of course, that
our player selection, UI, we need to click on that and
probably double the size. So in scale, I'll just take the scale and
make that a two. That might even need
to go up to three. Yeah, let's choose
three by default. Okay, so to handle
this issue where our panel container
basically goes to the right and down rather
than being centered here. We want to jump into the
love up selection scene and then go to panel
container, I believe. And over on the right,
we need to take the layout mode to anchors
and then center it here. Yeah. Okay, there. Now we
have what we're looking for. So this basically makes
everything that's a sub object centered
on that anchor point, where if we go to
the Canvas UI now, it's going to be
centered properly. Okay, really quickly, let's jump into the script for
Llevo selection. And on ready, we want to
hide our level up selection. So I'm going to
type and hide here. And that'll make sure that
when we start the game, it'll hide by default. Let's hit play. We're going
to LeveOp by picking up EXP. And there we have our
LevelUp selector pop up. We have three buttons to choose from. They don't
do anything yet. We may decide later we don't
want to pause the music, like what just
happened, but aside from that, everything's working. The game's completely paused
and our UI is showing.
45. Creating a Weapon Leveling System for Game Upgrades: For our level up system to work, we actually need
some upgrades to populate the list here
for our level up menu. So generally, that is going
to be weapon upgrades. So we need to set up
a leveling system for our weapons as well. So if we look at
the player scene, we have the default
spear weapon, and that has a
weapon definition. So the weapon definition is kept really simple
right here for now, but we're going to need to
add in a weapon upgrade. But we'll add into
here an array or dictionary of weapon
upgrades so that we can progress to the next level and actually get how much damage this weapon is supposed to do at any given level so that
when we launch our weapon, it's using the
appropriate settings. And that can also
include things like the cool down time or how many
targets a weapon can hit. There's a lot of
different properties that you can set up for
your weapon system. So if we jump into the
spear weapons script, we can go in further
to weapon definition. You can see in here, I already templated a get cooldown method, which is going to return
based on the level, the cool down for the weapon. So in here, if we
define our array, and then we pass in the
actual level of the weapon, then we'll be able to retrieve the correct set of stats for our weapon at
its current level. And then we'll also
need to generate a get seen method as well. So currently, this
is just loading the spear scene directly. But when we are
upgrading our weapon, we may add in the option to have completely different
projectile scenes spawn but we don't necessarily want to do
that at every level, so we'll be able to
make it default back to the original weapon projectile and just upgrade
the stats instead. So let's get to that by
creating something like a weapon level or weapon
level definition. So I'm going to go
to the file system, and we'll go down to weapons. Let's right click and
create a new script here. And then I'll call it
something like weapon level, which will itself be a resource. So let's create that. Open
up the weapon level script. We'll go to the top and create
class name weapon level. So this defines the stats for the weapon at this
particular level. So one weapon can have as
many levels as you want, and it'll progress in
power as it upgrades. So we can put in some stats
here that we would want to be able to access for a different
level of the weapon. So one obvious one
would be the cool down. So we can say at export var, cool down, and this
will be a float, and we can default to 1.0. Okay, so let's save
that real quick. I'm going to close all
my other scripts here, so right click and close all so that we can
see what we're doing. Okay, and then open up weapon level and weapon
definition here. Okay, so then those would be the only two we're
working with up here. So we want, I would say, a dictionary, if we want to be consistent with
our player levels. And at Export VR, levels, which will be a dictionary of integer and the weapon level. So for Git cooldown, we want to get the
level, and then we'll return the cooldown
from the level stats. So VR level is going
to be equal to levels dot Git and we're going
to be getting the P level. And return null by default. So now we want to remove
the underscore for the P level because we're
finally using this variable. Now, it can't infer
the type here, so we do have to give it the
concrete weapon level type, and then we're going to
return level dot cooldown. Okay, so right click into weapon level and do Loup symbol. Okay, make sure that's
actually there. So in get cooldown, it's going to find
the weapon level. If it can't find
the weapon level, it's actually going
to error here. I could see us re
using this line a lot. So I'm actually going
to also put down here a function Git weapon level. Which is going to take
a P level integer, and this is going to
return a weapon level. So we can just return this. And then instead, what
we can do in here is say, get weapon level. Pass on p level dot qual down. Now, one good reason why
we would do this is that if we want to later
add in some code, let's say there is no weapon level and we want to default to the previous level, or we want to portion error. We can do all of that inside of this function G weapon
level function. And then in all of
the other places we call Get weapon level, all of that extra code is
going to be pre handled. So this just kind
of sets it up if we need to do function get damaged, function g max hits, et cetera, then this will reduce
our code later on and centralize it
in this function. But for right now, especially when we're doing prototypes, I think I definitely want to know immediately if
I forgot to set up a weapon level where we
would expect there to be a weapon level defined
on the weapon definition. So I'm actually totally cool with it just throwing
an error as soon as we try to call dot quo
down on a null weapon level. So that's why I'm going to
leave it like that for now. Okay, now let's jump into
weapon level and we'll set up some other properties we
want to modify per level. Another obvious one would be the damage that a
weapon can deal. So at export var damage, and we might actually
do men damage to MAX damage if we want it to
be a little more interesting. So if we say at export
var men damage, and let's say that's a float, we can default it to 10.0, and then we have at
export var Max damage, which will be a float. And let's just set
that to 20 by default, whatever numbers you think is a good starting point,
you're going to adjust. Pretty much always for each
weapon level resource. As I've been
mentioning, we probably also want maximum hits. So at Export V max hits, and that'll be a integer. I'll set it equal to 1.0. Oh, also, the men
damage and MAX damage, I forgot we are dealing
with integers in the game, so I'm going to change
that to integer. So I'm going to change
that to integer in both cases to be consistent. Of course, you could use floats in combination with integers. But I think in
this case, when we always want the damage
to not be a decimal, it's just better
to be consistent across all of our scripts. Okay. And then,
likewise, maximum hit says integer should not
have a decimal point there. We're probably
going to also want a speed variable here somewhere. So export var speed, and that will be a float. I'll set it to 100.0 as roughly
pixels per second there. Then a quick comment
there, as well. So now we have a
bunch of properties. We want to set up
the resource in our spear weapon definition. Okay, so let's zoom out,
click on the spear weapon. Over here on the right, we
have our weapon levels, so we set them up. So the level corresponds
not with an array position, but the actual level
of the weapon. So we want to start at one here. And then we want to do
a new weapon level. Okay, so in our weapon level, this could be our base sets. Why don't we just add
the key value pair, and that'll be the default
for our level one spear. Let's add in two and
then a new weapon level. And we want to increase
the damage here. So I'll say 15 to 25, and maybe the speed
is a little faster, so like 110 for the speed. The cool down, we can launch a little faster, so I'll say 0.9, and MAX hits will
set to one still because adding an extra Max
hit is a pretty big deal. That could be like doubling
your damage effectively. So let's add that.
And then let's do level three, new weapon level. We'll say 20 to 25
damage, the speed, maybe 115, and then
the cool down sti 0.9, but we increase the
max hits to two. So you can kind of
get the idea here. We almost always want some
of the numbers to go up. I should feel every level is an upgrade over
the previous one. We don't necessarily have to change the upgrades
on a linear factor. So, for instance, one level could be a massive damage boost, but the speed of the
projectile stays the same. Anyway, add that, and we keep going as many
levels as you want. Okay, so to keep
going, let's do four. We'll put a new
weapon level in here, and it might help to expand the previous weapon level so you know what you need to upgrade. So we start at 20 men damage. Let's make it 25 now and then 30 for the MX damage
and the speed, 120, the cool down 0.8. The max hits two. Add a key
value pair. Just keep going. Okay, and I just added
in a fifth one for fun. So that one, you can see the stats right there
if you want to copy. One key stat I actually
forgot would be the number of projectiles
spawned per cast. So let's do export
var projectiles, and that'll be an
integer equal to one. So the number of
projectiles to instance per cast is a very strong property. So if we wanted a level
to be extremely strong, then we could say,
maybe level five. We have two
projectiles per cast, and that will make the weapon
deal a lot more damage for. Now, we don't want the
projectiles to spawn at exactly the same
time in most cases. One way would be to
offset them so you can see where they're
going to spawn at. Another option you
could add in would be to delay after a cast occurs, how long before each
projectile spans. So you could say, let's actually add both
in as an option. So we could say at Export
var offset or span offset. It's going to be a
float. It's going to be equal to zero, zero. And then at export VR, spawn delay is a float, and we'll say 0.05 seconds. So that's going to kind
of look like that. Spawn offset is
the maximum offset for the projectile
instant span position. So how far away from the center point can
it be when it spawns? And then the span delay
will be the time and seconds to wait between
additional projectile spans, and I should say, after the first, that's the
key value there. Okay, so in the next video, we'll work on taking these weapon levels and
making so we can actually select to level
them up inside of our UI.
46. Weapon Items for Leveling Up Weapon Power: Okay, so for the next
part, we need to introduce the ability for our level
up selection UI to get the item for the
weapon upgrade so that we can level up the weapon that's actually
active on the play. So the first step of that
is going to be to create an options list or
resource inside of our level up selection that we will use to populate
the options down here based off of
a random selection from the available
viable options. I see here on the
bottom left that projectiles is in
the levels folder. I'm actually going
to move that to weapons that just
makes more sense. So, since there's nothing
in the Levels folder, I'm going to rename that to be settings because what
we're really doing is setting up a list of weapon upgrades
or other upgrades that we can choose from
in our game level. And we might actually
differ this, depending on the character
or we might differ this depending on the level that
we're loading into our game, but we want to have the option to customize it either way. So we're going to
create a new script, and I'll call this
leveling options dot gD. This will be a resource, so we can edit it
in the GIDOEditor. And then I'm going
to go into here. And let's say class
name leveling options. Earlier on in the course,
we introduced pickups. Now, those pickups only exist currently as a scene
in the game world, it gets picked up
and immediately the EXP is added to the players. So we haven't had
the need for a item specifically because that
EXP only modifies a stat. But if we want
something more complex, like a health boost or the
ability to level up a weapon, then we need a more concrete item class inside of our game, and pretty much
all RPG type games are going to have
it at some point. So we want to at export a list of items that we can immediately
use with our player. So I'm going to do
export for items, and this will be a array of
let's say item for right now. Now, we haven't
created an item class. So let's create a folder in
our bottom left for items. I'll right click,
create a new folder. I'll call this items. And then inside of here,
I'm going to right click, create a new script. And this will be item dot
gD extending from Resource. Okay, so inside of here, we'll do class name item. Specifically for a
survivor like game, we may not necessarily have items that exist
in an inventory. So they still need to
apply immediately. But how they apply
is more complex than just the EXBPickup being an immediate assignment
of experience. So to make it so
that we can apply our items to a
player or a target, at least, I'm going
to create a function. We'll say function try apply, which is going to
take a P target, no two D, and it's going
to return a Boolean. So try apply because there might be circumstances
where it fails, and it might be
helpful somewhere in our code to have
that feedback of if it succeeded or we may
also want a can apply function so that we
can check ahead of time if it's actually
viable or not. So I'll say function can apply, and this will pass
in the target, no two D and return of booleans. So by default, I'll say return false here because
this is a base item. We can't apply it, and I'll just say return
false here as well. So I'll just put it
in a warning that we can't apply the base
item to a target, and then let's create
our weapon item because that's what we're
most concerned with first. So I'm going to right
click on items, create a new script, and this is going to be
weapon underscore item, it's going to inherit from item as the base class,
so we'll create. So weapon item here, we'll go to the top and do class name weapon item
that is a node two D. This is going
to return Boolean. So we need a way to access the weapons loadout of the
character if it has one. So a simple way we could
put it would be far loadout equals p target dot find child, and we're going to be
looking for weapons loadout. Okay? And that should return
a type of weapons loadout. So let's actually move that
into its own function. So function get loadout it's going to return
weapons load out. And then we're just going
to return this instead, return ptarget dot Fine
Child weapons loadout. So we need to pass
on the Ptarget here. And then we can infer the type. So I'm going to do colon equals, Get loadout on PTarget and then we return whether
that exists or not for now. And then we want to
check if the loadout has the weapons definition that we're currently trying to apply. So we could do at
export VR definition. And this is a
weapons definition, a weapon definition, rather. Okay, after getting the loadout, we want to check if the weapon exists on
side of the loadout. If it does, then we're
going to see if there is room to level it up or if we've already
hit the level cap. So inside of the loadout, we need to know what weapons are actually under it
inside of this system. So on ready, we can get all
of the preexisting loadouts, and then we'll add a method
so that we can add a new one onto it that will
append it to the array, keeping reference to all
of its weapon children. So let's say var weapons here is an array of weapon two D, and on function
underscore ready, which returns void, we want to get all the children
that are of weapons type. So for weapon in find children, and empty quotations for the pattern string were
not matching by name. But for the type, we're
going to do weapon two D, and casing matters here because that is the
class name type. So this will only return
children through of type weapon two D.
And then we can say weapons dot Penn weapon. And now when the script starts, we have all the pre
existing weapons. We could say we have a
function add weapon function, which will take a
P weapon P weapon, which is a type of weapon two D, rather, Vitun void and we'll
say weapons Penn P weapon. And we want to make sure that it is a child of this weapon. So we do that in one function. We'll say Add child of P weapon. That'll be good for right now. So let's go into
the weapons item. We have the loadout so we can
check if it has it already. So after getting the loadout, we want to get the weapon associated with this
weapon definition. So let's say, our
weapon is going to be equal to loadouG weapon, based on a definition. So we're going to
say definition here. Now, let's jump into weapons
loadout and let's do the function G weapon,
based on the definition. So P definition. Is
a weapon definition. We're going to return
a weapon to D. And let's say for
weapon in weapons, if weapon definition is
equal to P definition, we're going to
return that weapon. Otherwise, we return null. Since we only have a need to search through a few weapons, having a small loop like this is going to be perfectly
fine, not really an issue. Also, if you want
to limit the amount of weapons a player can have, then you could say
something more like at export VR Max weapons is an
integer of six at the top. And then we'll be able to check
if there's space as well. So we could say
function has space, and this will be a
Boolean return type, and we'll just say
return weapons dot si is less than the max weapons. Okay, so that's just a simple little helper function force. Now, back in weapon item, if they have the weapon, then we want to see if
we can level it up. It might even make sense to declare the type
weapon two D here, although it's already
inferred from the weapon function call. Okay, so first off, if
there is no weapon, so if weapon equals null, then we will return load out has space as whether
we can apply or not. Because if the weapon doesn't
exist in the weapon system, we just need to see if there's
space to add a new one. Otherwise, if there is a weapon, then we want to see if
we can level it up. So then we'll say return weapon. It could be something like C
level up or has next level. I think has next level
sounds like a good name to. So now we need that function inside of our
weapons to D script. So in weapons to D, we want
to use the level against the definition and just check
if there's a next level. So down at the bottom, let's create our function. So function has next level. This will return Boolean. So let's say
function next level, and that'll be a
weapon level equal to definition Get weapon level of our current level plus one, which can return null. So we will return next
level does not equal null. Because if there
is an next level, then we can hit the next
level. We can level up. If there's not, then
we've either hit the level cap or we
forgot to define a level. So we might have
defined level seven, but did we define level
six when we're level five? Because five can go to six, but we can't just jump to
seven, at least normally. So that should handle our needs there for checking
if we can apply. And then if we're
going to apply, we'll say function, apply. And let me check the
base class here just to make sure, we'll try apply. It might actually make
sense to copy this over. Okay. And then we're going
to override that down here just by re declaring
the same function. So first, we need to get the weapon if we're
going to apply. So Var weapon is a weapon two D, and this is going to be equal to loadouG weapon of
our definition. So if the weapon isn't
there, then we already fail. So if weapon equals
null, we return falls. But we also need to
get the loadout again. So Var loadouts going to be inferred type from
G loadout P target. So we're getting the loadout on the target that we're
trying to apply to. So once we have the weapon,
we want to lavo it up. So we're going to say
weapon dot tri level up, and that'll return whether
we are successful or not. So we'll return that value. Now we need to define
weapon dot tri level up and weapon two D. So let's go
down here and the function, function tri level up, and we're going to
return a Boolean. Okay, so we look up at the top of our weapon two D script. We can see we have the level and the weapon level exists in
the weapon definition to be a little bit more
efficient and just kind of be able to access what our
current information is. Once we've already leveled up, we could have a local
variable as well. We could say our weapon
level is a weapon level. So on read, let's say
weapon level is equal to definition dot Git weapon
level at our current level. And then whenever we level up, we're also going to set that. That way, if we need to see the stats on our
current weapon level, then we just access
this value right here. So we basically find the
value on our definition once, and until we level up again, we have access to the information
in this weapon level. Okay, so now with TrevelU, we want to get the next level. So our next level stats
is going to be equal to definition get weapon
level at our level plus one. So if next level
stats equals null, then we return false. Otherwise, we're going to say weapon level is equal
to next level stats, and we say level plus one. Level plus equals one, and we return true. Okay, so there's our
level up function. Okay, last thing
we're going to do in this video for the weapons
item is actually create it. So in items, right click, create a new resource, search for weapons item, hit Enter we're going
to put that in items. So we'll say spear item. And if you double
click on spear item, you can see we have a spot
here for the definition. We want the definition to be exactly the same definition as our spear weapons definition. So if you click on spear weapon, you can see it has the
weapon definition. Let's right click here and
save this inside of weapons. So up at the root,
we go to weapons, and we have spear
definition there. We want to double click
and override that. Okay? So here is our actual
spear weapon definition, and this is now a
shared resource. So we can click on Spear item, and we want to quick load
that same spear definition. So now the spear item is going to be linked in that way
to the spear weapon. So that we can reuse
the spear weapon two D. Let's save this
branch as a scene. Right click Save branch as SN. And let's put it in
the weapons folder as spear weapon dot TCN. Okay, so now we
could reuse that on enemies or anywhere
else we need it. The key point is just
that we make sure they are using the same
spear definition that's very important or else the
spear item will never be able to find the spear
weapon through the loadout. Okay, now we've done
all the groundwork. So next video, we'll
set up the UI and get the leveling up to work
for upgrading our weapons.
47. Creating a Weighted Item Selection System for Level Up Rewards: So at this point, we need to
add a list of weapon items to our level up
selection that we can choose from when we
populate the window. So that's going to be where the leveling option script comes in, where for a given game
world or level of the game, we just have this
array of items we can assign in the
level up selection. So in the level up
selection script, we need to at export
a VR for options, and this is going to be
of type leveling options. Okay, so we save this script, and now if we go look at the
inspector and two D view, so click Level Up selection, and then over on
the right, we can create our new leveling options. Now, this is something that is probably very level specific. So you might have a level one where you have lots of options, and then level two, maybe
it's a little tougher, so you restrict the item list, or it could even be per
character class if you decide to go that route of restricting items to
specific characters. Either way, this is going to
be a resource you want to right click and then
save into the project. So let's go into, say, the settings folder,
since that's why I put the leveling
options script, and then we'll put in level one leveling options
and then save, so that'll be a dot TRS. And now we can assign
an array of items. So I'm going to
increase this to one, and we're going to quick load the weapon item of
the spear item. And now that is one of
the possibilities we can use inside of our
level up selection script. In our hierarchy, we want the level up selection
to pass the options, let's say down to
this HBox container, which we could say is more
like the rewards selection. So in terms of our hierarchy, we have the option set at the
level up selection level. When we open up the menu
to level up our character, our reward selections
are going to be populated down here in
this H box container. So each of the rewards
were allowed to choose are going to pop up
as an option down here. By default, that would be three. But really, it depends on how many items we
have available. So we could rename
this HBox container to be something like a
rewards box container, and then right click
and give it a script. So I'm going to attach a script, and we'll save it in UI
slash Rewards Box Container. The reason I'm doing this is because the more you separate the different functions of your overall UI into
separate smaller scripts, the more reusable each of
those components are going to be because you're separating
the responsibilities out. So the rewards box container
is going to be purely responsible for
populating the rewards that are going to go under it. So we're going to do a class
name rewards box container, and then we're going to want
to export a packed scene to instance each of these
rewards selections from. So those aren't actually
going to be there. And the baseUI, those are
going to be cleared out and only instanced when we have a reward to actually populate. So let's create a
function populate and this will take
a options list. And we also want to
know how many items can be selected
from at one time. So I think leveling options
would be a good place to put that property rather
than the UI itself. So let's right click into leveling Options, Lookup symbol. And I'm going to say at export var MAX
Underscore Choices, which is going to be an integer, and I'll default that to three. And while we're
ahead, I do want to make one big change to
this array of items. I'm actually going to change
it to a dictionary of items so that we can apply
a weight to each one. And that'll make it
so that if we want to prioritize one item as
being a possible option, then we can do that by
increasing its drop rate effectively by giving it a higher float value or a lower float value
for rarer items. Now, since I changed the
type of this variable, that means I need to reset
it in the level selection. So if we go to level selection and we look at our
new dictionary here, we need to give the item. So quick load that spear item
and then give it a value. We could say one if we'd like. So now this spear item has a
chance of dropping of 1.0. And just like the other
instances where we've done randomness with weights, that 1.0 is relative to the total weight of
everything else on this list. So right now it's
100% because there's only one more than 1.0 weight,
so it's one out of one. But if we add four more items, then it would be a 20% chance. And, of course, because
we're giving three choices, it's going to roll three times. Once we select an item
on our drop list, it's removed from the
overall weights, as well. If we had five options, we'd
be taking three of them, three unique choices,
I should say. So let's go to the
Rewards box container. Now we have how many we're
going to actually choose from. Oh, and we want to
get the total weight. List of options, making
sure that each one is available to be used
with the specific target. So let's make another
function for that. I'll call it function,
generate choices. And this we'll take
the P options as leveling options and the
P target as a node two D. Okay, so first off,
we want to get the remaining choices
from the list of options. So if our remaining options is going to be equal to, let's say, Poptionst item weight
or items weight, and we're going to
duplicate Okay, so the reason we're duplicating
it here is because we want this dictionary of
items with their weights. To be local to this
function specifically, we do not want to mess with the original dictionary by removing items from
that dictionary. All the leveling
options should stay in the leveling options
basically for the remainder of the
game because that's a editor time definition, and this is our runtime
remaining options. So we duplicate the list, and we're going to be removing
from it after we make each selection or after we
invalidate a potential option. So the duplicate here
is important to make sure that we're not modifying
the original dictionary. Efectively. Now we have
the remaining options. We want to filter out all the
options that are invalid. So for option in remaining
options dot keys, why keys? Because the keys in
this dictionary are the items and the
values are the weights. So it might be actually helpful to declare
the type up here, even though it is inferred
from the items weight, it's better if we can
see it on screen. So I'm going to say
dictionary of item, and then we have the weight. Oh, wait, weight is afloat. And there we can see very clearly that the
item is the keys. And the float is the value. For each of these items, we want to check, can
the target apply it? Actually, it might be better
to call this item over here. For item of type item in remaining options
dot keys, we can say, if item dot can apply and
because we typed it out here, we get access to the functions in the
auto populated menu. So we want to see if we can
apply that to P target. So if we can't apply it to pre
target, let's invert that. So if not item Doc can
apply to P target, then we're going to remove
the key value pair. So when we do the dot keys, this is giving us an array. So this array is separate from the original dictionary
so I think we can actually modify that
dictionary without a problem because we already have the separate
array of keys here. So this loop here, if we modify the dictionary, it's not going to be a problem. Normally, you want to
be careful about that. Like, you shouldn't modify an array while you're
looping through it, but in this case, this array is separate from
this dictionary, so it should be okay. We'll see for sure when we
actually run the code, though. So if we can't apply the item, then we want to remove
it from the list of options because
we can't use it. So remaining options dot
remove or Ease, yeah. Okay, it's called erase here. And we have the key right there. The key is the item. So we
erase the item from that, and that's going to
remove that as an option. So after looping
all the options, we've already filtered out
all the ones we can't apply. So now we just need to make a selection based on
the total weight, and after we choose each one, we remove it from the
remaining options. So our total weight here is going to be a
float equal to 0.0, and we need to loop one more time through the
remaining options. So we could say like four
weight afloat and remaining options dot values then we say total weight plus
equals that weight. So I think that's a pretty
clear way of writing that out. So now we've totaled the weight. We want to make a
random selection between zero and
the total weight and then see which
one we picked. So VR random weight is
going to be inferred type from rand F times
the total weight. So we've done that a few
times in the course so far. The total weight is
our maximum value. 0.0 is our minimum value. And by taking basically 0.0 to one times
the total weight, we're going to get some value between zero and
the total weight. Okay, so now we have
our random weight, and we want to pick an item
out of the remaining options. And there's going
to be a loop here, by the way, but we can just write it out
for one item first. Okay, so now here four
option in remaining options. So we're going to say
if the current weight is greater or equal
to the random weight, then we pick that option. So now we need, actually a
current weight here as well. So our current
weight is going to be a float equal to 0.0. So first, we want to add in
the weight of the option. So current it's going
to be plus equal to remaining options
at this option. So we put in the key
to the dictionary. It gives us the value, and it might actually be
a safer way for that. So we could say, get option. And if there's no option, the default value is null. W would give us an error here. And maybe that's
actually what we want because we expect
it to be there. It's a big deal if we lock
up the key value pair, and it just doesn't exist. So yeah, we'll just let
that be an just like it would have been by doing
the square brackets option. Effectively, it's the same thing except this get
function allows you to apply a default backup with a comma and then
default variant. So it's a little bit more
flexible, but in this case, we do want a hard failure if
the option just isn't there, because it should definitely be Okay, so we added
the current weight. We want to check if
the current weight is greater than
the random weight. So if current weight is greater or equal to the random weight, then we're going to add this to the selected options list. We could just declare the selected options up
here at the top. So var selected is a array of item because that's what we
need to return at the end. So we're going to append the option to the
selected array. So selected append option, and then we need to remove
it from the list of options. And actually, I think
how I'm going to do this is we're going to set a
local variable up here. So if our next selection is
item equal equal to null, we'll make the next selected. Equal to that option
and then break. So we break out
of the four loop. We don't no longer
need the four loop. After that loop, next
selected does not equal null. Then we're going to
selected dot append option. Else, we want to break out
of our entire four loop. So we need a four loop
because we're going to do it multiple times. So let's see. So I think right about
here, 44 time in Range, zero, CP, underscore options
dot Max Choices with incrementing of one. So that basically means
we go from zero to the max choices, going
up one each time. When we hit the max
choices, we stop the loop. So if the max choices is three, this is going to
run three times. It's going to be zero, one, two, and when it hits three,
it's going to be equal to the max choices. I'll
stop the four loop. Okay, so basically we just
take all that and tab it over, and now it's going to loop
all of this code, right? So as part of
appending the option, we also want to take
the total weight, and we want to minus equals whatever the options weight is. So we'll say remaining
options dot get option. I'm aware that this is
calling this again, but that's not
really a big deal. So this is going to get the
float value of the weight. We remove it from
the total weight, and then we want to
come back up here. So there's also the
other possibility, which is that there's
nothing selected, which means there's
nothing left to select. So we could just do early
break out of the four loop. So we're talking about
this for loop right here. And that's looking
probably good to me. We just need to
return the selected. So return selected. So
at the end of this, we get up to three items. For each of those items, we want to populate the UI. So let's say VR up here, choices is going to be equal to generate choices
on the P options, and we pass in the
P target so that we can check each item against its target to make
sure it's valid. So we have a choices here now. And then we want to create the bbard selection scene and assign the choice to it
down here at the bottom. Since this bit is actually
outside of this four loop, we want to append the
next selected because we set next selected here equal to the option so that we could
use it outside the four loop. So we want to say
next selected here, and we want to get on the
next selected as well. So one more thing I
forgot to do down here is that after
adjusting the total weight, I also forgot to remove the next selected
from the remaining options. So we decrease the total weight because we're
removing the option, and then we remove
the option from the actual list that
we are selecting from. So when it goes back
up here now for the next four loop for
the remaining options, it's not going to have
that option there, and it will just completely skip over it because
it's no longer in that dictionary,
which is what we want. For the remaining options, we only want to
loop through what is still a possible selection, gluting selections that
have already been made.
48. Coding Reward Selection with Signal Propagation: From our Generate
choices function, we have this array of choices. It might actually
help us visually to declare the type right here
rather than inferring it. So since we have an array, we want to loop
through the array. So for each choice, we create an instance of our
reward selection scene, and then we are going to apply
the choice to that scene, basically set the choice
on the scene and have that populate its sub UI with
everything we need. So for choice in choices, we're going to create
a Reward selection, which currently is a control, and that's going to be equal to reward selection scene
dot Instantiate. Then we need to add it as a
child to this HBox container. So add child of
Reward selection. And then we'll say reward
selection dot SethoCE and we pass in the
Choice into it. Our Rewards selection, you can see over here, currently
has no script. This function doesn't exist, so we need to create a
function on the script, and we're going to do that
inside of the packed scene. So we want to click
Open and Editor. So we go into the
original packed scene for this particular UI element. So on Reward selection, we want to click on
attach a New Script, and we'll call it
Uilashrewardselection dot gD. Create that. And side of here. We'll give it the class
name Rewards selection that'll make it much easier to autopopulate its functions. And what we care about here
is function set choice, and we're going to
pass in a choice. And that choice is an item. I recall. So this
will return void. And from there, we need to
populate all these elements. So I'm going to take
the Rich Text label, and let's rename it
to be name label. Let's rename the text
direct to be icon rect. Let's rename the
second Rich Text label to be descript label, and let's rename the button
to be choose button. Okay, that'll make it a lot more clear what these are
actually supposed to do. Now that we have those
setup, let's go into the reward selection script again and get a reference
to each of them. Now, the way I've been doing
it through the course, and I think even in this case, I still prefer is to do an at export var and we'll say
something like name label. Is a type of whatever
it is, rich text label. Okay, and then you can
just assign that in the inspector. We know
how to do that, right? But this does mean whenever you set up a copy of the script, you have to manually set
the export variables. So just to show another option, you can do at on ready var, let's say, icon Rec and that's going to be
a texture rect. And then you give it an
eco sign and you set it to the path to its name. So if I say choose button, it'll automatically populate
here. We can select that. And I think this needs
$1 sign at the start. So dollar sign margin container, slash VBox container
slash Choose button. Now, what this means
is that on ready, it's going to automatically get this node path and assign
it to the text direct. So we do not need to manually
set it in the inspector. However, there is
a major flaw here, which is that this requires the path to that
item to be the same. So if your choose button
is called anything but choose button, it's
not going to find it. So that would be one reason that I might prefer to do an at Export because then if you happen to rearrange
your nodes here, as long as you still
have the reference set, it's not going to
break your script. Okay, so that's why I
don't generally use that, and I am going to switch back
to at Export var icon Rect, which is going to
be a texture Rect. And then we want at Export
var description label, which is a rich text label here at Export var choose button. Is a button. Okay, and for set choice, let's just return pass for
right now so the script can save and click
on Reward selection. Go to the top right and
assign each of those. So name label to name label, icon Rect to icon Rec, description label to
description label. And because we have
two Rich text labels, this is where having informative names like name
label and description label really helps rather than
them both being called Rich text label and
Rich Text label two. Just want to
point that out. And then finally choose button we assign to the choose button. So now we have a reference
to all of those, and we can easily fill them with information
from our item. Okay, so now to
handle set choice, we can do the name
label dot text, and that's going to be equal
to the name of the item. So right now our item class
does not have a name. The weapon item subclass
does have a definition, and that definition has a name, but we might want a
item specific name, and I think this would be the most straightforward approach to create a item name
in the item class. So if we go up to
item, we can do at Export VR display name, which is going to
be a string name, and I will default
that to unnamed item. So this is kind of distinguishing
between the item and the definition of
the weapon because an item isn't always
necessarily a weapon, so we can distinguish those with two separate properties,
at least for now. So now let's go back
to reward selection, and we choose pchoice
dot display name. Let me take a look at the
spear item definition, and we can kind of see here. So the definition here
is really set up to be more of a definition
of the weapon, how it levels up and
its damage value. So the item description sounds like something
that should be separate because you might pick up a copy of that
item on the world map, and it would say something like, Oh, this is a spear item. It adds the ability to have
the weapon to your player, but it's not the definition
of the weapon itself. So we do want to
distinguish those. So we can say description
label dot text is going to be equal to
pchoice dot description. Now we need item to
have a description. Let's copy that name, right click on item, Lou symbol. And we'll do at
Export var, well, and we'll do at export
var paste in description, which is a string, and
that'll be blank by default. Okay, so now in reward
selection, that's handled. Next, we want to
do the icon Rect. So we could say icon
rect dot texture is going to be equal
to pchoic dot icon. Once again, the item is distinguished from the
weapon definition. It should have its own
icon that can be set. So I'm going to do at
export VR icon texture. Okay, and we have
the texture icon, so we can go back to
Reward selection. Now, I do want to be careful
here because if you try to set the pchoict icon when
there is no pchoic dot icon, then you're going to be getting a null reference as soon as
you try to reference this. So you can either manually check if the icon
exists every time, or you could change this to pchoic dot Get icon and just
have that safety built in. But then we have to add this
function to the item class. So let's right click
Lo up the symbol, and then I'll create our
function G icon down here, function G icon, and this is going to return
a texture or nothing. So if icon, then we return icon, else we return null. Okay? I think that's simple enough. This method gives us
safety in case it's null, then we'll just return null instead of having
an immediate error. So then the last one is a
little bit more tricky here. We have the choose button. When the choose
button is pressed, we want to make the
selection with the choice. So we want the button to
know about the choice. And then we pass that signal of the button being pressed
all the way up to our level up selection
so that we can apply the effect to the player
and close out the menu. So here at the reward
selection level, we want to bind the signal
of the button being pressed with the choice that we pass in right
here, the item choice, and we'll emit that as a signal
on the reward selection, and then the level up
selection will connect to that going
further up the chain and we can basically know at the top level when any of our buttons are pressed and then apply the effects to our player. So we're going to need to do, let's say, choice button. So we're going to need to do choosebton press dot Connect, and we're going to
connect that to a new function on
Reward Pressed. But we're going to bind
it with the P choice. So when we're binding
it, we're basically passing an extra parameter to the callback that that function
wouldn't normally have. So function underscore on reward pressed now
gets the P choice, which is an item, and it's
going to return void, and now we can keep passing
this choice up the hierarchy. So we can have a signal
up AR at the top, which will say signal selected, which will pass in a
choice of the item. Now we just need to emit
that signal, so selected. Dot I MIT, and we
pass in the P choice, and now it can go
up the hierarchy. So yes, normally, a button press has no parameters
when you get it back. It just reports that
the button was pressed. But by calling bind, we add in this extra parameter whenever that gets pressed. So now our button can give us that very critical information of what choice was
actually being made. Technically speaking,
we could also set the local choice variable
here, and then pressed, we could just pass in the local choice for
choice of items. So if I set that when I set the choice, that
would also work. Two ways of doing
the same thing, but now you know a much
cooler way of using binding, so you can actually add
in extra parameters. Also, this doesn't require the reward selection to keep
maintaining that choice, though it definitely could, and it wouldn't be
problematic here. Okay, so let's go further
up the hierarchy. Now, in our Lavop
selection, which let's see. We have the Rewards
box container. So this is where we're actually creating the reward
selections from. So here, I need to propagate
the signal upward again. So let's say signal selected, which is going to pass in the
choice item just like that. So whenever we populate with
our rewards selection menu, and now that we
have a class name, we can and should change
this to reward selection. And we're going to connect
that to on Rewards selected. So down here at the bottom, we'll create this callback function underscore
on Rewards selected. We get the P Choice, which is the item
we return void, and we're just going to
propagate that upwards. So selected dot EMT. We're going to emit
with the P choice. So that goes further
up the hierarchy now. So that's at our Rewards
box container level. Now on level up selection, we just need to connect to
our Rewards box container. So let's Export var
our Rewards container, which is a Rewards
box container. And on ready, we want to
connect to its signal. Rewards container dot
selected dot connect on Reward selected. Function underscore
on Reward, selected. Let me zoom in here
a bit. And we're going to have our P choice, which is the item, and
we'll return void. So we're going to pass for now, and we're going to need to apply the effect to the player
and close the menu. We'll come back to
that in a second. So we had our start
selection method. What we want to do is basically reset the rewards box container. We do want it to populate,
but we also want it to clear any existing items. So in the rewards box container, I'm going to take populate and make that underscore
and generate choices. I'll also make underscore
for private so that we don't accidentally call
that from our main UI. So here we need to
underscore because we're doing underscore
generate choices. Now let's create the function we're actually going to call. So function setup choices. Yeah. Okay, that
makes some sense. And we're going to
pass in our P options, which is leveling options, and we're going to
pass in P target, which is our node
two D. Of course, we're going to populate with those options and our target. But before that, I also want
to clear out this menu. So any of the children boxes, I need to either remove
those before I populate, or I need to make sure
that my populate reuses the existing UI and then updates them
with the new choices. So because we're only going
to have a few choices here, it would be a little
overkill to worry too much about reusing every
possible sub UI component. So the easier short term option would definitely be
to just clear it, which is going to be
for child and Children. We're just going to
call child dot Q free. So now up here at the top, we can clear before we populate. Oh, and we need to finish a return type for
the setup choices. So we'll return void. Okay. Yep. So now, setup Choices, we can call that in our level selection script. So start selection,
we're going to do rewards Container
dot Start Selection, and that's going to pass in
the Options and the target, which is the context player. And this is the wrong
name for the method. It was actually setup Choices. Okay, so we start the selection.
We set up the choices. We wait for the player to
basically pick a choice, and then we're going
to get our callback. So at that point, we want
to close out the menu. So we could say rewards
container dot CE here. And I did make that
a private method. So in the Rewards box container, I'll just remove the
underscore here and here. Okay, so now it's
called internally, but also from other scripts. So if you need to make a method
public, you can do that. Technically, once again,
GD Script doesn't prevent you from calling a method with an
underscore on it. It's just the best practice
for indicating the intent. So back in level of selection
on Rewards selected, we want to apply the item
effect to the player. We'll say p choice dot
Try apply to the player. So that's going to be
the context dot player. Then we clear the container
and we close the menu. Okay, so let's see. We'll
just do a hide here. And that might be pretty
much all we need. So a little bit of cleanup here. Let's go to the
Rewards box container, and well, let me show the
two D view, actually. Here you can see that we
have these three boxes here. Having those in the
view here is okay as long as we free them before
we start using anything. So if we go to Rewards
Box container on ready, I'm going to want to call Clear. So function underscore ready. Let's call clear on
those UI elements. So that'll mean these will
get removed and they'll be recreated whenever the Rewards
Box container pops up. We want it to be as
optimal as possible, reusing each UI component is better than having to
recreate it from an instance. But for something
simple like this, you're only going to show
once every I don't know, 30 seconds of gameplay, it should have almost no impact, and it's not really a big deal. Okay, so we still have
the spear selection here. So if we actually
try to level up, we should see one selection. It's definitely time to go
ahead and give it a test and see if we actually get it
to pop up and work at all. So let's hit Play. And
we'll see how it works. Okay, so it looks
like I forgot to set the rewards container here
on the level up selection. So let's go to level up
selection. Click on that. And in the hierarchy, we need to assign the rewards container rewards box container. So that really is the weakness of export properties, right? If you forget to set it, then there's really
no indication there, other than you're going to hit errors at some
point in the code. So we don't need an assert
there because the connect to the signal is
already going to throw an error as
soon as it fails. Let's close, and
we'll run again. Okay, so let's go
pick up our item. Boom, we have only one
option to choose from. So we have named item here. I think I just didn't set
an icon or description, but you can see that
the Get icon method, because we handled
the null case, it's not going to
give us an error. Whereas if we directly reference the dot icon property
because that was Null, it would throw an error here. If we hit choose, is it going
to apply to the player? So let's find out about that. It should close the menu. It does, but I forgot
to resume the game. So we have to go back in here
and make sure that after we hide the menu or maybe
even before we hide the menu, we'll say Geth dot paused
is equal to false. Let's save that and
run one more time. So we come in here, we
make our selection, Let's choose the item.
Boom, it's already there. Now, it seemed to work, but the real test
would be if we had a display for our
weapon item levels up here, we'll work
on that in a bit. For right now, the best thing to do would be to hit close, and let's go to our spear item. I'm going to hit level two, and we're going to change
the cooldown to 0.1, because that'll be very obvious if we upgrade to that level. So let's hit play again.
I'll go pick up our item, I'll hit choose, and we see that the cooldown
doesn't update. So something is a
little off in the code. So I think the
problem is actually going to be in the
weapon itself. I'm going to check here
to make sure that we are actually assigning the level
up, so breakpoint there. And let me see here on ready, we get the cool down
definition for the timer. But you can see here we're setting the time
for the timer here. But I bet you down here, we actually don't reset
the timer to a new value. So, yes, that is a
very clear problem. We need to say the timer dot. Let's actually just say start is going to be the new value. So we need to do the
G cooldown thing. We need to get the
definition dot Get cooldown of a current level. So definition, dot Get
cooldown of our level. What this means is whenever
we are resetting the timer, we're getting the current
cooldown of our level. So if let's say halfway
through shooting a projectile, we change our cooldown
because our level went up. So this means the next time that projectile launches
after it casts, we're going to reset the
timer with our new value. So now it will use the
Level two property. And I think I don't even need
to have this breakpoint. I'm pretty sure
that's the problem. Let's go ahead and hit Play. I'm going to go ahead
and pick up our weapon, hit choose, and there you go. We basically have a
machine gun spear because our spear hit level two, and this is going to be
completely game brreaking. So I would recommend at the end, we just change the
cool down here for our spear projectile back to 0.9 now that we know it works, and that'll be a
wrap for this video.
49. Applying Weapon Level Stats to Projectile Spawning and Instances: Shown that we can upgrade
our weapons through the reward selection
on level up, but we need to make
sure that when we level up our weapons that we are using all of the correct
stats for the weapon, not just the cool
down reduction. So let's go into the
script for projectile. And we're going to see inside
of here that we're not using the stats of
the weapon level yet. We're using a built in speed, and the final damage is not done by any cut
of calculation. It's just passing 100
as the local variable. So if you've been noticing on the weapon level definition, it's not using those stats
yet, well, this is why. So when we create
the projectile, we need to pass in the
weapon level as well. We could have a totally
separate function to do it, but I think it might actually be more practical just to put the weapon level stats
into the launch function. So the launch function is meant to not only set
the direction but also set up the projectile
specific stats, but the launch is
also meant to set up the launch specific stats for this projectile based on the
current level of the weapon. So as an extra perimeter, we're going to want
to pass in P level, which is a weapon level. So that'll be all the stats
we need to bring into here. And instead of at the
top having a speed here, I'm going to remove and
we're instead going to say var level of
type weapon level, and that's where we're going to pull all of the stats from. So when we are doing the
physics process and we need to multiply by the
speed, we'll do level. Dot speed. Okay.
We'll add that in. And now on launch, we need to set the level
equal to the P level. Okay, so that's how
we pull the stats in, and then we just need to
update our calls to launch. But there's other information
here that's relevant. So, first off, when the
hit box hits a rt box, we need to pull the damage
out of the level stats. So here we need a random
damage calculation between our men damage for the
weapon and our MAX damage. So I'll remove this line, and we'll say var damage
is going to be an integer, and that's going to be equal
to a random integer range between the minimum damage
and the maximum damage. So level dot Min damage, and then level dot Max damage that handles the
randomness here. So then we have
our final damage. I'll just say that's equal
to the damage right now, and then we pass our final
damage to the Hurt box. So now we have random
damage based on the level and speed set by
the level, as well. So if we look at a
weapon two D script now, we're going to get an error for projectile dot launch because we need to pass in the current
level of the weapon two D, which is conveniently set up here on weapon two D.
So let's just say coma, weapon level here, and that just sends it
from the weapon level to the projectile instance. So we can test those changes
by running the game now. So we're going to go find our or enemy wherever
they feel like spawning. Okay, there's one. And
you'll see that we have random damage
between our men and MAX. Currently, you can
see that it's still passing through the enemy,
there's our level up. So, in theory, this should
be going a little bit faster in both the cool
down and the damage. You can see the
damages up there. So, for the most part,
that seems to be working. But there's still a few
things we have to adjust, such as how many hits it can do before actually deleting
the projectile. So on hipbox hit, we actually want to increment
how many hits we've done, and if the hits are equal
to our MAX hits or greater, then we want to free
our projectile. So up here at the top,
we'll have a var hits, and this will be an integer. It's going to start at zero. And every time we hit success. I'll go to the start
here and say var hit equals P hurt Box
dot Tri Damage. And then we'll say, I hit, then hits plus equals one. And then we can also
say, I hits is greater or equal to level Max hits, then we're going to
free on our projectile. So now this can free if the timer elapses for its basically persistence
in the game scene, which we need to replace
with the variable here, too, by the way. Or if the hits
hits its max hits, then it will free
immediately, as well. So then up here, we also want to say that when we
create the timer, we're going to grab
the level's property. So we say level, and I don't think we
actually set it yet. So we need, like, a duration. Let's create a duration
stat four our weapon. Okay, so level dot duration. That means we need to
jump into weapon level. Look up symbol, and let's create duration
down here at the bottom. We'll say at export var
duration is going to be a float equal to let's
say 5 seconds by default. So that's the maximum
number of time that the projectile can persist in the game world before removal, assuming that it doesn't get
removed by something else like hitting or exceeding
the maximum number of hits. Okay, so let's go
ahead and hit play once again. So I'll hit play. There's our spear projectile, we're going to wait
for some enemies to spawn. Okay? So we have that. And you can see,
very importantly, as soon as one guy gets hit, the spear removes itself. So that's a huge change. And if we level up to level two, it should still be the same,
but our damage goes up. And let's try to watch
this projectile, and it will despawn
after 5 seconds. So 45. Okay, there you can
see the projectiles despawning themselves
from the game world. That's very important
because otherwise, they may never be removed, and they would just be taking up system resources for no reason. So we do want those to
automatically remove eventually. Okay, now there's
also a few other properties from
our weapon level, the spawn delay, the span
offset, and the projectiles. So we need to make sure
that in our weapon to D, we're actually generating
the extra projectiles, we're spawning them relative to the offset and any
additional projectiles, have a delay after the
first one gets launched. And then with the span delay, any projectiles
after the first one, have a delay before
they get spawned. Okay, so let's go to weapons
to D. So each time we cast, we need the
projectiles to spawn. So we need a four
loop, let's say, four index in range
zero to weapon level. Dot projectiles, and then
we'll increment by one. So we just loop
this a few times. After each projectile launches, we're going to wait
Kit tree Create timer, and we need that weapon
level dot Span Delay. Okay. And then we're going
to wait for the time out. So after the first loop, we wait for the short
span delay to time out before we create
our second instance of a projectile per cast, and we're creating
projectiles based on how many projectiles the
weapon level allows us. Later, if we wanted, we
could also mix this with, say, a bonus from the player. So if the player has plus one projectiles for all weapons, then we have a function to add those two numbers
together to get the total number of
projectiles for here. So then finally, we
need to generate a random offset based on the offset distance
of our weapon level. So for our offset, it's going to be equal
to a vector two, and then we're going
to do a random F range for the X and the Y. So random F range, negative 1.0 to 1.0, and then random F range, negative 1.0 to 1.0. And we multiply this by the
weapon level. Dot offset. So this is going to
give us a random value between the negative max
distance on the span offset, and then the positive max
offset here for the X, and same for the Y
and the positive Y. So this will technically give a square shape because the X and the Y don't
affect each other, so it can be negative 1.0 on both X and Y or positive
1.0. I think that's fine. We're just trying to
have a simple offset. It doesn't really
matter too much if it's a square or circle unless
you want it to matter. So I'm going to go down here to the projectile and we're
going to add in the offset. Okay. And then that will make it so that whenever we
spawn the projectile, it'll have some offset if we
have that set on the weapon. So if I look at the
spear item, well, we can see that here we
don't have any span offset. So we might want to
set one. Let's see. What would be the maximum
pixel offset I'd want to do? Maybe like five. And then we'll just do
five for all of them. So open up each weapon
level, and we'll do five. So this can be
positive or minus five or anything in between
for both X and Y. And then I'm going to run, and we'll see how this goes. So watch where the
spears are spawning. And you can see that
they're a little bit offset from
each other, right? So it's not strictly in the
same position every time. And then also makes it a little more interesting
for some variance. Okay, now, what I want to do is take the weapon
level one for testing, and we're going to make
it to three projectiles, and I'm going to
make this bondlay 0.05 seconds, which
is already there. So let's play and run it, and we should get like a
chain of three projectiles. One, two, three, one, two,
three, one, two, three. So you can see here
one, two, three. Oh, now, one little issue here, you can see that if I rotate after the first
projectile launches, the other projectiles I also rotate when
they are instanced. Okay, so for this rotate Rads, I want to calculate that
outside of the four loop. I only want to do it once. Once the first
projectile launches, all the other ones should be
locked into that direction. So let's go ahead and hit Play. And now if I move
around, but I rotate, you should see that
all three of them are always going to go
in the same direction, which is usually what
you want, I think. So let's remove the extra
projectiles for our first we can keep that custom spawn offset
for all of these. If you want, we could even set that a default weapon
would have a span offset, but that's kind of up to
you if you want to go back and customize those
weapon level defaults. Let me look through the
weapon level script one more time and see if
we've handled everything. So minimum damage,
maximum damage, speed, cool down projectiles,
maximum hits, offset, delay, and duration. Yes, we are using all of the variables from
our weapon level, and that gives us quite
a lot of customization for any of the weapons
we want to create.
50. Building a Rotating Scythe Weapon: At this point, since we have
a working level up system, we want to take it and
add extra options to it. So now would be a perfect time to add a second
weapon to our game. So this will be similar
to the first one. We'll add a scythe, and the scythe will be able
to rotate around the screen rather than going straight
forward like the spear does. So if we open up the spear
dot TSC and weapons, you can see it
already has most of the components we're going
to need for our Syth. So I'm just going to
duplicate from this scene in the bottom right
by right clicking on the scene and
going to duplicate. So let's put in
Syth here dot TSCN. Okay, so now I can search Syth in the project
and open that up. Let's zoom in on the center. Some of the obvious things
we want to change first, the name of the projectile, so we'll call it Syth. And then the sprite should be selected from the atlas
texture for the scythe sprite. So I'm going to click
here, expand the at, and we'll use the
same wooden sprites. So if I recall, there's a
good Syth in here we can use. So we'll select
that and hit close. Now you can see that
the hit box no longer makes sense because it
has this curve here. So we want to click on
our collision shape, and I'm going to pull over here and then expand it to the
right something like that, or it could even make it more
like a rectangular square, if I want to cover the
whole edge of the blade. So let's do that. Now, if we
instance this projectile, it's still going to work
almost exactly like the spear, where it's just
going to go forward. So what we actually wanted to do is to rotate around
the center point. So that's going to mean
we need an offset, and we need to rotate
that offset around in a circle and that will give
us our spinning animation. So let's right click on site. I'll add a new node two D, and then this node two D will be the parent of the
Sprite and the hit box. So let's move those under here. And then I'm going to rename
this to be Rotator two D, and let's right click and
attach a script to it. So weapons Rotator
two D is fine. Let's create that let's give it the class name rotator two D, and then we want to put in a
couple of properties here. So let's give it a velocity
for this rotator node. So remember, this rotator is
a child of the base object. So the base object is going
to have its own movement, and then this sub object is going to be controlling
the rotation specifically. So they both move
at the same time, but they're independent
of each other. So at export var velocity,
which is a vector two, and I'll have this
default to ten pixels to the right and
zero on the Y axis. So this is going to affect
the movement on the screen. But we also want the node to
physically rotate as well, so it looks like our
site is spinning. So at export var
rotation speed degrees, that'll be a flow, I'll
default it to 360, which would mean one rotation
per second in this case. And then we just
need to update it on process or physics process. I'll do physics process to be consistent with other
movement in the game. So let's say if our amount is going to be equal to
degrees to radiance. So we're going to convert
this rotation speed degrees to a radiance amount. So we need the rotation
speed degrees multiplied by the Delta to get the amount of rotation since the
last frame update, and then we're going to rotate by that amount in radiance. So we can even call this amount RDs if we want to be clear that that is a amount of radiance and I'll
make it a float as well, just to give it as much
clarity as possible. Okay, then we need to
physically move the node, and that's going to be based
on our velocity up here. So I'm going to say our
move is going to be equal to velocity times Delta. And then we call translate
on the move amount, and that is essentially
our rotator script. So if you need to
customize any values, you have the inspector
over here to assign them. So now we want to
create a weapon two D that can
instance our sights. So let's look for
the spear weapon if we even saved
that to the project. So now let's look for
the spear weapon. We're going to open that up. We'll take a little look here. So this is the two D node with a audioStream player which plays the slash sound when
we instance a weapon. So I want to duplicate
this and then create a Syth definition
for our weapon. So let's duplicate the spear
weapon scene as Syth weapon. So let's search for Syth again. Double click Open Syth, rename spear weapon
to Syth weapon. And then over on the right, we need to create a new definition. So I'm going to zoom in here, new weapon definition, and
this is going to be Syth. Our icon, we can just copy from the actual Syth projectile. So I'm going to go to the Syth scene and then we'll click on the sprite and click right
click on the texture, copy it, go over
to scythe weapon, and then paste it into the
icon with right click Paste. Okay, now we need to define
some weapon levels here. So let's create our set. We'll start with weapon
level one, new weapon level. So how do we want
our scythe to work? Maybe the scythe moves slower
than a regular projectile. Maybe it's cool down is longer, so we could say like 1.5. Before Max hits,
I'll put it at five, so the scythe can hit a lot
of targets before it despws. For damage, let's also
bump it to 20 to 30. And projectiles one. Okay, so let's add
that key value pair, and then we'll do level two
with a new weapon level. Expand weapon level
one so I can compare. And then I'll do, let's
say, 30 to 40 damage, speed of 55, cool down of 1.4. Max hits of six, and I'll
add that key value pair. Let's do level three,
so on and so forth, however you want to
make the numbers. Okay, so here's the numbers I went with at weapon level three. You can pause if
you want to copy. Here we have weapon level
four, weapon level five. And weapon level six. And I think that should be
sufficient for right now. Okay, so if I recall, I have not set in the weapon definition
the ability to pick your packed scene yet. So in weapon definition, I want to add a G seen options. So for now, we'll default
it to a export variable. So I'll say export VR scene, and this will be a packed scene. Okay, and then instead of
Getsn loading the spear, we want to return scene. So because we're using
a get seen method, we have the ability later to come in and
change if there's some extra scenes we want to consider passing in rather
than the base default. But this should be good for now, but we don't really need a
second scene at the moment. Okay, so now we
just need to make sure that in the
scythe definition, we quick load the scythe. And then in the
spear definition, so search spear definition.
You double click that. And then we need to quick
load the spear scene. Okay, and that should
be good there. I think the last
thing we need for our Syth is the Syth item. Now we want to find Spear
item in our project, duplicate that and create
a Syth item dot TRS. So search for Syth item. And that Syth item
we're going to assign, of course, the
scythe definition. Now, for our Sites to be added
as a weapon to our player, we're going to need a
reference to the weapon two D. That we can add to our player's
loadout because that doesn't already exist on the player, so it
needs to be added. Okay, so to add our reference
to the weapon scene, we cannot actually use the pact scene because
if we do that, it's going to have a
cyclical reference because the weapon TD already references this
weapon definition resource. So in this case, to
get around that, we need to do a export file, and this will be in
quotations, star dot TSCn, which means we can
filter it by anything that has the extension dot
TSCN at the end of it. And we'll say var
weapon two D path, and this will be a string. This is the path to
the weapon scene for adding two weapon
loadouts like the player. Okay, so now, if we look
at our spear definition, we can assign the
weapon two D path, which is going to be in weapons, and then we have spear
weapon in here, yeah. So that's what we need. And then we need to do the same
thing with scythe. So search scythe. And it looks like I
called it scythe weapon, but that was supposed to be
site definition down here. The TRES is the definition. The scene is the site weapon. Okay, so double click
on site definition and then assign it the path. So you can actually
just drag and drop from here into there, and
that'll assign it. So in weapon item now, if the loadout does not
have the weapon already, then we need to actually add the new weapon at weapon
level one to the loadout. Rather than returning
false here and tri apply, we're actually going to
create the new weapon. So we can say weapon is
going to be equal to. Let's look at our definition
weapon two D path, we want to instantiate that. So we're going to say
load on the path, and then we instantiate. And that's going
to be our weapon. So we need to add
that to the loadout. So loadout Add weapon and
we're going to add the weapon. If that works, we
want to return true. So if we have our item
we can now add it to the list of possible items for
our reward UI to generate. So if I click on the level
up selection scene and we go to the first node
here over on the right, we have our level one
leveling options, and we already added
in the Spira item. So let's add in the site item. So quick load our site item, give it a weight of one, add it. Okay. And then let's hit Play and see what happens
when we go over here. So you can see it
says unnamed item, and we don't really
see any details. If I hit choose, it's not actually launching
the new projectile. So we still have some
work to do for that, customizing the items and making sure that they
properly load out, and we'll handle that
in the next part.
51. Creating a Weapons Display UI for Game Inventory: Project really needs now is the ability to see
what weapons we have equipped in the top
left with the UI indicator. And when we level up to either upgrading the first weapon
or choosing a new weapon, and I know these names and
descriptions here as well. That will also pop up
here so that we can associate what we should be seeing and what we're
actually seeing. So that'll help us
with the debugging, but it's also very helpful
for the player as well. So let's go ahead and
create a new UI scene, so I'll click Plus up here. So I want to be name this
control node to, let's say, weapons display and I'll also right click and change its
type to a panel container. Okay, that'll give us a
background by default. So inside of here,
I'm going to want a margin container
and then follow that up with HBox container
for organization. So right click Add
a margin container, and then right click
Add a HBox container. Okay, and then in here we're
going to lay out basically an icon with a level indicator for each of our
weapons left to right. Let's take the
weapons display and shrink it to be much
more like this. And then I'm going to save it to our project in the UI folder, so on the UI folder
weapons display. And let's put a copy into our gameplay UI Canvas so we can see how that's
going to look roughly. So search weapon, and
then we're going to grab weapons display and
position it under the UI. And we can see that's
quite large there. So you can click here and jump
into the weapons display, and let's shrink it down to maybe more like the
middle of the screen. Note the blue box indicating
our full UI size. So this is going to need to be quite a bit smaller, really. And I probably want to offset
from the center as well. Let's do layout center
by default, as well. Now we can go back on
the gameplay UI Canvas. We can see our weapons
display there. It's not really showing
up like we expected to. So in weapons display, I'm going to take the
anchors preset, and let's go with center
by default or top center, rather. So center top. And we can kind of see
the problem for why it shrunk down to nothing. So I'm going to take the custom minimum size up
here and let's set a default. I'm going to say 200 for the
X and then 50 for the Y. That'll just be so that
we can kind of see it and adjust it on
our gameplay UI. So I'm going to go over to
the gameplay UI Canvas, and now we can position
it where we need it. So I'm going to
position it, let's say, out here to the right of our
main health bar and EXB bar, and I'll drag it out like this. Okay. And then we
can have a few icons showing for our weapon levels. So to know about that,
we're going to want to use our player context so that we can show what weapons
the player has. So in weapons display, let's add a script islas weapons
display dot GD. And then we're going to want
to export var the context, which is the player
context here, and we can assign it
in the inspector. So quick load the
player context. And then on ready, we're
going to want to use that to get reference to
our weapon loadout. So function underscore ready, it's going to return void, and then we'll say
context dot player dot. Do we have a reference
to the loadout? We do not. So if we
don't want to make that another built
in property of the player and have that directly referenced on the player script adjust for this, we could say find children, and the pattern is going
to be empty quotes. The type is going to
be weapon loadout. So this is going to
give us an array. We'll take the first result of this by doing square
brackets in a zero, so that means array
index of zero. And we're going to assign
that to a loadout. So our underscore
loadout is going to be a weapon loadout
weapons loadout. And we're going to say
underscore loadout is equal to the result of that and we do want to assert that
that is not null. So assert underscore loadout
does not equal null. Must have a loadout on the player to show
for weapons display, percent name at the end so that we can get the
name of this node. And then we want the
class name weapons display up here at the top. This will display the weapons and the levels for the
context character. So let's do another
at Export var. This will be for the UIC. We're going to
instance for each of the weapons in our loadout. So do a at Export var. Let's say weapon display, which will be of
type weapon display, but we haven't created that yet. And to kind of indicate
that this display also does more than just
purely showing the data, but it also hooks everything up. I'm actually going to
rename this to view. That'll help also differentiate
from weapon display, the sub UI container. And then I'll rename
both the weapon display. And then I'll rename both
the weapons display dot GD and the TSCN. So this will be now
Weapons View and up here, weapons view dot TSCN. Okay. Also jump into the view scene and rename
the root node, as well. That's pretty important.
So weapons view there. Now we need to create our
individual weapon display so that it can go inside
of the weapons view. So let's add a new
UI to our game. This will be a user
interface at the start. And I'm thinking
that the root here, we may also want it to
be a panel container. You can always make
a panel empty, but having the ability to have the background is
kind of helpful. So let's change this
to a panel container. I'll rename the root
to be weapon display. Let's right click on it, add a child node margin container, just in case we need that
and then right click Add a texture wect
to display our icon. We'll right click add a label. And I'm actually
thinking I don't need a VBox or HBox container for this because I may
actually want the label to write over the texture. For a lot of games
that would have a texture and then
text at the same time, it would actually write the
text over the texture icon. You would see that a lot
in inventory systems for the number of items
in the inventory. Except in this case,
we're talking about the level of the weapon that
is equipped to the player. So as a test text, we could say level X here. Of course, we can see that the display is taking
up the full screen now. So let's actually shrink that. I'm going to go to the
base weapon display, and we'll take layout to
Anchors preset top left. Okay, that should shrink
it down dramatically. So now we can see it's
just showing the text. Let's set a texture
on this texture rect. So I will do a new
atlas texture, and then we will
quick load a atlas. Let's look for
maybe the 16 by 16. Sprite sheet in our
game for all the icons, and then we want to edit
region and select one. So something that
looks like a weapon. Here we have a sword that
works fine, and we can close. Okay, we can see that
the sword icon is there, but it's not respecting
the aspect ratio, so we want to go to stretch
mode and change this to keep aspect
centered, I think. Okay, there we have
that. And now we want to take our main weapon
display container, and we want that to be a
specific size as well. So I'm going to say custom
minimum size 48 by 48. That'll give us a square here. And I'm going to want
to take the label text, and we want it to be
centered at the bottom. So vertical alignment
to bottom over here on the you'll
see that doesn't actually change anything
because we need to expand the size of the
label for the text to be centered at the bottom. So go to layout now, and we are going to want container sizing
to fill, I believe. Yeah. Okay, there.
So now it's going to show level X there
on the bottom right. And now it's looking
like we want a little bit of a buffer between our texture
rect and our label. For right now,
let's save this to our UI folder, our
weapon display. And I think what we want is
a second margin container. So I'm going to click on
the first margin container, add in another margin container, and this one is going to be
the parent of the texturect. So let's position that on top again so that
the text shows over. Now, with this margin container, go to the theme overrides, and we want the bottom margin to be something like ten pixels. That way, the text is not
going to show you know, over the important
bits of our icon. Maybe we even need
it more than that. So let's try 12. Maybe we even need it
more than that, really. So that we can see how it's
going to look more properly, let's apply our game theme
to the weapon display. So click on weapon display, find theme and then quick
load our current theme. Okay, so now that's a lot smaller and it's in the
bottom left hand corner. I think I want it in
the bottom right. So click on label, and let's take the horizontal
alignment to right. Okay? That's looking
pretty okay, but it's a little too
close to the edge. So I want to take the
parent margin container. That's the one right
under the weapon display, and I want to set some margins. So let's go to theme overrides and maybe two pixels
on each of these. Okay, that looks right. Except maybe the margin bottom.
Maybe we don't need that. Yeah. Okay. I like that better. Alright, now, our weapon
display is going to need a reference to the
weapon two D node. And then whenever the weapon
two D node levels up, we change the text here
to show the new level. So let's add a script
to weapon display. This will just go on
Uilashwapon display dot GD. Okay. And this will be
class name weapon display. And we need a reference to the weapon two D. I could
just export it for right now. So this will be a var weapon of type weapon two D.
I'll say set value, and this will be
weapon equals value. Whenever we set the weapon, I think I'll just call
a refresh function, and we will set that here. So function refresh. So to set the text on
the label and the icon, we need to get an export
reference to each of those. So at Export var texture, text direct of type texture Rc. And then we need at
Export var label, which is of type label. And when we refresh,
we want to say texrec dot texture
is going to be equal to weapon dot definition
dot icon. Yep, that works. And then we'll say that
the label dot text is equal to weapon dot, if we have a name
here, weapon name. No, we want the definition
name because the name on the weapon is
actually the name of the node that won't have
spaces or anything like that, so it's better to get
the resource name because that's a custom
property we set. And then we want to
say plus equals space plus a string of the
weapon dot level. Okay? So that'll basically show the weapons name and then what
level it is currently at. And just in case the weapon definition
doesn't have an icon, let's make a get icon method. Which will work like we did with one of the other scripts before, which will just return null if that property happens
to not be set. So let's right click
on the definition. We'll go into that, right click weapon definition.
We'll go into that. And then let's create
our function G icon, which will return a texture two D. So if icon does
not equal null, then we'll return icon. Otherwise, we return null, and this should prevent
you from getting errors if icon happens
to be null here. Okay, and we probably
also want to try refreshing on R so
function underscore ready, I'll try doing a refresh. That'll also help us to confirm that if there's no
weapon two D set, it will work fine
and just show blank. The idea here is
that whenever we set up the weapons view, it's going to populate
with weapon displays. We set the weapon, and that's going to refresh immediately. So let's go to Weapons view. And inside of here, well, you see that I was
exporting a weapon display, but actually, what I want
to do is export the scene. So this will be a packed
scene that we can instance. And then we quick
load on the right our display for the
weapon, so weapon display. So on ready, once
we get the loadout, I want to populate this with
all of the weapon displays for each of the weapons that are actually in our weapons loadout. So I did something
similar with the level of selection where I just clear
and reset it each time, generating new scenes for each of the sub UI that
need to be created. But here, I'll actually
try to reuse it so that you guys can
see both approaches. This would technically
be the better use of your nodes by not having to delete them and recreate
them each time. So I think that'll be helpful
to know after the course. So one of the ways we
can easily access all of our active weapon displays is going to be to have
an array of them. So I'll say var
underscore displays, and this is an array. I'll zoom in of weapon display. Let's also create our
refresh function. So our function refresh
is going to return void. And for each of the weapons
in our weapon loadout, we're going to create a display if one
needs to be created. Otherwise, if it already exists in this array of weapon display, then I'm going to reuse
it by just refreshing the properties on it
with the new weapon by setting the weapon
on the weapon to. Okay, so for our
refresh function, we want to loop through
all of the weapons, and we'll do that
using the index positions so that we can match the weapon loadout indexes to
our weapon display indexes. So for IN range zero to underscore loadout weapons dot
size, incrementing by one. Then we're going to
first get the weapon. So our weapon is going
to be a type weapon two. Equal to underscore
loadout weapons at that index position, and we need to get or
create the display. So I'm going to say var
display is a weapon display, and we're going to set
that equal to get or create display at an
index position of I. And then on our display,
once we've acquired that, I'll say display weapon is going to be equal to the
weapon itself. And because the
refresh is part of the setter and that script, it'll immediately
refresh automatically. Now, I did talk a
little bit about side effects and if you
do the setter like this, it's not necessarily 100% clear that setting the
weapon is going to automatically refresh the UI. But because that's so integral
to the weapon display, I think it's okay for
right here to just say, Oh, we set the weapon. Of course, the weapon is going to show the
current weapon. But since weapon display
is literally what it does, setting the weapon and
implying that that would refresh what weapon
it's going to show in the UI. I don't think there's much too problematic with
that as a side effect. But it's definitely
debatable. So now we just need to get or
create the display. So function, get or create
display with index parameter, and that's going
to be an integer, and we're going to
return a weapon display. So let's say var
display of type weapon display's going to be equal
to displays dot Get at index. Okay, so that might be null. So if display does
not equal null, we just return the display. Otherwise, we need to create
one right here and add it to the so display is
going to be equal to the weapon display
scene instantiate. And I typed that name in wrong. So right here, Control C, Control V. And then we
want to add it as a child to this UI or rather it's going to go on the HBox
container. So, hold on. I need a reference
to the container. So export V display container, and that'll be any
kind of container. We just need to export
that on the UI. Then we'll take the
display container and add the display
as a child of that. We say displays dot
apen vi new display. And then, lastly, we want to return the display,
turn display. So it should be created here if it wasn't already
in existence here. So that'll handle creating
the new displays. Now, what if we remove a weapon? Then after we loop through the weapons and we have
all the displays set up, we want to check if the weapons
array is actually smaller than our displays array and then remove the extra displays. So say that we remove a
weapon from the game, and we want our display
here to refresh. Well, that's going to be a null reference on
the display now. So we can say for
display and display I display dot weapon is null, then we can just
free the display. So display dot Q free,
and hold on here. I need to have that underscore there because
that's a private variable. So we have our
display dot Q free. So we have our
display dot Q free, and we want to resize
that to our loadout SI. So let's actually get that as a local variable of
our weapons size. Now we can reuse weapons size
here and down here as well. Okay, so to walk through
this function again, it updates and creates displays as needed
to show all weapons, removes any displays
that have null weapons, and resizes the displays array. So we get the number of weapons. For each of those weapons, we assign the weapon to its corresponding
display position. So after we do that, we look at all the displays once
again, and technically, we could loop from the weapon
range to the end of it, but a little bit
over optimization for not much gain because we're only showing like
six items here. But if the display
weapon is null, but here if the display
weapon is null, which it could be if
we remove a weapon, then we're going
to queue free on the display and resize the set of displays so that this size is going to
match up with the weapons. In our G or create display, if the display exists
at the same index, it'll just return
that and we use that. Otherwise, it'll create
a new copy of the scene, and then we apply that
under the HBox container. Okay, now in the inspector, we got to assign
that HBox container. We could rename it to be Displays Container
if that helps. So displays container, keeping
the naming consistent. And we already have the
weapon display s here. So this will work on Okay, so let's run it and see if
it's working on run right now. So we run and we failed to find the
weapon loadout because I think it might
be called weapons loadout. So you can see Fine children is kind of fragile like that. So we have weapons
loadout there. So let's try to find
weapons loadout with the S, and then we'll run it again. Okay, so we get to where we have textre dot texture equals
weapon dot definition dot icon. Okay, so then we
hit another issue. I haven't set up the textrect in the label on the
weapon display. So let's go into weapon
display and rectify that. The top right, when you're
on the weapon displacing, assign the texture
wrectt double click, and then assign the label. Okay, and we'll run
it one more time. See if that resolves the issues. Okay, so here, when
there's no weapon, it's going to cause an issue. So maybe we actually
do want it to set a weapon before we refresh it. So since the refresh occurs
here when we set the weapon, I will remove the ready refresh. It's not really needed, and it actually just
causes more of a headache here than we need.
So I'll hit play. And okay, there we go. We have Spear one up there
on the top left. I'm going to go over here and
we'll select the new item. Now, this isn't going
to work because we haven't set up signals. Okay, so a mistake I made, when you call Git on an index that doesn't
exist in an array, it's going to give you
out of bound errors. So what we should actually
be doing is making sure that the index
is in the array. So we'll say if displays let's say size is
greater than the index, then we're going to
return this bit here, displays dot Git Index. Sure. Then we can remove the variable declaration var display weapon display down here to when we instantiate the
display scene, and we'll remove this
line at the top.
52. Connecting Signals and Configuring Weapons UI for Dynamic Display: Okay, so we have a few
more signals to connect on our weapons view and weapons display so that
when we're in game, it's going to properly show the right weapons setup
after we refresh the UI. So a very important one
here is going to be when our loadout
adds a weapon by calling the add weapon
method that we're going to connect to its
weapons changed signal, which I don't think
I've even created yet. So let's jump into
weapons loadou. I'm going to right click
here, open up the symbol. Now we're on the weapons
loadout dot GD script. So we have the add
weapon method, and we want to admit
a signal here so that our view can
respond to that. So at the top, let's create
our signal weapons change. If we like, we can pass the array of weapons
into the signal. So weapons, which is
an array of weapon two D. And now we just
need to admit it down here at the bottom
when we call add weapon. Weapons changed dot emit, and we pass in the weapons. Okay? Now back on
our view script, we subscribe to so on ready, after we get the loadout, we want to do underscore loadout weapons
changed dot connect. And we're going to say
on weapons changed. We'll make that method
down here at the bottom. So function on weapons changed. The parameter is the P weapons, which is an array of weapon two D, we're
going to return void. Now, we're going to
call refresh here. That's all we need to
do because refresh already handles checking the weapon loadout and which weapons we have,
what the size of it is. So we're actually not
going to directly use this P weapons here, so I'm going to underscore it
so we don't get a warning. And now before we hit play, I want to make sure
that our items are actually defined properly. So in the bottom
right file system, I'm going to search for
item. We have the Syth item. So let's see if we go
to the top right now, this is an unnamed item
with no description, and it needs an icon as well. Also, it seems like I
have the wrong also, it seems like I have the
wrong item definition. That makes a lot
of sense now, why the scythe weapon didn't appear. So let's quick load here
the scythe definition. Okay, so that's set up properly, but we have to set
our display name. Scythe. So for the description, I'm going to put a
projectile that rotates around the screen and
can hit many enemies. That's basically its main
difference over the spear. And for the icon, well, let's jump into the
scythe definition. I'll right click on the
icon here and copy it and then close that and paste
it into the icon here. Okay, now we need to
do something similar on the spear item. So
double click spear. Then we'll go to the top right. The display name will be spear. So the description can be
a basic projectile that goes forward from the
direction that the character, I guess, I will say, is looking. And then the icon, we
just copy that from the definition over to the
item icon. Just like that. And that should basically handle a lot of
that. So let's play. I think the only
thing now is that we haven't connected to
a level up signal, but okay, we can see spear
one in the top left. Let's go grab our scythe. So scythe one is now up there, and our scythe projectile
is also launching. We can see it's rotating. Maybe it's movement speed is not really enough because
we want it to actually go around in a
circle around the player. But let's make sure it can hit. Yes. Okay, so it does seem
to be hitting enemies. Real quick, in the
generated choices, there's a notice about
selected shadowing and already declared signal. So that's not great.
Going to take the selected array
and generate choices. This is on the
weapons view script. This is on the Rewards
box container here. I'm going to take this and let's control R. I'm going to
rename it to selected items. I'm going to hit replace not replace all
because remember, we don't want to mess with
the signal names up here. So replace inside of
this function only, and we want to go down to here. I should also match case
and match whole words that'll help reduce the number of extra stuff that
gets selected. So selected datapen
is now selected items datapen and then we
return the selected items. Okay. So that should
handle that warning. Okay, so our site scene was not rotating in exactly the way
that we would have expected. So one issue with the
projectile scripts is that if I try to say, run this scene without launching it from a
weapon two D first, that we're going
to get this issue where it's going to hit
that assert on ready. So what we could do instead
of that for testing purposes, mostly, is to have a
default direction. So I'm going to say
that the direction is going to be equal to
vector two dot right. So just have that default there. We'll remove the assert. And so I'm also going to
export var the level so that we have the option of setting a weapon level in the inspector. It shouldn't be
required for us because mostly we're going to be launching through the
launch method, right? That's how we set
up the projectile. But just in case we want
to test it in the editor, I could set up a test level over here and the right
so that we could define a default weapon speed. So let's hit Play. Okay,
so now with that setup, I should be able to
run this script, and we can see how it's
going to move by default, which is pretty helpful for
us right up there at the top. I could even create a
test scene where we have a camera looking
at the center of our so that'll help us further for our visual
debugging purpose, so we don't need to run
the full game and have to level up each time we want
to test the scythe item. So let's create our test scene. So this is going to
be a new two D scene, and I will save it at the root. So let's call it test
weapons dot TSCn. Save that, and I will drop
in the scythe weapon. So scythe weapon dot TSN. We just put that
at the root there. I'll rename the root node. So test weapons, and then
let's right click and add a camera to D. So this will have a Zoom of three.
I think is good. And let's try
running the scene by clicking the play
current scene button. Okay, so the reason we
don't see anything here is actually because we're not supposed to put the
scythe weapon here. We're supposed to put the Syth
projectile. So my mistake. So let's search forsythe dot
Tscn drop that on the root. Now if we zoom in,
we should be able to see that. Okay, that's good. And let's hit the run
current scene button. So now we can see what it's going to look like when our scythe shoots to the right, and that'll make
it much easier and quicker for us to
debug that scythe. So now with our
test scene setup, we can see how our weapons rotating and moving
on the screen, but that's not exactly
what we're looking for. So what we actually
need to change here is back on the scythe
scene, this rotator node, if we want the
weapon or the sprite to rotate around a center point, then we actually need to move this rotator offset to the right while keeping our sprite and hit box at the
same position. So I'm going to temporarily
move these two off of the rotator two D so that they are not
going to move with it, and then I'm going
to move the rotator two D over to the
right like this. Okay, so that'll
give us an offset so everything around
it can actually rotate around that point rather than just rotating
around here in a circle. So let's bring this right in the hit box back into
the rotator two D. Okay, good. And we might want to say up the X velocity to 25. Let's run the scene and see if that gives us
what we're looking for. So, yes, but that's a
little too fast, actually. So maybe we just lower that back down to the default
of ten and run it again. So it's the rotation that's
actually doing that. So we either need a
slower rotation or perhaps we need this to be
further out to the right. I'm not sure which.
This is kind of just guess checking for me here. So just if you do happen to move the rotator with this sprite
and hip box under it, just make sure you
move those back to the 00 center point. And then let's run the
test scene and let's see. I guess just 360 degrees
is way too fast. Okay, so I'm going to go back to the sth scene and we'll lower the
rotation degrees. Let's cut that in
half, maybe 180. And then let's run
our test scene. Okay, that's probably a lot
more like what we want, but I've also made the distance of the rotator too far now. Okay, so I'm going
to move the sprite two D out and the hip box out, and let's move the rotator
back in quite a lot. Move the sprite and the hip
box back under the rotator. And let's test it on
the test weapons scene. Okay, so let's run that. Uh, almost. But maybe the speed of the
projectile still too fast. I don't know. We need
to test it out in game. So let's hit play, and this will actually run the main game because that's the main scene. So let's grab the spear
the scythe projectile. And there's how our scythe weapon is looking at the moment. I'm not sure I'm super
happy with the pattern, but at least it's doing
the general concept. It's mostly a matter of we got to tweak
the variables now. Let me try having the player
get hit and destroyed. Okay, so he does still remove
himself from the scene, but we're gonna have to
create the game over screen. As part of the polish
of the course. So I'm going to
run that one more time so we get our
scythe projectile. It's kind of like
going up to the right. I'd rather it went more, like, centered to the right. Let me see if I can adjust that. Maybe that's the result
of the X velocity. I'll try turning that off, and let's run the scene again. Okay, so now it's
a bit like this. It'll loop back to
the start here. Because the rotator
has no extra velocity. And the direction it launches depends on whether you're
facing left and right. So at least it's
equal application to both sides. You
might like this. You might not. It's
mostly a matter of just tweaking the variables to get the pattern you want
for the movement. So at least for right now, I'd say it's pretty acceptable. Let's level up the
spear weapon here. And that is going to
level up the weapon, but we don't have a level
up signal connected here. Okay, so where we do that
is on the weapons display. Let's jump into weapons display. When we set the weapon here, we need to connect the signals. But at that point, I would say this is
actually putting too much stuff into a
setter for the weapon. It kind of hides what it's meant to do. The
refresher was okay. But I think if we're
connecting signals and refreshing the UI,
that's just too much. So let's create a
function set weapon. Which will pass in a
weapon weapon two D. Okay, so if weapon does not equal null and and we'll say weapon dot
level changed is connected, and then we'll have
a callback here, underscore on weapon
level changed. Of course, this is going to
give us an error because the level changed does not
exist on the weapon yet. So let's jump into the
weapon, look up symbol. We'll create a signal here, signal level changed, which you'll pass in the
level, which is an integer. If that's all I'm going
to do with this level, and I think it is, then
we'll say set value. We'll say if level equals
the value, we return wise, level equals value, and we
can level changed dot a MIT. On the levels. So we're just kind of making this
property observable, and that's all I'm
going to change there. The reason for this guard is if we set the level to the level, we don't want to emit
unnecessary UI signals because the level, in
fact, didn't change. So we only need
to do this update if we're setting
it to a new value. Now, I did forget a colon
up here at the top, so that will get
rid of that error. Now in our weapon display,
we have the level changed. We need to create
on weapon level changed callback function. So function, copy paste
on weapon level changed, and then we have the P
level, which is an integer. We're going to return void, and we want to
refresh the UI here. Uh, but what we're actually
doing here is we're disconnecting from the old
weapon signal if it exists. So we check if the old weapon exists here by checking
if it's not null. So if the signal is connected,
we need to disconnect it. Weapon dot level changed dot disconnect on
weapon level changed. No parentheses. And this
part is just verifying that the signal is actually
connected before we disconnect a signal
that isn't connected. So just safety guards. Okay, so we've prepared the old weapon to be
totally disconnected, so we can change the weapon now. So we'll say weapon
equals P weapon, and then we want to connect to this weapons level
change signals. But we can also put a guard checking if it is already
connected for whatever reason. This is a little bit
excessively safe, but we could just say, if weapon level change dot is connected to this function
down here already, and we want to
make that inverse. So we're saying if it's not connected, then we
need to connect it. So weapon dot level
changed dot Connect, underscore on weapon
level changed. And we also want to refresh, for sure. So let's refresh that. So when we set the
weapon, we refresh, and every time the level
changes, we refresh. So now we want to take
this property and underscore and just
make it private. So this will be more
like a private variable. And we don't want to
directly modify that. We want to strongly
encourage anyone using this script to use the set
weapon method instead. So I'm going to select here, and we're going to control and basically find and replace. So make sure you match
case and whole words, and then let's change this
to underscore weapon. I think we can get away
with the mass replace here. So all 11 instances
just replace all. That's Control R to
bring up that menu. Now, of course, if
we ran the game, then we would get
an error because we're trying to set weapon, not underscore weapon
from the weapons view. So if we go to the weapons view, now in our refresh, we want to call display
dot set weapon on the weapon two D with the weapon weapon
two D and remove this. So the whole reason for moving the reactive weapon
setting code into this explicit set weapon
method is so that we know that there's
more stuff going on here than simply setting
the weapon value. By the way, get rid
of that refresh. And we can even remove this, but here at this point, it's basically just
a regular variable. But we don't want to
hide the fact that it is connecting and
disconnecting signals and refreshing the UI. Can get a bit more explicit. So all of this code could
have just as easily been under the weapon
variable setter, but having this
big block function written kind of makes it clear
more what our intent is. And I know that
that does kind of contradict myself when
before I was like, Okay, you can just do the weapon setter
and use the refresh. But because we're
also doing signals, I think it's just too
much stuff going on, and we don't want
to hide that from anyone who is trying
to set up the display. We want them to
know, Okay, there's this big set weapon function. And that is handling all
of this extra stuff. Oh, right. So let's go
ahead and run the game, and I'm going to level up
the spear to Level two. Ah, okay, so you
can see there was another reference to the weapons variable, but we
made it private. So we want to have a getter to get the weapon rather than accessing the private
variable there. Let me take a look
at this function and see if we even
need it, though. Okay, so since the
weapons view is handling freeing up
the old displays, we do need the
display dot weapon to be replaced with
display dot Git weapon. So let's go create that
function in the weapon display. So function here, Git weapon that's going to return a weapon to D.
We're just going to say, if underscore weapon, we
return underscore weapon, else we return null. So why bother having
this Git weapon function when you could just get from the weapon variable directly? Well, we made this weapon private because we
want to make sure that we encourage any developer to use the set weapon
function explicitly. And because that's
a private variable, we shouldn't be accessing it
from outside of the script. So then we need a public Get weapon function
to access that. So it's basically
encapsulation and preventing other people from
doing things with the script where they're not supposed to do that
with the script. Now, once again, underscore
is just a practice of indicating it's meant
to be private effectively, but GD Script
doesn't really have private or public as keywords. So you can't actually prevent
anyone from accessing it. You can only indicate your
intent with the underscore. Okay, so let's close that and run the scene one more time. I'm going to level up our spear. And let's get the spear upgrade. So you can see
Spear went to level two right there
and the top left. Of course, that UI is too small, I need to make it larger. So let's go to the
gameplay UI Canvas, and I'm going to click here. I'll actually change the
layout and we'll center it. So Anchors is going
to be center top, and then I want to W
offset it from there, kind of over here. Also jump into the
weapons display scene. So if I go to transform here, I'm looking more like
a 400 pixels by 75. I want to make sure that the
anchor is still centered, so I can do custom here and then go back to center
top that'll recenter it. Let's look at the
gameplay UI Canvas. Okay, that's still good. I'm going to jump into weapons
display from here again. And I think the main thing
is that we just need to make sure that each of
those individual displays, the icon is set to
a minimum size. So if I look at
weapons display here, let's take the minimum
size and make it. We could even say 96, 96. Let's just make it big and then shrink it down if we need. So the text label here, do I need that to be bigger, as well? Maybe I do. Let's just override
the font size here. And I'll make it
like, let's say, 16. Okay, that's what
it's at right now 32. Okay, so we'll try 32. Zoom out. Let's hit play. How does it look in
the UI at the moment? Okay, that's a lot better. So I'll go get the other weapon. We have our scythe.
So spear, scythe one. I think the only
other thing is maybe a little bit of a margin
container would help. So close the game, and
in the weapons view, our margin container, let's
do a theme overrides. I'm thinking constants
of two on all sides. We'll hit play. Run it. And that didn't help so much. Let's do four pixels
on all sides, I guess, and hit play. Okay, that's at least more
visible. I'll make it six. So six on all sides,
that should be plenty. Okay, so we go into play mode. Okay, now we have a bit
of a barrier between our individual container
and the whole weapons view. And that's looking
a lot more solid. So it's showing
the type of weapon we have and the
level of the weapon, and that's probably the majority of what we actually
need it to show. So I'm thinking we're
good to go here. Let me level up for funzies. Let's get Syth Level two. Yep, Syth is leveling
up correctly, as well. So that pretty much
handles setting up the weapons view and its full having all the
signals connected, correcting the
items, making sure they have a
description and icon, and just having that clear UI, where we can see what's actually going on
with our player.
53. Creating Stat Boosting Items for Player Upgrades: So with our leveling
system working the ability to choose
a weapons item and upgrade our weapon loadout with a new weapon or an upgraded
level of a weapon, now would be a perfect
time to add in the ability to choose non
weapon related upgrade. For instance, a
step boosting item, which will permanently
increase our HP on level up. And we could even make it item which has a rare chance of dropping off
enemies if we want. In our items folder,
bottom left, I'm going to expand that and let's create another
type of item. I'm going to right click here
and create a new script. And we could call this a
stat Boost item dotGD. This will extend from resource. So it's kind of a
definition type. And then in our Stat boost
item, I'll open that up, give it the class
name Stat Boost item. Oh, and this is actually
going to extend from the base item
class, not resource. So if we look at item, we have the Trapply
and Can apply. So let's go ahead and
grab those because we need to implement those
in our Stat Boote. So I'm just going to
paste those in here. So for can apply, we want
to check if the target has, A, a stats block and B. Does that stats block have the stat we're trying to boost? We could create a bunch of
different classes for, say, HP boost item, cool
down reduction item. But instead, we're going
to simplify that by having a export var stat name, which is a string name. And we'll set that equal
to nothing by default. And then let's have a
export var value change, which is a float. And let's set that equal to 0.0. So first things
first, I want to get the Spblock off of the
player if it exists. So I'm going to say var
Stats is a stat controller, and we're going to get that
off of ptarget dot get, and we're going to get that
stat name as a property. So this can end up being null. So if stats is null, then we're going to
return false here. Another thing we want to check is the value change
actually set. So if value change
is equal to 0.0, I'm going to push a warning. Can't boost stat
if value change is set to 0.0 or let's actually say percent s to be a little more flexible, and then percent value change. And we'll return false here. So the reason for that is if
we haven't set up the item, it's not really going
to do anything, so we'll just skip applying it, and we'll put a
warning out there so that we know that
that's an issue we need to fix just by changing the value change property
to something else. There's not a ready function to check these things
like there would be with a node because
resources never hit on ready. So this seems like
okay place to put it. We'll just check it
before we apply. Otherwise, I think
we can return true. I think those are the two main constituents
we're watching out for. So, the other thing I
wanted to make sure, actually, was this bit. So we're seeing if the
stats has the stat name, so I actually set
this up incorrectly. We wanted to say if ptarget
dot get stats here. So first, we're getting
the stat controller, and then we want to check
if stats dot G stat Name. Does not equal null. If
that's equal to null, then we'll return false. So first, we make sure
it has a stats block, then we make sure
it has the stat, and then we make sure that
we've actually set up this item correctly
to change the value. Okay, so once if we
pass all of that, then we can apply it. So down here at the bottom, I want to get the stat
controller one more time, so I'm just copy pasting that
from up here to down here. We get the SATs controller, and then we want to get the current value and then
add the change to that value. So our current value is going to be equal to stats
dot GET stat name, and then we're going to
take that and increment it. So let's say, our new value, it's going to be equal to current value plus
the value change. And then we set that
value on the Sts. So stats dot set stat
name to the new value, and I add a little typo there, so make sure those
names are correct. And then we want to return true. So we can also check
here, once again, just to make sure stats
is still actually found. So if stats is null,
we'll return false. And we might actually want
an error here because we're supposed to check if we can apply right
before we apply it. So push and error Stats
controller stats could not be found on percent S,
percent P target. So that's going to get the node and replace that
there. All right. Yeah, so for stats
that get name, we're basically changing
this to a float for now. The new value is a float, and we set that back
on the stat value. Now, some of our stats
like HP are an integer. So when we set that back
on the stats controller, it's going to cast it
back down to an integer, and it's going to
lose our float value. So the idea here
is that if you're going to be boosting a integer, then just make sure that you use non decimal values in the
actual stat boost item. So it loses a little
bit of clarity in that we're just grouping integers and floats into
the value change, but it gives us
the flexibility of being able to handle
the vast majority of our stats through one class rather than creating
one for HP change, one for damage percent
boost, et cetera. Just a lot less scripts that
things could go wrong in. So let's create
our HP boost item. And the items
folder, bottom left, I'm going to create
a new resource, which is going to be
a SAT boost item. So this will go on
the same folder. I'll call it HP Boost, underscore item dots, save it. Now on the right, we
have our SAT boost item. So the SAT we want to change
is going to be Max HP, and we'd want that to go up 50. Now, I do see a flaw here, which is that if we
boost the MX HP, it's not going to boost
the base HP as well. So we have to make a decision about how we want
to handle that. One way would be that whenever
we increase the MAX HP, we adjust the HP
automatically in our stack controller
script by that amount. So that's an option.
Let me check the SC controller if
that makes any sense. So search stack controller
dot gD and project, open that up, and we can see
we have the MAX HP thing. So we can see the change
by doing the MAXHP minus the and we could just say here
if the change is positive, then we affect the HP. However, I feel like that is really starting to
clump up this setter, and there may actually be some items we want to
change the Max HP, but not the base HP. So I think a slightly better way of doing it would be in
our stat boost item, we actually have a dictionary of stats we want to change, and then we loop through them. That way, we can do MaxHP
and HP at the same time. We could even have
an item that boosts, say, Max HP, HP, and attack all on one go, and that's just going to require a little bit of four loop. So let's change
this let's make it a export var Stat changes. And this will be a dictionary of string name with
the float values. Okay? So we'll get
rid of this now, and that'll give us
some immediate errors. So we want to make sure
each of these string names exist in the stat controller. So we could say four key
in stat changes dot keys. We'll just check if the key is actually in that controller. So if stats Git key is null, it doesn't have the stats, we can't modify it,
that's a problem. And then we can
just do down here. For value in stat
changes dot values. Then we'll check if the
value is equal to zero, then we're going to
have that issue there, so just replace
those with value. Okay, so technically right here, we're doing two loops, and that's sub optimal. So a better way of doing this would actually be to just get the value from the key. So I'll say var value is equal
to stat changes dot G key, and then we can check the value without
the extra four loop. So I'll cut that
and paste it here, remove the second four loop. That should make it a lot more efficient and easier to read because now we're only
dealing with this 14 Okay, now down here at the bottom, we need to do something
similar for key in stat changes dot keys. We need to get the value
for each of these. So let's tab that over, so
it's part of the four loop. And we'll say stats dot Get key. It's going to give us
the current value. We take the current value
plus the value change. That gives us the new value. So where do we get
the value change? Well, we need to say var
value change is afloat equal to stat changes dot
Get key. Uh huh. And then we have the
new value when we add those together, stats dot set. On the new value, hold on. Set name not declared in the scope because the
set name is the key. So we want to set the key with the new value
on the stats controller. Okay. Now, let's search
item in the project. I'll open up the HP boost item. Now we have the
ability to handle multiple stat changes in one go. So I will just do MaxHP
and that'll be 50. I want to do MXHP
before HP because I want to increase the MX HP
before I try to boost the HP. So the HP will also be 50 here. So we increase the MX HP, and then the HP will go up 50. If you're at FHP, it should
be at FHP a second time. And then let's call the
display name here, HP boost. The description will be
increases the maximum HP by 50. And then for icon, let's
create a new atlas texture. I'll expand that. Let's
quick load from our project, and I'll get the 16
by 16 sprite sheet. So let's edit region
and pick one out. So zoom in, do we have
anything like a health potion, something that
makes sense for us. There's a drop of blood
that could be viable. Or here's a heart. That's
another good option. Okay, I kind of like this flour. So let's get this
crimson blood red flour, and we'll choose that so
that can be our item icon. And let's test this
by adding it as a option in our player
level up scene. So in level up selection scene, I'm going to click on
our LevelUp selection. And we have our
item choices here. I'm going to quick
load a new item. So this is going to
be our HP boost item. We're going to give
it a drop chance of one and hit Add key value pair. Okay, so now we can hit
play, and we'll go in here. I'm going to level
up, and we're going to select our HP boost. Choose that, and
you can see, oh, all of a sudden, we have 160
HP, and that's how level up. So you can see from here
it would be very easy to increase other stats
like attack damage, cool down reduction, what else? Movement speed, et cetera, for your character as you
need to give upgrades.
54. Implementing HP Boost Pickups and Level Up UI Fixes: Wrap up with our upgrade system by creating a pickup
that will allow us to pick up and
immediately apply one of these items like
our HP boost item. So we're going to want to search our project for the pickups. So we have our object pickups. Let's create a HP boost pickup. Start by duplicating the
XP pickup two D scene. So I'll right click
and duplicate that and then this will be
HP Boost pickup, underscore two D, sure, and double click into that. Okay, so first thing
I want to do here is change the sprite to be the same as on my item. So I'm going to click
Animated Sprite twoD. Let's go to animation here
and create a new Sprite frames resource so that we
can set a different icon. I'll add frames from
a sprite sheet. Let's go into art. Not crimson fanta
see Raven icons, full sprite sheet,
16 by 16, zoom in. Make sure on the right,
you set the size to 16 by 16 here so it can divide the sprite
sheet appropriately. And yeah, we'll find
that flower I was using. So click that, add the frame. And boom, there's our item. I can look at the XP pickup, but this is actually
just a pickup that immediately
adds experience. So this script actually skips the whole necessity
of having the item. So what we're looking
for is more of an item pickup two D. So let's rename our base
node, item pickup D, and we're going to
right click it, detach the script, right
click attach a new script, and this is going to be a
item underscore pickup, two D, this is going to
extend from pickup to D. We create that, give it
the class name item pickup two D. So this is going to apply the pickup item to the
object that picked it up. So in the level
UI, we immediately applied the item to our player. Here's just a little different. We have to pick up the item
on the game map first, like we pick up the EXB
and then it's going to apply the item effect
directly to the player. If we add items in an inventory, that apply method would
be more like add one of the item to the inventory
array and go from there, but our game doesn't
need inventories, so we're basically skipping. But our game doesn't really need inventories aside from
our weapons loadout, which works a little different. So we don't really need to get into that for this project. So our item Pickup
two D, of course, we want to export var an
item of type item. Okay? So save that, and then we can click here and let's
assign if we want, we can assign the default item. Maybe I'll just leave
that blank, though. Let me take a look
at the base pickup two D script, right
click into it. So we need to implement
the take function. So let's copy that into item pickup two D.
I'll paste that in. We'll implement it in a second. I'm also going to do function underscore ready, returns void. And we're going to assert
that item is not null. Each item pickup must have
an item assigned to pick up. Actually, I think assert is
a little too strong here. I would probably go with
more like push error. So we can see what's going
wrong in the console, but it's not going to interrupt the game flow because when
we instance the item pickup, it's probably going
to be off of a drop, and we don't necessarily
want that to completely break the flow of
our game testing. So I think push error is a
little more appropriate here. So for we're going to remove the underscore
from the P target. So first, I'm going to say, I item dot can apply to P Target, then I am going to apply
it to the P target. So item dot try
apply to PTarget. And if that happens, I will remove it from
the scene quarter. Now, maybe it would be better
if the T function also had a boolean to return false. So here, the nature
of changing this to a Boolean means it
would have to go back to the base class
and change that. And then any class that also overrides the T function,
it'd have to change it. So that's one of the
flaws of depending too much on inheritance
for your classes, rather than using a
composition approach, like you see in the
inspector right now, composition would be more
like you have a bunch of different small scripts that each have their own
single purpose, instead of having
one base script extend a bunch of other scripts. So, for instance, player might
have extended character, and then there might
be another class above player like
Spearman or dragoon, which adds more stuff onto that. But then if you get
a lot of scripts directly depending
on each other, a small change like just
changing this to a Boolean might mean you need to update it in multiple places. So just
something to think about. Not really that
problematic here. Okay, so what we're
going to do with this item pickup is put
it on the game map now. So HP Boost Pickup. We have that. We've saved
this to the project. Oh, really, what I think
I want to make this do is make this a base
scene so I'm going to rename it item pickup two D. Now let's search
for pickups down here. So I'm going to right
click here and I'm going to do a new
inherited scene. And then in this inherited
scene, I can assign the item. So I'm going to quick load
our HP boost item and then save it back in item
pickup as HP Boost pickup. Two D. And then that is the scene that we put
into the game world. In a way, creating
an inherited scene like this is really similar to extending a script
and getting all of the base into your new object. But in certain
circumstances like this, where we're just changing
which type of item that scene setup has
the HP boost pickup, having the HP boost item, but otherwise just
being a normal item pickup two D. I
think in cases like that, this actually makes
a ton of sense. So we want to put our item
into the game world now. I'm going to search
for world dot TCN. Okay, I'll zoom in here. We have a player there, so
I'm going to search for the HP Boost pickup and try to drag this into
the game world. I could just put it in there, but let's put it under pickups first so that it has the
right parent W to move it. Let's put it over
there and maybe a second one over here just to make sure we can do it twice. Let's hit play, and then we're going to
go get our pickup. So I'm going to walk
over here, and boom, we have 150 HP. Here we can choose if we
want spear or scythe. Interesting, it's not providing the option for the third pickup. Maybe I need to be added in. And then I'm going to go here
and get our next HP boost. So there is our permanent
Sp boosting item, and that works a little
different than our EXP, which just directly
modifies the stab. So let's check the
level up selection, and I'll just make sure that
the item is still there. Somehow it got onset, so I'm going to quick
load the HP boost item value of one, added in. So now we'll hit
play one more time. I'll just make sure
it's working again. Get the HP boost from the
level up, select that. HP goes up, we select
here and here, so we get three HP boosts. That's working good. Very last thing I
want to adjust, and this is a quick one is that for our reward box selections, they don't scale down
to the same size. When one of them has
a longer description. So I want to jump into the
reward selection scene. Click on the root, and let me see here. One of these needs to
expand all the way. So actually, the quickest way to debug this would be to hit play. And while the game is running, I'm going to go over here
and we're going to test, change the size here. And then I'll tab back
and see if it updated. Okay, so our HP boost area,
let me check the root. This is set to fill expand. Maybe it's the description
that needs to do and expand. So we can see the root here has expand and fill set already. So I'm going to check
the margin container and make sure it's
also like that. So we'll do layout
container sizing. Fill. Okay, so another
thing I could look at remote view to see the notes that are actually currently
running in the game. And let's expand world
Canvas layer down to UI. We have the level of selection. We have our panel
container under that, the margin container,
the V box container, and, the Rewards box container
and our three selections. So why are these
sized differently? So we can see if we look
over on the right over here that the sizes for the
Y, they're all equal. That's what we would
expect looking at this. They're all stretched
all the way down. So let's find the level
where they're not. The smallest one
is the third one. So let me expand that. Click on Margin Container. We can see the margin container. Is the 20 pixels full
size, so that's not it. Let's go down to VBox container. The VBox container is 214. That's also what we
would expect because the margin container
has a margin. So I guess it is actually
this description box. You can click on the
nodes and actually see a box when it's selected
here. That's kind of cool. I think that might
actually be new. So we just want the description to push everything
all the way down. Okay, so we know what
we're trying to expand. So in the local view now
up here at the top left, let's click on description.
Ah, and there we go. There we go. So down here
in container sizing, it's set to shrink begin. I want that to actually
be fill and expand. Now if I I'll tap
back to the game, it is going all the
way to the bottom. Okay, so you can
see that, right? These three are going down. I'd also like the icon
sizes to be equal. So maybe we need to set a specific absolute
size for the icons. I think maybe the spear icon
was set using a bigger one, but I want to make sure that they all go to the right size. So if we click on icon Rec here, let's try changing keep
size to ignore size. And I think that's
making them equal now. Yeah, okay. It seems
to be lined up. If you have any other issues, that would be kind
of a debugging process you can use to kind of fix your UI while you
actually have the game running. So it's pretty helpful
because you can actually see how it's
going to immediately update you can change your control inside of
Gado and your local scene, and it will update immediately
in the remote view. So that's actually one of the really cool
things about Gudo. Okay, so with that, I think
our upgrade system pickups, everything we need,
in that regard is just working really good now. So that is really the bulk of the course up to this point. We're just going to have some polished things
to go through, like saving and loading
a score system, like how many enemies we've
killed between play sessions, gilving a game over screen
for when we actually lose. So we can go to
our main menu and restart the game,
that kind of stuff. But the core gameplay is
pretty much there now. So that's going to be
it for this video, and then we will
go into the wrap up and polishing
stage of the course.
55. Creating a Health Potion Pickup for Enemy Drops: I want to tack on one more video about doing pickups for
your game items in Gudo. So I created the pickup
for HP boost item, but I want to create an item that would be dropped
by the enemies. This will just be a
simple health potion. So I'm going to search for our HP boost item in the project, and let's duplicate
the HP boost pickup. So right click and go down to duplicate and we're going
to call it Health Potion. Two d dot TCN. Now we want to open
up that scene, Health potion tod dot TSCn. So our item here is going to be called Health Potion two D, just to indicate it's on
the two D game world. For the HP Broca
item on the right, let's click on the drop down
and then make this unique. So we're essentially taking the base and creating
a unique copy of it, and now I'm going to right click here and save it as
a health potion. So right click Save As, and then put it in the
project and items, and I'll do Health potion. Underscore item TRS. Let's change the stat changes to only increase the HP by 25, and let's remove the
Mx HP from this boost. Now for our display name, I'll say Health potion. And for the description, I'll say restores 25 HP, period. And then for the
icon, let's select another icon from our region
and find a Health potion. I'm sure there's a
few of them in here. So yeah, closer to the top, we have more than a few. Really Take your pick here. I'll go with the
typical looking one. And let's select that. And then we also need to
grab that same icon for the animated Sprite
two D. So click there, go to animation, do
a new Sprite frames, and then selectelect
from the frames. Art icons, full sprite
sheet 16 by 16, zoom into the top set
size to 16 by 16. And then control
middle mouse reel zoom into the top and grab
the same Health potion, which is right here. Should be only one selected. Hit add. Okay, and there's
our Health potion. So this should already
be able to work. Let me throw it into the
game world as a copy. So we have our Health
potion two D. I'll just put it in here
for right now, and then we can just
use the Health potion. So actually, the base
health starts at 50. So if I go over here,
you can see 50 goes to 75 out of 100.
So that's working. Okay, so then we need to go
into the skeleton scene, and we would need to modify
our drops component, but we actually never set up a drops for the skeleton.
So let's go to Oc. I'm just going to select
it here and Control C, copy it over to the skeleton. Then in the top right, we have our drop definitions, which is already going
to have the EXP drop with a weight of 1.0. So we want to add in the
ability to sometimes drop a HP potion instead of the EXP. I'm going to add an element, and I'm going to click
a new drop definition. Then just for testing purposes, I'll give it a five drop weight, which means 86% of the time it's going to
drop a health potion, not a EXP, just so you
can see it working. And then let's quick load our
health potion two D. Okay, so for our drops
that actually work, we also need to add it as a
call on our death animation. So let's click on the
animation player, go to Death, and we'll
do a call on drops here. So at the start, let's add
a call method on drops. And we're going to
right click insert a key to drops with one time. That's fine. There was another mistake I made
on the item pickup. When we push the error
for item is not null. That should be
actually pushing if it is null because we're
trying to verify that we actually
remembered to set an export item on
the item pickup. That was a simple
logic reversal there. Okay, now let's go ahead and hit Play and we'll try to
defeat a skeleton. I'll just collect some items
in XP here, get the site. And we'll just wait for
a skeleton to pop up. In the meantime, we can see that the orbs are still just
dropping their XP. Always, that's the
only possible drop, and there we have a skeleton. So if we defeat this guy, 86% of the time or so, it should drop a health potion. So that time didn't. We'll
have to defeat a couple more. And let's see. Do we get
the health potion to spawn? Yep, there we go. Health potion. So we actually saw that
both of them can spawn. Another health potion came from that skeleton, another
health potion. So you can see that
the weighting is definitely working
the vast majority of the time we're getting the health potion
instead of the EXP. So now that we
know it's working, we can just go to the skeleton. We'll go to drops. Let's take the drop definition on the
potion and make it a 0.1. So that means ten out of 11 times the EXP is going to drop, and one out of 11 times the health potion
is going to drop, and that's kind of how
the weighting works. You add in all the possibilities
plus their weights, and your odds are
your weight for that specific one divided by the total weight of
all of them together. Okay, and that's
pretty much how you add a health potion
into the game. So you can see that once
we have our stat item, our pickups that
give us a stat item, and we can just apply them
to the player and we can just set as a string name
what stat we're affecting, it becomes really easy to modify a lot of different
things on our character.
56. Implementing Camera Shake for Player Hit and Death Effects: We're now moving on to the
polished part of the course, which is going to wrap things up by adding in some
nice to halves. We're going to start
with a camera shake on hit for our player to make the game a little
bit more intense. And we'll also do
things like add in a game over screen and the
ability to track high score. So let's start off
with the camera shake. Let's go to world. And on the world, we have
a camera two D here. So we could just
attach a script right onto the camera two D. So
I'm going to click here, attach a script,
and let's say shake camera two D. Underscore
two D, I should say. And we'll create and
we'll put that in, I suppose, the UI folder. So open that up and create. I'll give it the
class name shake Camera two D. And let's
work on setting this up. So, first off, we need a
reference to our player like many other scripts so that
when the player takes damage, we can have the shake
effect trigger. So let's do Export var
context of player context. So just like before
with other UI elements, we get access to the stats
through the context. And then let's add
some variables to control our shake strength. So, first off, export var Max shake strength
which is a float, and I'll set that
to 10.0 by default. We can also have
a shake duration. So export var shake duration, which is a float,
and we'll set that to 0.3 seconds by default. And then I want a
separate set of variables for when
the character dies. So this should be an
extra strong shake to give the impact of, Oh, you lost your player,
you lost the game. So let's do Export Var
death shake strength, and that's going to be a
float up default to 20.0. And then we have at export
Var death shake duration, which I'll set to a float
of 0.6 seconds by default. So when our script starts, I want to get the
stats of the player. So I'm going to say
var underscore stats is going to be a stat
controller here, and then we need function
underscore ready. So we'll say context
player dot stats, and we're going to set
underscore stats equal to that. Let's assert that
that's not null. G stats does not equal null. Must have a reference to the player stats in order
to shake the camera. Okay, so when our camera is
shaking all over the place, we want to remember its original position so
that once we're done, we can return to that position. Let's have another
variable here. This is going to
be a vector two D, underscore original offset, which is going to
be a vector 20. Okay, remember the
equal sign there. So we're inferring it's a vector from vector two dot zero, and this makes it a
vector 20 as well. Okay, so our original
offset on ready is going to be equal to
our current offset. So this is the offset
of the camera because our script extends
this camera directly. And then we need to connect to the stats controller
HP changed signal. So underscore stats Hp
hanged dot Connect. And we're going to connect
that we're going to connect that to on HP changed. So let's create that
function down here, underscore on HP changed, and that's going to
have the stat data. So P data and this is
stat changed data. This time when we have
our callback function, I'm actually going
to use the P data directly because it's
very useful here for determining whether we're
doing a regular shake or we're doing a death shake. So if the HP change on the event is greater
or equal to zero, we just want to return
because we're not shaking on a healing or if the
HP doesn't change. So now we want to check if the event dot u value
is greater than Zarel. So is the character still
alive, essentially? So, if that's the
case, then we're going to use the normal shake values. Otherwise, we're going to
use the death shake value. So underscore shake
strength is going to be equal to our
max shake strength, and then underscore shake time is going to be equal
to our hold on. Our Shake duration. Okay, so we need those
local variables up here. So var underscores
shake strength is a float and Var underscores
Shake time is also a float. There's of course, going
to default to zero. Oh, and when I put event here, I actually meant Pdta. So pata dot change
and pdata dot neu. Okay, so otherwise, there's
one other case here, which is that the
health is below zero or equal or below
zero, I should say. In which case we're going to set the shake strength
to the death values. So underscore shake
strength is going to be equal to the death
shake strength, and the shake time
is going to be equal to the death shake duration. Okay, now, there's
one other thing here, which is that depending on
how healthy our character is, we may want to increase
or decrease the strength. So that's why I had MX
shake strength here. Right now, it's always
going to be that max of 10.0 I set up there. So I think a good way of
doing this would be to check how much damage you dealt versus what the max HP of
the character is. So if an attack does
half of their health, you want that to be a
pretty strong camera shake. But if it's just a damage
over time tick, like, Oh, it does one damage a second, that's pretty
inconsequential. You can just make that
a very small shake or completely eliminate
the shake altogether. So var, damage ratio is going to be equal to absolute value F, of the event dot hg. This is the amount of
health we just changed, and that's going
to be divided by the max of either one point oh. So while editing, I noticed that the math here is incorrect. Calling the max
between one point oh and the stats dot MX HP doesn't make a lot of
sense here because we actually want the ratio to be between the
change and health, the absolute value of that
anyway. And the MX HP. So really what we want to do
is say var percent change. And what we really
want this to say is the absolute value float of the p data dot change divided by underscore
stats dot MX HP. So that gives us our ratio, which should be 0-100. But we want to clamp that down. So let's say clamped ratio now. I'm going to cut away
this and let's say clamp F on the percent change, and we're going to force
it between 0.0 and 1.0. So if for some reason our change value was actually greater than our
total MX HP, like, let's say we took 9,000 damage
and our max HP was 100, then that would actually
be a percent change of 90. So we actually want
to just limit that to either 100% or if we're
going the other direction, it can't be below 0% change. So our total value is
going to be 0-100, no matter how big or
small the change was. Multiply Max shake strength
by the clamped ratio. The last thing we got
to do here to make sure this doesn't
actually return as zero is take the MX HP
and cast that to a float. So that's important
because otherwise, integer divided by an
integer would just drop the decimal point before we even convert it to
a percent change. Now we can go ahead
and run and make sure it's still
working as expected, so I will find a guy to hit us. Okay, and we still have
that screen shake, right? Of course, the death one
will be much bigger. So because we changed the math, you may also want to adjust the max shake strength and make that stronger,
like 20 or 30. And remember, this is
always going to be a percentage of the
max shake strength. So you need to factor that
in when you're actually figuring out how strong
the shake is going to be. So, let's see, at 30, max strength is kind of like
that. Maybe a little strong. So I might even drop
that down to 20. As more of a final number. Okay, now, finally,
we actually need the process for the shake. So let's go up to the top here. After underscore ready, I'll say function underscore
process Delta. So I'm just autofilling that. And then we're going to say, if the shake time is
greater than 0.0, then we're going to
actually shake the screen. So how we do that
is with some math. So we'll say underscore
shake underscore time is minus equal the Delta. So we remove the time from the time that we're elapsing here, and then I'm going to
get the X shake and the Y shake value and apply
that offset to the camera. So var shake X is going to
be equal to random F range of negative 1.0 to 1.0
times the shake strength. So essentially, we're
getting a random direction multiplied by our
current shake strength. And then var shake Y, of course, you can
imagine that's the same. Is going to be equal to
random F underscore range, negative 1.0 to 1.0 times
underscores shake strength. We take both of those, and we apply it to the camera offset. So offset is going to be equal the normal offset
plus vector two, and this is going to be
shake X and shake Y. So essentially on
every frame update, the camera is going to shake to a new random position slightly
off the original offset, and it'll do that until
the shake time is done. So if the shake time
is zero or less, so we'll say else, then we want to return the camera
offset to its originals. So offset is going to be
equal to original offset. Okay, and if I have this right, that is pretty much
our script there. The last thing we're
going to need to do is assign the player context
and the inspector. So quickly over here, drop down, quick load, player context. And let's run the game
and let our player get hit and see if it
works as expected. Okay, so here's an orc, and there's our camera shake
every time we get hit. And there's our camera
shake on death, which is much stronger,
as you can see. So essentially, that's our
whole camera shake effect. Next, obviously, would
be a good time to put in the GameOver screen so we can return to the game Start menu, which we also need to create.
57. Creating a Game Over Screen with Main Menu Navigation: One of the last things
we need to add is a GameOver screen and
a min menu screen. So GameOver is, of course, going to pop up when our
character is defeated. So we'll need to access
the player context again so we can check
if I live is false. So first, I'm going to open up the file system
and go to the UI, and we're going to find our
main gameplay UI Canvas that'll make it a
little easier to work on so that we can see where we're going to
put the game over screen. So for now, I think
I'm going to take the level up selection
and hide that. It'll make it easier to work on. And let's take the UI. I'm going to right
click Add a child Node. And so we want a new control this control is going
to be centered. So in the layout, let's do layout position
anchors and do center. Now, te click also go to mouse
and turn filter to Ignore. And let's right click and add a panel container to
this control node. So panel container.
That'll be the background. We'll take the panel
container and stretch it out. So zoom in, make sure panel container is
selected over here, and let's expand that. So our panel container we want centered on our parent control. So let's go to layout now and
do layout anchors center. That should put the parents anchors right here in the
center, so that's good. It'll make it centered
on the screen. Let's rename the parent
node to be GameOver screen. Also, right click
it and save it as a branch in the UI
folder, GameOver screen. So now we can just jump into
this and edit this purely. So our GameOver screen, we're of course going
to need something like a rich text label as the title. Let's right click Add
a rich text label. And I can just say
GameOver inside of you. We'll probably want to
make the roots based theme as our main
game theme as well. So quick load the theme here, game theme that'll give
us the Pix art font. Now we can also take
the Rich text label, and let's say fit content. We also want to
say centered text, and it appears to be
too large as well. That won't be an issue, I think, after we add the VBox
container in so, right, click on the
panel container, add a VBox container. Move the rich text
label under it. Okay, that fix the sizing of that since now it's
controlled by the container. Let's right click
on the VBox and add maybe a texture rat and then we'll get a
sad icon for there. And then we'll get
a GameOver icon. If we use texture and do
atlas texture, we expand it. And then for the atlas, let's quick load our icons. We could just use the 64 64 here if we're going to
keep that in the project. Edit region. Okay,
and then we need to zoom out and just select something that
would be good for, like, a game over. So there's a lot of
options to choose from. I'm thinking this skull that kind of looks
like it's sleeping. It's not too dark,
it's not too cute, so I think that might work. Let me move this out of the way so I can see the region editor. I might need to actually
change the size here. I'll try a step of four
on the X and four Y. That way, I can stretch
this out to the right, and then the left and the right are going
to match each other. I'll also pull the
bottom up a little bit. Okay, so with the step 44, that worked a little bit nice
so I can get close here. We zoom in on our
GameOver screen. I'm going to want I
think filter clip. I think also for
the stretch mode, we need to keep aspect centered, and that looks fine to me. So we will also add
in a description box. So, right click on the VBox, add another rich text label, and we can say, you
have been defeated. We try to try again. Sure, let's go with
that, fit content. Yep. And then we just
need a button here at the bottom to go
to the main menu. So, click on VBox, add a child node button. This button will say main menu. I'll attach a script to it. So this will be a new button. Okay, next, let's
take this button here and I'm going to rename it scene change button because we're going to go back
to the main menu. Let's attach a script to it. So this will be scene change
button dot GD. Create that. And we want to add
Export var a same path. So we can actually make
this an export file, and this would be quotations
star dot TSCN Okay, so our scene path is
a string and then on ready we'll connect to
its pressed signal, so pressed dot connect
on button pressed, and then function
underscore on button pressed will just
change to that scene. So let's do Gtree dot
change scene to file, and we put in the scene path, and then that'll
change our current scene to the main menu scene. As an important check, it would probably help if we say assert scene path is empty. Equals false and
say there should be a seeing path to change to
when this button is pressed. Okay. So one more thing I probably want is a
margin container. Let's right click
on GameOver screen, add a margin container. I'm going to put
it directly under the panel container and
then VBox under that. Okay? So now we
can set values on the margin container
and theme overrides, something like four
for all sides, and that'll make it
look a little nicer, giving a little bit of
a buffer for the text. And our game over screen, we need to make sure
that this will pop open the UI when the game is lost. So I'm going to attach
a new script here. UI game over screen. Create that. Might want a class name here,
game over screen. And let's get a reference to the player
through the context. So at Export vara Context
is of type player context. We'll say function underscore
ready, and on ready, we'll do context dot
player dot stats, and we'll get the Alive change. Let's connect to that and
say on player Alive changed. So we have our
function underscore on player Alive changed. I don't remember if that
had any parameters, so I'll click on the symbol Alive changed, and we'll check, it has the live Boole so go
back to the game over script, and we need that as
a parameter here. So P Alive is a Boolean. This will return void. And if P Live is equal to false, then we are going to show
our whole game menu here. Okay, now, our scene change
button style isn't going to work because we have
no main menu scene. So let's create a placeholder. Add a new scene user interface, and this will be main menu. I'll save it into the project. So let's just do
that at the root of the project main menu, save it. And then in the
game over screen, let's assign that main menu as a path to the scene path and the scene change
button like this. You can just drag and drop. That's the easiest way to do it. Okay, also, before I forget, go to Game Over screen and
assign the player context. So just quick load that
player context resource. Now we want to go to the world. We can see the game
over UI is right there. So let's see if we can
just test it real quick. And make sure we can go to
the main menu through it. So I hit Play. And
it's right there. I'm going to hit main
menu, and that gives us a blank screen
because we haven't actually added anything
to that scene. It's just a blank UI
element, essentially. So that is actually working. I think the other
thing we want to do is on ready for the
game over screen. Let's just make sure it hides. So we'll say hide on ready. In our gameplay UI Canvas, we may also want
to hide it there, but no need necessarily
mean if it helps. But I'll just leave it visible by default so we can
test the height. So it's hiding, and now we
just need to go get defeated, and it should pop up if
it's working correctly. So two hits, three hits,
four hits, five hits. There's our game over pop up, main menu, and the
flow is working. Last thing I think I need
here is just to resize this. So I'll take the
game over screen inside of our game
play UI Canvas, and let's take the layout form and take the scale to three. That should be big enough. So I can test it one more
time real quickly in game. Okay, so one hit, two it, three, four it, five
hit, game over. And we get the main menu button. And that's the basic flow.
58. High Score Tracking and UI Display for Enemy Defeats and Survival Time: One nice to have that our game is lacking is a high score, being able to track how many enemies we've
defeated in a game, and then remember that
score in a safe file for a high score sheet that we could see in the
main menu screen. So to implement that, we
have to first start by counting how many enemies we've defeated whenever an
enemy is defeated. So we're going to
need some combination of reporting on the death state to a stats manager or
high score manager, and then taking that
value after the game ends and saving that to
update the Safe file. So earlier on in the course, we added in a game manager Singleton and never
really did much with it. In fact, the way I implemented the game over screen
ended up just being using the player
context rather than having to report on the
game manager directly. So let's actually
give the game manager something of a purpose so we
can use it in the course. So we'll have an
object inside of a game manager for game stats, and then we'll track
that during the game. And then we set that
whenever we start a new. So in systems, I'm
going to right click and do a new script. Let's say it's game stats, which will be a ref
counted because we're actually going
to just make it a child of the game
manager Singleton. So open up game stats, and then let's say if
our enemies defeated, which will be an integer
that defaults to zero, and we'll need a class
name up here at the top. So game stats. And
then in game manager, I'll have a function start, which will return void, and that's going to
take our game stats and reset it or set
it to a new object. So for stats of game stats. This is going to be stats
equal to GameStats dot nu. Let's go with that for now. And when the world loads, I'm going to want to take the
game stats and reset that. So let's attach a
script to the world. All right click here,
attach a script. We'll put it in the base scene. So create that and I'll
say function ready, and we'll say game
manager dot SAT. Okay, so whatever
we need our game to load at the start, we'll
just have that there. So as long as we are using
the world script here, then we'll be able
to manage that. We may also want to
give this a class name. It could end up being
a pretty major script if we were to continue
with the series. So I think it deserves
the class name world. So now, whenever we
start a new game, it's essentially going
to reset the stats. And then maybe report game over. We could consider going
in here a little later and save high score
stats as to do. Okay, now I want to
modify our stats, and I want to report whenever
our stats are modified. So in game stats, we'll create a signal for our
enemies defeated. So signal enemies
defeated, changed. We'll take a amount and
integer as a parameter, and we'll follow
a similar pattern where whenever we
update the value, and we'll guard against
if it's the same values. I enemies defeated is equal
to value, we just return. Otherwise, we're
setting a new value. So enemies defeated
equals value, and then we say enemies defeated changed dot m with a new value. Okay, so just whenever
we update the property, we report it with a signal so that we can
observe the property. And I think that's about
all we need to set up a UI. Okay, so now that that's there, whenever we have the
Death state on, say, our orc or our skeleton, let's jump into the
death state script. So on our Death state, I'm thinking on Enter
makes the most sense for saying game manager dot stats, EMS defeated plus equals one, and that's how we will
update that counter keeping track of all the
enemies we've defeated. Now the last thing we need is a little UI in order to show how many enemies we've defeated and so inside of our
gameplay UI Canvas, let's add another let's just
make it a panel container. Add child panel container here, and we will put the layout as the anchors in the top right. I'll stretch it out
a bit like this. Offset it. Let me name this
to be Game Stats view. Then right click save a branch as a scene
and the UI, of course, jump into the scene, and let's right click it
and give it a script. So this will be Gamestts view. I can give it the class
name, gamests view. And in the two D view
where we edit it, let's add a label for our
actual enemies defeated. So right click Add child node. We'll start with a HBox
container, I think, and then right click Achil
Node, Rich Text label. I'll just duplicate that. So the first one will be
for showing the text, and then the second one
will be the number. So this will be the stat label. And then the bottom one
will be the amount label. Okay. So the SAT
label is going to consistently show
enemies defeated, colon, let's fit to content. And then the second one, I'll
pour square brackets, zero. So that's just a placeholder. And I think we need these to stretch out so we
can actually see them. So let's try going to layout, container sizing and
expand on the horizontal. Yep. And then amount label, same thing. Go to layout. Container sizing, expand on the, expand on the horizontal. Okay, so now this is kind
of what we get right now. Okay, so now take the game stats hit Q to go into select mode, and we'll shrink it
to what we need. A margin container would
also be nice here, so I'll right click and add
a child margin container, and let's put HBox container under that on the
margin container. Theme overrides
constants and maybe four pixels on each side.
That looks a lot better. I think that might be functional
enough for right now. I'll go back to game UI Canvas
and make sure that this is once again in the top right
hand corner as an anchors, top right, and then offset
it by hitting W to move it. Then just move it down off
the corner a little bit. Okay. And then I need to in
the script of GameStts view, I need to actually connect to
the signal on the Gamests. So function underscore already. We are going to do game
manager dot stats dot enemies defeated changed connect on
enemies defeated changed. I'll take that name and make
a new function down here, function on enemy
defeated changed, which is going to
take the P amount and integer return void, and we are going to
update the value there. Okay, so basically we
take the new amount and we set that to the label. So at Export VR. So anyway, we take our
enemies defeated label, and we set the text
equal to a string of the amount and that
is pretty much that. We just need a colon
here at the end. Maybe on ready, we want to assert that enemy's
defeated label exists. Must set enemies defeated label to show enemy defeated
count or mount. We'll be a little
bit more precise. Now, click on the Game Stats
View, jump into the scene. And inside of here, we want
to assign that amount label. So click on assign and
choose the amount label. And if I've got it right, that
should be everything here. So let's play and
give it a shot. All we need to do is defeat a couple enemies and see it
pop up in the top right. So problem here is that stats is not initiated into anything, so I want to right
click Lou symbol I guess the issue here is that the stats might not be set
before ready runs on the UI, since that's the
top of the world, I guess the children
ready first. So a better way to
do this, I guess, would be to say the stats
are equal to gamestts dot u. So this will always be
created when the game starts. And then we want to
call stats dot reset. So we'll just make a
new function for that. This will also mean
that we don't need to reconnect signals because we're still using the same object. So it is actually a
better way of doing. Let's jump into Game Stats and
create our reset function. So function reset. We just take everything
back to its default values. So enemies defeated equals zero, and that should be all
we need for right now. So now stats should
be set as soon as the game starts before
world is even loaded. And then from there, we should be able to
connect to the signal. Okay, let's try one more time. Alright, play. Okay, we got
a count in the top right. Of course, it's
way too small, but we can at least
kind of see that. So let's defeat one orc and
make sure it's working. And we got one there to show
up if we kill another guy. Okay? There we have
three. So yes, the count is working for sure. I just need to scale up the UI. So in gameplay Canvas,
we'll just do that here. A simple transform
scale of three. Move that over to
the right here. And now we need to actually cut off a lot
of this extra space. That's just taking up way
too much for no reason. So I'll need a little
bit of an edit there. Let's jump into
the gamests view. The amount label, I'll
actually change that back to zero without the square brackets so that before we
defeat the first enemy, it doesn't have
any wonky display. Okay, now for the
size being too big, I think a lot of
that is just that I manually set the
gamests view size. So let's shrink that over here
to what we actually need. So we can make these
a lot smaller. And this second view does not need to be nearly as
big as the first one. So there's no
reason to have them take up an equal
amount of space. I think what we might actually want is more of a
flow container. Let me change the type
of H box container to a flow H flow container. Okay, now I'll go down
to the amount label, and let's uncheck expand here. And then let's try taking the custom minimum size
and setting this to, like, 40 pixels,
probably big enough, so I can hold up to the
thousand digit at least. And then the rest
of the space is going to be occupied
by the enemy defeated. Let's also change the
view on the base to our default theme of
Quick load game theme. Yeah, okay, that's going
to have a big impact, too. So now, if we look at it, I can actually shrink this down. I should have done that
at the start, really. So something like
that, that's our space for the label and the
number over there. We can go to the game play UI Canvas and we can shrink
this down dramatically. You can see that
everything that's not that right side pixel just gets consumed by the
text label on the left. So it's better if both of these
elements aren't trying to take up an equal share
of the view size, but one is just
like a static size, and the rest just expands. And then I'll just put this a little bit more in the
corner over there. We'll hit Play. And see
how that turns out. Okay, that is much better,
more appropriate size. So, of course, just
to test again. We'll just defeat a guy, and that gets updated, so we have our enemies defeated count. So let's add one more
stat, which is how long our player has
survived in the game, another useful stat to have
and good for our high score. So in our Gamests view, I'm going to duplicate
this HBox container. Now we're going to
need a VBox container so they can layer on
top of each other. Right click on Margin
Container, add a VBox. Move both of these H boxes
on under it, like so, and now change the
stat label on the left to time survived colon. And if we go into GameStatsVew, it's going to be a
really similar setup. We'll just connect to
the stats of how long we've lived and show that
on the time survived area. So let's right click into Stats, look up symbol,
and open that up. So we're going to have
var time survived, which is a float equal to 0.0. We'll set the value on it. We shouldn't really need this
guard. It's just an extra. So I'll just say time
survived equals value. And then, of course,
time survived, changed emit, time survived. And then we'll have that
signal up here at the top. Time survived, changed. Which is going to
be a time float. Okay, so the whole
reason to have having this guard is just that
if for some reason, something in the game set the value to the value
it's already at, like, enemies defeated was
50, and now it's 50 again. I would trigger an
extra UI refresh. But honestly, first off, that's unlikely to happen. And second off, it
shouldn't really make a big deal out of anything. So it's more of just on
a technicality basis. It's only changed if
the value actually changed rather than really
a problem for the game. So I just wanted
to clarify that. Okay, now, our reset
also needs to take time survived and set that equal to 0.0 because we're
resetting the time. Okay, so let's see what else? In our game world, I'm thinking we want
to increment the time. So when the world is running, we'll have a function
underscore process. Going to take the game
manager dot stats dot TV plus equals the Delta. But this can only really occur
while the player is alive. So let's just do Export for the context at Export VR
context, player context. You can see where we're using this context resource all over the game in
different places. So having that as
a saved resource, is extremely useful
for being able to spread out the reference
for all of our UI and other nodes that need
to reference the player without directly having scene
reference to the player. So hopefully, I've made it
clear how helpful that can be. So we have the player context, and we only want to
increment the time survived if the player is alive. So if contact dot player
dot stats dot Live, then we'll increment the time. Otherwise, we're just
going to skip over that. And that should be
good enough for now I need a reference to
the new amount label. So we'll say at Export var, Time label is a rich text label, and this is the the
GameSts view, of course. So say game manager dot stats dot Time survive
change dot Connect, on Time survive
survived, changed. And we just have another
callback down here. Function on time
survived changed, gets the PT float. We're going to return void, and then we set the
text on the label. So time label dot
text is going to be equal to a string of the PT, and that's probably good enough. I'll just do an assert
on the time label as well. Make sure it's set. Need the time label set to
update the time survived. Okay. And in our Gamests view, let's assign that
other amount label. We may want to rename
these HBox containers. So the first container was the enemy's defeated container. And then the second one is
the time survived container. So just clarifying
which one's which. Let's make sure it's
in gameplay UI. We'll go out to the world,
see that it's still there. Hit play to test and run and see how
that's going to look. Ah, okay, so I haven't set the context in the
world, of course. So on the world node, we have to quick load
the player context. Okay? And now, if we run, the time should be
able to increment. And then we have the time
up there in the top right. Now, this is mostly working. I think there's a
couple issues here. One, it looks like we need to expand this a little
bit larger vertically. Oh, nice. When the
game's positive, it also stops. That's important. And then, secondly,
I don't think we need to show all of
the milliseconds. I think reducing this down
to one decimal place would be a little less
stressful on the eyes. You can see that the second and third decimals are flying. You can't even read
it. So there's no point in showing
that information. Okay, so to change our
text setting function, we're instead going to
do quotation percent one F. So this is going to give us the float value with
one decimal position, and then we give a
percent symbol so that we add the PT variable to
actually set up that string. Okay, and now we can
hit play and we'll see that the time only
shows the first decimal. That is ten times more
readable and less distracting. Major upgrade. So I think that is looking
great for our game stats. I think the only
thing I might change is put the time survived on top and the enemy is defeated on bottom and hit play
one more time. I think that's just going
to look a little better. Time survived. I think I want the most flashy
one in the top right, and then the other more minor
stats showing under it, the ones that are
going to update less. Just a minor visual
preference there. Okay. And just to show, we
can still defeat orc, and that counters working. So the game stats are good
in the gameplay at least.
59. Saving and Loading High Scores with JSON: Okay, now that our game
stats are working, we want to take
that and on defeat, we want to update
the high scores and save that to a JSON file. So this would be basically the start of a save load system. And this type of
game, specifically, there isn't so much a save
state as just high scores, though, but the process would be really similar if you
needed to save a game file. Basically, you need to
serialize data to a file, and one of the most
convenient ways to do that is JSON format. So for saving and
loading the data, I want to have a
save load object. Now, normally, I would make
this a separate system, but the only time
we need to save the data is when our
character is defeated, and we're going to handle that
through the game manager. So I'm actually just
going to in systems, create a new script, and I'm going to
call it save load. I'm not going to call it
system because this is more of a component for the
game manager itself. And this will be ref counted. So I'll create that script. Let's expand
systems. Open it up. We could give it the
class name save load. And we're going to
need two functions here function save stats, and this is going to take the current game stats
as a parameter. So stats of game stats, and we'll return void here. So this will take the
current stats and turn it into a
dictionary of data, and then we'll
serialize that data. So VR data is equal
to and I'll put in and I'll put in curly
brackets here at Colon, stats dot two dictionary. Now, this method, of
course, doesn't exist. We will write that in a minute. And once we have the
data in a dictionary, this is a dictionary
of data, by the way, I might make it more clear
by declaring the type there. But whenever you see these
curly brackets and GD script, you're talking
about a dictionary. So next we turn that
data into a JSON string. So that's very simple
to do as well. We'll say var JSON string is
going to be equal to JsunNU. So we have a new JSON
object which allows us to call this string of
Pi method onto our data, and that makes it in a JSON
readable JSON string format. So now we need to
open the file path. So we'll say var
file is going to be equal to fileaccess dot OP. So this is another static
class in the Godo library, and we need to give it a path and then file access of right. So we'll say Save File path, which we haven't defined yet, and then we want access here. So where are we going to get the safe file path up
here at the top? We'll say var, save
file path is a string. This will be equal to user
slash LASH gametatt JSON. Okay, so what this
Fopath basically means is it's going to
save in the app data, assuming you're on Windows, for this specific game project, in the root of that folder, there'll be a new file
called gamesatt JSON. So each game made in GADO has
a user folder in Windows, that's the app data slash Gdolah the name of
your project, I think. And then it's going to save
GameStatst JSON at the root. So this JSON file is
effectively your save file. For a survivor game, the say file is really
just your high stats because every time
you start a new game, you have a new character,
you don't save and load the game like you would
with a typical RPG. But the process is
more or less the same. So if the file exists and we had to open it up
for writing, correct? So if we don't find it, then there must
have been an error. But if we do find it, we'll say file dot store string because we have a
JSON string up there, right, so we just store the
JSON string in the file, and then we do file dot Close. And that's how you
write to the file. And otherwise, if the right
operation is failing, then we want to push
and error, of course. So push error is going to be failed to open sa
file for writing, and I could say at path, percent s, and we'll
do and the quotations. Then over here, percent
save file path. So giving as much
information as we can, and then we can debug from
there if there's an issue. Of course, you have to spell
porti error correctly. That would help, and that
is the safe function. Okay, now we want to do
our load Stats function. So function load stats, and we're going
to have the stats object we're loading
the data onto. So stats is of type game stats. Of course, we get this from
the game manager later. And first, we want to open
up the file for R to access. So VR file is equal to God,
I cannot type right now. VR file is equal to fileaccess dot OP at
the save file path. So we're loading and saving
to the same file path. And then we have Fleaccess
dot RED. Then we go. So let's say, if not file, so we failed to open, then
we can say, push error, failed to open file for
reading at Path percents, percent, save file path. We might actually even want
to make this a boolean. So if we make it a boolean, then we can return false here, as in we failed the operation. That might be a little
bit more helpful. Later, you can change Safsts to do the same
thing if you want. And we're returning false, not literally the keyword
so at this point, our files open for read access. So now we need to get the
string from the JSON file. So our JSOtext is equal
to file dot Get as text. Okay. And the reason
that's not showing is probably because I did
not type this here. So I could say file access. Let's see, JSON text should also be underscored there,
and we can keep going. So we've got the text from the file, so we're
done with the file. Let's close the
file, filet close. If the file is empty, then we can return here. So JS dot text is empty. Then you can either return
true or false here. It kind of depends
on what you want. I'll return true, actually, so that we can indicate that
the operation succeeded, but there was no data to load. So we'll say loads the stats data onto onto the
game stats object. So if the file is not empty, though, then we
need to convert it. So var JSON is going to
be equal to Jsunt Nu. So we have a new JSON object, and then we want
to parse the text. So let's say var error equals json dot parse on the JSON text. So if there's an
issue with that, will return an error. So if error, then we
want to push error, failed to parse JSON
with error percents, and I'll say percent error. I think that might add in some
extra useful information. And then we return false
here because, of course, if it erred on parsing the file, then there is a major
issue with our SAI file. It probably wasn't
formatted correctly. Okay, so if there was no error, we know that the parse worked correctly, so we can say var. A data of dictionary is
equal to json dot g data. So we're basically
turning it into the GIDORcognized
format as a dictionary. And then we'll say
Varsats data is a dictionary equal to data dot Get in quotation
Stats, I believe. And the default here
is a empty dictionary. So that stats should match
what we have up here. We saved the stats into
the stats key value pair, and we need to retrieve it
from the stats key value pair. So once we have our
actual stats data, which is a dictionary, then we can do stats dot
From dict for dictionary, and we're going to pass
in the stats data. And that's all we need
to do for our loading, so we return true. Okay, I think that looks
pretty much right. So what we need now is
a two dictionary and from dictionary method
for our game stats. So if we open up game stats, let's say function to dict
returns a dictionary, and then function
from dict is going to take a data dictionary
and return void. So to turn this
into a dictionary, we need a VR dictionary like that is going to be
equal to a new dictionary. And then, actually, you
know, inside of here, we can just wrap this
all in one thing and put each new key
value pair on a new line. So we'll say enemies defeated is equal to
enemies defeated. CD of colon for the
key value pair. On the left, you
have the string name and on the right, you
have the actual value. So on the left here,
we have time survived. And then colon, and we do time survived to the
variable on the right, and then we return
the dictionary. And it's really that simple. So next for from dictionary, we want to load the old values
into the current values. So we're going to do
enemies defeated, equals p data dot git, the key value pair
enemies defeated. And then the default value, let's make it null so that
if there isn't that value, we actually hit an error because otherwise we would load it
to something like zero, and that would permanently
override all of our save data. Kind of arguable which
way you want to go here. Like, do you want it to immediately fail
the save loading, or do you want it to succeed but then override the
old actual data, which doesn't really sound
very desirable, right? You don't want to override
the player's progress. So maybe it would be more like you need to release a hot
fix or something like that. If you run into
this circumstance where the JSON just
didn't load properly, you don't have this field, it's missing the name,
something like that. So that's why I'm
kind of going towards more like, Oh, it
should be null. So when it tries to set the float to a null or
the integer to a null, you're going to hit an
error, and it's got to be fixed or otherwise going to break all of
your players games. I think that is probably
the way to go here. So time survived is
going to be equal to pdta dot get on time
survived as a string, and then the default
value is Okay, so we have the game stats. Currently, the only game
stats is our main game stats, our current game stats, but we want to also have
a persistent game stats, our high score game stats. So I want to put a
function in here to take the highest value when comparing
to a current game stats. So I'll say function
set to highest, let's say, set high scores. To the P current game stats, and this is going
to be turn void. So we're only going
to call this on our high scores game stats, but this is going
to be basically we take the higher of
the two values. So the enemies defeated is
going to be equal to the max of enemies defeated or the P current dot
enemies defeated. Okay, so we're just
taking the higher value, and we're setting it to the
integer enemies defeated. And we do the same thing
with time survived. Time survived equals max. Time survived, and the P
current dot time survived. Okay, so that's how we
get the high scores. We just compare the current
stats at the end of the game and see if that's higher than our
current high scores. So we need the save load object, and we also need the
high score game stats in our game manager. So let's open up the
game manager now. Game manager dot GD. So we'll also have here
the var high score stats, which is going to
be a gameststnU and we'll also have here the var saveoad which is going to be equal to our save
load dot neu object. So this is just a local
component of our game manager. We might even make it private. Yeah, why not? Make it private. Because really the
saveoad logic here, we only want to belong
to the game manager. It's not really
something other scripts should be touching on,
so we make it private. So this function here, let's
say function End Game, I think is a little bit
of a better rename. That'll be turn void. So at the end of the game, we want to take our
high score stats, and we want to set high scores
with our current stats. And then we might want to
reset the current stats, but I'm actually not going to do that because we might
want to show in the main game menu what the
last session scores were, so we won't actually reset until we properly
start the game again. I think that's slightly better. Oh, and of course, after
we set the high scores, we want to call save
load dot save stats. So we're going to save
the high score stats. And I think on ready. Yeah, on function Ready. And remember, this is
Auto Load Singleton, so as soon as the game starts, it's already going to be loaded. And I want to do underscore save load dot load stats
onto the high score stats. And yeah, that actually
handles the rest. So the logic queue for saving
and loading is basically hidden inside of that
save load object or more like
encapsulated, really. So all the save load
is inside of here, but the game manager
actually handles when it should save
and load that data. And you can see this makes
the game manager a lot easier to read because we break its functions out into
different components rather than cramming
everything in one giant game manager script. So this video is going
on a little long, so I'll make the next video about building out
the main menu screen. We already have the template for it because our game over
button jumps to here, but we haven't set
anything up here. So we'll connect
this to the high score stats, have a display, a Start game button, and a splash screen
background of some kind.
60. Designing a Main Menu with High Score Display and Game Start Functionality: Going to take the layout
on High Scores container, and let's put it on the anchors of top or
right. Then I can zoom in. I'll hit Q to move this
position it about there. Let's stretch out our
high scores container, and then we'll build out some
other control nodes here. So I'll click here,
add a child node. Let's say Margin Container. Same story with
Margin container. We want a theme override, four pixels on all of those. And then to save some
time, I guess we could go to the Gamesets view. And I'm going to copy paste that time survived
and enemies defeated. I'm just going to
control see all of and let's paste it
over on the main menu. So inside of there. And I did forget
a VBox container, so add a VBox container
and then move those two text containers
under it. Like that. And then that's roughly how
it's going to look right now. Let's save the high scores
container in its own scene, right click, say
branchien scene, like so. And then I want to take
the layout transform and triple the scale, like that. Okay, so when we do
that, it's going to mess a little with
our Anchors preset, so I'm just going to
position this over there. No need to get it
really perfect. Let's also open up the
high scores container and add a title text. So zooming in here in
the VBox container, I'll right click and
add a rich text label. And let's position this on
top of the VBox container. Check Fit content, and
let's say high scores, but I'm going to preface that
with, let's say, rainbow. End the square brackets, go to the end Rainbow. Of course, for this
to work, we need to check BB code enabled, and there we get a
default rainbow. So I think I want to
lower the frequency and make that less dramatic. So let's say
frequency equals 0.5. Well, let's go to 0.25 even. Yeah. That's a lot better. And then we need
SAT for saturation. So let's say saturation
of 0.2, maybe 0.5. Okay, that's a bit
tamer. So that's good. And we'll do horizontal
alignment center. Okay, that's not too bad. So in main menu, we can see how
that's going to look. Back in high scores container, let's assign the right label. So Max defeated label is the amount label under
enemy's defeated container. And then longest survived label is in the time
survived container. Okay, save that, and
back on our main menu, we need a texture background. So I'm going to right
click on main menu. Let's add a texture act. I'll rename that to be
background wrecked. We'll come back to that.
Also, the main menu screen, we want to make sure we turn
off the mouse filtering. So mouse on the right, we turn filter to ignore.
That's important. Otherwise, the main menu would actually block the
buttons under it. So let's add in a button
now for our Start game. So add a button. Let's rename that to
Start game button. And if I recall, I just have a script that can switch
scenes immediately. So scene change button, I'm going to drag and drop this right onto the
Start game button. And then we need to give it
the scene path of the world. So in the top right, scene path, select world dot TCN, and now that button should be
able to open up the world, which is our gameplay scene. This button is really
small, of course, so we need to go to layout. And I'll triple the
scale down here. Let's also position it in anchors and then say center
bottom. We can zoom in here. I'm going to hit W. Let's
offset it up to here. Let's give it the
text Start game, and we may want to
select an icon, as well. So let's do icon
as atlas texture, Quick load, and atlas
from our game project. I'll do 64 or 64. Dit region and just
pick whatever icon you like for starting the
game. So I don't know. Since this is a
combat based game, maybe one of these
swords would be good. So I'll just kind of grab this, make sure to get all the pixels. So I need to stretch this
out to the left, hit close. Just in case I'll also
check filter clip. We don't want edge
pixels to get in there. Then that's basically
our start game. Button let's W and
make sure it's within the bounds of our main menu. Let's also go to Project, and we'll take the main menu and make that our
game launch scene. So in general Run, instead of launching
the world now, we're going to load. The main menu dots TSCN. Okay, now let's see
kind of where we're at. I'll hit play. We have the high score up there,
we have Start Game. Let's hit Start Game, which launches our main
gameplay scene. That's what we would expect.
And real quickly, actually, let's get defeated
so that we can see the high score
on our main menu. Of course, I renamed the Game Manager dot report game over, so we need to change this to
game manager dot end game. That's on the player
defeated state. Okay, now let's
launch one more time. So I'll just make sure I get defeated by one of these works, three hits, four
hits, five hits. And let's see. The players
already been freed. It's on the World note because we actually
freed the player. We also need to say
here for the worlds. If process function, where we're adding
the time survived, we'll have to say if
context out player. And we'll have to say
does not equal null. I think for previously
freed references, if you just do Context Out player, that'll
still hit an error, but if you say does
not equal null, then that will get around that. Okay, so basically, if the
player doesn't exist anymore, or the player is
no longer alive, then we're going to add time
survived to the game stats. Since the player got removed, this will handle that bit of it, and we should be good to go. Okay, so one more hitting play, making sure that
that actually runs. Oh, we can see that
the high score updated here, though,
so that's good. We might need to resize
the UI a little bit. As you can see,
the problem here, it's showing like
12 decimal points, so we will change
that in a minute. Let's see if we can get defeated faster than 7
seconds. Okay, let's go. One, two, hold on. Upgrade this gear,
and it failed. Okay. So we got to find another enemy or
two. There we go. 12 seconds. Main menu. You can see our longest
survive now is 12.6 seconds. Enemies defeated two. And let's close the game and hit play. That will verify
that the loading from file is actually working. So let's close and hit play. I may not have set it up yet. It's interesting to
see if it has or not. Okay, so yes, when
the game loads, the game manager is, like, one of the first things to load, and it already loads
into the high score what saved high score
from the file is. Okay, so as a cool time
to show where that's at, let's do present app data
in Windows File Explore, and then we want to find Godot. And then I believe
it's App user data, the latest project Script
Survivor's GD script. And right in here
we have ARJSOFle. Okay, so let's edit in Notepad. And there you can
see right there. Our actual game stats
saved to a file. We have enemies defeated at
two times survived at 12.6, and that is our saved game data, which updates whenever we get defeated if we happen to
be any of those numbers. Okay, so for the high
scores container, the longest survive text, we just kind of want to
copy over what we were doing in the game stats view. So I'm going to jump
into that script. And then I want to copy
over this bit right here, which takes the
flow and gives us a string that only has
one decimal place. Go to the high scores container, and I'll replace
that here with this. The PT, of course,
is actually going to be the game manager dot high
scorstts dot Time survived. Now if we at play, we
get to the game menu, we should see 12.6
seconds over here. That's much more readable
as our high score. So now for our background, I'll literally take a
screenshot of the game. And you could turn
off the UI for this if you wanted
to do a screenshot. That would probably be
a little more helpful. Okay, so let's also turn off the collision shapes before
we take the screenshot. So debug collision shapes off. Okay, so let's go ahead and grab our site as an upgrade
so we can show that. I'll leave a couple of these
pickups on the game map. We'll wait for the
skeleton to spawn, and then we'll see if we can get a nice screenshot in the game. One thing I would
consider would be turning up the spawn rate if we want to emphasize how many enemies there
are in the game. Okay, so let's screenshot here. I don't like that one so much. Let's maybe screenshot there. So, print screen to take a screenshot if that
wasn't obvious. And I'm going to open up Gap. So Gap is a free image editor. It's kind of like Photoshop. But free. So then
let's do File New, and I'll do 1920 by
1080 pixels. Open it. Control V to paste
in our screenshot, and let's grab
something like that. It's not really an image
editing tutorial here, but really quick, I'm going to kind of paste a
top bar in there. So using a black fill, then we can give it the
text if we so desire. I'll choose a font. So let's
go with pixel regular. You'd have to install that
font if you want to use that. Okay, so I just spent
a little bit of time creating kind of
like a custom thumbnail, except we're going
to be using that in the end game project. So just slapping on a black
background at the top, some Pixar text, mentioning
it's a GD course, mentioning it's a Do course,
and then the Gudo icon. This probably won't be the final thumbnail
for the course, but we can use it for the game menu screen
for now at least. And I will open this up. I exported it to the
project, of course. So where you have that saved, you can see I have the
main menu screen here. So what I want to do and the UI, this is actually
the wrong screen. We want the main menu.
We go to background Rec. I'm going to quick load, and I'll search for main menu
screen. Pop that in there. I may need to increase
the size here, so let's go to layout mode, anchors and do full wreck. Okay, so with this and
full wrecked now we want to make sure that nothing
is blocked by this. So I think we need to move
this to the top on the left. Okay, there the high
scores show we can play. Okay, and we can play, and we should see it there. Script Survivors high score
up here. It's a Gado course. We have the Start Game
button and the Gado icon, which I mean, pretty
solid for like five, 10 minutes of work, I think, of course, can be improved. Okay, so now at this point, pretty much the game
is just about done. We have our splash screen there. We can start the game,
the high scores, showing during gameplay
for the current scores and on the main menu screen for
the all time high scores. We can defeat the enemies.
We can lose the game. We can upgrade maybe we
want to do some cleanup, like removing these starting
upgrades from the game Map. That's a little weird
there. But that was just there for
testing purposes. Perhaps we want to ramp up the difficulty, add
new enemies in. We could have our main
character, you know, showing the spear
if we want that to be there rather
than kind of armless. But, I mean, it works
for what it is now. And we can level
up multiple times. It's not ready for full
release, of course. There'd still be a lot
of work to do there, but everything is basically working as intended.
Got the game over. There's our high score.
Perhaps the main menu could use a soundtrack, that kind of so I might do one more polish video to just kind of clean
up the course, but that is pretty
much going to be it. So I hope all of you
guys have learned a lot following
along to the end. I know this was a
pretty long course, 11 hours plus total
edited runtime. So, I mean, if you got
here and you followed everything along and your
game's working as it is, I mean, good job because that
was actually quite a lot. Okay, so thank you
guys so much for watching to roughly speaking, the end of my script
survivors course, GD script in Gadot 4.4.
61. Polishing Script Survivors ~ Gameplay Enhancements & Debugging: So in this video, we're
going to try to add some polish to our script
survivor's prototype. So first off, at the
end of the last video, I have seven errors
in the console. So that's the first thing
we're going to take care of. Anytime you see an error, it's a good time to work it out, debug it, figure out
what was going wrong. So we have item pickups that do not have an item
actually assigned to it. So I think what would
be really handy here would be to
say percent South, and then we put that
in here as percents. Okay, so you can see
that the error message here isn't as clear
as it could be. An item pickup needs
to have an item, but we don't know which item
pickup actually failed that. So how you can do that in
the item pickup two D script is at the end of
assigned to pickup, do a percents and then follow up the string
with a percent self. So that will replace the
percents with the node, which is going to resolve to a node name or in other words, which node actually didn't
have the item assigned. Okay, let's run
that one more time. I'll just play for a little bit, see if we hit any new issues. Okay, so since those item
pickups and the health portion, it says that the item
is not set on ready, which is weird because the at export definitely
should be doing that. So if the item is equal to null, then we report an error. Oh, okay, right, right.
So I made a mistake here. If that was an assert,
that would work fine. But what we actually
want to do is say, I item is equal to null, then we pushed the error. I was still in assert logic there. So
let's tab that over. If the item is null,
we push an error. Now, if we play, it
should work, of course. Otherwise, it was going
to always push an error. So you can see in the debugri down here, there's
no issues now. I'll collect some items. Let's get enemies to drop them, and that should work
just fine for that. So so far, it doesn't seem like there's any
other new errors. I did notice a little bit
of a UI layout issue here. So I think if I take
the scythe here, it might kind of reposition
this UI in some weird way. Oh, okay, that's
what's happened. The HB box was actually
expanding to get bigger. So to fix that, I either need to lock
the size in place. Boy, I should just make
it bigger and then move this over to the right. I think that would be better. Okay, so for this player UI, we need to expand it a little bit so that it has
room to grill. So I'll go into player UI. And then let's take the
root panel container, stretch this out to the right
so that it has extra space. I'll go to Game Play UI. Let's move this
over to the right. Now if I hit Play and we
happen to upgrade our health, and it resizes this it
should not actually push it further to the
right anymore because it already has space to fit
that full text label. And I do need to resize the
weapons layout a little bit. I offset it a little
incorrectly there, so I'll just line this
up to the top there, we'll hit play, start game. Okay, and that's
looking pretty solid. In the world, I do want to remove all these
pickups by default. Those were just for testing. Those shouldn't be in
the final prototype, so I'm going to cut those out with Control X, having
them all selected. And I do think I want to make the standard game play a
little more difficult. So I'm going to go to
the spawner system, and let's increase
the span rate. So I'm going to
pull this down at the start to a
higher spawn rate. Also add an extra point
here, kind of like that. Adjust the handlebars,
something like that. I just want to steeper
curve so that we get more enemies to spawn faster so that I can show off a
more intense gameplay. In the demo. Okay? And all play. Let's test it for
a little bit and see how that goes. So
I'll start the game. We want our enemies to
come up to the center. Maybe the orcs are
a little too slow. I might boost the
movement speed of both the orcs and the skeleton. So there's really no need
to make it easy here. Okay. So there we have our
skeletons and the orcs. So I'll boost the movement
speed of both of them. I'll look up the orc
stat controller. Let's change the orc stats. The speed can be, let's say, 35, and then let's
go to the skeleton. Sat controller speed of 60. I'll just make it
kind of hard here, and then the MX HP can
be like 100. All right. And the orc HP is 40. That's okay. Let's
play. Start game. One thing you could
add, I'm not going to do it here, but having, like, a enemy spawn burst at the start would make
the initial game play a little bit more intense
if you start the game by instantly spawning ten
guys to come at you. That might help.
But I think that's a little bit overkill
for just the prototype. So the works do move
a little faster, and the skeletons
are quite fast now. So this will definitely
be a lot more difficult. I do need to take the
current HP and set that to 100 at the start. Okay, maybe the skeletons are even a little
too fast, honestly. Okay, so let's fix the
players starting HP, player, stat controller, player
stats, HP to 100. And I'll nerve the
skeletons a little bit. Let's say 50 speed. I
think that's pretty fair. Okay, on the player
level definitions, I'm also going to make
the level up pretty easy. So I'll take level three
and set that to five. Level four will be
ten XP, level five, can be 15, level six, can be 20, and level
seven can be 25 HP. And then I'll just
add one more for 100. And that'll be like the one
you don't really get to. In fact, let's
just add one more. Let's say 1,000 new
level definition. The reason I'm making it
hard to go to is just so that it doesn't really happen. I kind
of messed that up. That was Level 1,000 takes ten XB. That's
not what I want. What I wanted, and let's
delete that is level nine is going to be
a level definition with 1,000 EXP needed, add that. So this just makes it so
that I don't have to plan for that amount of scaling
in the prototype demo. It would just be kind of
overkill for right now. So let's hit Play, and I'll
see how the game plays going. Start the game, and we'll wait for the first
enemies to spawn. Okay, I think it's a little
too easy at the start. I want it to be intense quickly. So I think what I will do is actually put in that
instant spawn of ten dudes. So in the world, let's say
at export V initial spawns, and I'll set that
as N equal to ten. The world will have
a reference to the spawner system,
so spawner system. And then on ready, I'll say, I Initial spans is above zero. Then we'll say spawner system. Let me see. What were
the functions called? Random spawn. And do I call
that ten times? Hold on. Okay. Yeah, and it looks
like I need to call that ten times or the
number of initial spans. So for a time in initial spawns, we're going to call
spaorsystem dot random span. So ten initial spawns, this should make us spawn ten dudes at the
start of the game. Okay, don't forget to set the sponnor system in the world. So assign that sponnor system. Direct node reference,
let's hit Play. And if this works, we'll have ten dudes at the
start of the game, which should make
it much harder. Okay, yeah, that is a lot of guys for the
start of the game. So we just have to
avoid all of them. That might actually be
too hard, honestly. I'm going to nerve that to five, and I'll change that to five
in the script, as well. I think ten guys at the start when you
haven't even leveled up is it's just a lot
to deal with a one go. Okay, so we have our
five guys starting now. Let's get an XP. I'll upgrade with the
Syth so that'll give us an extra way to hit
enemies. Much easier now. And I'll just keep going here. Let's get some upgrades since we have the EXP curve
made a lot easier, then we should be able to
level up pretty easily. But I got to say,
increasing the speed on the enemies definitely makes it a lot harder to dodge them, so that's something
to keep in mind. Let's keep upgrading the spear. So I think at a certain level, the spear hits two guys at
once. It's already there. Level three spear
can hit two guys. Which is definitely
a huge upgrade. I'm just kind of going
through the gameplay here. The skeletons at 50
speed are pretty tough. 60 speed for the skeletons
would definitely be too much. Let's just keep going
with the spear. Based on the current settings, I actually have no idea if the spear or the
scythe is better. Probably with large
groups like this, the scythe would
be better because, you know, it can hit
all of them at once. But sometimes single
target is important, too. I mean, we're just
getting into, like, how to play the game
now at this point. So that is a ton of skeletons, and at some point, they're
going to kind of catch me. We can see a little
bit of an issue, which is that kind of clump
up too much together. So I do want them to not
stack on each other, but I think their
collision shapes are probably too big right now. This is kind of making
them unable to move. So we actually dun into
a nice issue we can fix. Let's pause the game, and then I'm going to
take the skeletons. On all the scenes, let's
take the collision shape, and I'm going to
shrink that a lot. Let's make that like a two
and then we'll go to the orc. We'll take his collision shape. This is the base character
body collision shape, and let's take that
collision shape, expand it, and set
that to two as well. If we unpause the game, if that's working correctly, and I think it kind of is. I can't say 100%. Then because their
collision shape is smaller, they'll clump less. But, yeah, it's still pretty hard when there's this
many guys coming at you. If there was like,
projectiles too, then you definitely have a pretty difficult
game at this point. So I can get a
couple more things. Let's upgrade the
scythe this time. And let's just go for
the game over screen. I could probably run
around for a while, but we just want to verify everything still working
with the game polish, fruity, solid
prototype in there. And let's go to main menu. We have our time survived. I think I do need to bring
that out to the left a bit just so that there's room for the decimal point in this case. Aside from that, I think we
are looking pretty good. So on the main menu, so I'll just expand this
out to the left a bit, get everything on one line. That'll look better. And maybe I put on the music on
the main menu as well. So I'll right click at a node. Let's do audioStream
player not two D, because, I mean, there's
no two D world now. We're just looking
at the game menu. Let's quick load
our AudioStream, so.p3 because I need to go
find it in the project. Okay, so let's add
it in the audio now from the audio folder. We have three Red
Hearts Box jump. We'll just use that for
the main menu as well. I'm gonna put on headphones so I can kind of test
this for myself. Let's do play. Oh, I didn't
actually audio play. Okay, so let's do auto playing. Yeah. And then play. Okay, actually, I want to
check the main game world, and I want to see
how loud that is. It's -40 decibels. Okay, so I even want that to be a little quieter
on the main menu. So let's make that -45
decibels. I'll hit Play. Test it out. Okay, and
then we go to Start Game. It's a bit louder here.
Maybe the endgame music I make a little louder
than that, too. Okay. So the endgame world, I'm going to take that and
set it to negative 35. I think I made it super quiet during the course because I just didn't want it to
be an interference when I was talking over it. Okay, so let's hit Play. We can start the game.
Okay, that's good. One other sound effect
I might want to add is whenever we pop up the
level up selection screen, I think that could
use a sound effect. Aside from that, I think
we're looking good. So let's go up to the
level up selection screen. Alright, click Add a
audioStrem player here. Let me see if we have
some good level up sound effects in the project. So it's going to be under audio UI menu.
Sure, let me check. Okay, use item seems
like it would work. So I'll take the audio
stream player here. We'll pop in use item. This will not Autoplay, so I need to go to the
LevelUp selection. We'll add in a
Export var pop up. AudioStream, which will
be a AudioStream player. So we're going to say pop
up audiostream dot play, and I just got to
fix that export. Okay, and then be sure
to add the pop up AudioStream in the export. So we have to add that in. We could do at
ready assert if we want assert pop up AudioStream. Remember to set the
audioStream player for playing level up sound. Okay, now let's hit play.
We can go to Start Game. And let's test by defeating
an enemy, we level up. And then we take our upgrade. The music continues. And I think that flows pretty nicely. It's definitely nice to have an actual sound effect
for the veo up. So let's keep going
with Syth this time. The Syth definitely
putting in war here. It might be OP, actually. Okay, so Syth four,
let's see how that goes. Definitely, it's really hard. That's a big deal. Since
it's basically an AOE, it probably shouldn't be doing this much damage,
let's be honest. Okay, I think I've
seen enough there. I'm going to just
buff the spear. And make spear more
fun to actually use. So let's jump at player,
the spear weapon. Let's jump into the
scene definition. We can just buff it up however much we
feel like, I think. Level one short. We can
leave that as the defaults. Level two, three,
four, five. Let's see. For level two, I'll just make
it deal 20 to 30 damage, and the cool down. Yeah, we can do 0.9. Level three will be 30 to 35. I think the spear can also do two hits at level
two. That's fine. And then level three,
we'll do 30 or 40 damage. Okay, so the max hits two, and then we'll bump up the projectiles to
two at level four, and this will be 35 to 45
damage at 0.8 cool down. And then let's do
40 to 45 damage. It can go faster. Let's
make the cool down 0.6. The projectiles can be why not? Let's just say
three projectiles. That seems pretty
bonkers, but hey. And then I'll do a quick
run through with the spear. Let's see if the spear can
even compete with the scythe. Okay, more spears. That should give us two projectiles.
That should be a lot better. Getting to Level four
was a little rough. I'm not going to
balance this too much, but I did want to make
it at least bearable. Oh, God. The spawning is
getting kind of ridiculous. Oh, I forgot to loop the music. Okay, open up the game world. That's pretty important.
So game world. Let's open up that scene. We have the audio stream player. We want to take this
music and check loop. So I think we need to
do that in the project. Let's do three. Double click
on this and enable looping. Alright. Close.
And then autoplay. So on your audio stream player, let's check Looping
on for that audio. That should also be on the
main menu screen just in case. So here parameters, looping on, and then I'll unpause the game. So that won't start until
we get back in there. So since we hit
Level five Spear, we cannot select spear anymore. That's good to double check
and make sure that, you know, it doesn't irr out by trying
to level up to Level six. So we can still choose
the scythe, of course. Okay. And I'm just going to
let myself die at this point. I think Yep. Everything seems
pretty much okay. Oh, and the high
score screen here, if that's still not
displaying correctly, let's jump into it. So for the high scores, to make sure that there's
enough space here, let's set a minimum
size on these. I'm going to set this
to 60.0 and then let's expand the high score
base over to the left. Okay, also take
the amount label, and let's do a minimum
size of 60, okay? And we can take the high scores and make that even
bigger to the left. All right. And then hit play. And just make sure
that the high scores don't go over the edge anywhere. So this would be good for at least one more digit, I think, without going over the edge
and wrapping or anything. Okay, so at this
point, pretty much, I think the prototype
is relatively polished. I'm sure there are tweaks
we can make in the code and content to be added to the game and just make
it a full fledged game. But as far as the course goes, showing the basics of
how to set all this up, a survivor like game
and GD Script Gudo 4.4 I think that is pretty much about
all there is to show. So I hope you guys
really enjoyed following Long all
the way to the end. There may be a few
small update videos, but that is pretty much it. So thanks again for
watching Iben Chris, and I will see you guys in
my future video content.