Transcripts
1. 0-0 Trailer: Welcome to my course
on how to build ametroivnia in the
Gadot game engine. My name is Thomas Ionizlo
and I'll be your instructor. I have a bachelor's degree
in computer science, and I've been teaching
game development across multiple platforms
for over five years. I've included a
complete set of assets, including sprites,
sound effects, and music that you
can use to follow along or you can use your
own assets if you prefer. This course will help you become familiar with the
GdoGame engine. It's very efficient workflow for building games and its native scripting
language GD script. You'll learn how to build
and edit scenes using Gado's various nodes and alter their behavior
with scripts. While writing these scripts, you'll learn how to apply object oriented
programming principles to keep your code concise, scalable, and reusable
for other projects. Most importantly,
you'll learn how to structure a complex project of this magnitude by breaking it down into its
individual components. We'll start by building and animating a two D
platforming character. Then build an environment
for the character to explore and move
the game's camera. Next, we'll allow the character
to transition between different rooms and
draw the world on a map to help the player
explore your world. After saving and loading
the player's progress, we'll draw a heads up display. Then add our
unlockable abilities, including a double jump, a wall jump, a dash, and shooting a projectile. Finally, we'll add in
some enemies for combat, a boss fight, and keep track of the player's
progress through the game. Whether you're new
to game development, new to the Gadot game engine or just new to Metroid venias, this course will teach you
everything you need to know to build a complete
game from the ground up. If you need any help, our
discord server is full of other students and
budding game developers who can answer any
of your questions. Click the link in
my profile to join. You can preview the first two sections of the course for free, and I have another course
that covers the basics of GD Script programming
completely for free, too. Once you're ready,
we can get started learning how to build
a metrovnia in cado.
2. 0-1 Download: Hello friends. In order to
follow along with this course, you'll need the Godot engine. Open any web browser
and head over to godot.org and download
the latest version, which as of the recording
of this video is 4.5 0.1. The engine is free to use, but you may optionally choose to donate to support the
development if you want to. After the download is complete, you'll need to extract the
files from the zipped folder, then store them in a safe
location on your computer. To run the editor,
all you have to do is run the larger file that
doesn't end in console. There are no installers or
launchers to worry about. The first time you open Godot, your project list will be empty, and it will ask you if
you want to explore some of the example projects
that are available. We're going to ignore this
and create our first project, which opens a new window. Here we can decide where
to save the project. The default location is
in the documents folder, but it's a good idea to create a separate subfolder to
hold your Godot projects. Next, we can give
our project a name. Don't worry about this
too much right now. You can always change it later. A new subfolder will
also be created with the same name to hold all of the files related
to this project. Three different renderers
can be selected, each optimized for
different purposes. I recommend starting all
of your projects using the compatibility renderer
to start since it is best for web Builds and only
switch your renderer later if you need better support for better quality
visual effects. Version Control with Gi
Hub is automatically supported and I highly recommend
learning how to use it. Creating this
project, it will be opened automatically
so we can edit it. I'm going to close the
editor though and reopen it. Our project is now included
in the project list, and we can open it either
by double clicking on it or selecting it from the list and clicking the Edit button. We now have access to the Godot and we can get started
editing our first project. In the next video, we'll explore the layout of
the editor window. I'll see you in the next video.
3. 0-2 Editor: Hello, friends. Before we
start editing our project, we'll need to
familiarize ourselves with the layout of the
Godot Editor window. The majority of the
window is filled with a three D view of
the current scene. This project will be in two D, not three D. We can
switch the view between three D and two D using the buttons at
the top of the window. In two D, we have the
X axis drawn in red, the Y axis drawn in green, and a blue rectangle
which shows the viewport, or what will actually be drawn on screen when
running the game. Note that the viewport
is below the X axis with Y equals zero representing
the top of the screen. The Y axis is inverted, so positive values for Y will move down the
Y axis, not up. To the upper left, we have a tab for the current
scene's structure, which is currently empty and is asking us to create a
root node for this scene. Since our game will be two D, let's make a two D scene. The scene tab now shows
our scenes root node, a node two D. We'll explore more about scenes
and nodes in the next video. This dock also contains
the Import tab, which will allow us to change
the import settings for imported files we will use in our project like
graphics and audio. These files will be stored and organized within the
project file system, which we can see
in the lower left. Currently, our project
has only one file in it, which is the Gadot icon. In the file system tab, we can keep our project
files organized by creating folders and searching through
them using the filter. Returning to the scene tab, if we have a note selected, then its properties will be presented in the Inspector
tab on the right. We can expand categories of these properties and
edit their values here. This dock also
contains the node tab, which provides a list of
signals this node can emit, as well as which groups
it is a part of. The history tab contains a
list of every change we have made within the current scene
or for the entire project. You can see all of
the changes we made, plus I also edited
the Editor Fontie. The bottom dock is
currently collapsed, but we can click on the
output button to expand it. The output will also show
us changes we make to the project and also display
text output from our game, which is very useful
for debugging. We can manually
output messages here, but the editor will also output warnings and errors here too. The debugger has a variety
of useful tools to help us figure out what is slowing down or
crashing our game. There are also tabs
specifically for audio, animation, shaders and more tabs will be added here
as they are needed. We can collapse this dock by clicking the
currently selected tab. By clicking on the three dots, these docks can be rearranged
to fit your needs, however you think will work best to accommodate your workflow. The individual tabs can also
be dragged between docs. When you find a layout
that works well for you, you can save it from
the editor menu. You can also change a variety of editor settings to personalize the editor to fit your needs. When we eventually have
a game to actually run, we can do so from the buttons
in the top right corner. Either playing the game from the beginning or only
running the current scene, pausing or stopping the game. In the next video, we'll
learn how to build a scene. I'll see you in the next video.
4. 0-3 Scenes: Hello, friends. Now that we're familiar with the
layout of the editor, let's start editing
our first scene. We'll start by
clicking two D scene, making the root node
of our scene a node two D. A node two D doesn't actually
represent anything in our game. It is only a two
dimensional transform. A transform is a combination of a position, rotation, and scale. Godot also includes a skew
component in two D transforms. If we actually want
to draw anything, we'll need to add another
node to our scene. Either right clicking on
the root node and selecting Add child or clicking
on the Plus button, we can add a child
node to our scene. There are a lot of
different kinds of nodes that all serve
different purposes. The most common way
of drawing something in two D games is
using a sprite. So we'll add a Sprite
two D node to our scene. Node types inherit
from other node types. So a sprite two D node is also a node two D and a canvas item, and a basic node, and has all of the same properties
inherited from these types, plus some more of its own. The Sprite two D node is added as a child
of the root node, indicated by the indentation. Scenes are organized
into a hierarchy, starting with a
singular root node then branching into any
number of child nodes. These child nodes can also have their own child nodes
forming the scene tree. We can collapse
branches of the tree or the entire tree by clicking
on the arrow beside them, and again to expand them. The sprite node
still isn't drawing anything because it hasn't
been told what to draw. Selecting it, so we can see its properties
in the inspector, where it indeed has properties
inherited from node, Canvas item, and node D. Under the Sprite two D section, it also has a texture property, which is currently empty. We can load a
texture file or drag the GAD icon from the file system tab into the texture field
to populate it. And now the Sprite
two D node is drawing the GADO icon texture as
a sprite in our game. Let's duplicate this
Sprite two D node in the scene tree by right clicking and selecting duplicate or selecting the node and
pressing Control or Command D. The duplicate has all of the same properties
as the original node and is renamed by appending an
increasing number to its name. Siblings in the scene tree are not allowed to
have the same name. We can move either
of our sprites around in the scene view
by simply dragging them. This will automatically update their transformed two D position property in the inspector. We can also rotate the sprite, scale it, and skew
it if we want to. Next, let's drag this Sprite Tu Di node
onto the other one, assigning it to be a child node of the other Sprite Tu Di node, indicated by another
level of indentation. Now, if we select the parent Sprite tu Di node and edit
its transform properties, these changes are
inherited by the child. The child's own
transform properties are defined in relation
to that of the parent. This hierarchical structure
of scenes and inheritance of properties allows us to build complex scenes with multiple
different components. We can add any number of different nodes to the
scene of different types, such as an audio stream
player two D node, and a GPU particle two D node. Now this two D scene can
draw something, emit sounds, and particles, and all
of these components will inherit to their transform
position from the root node. To make things easier, the transform of a root
node should always have all default values and have only child nodes with
their transforms edited. We can rename nodes, which is particularly useful for the root node of a scene
to describe what it is. Let's call it
something like robot. Then we can save this scene as a scene file in our project, and it will automatically be named the same as the root node. Then create a new
two D scene and imagine this to represent a
room containing the robot. Our robot scene can be added to the scene
as a single node. We can even have multiple
robots if we want to. From the context of
this larger scene, we can now edit the
transform properties of a robot to move it around, rotate, scale, skew, et cetera. Let's rename this scene
Room and save it. If we return to the robot scene and edit the robot's
child nodes, these changes will be applied to all instances of the
robot in other scenes. The room scene does
not need to be concerned with how the
robot is constructed, only where it is
within the room. Constructing scenes from individual components
allows us to keep the details of
individual objects contained and hidden
away from larger scopes. It also allows us to easily produce multiple copies
of the same thing, but with only one source for that object that
needs to be edited. Building an entire game
will consist of building many smaller scenes that represent individual
game objects, characters, and locations,
then combining multiple of these scenes
together to create a larger scene while
the game is running. In the next video, we'll
explore how the engine provides useful built in node types for
simulating physics. I'll see you in the next video.
5. 0-4 Physics: Hello, friends. Now that we understand how
scenes are constructed, we'll go over how game
engines provide built in functionality to make game
development much easier. We can get rid of the robots and delete that scene
from the project. Let's start by adding a
rigid body to our scene. In the context of physics, a body is any object which can collide with other
objects and has mass. A rigid body represents
a solid object. It will be affected by
external forces like gravity, collide with other
solid objects, but will not deform or break. It will maintain its shape. This rigid body needs a shape, which is another
node which must be added as a direct child
of the rigid body. This shape node doesn't
know what shape it is until we give it a shape
resource in the inspector. From the drop down, we can select a few different options. I'll make a rectangle shape. Now this rigid body has a shape, but nothing is
actually being drawn. So we'll add a
Sprite two D node to it and have it draw the
Godot icon texture. The sprite and collision
shape do not match. So selecting the
collision shape node, we can adjust it to better fit
the sprite by clicking and dragging the red dots or by editing its properties
in the inspector. Holding down the Alt key with the rigid body
node selected, let's position it
to be somewhere around the top middle
of the screen. Then press the run
current scene button or press F six as a shortcut. The game engine automatically
applies forces to the rigid body to simulate real world physics like gravity. The mass and gravity scale
can be quickly adjusted in the inspector to tweak
how the rigid body behaves. We can even give it
a physics material to add friction or bounce, adjust it center of mass, and many other options. For now, let's have this simple
box collide with a floor. Very sturdy objects which
we do not want to move are usually constructed
from a static body instead of a rigid body. A static body can still
collide with other bodies, but it won't move as a result of that collision and won't be affected by forces like gravity. A static body also needs
a collision shape. From the drop down, I'll
select world boundary. Unlike other shapes, a world boundary
extends into infinity. In two D, this is
represented as a line. The arrow in the two D view points away from the boundary. By default, it will
create a floor. Moving the position of the static body node
down to the bottom of the Viewport will stop the rigid body from falling
beyond this world boundary. Let's duplicate the
rigid body and see how it behaves when colliding
with another rigid body. We can click on a node
and change its type. So let's change this rigid
body into a static body. Then position and
rotate it so that the rigid body will
collide with it in midair. Remember to rename nodes after you change their type so you
know what type they are. But you can also tell by the
icon beside their name, too. If we were to manually program
all of this ourselves, it would take a lot of work
to draw these sprites, calculate how they are
affected by forces, figure out when they
collide with each other, and determine how that
affects their behavior. Along with physics, game engines help us by automating a lot of the functionality
common in most games to drastically speed up
and simplify development. In the next video,
we'll learn how to attach scripts to nodes
to edit their behavior. I'll see you in the next video.
6. 0-5 Scripts: Hello, friends. While
the game engine gives us a lot of built
in functionality, we can also add our own by
attaching scripts to nodes. Selecting any node
in the scene tree, I'll use the static body
that is a floating box. We can either right
click and select attached script or press
the attached script button. This opens a new window. The script will
automatically inherit from the type of node it
is being attached to, and we can start our script
from a template if we want. We will also specify a
name for the script, which should reflect
its purpose. I'll name my script spin, as well as the folder
it is saved in. Clicking the Create button will automatically switch the
view to script view, so we can start
editing the script. The template starts us off with two functions,
ready and process. Any text following the
Octothorp symbol is a comment. It is not used by the
engine and only exists for our benefit to help us
better understand the script. As these comments describe, the ready function will
automatically be called when this node enters the
scene tree for the first time, which is a little
bit misleading. But for now, it will mean
when the game first starts. Process, on the other hand, will be called every frame
while the game is running, which with default
project settings will be 60 times per second. A function declaration starts
with the keyword funk, followed by the name
of the function. These function names
start with an underscore, which is the way in which Gudo marks functions as private, meaning they are
only meant to be used within the context
of this script. The function name is
followed by parentheses, which will contain parameters for the function
if there are any. The process function
has one parameter, Delta, which is the number of seconds which have passed
since the previous frame. So this will most
often be one 60th, but will sometimes increase if processing a frame takes
longer than expected. The parameters are followed by a hyphen greater than an arrow, then the return type
of the function, which in both cases are void, which means they do
not return any value. Declaring return
types is optional, and these can be
removed if you prefer. The contents of a function are indented one tab,
no need for braces. Both of these functions
contain only the keyword pass, which basically
means do nothing. But we can replace this instruction with
something different. To see how this
works, let's change the body of the ready
function to do something. The print function will
output anything we want to the output panel
when the game runs. We can call functions
by specifying their name followed
by parentheses. Then inside the parentheses, we can provide arguments matching the functions
declared parameters. Inside quotation marks, we
can write anything we want, such as ready to spin. Anything we put inside quotation marks is
known as a string, short for string of characters. In the process function, let's do the same
thing, but this time, print the value of Delta. Let's also do what this
script was meant to do and modify the value of
this nodes rotation, increasing it by Delta using
the plus equals operator. The plus equals operator
will take the value of Delta and add it to
the value of rotation. Then also assign the
result to rotation. Running the scene, we can see
that the box is spinning. Stopping the game, we can see in the output that the ready
message was printed first, only once, followed
by many numbers, most of which are one 60th. How does this work? Both ready and process are
functions of node, meaning every node in our scene tree already
has these functions, but they aren't doing anything. Our script is overriding
the definitions of these functions for
one particular node that this script is attached to. We can see in our script when
a function is overriding a function definition when it has a blue arrow
in front of it. When we run the game,
the engine will reconstruct the scene
tree for this scene, starting from the root, adding one node at a time
from top to bottom. The ready function
for each node will be called after it is added
to the scene tree, but also after all
of its children have been added and their ready functions have
been called first. This means parents can rely
on their children being added to the scene tree before
they are considered ready. After the entire
scene tree is ready, then the process
frames will start. Process will be called
on every node in the scene tree from top to
bottom in sequential order. Unlike ready, parents will not let their children
process first. Now that we have a basic understanding
of how Godot works, in the next video, we'll start setting up the project
to make our game. I'll see you in the next video.
7. 0-6 Game: Hello, friends. Before we
start our game project, we'll need to prepare
a few things. First, we can either
delete everything or start a new project so
we have a clean slate. I'll remove everything
except the floor collision. Then rename this scene and
its root node to game. The game scene will be one centralized scene where all of the main
gameplay takes place. The game root node will have
two main child branches, a world environment node, and a canvas layer node. These represent a
clear distinction between what exists
in the game's world, like the level the player
is currently playing in, all of the characters
items, et cetera, and what exists outside of that world purely for
the player's benefit, information displayed on a heads up display or user interface. The world Environment node
will have several children. One or more nodes for the
current levels that are loaded, for now we'll just represent
that as a static body tutti. A character body tutti node
is most often used for the player character and a camera Tutti node that
usually follows the player. I'm going to reset
the position of the floor collision back
to the scene origin. With a camera node in the scene, the blue rectangle no longer represents what part of the
world is drawn on screen, instead, the camera's view
port is shown in Magenta, currently centered
at the origin. So the floor will be at the
center of this camera's view. The canvas layer will have children for things
like displaying the player's information on
screen, menus, et cetera. These will all be represented
with control nodes. The Canvas and control
nodes will still use the blue rectangle
as the viewport for the sake of arranging
their positions on screen. How this works while the player is playing the game is that the camera will view the
world and draw what it sees, all of the children of the
world Environment Node. After the camera has rendered a rectangular Toti
image of the world, the Canvas layer will draw its
children over top of that, creating the typical gameplay screen that we are
used to seeing. These two branches also share
another clear distinction. The world will
need to be paused, but the canvas layer will not. So selecting the game node
in the inspector under node, expanding process, will change its process
mode to always. The world environment
node will be set to pausble and the Canvas layer will stay as the
default inherit. It will inherit the process
mode of its parent, which is the game node, so it will also be always. Since the default
process mode is inherit, all children of the world
environment will inherit being pausible while children of the Canvas layer will
inherit always processing. The first time we press the Run Project button
in any new project, we will be prompted to
set the main scene. We can select the current scene or select another one
from the project. With the game scene
currently open, we can set it as
the main scene for now and change it later
when we have a title scene. This can be changed
in the project settings under the run settings. It's also a good idea to set your display settings
before getting started. So select window. Here we can set
the viewport width and height measured in pixels. I find 12 80 by 720 to be a
good size for most games, and it's compatible
with most platforms. Under stretch, changing the
stretch mode from disabled to Canvas items will ensure that two D nodes will stretch or shrink to fit
the game window. Since I'll be using pixel art, I'll change the scale mode
from fractional to integer. So pixels will only
double or half in size, avoiding mixed size pixels. Under rendering
textures, I'll also change the default texture
filter from linear to nearest. This will draw my pixel art
assets exactly as they are, preventing them
from being blurry. After closing the
project settings, you may need to trigger an update by just
zooming or panning the viewport to force the blue viewport rectangle or camera to resize themselves. Now we are ready to start
breaking down the project into its individual components and tackle them one at a time. In the next section, we'll create a basic Tutti
platforming character that the player can control. I'll see you in the next video.
8. 1-1 CharacterBody: Hello, friends. In this section, we're going to build a two
D platforming character that the player can control. If your game scene
doesn't already have a character body two Dnode,
you should add one now. The character body two D isn't a body in the
sense of a person, but a physics body, like the
rigid body or static body. Unlike rigid bodies, a
character body does not automatically have
gravity applied to it but will react to collisions, unlike a static body. So the character body node also needs a collision shape as a child of it with a shape resource to tell
it what shape it is. Most games use a capsule
to represent a character, but rectangles and
circles are also used. For now, I'll use
a rectangle with dimensions of 128 by 128. We can always change or
resize to collision shape after we have a drawing of the character to base
it off of later. In addition to a
collision shape, we'll also need a sprite
node to draw the character. It is important not to use an Animated Sprite node as this is not its
intended purpose. Animated Sprite nodes are only for drawing simple
animated sprites. As soon as you have
to synchronize any other nodes with your
animations like sounds, particles or
functions, you'll need more flexibility than the Animated Sprite
node can provide. For now, I'll just use the Godot icon as the
character's texture, which is 128 by 128 Pixel square matching
the collision shape. Let's rename the character body Toti node to the name
of the character. While we might not
have any intention of having multiple copies
of this character, it's still a good idea to compose its structure
in a separate scene, isolated from the game scene. We can accomplish this by
right clicking on the node, then selecting save
branch as scene. Let's create a new
subfolder for scenes, another inside that
for characters, then save it as the
name of this character. In the game scene,
this character is now represented as
only a single node with a clapperboard icon, telling us that it is a scene. All of its child
nodes are hidden, now contained within
the character scene. We can open the character scene by clicking on the
clapperboard icon. In the character's scene, it is good practice to consider the origin of the
character to be at the center point between their feet as if the
X axis is the floor. This ensures that all
characters will have their origin point in common
regardless of their size. So let's move the Sprite and collision shape nodes
up along the Y axis. In addition to the collision
shape and Sprite nodes, our character will need
an animation player to play the Sprite animations. Also an Animation
Tree to control which animation is playing
at any given time. An audio stream
player two D node will allow us to play
sound effects too. We may also want a GPU
particle emitter to create particle effects for things like footsteps and jumps. Each of these nodes
will be covered in their own video to explain
them in more detail. For now, it is important to understand how a scene
can represent anything in the game with its own scene having many child nodes
that are used to build it. The root node of this scene will usually have a script
attached to it. Depending on how you prefer
to organize your files, you may want to
keep this script in the same folder as
the scene it is attached to or in a separate folder
specifically for scripts. You should name
your scripts after their intended purpose in as
vague a name as possible. This script won't be specific
for my kiddy character, but should be written
such that it could apply to any character,
at least for now. The purpose of the
script will be to not only control the character
body node itself, but also to conduct the
operations of all of the child nodes to operate in harmony as needed to create
our character. This script attached to the root node of our
character scene is also accessible
from the game scene and will serve two
additional purposes. It will obfuscate the processing of the individual components of the character scene while also providing a clean
and concise interface for interaction with other scripts and objects in the game scene. This will be explored in further detail throughout
the rest of this section. Now that we have a basic
character structure ready, we'll need to receive input from the player to control them. I'll see you in the next video.
9. 1-2 Input Map: Hello, friends. In this video, we're going to
receive input from the player and send
it to the character. Since I created the script for my character body Tu Di
node from a template, it already has some
code that will receive input and move the
character left or right. If we run the game scene, pressing the left and right arrow keys will
move the character, but it doesn't feel very good and isn't implemented very well. The comment provided even tells us that we should
at least replace the UI left and right actions with our own custom
gameplay actions. To do this, we'll open
the project settings, then switch to the
input Map tab. In the field that
says add new action, we'll type the name
of our action, starting with move left. This must not contain any spaces and is usually written
in lower snake case, all lowercase separated
with underscores. Press the Enter key or click the Add button to add
the input action, then make another
one for move right. And we'll also need
one for Jump later. To the right of each
of these actions, click on the plus button to add an event to
this input action. This will be the button that
is pressed to trigger it. The listen field will
automatically detect input events, or you can search through
the input events available. To move left, I'll use
the left arrow key, moving right with
the right arrow key, and jump will be the z key. We can add multiple different
events to each action, meaning we can add
controller support easily by adding extra
events for the controller. I'll use directional pad left and right for
moving the character, as well as left stick
left and right, and also the bottom face
button for jumping. When you're done, close
the project settings. In this video, we're
only going to change this one line where we
are retrieving input, and more importantly, move
it to a separate script. It's good practice for all of your scripts to have
only a single purpose. This script will only be
used to control a character, not to receive input
from the player. Adding a new node
to the scene tree, this one can just be a
normal node since it won't be doing anything other
than running our script. Let's name it Player. Then attach a new script to it. There are multiple
ways to receive input. The most common is to
override the input function. Function overrides must be an exact match to the
function being overridden. So the name must have
the exact same spelling, the same parameter types
and the same return type. The input function
has one parameter, an input event, which we
can name whatever we want. But I prefer to use
their default names, and it returns nothing. The input function
will be called during any frame when any input
action event happened, whether that is
pressing a button, tilting an analog stick
or moving the mouse. Inside this input function, we can access the input event by its name and access its
contents with a period. We want to call a function of the input event called
Is action pressed. When calling a function, the function name must be
followed by parentheses. Inside the parentheses,
we provide arguments to the function
matching its parameters. In this case, a string, the name of the input
action we are checking. So we'll write jump
in quotation marks, being careful to match
the exact case and spelling of the input action we created in the
project settings. This function returns a boolean, either true if the button was pressed or
false if it wasn't. By putting this function
after the keyword if, then followed by a colon, we can create a new indentation
level for our script. Anything we put
inside here will only run if the result of the
I statement is true, so only if the
button was pressed. For now, we'll just
print out the word jump. Running the game,
if we press any of the input events we specified
as the jump input action, we will see that jump is
printed in the output panel. We don't need the ready
function, so let's remove that. For things like movement, especially when using
controllers with analog sticks, it can be useful to check for this input inside the
process function. Just like the character
script was doing, we'll need to use
the input singleton. This singleton class has a
function called get axis. The G axis function
requires two parameters, a negative action and
a positive action. Left is always considered the negative and right positive, matching the X axis
in our two D world. So we'll write the names
of our input actions we created in the project
settings in quotation marks, move left and move right. This function will return a decimal number between
negative one and positive one, representing how
far the left stick is tilted in either direction, or simply negative one or positive one if using
keys or buttons. Since a value of zero, meaning no input is being
given is still a valid input, we will always want to send this information to the
player every frame. So there's no need for
an if statement here. Let's just print out the value being returned here
so we can see it. Running the game, the number zero is being
printed repeatedly. Pressing the left or
right arrow keys, the direction pad keys on
my controller or tilting the left analog stick changes the value between negative
one and positive one. If you ever want
more information on any of these
classes or functions, you can access their
documentation by holding the control or command
key and clicking on them. Opening the character
script from before, we have a variable
named direction being declared inside
the process function. Let's comment this out and
move this variable into the global scope
of our script by declaring it at the top
of the script instead. Switching back to the player
script where we received movement input to pass this information from
one script to another, we'll need a reference
to the other script. In the player script, we'll create a
variable which holds a reference to a
character body to dende. By adding an export tag to the front of the
variable declaration, the variable will be
accessible from the inspector, and we can assign its
value as our character. Then instead of printing out the value of input dot get axis, we'll send it to
the character by assigning it to the character's
direction variable. When we run the game, the
player node will be checking every frame for the state of our move left and
move right inputs. Then assign that to the
character's direction variable. The character scripts
process function will check the
direction variable, every frame and use that to move the
character left and right. We now have our player giving input to move our character. I'll see you in the next video.
10. 1-3 Movement: Hello, friends. In this video, we're going to
improve the character movement by applying physics. In the character script, we can see that the
default template is checking if the value
of direction is zero. Zero is treated as false and
any other value is true. So if given any value of
direction that is not zero, the character's X
velocity is being set to direction
multiplied by speed. Or if direction is zero, then it is moving the value of the character's X velocity
toward zero by speed. The result of this is that the character's velocity
is either zero or speed, except in the case of
tilting the analog stick. This isn't very realistic
and doesn't feel very good. These speed and jump
velocity values are also defined as constants, which, unlike variables are
never allowed to change. This is problematic, and we
should change this if we ever want to have the characters move at different speeds in our game. We'll deal with jumping later. Another thing to note is that these values are
measured in pixels, so they will vary based on
the size of your character. For now, let's replace
our speed constant with a move speed
variable of type float. I'll set mine to 256
pixels per second. It's a good idea to separate variables and functions
into what is meant to be public versus private by preceding private names
with an underscore. Anything private
is meant to only be accessed from within
this script itself, unlike the direction variable, which is being
accessed and changed from the player script,
so it is public. In the physics process, we'll replace the speed constant with our
move speed variable. And now everything works
exactly as it did before, except that the speed
is a bit slower. If we add an export tag to the front of this
variable declaration, we'll be able to change the
value of it in the inspector. This is a great
way of testing and tweaking the value to get
it feeling just right. Since we can run our game, see if it feels good, then
change it and test again. Another benefit is if we have multiple characters
using this same script, each can have their own
value for this variable. But characters don't usually
go from standing still to moving at their maximum
speed instantaneously. Instead, they have some
amount of acceleration applied over time before
reaching their maximum speed. This can be accomplished
by declaring another variable
named acceleration, also afloat and will be measured in pixels
per second squared. If it makes it easier, you can consider its value to be a multiple of your speed. If my acceleration
is double my speed, then I'll reach maximum
speed in half of a second. Or if it's half of my speed, then it will take 2 seconds
to reach top speed. To apply acceleration
in the physics process, all we have to do is move the value of our
current X velocity toward speed multiplied by direction at a rate
of acceleration. But this will happen all
at once in a single frame. To adjust velocity over time, we have to do what the
template is doing here where it applies gravity
and multiply by Delta. Since Delta is the
amount of seconds that have passed since
the previous frame, usually one 60th of a second, this will cause the rate
of acceleration to be applied over time rather
than all at once. The move toward function takes what is in the
first parameter, moves it toward the value of the second parameter by the amount of the
third parameter, and returns the result. It doesn't modify the value
of the first parameter, so we have to assign
it to overwrite the previous value
of our X velocity. The move and slide function is defined in the character body two D class and will perform all of the difficult
calculations for us, considering the
character's velocity, external forces like gravity, collisions with other
physics bodies, and then decide where the
character should be each frame. Running the game now, we have gradual acceleration
being applied before we reach
our maximum speed, and we can also
adjust the value of our acceleration
in the inspector to get it feeling
the way we want. We can remove these comments. The same logic can be applied to deceleration by simply
adding another variable. This value is usually much
higher than acceleration. The code in physics process
is already using move toward. We just have to replace speed with deceleration times Delta. However, there is another time
when we may want to apply deceleration other than when the player isn't
giving any input, which is when they want to turn around and go the other way. We can expand our I statement
to consider this case. After checking if
direction is not zero, if our X velocity is zero, the character is standing still, then we still want
to accelerate. But also, if the
sin of direction is the same as the
sine of X velocity, then the player is trying to keep moving in the
same direction they are already moving and we want to keep accelerating
up to speed. The sine function will turn
any number into negative one, zero or positive one, making comparing whether
these two values are both positive or
both negative simpler. We can combine conditions
together with or. So if either of these
values are true, then the whole
statement is true. If both of these are false, then we want to turn around, which means applying
deceleration instead of acceleration, resulting in a much quicker
turnaround than it would by applying acceleration
if the value of deceleration is higher. Testing this out, moving
our character left and right should feel a
lot more realistic now, and we can easily adjust
the values for our speed, acceleration and deceleration
in the inspector. I'll see you in the next video.
11. 1-4 Jumping: Hello, friends. In this video, we're going to improve how
the jump is implemented. Before we get started
with jumping, I'll move the camera
up along the Y axis so the floor is at the bottom of the screen, not the middle. The template script
for our character already has a jump
button using UI except, which is the space bar, setting the character's Y
velocity to jump velocity. But we already added our own jump button
in a previous video. Unlike gravity or acceleration, which are multiplied
by Delta and applied to the character
over multiple frames, this jump velocity is applied to the character all at
once immediately. Running the game, we can
press the space bar to jump, but there are multiple problems. The jump is always
the same height, and we have the character
script checking for input. Let's first remove this
from the physics process of the character and instead
put it in a public function. We'll call it jump. Then in the player script, when pressing the jump button, instead of printing jump, we can tell the character
to jump instead. Accessing the character's
contents with a period followed by the
name of the function, and the function
has no parameters, so there's nothing
in the parentheses. But now we are
allowing the character to jump anytime the
button is pressed, even if they aren't
on the floor. We need to check if
the character is on the floor first before
allowing them to jump. This is on floor function is defined in the character
body two D class. Now the character can only
jump if they're on the floor, but the jump is always
the same height. In most games, the
character will jump higher holding down the button
than if it is just pressed. Sometimes we can
measure how long the jump button is held
before releasing it, then pass that
information through the jump function call to
adjust the jump force. But especially in two
D platforming games, we usually expect jumps to happen immediately when
we press the button, which means we can't wait for the button to be held
down or released. So instead, it is more common to assume that
the jump will be the full jump height
and then cut it short if the player
releases the button early. The same way we reacted to a button being pressed
in the player's script, we can react to it
being released too. Using the keyword if, this will only be checked if the previous if conditions
were false, removing the redundancy
of checking if the button was both pressed and released at the same time. If the jump button is released, we can call a
different function to cancel the jump
cutting it short. Back in the character script, we need to declare the
canceled jump function, requiring no parameters
and returning no value. A simple way to do
this would be to set the character's
Y velocity to zero, but this can feel pretty unnatural coming to a
complete stop mid air. So instead, I'll divide
the Y velocity by two. But we only want
this to happen if the character's velocity is
currently less than zero, meaning they are
moving up and only if their velocity is less than
zero because they jumped. If there were any other reason why the character was moving up, but they did not jump,
we wouldn't want to let them cancel a jump and
reduce their velocity. We can control this by
declaring a new variable. We'll call it Is jumping
of type Boolean, which will naturally
default to false. If we set this variable to
true when the character jumps, then we can check
if the character is jumping before allowing
them to cancel the jump. In the case that they
don't cancel the jump, we need to set the is jumping variable back to
false automatically. So in the physics process, checking if the character
is jumping and if their y velocity is greater
than or equal to zero, meaning the character is falling or at least
not moving up, then we can set is jumping
back to false because it is too late for the character to release the button
to cancel this jump. This works great, but
now we might want to adjust how high the
character can jump. The template script declares
jump velocity as a constant. Just like we did with
the movement speed, we can switch this
to be a variable. But knowing how much
velocity is required to reach a specific height
isn't very intuitive, and from a design perspective, it's often better to be able to control exactly how high
a character can jump. So let's declare
another variable called jump height
as an integer. This will be measured in pixels. I'll set mine to 256, which is twice the
height of the character. Using the on ready tag, we can use some
math to calculate the jump force required to
reach this exact height. The on ready tag will
run this line of the script when the node is ready before the ready function. I'm not a physicist,
so I would not be able to explain why this
equation works, but it does. Just take the square root of gravity multiplied by
jump force times two. Then also multiply all
of this by negative one since negative
along the y axis is up. In order to do this, we need
to know the force of gravity being applied to
the character as a decimal number, afloat. So declaring another variable
using the on ready tag, this one must be above
the jump force variable, so it will be set first. We can get the force of gravity from the
project settings. We just have to specify the path to the setting
we want in a string. Finally, when the
character jumps, we'll set their Y velocity to
this calculated jump force. Now the player can control
how high they want to jump. And we can easily edit a character's maximum jump
height from the inspector. I'll see you in the next video.
12. 1-5 Falling: Hello, friends. In this video, we'll adjust how the character feels and controls in midair. Our character can
jump higher now, but it feels very floaty, and the character can
control their movement in the air just as easily as
they could on the ground. You may want this for your game, but often the player has less control over the
character while in the air. In the character script, we can remove these
old constants since they're no longer
being used anywhere. Since our script is
doing multiple things, moving the character left
and right and jumping, it would be a good idea
to keep it organized. I'll keep my move
speed, acceleration, deceleration and direction
variables grouped together and do the same with variables
related to jumping. We can even create categories
for our variables using the export category tag and specify the name of
the category as a string. Let's have a separate
category for variables related to locomotion
and another for jumping. This adds headers
to the inspector, so our exported variables will be organized into
these categories. Our jumping category
has only one variable, but we'll add some
more, starting with a gravity multiplier. This will be a float and I'll give it a
default value of one. Then multiply our
gravity by the gravity multiplier after retrieving
it from the project settings. Since this number is
used to calculate the jump force required to
reach our desired jump height, the character's jump
height will not change, and the force will automatically be increased to compensate. Opening the project settings, looking under Physics two D, default gravity is set to 980
pixels per second squared. We can modify the force
of gravity here for the entire project and will affect all of our
physics bodies. We may want some things to feel heavier or
lighter than others, and an easy way to do
this is to multiply their own individual gravity
by a different number. Increasing our character's
gravity multiplier will make their jump faster and make
the character feel heavier. Lowering the gravity multiplier will make them feel
more light and floaty. Next, we can reduce
the amount of control the player has over the character
using more multipliers, commonly referred to as air
control and air brakes. Air control being used when the player is giving
movement input in midair and air brakes when they are not giving
input in mid air. I'll set both to be 0.5, meaning the character's
acceleration and deceleration will be halved. Down in the physics process, we can better organize
everything that is happening by dividing it
into different functions, one which is called only when the character is on the floor
and one when they are not. We'll pass along the value
of Delta to both functions. Defining a separate
ground physics function, we can now focus
on only applying the appropriate physics for when the character
is on the ground. Then create another function
for air physics and use this function to
control the character differently than when
they are on the ground. Here is where we'll apply
the force of gravity. Then also copy the
controls for the character moving and not moving
from the ground physics. Turning around is less
important in midair since there is no ground
to provide any friction, so it wouldn't make sense
to turn around any faster. All we need to do is multiply the character's natural
acceleration by the air control multiplier and their deceleration by the
air brakes multiplier. Another method is that you could consider air control
and air brakes to be alternative values that
replace acceleration and deceleration rather than
using them as multipliers. You may also want to consider
adding a terminal velocity, depending on how much falling the character will be doing for long distances and
whether the player needs to have better
control in such situations. A terminal velocity is the maximum speed an object will be able to travel
through the air, since the force of
air resistance will eventually equal the force of gravity and cancel
each other out. Since we don't really have a proper environment
to test this with yet, all we can really do is set the character's jump height
to something very high, maybe even zoom the camera
out so we can see higher up. But we can see that
the character will move very fast when
following long distances, and it is very difficult for the player to control
with any accuracy. So let's export another variable for our terminal velocity. In Earth's atmosphere,
a skydiving human will reach terminal velocity at
about 54 meters/second. If 100 pixels is 1 meter, then that's 5,400
pixels per second. To apply this limit in
the air physics function, we can simply use the
Min function to return either the character's
current Y velocity or the terminal velocity,
whichever is lower. Then assign the returned value to the character's Y velocity. Now we can easily limit how fast the character is allowed
to fall in the inspector, giving the player more control when they are following
long distances. I'll set mine to 4,096 for now. I'll see you in the next video.
13. 1-6 Running: Hello, friends. In this video, we'll add a run button
that the player can hold down to change
their movement speed. First, I'll reset
the camera's Zoom and position it
where it was before. The character's
movement speed when playing with the keyboard
is always the same, but we can adjust
the movement speed by only partially tilting
the analog stick. In some games, this is enough to allow the player to
either walk or run. But in others, there's
a designated run button for the player to hold
down to move faster. Opening the project settings, we can add a Run button
to our input Map. I'll use the shift key or the right face button
on my controller. In the player script, we'll use the same functions as we did with the jump to check for the run button being
pressed or released. When the run button is pressed, we can tell the
character to run, and when released,
tell them to walk. While we're here, we can
deal with this warning that the process function is not actually using the
value of Delta. In order to override
an existing function, the parameters must
match exactly, so we can't remove
Delta entirely. Instead, the Godot engine
would prefer that we mark this parameter as unused by preceding its name
with an underscore. Alternatively, we can open
the warnings panel and click on Ignore to tell the engine to ignore
this warning. Now the character
script will need these public functions,
run and walk. We'll start by adding two more exported variables
in the locomotion category, a walk speed and a run speed. Then the move speed can have
its tag changed from export to on ready and its value assigned to
walk speed by default. Giving the walk
and run functions a declaration, accepting
no parameters, and returning no values, we only need to set the value of move speed to run
speed or walk speed. Then the physics process
will handle the rest by using the current
value of move speed. That's all it takes to change our character's movement
speed while holding a button. We could easily reverse this to have the character
run by default, but hold down a button for walk by just changing the
default value of move speed to run speed and having the player script
call the opposite functions. Since this lesson was so short, I'll also prepare for the next video by adding a block for the character to jump off of
using a static body node. A I'll see you in the next video. And
14. 1-7 Coyote: Hello, friends. In this video, we're going to help the
player out by adding accessibility features
that will make our character feel more
forgiving to control. I'll start by resetting the character's jump height
back to a reasonable number. In most two D platforming games, the game will allow
the player to press the jump button for
a short time after their character has
walked off of a ledge and also when they are falling
and very nearly landing. In both of these cases, the is on floor function
will return false, but it would make the
game feel better to the player if they are still
allowed to perform a jump. To add these features, let's start by changing
the return type of the jump function from
nothing to a boolean. So we will return true if the jump was successful
or false if it wasn't. That way, the player script can react to the jump
button being pressed too early and retry the jump after the character
lands on the floor. Switching to the player script, instead of just calling the
character dot jump function, we can react to its return value by putting it inside
of an if statement. If jump returns true, we don't need to do anything, but if it returns false, we can assume that the
player wants to jump when it becomes possible if that
happens relatively soon. So if not jump, if jump returned false, then we will buffer
this jump input for a brief window of time. The easiest way to keep track of time is using a timer node. We can add this as a
child of the player node, or since the player node doesn't have a specific type
that we're using it for, we can change its type
to be the timer itself. Looking at its properties
in the inspector, we can set how
long the timer is, which will be the window of time during which the input will be buffered and set its one
shot property to true. This means that once started, the timer will only
count down to 01 time. The default behavior would be to count down to zero repeatedly. Next, let's declare
a new variable. We'll name it buffered
input of type callable. Callable is a class that holds a function call complete
with arguments. So if our jump fails, we want to set our
buffered input to be character dot jump
without any parentheses, since we are not calling
the function now, but storing it in a variable. And also start the timer. Since changing the type
of this node to a timer, we'll have to change the type of node that this script extends at the top of the
script to be able to access functions of
the timer class. Then in the process function, if this timer is running, if it is not stopped, then call the buffered input. If the buffered
input returns true, then it succeeded, and
we can stop the time. If the buffered input
returned false, then it can't happen
yet this frame, and we'll want to
try again next frame until the timer runs
out or it succeeds. For testing purposes,
let's also use some print statements to print out when the
jump is buffered, when the buffered input fails, and when it succeeds. Running the game, if we
press the jump button early, the jump input is buffered. It fails a few times then succeeds when the
character hits the ground. If we press the jump button when the character is
way up in the air, then the input is buffered, but it fails until the timer
runs out and never succeeds. Once we're satisfied with the performance of
our input buffering, we can remove or comment
out these print statements. Next, we want to allow
the character to jump if they have recently
walked off of a ledge, commonly referred
to as coyote time, referring to Wiley coyote. In the character
scene, we can add a timer node and
rename it coyote. Like the input buffer timer, it will be a one shot timer, and we can adjust the
duration of the timer. With the character scene open
in the character script, we can grab a reference
to any child node by holding down the
control or command key, then clicking and dragging
the node into the script. This automatically
creates a variable with the same name and uses the on ready tag
to set its value using the node path notation
starting with $1 sign. Using this dollar sign
node path notation requires that this node must exist with the exact node path relative to the node that
this script is attached to. Otherwise, it will
cause an error. However, this coyote
time is a feature that is generally only applicable
to the player character. We may not want
to have this node in every character scene. So we can use the
function get node or null passing the same node
path as a string argument. This function will either return a reference to
the node following this node path or null if it doesn't exist without
causing any errors. In the jump function, our check to see if
a jump is possible will now either be if
the character is on the floor or if
the character has a coyote timer and the
coyote timer is not stopped. In logic operations, and will be evaluated before or unless
you include brackets. In regards to the A statement, the left side will be checked
before the right side. So if the coyote
timer does not exist, the A statement will
be considered false. So the right side
of whether or not the coyote timer is running
will not be checked. Now all we have to do is
start the coyote timer. To do this, we need to know when the character
walks off of a ledge. I'll consider this to be included in the
jumping category, and we'll declare
two new variables. Is on floor to store the returned value of the I on floor function
for this frame. This will allow us to
perform this check once per frame and store the results so we can use it multiple times, saving the need to call the
function multiple times per frame and was on floor, which will hold the value of is on floor from
the previous frame. During the physics process, we'll set the value of was
on floor to is on floor, then is on floor to the returned value from
the function is on floor. Any other calls to the
is on floor function can use our variable instead
to be more efficient. Then if the character
was on the floor in the previous
frame and is not on the floor of this frame and their y velocity is greater
than or equal to zero, meaning they did not jump, the only logical conclusion is that this
character has walked off of a ledge or the floor they were standing
on has disappeared, and this is the exact moment when the coyote
timer should start. For testing purposes, let's add print statements
so we know when the coyote timer starts and when a coyote jump is
successfully performed. This will require a
redundant if statement, but we'll remove it later. Okay. Running the game to test it out. We can walk off of a ledge and see that the coyote
timer starts. If we press the jump button while the coyote
timer is running, then the character is still
allowed to perform a jump. We now have a solid Toti platform and
character controller. In the next section, we'll draw and animate the character. I'll see you in the
next section. I
15. 2-1 Sprite: Hello, friends. In this section, we're going to take
our simple Gadot icon and polish it into a
fully animated character. For that, we're going
to need assets, images and audio files. The ones I'm using will be included if you
want to use them, but I recommend
intermediate students to use your own or
other free assets. Before we start
importing assets, it's a good idea to create a separate folder to hold them. We can also create
different subfolders for different types of assets like sprite sheets and sound effects. Click on the folder you want your imported assets to be in, then simply click and drag the assets into
the Gadot window. The importing process will
happen automatically, and they will be sorted
into the focused folder. If they're not in
the correct folder, you can always drag them into the file system tab to
reorganize them too. But this will
trigger a reimport, so it's better to import them into the correct
folder to begin with, especially for larger files. Open your character scene
if it isn't open already. Using these assets to draw the character might be
as simple as replacing the Gadot icon with your
imported image file if your assets have only
one sprite per file. The assets I'm using are
organized into a sprite sheet, so there are multiple
sprites in a single texture, which isn't what I want to draw. Using the drop down, we can select other types of textures, and the one for
using a sprite sheet is called an atlas texture. Clicking on the atlas
texture to expand it, we can edit it right
here from the inspector. Assigning the sprite
sheet as the atlas. We can then edit the
region of the atlas, which will be drawn
by the atlas texture. When selecting a region of
the sprite sheet to use, we can use the auto
slice feature to select a single sprite or pixel snapping to click and
drag a specific region. Since these sprites are all
evenly sized and spaced, I'll use this 32 by
36 pixel region. When changing the sprite, the width and height
will not change. Only the X and Y position will increment by
uniform values. This makes it much easier
to animate the character. Before committing to using a set of assets to
animate a character, it might be worthwhile to
edit your sprite sheet to have this even spacing to
save your time and effort. I'll move the sprite down
so it rests on the X axis. Its Y position is now half of
the sprite region's height. Now that we have an accurate visual representation
of our character, we can better fit
the collision shape. We can reorder the
child nodes to draw the collision shape over the character sprite to
make things easier. I'll use a capsule shape
and make it large enough to include the head but
not the ears and the body, but not necessarily the
arms, tail or weapon. This will be used to collide
with the environment, so anything outside
the collision shape will overlap with the
walls, ceiling or ground. Make sure that your
collision shape and sprite remain centered about the Y axis and sit comfortably on the X axis
as if it was the floor. If your character's size
changes significantly, you will probably want
to change the values of their variables for things like movement speed and jump height. We can also type mathematical equations into these fields, and the engine will
determine the result for us. But I'll reorder the collision shape to be behind the sprite again. With the character
centered along the Y axis, in order to make the
character face left or right, all we need to do is
change the value of the flip horizontal variable
in the Sprite two D node. Switching over to the
character script, we'll grab a reference to the Sprite two D
node and store it in a variable by holding
down the control or command key and dragging
it into the script. Then we'll declare a public
function named face left, accepting no parameters
and returning no value. While the current
intention is to only use this function privately
within this script, you can probably imagine
that telling a character to face a direction could be
useful in a public context. To face left, all we
need to do is set the value of the sprites
flip H property to true. We could also write another
function named face right, but the function
would only be setting the value of this
variable back to false. Sometimes it's easier to write our functions to
be more flexible. Instead of having both a face left and face right function, we can give the face left function a parameter
that lets it do both. I'll just name it left and give it a default value of true. Giving a parameter a default
value makes it optional. So this function
named face left, can be called without
providing any arguments, and we will assume that
they want to face left. We can then set the value of
flip H to the value of left. To make the character
face right, all we have to do is call
face left but provide false as an argument rather than calling a completely
different function. In the physics process, if a character's
direction is negative, then they want to face left. And if it's positive, then they want to face
not left, which is right. If direction is zero, then they will remain facing the same direction they
were in the previous frame. To get a better view
of my small character, I'll zoom the camera in
by a factor of four and reposition it so the floor is at the bottom of the screen. O. When we hit play, the character is being drawn and their sprite flipped to
face the correct direction, but they still aren't
being animated. I would like my
character to be faster, so I'm going to increase their values in the character scene. I'll see you in the next video.
16. 2-2 Animation Player: Hello, friends. In this video, we're going to add
animations to our character. If your character doesn't have an animation player node,
you should add one now. With the animation
player node selected, the animation tab should
expand automatically. If it isn't open, you can open it by clicking on the
Animation button. The animation panel is currently empty as there are
no animations yet. Clicking on the
Animation button, we will select New to create a new animation
and name it idle. With an animation selected, there is now a timeline
measured in seconds. If you prefer, you can change the seconds to
frames per second, and the timeline will be
displayed in frames instead. The frames are very
close together, so I'll zoom in to display frames further apart
from each other. We can change the
frame rate too. I'll set mine to 12
frames per second. It also helps to apply snapping to the timeline
cursor with this toggle. Now when we scrub through
the animation timeline, it is automatically snapped
to the nearest frame. From this panel, we
can add tracks to the animation by clicking
on the plus button. The most common being
a property track, which allows us to edit any property of any node in
the scene tree over time. The node we want to animate
is the sprite Ti node, specifically, it's
texture property. This adds a track to the animation timeline,
and on this track, we can add keyframes to edit the property to a new
value at that time. Right clicking on the track, we can select Insert key to
add a keyframe to this track. By default, the value of this keyframe will be
its current value, which is the same texture we set it to in the
previous video. This is good enough for
single sprite files, since we can just change
the texture here. But since I'm using
a sprite sheet, I don't actually want
to change the texture. I just want to change the region of the texture that
is being used. There are a couple
of ways to do this. One is to click on the property track and
edit its property name, adding a colon and the
word region after texture. This tells the animation
that I want to edit the region property
of the texture, not the texture itself. I'll reset the animation
back to being empty for now. Make sure to have the
animation player node selected in the scene tree, then select the
Sprite two D node. Expanding the atlas
texture property, we can see its properties
in a nested panel. Hovering the mouse over the
region's property name, the pop up will show us
the name of the property. This is helpful for
ensuring you have the exact case and spelling when trying to access
it through code or through the animation track
name like we just did. But the second method of adding property tracks to an animation, which is much easier is to use the key button to the
right of the property. This button will only be here if the animation player node was previously selected
in the scene tree. An animation is currently open, and then another node in
the scene tree is selected. Clicking this key button
will add a keyframe to the current animation at the
timeline cursor's position. The first time we
add a keyframe for a property which does not currently have a track
in the animation, Godot will ask us if we want
to make a track for it. And also, if we want to add this track to the
reset animation, we will say yes to both. This adds the track
to the animation, plus it also adds the first
keyframe at the same time. Clicking on the
idle animation name to expand the drop down, it also created the
reset animation, which is how GoDO will store the default values
of the properties. This helps prevent problems with running animations
in the editor and permanently changing the values of properties
unintentionally. Godot will reset all
properties back to the default values specified
in this reset animation. To make this smoother,
try to always add property tracks to animations when they are in
their default state. Now we are ready to start the
actual animation process. You should either have a
texture track for changing individual sprites or
a texture region track for changing sprites
within a sprite sheet. Your assets may also use different sprite sheets
for different animations. If this is the case, you
will also need a track for both the atlas texture
and the texture region. All we have to do is scrub forward in the
timeline one frame, then edit the value of
either the texture or the texture region and click the key button to add
another keyframe and repeat. Since I know that all
of my sprites are the same size and evenly spaced, I can do this even easier
by simply inserting new keyframes than manually
editing the X property, increasing it by 32 each frame. If we test this animation, the engine will assume
that we want to tween the values of these
properties between keyframes. Since the frame rate of the overall game is
60 frames per second, but this animation is
12 frames per second. The engine creates
four additional frames between each keyframe to
create a smoother transition. Obviously, this isn't what we want with a sprite animation, so we'll need to switch
the update mode of this track from
continuous to discrete. Now the property will only
change when the animation hits a keyframe and it will
change the value immediately. The animation is too long, so we'll need to set
the number of frames of the animation so it ends
after the last keyframe. This animation will
also need to loop, so click the animation looping button so that the
looping icon is blue. If you click it once more, it will be set to Ping Pong, which is a different
kind of looping. For character animations, we generally want forward looping. It's a good idea to also set the reset
animation tracks to discrete in case
you decide to use it to reset a character
through script. This idle animation
is now complete, and we can continue
to make the rest of the animations we need for
our platform and character. Clicking on the
animation button, we can add a new animation
and start from scratch, or we can duplicate this idle animation and
use it as a template. Since I know that my
sprites are evenly spaced, I only need to edit
the Y properties of my keyframes to switch
from idle to walk frames. We can copy and paste keys
to make things easier. A line connecting two keys means that the values aren't
changing between them. When making the
running animation, I'll simply double
the frame rate. We can click and drag keys to move them
along the timeline. I'll also need to make jumping
and falling animations. Depending on the
assets you're using, you may need to make
different animations. For example, you might
need a jump start, jump idle, fall idle
or landing animations. Make sure you know
which animations need to loop and which don't. Now the character has
all the animations, but they're stuck in
the idle animation. So we'll need to
tell the character when to switch to
other animations. I'll see you in the next video.
17. 2-3 Animation Tree: Hello, friends. In this video, we'll control which
animation is playing in the animation player
using an animation tree. If your character
scene doesn't have an animation tree node,
you should add one now. The purpose of the
animation tree is to control the
animation player, deciding which animation should
play at any given moment. So with the animation tree node selected in the scene tree
looking in the inspector, we'll set its animation
player property to our animation player node. The tree root property is a
resource that will be used to determine which
animation will be played by the
animation player node. Clicking on the drop down, we have a few different
options for this, most of which are
more applicable when using three D
or bone animations, which often blend multiple
animations together. Since this is a
simple two D sprite, we'll use a state machine. Clicking on our new
state machine we'll open the animation tree
panel at the bottom of the editor window with a state
machine open for editing. Our state machine currently has a start node and an end node. These are not the same as
the nodes in the scene tree, but are nodes in the
context of a node graph. We can click in empty
space to add more nodes. Let's add the idle
animation as a node. We can click and
drag these nodes around to organize
them however we want. Their positions inside
this space are irrelevant. Changing our edit mode from select and move nodes
to connect nodes, we can click and drag
from one node to another, creating an arrow which
connects them in one direction. This is a transition from the start node to the idle node. So when the state
machine starts, it will immediately
transition to the idle node, which tells the animation player to play the idle animation. Switching edit mode back
to select and move nodes, let's add the walk and
run animations, too. And switching back to
connect nodes once more, create a transition from idle
to walk and walk to run. When this state machine starts, it will immediately transition all the way from the start
node to the run node. So we want to add conditions to these transitions to restrict when this transition
is allowed to happen. Selecting the transition
from idle to walk, expand the advance section. Under the advance
expression is a text area. Here we can specify any
logical expression we want to control when this
transition is allowed to occur, as long as the result will
be a boolean, true or false. In this case, we want to
play the walk animation if the character's X velocity
is anything other than zero. So this will accommodate
walking both left and right. This expression will be
checked on the node which we specify in the properties
of the animation tree node, the advance
expression base node. Setting this to be the
root node of the scene, the character body two D node, we can have the animation
state machine react automatically to the
character's velocity to change the animation. Returning to the state machine, transitioning from walk to run, we will want to check if
the absolute value of the character's X velocity is greater than the
character's walk speed. You may want to specify
a different threshold, such as requiring
the absolute value of X velocity to be equal to run speed or greater than the average of walk and
run speed, et cetera. Now we need to add a transition
back to idle from walk. When creating transition loops between two or more nodes in
a state machine like this, the conditions of
all transitions must not all be true
at the same time. This would make
it impossible for the state machine to decide which state should
be currently active, as it will just cycle between
the states indefinitely. Since the condition for
transitioning from idle to walk is for the X velocity to be
anything other than zero, then the transition
from walk to idle should be if the X
velocity is equal to zero. These two conditions can't
be true at the same time, so the state machine can't get stuck looping between
these two states. Applying the same logic to
transition from run to walk, the conditions for this
transition must be logically incompatible with the transition from walk to run. So if the absolute value of the character's X velocity is less than or equal to the
character's walking speed, this will never be true at the
same time as greater than. When we run the game, the
state machine will start from the start node and move along transitions between
different states, each state representing
one or more animations. In some cases, the state machine may transition to the end node. In which case it
will stop working. When animating our character, we never want this state
machine to reach the end node, so there's no transition
leading to it. This works to animate
the character idling, walking and running
left or right, but we also need to add
animations for jumping. If we were to add the
jump animation here, we would need to add
transitions from all of our existing states to
the jump animation, since the character can jump
from any of these states. This will get tangled and
messy and difficult to manage, especially if your character has multiple different
jump animations. What we can do instead is
create a nested state machine. We'll start by saving our current state machine
as a project resource. Call it locomotion. Then replace this state
machine with a new one. In our new blank state machine, right, click in empty
space and select Load. Find the Locomotion
state machine and add it to the new state
machine as a single node, and rename it so you know it is the Locomotion
state machine. Then add a transition
from start to locomotion. The entire Locomotion state
machine now exists as a single node which
we can open by clicking on its Edit
button with a pencil icon. We still have the idle walk and run animations
transitioning as before, but now it is nested within
a larger state machine. We can return to the
other state machine by clicking on the bread crumbs. In this state machine, we'll add any
animations related to jumping and connect
them with transitions. This is what an infinite loop in a state machine looks like. We want the character to switch
from any locomotion state to jump if their Y velocity
is less than zero, meaning they are moving up. If their Y velocity is greater
than or equal to zero, they have reached the
apex of their jump, and I'll switch to
the fall animation. Then transition
from fall back to locomotion if the
character is on the floor. We can call the function or use our private variable
that we created earlier. We may want to skip
jumping and go straight to falling if the character
walks off of a ledge. So let's add that as
another transition under the condition
that the character is no longer on the floor. This creates a conflict in our state machine when the character is in a
locomotion state and jumps. Their velocity is both less than zero and they
are not on the floor. So which transition should
the state machine use? In cases like this, we
can give our transitions priority over others by lowering
their priority property. If the character jumps, their Y velocity
is less than zero, and they are not on the floor. But the transition to
jump is given priority. So that is the transition
which will be used. If the character
walks off of a ledge, their velocity is
greater than zero, and they are not on the floor, so there is no conflict. Our two D platforming
character is now animated, but we can make it feel a lot better by
adding more polish. I'll see you in the next video.
18. 2-4 Sound Effects: Hello, friends. In this video, we're going to add sound effects to our animated character. For that, we'll need to import some sound files
into our project, sorted into an
appropriate folder. I'll include some
sound effects you can use to follow along
with this lesson, but I recommend
intermediate students use their own or free assets. First, I'll get rid of
the static body node that is this white rectangle since
I'm not using it anymore. Start by adding an audio stream Player two D node to your character scene if you
don't have one already. The audio stream
player two D node differs from an audio
stream player node in that the volume
of the sound effect will vary based on its
proximity to the listener. The default listener
being the camera. We can populate the
stream property of our audio stream player with a sound effect and click on the playing
toggle to hear it. If we take a look at our
character's animations, particularly the jump animation, We can add an audio playback
track to the animation and select the audio
stream player to denode to be the
player for this track. We can then keyframe sound effects to play as
part of this animation, but this comes with a
lot of limitations. If the sound effect
is longer than the animation, it
will be cut short. If the animation is looping,
it will be repeated, and with sound effects,
the player will hear many times while playing
the game like this one. It's a good idea to
add some variety and randomization to it so the player doesn't get
tired of hearing it. So let's remove this track from the animation and try
a different method. Since we know the exact moment when the character
jumps from the script, it would be easier to
play the sound that way. Let's rename the audio
stream player to DnodeT jump and grab a reference to store it in a
private variable. When the character jumps, all we have to do
is tell it to play. Keeping the sound effect
out of the animation, it will play in its
entirety and only once. We can duplicate this audio
stream player two D node to add more sound effects, too, like one for
a landing sound. Then in the script, we can play it the same way
as the jump sound, but we don't have a
convenient function for when the character
lands on the floor yet. In the physics process, we have an if statement checking if the character
has walked off of a ledge. Reversing this logic, we can check if the character
has landed this frame, if they were not
on the floor last frame and are on the
floor this frame. We'll then call a new
private function. Let's call it on landed. For now, this function only needs to play the
landing sound effect. These sound effects will play when the character
jumps and lands, but they will
always be the same, which will get annoying to
the player after some time. We can improve this by
slightly randomizing the pitch scale of the sound
every time it is played. So let's attach a script to our audio stream player two dende and name
it something like sound effect randomizer without using a template and save it
in the appropriate folder. This script will need
a public function telling it to play a
randomized sound effect, accepting no parameters
and returning no value. The end result of
this will be to call the play function defined in the audio stream player
to denode class. But before playing
the sound effect, we want to edit the value of
the pitch scale property. Using the built in
function rand F range, we can generate a
random float between a minimum and maximum
value that we specify. With one being the
default pitch scale, I'll randomize it to be
between 0.5 and 1.5, which should produce a wide
range of similar sounds. We can attach this same script to the other audio
stream player to Dnode by clicking
and dragging from the file system tab onto
the node in the scene tree. Back in the character script, instead of calling
the Audio stream Player to D classes
play function, we'll call our play
random function instead. This might be
sufficient to reduce the monotony of our jumping
and landing sounds, but footsteps will be heard constantly while
playing the game. So you may want to have multiple different sound effects
for something like that and have the audiostream
player pick one at random. Let's duplicate an
audioStream player two D node to make another one
for footstep sounds, also with the same randomizer
script still attached. In the script, we'll export
a variable to hold all of the different footstep
sound effects as an array of audio streams. This can quickly be populated by group selecting all of
the footstep sounds in the assets folder
and dragging them onto the exported array
in the inspector. Then when playing a
randomized sound effect, we'll check if the size of
this array is not zero. If the array isn't empty, that means we should
set the value of stream to be a random
element of this array. Accessing a member
of the array with an integer index number as a random integer between zero and the size of the
array minus one. This will simply pick
one random sound from our array of sounds. In the case of our other
randomized sounds, since they aren't specifying
any value for the array, its size is zero, and it will not try to
randomize the sound effect, only adjust its pitch scale. Now we just need to
have the sound effect triggered by the walk
and run animations. Switching from script
view to two D view, first select the
Animation Tree node, then the Animation Player
node to temporarily preview the animations without the Animation Tree interfering. With either the walk or
run animations open, we can add a call method
track to the animation and call a method on the footsteps audio
stream player to denode. Inserting a key in the
animation on this track, we'll call the play
random method. A method in this context is
synonymous with a function. We can scrub through
the animation to find the frames when the
character's feet actually make contact with the floor and set the key frames to be
on these exact frames. Then repeat this process
with the other animation. Now we have a bunch of
randomized footsteps, jumping and landing
sound effects, but we may also
want dust clouds. I'll see you in the next video.
19. 2-5 Animated Sprite: Hello, friends. In this video, we're going to create
dust particles for our platforming character. For that, we'll need to
import more sprite sheets. Starting in the character scene, we'll add an Animated Sprite to de node and name it
something like Jump dust. The Animated Sprite
to dende differs from a regular Sprite to de node in that it doesn't have a
single texture property. It has a Sprite frames resource. Clicking on the dropdown, we can create a
new Sprite frames resource for this Animated
Sprite two D node. Then click on the
resource to open it. This will open a new tab in the bottom panel
labeled Sprite frames. In this window, we can create one or more Sprite
frame animations that this node will
be able to play. These animations can only
change the Sprite itself. They can't be
synchronized with audio, function calls or
other properties the way an Animation
Player node can. Depending on the
assets you're using, you can either add sprites from individual files or from
a single Sprite sheet. I'm using Sprite Sheets, so I'll click on the ad frames
from Sprite Sheet button. Browse through the file system to find your desired assets. If using a sprite sheet, you'll be prompted
to select which frames of the sprite
sheet you want to use. Unlike an atlas texture, these sprite frames
must be evenly spaced. Select the number of frames in each row and column
of the sprite sheet, and they will be sliced for you. You can then select each of the frames you want to
use by clicking on them. The number in the corner
of each sprite frame is the order in which it will
be added to the animation. If they are not in
the correct order, you can deselect and
reselect the Sprite frames or import them out of order
and reorder them later. Just like with the
character animations being one frame longer
than the last frame, it's a good idea for sprite animations like this to have an empty
frame at the end. So the last frame will
stay drawn on screen for the full duration of a frame before the animation is
considered finished. We can set the frame
rate of the animation. I'll use 12 frames per second and whether or not
this animation should loop, which I will turn off. There is also an option to
autoplay this animation. Each Animated Sprite
Toti node can have only one
autoplay animation, and the node will
play this animation when the node is added
to the scene tree. When a dust cloud is created
by the player jumping, we wanted to play
this animation. So let's make this
animation autoplay. We can preview
what the animation will look like by clicking
on the play button. To see the animation better, we can hide the character
sprite and the collision shape temporarily by clicking on the eye icon beside
them in the scene tree. Just like the character,
this animation needs to rest cleanly on the
floor of the X axis. So in the offset section
in the inspector, we can set its Y offset to be
half of its vertical size. With the assets I'm using, that's 16 pixels up. This keeps the nodes origins at 00 while still drawing
the Sprite offset. If the color of the
sprites isn't quite right or you want to
adjust their transparency, you can do so from the
canvas item section of the nodes properties
in the inspector. The modulate property is a color with a default
value of white, which has values of one red, one green, one blue,
and one Alpha. Each pixel in the sprite
will have its own color multiplied by this modulate
color before being drawn. So we can adjust its red, green, blue, or Alpha values. I'll give my dust cloud half transparency by setting
its modulate Alpha to 0.5. We can reorder the sprites by clicking and dragging them
using the arrow buttons, delete frames, add empty frames, or import more frames
if we need to. Once you're happy with
how the animation looks, remember to unhide the character sprite and
collusion shape. I'll turn on looping for
demonstration purposes. When we run the game,
the dust follows the character around since
it is a child of it. But what we typically want for
something like this is for the dust cloud to spawn in a specific location
and not move. This dust cloud will need to be a sibling of the
character, not a child. Right clicking on the
Animated Sprite two D node, we can save it as its own scene. Put it in a new folder
for dust clouds. Delete it from the
character scene. Then open it up so we can
edit it in isolation. Let's turn off the looping
from this animation. We still want it to autoplay when the dust cloud is created, but we'll deal with
that in the next video. For now, we have two more
dust clouds to create. One for when the character lands and another for
when they walk or run. So we can duplicate our existing jump dust scene
to make things easier. It won't make a difference, but I like to make
sure the name of the root node of my scenes
matches the scene's file name. Then just change the sprites out for the correct
ones in each case. A Make sure that all are properly offset to rest cleanly on the
floor of the X axis. We now have three
different dust clouds we want to use with
our character. We just have to
instantiate them. I'll see you in the
next video. And
20. 2-6 Instantiate: Hello, friends. In this video, we'll instantiate
the dust clouds in reaction to the movements
of our animated character. Starting from the game scene, we want our dust
clouds to exist in the world as a sibling of
our character, not a child. Let's attach a new script to
the world Environment Node. I'll name it world and put
it in the scripts folder. This script's main
purpose for now will be to spawn new scenes
as its children. So we'll write a new public
function named spawn, which accepts a packed scene
as the first parameter. I'll just call it
something vague like SN and also a position
where it should be spawned, which is a vector two. Then this function will return the node it creates in
case we want to use it. We can turn a packed scene into a node by calling its
Instantiate function. We will need to store this node in a variable to
be able to use it. I'll name it instance. Creating a node does not
add it to the scene tree. We have to do that, too. But to add any node
to the scene tree, the node will need a parent. This world environment
node will be the parent, so we'll call it
add child function, giving it the instance
as an argument. This will add the node
to the scene tree, but its position will be at the scene's origin,
coordinates 00. So we want to set
the position of the newly instantiated node
to be the spawn position. However, this assumes that the newly instantiated node
has a position property, which isn't true of all nodes. While this wouldn't
cause an error yet, it might in the future, if we use the spawn function to create different
types of nodes. So it would be a good idea
to check if this instance is a canvas item before trying to access its
position property. We can make the spawn
position parameter optional by giving it a default value
of vector two dot zero. Finally, we can return
the instance node. Now we need to call
this function as a reaction to the character
performing certain actions. The easiest way to do
this is using signals. In the character
script, we can declare signals at the top of
the script for each of the three times we want to spawn a dust cloud when the character has stepped,
jumped, or landed. Signals are conventionally
named in past tense, so as to say that the event
in question has happened. Just like functions, signals
can have parameters. In order to spawn
the dust clouds, we'll need to know the
character's position and which direction
they are facing, indicating whether the
sprite should be flipped. In the jump function, we can emit the jumped signal, passing the character's
current position and whether or not
the characters sprite is flipped
horizontally as arguments matching the
signal's parameters. The same can be done when
the character lands. For the footsteps, we can
declare a new private function. Let's name it on step
and emit the signal. If you want, you can also tell
the footstep sound to play inside this function
instead of including it in the animations
as a separate track. But in order to do that,
I'll need a reference to the footstep audio player to denote at the top of my script. Selecting the
Animation Player node and the walk animation. We can add a call method
track to the animation, calling a method on
the scenes root node. Then insert keyframes to
call the on step function during the frames when
the character's feet make contact with the ground. If triggering the sound
effects through the script, we no longer need
to do this from the animation and can
delete this track. Then I'll also need to repeat this process for
my run animation. Switching to the game scene, we can connect signals to scripts to react to
them automatically. Selecting the character, switching the inspector
tab to the node tab, we can see our list of signals. Either double
clicking on a signal or right clicking and
choosing Connect, a new window is open. From this window, we can select the node we want to
connect the signal to. In this case, the world
environment node. Then either enter the name
of a function to call as a reaction to the signal or
picking one from a list. I'll manually enter the name of a new function and name
it spawn Jump dust. Clicking on Connect will
close the window and display the World Environment
Node script with this function
added automatically. This function also
has a green icon indicating that it is being called by a signal connection, and the character node in the scene tree has a signal icon to show us that it is emitting a signal
with a connection. This signal connection is
also listed in the node tab. We can export variables to hold our three different
dust cloud scenes, which are of type packed scene. Then populate each
of these variables with the appropriate scenes
from the file system tab. For now, let's just call
the spawn function passing Jump dust as an argument and also the character's
current position. Godot doesn't like
it when we name our function
parameters the same as a node's already
existing properties. But we can rename
this parameter to something more specific
like character position. This will instantiate
a jump dust cloud automatically anytime
the character jumps. The Animated Sprite
two D node will auto play the Sprite
animation we created, but we don't want this
dust cloud node to continue to exist after
the animation finishes. After the character has
been jumping many times, the scene tree
would have lots of Animated Sprite two D nodes
that aren't doing anything. So we will need
to clean them up. The way to remove nodes from the scene tree is to call
their free function. But we don't want to do that until after their
animation has finished, which is a signal of the
Animated Sprite two D node. Since we know the node type of our dust cloud scenes is
an Animated Sprite two D, we can store them in
a private variable of that type and name it dust. We can connect signals through code just
like we did through the inspector and connect its animation finished
signal to its free function. Remember that when referencing a function rather than
calling a function, we do not use parentheses. We can also flip the dust
cloud horizontally if the character's sprite was flipped when this
signal was omitted. Repeating this process for
the other two signals, we can change the type of
dust cloud that is spawned. A running the game, we will return to
the Editor window and click on the remote
button in the scene tab. We can see the scene tree of the game that is
currently running. Expanding the tree so we can see the children of the
World Environment Node. We can see that the
Animated Sprite two D nodes are being created
and destroyed as we jump, land, and walk around. The remote scene tree
doesn't update every frame, so some dust clouds
may be created and destroyed without
being displayed in the remote scene tree. I'll see you in
the next video. A
21. 2-7 Audio Bus: Hello, friends. In this video, we'll add a separate sound effect for the
character's voice. I'll need to import
some more audio files for the character's efforts. Voice sound effects differ from other sound effects in that we only ever want to
play one at a time, since our voice boxes can only produce one
sound at a time. So we'll only need to have one audio stream player TD node for all vocalizations
for a character. In the character scene, let's duplicate any of
the existing audio stream player TD nodes to create a new one for
the character's voice. You may also want to
position this node somewhere around the character's neck or head instead
of at their feet. Although this won't make much of a noticeable difference
on my tiny character. The sound effect randomizer is no longer applicable
to this node, so we can remove the script by clicking on the
detached script button. We'll replace this
with a new script. Let's call it voice.
In this script, we can export any number of
standard voice sound effects, commonly referred to as
efforts or exertions. Let's export a variable for Jump effort sound
as an audio stream, which we can populate
in the inspector. We can preview what this will sound like by clicking
on the resource. Then we'll add a function which will play a
jump effort sound. However, this function will
not need to do anything if the voice audio stream player to denote is currently busy
playing any other sounds. Only if not playing, then we will set the stream
to Jump effort and play it. This will prevent
any new requests to play another effort sound from interrupting
the previous one. We can also use the
same techniques as we did with our
sound effects here, like randomizing the pitch scale or having an array
of audio streams and choosing one at
random each time. And unlike regular
sound effects, which are always expected
to be played every time, efforts aren't always necessary. So we can also add in a random chance that the
effort doesn't play, even if it is possible. Using the rand F function, this will return a
random number 0-1. We can check if this is less
than a percentage chance, let's say, 25%,
expressed as 0.25. Appending this to the
conditional statement with an and operator, this character's
voice must not be busy playing any
other sound and also must pass a random 25% chance before being allowed
to play this effort. All we have to do to
trigger this is connect the character's jumped signal to the voices play Jump
effort function. But the character's jumped
signal provides two arguments and the voices play jump effort function doesn't
accept any parameters, so they aren't
considered compatible. If we click on the compatible
methods only toggle, all functions will be shown
even if they don't match, and we can select the play
Jump effort function. To prevent this from
causing an error, we'll expand the
advanced section by clicking on the
Advanced Toggle. Here we can bind new arguments or unbind the
existing arguments. Simply unbind the two
existing arguments from the character's
jumped signal, and now the signal is compatible
with the function call. Testing it out, we will hear the effort sound randomly
played with a 25% chance, and it will never
interrupt itself. B. Another thing we can do
to separate voices from other sound effects
is to give them a separate Audio Bus on
the Audio Bus layout. Click on the Audio
button at the bottom of the editor to open
the Audio Bus layout, which currently has only a
single bus, the Master Bus. But if we click on Add Bus, we can add more buses to
the Audio Bus layout, one for sound effects, and one for voices. It's also a good idea to
have another one for music. This allows us to sort our audio into different
categories and easily control the
entire category all at once across
the entire game. We can adjust the volume of
all sound effects, voices, and music independently, and master volume will
affect all of them. There are also many
different audio effects which can be added to
each bus if we want to. In our character scene, we have multiple different audio stream player two D nodes, and these each need to be told which Audio Bus they belong to, with the default
being Master Bus. Each of the sound effects can be told to play on the
sound effects Bus, while the voice audioStream player two D node can be told
to play on the voice Bus. Now we can adjust the volume of our sound effects
independently from voices. M. M. We now have a very polished two
D platforming character. Next, we'll start building environments to move around in. I'll see you in
the next section.
22. 3-1 Tile Map: Hello, friends. In this section, we'll build an environment for our character to move
and jump around in. To do that, I'll need to
import assets for my terrain, sorted into a new folder
for terrain sprite assets. Double clicking on the asset, we can preview it
in the inspector. We can see that this asset is a sprite sheet just
like the character. Except instead of
animation frames, this sprite sheet contains
different sprites to draw terrain based on
its surrounding tiles. Starting in the game scene, we'll make the current
level static body to denode its own scene. I'll make a new folder
for room scenes. And depending on the
size of your game, you may also want to
sort rooms into regions. So I'll make another
folder for rooms in the forest region and save this room as something
like start room. Then open this new room scene. I no longer want the static body two D node to be the root node, since this room will contain
more than just a floor. So I'll add a regular node two D to this scene as a child. Then right clicking
on this node, we can select make scene root to reorganize the scene tree so that this node is
now the root node. Now the floor is a
child of the root node, and we could add
more static bodies to this node for the walls, ceiling, platforms, and
obstacles to build our room. I'll rename the root node to be the same as
the scene name. When using tiles from a sprite sheet that are
equally sized and spaced, we can use another node
that Gadot provides called the tile map layer
to draw them easier. Adding a tile map layer
node to the scene opens the tile map panel at
the bottom of the editor, but we can't do
anything here yet. First, we need to take a look at the tile map layer nodes
properties in the inspector. The tile map layer node can't draw anything until
it has a tile set. Clicking on the drop down, we'll create a new tile set. Then click on the tile set
to expand its properties. The first option is the shape of the tiles in this tile set. The tiles I'm using are square, but there are also
options for isometric, half offset square, and
hexagon shaped tiles. These are usually used for
top down perspectives, and this game is
a side scroller, so I'll be using
square shaped tiles. Next, we need to set
the size of the tiles. My imported sprite sheet has
the tile size in its name, 32 by 32 pixels. We will ignore the rest of the settings for now and switch the bottom panel of the editor
from Tilemap to tile set. Now we can add tiles to the
tile set we just created. Either clicking on the
Plus button at the bottom, then navigating through
the file system to find your assets or clicking and dragging the assets from the filesystem tab into
the tile sources area. This will give us a
prompt for the engine to automatically divide the sprite
sheet into tiles for us. If we select yes, and if we
set the tile size correctly, then the tiles in the sprite
sheet will be automatically sliced to that size for us
and added to the tile set. Clicking on the eraser button, we can click on tiles to
remove them from the tile set. Clicking and dragging will erase all of the tiles we drag over. Holding control or command, then clicking and dragging, we can remove all tiles
in the selected area. Toggling the eraser back off, we can use the same controls to add tiles to the tile set. If you want to create
tiles that are oversized, you can do so by holding
down the Shift key, then click and drag
over multiple tiles. This will treat multiple
tiles as if they're always meant to be drawn as
one larger group of tiles. But this sprite sheet doesn't
have any oversized tiles, so I'll erase this
and add each of these tiles to the
tile set individually. Now that our tile
set contains tiles, our tile map layer can use this tile set to draw the tiles. So we'll switch from
the tile set panel to the tile map panel. With the paint tool selected, we can select a tile
source from the tile set, then a tile from
the tile source. In the two D scene view, the editor provides
orange guidelines of how the tiles will be laid out and we can now paint this tile wherever
we want by clicking. If you want to change
the offset of the tiles, you can adjust the tilemap layer nodes transform position. Like in the tile set, we can use the erase toggle to erase tiles
instead of painting them. The picker tool can
be used to select a tile from the T D view rather
than the tile set panel. We can draw tiles
flipped horizontally, vertically or both
and rotate them clockwise or
counterclockwise as needed. We can also group
select multiple tiles and paint them all at once. Toggling place random tile, this will instead pick from
the selected tiles at random. Increasing scattering will leave some tiles unpainted
while dragging around. You can draw lines too. With multiple tiles selected, they will be repeated
along the line. And the rectangle tool can fill an entire area of the tile
map layer very quickly. The bucket tool can
be used to fill an empty enclosed area
of the tile map layer. The random and
scattering options also apply to the line wrecked
and bucket tools. The bucket tool also has
a contiguous toggle, which when turned
off can be used to effect all matching tiles across the entire
tile map layer. With the tile map layer
node drawing our terrain, we no longer need
the static body tutti node and can delete
it from the scene. Back in the game scene, we
can see our start room. The node is not renamed. It still says current level. If we run the game,
the character doesn't collide with the terrain
and just falls through it. We now have a tile map layer using a tile set to draw tiles, but we need to do more work
to configure our tile set. I'll see you in the next video.
23. 3-2 Terrain: Hello, friends. In this video, we'll use more tile
set options to make painting tiles onto a tile
map layer quicker and easier. Starting in the room scene, select the tile map layer node, then expand the tile
set in the inspector. Expanding the physics
layer section, it is currently empty, so we'll add a new
physics layer. We don't need to edit any of the physics layer properties. It just needs to exist. We'll go over collision layers and collision masking
later in the course. But if you wanted to,
you could also add a physics material to
adjust its friction, roughness, bounciness
or absorbency. I don't want a physics material, so I'll clear it from
the drop down menu. With the physics layer
added to our tile set, we can open the tile set
tab at the bottom of the editor to apply this
physics layer to our tiles. Switch from atlas setup to paint properties by
clicking on this button. Here we can paint a lot of different properties
onto our tiles, but the one we want
to paint right now is physics layer zero. Under painting, the entire
tile should be filled in with four white nodes
at all corners by default. If it isn't, click on
the three dots and select reset to default
shape or press the Fkey. We can now paint this physics collision shape onto our tiles. All of these tiles are designed to be the
same square shape. But if you're using
different assets, you may need to adjust the collision shape
for different tiles. We can click and drag
these white points to adjust the collision shape, delete them or add more as needed to create
any shape we want. If you need a better
view to be more precise, you can expand this editor
to fill the entire panel. There are also grid
snapping options available. Setting the grid snapping
to the pixel size will allow you to create pixel
perfect collision if you want. A after painting collision shapes onto the tiles, running the game
again, we'll have the character colliding
with our tiles. Now we have collision,
but drawing terrain one tile at a
time is very tedious. We can use tools built into
the editor to make it easier. Expanding the terrain section, we can add a terrain
set to this tile set. The terrain set needs a
terrain added to it also. So we'll create an
element here, too. We can name the terrain
whatever we want. I'll name mine forest
and give it a color. This color won't be
drawn in the game only in the editor as a
way of setting it up. It helps to choose a
color which will contrast with the colors used in the sprite sheet so it stands out. I'll use pink to contrast against my green and
brown terrain tiles. Next, open the tile set panel at the bottom of the editor. Terrains are a way of
drawing tiles that are designed to connect to each other like this
sprite sheet is. Switch to paint properties, and the property we want to paint is the terrain property. We can then select our terrain set we just created terrain set zero and the terrain in that terrain set,
the forest terrain. To paint the terrain
property, first, we have to select each
of the tiles which are part of the terrain so
they are no longer dark. In this sprite sheet,
that is all of them. When mousing over a tile that has been added
to this terrain, the tile is divided
into nine squares. Middle square needs
to be filled in to draw this tile as
part of our terrain. The other squares tell the editor to draw
this tile if there is another terrain tile adjacent to this one
in that direction. Starting with this tile, this is meant to
be drawn if it has no other terrain
tiles adjacent to it. So only the middle
square is painted. The tile over to the
far left is meant to be drawn if there is another terrain tile to the right of it, so it will have the
right square painted. The opposite tile to the right follows the
same logic to the left, and the middle tile
is drawn if there is adjacent terrain both to
the left and the right. The vertical tiles are the
same, just up and down. The large block in the top left corner combines these two, adds corners, and also has the solid tile in the middle
with all squares painted. This will be sufficient to draw convex shaped blocks of terrain. Opening the tile map panel, switch from tiles to terrains, then select the terrain
we just created. With the paint tool selected, we can paint terrain tiles freely onto the tile map
layer, same as before. But now, if our terrain
tiles are adjacent to other terrain tiles and
form rectangular shapes, then they will automatically update to the correct
tile from the terrain. Erasing tiles will also
update surrounding tiles. This also works with the
line wreck and bucket tools. If we want to create
concave corners, we will need to return
to the terrain painting. Adding each of the remaining
tiles to the terrain, we'll fill in any squares which match whether there should be another terrain tile adjacent to this one
in that direction. If you're following along
with these assets and are unsure of what pattern
should be drawn on each tile, you can pause the
video and copy mine. Some tile assets will
provide a guide or reference to help you with the
adjacency rules for tiles, or you can just experiment
and see what looks good. Try drawing some terrain in a variety of different
shapes to see if every possible combination is accounted for and
drawing appropriately. The tiles at the bottom
are also meant to be drawn with adjacent
terrain in all directions. Every tile in a tile map is usually designed to
have a unique pattern. But if you have multiple
tiles with the same pattern, the terrain tool
will pick one at random with the same
pattern when drawing. We can adjust the probability
of this randomness by painting another property from the drop down list probability. Simply input a probability. I'll use 0.01, which is 1% and paint this onto
the tiles at the bottom. Anytime the terrain tool draws a tile with adjacency
in all directions, it will have a 1% chance
of drawing each of these tiles at the bottom or use the blank
tile by default. When painting terrain
from the tile map, if the random distribution of these tiles doesn't
look very good, you can easily keep
drawing over it. The terrain editor
will recalculate the random distribution
each time. We can now easily draw rooms for our game using a
tile map layer node, but the camera isn't moving, so we can't move our
character very far. I'll see you in the next video.
24. 3-3 Camera: Hello, friends. In this video, we'll have the camera follow the character so they can move further around the environment. In the game scene, we
have the character and camera two D
nodes as siblings. Obviously, if we assign the camera as a child
of the character, then it will follow
the player around. However, this isn't a
very good way of doing it because most games
will have transitions, cutscenes, or other
areas of the game that adjust to the camera's view independently of the
player character. In some games, the player has a choice of which character
they want to play, and that character
must be loaded, so it can't be determined in the game scene that the camera must follow this character. It's better to leave the
camera as a sibling and have the camera follow
the player character through a script instead. Let's attach a script to
our camera to denode. We could call it camera or
name it after the behavior, follow player or combine
the two, camera follow. We could export the
player character as a variable like we did
with the input handler, but let's instead
assume that this camera will be told to follow
more than one character throughout the game and instead define a public
function named follow, which accepts a subject
as a parameter. Theoretically, this subject
could be anything as long as it exists in the two D
world and has a position. So we'll give it a type
of node two D rather than the more specific type of
character body two D. That way, this camera could
be told to follow a rigid body or
anything else we want. This function doesn't
need to return any value. All it really needs
to do is store this subject into a
private variable. Then overriding the
process function, if this camera has a subject, then it will set
its position to be the same as the subject's
global position. Since the subject may be
part of a larger scene, we don't want to use
its local position relative to its parent, but rather its global position within the scope of
the entire game scene. We aren't using the
Delta parameter, so I'll proceed it
with an underscore. Now we need another script
to call this function from a node which has access to both the camera
and our character, which will be the game node. Attaching a script
to the game node, we'll name this
the game manager, and I'll put it in a
new scripts folder for manager scripts. It will be the responsibility of the game manager to manage when the player is playing
the game or in a cut scene or transitioning
between scenes. For now, when the
game manager is ready overriding
the ready function, we'll tell the camera to follow the player and activate
the input handler. To do that, we'll need to store each of these nodes
in variables. Since the hierarchy
of the game scene is quite complex compared
to other scenes, we may need to rearrange nodes, changing their parentage
in the process. When that happens,
the node path in our script will no longer
be valid causing errors. We can avoid this by right
clicking on nodes in our scene tree and selecting
access as unique name. This adds a percent sign to the right of the
node in the scene tree. This allows us to replace
the dollar sign annotated node path with the percent
sign annotated unique name. If the node has a unique
name already assigned, it will be automatically
applied when holding control or command and
dragging it into the script. Now that we have all of
these node references, when the game manager is ready, we can tell the camera to follow the player character and
enable the input handler. We haven't written
an enable function for the input handler yet, so let's switch over to
that script and write it. Creating an is enabled private
variable as a boolean, which will default to false, and a public enable function we can set is enabled to true. Then before we handle any
inputs from the player, we'll check if the input handler is enabled and return if it isn't will exit out of the function if
is enabled is false, preventing any of the
remaining code from running. We'll also need a
disable function that sets the is enabled
variable back to false. This may seem excessive, since we could just make the
is enabled variable public. But using this
encapsulated structure, we can do other logic
inside of these functions. Such as when the input
handler is disabled, tell the character to
stop moving by setting direction to zero and
also tell them to walk. When enabling the input handler, since movement is being
checked in process, it will already be
detected automatically. But if the player
started holding the run button before the
input handler was enabled, the input will
have been ignored, but the player will still
expect their character to run. We can check the status
of the run button when enabling the input handler and tell the character
to run accordingly. If using the input
function for movement, we could check the
input singleton for the status of the move left, right axis, and tell the
character to move here too. Returning to the game scene, since the character's position
is also at the origin, this is what the camera will
draw when the game starts. This isn't a very good framing of the character or the room. Usually the player
needs to see more of what's above them or
what's ahead of them. We can give the
camera a position offset to move it up along the y axis and to the right along the X
axis to achieve this. I'll put it 64 pixels up and to the right and also
change my Zoom to two. So when the camera's
position is set to match the character's
position, every frame, it will be offset by
this amount after that, giving the player a view slightly up into the
right of the character. We now have our camera
following the character around, but the camera only
looks to the right. I'll see you in the next video.
25. 3-4 Look Ahead: Hello, friends. And
this video will allow the camera to look ahead of the character either
to the left or right. Starting in the
character script, we can make things easier for ourselves by providing
class names for some of our scripts using the keyword class name at
the top of the script. Let's name this class something more generic like character. This will allow Godot to auto complete the
names of variables and functions when accessing members of this class
in other scripts. Just like how we used
signals to react to the character's actions
to produce dust clouds, we will need to make
another signal for when the character changes the
direction they are facing. Declaring a signal named
changed direction, we'll need to pass
which direction the character is now
facing as a parameter. Adding another variable to
the locomotion section, we'll need to know which direction the
character is currently facing to know if they have changed their facing direction. Since a Boolean variable
naturally defaults to false, I'll name the variable
is facing left, which when false implies
that they are facing right. In the face left function, we can emit our new signal and pass the value of
direction as the argument. Oh down in the physics process, when telling the character
to face the direction, this will be
happening every time the player gives
input every frame. We don't want to emit
this signal every frame, only on frames when
they change direction. So we'll add the condition
that the character is facing left and the direction
is greater than zero, or the character is not facing left and the direction
is less than zero. Now these functions will
only be called during the frame when the character actually changes their
facing direction, which will emit the signal
for us to react to it. Switching over to
the camera script, we'll need a function
matching the signature of the signal we just
created to connect it to. Let's call it on subject
changed direction, since the character
will be the subject, accepting the direction
the character is facing as a float and
returning no value. What we want to do
is have the X offset of the camera to match
the sign of direction. So we'll need to
store the value of offset in another
variable unaltered. We'll name it default
offset and set it to the value of offset
using an ready tag. Now we can safely
edit the value of offset without forgetting
what the original value was. H. When the subject
changes direction, we'll set the value
of offset to be the default offset multiplied
by the sign of direction. So if direction is positive, then the camera's offset
will be positive too, and likewise for negative. To connect this signal when telling the camera
to follow a subject, if the subject is not null and the subject
is a character, the class we just named, then we can safely access the character's changed
direction signal and connect it to our function. A Godot will not recognize new class names until after you've
saved your scripts. But if we call this
follow function on the same subject more
than once for any reason, this will cause an error since the signal is already connected. First, let's check if the subject the camera
is being told to follow is the same as the subject they're
already following. In this case, we
don't have to do anything and can just
return it to this function. Now we know that the subject is something other than
the current subject. Let's check if the
current subject is not null and if it
is a character. If both of these are true, then we should also check if the character's changed
direction signal has been connected to the on subject
changed direction function. And if this is true, we'll
disconnect the signal, since the camera is no longer following the subject and
doesn't need to react to it. Running the game, this will position the camera
ahead of the character, but it will happen
instantaneously, which is quite jarring
for the player. What would be
better is to adjust the value of offset
gradually over time, which is something we can
do easily using a tween, short for in between. So we'll declare a new variable, call it something like
look ahead tween. A then when the character
changes direction, assigning the value of this
variable to create tween. Create tween is a function
of the node class, so any node can use it. When the tween is created, we'll access its tween
property function. This accepts an object
as the first argument, which is this node itself. We can specify this
with the keyword self. Then the property we want
to tween as a string, which is the offset property, specifically the X component, which we can specify
with the colon. The next argument
is the final value, which is the default X offset multiplied by the sign of direction we calculated earlier. And finally, the duration
of tween in seconds. For now, I'll set
it to 1 second. Since this tween
will take 1 second, but it's very possible
for the character to change directions multiple
times during that 1 second, this would create multiple
tweens all trying to edit the value of
offset at the same time. So before creating the tween, we should check if it
already exists first, and if it does kill it. This way, no more than
one of this tween can exist at a time and be
trying to edit the X offset. Running this now, the
camera's X offset is tweening to look ahead of
the character over 1 second, but it's still a little jarring. We can adjust how this tween happens by exporting
some more variables. First, let's export a
duration for this tween as a float and give it a
default value of 1 second. Then replace the one in our
function with this variable. Next, we'll add a transition
type and an easing type. Together, these three variables will allow us to create
a wide variety of different tweens by adjusting how fast the value changes at the beginning and
the end of the tween. Since create tween
returns a tween, we can access its set
trans function and pass our transition type as
an argument in the same line. The set trans function
also returns the tween, so we can continue
this line and call the set Es function
immediately after that, too. Passing our Es variable
as an argument. All of this will
return the tween with the transition and ease type set and assign the
result to the variable. From the game scene, selecting
the camera tutti node, taking a look at its
properties in the inspector, we can edit the values
of these variables with the transition and easing types providing convenient
drop down lists. We can also make the duration of the tween shorter or longer. I'll have mine use a quad in
out tween over 2 seconds. Try experimenting with
the different options to get a feel for how they work. The transition type determines what mathematical formula is being used to alter
the behavior, while the easing type
decides how it will affect the first half or
second half of the tween. We now have the camera
gently panning left or right to show what's ahead of the direction the
character is facing, but sometimes the player may want to look
up and down, too. I'll see you in the next video.
26. 3-5 Look Up: Hello, friends. In this video, we'll allow the player to tell the character to
look up and down. First, we'll need to add
more input events to the input map in the project settings to
look up and down. I'll use up and down arrow keys. The up and down
directional pad buttons, and the right stick
tilted up and down. Then in the player script, unlike the movement direction, which is only being
used by the character, the look direction will be used by both the character
and the camera, which means we will need a
reference to the camera. It's worthwhile to store the look direction
in a variable, so we only need to calculate
it once per frame. Then in the process function, get its value from
the input singleton, the same way we did with
the movement direction using the get axis function, changing the names of
the input actions. Remember that since
up is negative, it will be the first argument. We can then send
this look direction to both the character
and the camera. For the camera, I'll
use a function call. But for the character,
I'll disset the value of a public variable. To make the name of
the direction variable of the character class
a little more clear, it might be smart to rename
it to move direction. Since this function doesn't
exist in the camera script, I'll comment it out for now to avoid errors until I'm
ready to write it. Over in the character script, we can open the replace dialog with Control or Command R and replace direction
with move direction so it is more clear what
it will be used for. Then add a new public variable
for the look direction. The L direction variable doesn't need to be used
anywhere in the script. It will only be read
by the Animation Tree. From the characters scene, selecting the Animation Player, I'll need to create LU
and L down animations. I'll duplicate my idle animation
to make things easier. Then just edit the Y values of my key frames to draw the character looking
up or down accordingly. Then in the Animation
Tree state machine editing the Locomotion
state machine, I can add the look up and
look down animations. Transitioning to them based on the value of look direction. And making sure their X
velocity remains zero, otherwise, they should
transition to walk instead. And back to idle in the case that look direction
is equal to zero. Or if they have started walking, This will have the
character animating to look up or down as we press the
up and down directions, but the camera isn't moving yet. In the game scene, try altering
the camera's Y offset and decide how far you would like the camera to move when the
character looks up or down. I'll use 128 pixels and reset back to the
default of negative 64. Returning to the player script, I'll uncomment the
camera function now, then switch to the camera script to declare this function. I will accept a direction as
a float and return no value. The body of this function
will be almost identical to the on subject changed
direction function. All we have to do is replace all the instances of
a head with up down. Then declare these
as new variables. We'll also need another
exported variable for the max look distance
we decided on earlier, which we will use instead
of the default offset, and offset X will be offset Y. However, when we let go
of the up or down arrows, we want the camera to return to the default offset
value, not zero. So this will require a
little bit of extra logic. Another way of adding
simple if statements to your scripts is to use
a ternary operation, which in GDScript uses the same if keywords as a
normal if statement, starting with the default case, which is the value we've
already calculated, but only if direction is
not zero, followed by else. So if direction is zero, then we want to use our
default Y offset instead. This works, but in most games
that have this mechanic, the player has to hold down the button for some time
before the camera moves. We can achieve this by adding a timer node to the player node. Let's name it. Look hold. We can adjust the wait
time from the inspector, but 1 second is fine, and the timer will
need to be a one shot. Connecting this timer's timeout signal to the player script, This is when the players
script will tell the camera to start looking
toward the direction. At the top of the script, we'll need to grab a reference
to the old timer node, and we'll also need
a boolean so we know if the player is
currently looking or not. In the process function, after getting the value of look direction from
the input Singleton, if the player is
already looking, then also if look
direction is zero, we'll reset the
camera by calling its look function and passing
zero as the argument. Then set is looking to false. The reason why I didn't use an and statement is
so that I can use an else if here and now assume
that is looking is false. Then also check if look
direction is not zero. In this scenario, where the
player isn't looking yet, but they are giving
look direction input, then if the hold timer is stopped, we'll
need to start it. Adding one more
else if the player isn't looking and isn't giving
any look direction input, then also if the look hold
timer is not stopped, that means we need to stop
it and cancel the look. Adding print statements
for all of these, we can confirm that
each is called only once when it
is appropriate. We'll also need to set the
is looking variable to true when the look
hold timer times out and the camera
starts to tween. Running the game, tapping the up or down arrows
without holding, the hold timer is started but stopped and the
camera doesn't move, but the character is still
animating looking up and down. Holding down the
up or down keys, the timer times out after 1 second and tells the
camera to look up or down. Releasing the up or down keys, the camera is reset back to
its default offset position. I'll comment out to these
print statements and adjust my tween settings
in the inspector to quad out over
half of a second. The player can now hold down the up and down directions
to look up and down, but we may want to restrict
how far the camera can move to avoid showing content
outside of the current room. I'll see you in the next video.
27. 3-6 Boundaries: Hello, friends. In this video, we'll restrict the camera to stay within boundaries
of the environment. Starting in the room scene, we can change the root
node to be an area two D node rather than
just a normal two D node. This is similar
to a physics body in that it has a
collision shape, but it doesn't have any
mass or react to forces. Instead, you can think of it like a motion or heat detector. It represents a region
of space and will emit a signal when physics
bodies enter or exit it. This will be useful later for detecting when the player
character enters the room. But for now, all we need to
know is what shape it is. Adding a collision shape
to denote as a child, we can give it a shape resource. We'll use a rectangle shape. We can then move and resize the shape to create
room boundaries. A attaching a script to the root node of the scene, we'll name it Room and sort
it into the scripts folder. We'll need a reference to the
collision shape to dende. I'll rename it to shape node. Then we need functions
which can be used to retrieve the
upper left corner and bottom right corner
of this rectangle so they will return
vector two coordinates. We can access the
collision shape two denodes shape property
by its name shape. Then we also want to
call a function on the shape resource, Get wrecked. The G wrecked function
returns a wreck two, which if we open the
documentation for this class, we can see it has a
position and an end. The position is the
top left corner and the end is the
bottom right corner. But these are relative
to the nodes position. We will need these
coordinates in global space, so we'll add them to the
nodes global position. The global position of a node will be relative to
the game's origin, not the parent nodes position. We can then return these values. Now, all we have to do is tell the camera to stay
within these limits. From the game manager
scene, for now, we'll just grab a reference to the start room and store it in a variable named current room. When the game scene is ready, we can get the top
left corner and the bottom right corner
from the current room, and we'll pass them
to the camera by calling a function which
sets its boundaries. Switching over to
the camera script, we'll declare this
function accepting the two vector twos retrieved from the current room
and returning no value. In this function,
we'll figure out how close the camera can get to the top left and bottom
right corners of this room without seeing
anything beyond those points. To do that, we'll need to create these variables as vector twos, and it will help to
have a boolean for whether or not the camera
is currently bound. We'll also need to know
the size of the Viewport, which we can retrieve automatically with the
function Get Viewport, then access its size property and store it in a variable
using the on ready tag. The Get Viewport function is declared in the node class
and returns a vector two, matching the size of the window we set in the project settings. This Viewport size
isn't necessarily the same as the amount of pixels which will be
drawn by the camera, since the camera
can zoom in or out. We'll need to divide
the Viewport size by the camera's Zoom to get the real pixel size of the
area the camera can see. But a vector can't be divided by another vector. We
have to use a float. I'll use just the X Zoom and assume that the cameras Zoom
will always be uniform. But this still isn't
good enough because the camera's position is
at the center of its view. The closest position
that the camera can position itself to
the top left or bottom right corner of the room
without drawing beyond these points will actually
be half of the size. Dividing this value by two, I'll rename the variable
to reflect its new value. When setting the boundaries, the top left most
position will be the top left corner of the room plus half the size of
the camera's view. While the bottom right
most position will be the bottom right corner of the room minus half of the
size of the camera's view, and we can set the is
bound value to true. H. In the process function, after telling the camera to
match the subject's position, we'll then apply these
boundary restrictions using the Clamp F function. The clamp F function takes a float parameter like
the camera's position, then a minimum value
and a maximum value, then returns a value which
will stay within those limits. We'll then overwrite the
camera's position with the clamped result and
repeat this for the Y. But since our camera
also has an offset, its position isn't an accurate
enough representation of the center of its Viewport. We are also editing the value of offset at unpredictable times. So it's not something
we can factor into the top left and bottom
right calculations, since the value will
likely change often. So we'll need to
factor the offset into the clamping functions
by subtracting it from the top left and
bottom right every time. If you want to have your
camera zooming in and out during gameplay while still
respecting these boundaries, you would also need
to factor these into the calculations inside the
process function as well. This works, but
the camera tweens seem to struggle against
the boundary limits. The tween moves the camera's offset slightly outside
the boundaries. The frame gets drawn, then
the next process loop pushes the camera's
position away from the boundary to compensate
for the new offset. This happens repeatedly
until the tween stops. This is because the tweens
are run after the process at the end of the frame just before drawing
everything on screen. To fix this, we can
change our tweens from using tween property
to tween method instead. By calling a method,
we can apply the boundary limits
at the same time as we are changing
the value of offset. The arguments for tween method, instead of a reference
to an object, it needs a callable method. We'll say set Y offset. Followed by from
the starting value, which is the current value
of the camera's Y offset, then the remaining
arguments are the same. Two is the final
value at the end of the tween and the
duration of the tween. We can do the same thing with
the look ahead tween two. We just have to write this
set X offset function, accepting a single
parameter which matches the type of the
from and two arguments. After changing the
value of X offset, we can enforce the
boundary limit on the camera's position
to compensate immediately and do the
same for the y offset. Running this now,
the offset tweens will not peak beyond
the room's boundaries. We now have the camera limited to only show what's
in the current room, but the environment
looks very flat. I'll see you in the
next video. And
28. 3-7 Parallax: Hello, friends. In this video, we'll fix the
parallax scrolling of the foreground and background
with a custom script. Starting in the
second room scene, I'll copy all of my
modulation colors from the Parallax Tut nodes
to their child nodes. Then I can unparent
the sprites and delete the Parallax Tut nodes while still maintaining their
color modulation. I'll then attach a new script
to any of these sprites, name it Parallax, and put
it in the scripts folder. This same script can be attached to all of the sprites
I want to Parallax. To create a custom behavior, it helps to take
some time to think about what it is you're
trying to achieve, which node references and other information
will be required. I have a number of
different sprites. I want to move at
different amounts relative to the
position of the camera, and I know that the camera is currently bound within
the limits of this room. I'll temporarily add a camera
two D node to the scene and zoom it in by a factor of two to pretend that this
is the game scene. If the camera is exactly in
the center of this room, I wanted to draw everything
exactly as it is. But when the camera moves
away from the center, I'll move all of the
parallax sprites by a relative amount with the background elements moving
less than the camera and the foreground elements moving
in the opposite direction. To do this, each sprite
will need to know the position of the
camera relative to the center of the room, as well as its own
original position. In the Parallax script, I'll declare an
exported variable to hold the factor
by which the sprite moves relative to the camera and name it scroll scale
as a vector two. This will work similar to the scroll scale variable
in the Parallax two D node. When this room is loaded
into the scene tree, we can use the on ready
tag to get its position and store it in a variable
as the original position. Getting a reference to
the current camera is easy thanks to the node
G Viewport function. Returning the Viewport that
this node exists within, which also has the Get camera to D function to get the camera. We can store this in
a variable as well, using the on ready tag. Overriding the process function, we don't need Delta and
can mark it as unused. I'll just set the
position of the sprite to be its original position plus the distance of the
camera from the midpoint of the room multiplied
by the scroll scale. Now the camera just needs a
public function which can tell us its position relative to the current
room's midpoint. Switching over to
the camera script, I'll write this function,
offset from midpoint, which will be the
camera's position plus its offset minus the midpoint
of the current room. Declaring this as
a new variable, I can calculate it
as the average of the top left and
bottom right corner of the room when accepting them from the set
bounds function. We could modify this function
to accept the midpoint and size of the rectangle rather than its top left and
bottom right corners, but it won't make
any difference in efficiency anyway,
so I won't bother. Now we can set the
scroll scale for each of the parallax sprites to give them each a different
distance from the camera. I'll still only use
horizontal parallax, giving my hills a scroll scale of one third along the X axis, and the hills further
back two thirds. For the foreground,
they will need to move in the opposite
direction as the camera, they'll have a scroll scale of negative one third and
negative two thirds. This will be sufficient
for creating the illusion of
depth for our rooms. Running the game, we can move
over to the second room and see that the parallax effect looks identical to
the start room, despite being performed
by a very simple script. Since the foreground sprites are moving in the opposite
direction of the camera, they will need to
either repeat and wrap their position to
create an infinite loop, or we can just make the
sprite larger than the room. Making my shrubs 1.5 times longer than the room and the
grass double the length of the room will leave
enough extra on either side so the edges never
reach the camera's view. A it would be possible to use the
Parallax two D node to draw the foreground and background using
separate Viewport and draw them on
different canvas layers or try to always draw the current room at the
game scene's origin. These solutions would lead
to other complications and limitations down the
line when it comes to drawing the game's
map, for example. I find writing custom
solutions like this to sometimes be easier than
using the provided nodes. The solution also keeps everything for a
room in one place, which makes it simpler
for designing and building of the rooms in
a more intuitive way. I no longer need this
camera Tuti node, so I'll delete it. Removing the clouds from
the Parallax touti node, we can alter the
Parallax script, also auto scroll the clouds too. Expanding the region wrect, all we need to do to move
the clouds to the right is to decrease the
exposition value over time. Attaching the Parallax
script to this node, we can first set
its scroll scale to one so it matches
the camera's movement. This will make the clouds appear an infinite distance away
compared to the Parallax hills. Exporting another vector two
for the auto scroll speed, we'll need to use Delta in the process function now and
can remove the underscore. Accessing the region rect
property and its position, starting with the
X, we'll subtract the autoscrollX value
multiplied by Delta. So this will make the autoscroll
speed pixels per second. Then assign the result to
the region recs X position. But if we were to run this
for a very long time, this number would
approach the limits of a floating point number
and may cause problems. The X position value
matches the size, the result is actually the same. It would be better to reset the X position back to
zero when this happens, so we can loop this around for an infinite amount of time. Using the AP F function, we can provide a minimum
and maximum value to limit it to only be between
zero and the rec size. This function will
return a result which must be between
these two values, but unlike clamping, the remainder will still
be included in the result. And we can do the
same thing if we want to use vertical
auto scrolling to. I'll give the clouds
a large auto scroll along the X for demonstration. And when we run the game, the clouds are auto scrolling to the right, looping infinitely. Mom. Mom. I'll reset the auto scroll value back
to a reasonable amount. We now have the foreground and background parallax
scrolling for both rooms. But the transitions between
rooms is very sudden. I'll see you in the next video.
29. 4-1 Contents: Hello, friends. In this section, we'll allow the player to move between multiple
rooms in our game. Opening the start room scene, we can see that the
visual components of this room go outside
the camera boundaries. If we were in a room
adjacent to this one, we wouldn't want to
see these sprites drawing when they are meant
to be in a different room. Plus, if we were to imagine our entire game filled
with many different rooms, it would be a huge waste of
resources to have all of their visual components all loaded when we are only
viewing one at a time. So we'll add another
node two D to the scene. Name it contents. Then reparent all of these other nodes to
the contents node, except for the collision shape. Right clicking on
the contents node, we'll save it as its own scene. I'll make a new folder in the forest rooms folder
for contents. Then name the scene
Start Room contents. Then delete the contents
from this scene. When the player character
enters this room, all we have to do is load its contents and add
it to the scene. Then delete the contents
when they leave. We can detect when the
player character enters the room by connecting
its body entered signal, since the character body
Tutti node is a physics body. Connecting this signal
to the room script, it will automatically add a function for us
on body entered, accepting the body
as a parameter. We don't need the
body, so we can proceed it with an underscore
to mark it as unused. At the top of the script, it will be helpful to give
this script a class name. Let's name it Room. Then export the contents
seen as a packed scene. And populate this variable
by clicking and dragging the content scene from the file system tab into the inspector. We'll also declare a contents
variable as a node two D. Declaring a new function
named load contents. I contents is null, then we'll assign
it the value of the contents seen instantiated. Then add the contents node
to the scene as a child. When the player character
enters this room, we'll call the load
contents function. Let's temporarily print out the value of body so
we can see what it is. We'll also need
another function to unload the room's contents
when they're no longer needed, although we won't be able to test this out until
the next video. First, checking if there are
any contents to be unloaded. If there is contents, call its Qu free function and
set the variable to null. Freeing a node from
the scene tree doesn't erase the contents of the
variable automatically. So if we were to leave a room, unload its contents, then
re enter that same room, this if statement, checking if the value of
contents is null, would say, yes, there actually is something in
this contents variable. However, the node
which was stored here was previously freed
from the scene tree, so it will cause an error. So setting the
variable to null after removing it from the scene
tree fixes this issue. Running the game,
our character is inside the start
room's collision area, so it triggers the loading of
the start room's contents. However, we don't want
any other physics bodies triggering the signal
and accidentally loading room's contents. We need to make sure that
the player character will be the only body which
can make this happen. Switching to the player
character scene, we can select the character
body Tutti node and expand the collision section in the properties inherited
from collision Object. Here we have a collision
layer and a collision mask, each with 32 numbered buttons. These are bit masks, which are just integer numbers
used in a particular way. Since integer numbers are stored as 32 binary zeros and ones, we can use each individual
bit as a layer. When it comes time to
check for collisions, applying simple logic
operations between one object's collision mask with the other object's
collision layer, we can determine if they
should collide or not. Only if the first object's
mask has a one in the same bit position as the second object's layer
will trigger a collision. Otherwise, it will just pass through as if it wasn't there. Changing the
character's collision layer to any other number, it will now exist on its
own unique collision layer separate from everything
else in the game. Since the numbers are
in groups of eight, I like to use the
first eight layers for environmental
collision layers, the next eight for
the player character, the next eight for
enemies, et cetera. The character body is
still masking layer one, so it will still collide
with the terrain during its physics process
when we call move and Slide. We can name these layers in the project settings under
layer names Physics two D. I'll name layer one as terrain and layer
nine as player. These names will be
displayed when hovering the mouse over the layer
buttons in the inspector, along with their bit
and integer value. In the start room scene, we can change the
collision mask, so this area two dende will only mask the player
character collision layer, only the player character will trigger collisions
with this area. Running the game again,
nothing will change, but we can be sure
that no other bodies will be loading room's contents. We now have our room dynamically loading its contents when the
player character enters it, but we still only have one room. I'll see you in the next video.
30. 4-2 Rooms: Hello, friends. In this video, we'll add a second room for
the character to move to. We'll begin by duplicating
the start room scene. I'll name this 1 second room. Then also duplicate the
start room's contents scene and open it up so I can edit it, then remove the
trees and canopy so it will be visually distinct
from the start room. We can also edit the
terrain and boundaries if we want to make the two
rooms even more distinct. Opening the second room scene, I'll rename the root node of the scene to
match its file name. Don't forget to assign the
second rooms content scene in the second room
space scene so it will load the correct
contents when entered. In the game scene, since my world will be
divided into regions, I'll add a node two D
and name it forest. Then make both of my rooms
children of the forest region. Save the forest region
as its own scene in the rooms folder and open the forest region
scene to edit it. In the forest region scene, we can now easily position these two rooms
next to each other. We want to position the rooms beside each other so that we can switch rooms when the player leaves one room and
enters another. Toggling on grid snapping, then expanding the
snapping options and selecting configure Snap. We can set the grid step
to the tile size of our terrain tiles -32 pixels. This makes it very
simple to position the rooms exactly aligned
next to each other. The area Tutti nodes
body entered signal will be emitted as soon as the character Tutti's
collision shape overlaps with the
room's collision shape. To keep things simple, we'll need to leave a bit of
space between rooms large enough that the
character body can't overlap with more than
one room at a time. Otherwise, we would need to
add extra logic for when the character enters a room without fully leaving
the previous one. Their character
body overlaps with the areas of both rooms
at the same time, but then turns around and
goes back to the first room. It's much easier if the
rooms are spaced out. When the character
enters a new room, we know that they have already
exited the previous room. This will also allow the character to move
completely outside the camera boundaries before triggering the transition
into the new room. Since my character's collision
shape is only 12 pixels wide and the gap between the
rooms is 32 pixels wide, I know for certain that
the character will fit between these two rooms without touching both at the same time. Having this gap
means that all of our rooms will also
need to extend their floor collision far enough outside the
camera boundaries of the room to prevent
the character from falling before they reach
the adjoining room. I'll zoom out the camera a
bit and comment out the line of code which binds the camera to the current
room's boundaries. Running the game, we can see that the start
room's contents are loaded immediately
because the character was already inside this room. Moving over to where the
second room will be, its contents are
loaded as soon as the player character
crosses the threshold. But we also want to unload the contents of the
previous room we just left. It will be the responsibility of the game manager to
handle transitions, since it will involve
many components of the game scene
working together. We'll need to send a signal from the room which was entered
to the game manager, which aren't even in the
same scene in our project. We'll also have a lot of different rooms in our game
and it would be a pain to have to manually connect this signal for every
single room one at a time. So it would be easier to use
our scripts to automate it. From the room script, we'll add a new signal at the top of the script which we can emit to say that the player
character has entered this room with the room which
was entered as a parameter. Then when the body entered
signal is received, we'll emit this signal
instead of loading the contents and pass this
room itself as the argument. When this room is ready, we can access the Scene
trees root by starting a doll or sign annotated node
path with a forward slash, then the word root and
another forward slash. From the scene Tree root, the first node we want
to access is named game as it is named
in the game scene. This gives us access to the game manager
script with a period. Then we can reference a function
we haven't written yet. Let's call it on
player entered Room. Then we can write
this function in the game manager script with
a matching room parameter. I'll name it room Entered to be more clear
in this context. In order to unload the
previous room's contents, we'll need to know what
the previous room was, so we'll need a variable to hold the room that the player
character is currently in. I'll replace this current level with current room
and leave it empty. I no longer needs
the on ready tag. The first thing we should do
is check if the room being entered is the same as the room the player character
is currently in. In which case, we don't
really need to do anything and can just return
out of this function. Now we can assume
that the current room and the room entered
are different rooms. When first starting the game, there is no current room. We'll first check if
current room is not null. If there is a current room, we should tell it to unload its contents using the function we wrote in the previous video. Then assigning current room
to be the room entered, we can now tell this
current room to load its contents and finally bind the camera to the
current room's boundaries. But I'll comment this out so we can see what happens
with it zoomed out. Now when we run the game, entering the second room sends a signal to the game manager to unload the contents
of the start room before loading the contents
of the second room. We now have multiple
rooms in our game, but the foreground
and background parallax scrolling do not work for rooms which are not centered about the scene origin. I'll see you in the next video.
31. 4-3 Parallax: Hello, friends. In this video, we'll fix the
parallax scrolling of the foreground and background
with a custom script. Starting in the
second room scene, I'll copy all of my
modulation colors from the Parallax Tut nodes
to their child nodes. Then I can unparent
the sprites and delete the Parallax Tut nodes while still maintaining their
color modulation. I'll then attach a new script
to any of these sprites, name it Parallax, and put
it in the scripts folder. This same script can be attached to all of the sprites
I want to Parallax. To create a custom behavior, it helps to take
some time to think about what it is you're
trying to achieve, which node references and other information
will be required. I have a number of
different sprites. I want to move at
different amounts relative to the
position of the camera, and I know that the camera is currently bound within
the limits of this room. I'll temporarily add a camera
two D node to the scene and zoom it in by a factor of two to pretend that this
is the game scene. If the camera is exactly in
the center of this room, I wanted to draw everything
exactly as it is. But when the camera moves
away from the center, I'll move all of the
parallax sprites by a relative amount with the background elements moving
less than the camera and the foreground elements moving
in the opposite direction. To do this, each sprite
will need to know the position of the
camera relative to the center of the room, as well as its own
original position. In the Parallax script, I'll declare an
exported variable to hold the factor
by which the sprite moves relative to the camera and name it scroll scale
as a vector two. This will work similar to the scroll scale variable
in the Parallax two D node. When this room is loaded
into the scene tree, we can use the on ready
tag to get its position and store it in a variable
as the original position. Getting a reference to
the current camera is easy thanks to the node
G Viewport function. Returning the Viewport that
this node exists within, which also has the Get camera to D function to get the camera. We can store this in
a variable as well, using the on ready tag. Overriding the process function, we don't need Delta and
can mark it as unused. I'll just set the
position of the sprite to be its original position plus the distance of the
camera from the midpoint of the room multiplied
by the scroll scale. Now the camera just needs a
public function which can tell us its position relative to the current
room's midpoint. Switching over to
the camera script, I'll write this function,
offset from midpoint, which will be the
camera's position plus its offset minus the midpoint
of the current room. Declaring this as
a new variable, I can calculate it
as the average of the top left and
bottom right corner of the room when accepting them from the set
bounds function. We could modify this function
to accept the midpoint and size of the rectangle rather than its top left and
bottom right corners, but it won't make
any difference in efficiency anyway,
so I won't bother. Now we can set the
scroll scale for each of the parallax sprites to give them each a different
distance from the camera. I'll still only use
horizontal parallax, giving my hills a scroll scale of one third along the X axis, and the hills further
back two thirds. For the foreground,
they will need to move in the opposite
direction as the camera, they'll have a scroll scale of negative one third and
negative two thirds. This will be sufficient
for creating the illusion of
depth for our rooms. Running the game, we can move
over to the second room and see that the parallax effect looks identical to
the start room, despite being performed
by a very simple script. Since the foreground sprites are moving in the opposite
direction of the camera, they will need to
either repeat and wrap their position to
create an infinite loop, or we can just make the
sprite larger than the room. Making my shrubs 1.5 times longer than the room and the
grass double the length of the room will leave
enough extra on either side so the edges never
reach the camera's view. A it would be possible to use the
Parallax two D node to draw the foreground and background using
separate Viewport and draw them on
different canvas layers or try to always draw the current room at the
game scene's origin. These solutions would lead
to other complications and limitations down the
line when it comes to drawing the game's
map, for example. I find writing custom
solutions like this to sometimes be easier than
using the provided nodes. The solution also keeps everything for a
room in one place, which makes it simpler
for designing and building of the rooms in
a more intuitive way. I no longer need this
camera Tuti node, so I'll delete it. Removing the clouds from
the Parallax touti node, we can alter the
Parallax script, also auto scroll the clouds too. Expanding the region wrect, all we need to do to move
the clouds to the right is to decrease the
exposition value over time. Attaching the Parallax
script to this node, we can first set
its scroll scale to one so it matches
the camera's movement. This will make the clouds appear an infinite distance away
compared to the Parallax hills. Exporting another vector two
for the auto scroll speed, we'll need to use Delta in the process function now and
can remove the underscore. Accessing the region rect
property and its position, starting with the
X, we'll subtract the autoscrollX value
multiplied by Delta. So this will make the autoscroll
speed pixels per second. Then assign the result to
the region recs X position. But if we were to run this
for a very long time, this number would
approach the limits of a floating point number
and may cause problems. The X position value
matches the size, the result is actually the same. It would be better to reset the X position back to
zero when this happens, so we can loop this around for an infinite amount of time. Using the AP F function, we can provide a minimum
and maximum value to limit it to only be between
zero and the rec size. This function will
return a result which must be between
these two values, but unlike clamping, the remainder will still
be included in the result. And we can do the
same thing if we want to use vertical
auto scrolling to. I'll give the clouds
a large auto scroll along the X for demonstration. And when we run the game, the clouds are auto scrolling to the right, looping infinitely. Mom. Mom. I'll reset the auto scroll value back
to a reasonable amount. We now have the foreground and background parallax
scrolling for both rooms. But the transitions between
rooms is very sudden. I'll see you in the next video.
32. 4-4 Transition: Hello, friends. In this video, we'll smooth the transition between rooms with a
simple fade to black. In the game scene,
most games will start with either a loading
screen or completely black, then fade in when
the game starts. Adding a node to
the canvas layer, we can use a color wrecked node to draw simple colors easily. Expanding the layout section, we'll change the anchors
preset to full wreck. This will tell the color wrecked to fill the entire canvas. It's currently white, but
black is more common, so we can change the color
to black at the top. When the game starts,
all we need to do is gradually change this
color from black to clear, revealing the camera's
viewport behind it. With our small simple rooms,
they load instantaneously. The loading and unloading of room contents doesn't
currently cause any lag. But if we imagine that
our rooms will consist of many more sprites and especially sound effects and background music being loaded, this will cause the room loading to take longer
than a single frame. We can use the same effect
backwards to fade to black, do our room unloading
and loading, then fade back in
when it's all done. This project won't get so big that we need an actual
loading screen. Let's rename this node fade
and give it a unique name. We will need to
make sure this node remains at the bottom
of the scene tree to be drawn over everything
else or increase its Z index. Having this big
black rectangle in the game scene can get in the way of working
on other things. We might want to have this
node hidden by default. Attaching a script to this node, I'll name it fade and save
it in the scripts folder. Overriding the ready function, the first thing I want to do is set its visible
property to true, so it covers
everything in black. We will need two
public functions to adjust the color to
either black or clear. To adjust the value of
a property over time, the easiest way is
to use a tween. So we'll declare
one as a variable. While I don't expect to allow fading more than once
in rapid succession, it's still a good
idea to check if the tween already exists
and kill it just in case. Then create a new tween. No need for fancy transition
or easing types here, a linear tween will
work just fine. And will be tween
this node itself, its color property and changing its color to be transparent
over a duration of time. For now, I'll put 1 second. There is a transparent
color defined in the class, but if we hover
the mouse over it, we can see that it
has red, green, and blue values of one, meaning it is transparent
white, not transparent black. This won't create
the effect I want, so I'll define my own
transparent color as a constant and name it clear. Using all uppercase is conventional when
naming constants. This will be a color
with zero red, green, blue and Alpha values. We can also define a
constant for the duration of the tween and replace the 1
second with this constant. This makes it a little easier to change the duration without
having to look through the script and without
exporting a variable for something you want
to keep consistent for your entire project. For the feed to black function, we could copy and paste
all of this code, only changing the
final value argument of the tween property
function call, but it would be better
to avoid redundancy. Et's instead make this its own private function
named tween color, accepting the final
color as a parameter, then pass this to the tween property function
call as the argument. Then both the fate to black
and two clear functions can call this function,
passing different arguments. In the game manager script, we'll grab a reference to
the fade node at the top. When the player character enters a room after loading
the room's contents, we can tell the fade to
tween its color to clear. When the game first
starts, there won't be a current
room to unload, so this will fade in right away. After entering the second room, we'll want to fade to black before unloading the
current room's contents, then proceed with loading the second room and
fading back in. But this function will
not wait for the tween to finish before unloading
the contents of the room. It will do it immediately. To pause this function while
the tween fades to black, we can use the keyword await. Await will be followed
with a signal. The signal we want to await is the tweens finished signal
in the fade script. So we can return this signal
from our tween function. Changing the return type of our function from
void to signal. Passing the same
return signal along again through the fade to
black and to clear functions. Back in the game manager script, the game manager can now access this signal anytime it
calls these functions. We can await fade to black
or await fade to clear, so this function will pause and wait for the tween to
finish before proceeding. We generally don't want
the player to have control over the character
while this is happening. So we can call
player dot Disable at the top of the
function before starting the transition and re enable the player node at the end after the transition
is complete. I forgot to update
this direction variable to move direction
when I renamed it. While these rooms
are quite small and should be unloading and
loading quite quickly, we still might be able to see an instance
where the previous room unloads and because there is no terrain for the
character to stand on, they fall through
the floor before the next room's contents
load to provide a new floor. So we should also
disable and enable the character at the same time
as we do the player node. Another way we can
enable and disable nodes is by changing their
process mode property. Setting it to disabled when
the transition starts, then inherit when the
transition is complete. Now the player can't
control the character, and the character
won't be affected by gravity while transitioning
between rooms. Next, we'll work on drawing
our rooms on a map. I'll see you in the next video.
33. 4-5 Viewport: Hello, friends. In this video, we'll start working on
drawing our rooms on a map. While it is possible to draw the map separately
from the game's world, it's much more of a hassle to maintain consistency between the world and the map
if you have to update both separately every time
you change something. Positioning icons on the map, especially icons that can move
like the player character, can become an even harder task. A much easier method of creating
a map is to simply have a second camera viewing the world from a Zoomed
out perspective, but draw the world and its
contents in a simpler way. So let's add another camera to our game scene and rename
this one to be Map camera. By default, only the first
camera will be used, and it will draw the
game's main Viewport. To draw the map, the map camera will need to draw a
different Viewport, so we'll add one as a
node in the scene tree, which is a sub Viewport node. Rename it to Map Viewport
and give it a unique name. In some of our scripts, we have already used
the G Viewport function to get the game's main Viewport, which is being used to draw
the game's two D world. The two D world in
this context is not the same as the
world Environment node. It contains all of the TD
nodes currently in the centre, including the game manager. If we child the map camera
to the Map Viewport, this camera will now draw the Map Viewport instead of
the game's main Viewport, but it doesn't have anywhere
on the screen to draw it. And so we'll need to add a texture wrecked node to the canvas layer to give the camera a place to
draw the Viewport. Let's name this to be
Map Viewport texture. I'll remove all of these dummy control nodes
and make sure that the texture wrecked
node is drawn before the fade node by putting it higher in the
scene hierarchy. Clicking on the
texture dropdown, we can select Viewport texture then select the
map Viewport to be drawn as the texture
for this texture wret. The texture rect will
automatically be resized to match the
size of the Viewport. It's currently just drawing the default gray
background color because the map camera and the
Viewport can't see anything. By default, every Viewport
will have its own world. You can think of
these worlds like parallel worlds or
planes of existence. They can occupy
the same space at the same time but exist
independently from one another. But we want this Viewport to draw the same world
as our main Viewport. In the game manager script, we'll grab a reference to
the Map Viewport node. Then in the ready function, set the world two D
property of the Viewport. We can access the
main Viewport by calling the game manager
nodes get Viewport function, then access its world
two D property. Now, both the main Viewport and the Map Viewport will
draw the same world. We generally want our maps to view a lot more of the world, so I'll set it to
one eighth Zoom. If we want to change the
dimensions of the map texture, we can set the
pixel dimensions of the Viewport by selecting
it in the scene tree. I'll make it half
the dimensions of my game 640 by 360 pixels. Rather than drawing this gray
color when there's nothing, I would rather the background of my Map Viewport be transparent. I'll toggle on
transparent background. However, this makes the
map difficult to read, I would like to have this
map texture contained within a panel to make it appear
separate from the main game. Adding a panel container
to the Canvas layer, I'll reparent the map texture to be a child of the
panel container node, then rename the panel container to Map and give
it a unique name. With the default theme, the panel container will draw a transparent black color
and since it is a container, it will automatically resize itself to contain its children. This makes it clear
that the map texture is being drawn on the canvas, not as part of the world, and we can adjust how
the panel container looks later through
the theme editor. I'll use the panel
containers anchors to anchor it to the
center of the screen. We generally want the
map camera to follow the player character around
like the main camera does, but without any of the
tweening or other features. Unlike the main camera, the map camera will never
need to do anything else, so it is safe to
always have it match the player character's
position exactly. There's no way to
have the map camera childed to the player character, since it must remain a child of the map Viewport
to draw the map. We could write another
script to move this camera, but there's a node we can
use to make things easier, the remote transform two D. With the remote transform two D as a child of
the player character, we can set its remote
path to the map camera. Now the camera's
transform properties, position rotation and scale will automatically update to be the same as the
player character. We can toggle off
the rotation and scale by expanding
the update section. Even if we were editing the player character's
rotation or scale, we wouldn't want the map
to copy these properties. Mm. We now have our Games world being
drawn on the Canvas, but we want to draw a
simplified version of the world rather
than an exact copy. I'll see you in the next video.
34. 4-6 Visibility: Hello, friends. In this video, we'll have the map camera draw a simplified
version of the world. Starting in the start room, let's temporarily add in
the contents of the room. I don't want to draw all of
these sprites on the map, just an outline of the
room's shape and terrain. We can use any two D node to draw this depending
on the style we want. I'll use a Tile Map layer node. Give it a new tile set with tiles that are 32 by 32 pixels. I'll import a new sprite, which is just a simple 32
by 32 pixel white square. Then add this to the tile set. Drawing this white square
on my Tile Map layer, I'll draw an outline of the room using the terrain
as the floor rather than the camera boundary and leaving doorways to
connect to other rooms. We want the main camera to
draw only the contents, while the map camera will draw this Tile Map
layer note instead. While both are looking
at the same room, they will see two
different versions of it. Now that I have the
shape of my room, I'll delete the contents
from the scene. Since the Tile Map layer node will always be viewed
by the map camera, it will always
remain loaded rather than being loaded and
unloaded like the contents. Expanding the
visibility section, I can use the modulate
color property to change my white squares into colored ones if I want to. Since this is part of
the forest region, I'll draw its map in green. To have the map camera draw
this but not the main camera, I'll change its
visibility layer 1-2. The default visibility
layer one will be everything that is drawn
by the main game camera, while layer two will be everything I want
to draw on the map. Just like with the
collision layers, we can name these
visibility layers in the project settings under
layer names two D render. I'll name layer one
world and Layer two Map. These visibility layers are dependent on the visibility
layers of their parents. If the Viewport can't see
the start room area node, then it won't bother
to check if it can see the Tile Map
layer node child. The start room parent
node will need to have its visibility layer set
to both one and two, since it will have children that can be drawn by both viewports. Proceeding up through
the hierarchy, the forest region needs to
have its visibility set to use both visibility layers as does
the game scenes root node. The world environment Node doesn't have a visibility
layer property. Now selecting the Map Viewport, we can set its Canvas call mask to only view layer two
and not layer one. This works similar to the collision mask property
for collision objects. Only if the bit for the
mask is set to one, will it be able to see anything whose layer has the
same bit set to one? But we can't access the main Viewport from
the editor to change its Canvas call mask to only view layer one
and not layer two. By default, it will be
masking every layer. So we'll need to edit
it through script. In the game manager
scripts ready function, we can access the main Viewport with the G Viewport function, then set its Canvas
call mask property. Hovering over layer one, we can see its integer
value is also one. We can just set the Canvas
call masks value to one. Now it will only view
visibility layer one and not layer two or any
other layer for that matter. Copying the Tile Map layer
node in the start room, I'll paste it into the second room to also
draw this room on the map. I'll instantiate the
room's contents, then edit the tiles to more accurately reflect the shape of this room and delete the
contents when I'm done. Remember to set the
visibility layer of the parent nodes to exist on both visibility
layers in order for the children to be seen by
their respective viewports. Now, both rooms are
clearly visible on the map with very little effort
required as we add more rooms, only needing to add the map for each room in its scene on a
separate visibility layer. We can easily add other
things to the Map two by adding more sprites to them and putting them on
visibility Layer two, like a player
character map icon. Opening up the player
characters scene, let's add a Sprite to denode
and name it Map icon. I'll populate the texture
with an atlas texture, use the same sprite sheet, and select a region to
use as the map icon. I'll use the character's
head somewhere in the sprite sheet where
the tail isn't in the way. After populating the
texture property, we'll set its visibility
layer to two and not one. Then the root node
of this scene will need to have both
layer one and two. I'll adjust to the
offset of the sprite, so it sits on the floor
like the character does. Knowing that the camera's
Zoom is one eighth, I'll scale this icon up eight times making it appear
very large in the scene. Since I don't want
this icon covering up my character anytime
I work in the scene, I'd rather have it hidden
and this will be true of everything in my game that I want to attach a map icon to. I'll add a script to this node, name it Map icon. And I'll just have it set the visibility property
to true on ready. Now, this very large
map icon doesn't need to be drawn in the editor while I'm working
on my character, but it will be drawn
when I run the game. We now have our map
drawing outlines of rooms and the player
character with an icon, but we may want to have the map toggle on and off with a button. I'll see you in the next video.
35. 4-7 Map: Hello, friends. In this video, we'll allow the player to open and close the
map with a button. Starting in the game scene, I'll have the map
hidden by default by toggling the visibility of the
container node holding it. Then attach a new script
to the container. While I'm currently using a
panel container for this, I won't need to access any of the features of the
panel container in this script and can inherit from the more generic
type of control. If I want to use
a different type of control node in the future, I won't need to
modify my script. This script will need
public functions to open the map
and close the map. To keep things simple, I'll just set the visibility of the node to true or
false accordingly. There are also show
and hide functions defined in the Canvas item
class which do the same thing. While this may seem
pointless to have a function that just
calls another function, keeping your implementations
encapsulated in public functions like this means we can easily update
this in the future. We can add extra logic
in these functions to make things happen when the
map is opened or closed, or we could use a tween to move the map on an off screen or having it grow and shrink rather than just toggling
its visibility. If I want to make these changes, I only need to edit this script, rather than also needing to edit the player script or
any other scripts that are calling
those functions. Since I've decided to use a toggle button to toggle
the map on and off, instead of calling open
or close functions, I would rather have a single toggle function that does both. Adding a private variable for whether or not the map
is currently open, I'll have the toggle
function close the map if it is open or
open it if it is closed. Then the open and
closed functions can set the value of I
open accordingly. Opening the project settings, the input Map tab, we need to add a
new input action for opening and closing the map. I'll use the backspace key and the select button
on my controller. Most games with this
kind of game map will allow the player to
pan the camera around too. I already have look up
and look down inputs. I'll just add look
left and look right. Using the left and right arrow keys or the right stick tilted left or right
on my controller. In the player script, we can export a variable to
hold a reference to the map node and populate
it in the inspector. Then in the input function, tell the map to toggle on or off if the map
button was pressed. Running the game, the
map starts hidden, but pressing the map button, displays the map and pressing
it again, closes it. To pan the maps camera, we'll just need to
adjust its offset value. So we'll give the map script a reference
to the map camera. Adding a public
function named pan, accepting a vector two argument for the direction
to pan the camera. We'll add to the camera's offset this direction multiplied
by a Pan speed, and we can access Delta
even though we aren't in a process function by calling the node classes Get
Process Delta t function. Exporting this pan
speed variable, I'll set mine to 1024. This may seem fast, but remember that the map camera is
zoomed out very far, so it will appear to move
at one eighth of the speed. In order for the
player script to control this camera
when the map is open, it will need to be able to
know if the map is open, rather than making the
is open variable public, allowing any other script
to modify its value, we can write a function which returns the value
of the variable. When the map is closed, I'll reset the offset of
the camera back to zero, so it will once again be centered on the
player character. Switching over to
the player script, we'll add the controls for panning the map in
the process function. But only if the map is open. If the map is open, we'll
tell the map to pan. Just like we used G axis to get the state of two opposite
directions as a float, we can use G vector to get a vector two from
four directions. We just have to give
it the names of the four input actions
matching the X and Y axis. Look left, look right, look up and look down. The last argument
is a dead zone, which is the minimum length of the vector that will be used. If the vector is too short, we can ignore it, which is useful for analog
sticks to ignore drift. While this won't
cause any problems on a controller since I'm using
the right analog stick, my PC controls are using the arrow keys for both
movement and looking. So if the map is open, I'll put the rest of the
looking and movement controls in an block to skip over them. If you want to allow the
player character to still move and look up or down
while the map is open, you'll need to change the input actions so they don't overlap. To prevent the character from continuing to move
after opening the map, I'll set their move direction to zero when the map is opened. The player can now press a
button to access a world map, displaying our two rooms and the player's
current position, and we can pan the map
camera around if we want to. In the next section, we'll
start keeping track of the player's stats and draw
them on the heads up display. I'll see you in
the next section.
36. 5-1 HUD: Hello, friends. In this video, we'll start displaying
information about the player character on
the heads up display. Since starting this course, Godot 4.6 was released. Updating the project to Godot 4.6 did not cause any problems, so I'll continue the lessons
in the updated version. I don't recommend updating to a newer version of any
game engine while you're working on a project
unless you have version control to revert
back if you need to. I'll need to import
some UI sprites into my project to draw the
character stats on the screen. In the game scene, we have a canvas layer which is set to layer one
in the inspector. The main Viewport which draws the two D world of the game
is considered layer zero. So everything in the two D
world of the game will be drawn on layer zero
regardless of their z index. Canvas layer, layer one, will draw over all of that
to draw things like menus and our characters heads up display like their
health and magic. The nodes in the Canvas layer are usually Goy control nodes, which all have
green icons to set them apart from two D
nodes which are blue. Let's start by adding a control node as a child
of the Canvas layer, rename it to HUD, reorder this to be at the
top of the Canvas layers children and make the
map a child of the HUD. We can then add as
many icons, gauges, and text as we need to this parent node to create
our heads up display. It helps to have a basic
control node act as the parent of your entire
HUD for several reasons. Some of the properties
of the parent HUD node will be inherited
by all of the child nodes. Since the canvas layer
node isn't a control node, it doesn't have the
same properties, so can't provide this benefit. If you want to give all of your HUD elements a
consistent look and feel, you can add a theme to
the parent node and it will be applied to all of the child nodes
automatically. It also keeps the
fade transition at the bottom of the
scene tree naturally, since you'll only be
adding children above it, so you won't need to worry about rearranging the scene
tree every time. Unlike two D nodes, which use their transform
position property to define where they are relative
to the scene's origin, control nodes use
both a position and a size to define their location
within a defined space. Usually the game's window, which is displayed as
this blue rectangle. We can see that the HUD
node has a size of 40 by 40 pixels and is at position 00, which is the top left
corner of the window. While we can just move and resize the control node
editing these values, these changes will not
scale well or work effectively if we were to change the window size in
the project settings. So control nodes have these green anchors we can use to set the
position and size of each control node relative to their parent with
the canvas layer representing the entire window. We want the Hud to cover
the entire window so we can set its anchor
preset to full wreck. This puts the anchors at each of the four corners
of the window, then automatically updates the size and position
values to match. If we make the map visible, its position hasn't
been updated, so I'll reset its
anchors to center. Now we can see that since it is anchored to the
center of its parent, moving or resizing the HUD node will move the map
to remain centered. To draw the character's
health and magic, I'll add a new
control node to hold these elements grouped
together and name it vitals. I'll leave this
anchored to the top left but reset its size to zero. Next, I'll add a
texture wrecked node to draw a simple texture, a profile of the character, and populate its
texture property by dragging the asset
from the file system tab. Adding another
texture wrecked node, I'll name this one health icon and populate its texture
with an atlas texture, selecting a sprite
from my sprite sheet. I'll need to have
multiple of these to represent my
character's total health. So I'll duplicate it a bunch of times and position them
roughly in sequence. It would be a good idea
to keep these icons grouped together under
a single parent node. So I'll add another
control node named Health Counter and reparent all of the health icons to
be a child of this node. Manually positioning each of these icons is kind of a pain. So Godot provides control nodes called containers
to do it for us. Since I want these icons to
be arranged horizontally, I'll change the type of the parent node to be a
horizontal box container. This arranges all of the
child nodes automatically. They're being stretched to fill the vertical height of the
horizontal box container, which has the default
size of 40 pixels. Resetting this value
will force it to collapse to the height
of the largest child. I would like them to
be closer together, so under the theme overrides
section constants, I'll set the separation to
be negative eight pixels. Adding more icons
to this counter is as simple as duplicating
the child node. It's a good idea to lay out your HUD elements with their
maximum limits in mind. So I'll have nine health icons, which is the maximum amount I
plan to include in my game. Selecting any of
the child nodes, their position values can no
longer be edited manually, since the container node is now in charge of controlling
their position. So keep this limitation in mind. If you wanted to animate the positions of these
textures individually, you wouldn't be
able to do so while they're inside a
container parent. Next, I'll add another
texture wrecked node to draw the
character's magic gauge. Then populate its
texture property with the asset from
the file system tab. While this texture isn't
functional as an actual gauge, it helps to draw everything you want to include on
your hood to get an idea of how you want it laid out before getting
into implementation. Like the health icons, I want to see how this
magic gauge will fit into my layout when it is
at its maximum limit. Stretching this image to
make the gauge longer stretches the individual pixels of the image, which looks bad. We'll deal with this in the
next video, but for now, we just need an idea of how large or small the
magic gauge can be. You may want to take
some time getting the layout of your hut elements
looking the way you want. This layout looks good. But when I run my game,
it is very small and doesn't match the pixel density
of the rest of the game. Since I'm using a
camera zoom of two, all of my pixels are
doubled in size. So I'll set the scale of
my vitals node to two, doubling the sizes of
all of its children. This is easier to read and matches the aesthetic
of my game better. Changing the anchor presets
of the vitals parent node, we can easily
change it to define its position relative
to any corner of the screen or the
center of any edge. Or the center of the
screen like the map. You can then adjust
the positions of the child nodes relative
to the anchored parent, and they will retain
their relative positions even if you decide to change
the resolution of your game. I'll keep mine in the
upper left corner and give it 20 pixels of padding from
the edge of the window. We now have some textures
drawn on the heads up display, but they aren't actually
doing anything yet. I'll see you in the next video.
37. 5-2 Gauge: Hello, friends. In this video, we'll implement the magic
gauge by using style boxes. Let's start by changing the
type of the magic gauge node. There is a control
node that does exactly what we need
a progress bar. Changing the type
removes the texture. The progress bar is drawn as a semi transparent
gray rectangle, and it has a percentage
displayed in text. I don't want this
percentage label, so I'll disable it
in the inspector. The indeterminate toggle is
not applicable to a gauge, but is meant for progress bars which do not actually
measure any progress. After setting the minimum
and maximum values for our magic gauge, we can set the
value to any number in between to see the
progress bar fill up. The step value is the smallest
the value can increment, forcing it to snap
to the nearest step. The other properties
inherited from range aren't really appropriate for
a gauge and can be ignored. We can also change the
direction in which the progress bar will be
filled if we want to. I'll leave mine as begin to end. How the progress bar looks
is determined by the theme. Since we don't have a theme yet, it is using the default
theme provided by Godot. If we only want to change the theme for this
node by itself, we can expand the theme
overrides section. Since I'm not using the
percentage text display, the colors, constants, fonts and font sizes
are all irrelevant. But you can use
them to alter how the font is written on your
progress bar if you want to. Under the styles section
is where we can find the style boxes for how the progress bars background
and fill are drawn. Clicking on the drop down, we can select style box empty, which will just draw nothing. Style box line to draw
only a simple line, style box flat to draw a simple rectangle or style box texture to use
an imported texture asset. Clicking on the
style box texture to expand its properties, we can populate the
texture property with an asset from
the file system tab. If we change the size
of the progress bar, the texture will be
stretched to fill the space, which doesn't look very good. Style boxes have more properties to alter how the image will
be drawn when stretched. The texture margins will turn the texture
into a nine slice, slicing the image twice horizontally and
twice vertically, so it is divided
into nine segments. The corners will
not be stretched. The edges will be stretched in only one direction to
connect the corners, and the middle will
be stretched in both directions to fill the
remainder of the space. Or we can toggle off drawing the center of the texture
and draw only the borders. If you don't want the individual segments
being stretched, the axis stretch properties will give the option to
use tiling instead, or the tile fit option
will use a combination of tiling and stretching to
best fit the allotted space. Expand margins will
draw the texture beyond the rectangle defined by the nodes position and size. A sub region can be used to draw a region
of a sprite sheet. Modulate works just like it does with the Canvas
item visibility, multiplying the color of
each pixel by this color. Content margins are for style boxes used to
draw container nodes, so they won't be
applicable here. Adding another style
box texture for the progress bars fill and populating it with
the appropriate texture. If you do not want to enter the texture region
values manually, the sub region
editor can also be used to edit the texture
margins visually, too. Using these assets, the magic gauge isn't meant
to be stretched vertically. I'll set its height
to be the same as the assets at 16 pixels. But using the style boxes, the gauge can be stretched as much as needed horizontally, and it will be drawn without
stretching the pixels. The value can also be edited
and the fill texture will also stretch as much as needed without the ends of the
bar being stretched. If we wanted to have
these style boxes applied to more than
just one progress bar, we can make this
automatic by editing the theme rather than
using theme overrides. Selecting the HUD node then
expanding the theme section, we can create a new theme
from the drop down. Clicking on the theme
resource will open the theme panel at the
bottom of the editor window. Here we can see most of the
control nodes Godot has to offer and how they are currently being
drawn by the theme, including a progress bar here. Clicking on the Plus button, we can add Progress bar to
this theme and override its theme properties
the same way we did from the inspector for
the progress bar node. All of the same theme
properties are here for colors, constants, fonts, font
sizes, and styles. If we click on the plus buttons beside background and fill, we can add style boxes to this theme for
the progress bar. Since we already created
these style boxes, we can select the
progress bar node. Then click and drag the
style box resources from the inspector
into the theme editor. The progress bar in
the theme preview is updated to use
the style boxes. Now every progress bar which inherits from the HUD
node will inherit this theme and use these style boxes to draw the
progress bar automatically. This gives us the
option of having all control nodes of the
same type drawn the same way or using theme overrides to draw individual control
nodes differently. We can remove the
theme overrides from the progress bar node since these style boxes are now being inherited from
the HUDs theme. If you want to use
this theme for your entire project without needing to copy it
into every scene, you can save it as
a project resource. Then in the project settings, under GUI theme, we can set a custom theme by selecting
the saved theme resource. This will require restarting
the editor to take effect. Now, every progress bar in every scene of the
entire project will be drawn the same way. But let's imagine
you want to have multiple magic gauges that use these style boxes and multiple other progress bars
that use other style boxes. Rather than having
multiple themes, we can use theme
type variations. Clicking on the Plus button, we can type a custom
name to add to the theme like Magic gauge. Then click on the Tools button
and give it a base type of progress bar either by typing it in or selecting
it from the list. Now the other tabs contain the properties inherited
from Progress Bar, and we can change the values of the background and
fill style boxes. I'll copy the style boxes
from my magic gauge node, then click on the
chain link icon to unlink them from
where they were copied. Switching to the progress
bar theme properties, I'll remove the style boxes, reverting everything back
to the default theme. Then selecting the magic
gauge node in the scene tree, expanding the theme
section in the inspector. I can select magic gauge
in the type variation to tell them to use the theme properties
for the magic gauge. Now my game can have
multiple magic gauges if I need without having to set up the styles for each
of them individually. I only need to tell the theme to use the magic gauge
theme properties, and I can still have
other progress bars in my game that are
not magic gauges, which can be drawn using
different styles too. We now have a
simple magic gauge, but we still need to
implement the health counter. I'll see you in the next video.
38. 5-3 Icon: Hello, friends. In this video, we'll create a
health icon that has animations for
when the character recovers or loses health. We currently have the
health icons displayed as texture wrecked nodes organized horizontally by the horizontal
box container node. If you wanted to animate
these as sprites, you might think to change them into Animated
Sprite two D nodes. But mixing control nodes with two D nodes causes problems. I'll quickly set up the Animation Sprite frames resource so I can demonstrate. Two D nodes will not
behave like control nodes. They won't anchor
their positions relative to their parents or be organized by container nodes or automatically resize to
fit the Window resolution. Rather than changing the
type of node we are using, we will need to animate this
texture wret node as it is. If we expand the texture
property dropdown, we can see that there is an
animated texture option. This animated texture resource can only hold a
single animation, already, it doesn't really
fit our needs in this case. If we wanted to use this
to animate the icon, we would need to set each individual frame of
the animation as a separate atlas texture with a duration in seconds through
this clunky interface. And after all of that,
we would need to use a custom script to control which frames of the animation
are being used anyway. If we wanted to have
the same texture, play multiple animations. So it would be better to leave this texture property
as an atlas texture. While we could write a
custom script to change the value of the region
property of this atlas texture, the amount of time and effort it would take wouldn't really be worth it when the Animation Player node already exists. So let's just add an
Animation Player node as a child of this health icon. Before we do any animations, it would be a good idea to save this icon as its
own scene first. This will encapsulate the
Animation Player node, since it isn't really relevant to the context of
the game's HUD. In this scene, first, I'll set the icons
region to what I want to use as its default state,
which will be empty. Then create a new animation
for when this icon fills up, setting the frame rate
to 12 frames per second, adjusting the Zoom and
turning on frame snapping. I'll then key frame
this to the start of the animation and add
this to the reset track. Even though I don't want to use this as the first frame
of the animation, it's now the default state being used by the
reset animation. Next, I'll overwrite
the first key of my fill animation with
the actual first frame. Then continue with the rest of the frames filling up the icon. I'll set the duration of the
animation to eight frames. Remember to change
the update mode to discrete when using
frame animations like this. That looks good, so
I'll duplicate it and make another animation for when the character takes damage, which will empty the icon. Since I know that my
sprite sheets sprites are all evenly spaced, I can simply edit the
keyed region values by multiples of 32 pixels to
get the frames that I want. This animation is shorter
at only six frames. It's important that these
animations end with the frame that will be
persistent after they finish. Once this empty animation plays, the icon will remain empty until the fill
animation plays. Likewise, after the
fill animation plays, the icon will stay filled until the empty animation plays. Attaching a script to the health icon's root node and save it in a new
folder for UI scripts. We'll assume this
script can be used for any animated icon in case we want to use it for
something other than health. This script only needs to get a reference to the
Animation Player node when it is ready. Then provide public functions to play each of the
animations when needed. Back in the game scene, we can delete all of the
other health icons. I'll rename this first
one to Health icon one, then duplicate it eight
times to recreate the rest of them with the Animation Player and
script attached. If we want to test this out, we can use the player
script to tell the health icon to fill
or empty with a keypress. When pressing the Run button, I'll use the dollar
sign annotated node path to tell this
first health icon to fill. When I release the Run button, I'll do the same thing
to tell it to empty. Since this is only
for testing purposes, I don't need to care how
inefficient this code is. Running the game,
pressing the Run button, fills all of the icons, even though I only told
the first one to fill. This is because Godot naturally tries to be as
resource efficient as possible and has created only one atlas texture resource to be used for all of the icons. So when we animate one icon, all of the icons are
using the same texture, so all of them are animated. In the health icon scene, we can override this behavior by expanding the atlas texture
resource in the inspector. Under the resource section, toggle on the local
to scene option. This will tell Godot that every instance of
the scene will need its own unique copy of this atlas texture resource
with its own region property. This sprite sheet will still be shared by all of the icons, but which region of
the sprite sheet they draw will be unique. Running the game again. This
time only the first icon fills when I press
the run button and empties when I
release the button. With the test successful, we can remove or comment out
the extra lines of code. We now have animated
health icons, but the health counter isn't actually counting our
character's health. I'll see you in the next video.
39. 5-4 Counter: Hello, friends. In this video, we'll write a
script that manages the animated icons to reflect a value and
a maximum value. We have our animated icons here, but we need to attach a script to the parent node
that will control how and when they animate to reflect the counting of the
player character's health. I'll name this icon counter and put it in the
UI scripts folder. There isn't anything
in this script specifically using the features of the horizontal box container, so I'll extend the
more generic type of control to allow changing the type of this node
later if I want to. The script will
need two integers, one for the character's
maximum health and another for their
current health. Since I want this script to be useful for
counting anything, not just health, I'll use
the generic term value. For now, we can set
the default value of maximum health to be equal to the number of children this node has using
the on ready tag. Next, we'll need two
public functions that will set both
of these values, accepting an integer parameter and overriding the old
value with the new one. When we have our
player character taking damage and
recovering health, we'll be able to call
these public functions to communicate the health value
changes to the player. It will be the responsibility of this script to control
how many icons are displayed and which ones
are filled or emptied based on the values of these integers and
how they change. We'll start with setting the value of their
current health. The first thing we should do
is validate that the number we're being given to set the
value to is a valid number. We can't display the character having less than zero health, nor anything greater
than the number of available icons,
their maximum health. So we'll first set the
value to the return of clamp integer between
zero and max health. If we try to call this function
with a negative number, it will set health
to zero instead. And likewise, anything above the maximum will be
capped at the maximum. Next, if we're setting the value to the same
number that it already is, we shouldn't have
to do anything, so we could just return
out of this function. Once we've established that
we are changing the value being displayed by
this icon counter to a new valid number, the process will differ if the number is increasing
or decreasing. If the new value is greater
than the old value, we'll tell some of
the icons to fill. But which icons? The icons
are conveniently drawn by the horizontal box container in the same order as their child
index in the scene tree. Using a simple four loop, using I as the index, we can count through the
range of value to new value. Range returns an array
containing the sequence of integers counting up from the first number
to the second, but excludes the second number. So if the old value was one
and the new value is four, this will give us the
numbers one, two, and three. Since the child index of
the first child is zero, not one, this lines up perfectly with the indices of the icons
that need to be filled. So we'll get the child node at Index I and tell it to fill. The process for decreasing
the value is very similar. Only since we know that the new value is less
than the old value, we need to provide
an extra argument to the range function, negative one, which will
tell it to count down. If the old value was four
and the new value is one, range will return 43 and two. These are not the
same as the child indices of the icons,
but are one greater. So we can fix this by
subtracting one from I in the get child function and
tell these icons to empty. Only after all of this is done, do we set the old value
to the new value? We can test this
out without adding any extra scripts
or input mappings. All we have to do is add an
input function to the script. First, we'll check if
the input event was an input event key and if the event was
a key being pressed. Then use a match statement
on the event's key coode. If the key that was pressed
was any of the number keys, we can set the icon counter's
value to that number. Declaring a number
variable as an integer, we can calculate the number
that was pressed by simply subtracting key zero
from the events keycode. This works because key
coodes are an enumeration, a simple numbered list. Whatever the keycode
is for key zero, the keycode for key
seven is going to be the key coode for
key zero plus seven. We can print out a statement
saying something like set current health two plus the
number converted to a string. Then call the set value
function passing the number. If we run the game, pressing any number key will set
the icon counter to that number and animate the icons according to if they are being
filled or emptied. And we can do the same thing for setting the maximum value too. While we could have the
maximum number of icons in the scene already and just
hide the ones we aren't using, it would be more scalable to instantiate new icons
as we need them. Since this doesn't
happen very often, it won't affect performance
and will accommodate adding as many more as we need to if we decide we want to
hire maximum later. So we'll export a
variable to hold the icon scene as
a packed scene. Copying the contents of
the set value function. After changing every instance
of value to max value, there's only a few more
changes that need to be made. First, we can't validate the maximum value
based on itself. So we'll need to set some limit to how high the maximum
value can possibly be. You may want to export this cap rather than
hard coding a value. Instead of filling icons, we'll be instantiating new icons and adding them as children. And instead of emptying icons, we'll be removing them from the scene tree by calling free. To test this out, I'll use the same number
key input system, but require that I hold down the up arrow to set
the maximum value. So if input dot action
pressed, look up, then I'll print out Max
Health and set that value instead and put the current health
part in the else block. Running the game, I can change the maximum health
value by holding down the up arrow and pressing
any of the number keys and releasing the up arrow, I can still change the
current value health, too. Remember to comment out the
input function after testing. We now have an animated
icon counter that can keep track of the player characters maximum and current
health values. But neither the health
icon counter nor the magic gauge are actually connected to the
player character. I'll see you in the next video.
40. 5-5 Hurt Box: Hello, friends. In this video, we'll give the character
some health to connect to the
health icon counter. We could add health variables to the character script
and track it that way. This would allow
every character in our game to have health
without any extra work. But if we consider
that things that are not characters may also
need to have health, like breakable
walls, for example, this becomes less beneficial, especially since our
character is a character body two D and the breakable
Wall would be a static body two D. And maybe we have another rigid body
to D that also needs health. We would need to have
the same variables and functions in multiple scripts
for each of these cases. We could rewrite our
character script to inherit from
physics body two D, everything in our
game that needs health can also use this script. But there's a lot of stuff
in this script which would not be applicable
to these other objects. It would be wasteful
to have all of these variables and
functions attached to a breakable wall
which would never need to use them and likewise, have the variables and
functions meant for a breakable wall attached
to our character. In cases like this, you may want to use the composite
pattern rather than object oriented inheritance to reuse your scripts across
your game project. By creating a health component, it can exist independent
of what it is attached to. Characters can have a
health component and breakable walls can have
a health component too. In the character scene,
we'll add a new child, an Area two D node, and name it Hurt Box. Then add a collision
shape TD node as a child of the Hurt
Box to define its shape. This will be the area
on air character where if they get hit,
they will take damage. You may not always
want this to be the same as their character
body's collision shape. Also, you'll
generally always want the character body's
collision to remain active, while the Hurt Box will often
be toggled off to prevent the character from
taking damage while dodging or during cut
scenes, for example. So it helps to
give your Hurt Box a separate collision area. I'll make mine also a capsule, but slightly smaller than
the character body and set its debug color to green so I can tell
them apart more easily. This area doesn't need to be responsible for
monitoring anything. It only needs to be monitorable so hit boxes can detect it. Expanding the collision section under collision Object two D, we can set this to be on layer ten and remove its
mask entirely. In the project settings, I'll give two D Physics layer ten the name of player Hurt Box. Now, anything in the game
which is both monitoring and masking layer ten will be able to hurt the
player character. Attaching a script to this node, I'll name it Hurt Box. This script will need
the same maximum health and current health values
as the icon counter has. Since I'll be indicating the
health value with icons, these will need to be integers. If you're using a gauge for
your character's health, you have the option of
using floats instead since a gauge can
indicate decimal values. For now, I'll just set the
maximum health to five and use the on ready tag to set the current
health to Max Health. Unlike the icon counter script, the main way in which a Hurt Box will be modifying the value of current health will
be by receiving damage from hit box
or recovering health. So these will be our
public functions, both of which will
accept an amount of health to lose or
gain as an integer. When taking damage, we'll simply subtract amount from the
character's current health. We can then use the max function to either return
this value or zero, whichever is greater, and set that as the new
value of current health. This will prevent their health from ever going below zero. We can make the
recover function multi use by giving the
parameter a default value. If we call this recover
function with no arguments, we'll assume that to mean that
they should recover all of their health up to their maximum setting current
health to max health. If there was an argument given
to the recover function, then add that amount to the character's
current health and use the MIN function
to either return this result or Max health,
whichever is lesser. So current health will never
exceed maximum health. Both of these cases, the value of current health
is being changed and we need this change to be reflected by the icon
counter on the HUD. But the Hurt Box is in the character scene and the icon counter is
in the game scene. In most cases,
whenever you need to communicate information between nodes and
different scenes, the best way will be
to use signals and form their connection through their closest common ancestor. In the game scene, the closest common ancestor
between the character and the health counter
is the game scenes root node, the game manager. Let's give the health
counter a unique name. Then grab a reference to it
in the game manager script. We could try telling
the health counter to monitor the
character's Hurt Box. We can use the Get node function to retrieve the
character's Hurt Box. But then our icon counter
script will be relying on certain functions or variables existing in the entity
it is monitoring. So it would essentially only be useful as a health counter, which isn't reusable as a script for counting
other things. Reversing this logic, it
would be better to tell the Hurt Box to be monitored
by that icon counter. Then call a function we
haven't written yet like set counter and pass it the health counter
as an argument. Keep in mind that this get node function will fail
if you change the name of your Hurt Box node or
rearrange the structure of your scene tree so it is
no longer a direct child. I prefer this dependency over
writing a new function in the character script to retrieve the Hurt Box node since that script is
already quite long. Back in the Hurt Box script, we'll write this new function, accepting an icon counter as
a parameter of type control, since we didn't give it
a specific class name. Now we have the Hurt Box and the health counter
in the same script, so we can easily
communicate between them. More importantly, the
icon counter script can still be used for
anything we want, and any Hurt Box in our game which don't need to be
monitored by a counter, can just ignore
this one function. We could call the icon
counters functions directly, but that would result
in every Hurt Box in the game constantly checking if it has
an icon counter monitoring it before
calling these functions. It would be better to use signals to communicate
these changes, one for when the
max health changes and another for when the
current health changes. Both will need integer
parameters for their new values. We may also want another for
when the health hits zero, which won't require
any parameters. Now when their current
health changes as a result of either taking
damage or recovering health, we can emit this signal passing their new current
health as the argument. If current health is zero
after taking damage, we should emit the
dyed signal two. Now when we set a counter
to monitor this Hurt Box, we can simply connect
these signals to the icon counters
set value functions, since the parameters passed
by the signals match the icon counters
functions parameters in both quantity and type. It would also be a good
idea to emit these signals immediately so the icon counter can set its values to
match the Hurt Box. To test this out, we can write a simple input function
in this script. If I press up, the character
will recover one health. If I press down, the character
will take one damage, and if I jump, the character
will recover full health. Running the game,
pressing down causes the character to take one
damage with each button press. Taking damage at zero
health is ignored. Pressing up recovers one health, and recovering health
at Max is ignored. Going back down to a
lower value first, I'll then jump and fully
recover all health. Remember to remove or comment out this input function
after testing. We now have our character able to take damage and
recover health, but their health
isn't being saved. I'll see you in
the next video. O
41. 5-6 Data: Hello, friends. In this video, we'll create a custom resource
that can keep track of the player's stats and progress through the game
between play sessions. If we want the player to be able to earn Max health upgrades, then keep those upgrades
the next time they play. The game will need a
way of knowing what they have and have not
found in the game. To do that, we can create
our own resources. We've already been using several resources
in this project, like the character Sprites
Atlas Texture resource or the Animation Tree
state machine resource. And the theme. These
resources are all types defined by the Godot
game engine to provide specific functions
for the nodes that use them. But we can create our own to suit our own
specific needs too. Let's create a new folder inside the folder named resources. We can then create a new
script in this folder named data rather than
inheriting from node, this script will
inherit from resource. Resources are not represented
as nodes in the scene tree. The first thing we want
to do when creating a custom resource is make
sure we give it a class name. Otherwise, we won't be able
to create instances of it. Since the purpose
of this resource is to keep track of
the player's data, we can include as many
variables as we need for everything we want saved
between play sessions. For now, I'll need variables
for the characters Max health and current
health as integers. I'll also add their
maximum magic and current magic as floats. All of these variables will be publicly accessible
to other scripts. We can also include
strings for things like their name or booleans to keep track of something
being turned on or off. Arrays can be used to store groups of
related information, like an array of
booleans to know which upgrades have been found by the player
and which have not. Dictionaries are very useful for storing entire classes
of information, since we can use
attribute names as the keys in the dictionary with their corresponding values. Beyond the basic variable types, we can also include
other classes built into Godot like vectors for
storing two D coordinates, colors, or even other custom
classes that we create. Any variable you
want saved between play sessions will need
to have the export tag. Other variables
which do not have the export tag may still
be useful if, for example, you do not want to save the
character's current health, only their Max health
and start the player off with full health every
time they load their game. These non exported
variables will still remain if your game changes scenes during the play
session, however. If you were to transition out of the game scene
for any reason, maybe to play a cut scene, then return to the
game scene once more, these variables will
retain their value. While we can write functions
in this script as well, it's not usually necessary. If you want to perform some automated initialization
of your resource, we can override
the init function. This innit function will be
called automatically by Godot anytime we create a new instance of this resource
through a script. So we can give the variables
some default values, set the lengths of arrays, or add some keys to
our dictionaries. Be sure to save this script with the class name defined before you try to
use it in any way. So Godot will recognize
this class name. We will need another script to access our custom resource, which we will cover
in the next video. For now, we can test
our custom resource by creating one in
the file system tab. We clicking, select
Create new resource, then type the class name or
select it from the list. Then give it a name.
Creating this resource we'll open it automatically
in the inspector, where we can see and edit all
of our exported variables. Creating a resource in this way does not use the nit function, but will use values provided for variables assigned in
their declarations. If you select something else and want to return to
editing this resource, you can double click on it
in the file system tab. I'll quickly test out creating a data resource in the
game manager script. We can create an
instance of our resource by first declaring a variable with the class name as the type. Then assign the value
of this variable to be the class name
followed by dot Nu. We can then access any of the variables we
declared in this class, both exported and not exported, overwrite their values and use them as needed
while playing the game. Remember to remove
or comment out this test code and delete
the test data resource file. We now have a data resource that includes our characters Max
Health and current health, but it isn't being saved yet. I'll see you in the next video.
42. 5-7 File: Hello, friends. In this video, we'll save and load the player's data using
our new custom resource. To do this, we'll need
another script that manages access to the resource we
created in the previous video. This script will need to be accessible from anywhere
in the project. The character needs
to know their stats. Rooms will need to know if
the player has opened a door, found an upgrade or
defeated a boss. Even the user interface and the map will need access
to the save data. We can make some nodes easily accessible like this
by making them global. Let's create a new folder inside the scripts folder
named Globals. Then create a new script
in this folder named file. The purpose of this
script will be to provide quick and easy access to the players save data to every other script we write
across the entire project, as well as saving it on the player's system
and loading it too. We'll need a variable of the
type of our custom resource. I'll name it data
with a lowercase D. We'll then need three public functions
for managing this data, starting a new game,
saving the game, and loading the game. Starting a new game is simple. We just need to set
the value of data to be our class name
followed by dot neu. This will create a new instance of the resource and initialize. When we're ready
to save our game, we'll need to know where
we want to save it too. For this, I'll declare
a string constant, since this is something
that I won't ever change, the path to the location where I want to save the
player's data file. Godot provides a
simple shorthand that works on any platform. Windows, Mac, Linux, web, and consoles can all use this. User Colon forwards
forwardslash. Do will automatically replace this with the
appropriate path for the platform as a
location which is specific to both the user
of the system and the game. Declaring a variable for the
filename also as a string, I'll name mine save followed by dot TRS short for text resource. Now to save the game, we can access a Singleton
class of the Godot, Resource saver, then
called the save function. Passing the resource
we want to save and the location we want to save it to by appending the
path and filename. If we wanted to have
multiple save slots, all we have to do is change
the file name for each slot. Loading our game is
similar to saving. All we have to do is use
Godot loader Singleton. The load function only requires the location of the
resource we are loading. And it returns the resource. So we'll assign it to
our data variable. Since our game doesn't
have a title scene, I'll just automatically assume that if a save file exists, I'll want to load it, or if
it doesn't start a new game. So in the REDI function, I'll use the resource loader to check if there is a
data file to load, and if so, load it. Otherwise, start a new game. Even with a title scene, it's helpful to be able to start your game directly
from the game scene without having to go
through the title screen every time while you're
developing and testing. We have our data resource here. We can create it,
save it, and load it. Now all we have to do
is make this script easy to access from
anywhere in our project. Opening the project settings, switch to the Globals tab. Click on the folder button
to open a browser and navigate to the file script we just created. Then click Open. We can provide a
different name for this global node if we want
or use the one provided. Then click on add to add the script to the
list of global nodes. Now, any script we write can access this file script
with just its name. Much in the same way we
have been using the input, resource saver and resource
loader singletons. To test this out, I'll set the character's max
health to match what is in the Safle rather than setting it at the
top of the script. So I'll add an
initialized function to this script after accepting
a new Max value parameter, setting the old max value
to the new max value. Then the current value
to the maximum value, I'll emit both changed signals. Now the game manager
after getting the character's Hurt Box can initialize it with the maximum health value
from the player's data file. I'll also add an input function, which when pressing the
up or down arrow keys will increase or decrease the character's
maximum health in the data file and re initialize
the Hurt Box to match. Then save the game by
calling file dot save game. I'll need to reverse
the order of these operations to display
the correct values. Running the game, the character starts with five Max Health, but I can change this value by pressing the up or
down arrow keys. I'll then close the
game and run it again to see that
their Max health has been saved and loaded. To find the data file we
saved under the project menu, select Open user data folder. This will open a
file browser window, and we can see our
save dottRs file. Right click on this file and open it using any text editor. We can see that only the
maximum health was saved. Godot keeps track of which
variables are used by the game and will only save the variables we
actually use and modify. So keep this in mind. We can actually edit this file here, and the game will
actually load it with the edited values as long
as the formatting matches. This is great for us while we are building and
testing our game. But maybe you don't want
to let your players easily edit their safe file
when you're ready to publish. In the file script,
simply change the TRS extension in the
file name to dot RES. Instead of a text resource, this will now be saved
as a binary resource, which can't be opened and
edited in a text editor. Running the game again, I'll open the save dots
file I just created. This is now impossible
for me to edit. We now have the ability
to save and load data about the player's
progress through our game. In the next section, we'll add our
unlockable abilities. I'll see you in
the next section.
43. 6-1 Double Jump: Hello, friends. In this video, we'll give our character
the ability to double jump. While many characters
in our game may need the ability to walk, run, jump, and fall, the
unlockable abilities are usually exclusive to
the player character. In the character
script, rather than adding more variables and
functions to this script, which is already quite long, we can create a new script that will only be used by
the player character. Make sure your
character script has a class name and is
saved before proceeding. From the character scene, right click on the
character's root node and select attach script. Rather than inheriting
from CharacterBody two D, we can inherit from
the class name of our original
script character. Then give this new script a name and save it in
the appropriate folder. Since this new script
inherits from character, it has access to all of the same variables and functions that we created for that class, and all of the values for
the exported variables and signal connections are also copied over to this new script. But we can now add more that are only useful for
the player character. Following this pattern
of inheritance, character is called
the superclass and our player character
is a subclass. First, we'll need a variable
that tells us whether or not this character has unlocked
the ability to double jump. And in order to limit the character to only
double jumping once, we'll need another variable to know if their double
jump is ready. It might also help to have
a signal to emit when we successfully perform
a double jump to draw a particle
effect if you want, just like we did
with the other jump. Since double jumping typically uses the same button
as normal jumping, we don't need to do any extra
work with the input map in the project settings or edit the player script to
call any new functions. We can simply override the
Jump function declaration. When overriding a
function declaration, the function signature
must remain the same. The name of the function,
its parameters, quantity, order, and types, as well as the return type. Since the original
jump function in the character script accepted no parameters and
return a Boolean, this jump override must also have no parameters
and return a Boolean. We still want all
of the original functionality of
the jump to happen. So this character
will be able to jump if the right
conditions are met. We can access the
superclass functions using the keyword super
then use a period, the function name, and parentheses just like we would calling a
function any other way. Since the superclass jump
function returns a boolean, we can also return the result to satisfy the requirement
of returning a boolean. When the player presses
the jump button, the player script will call this player character
jump function, which will then call the more generic character jump function. This structure allows
us to intercept the process by adding extra
code above the return line, allowing us to check if
this player character can double jump in response
to the request to jump first, then only proceed with the default jumping
behavior after. So if this player character
can double jump and the double jump is ready and the character is
not on the floor, then we should allow
them to perform a double jump and return true. If any of these
conditions are false, then we need to proceed with checking if this
character can jump under normal conditions using the character superclass
jump function. We can pretty much just
copy the lines of code that implement a jump when
performing a double jump, but emit a different signal. And we'll need to set the double jump is
ready variable to falls to prevent the
player character from performing more
than one double jump. In most games with
a double jump, the ability to perform
the double jump is restored after
landing on the ground. Since the character
class already has a function which will be called
when the character lands, we can also override that. Anytime this character lands, if their double
jump is not ready, we can set the variable back
to true to make it ready. Then call the
superclass function. In this case, the order
of whether we want to run the superclass function before
or after will not matter. To test this out, I'll add a print statement
when the character double jumps successfully. And another when the
double jump is restored. Then set the default value
of C double jump to true. Running the game, we can press the jump button
twice to double jump. Pressing it three or more times does not
produce more jumps, and the ability to double jump
is restored when landing. Now all we have to do is lock and unlock this ability
to double jump. So I'll set the default value of C double jump back to false. Over in the player save data, I'll remove all of these
unnecessary variables. We'll need variables to tell us which abilities the
player has unlocked, which we can store in
an array of booleans. Arrays are indexed
using integers, we'll need each of our abilities to be assigned a
number in sequence, starting from zero counting up to act as their
indices to this array. If you don't want to memorize
a bunch of integers, we can create an
enumeration to give each integer a name that
is easier for us to use. I like to put these
enumerations in a separate script named enums. While Godot forces us to inherit from an existing class
when creating the script, we can remove this line from our script after it is created. Then give this script
a class name so we can reference it
in other scripts. I'll typically use
the same script to hold every enumeration
for the entire project. We can declare an
enumeration using the keyword num,
followed by braces. Inside the braces, we
can write the names of each of our abilities in
a comma separated list. I've already created
a double jump as my first unlockable ability, and I'll create a Wall Jump, dash, a hat, and
shooting a projectile. Enumerations are
conventionally named in Pascal case while the
enumeration entries are in upper snake case. The enumeration will
assign the value of the first entry
as zero, then one, two, and so on,
meaning we can use these as indices to our
array quite easily. These don't need
to be in the order in which they will be
unlocked by the player. The order is arbitrary and only serves to assign each
ability a unique number. When initializing the
player's save data, we can resize the array to match the size of
the enumeration, automatically giving us
the correct number of booleans and defaulting
all of them to false. Remember that
variables must have the export tag to be saved. Back in the player script, we can reference the
abilities unlocked array and store it in a variable of the same name using
the on ready tag. Note that this
does not just make a copy of the array
in its current state. It creates a reference
to the array, so any changes will affect both. This means that in order to unlock an ability for
the character to use, we only need to set its value
in the save data resource. We can now replace the C double jump variable with indexing the abilities unlocked array with Enums dot abilities
dot Double Jump. This abilities unlocked
array can now be used throughout this entire
script for every ability. In the game manager script, I'll use the input
function to test out unlocking the
double jump ability when pressing the up arrow key. Setting file dot data
dot Abilities unlocked, index dot enums dot abilities
dot Double Jump to true. We can then save the game
to make this permanent. Running the game, my
character cannot double jump. Back in the Godot Editor, I'll switch to the
remote tab to view the scene tree of the game
that is currently running. Here I can see the
file global node, which has a data resource. Inside this data resource, the abilities unlocked array has five Booleans, which
are all false. The player character node also has a reference
to the same array. Returning to the simulation, pressing up unlocks the
double jump ability. In the remote scene tree, I can see that the boolean
in the array was set to true in both the data
resource and the character. Closing the game and
running it again. The double jump is already
unlocked since it was saved. We can remove the print
statements now that the double jump is
confirmed to be working. We now have an unlockable
double jump ability, but we have several more
unlockable abilities to add. I'll see you in the next video.
44. 6-2 Wall Slide: Hello, friends. In this video, we'll allow the character
to slide down a wall, adding friction to
slow their descent. First, we have to decide what this ability will do and
when we want it to happen. When implementing a
new feature like this, it's a good idea to set up tests first that you
want to be able to pass. I'll start in the
room contents scene and hide the trees
so I can see better. To create a wall
sliding mechanic, I'll create two
walls in my room. I'll consider my
Wall Slide mechanic to be complete when the
character can jump, then slide down either wall
at a reduced falling speed. I would also like to play a different animation
while this is happening. The Wall Slide should end when the character is no
longer in contact with the wall if they
touch the floor or we press the movement direction to move the character
away from the wall. So implementing this will be altering the character's
aerial physics. In the player character script, we'll override the character's
air physics process. Then call the superclass's
Air Physics process from the character script. Here we can separate our code
into three separate blocks. When the character is
performing a Wall Slide, checking when they can start
performing a Wall Slide, and any other time we call the superclass
Airphysics process. We'll need to know
when the character is performing a Wall Slide, so we should declare this
as a Boolean variable. It would also be helpful to
export a float to use as a multiplier to the
character's gravity while performing a Wall Slide. I'll use 10%. Returning to the air
physics process, we can now branch
based on whether or not the is wall sliding
variable is true or false. If the character is
already wall sliding, we can either continue
the wall slide or end it depending on if
other conditions are met. If the character is
not wall sliding, then we need to check
if they can wall slide, and if the conditions are met to start wall sliding, then do so. And finally, if all
of this is false, then we should proceed with the superclass air physics
as the default. Writing your algorithm out in pseudocode comments like this makes it much easier to fill in after and rearrange if needed. It also makes the end
result already commented. Let's begin with
starting a Wall Slide by checking if the Wall Jump
ability has been unlocked. Then also add the condition that this character
must be on a wall. The character body
class provides this function which acts
the same as is on floor. I would also like to add
more conditions that their Y velocity must be
greater than or equal to zero. I do not want the character to start wall sliding if
they are moving up, and the player must be pressing the move direction
input toward the wall. The character body two denode also gives us another function. Get Wall normal. This returns a vector
pointing away from the wall. So if we use the X
portion of this vector, this will always be negative
one if the wall is on the character's right
or positive one if the wall is on the
character's left. Comparing this to the
character's move direction, I'll multiply either
side by negative one and require that
the results be equal. This will not allow move
direction to be zero. There are several conditions
in this I statement, and they do not fit
inside my script window. If you want to write your
if statement conditions across multiple lines, you can do so by wrapping
them in brackets. This can make your
code easier to read and debug if you
prefer this format. While my walls are
perfectly vertical, you may need to use the sine of the walls normal X if
your walls are angled. If all of this is true, I'll allow the
character to start a wall slide by
setting I wall sliding to true and capping their Y velocity at zero
using the min function. This will create
a brief reset of falling velocity to make
the wall slide feel slower. I'll also print out a statement
for testing purposes. Now we'll assume
that the character is already performing
a wall slide, and we want to override their gravity to
slow their descent. Simply adding to the
character's Y velocity, gravity times Delta times the Wall Slide
gravity multiplier, which I have set to
10% of normal gravity. Thinking about this from
a physics perspective, this is the equivalent of saying that there
is a coefficient of friction of 0.9 between
the character and the wall. And last, we can think about
when to end the Wall Slide. If the character is no
longer on the wall or if the player
presses the opposite move direction away
from the wall. So if not is on
wall or if the sign of move direction is the
same as the wall normals X, then I'll set Is wall sliding to false and print out a statement that the
Wall Slide has ended. It would be simpler
to put the wall slide continuing as the else
block to this I statement, so I'll cut and paste it here. There is one more condition
that needs to end a wall slide when the
character touches the floor. Since we already have
a function which is automatically called when
the character lands, I'll simply add another I statement here that
ends the Wall Slide. If the character
is wall sliding, set I wall sliding to false, then print out
another statement. It helps if these two
statements are different so we can see when each is actually
happening during our tests. This will work for keyboard, but not for a controller since the move direction can
be a decimal value. So I'll need to apply
the sign function to both of these checks to work
with the controller input. The last thing I
need to do is add a Wall Slide animation to the character's
animation player. So I'll duplicate
their walk animation. Change the Y position of the region of the
spreadsheet that is being used and delete the
footsteps track. Adding this animation to
the state machine resource. I'll allow the character to
transition to this state from either jump or fall if I
Wall sliding is set true. Then transition out
of a wall slide into falling if I wall
sliding is set to falls. Or to locomotion if
they touch the floor. In the game manager script, I'll change unlocking
the double jump to unlocking the Wall Jump. This should be
sufficient to pass my test, so I'll run the game. The character can
still jump normally, and jumping against the
wall doesn't perform a wall slide since that
ability is still locked. But pressing up to
unlock the Wall Jump, now if I jump while
moving toward the wall, they slide down it and stop
when they touch the floor. Trying this on the other wall, the wall slide ends when they reach the
bottom of the wall. And another try against
the first wall, the wall slide doesn't start until the character
starts falling, and I can end the wall slide early by moving
away from the wall. The character's normal
jumping behavior is not affected by performing
any of these actions. With all of the tests passed, remember to comment out
the print statements. We now have our character
able to perform a Wall Slide, but we still have to
implement the Wall Jump. I'll see you in the next video.
45. 6-3 Wall Jump: Hello, friends. In this video, we'll allow the
character to jump away from the wall while
performing a wall slide. In the character script, we've already overridden
the jump function to do a double jump. When we want the
character to wall jump, all of these conditions on the double jump
may also be true. So we'll need to consider
the Wall Jump as a possibility first
and rule it out before checking for
the double jump since the conditions for the Wall
Jump are more specific. We'll only need to check if the Wall Jump ability has been unlocked and whether
the character is currently performing
a Wall Slide. If these conditions are not met, then we can check
if the character can double jump instead. However, this will not
allow the character to perform a wall jump until
the Wall Slide has started, which is waiting until
the character has reached the apex of their jump. If you want to allow wall
jumping before starting the Wall Slide when the character's Y
velocity is negative, then we can also add
more conditions. Either the characters performing a wall slide or they are not on the floor
and are on a wall, and the move direction
is toward the wall. With the order of
operations for logic, we need to put the
or in brackets so it will be checked
before the first and. We can use multiple
levels of brackets and indentation to
visually lay out the order of operations
of complex logic like this to make it easier
to understand if we need to. Now that we are ready
to perform a wall jump, we can do all of the same things like we would with
any other jump. Then return true, so
the input handler knows that this jump button
press was successful. As for setting the
character body's velocity, we want it to not only go up
but also away from the wall. So rather than just setting
the character's Y velocity, we'll set the whole
velocity vector, creating a new vector too. For the X, we can use the
walls normal vector dot X, and for Y, we can use one. This gives us a vector pointing up and away from the wall, so we have the
correct direction. Multiplying this
by the jump force, we will get a vector with
a magnitude greater than our normal jump since the original vector already
had a length longer than one. We can normalize this direction
vector if we want to, but the way it works right now will actually produce
a better result, in my opinion, with the
full height of the jump, plus the extra force being
applied away from the wall. But since jump force
is a negative number, this will invert
the Wall normal to point toward the wall
rather than away from it. So I'll add a negative
to the front of wallnormalt X to make it a double negative, which
becomes positive. To animate the Wall Jump, the character's state machine
will need a transition from Wall Slide to jump under the condition that
the character is jumping. This conflicts with the
transition to falling since the character is no longer wall sliding
at the same time. So the transition to
jumping will need a lower priority value
to be considered first. Instead of double jump is ready, we'll need to set Is wall sliding to falls when
performing the Wall Jump. If we run the game, we
can already perform the wall jump since it was unlocked in the previous video. Jumping against the wall,
we can jump off of it. The amount of lateral
velocity away from the wall is equal to the
vertical force being applied. But since gravity is pulling the character
downward constantly, it's a lot less noticeable than the force away
from the wall, which is only being reduced by either the character's air
control or air brakes. I would prefer if
my character had much less lateral
force applied and could return to the
same wall after performing a wall jump and
continue climbing upward. This will require multiple
different alterations of the physics in order
to work properly, both reducing the amount of lateral force being applied to the Wall Jump
away from the wall, as well as giving the
character more air control in order to return to the
wall at a higher altitude. First, let's deal with
the lateral force. Rather than hard coding a value, it's always better to use exported variables so they
can be twisted and tweaked. I'll export a vector two
named Wall Jump force multiplier and give it a default value of
negative one positive one. We can then multiply the resulting velocity vector by this new Wall Jump
force multiplier. This negative one will
remove the need to have the negative applied
to the wall normal vector, which I find more
intuitive since I think of negative as
away from the wall. When multiplying two vectors, the result will
be the product of both Xs and the
product of both Ys. With these values, this will work the same as
it currently does. But since the value is exported, I can change it in my character
scene to negative 0.5, reducing the X velocity by half. If you want to give yourself reminders of how
your variables work, adding an extra number
sign in front of a comment above the
variable declaration will create a tool tip that
will be displayed when you hover the mouse over the
variable in the inspector. Running the game
again, my character has the full upward
force of the jump, but only half of the jump force is applied in the X direction. But I still can't get back to the wall at a higher point
to climb any higher. For that, I'll need to give my character much
more air control, but I don't want
the character to have more air control
at any other time. So I'll need to override the air control value specifically only when
performing a Wall Jump, then set it back to
its normal value. In the variables exported
for wall jumping, I'll export a new float for Wall Jump air control and set it to something
very high, like four. This will make the character accelerate four times
faster than they would on the ground and eight
times faster than they would during a normal jump with my air control
defaulting to 0.5. I'll also store this in another variable
using the ready tag. So when I want to set it back to its normal value, I'll
know what that is. Since I don't know
whether the player will be jumping away from the wall or back toward it or anything else that
they want to do, the only reliable way
to set air control back to its default setting
would be to use a timer. Adding a timer
node to the scene, I'll name it Wall Jump, give it a unique name, then get a reference to it
with the on ready tag two. I'll name this Wall Jump
air control override to be a little more
clear in this context. Now when we perform
the Wall Jump, we can change the value
of air control to Wall Jump air control and start the Wall Jump air
control override timer. Selecting the timer node, we can set its one
shot property to true and give it a short
amount of wait time. I'll use half of a second. Then connect the timer's
timeout signal to the script. When the timer times out, we can set the
character's air control back to its default value. Testing this out, it's closer, but I still can't
get up the wall. So I'll need to adjust
my Wall Jump force multiplier and Wall Jump air control values until it works. Running the game again, now
when I perform a Wall Jump, my character has
enough acceleration to return to the same
wall at a higher point, allowing me to
climb up the wall. We now have our
character able to wall jump to scale up
vertical surfaces. Next, we'll add a
dashing ability complete with
invincibility frames. I'll see you in the next video.
46. 6-4 Dash: Hello, friends. In this video, we'll give the character an
unlockable dash ability. Starting in the
project settings, the input Map tab will need a button that can
be used for dashing. I'll use the D key or the right shoulder
button on my controller. Then in the player script
in the input function, if the dash button was pressed, try to dash, and
if unsuccessful, buffer the dash input to
dry again next frame, just like we did with the jump. The dash will need a direction, which we can get using
the input Singleton, get axis, move left, move right. To provide arguments
to a callable, we use the bind function, then pass the arguments
to that function instead. So it would be better
to only call the input get axis function once
and store the result. Since this will
only happen during frames when the dash
button is pressed, it's not so bad to
declare a variable here. When the buffered
input is called, it will pass the
bound arguments. If the dash was originally pressed with the
left stick tilted, it will buffer with that
same directional input. Now we can write
this dash function in our player character script, accepting the
direction to dash as a parameter and
returning a boolean, returning false by default. Before we write this function, we'll need some variables. The distance we want
the character to dash, I'll set mine to 128 pixels. The amount of force
required to reach that distance calculated
using the on ready tag, we can use the same formula as the jump height calculation, the square root of distance multiplied by the
negative force, which in this case is
deceleration times two. I would also like to suspend gravity while the
dash is happening. So I'll also store the character's
default gravity in a variable using
the on ready tag. Now for the dash function, if the dash ability
has been unlocked, then we want the character to face the direction of the dash, which we can do by setting
move direction to that value. Then set the
character's velocity to be a new vector two with the dash direction multiplied by dash force as the X
and zero for the Y. Since the left analog
stick can be tilted, this would mean that the
player can dash slower or faster based on how far they tilt the analog stick
on the controller. I don't want this to happen, so I'll use the sine function to remove tilt
from the equation. We can suspend gravity while
the dash is happening, setting the value
of gravity to zero, then return true because the dash was successful and
doesn't need to be buffered. As for the direction to dash, there are several
cases to consider, not just if the player is
giving directional input, but also if they
are not and also if the character is currently
performing a wall slide. Since that is the
most specific case, I'll check that one first. If the character
is wall sliding, I'll overwrite the
value of direction to be sign of wallnrmaltX. If they are not wall
sliding and the player is giving directional input when they press to the dash button, then I'll use the sin of
direction as the direction, removing any analog tilt from the input so I can remove
this from the line below. If the player is not giving any directional input when
they press the dash button, I'll assume they want
to dash forward. I'll use a ternary if to set the direction to
be negative one if the character is facing left
or positive one if they aren't restore the
gravity back to normal, we'll need another function to call at the end of the dash. This doesn't need any
parameters or return a value. We'll just set the character's gravity back to its default. In the character scene, the dash will need an animation in
the Animation Player node. Setting the frames per
second and zooming in, I'll add my key frames
to animate the dash. We'll need to add a call
method track and a keyframe on this track to call
the end dash function at the end of the animation. Or whatever frame you want to restore the
character's gravity. Using this procedure, if it is possible to interrupt this
animation for any reason, we will still need to call the end dash function to restore
the character's gravity, unlike the timer we used for restoring the
character's error control. So keep this in mind and make a comment to remind
yourself of this later. Adding this animation
to the state machine, it should be possible to perform a dash from any of our
existing movement states. Therefore, it would be better to put this animation
on a new layer. Saving the current state
machine as a project resource, I'll name it movement and put it in the character
scenes folder. Then create a new state machine resource for
the Animation Tree. Adding the movement
state machine as a node in this
new state machine, I'll rename it to
movement and create a transition from start to movement, so it
is the default. Adding a new dash animation
to this state machine, we can add a transition from movement to dash and
back to movement. In order to play an
animation one time, the transition leading to it
should be set to enabled, and the transition
leading out of the animation should
have its switch mode set to at end and leaving the
advanced mode as automatic. We can test this animation by clicking on the play
button in the state. With each click, it plays
once in its entirety and returns back to the movement state when
it is finished. Since the transition leading to the animation is not automatic, it will be up to us to trigger this animation manually
through our script. Declaring a new variable, this will be used for
multiple different abilities, so I'll put it at the top and just name it
animation for short. The type of this
variable will be an animation node state
machine playback. This is what Godot uses to navigate the state machine
and play animations. Selecting the node
in the scene tree, we can see this
playback resource under parameters playback. We can retrieve this
resource by clicking and dragging the Animation Tree
node to get its node path. Then use square brackets to get a resource
from this node. Clicking and dragging
this resource from the inspector into the square brackets gives us
the path to the resource. If we expand movement
and locomotion, you can see that each layer of the state machine has its
own playback resource. While we currently
only need this one for manually
triggering the dash, we will be adding
more abilities soon. This path will also change as we add more layers to
the state machine, so it will need to be updated. To trigger the dash animation, when successfully
performing a dash, we can tell this
playback to travel to the dash state by specifying its string
name as an argument. This line of code is the
equivalent of pressing the play button in
the Animation Tree when we previewed the animation. I also want to remove
the player's ability to control the character
while the dash is happening. So overriding the
physics process, we can quickly interrupt
it and check which state the animation state
machine is currently in using the function
G current node. This returns the string name we wrote on the node in
the Animation Tree, which we can compare to see
if it is movement or not. If it is anything
other than movement, I'll set move direction
to zero before proceeding with the normal
superclass physics process. This will prevent
the character from moving or facing a
different direction while performing any of the abilities on the top
state machine layer. In the game manager script, I'll change the up arrow key
to unlock the dashability. Running the game, pressing
the dash button does nothing. After pressing the up arrow key to unlock the dashability, we can dash, causing the
character to dash forward. Pressing a direction will cause the character to dash
in that direction. And if we dash
while wall sliding, the character will dash
away from the wall. We now have our player character able to dash around
the environment, but we will need to add a cool down timer to prevent the
player from using it too often. I'll see you in the next video.
47. 6-5 Cooldown: Hello, friends. In this video, we'll add invincibility frames and a cool down to
our dash ability. Before we start adding
invincibility frames to the dash, it would be helpful
to have a hit box in our scene so we can test
out dashing through it. In the start room
contents scene, I'll add an area to denode
and rename it to hit Box. Then add a collision
shape as a child. Give it a rectangle shape as a resource and set its
debug color to red. You can imagine that
this will represent an enemy that damages the
player character on contact. I'll make it one tile thick, 32 pixels, and two
tiles high, 64 pixels. Then move it over to the left. I'll consider my test passed if the player character can dash through this area
without being harmed. In order to have this collision area deal damage to the player, it will need to be monitoring. It does not need
to be monitorable, although leaving this box checked won't make
any difference. It does not need to
exist on any layer, but must be masking the
player Hurt Box layer. If it helps you to understand
collisions better, it is fine to consider
this hit box to exist on an enemy hit box layer or an
environmental hazard layer. The only requirements
are that it must be monitoring and masking
the player Hurt Box. Attaching a script to this node, we'll connect its on area
entered signal to the script. After checking if the area
has a take damage method, we'll call this method and pass one as the
amount of damage. This will make sure
that the area entering this one has the Hurt Box
script we created earlier, which has the take
damage method, accepting the amount of
damage as a parameter. If another area triggers the signal without
a Hurt Box script, it would cause an
error since there wouldn't be a take
damage method to call. To be able to see our collision shapes while testing the game, we can open the debug menu and select visible collision shapes. If we run the game, we
can see the hit box, and the player character takes one damage every time
they enter this area. There is an error message because the room's
contents are being loaded in response to our character entering
a collision area, and Godot doesn't like adding another collision area
during a collision event. We'll ignore this for now since we're just
performing a test, and this isn't going to be
part of our final game. In the character scene, select the Animation Player node
and the dash animation. To add invincibility frames
to our dash animation, all we have to do is keyframe the Hurt Box's
monitorable property. We can set the exact frame when the Hurt Box turns off and
when it turns back on. If you want, you
can also keyframe the visibility of your
Hurt Box to match its monitorable
property so you can see it turn off and back on
again while playing the game. This makes it easier to know when your character is
actually invincible. It might also help to
toggle off the visibility of the character
body's collision shape to better see the Hurt Box. Running the game,
we can now dash through the hit box
without taking any damage. Walking into the
area still inflicts damage as does ending the
dash inside the area. Now let's add a
Cooldown to the dash. We'll start by adding a timer node to the
character scene. I'll name this timer dash Cooldown and set it's one
shot property to true. I'll leave its weight
time as 1 second. This will be the cool
down time of the ability. Considering this is
the second ability with an associated timer node, and we may want to add
more in the future, you may want to consider using a normal node as a collapsible
folder to contain them. But it will help to
give it a unique name in case we want to
restructure the scene tree. We don't need to
connect the signal to the script to use this
timer node as a cool down. In the variable declarations
for the Dashability, I'll use the on ready tag to get a reference to
this new timer node. All we have to do is start the timer either when
starting or ending or dash. It's up to your
preference which, but if you want
to start the cool down at the end of the dash, another condition
will be required to prevent the player from
dashing twice in a row. I'll start the Cooldown
timer after the dash ends. Then return to the I
statement where we check if this character
can start dashing. We can check if the
current node of the animation state
machine is movement. This will also have
added benefits later, since we won't allow
the character to dash while performing other
similar action abilities. To enforce the Cooldown timer, we only need to check
if this timer is stopped to know if we should
be able to dash again. If the dash animation is already playing or the Cooldown
timer is not stopped, that means the dash will fail. Running the game now, pressing the dash button allows the
character to dash only once, then forces a 1 second cool down before they are
able to dash again. This works, but it isn't being communicated to the
player in any way. Usually games like this will
have a diagetic indicator, something that
exists in the game, not on the HUD to tell the player when the
cool down is finished. So let's add an Animated
Sprite two D node to the character scene and name
it Dash Cooldown effect. Creating a Sprite frames
resource for this node, we can import a Sprite sheet. I'll just put this in
the dust folder for now rather than create a new asset folder
for one Sprite sheet. And add the Sprite frames
from the Sprite sheet. I'll turn off looping, set
the frames per second to 12, and also add a blank frame
at the end of the animation. This isn't centered
on the character, so I'll move it up 12 pixels. This animation is
longer than 1 second, but the 1 second mark is when I want the cool down to
be considered finished. I'll give this node
a unique name. Back in the script,
all we have to do is grab a reference
to this node. An tell it to play
when the dash ends. But since my animation is longer than the Dash
Cooldown timer, I'll actually need to
stop the animation before playing it in case
it is still playing. This will reset the
animation back to the start. A Now, when we run the game and
use the dash ability, the animation plays, but it isn't following
the character. This is because a normal node
does not have a transform. Therefore, it doesn't
have a position. This breaks the
line of inheritance from the character's root node. So the Animated Sprite
two D node is now starting its own ancestry
for two D transforms, starting at position 00. We can change the type of
the abilities folder to no two D so that it will provide a line of succession
through the scene tree, and the Animated Sprite
will follow the root node. It is now clear to
the player that their dash ability is
on Cooldown and exactly when they can dash
again without having to add any extra
information on the HUD. We now have our dash ability able to dash through hit boxes without getting hit and cooling down before being
able to be used again. Next, we'll add a
passive ability which changes the
character's appearance. I'll see you in the next video.
48. 6-6 Hat: Hello, friends. In this video, we'll implement a
passive ability that alters the
character's appearance. Starting in the room
contents scene, I'll duplicate the
collision area from before for this test. This time, the
collision area will specifically represent
intense heat from inside a volcano. I'll change the color to orange
so I know which is which. I want the player to be unable to pass through
this area until they have unlocked a magical hat that allows them to
proceed unharmed. But wearing the hat
will still allow them to take damage from
normal hit boxes. So rather than have this masking the normal player
Hurt Box layer, I'll use a different layer. I'll use layer 14 as a layer dedicated to this
environmental heat effect. This will be easy for me to remember since it is
directly below ten. In the project settings, I'll name this layer heat. Then in the character scene, I'll have the Hurt Box
also exist on this layer. Now this will work the same way as the collision
area before, dealing one damage to the player every time
they enter this area. Only it is checking
for collisions on layer 14 rather than layer ten. So all we have to do to make the player character immune
to the heat damage while wearing the hat is remove layer 14 from their Hurt Box
collision layer property. To alter the
character's appearance, I'll layer additional
sprites over the character. I'll import new sprite sheets for three different upgrades, a gem, a cloak, and a hat. Then in the character scene, I'll add an extra Sprite Tu
Di node for each of them. All with atlas textures
just like the body sprite. Then assign the sprite sheets as the atlas property
for each node. The way these assets were drawn, I'll need to layer them specifically in the
order of the cat, then the cloak, then the
hat, and the jam on top. I'll start each off with the first sprite at
position 00 with a size of 32 by 36 pixels and give them the same offset to move
them up by 18 pixels. These assets are
drawn to animate synchronously with the
normal character sprite. So all of the region
values will be the same for every layer
at the same time. All I have to do is add each sprite's atlas texture
region to the animation. Then copy and paste
the keys from the body Sprite track
to the other tracks. And change their update
mode to discrete. Now the animation
also includes all of the sprite layers in
perfect synchronization. I'll need to do this for
all of my animations, but it won't take
that much effort. In order to flip each
of these sprites, we could set all of
their flip H properties every time they
change direction. But there's an easier way using the scene
tree's inheritance. If we make all of
the other sprites a child of the body sprite, they will inherit their
transform from it. And we can flip a sprite using its transform by setting its
X scale to negative one. Note that this trick
should not be used for anything involving
physics or collisions. It works fine here because
we are only drawing sprites. In the face left function
in our character script, rather than setting flip H, we can set scale dot
X to negative one, if left, otherwise,
positive one. This will now synchronize the flipping of all
of the sprites. With all of the setup of
the sprites complete, I'll set the visibility of the other sprites
off by default, then turn them on as
abilities are unlocked. In the player character script, we'll need a function to
manage which Sprite layers are visible based on which abilities
the player has unlocked. I'll name it Update
Sprite visibility. Then give each of the sprites a unique name and set their visibility to match the abilities unlocked value
for the matching ability. The double jump
will add the cloak. The hat is obvious. And the ability to shoot
projectiles will add the gem. In the game manager script, when we first start
playing the game, we can call this function to update the characters
Sprite visibility right away in case we have loaded a safe ale with abilities
already unlocked. While playing the game,
we will want to also call this function anytime the
player unlocks a new ability. So we'll add a signal
to this script for when an ability
has been unlocked, with the abilities integer
number as a parameter. Then write a public function
to unlock a new ability, accepting the same
ability number and omitting the signal. We can then connect this signal
to the player character. However, we did not
write the update Sprite visibility function
to accept any parameters. So when we connect the signal, we should unbind one argument to avoid causing any errors. This will work to change
the character's appearance, but they will still be taking heat damage while
wearing the hat. When updating the character's
sprite visibility in the specific case
of adorning the hat, I'll also remove
the collision layer for heat damage to make
them immune to it. To remove a single specific
layer from a bit mask, first, hover your mouse cursor over the layer and take
note of its value. Layer 14 has a value of 8,192, which is two to the power of 13 or two to the power
of 14 minus one. The collision layer is
actually an integer, which in binary is a
sequence of zeros and ones. Currently, it is all zeros, except that the tenth
and 14th bits are one. All we need to do is change the 14th bit to zero to
remove it from that layer. We'll start by grabbing a
reference to the het box. Since this function is not
being called very often, it's fine to use a node path. To remove the collision layer, we could subtract
its value 8,192. However, this depends on the collision layer currently
existing on layer 14. If we were to call
this function for any reason when layer 14
is already turned off, simple subtraction
would not work. Using the bit operator and on our Hurt box's
collision layer, if we can provide a sequence of all ones with only
the 14th bit as zero, the result will be
setting the 14th bit 20 regardless of
its current value. To get that number, we
can reverse the bits of 8,192, the layers value. This is done with the
bitwise naught operator, which in GDScript is a tilday. This is a very
mathematically efficient way of removing collision layers. If you want to add
a collision layer, setting its bit to one, all you have to do is use the or operator instead of and
and remove the knot. But we aren't interested
in doing that here. In the game manager script, I'll update my input
function to unlock each new ability in sequence when pressing the up
arrow for my test. Declaring a new variable here, so it is bundled with
my testing code. I'll remember to
delete this along with the input function when I no
longer need it for testing. I'll name it next ability as an integer which naturally
defaults to zero. Each time I press the up arrow, I'll unlock the next ability and print out the
name of that ability. We can use the keys function on an enumeration
to get an array of strings that are the
same as the ones we wrote as the
enumerations entries. So indexing the keys array with the ability number gets
the name of that ability. I'll then increment
next ability, but keep it clamped to be
between zero and the size of the abilities enumeration minus one to prevent any errors. I'll delete the
current safe file so that my abilities will start locked and turn on
visible collision shapes. Running the game,
we start off with no abilities unlocked and the
default character Sprite. I'll open the remote scene
tree and select the characters Hurt Box to verify that they
are on layer ten and 14. Pressing the up arrow once, the double jump ability
is now unlocked, so the character is
wearing a cloak. Pressing the up arrow again, the wall jump is unlocked, but the character's appearance
remains unchanged since that ability was not associated
with a cosmetic change. Touching either collision area will cause the character
to take damage. Pressing up once more, now the dash is unlocked, too, and still the
character looks the same. Up again to unlock
the magical hat, and we can see that the
character's Hurt Box is no longer on layer 14, but still on layer ten. Touching the normal
hit box causes damage, but entering the new volcanic
heat hit box does not, meaning the character is immune to it while wearing the hat. Pressing up one last time, we unlock the shoot
ability and add the gem, but we haven't
implemented that yet. We now have our
character changing outfits to match their
progress through the game. The last ability we will add
is shooting a projectile. I'll see you in the next video.
49. 6-7 Projectile: Hello, friends. In this video, we'll add a
projectile spell that the player can shoot by
spending their magic points. First, we'll need to give this character magic
points to spend. In the player character script, I'll add a new export
category for casting spells. Then two variables for their maximum magic and their current magic
both as floats. We'll also need signals to emit anytime these values change
so we can update the HUD. Overriding the ready function, we can set the maximum magic to the value from the
player save data. Then I'll just start the
player off with full magic, setting the current value to the max value and emit the changed signals
to update the HUD. From the game scene, we can easily connect the
player character signals to the magic gauge since they exist
in the same scene. The magic gauge doesn't have a script attached, but
it doesn't need one. Since the progress bar already provides the functions we need, we just have to toggle
off script methods only. The range class gives us the set max function
accepting a float parameter. As well as the set
value function, also accepting a
float parameter. Now the magic gauge
will automatically update with the player
character's magic variables. In the project settings, the input Map tab
will need a button that can be pressed by the
player to cast a spell. I'll use the C key or the top Face button
on my controller. Then in the players script, we can copy the same
format as the jump to try to cast a spell
and if unsuccessful, buffer the input to
try again next frame. Back in the player
character script, we can write the cast function, accepting no parameters
and returning a boolean. All we really need to do here is check if the ability has been unlocked and if the character is not busy performing
any other actions, if the animation's
current node is movement. Then tell the animation to
travel to the cast animation and return true if the input was successful or
false if it wasn't. In case we don't want the casting of our spell
to be instantaneous, everything else will need to happen in a separate function. We'll name this one
shoot projectile. It doesn't need any arguments
or to return any value, and we don't have any of the information to write
this function yet, but we'll need this
function declared and saved before we can include
it in the casting animation. Switching to the
character scene, we'll start by adding
an Animated Sprite to dende to use as
the casting effect. This will need a Sprite
frames resource. Before importing my sprite
sheets for spell casting, I'll make a new folder
for magic sprites and move the dash Cooldown
effect into this folder. Then import sprites for casting, the projectile and
its impact sprites. Adding the casting
animation sprites to the Sprite frames resource. Y I'll set it to 12 frames per
second, not looping. And I'll add a blank frame
at the end of the animation, then move it up 18 pixels
with the Y offset. This looks good, but if we til this to the
character Sprite, it will also flip to face the same direction
as the character. Selecting the
Animation Player node, we can duplicate any of the existing animations to
create a casting animation. After removing
unnecessary tracks and updating all of the
Sprite region keyframes, We'll add call method tracks to both the character's
root node and the casting effect
Animated Sprite no two D. For the casting effect, all we have to do
is tell it to play. For the character, we'll add a keyframe for shooting
the projectile. These keyframes can
easily be adjusted to happen anytime during
the casting animation. I'll start the cast effect on the second frame and fire the projectile
on the third frame. The rest of the animation
will act as recovery. In the Animation Tree, we only need to repeat the same process we
used with the dash. Transitioning to
cast is enabled, and we're turning back to movement at the end
of the animation. Next, let's assemble
the actual projectile. Start by adding an area two D note to the
character scene. Then give it a collision
shape and a shape resource. I'll use a circle but reduce
its radius to eight pixels, and I'll change its
debug color to something I haven't used yet, like purple. Then add an Animated Sprite to denode and give it a
Sprite frames resource. First, we'll add
the sprite frames for the projectile as we want it to start and use that as
the default Sprite animation. This will be looped and I'll set my frame rate to 12
frames per second. Then we'll click on the add
Animation button to create a second Sprite animation and name this one
something like Impact. Now we can add more
Sprite frames for an impact animation
that will be played by the same projectile when
it collides with something. You may also want to add an
empty frame at the end of this animation to allow the last frame to draw
for its full duration. Remember to set the default
animation to autoplay, so we will play
this animation when the projectile spawned
automatically. Still within the
character scene, reposition the
projectile to where you want it to start in
relation to the character. You can make this
easier by selecting the projectile and
the scene tree and holding Alt
while you move it. Mine is 12 pixels to the right and up from
the character's origin. Next, add a marker two D node as a child of the projectile. This will ensure
that its position matches the projectile exactly. The marker to D node is exactly
the same as a node two D, except that it draws
a little gizmo in the editor so we can see
exactly where it is. This gives us the position
where we want to spawn the projectile into our scene
when casting the spell. So we can name the Marker
Tutti node accordingly. Then reparent the Marker Tutti node to the character Sprite. Since it is a child of
the character Sprite, it will flip its exposition when the character changes their facing direction automatically. Save the projectile branch as its own scene and save it in a new scenes folder
for projectiles. Then delete it from
the character scene. We now have a projectile and we know where we
want to spawn it, so we'll return to the
player character script. We can export the projectile as a packed scene and populate
it in the inspector. We can also grab a reference
to the spawn marker. It would be better if
it had a unique name. And we'll need to specify a magic cost to firing
this projectile. I'll set mine to
16 magic points. Before allowing the player
to start casting the spell, we should also
check if they have sufficient magic
points by checking if their current magic is greater than or equal to the magic cost. Then when the cast animation calls the shoot
projectile function, since there is a
very brief window of time between when these
two functions are called, it's not a bad idea to
also still verify that the character has
sufficient magic remaining to cast the
spell again here. So checking if current magic
is less than the cost, returning out of this function will not fire the projectile. Knowing that this
is not the case, we can subtract the cost
from our current magic and emit the current magic changed signal to
update the HUD. Then we'll create
a new projectile by instantiating
the packed scene. We don't want this to follow
the character around as a child rather as a
sibling within the world, so it can move on its own. So we'll get the parent first before adding this
projectile as a child. Then set its position
within the world to be the projectile spawn
marker's global position. We have to use global
position here because the marker's local position is in relation to the
character's position, but the projectile
is being added to the world node,
not the character. So we want the position of the marker in relation
to the world. And finally, we'll tell this projectile which
direction it is firing. We can pass an argument
as Vector two dot left if the character is facing
left or right, otherwise. Now we'll need to write this
function for our projectile, opening the projectile scene, then adding a script
to its root node, we'll name it Projectile and save it in the
scripts folder. It will help to give this script a class name so we can
reference it later. This projectile will need
some exported variables, like a speed for how fast it moves and the amount of damage it does when
it hits something. I'll name this default
damage so it can be overridden and give
it a value of one. We'll also need a reference to the Animated Sprite Tu Di node to tell it which
animation to play. It needs to know which
direction it's being fired and how much damage
it will actually deal, if not the default, and whether or not it's
currently moving. When told to fire, we can accept the direction as a vector two and the amount of damage as an integer with a
default value of zero, making it an optional parameter. We'll set the direction to
the direction parameter and flip the sprite if
direction dot X is anything less than
zero, meaning left. Then set damage to damage if any value of
damage was provided, otherwise, use the default
damage for damage, and finally set is
moving to true. Overriding the process function. If this projectile is moving, then we can set its
position to be plus equals direction times
speed times Delta. This projectile will
need to collide with both bodies and areas. So we'll need to
connect both on area entered and on body entered
signals to the script. The bodies this projectile will be colliding with
will be the terrain, while the areas
will be Hurt boxes. Both of these will do
much of the same things. So we'll call a separate
private function from both and name it on impact. When the projectile
impacts something, we'll have it stop moving, then disable any more collisions by setting its
collision mask to zero. This turns off every layer
of the collision mask. Then play the impact
animation and connect the animation finished signal to the quarter function to automatically remove the
node from the scene. If the projectile
hit a Hurt box, we can tell that
Hurt Box to take the damage amount before
calling on impact. We aren't using the
body parameter, so we can mark it as
unused with an underscore. If the projectile
doesn't hit anything, we still need to clean it up. A simple way is to have the room automatically destroy it when it leaves the room
it was spawned in. From the room script, rather than connecting
this signal through the inspector every
time for every room, it would be easier to do
it through scripting. So we'll write an on
area exited function, accepting an area as a no two
D and returning no value. If the area which exited
this room was a projectile, we'll queue it to be freed
from the scene tree. Then connect the area exited signal in
the ready function. We can also do this
to automatically connect the body
entered signals, too. Just remember that if any of your room scenes already have
these signals connected, you should disconnect
them to avoid errors. But as we create
hundreds of rooms, making these signal
connections automatically scripted will save us way
more time and effort. In order for the room's collider to detect a projectile
exiting it, it will need to mask
the collision layer that the projectile
itself is on. Running the game, I already
start with every ability unlocked from the previous video and my magic gauge is full. I'll open the remote
scene tree so I can see my projectile spawn
and get cleaned up. Returning to the game, pressing the shoot button cast a spell, which fires the projectile. The projectile collides with the wall and explodes on impact. My magic gauge is reduced
by the cost of the spell. Aiming in the other direction, the projectile has
nothing to hit, and so it will leave
the current room. But rather than continuing
to move off into infinity, it will be destroyed once it leaves the boundaries
of this room. We now have all of our unlockable
abilities in the game, and we're ready to
start adding enemies. I'll see you in
the next section.
50. 7-1 Pace: Hello, friends. In this section, we'll add some
enemies to our game. I'll start by making
a new folder to hold my enemy sprites and import a sprite sheet from my first enemy, the blind mouse. We'll need a new scenes
folder for enemies, too. We can make things
easier if we duplicate our existing player character to use as a template
to make our enemy. Then move it into
the enemy's folder. Opening the new scene, I'll rename the root node to
match the scene name. Then go through the
scene hierarchy and decide which nodes are needed for the enemy
and which are not. We'll still need
a collision shape to collide with the terrain, a Hurt box to take damage, and a sprite to
draw the character. But the children of the
Sprite are not necessary, since this enemy doesn't have any other sprites or the
ability to shoot projectiles. We'll keep the
Animation Player, tree, and sound effect nodes, although you may want
to change the sounds. The Coyote timer will
not be necessary. Whether or not you
want your enemies to be drawn on the map is
up to your preference, but you'll obviously
need to change the texture if you decide
to keep the map icon. And we can also delete
all of the ability nodes. The root node has the player
character script attached. We'll need to replace it with the normal character script, since this is not the
player character and they do not have any of the
player character abilities. Selecting the Sprite Toti node, we can replace the atlas
textures atlas property with a Sprite sheet
for our enemy. Since the sprite sheet is different from the one
for my player character, I'll need to go through each of my animations in my Animation
Player node and check that they have the
correct number of frames and the correct
region values keyed. This enemy has fewer animations than the
player character, so I can delete everything
other than idle and walk. The idle animation works fine, since the keyed region
values match exactly, but the walk animation
doesn't match, so I'll need to fix it to match the region values for
the sprite sheet. I'll quickly update to the keyed Y region values to
get this animation working. We'll ignore the hit
and dead animations for now since we
can't test those yet. The Animation Tree will need a much simpler state
machine for this enemy, not the same one that the
player character is using. So we'll make a new
state machine resource. Then add the idle
and walk animations transitioning from
start to idle by default to walk if X
velocity is not zero, then back to idle if
X velocity is zero. That's all this enemy
needs to do for now. Taking a look at the character
script for a moment, the script already
has everything we need to make a character
pace back and forth. Although since I don't want my enemies to have coyote time, I'll need to either
move any code related to it to only be in the
player character script, or I can simply make
sure that none of the Coyote time
related code runs if there's no Coyote
time or node. Since I've already used the get node or null function to check for the
Coyote timer node, this variable will be null
if no such node exists. The only time we are using the Coyote timer node is
in the physics process, and we can just
check if there is a Coyote timer node
before attempting to call it start function
to avoid any errors. This same process could also be used if you don't want
to include all of the exact same nodes in
your various character and enemy scenes for things like
sounds or particle effects. Let's switch to any of our existing room content scenes and add this new
enemy to the room. My plan for this enemy is to have them pace
back and forth. When they hit a wall, I
want them to turn around, wait for a second, then walk
in the opposite direction. So I'll edit my
terrain to create a simple platform with
walls on either side. So this enemy will pace
back and forth in here. I'll also add another
platform without walls, and the enemy should walk off
of the ledge in this case. You may want to write
another script like the player character script
inheriting from character, which also includes
this behavior. But it's better to think
of this behavior as an alternative to the player
node in the game scene, through which the player can control the player character. Instead, we write a script which controls this
enemy's behavior. So we'll create a new node
as a child of the enemy. I'll name this node pace. This way, the same
behavior script can be used for multiple
different enemies, and also the same type of enemy could have multiple
different behavior scripts. Including this node in the
enemy's own scene would make it automatically applied to
all enemies of the same type. While putting it in the
room contents scene will allow for the same enemies to potentially have
different behaviors. We'll attach a script
to this node and put it in a new scripts
folder for enemies. In order to tell an
enemy what to do, we'll need a reference to the enemy itself
stored in a variable. We can call the
function G parent to get the direct parent of this node and assign it to the variable using
the ready tag. Note that this will only work if the behavior node is a
direct child of the enemy. We should also
export a variable to say which direction this
enemy will go first. So I'll name it
direction and give it a default value of one,
meaning to the right. Overriding the process function, all we have to do is set the enemy's move direction to this direction to get
them started walking. When the enemy is on a wall, we can tell the
character to face left and set their move direction
to zero, so they stop. To wait for a short
time, we need a timer. Since this node
doesn't have a type, we can use this node as the timer itself by
changing its type. The type that this script extends will also
need to change to timer in order to access any of the timer nodes,
properties or functions. We don't need to connect
the timeout signal, since all we really need to know is whether or not the
timer is stopped. If the timer is stopped, we'll set the enemy's move
direction to direction. Otherwise, we can set it to zero because this enemy
is currently waiting. When the character hits a wall, we'll set the value of
direction to be the sine of the walls X normal
and start the timer. To make the character face right when they hit a
wall to their left, we'll need to also make this dependent on the sine
of the walls X normal. We also only want to stop the character and
turn them around if their current
pacing direction is not the same as
the walls normal. We are calling this get Wall normal function several times, so it would be more efficient to store it in a local variable. Let's do some light
refactoring here. In the room contents scene, we can adjust the amount of wait time and set the one
shot property to true. I'll duplicate the pace node and attach it to
the other enemy. Then give my two enemies different values for their
initial pacing direction. Now when we run the game, with the help of our
behavior script, the enemies pace, one to the left and the
other to the right. The second enemy walks
off of the ledge, but the first hits the wall, turns around and
waits for a second before turning around and
walking the other direction. And they both repeat
this behavior. But you may have noticed that their animations don't
always match their velocity. Both characters are actually animating in synchronization. This is because just
like the health icons, they are sharing a resource. In the enemies scene, we'll need to select the
Sprite two D node and make the atlas texture resource
local to this scene. Now every instance of
this enemy will have its own atlas texture and we'll be able to animate
independently of each other. However, if any of
your animations are changing the texture property
of the Sprite two D node, this may break the animations
which are referencing it. I'll delete this track
from my reset animation. We also don't need the
signal connection, which was copied from the
player character scene, which is used to
play Jump efforts. Running the game again, now
we can see that they are able to play their idle and
walking animations correctly. We now have an enemy which
can pace back and forth, turning around at walls, but we also need to make sure
they can't leave the room. I'll see you in
the next video. Oh
51. 7-2 Invisible Walls: Hello, friends. In this video, we'll stop our enemies from leaving the room they
were spawned in. To do that, we'll need our
enemies to have collisions with invisible walls that don't affect the
player character. We have the player
character's body on collision layer nine, masking the terrain
on layer one. In our enemies scene, we'll set the body
collision layer to something different
like layer 17. They still need to mask
the terrain layer, but we'll also have them mask a second layer that we'll
use for invisible walls. I'll use layer five. Opening the project settings, layer names Physics two D. I'll name layer 17 to be enemy and layer
five to be invisible walls. In my room scene, I'll copy
the Tile Map layer node, which is drawing the map, since this already has the
boundaries of the room. Then paste it into the contents scene and rename
it to Invisible walls. With the Tile Map
layer note selected, clicking on the
Tile set resource, we'll need to add a physics
layer to this tile set, existing on layer five
for invisible walls. From the Tile set panel, we can paint this physics
layer onto the tile. Switching to the Tile Map panel with the paint tool
and the tile selected, we can close this
room's doorways, completely sealing the
room on all sides with a physics layer
only being masked by enemies and not
the player character. We don't want these invisible walls being drawn on the map, so they shouldn't be on
visibility layer two, and we want to be able to see
them while we're testing. Let's put them on
visibility layer one. I'll edit the terrain and move my enemies so they can freely walk across
the entire room. Now, when we run the game, the enemies walk
towards the edge of the room but are turned
around by the walls. While our player character
can walk right through them. I would prefer if
these walls were outside the visible
area of the room, so I'll move them each
out one more tile. All we have to do to make the
walls completely invisible to the player is toggle off
their visible property, either in the scene tree
or in the inspector. Now we need to make this enemy actually threatening
to the player, which we've already done before. All we have to do
is add an area to D node, name it hit Box. Give it a collision
shape to D node as a child and give the collision shape Tut
node a shape resource. Set the hit box to mask
the player's Hurt Box. And it must be monitoring. Then attach the Hit box script and connect the on
area entered signal. A now when we run the game, making physical contact with an enemy causes the player
character to take damage. We'll deal with
making the combat more engaging in
the next section. For now, let's deal with
these error messages. These error messages happen when we load a room's contents. This is because we are loading the room's contents
in response to a collision event when the player character's
body enters a room. And Godot doesn't like us adding more collision areas or objects while it is already busy resolving collisions
for this current frame. So if we follow the
advice being given by the error messages and
use called deferred, this will tell the game engine to load the room's contents after the collision resolution for the current
frame is complete. Godot cycle starts with input handling,
followed by physics, which includes collisions, then processes and ends
with deferred calls. So we can use this to
perform any action we want as the last thing
before the end of any frame. Running the game again, there
are no more error messages. Our enemy will now stay in this room and can hurt
the player character. But we need to add more
enemy variety to our game. I'll see you in the next video.
52. 7-3 Ledge Detection: Hello, friends. In this video, we'll add another
enemy which can detect wedges and turn around instead
of walking off of them. We'll start by duplicating
our existing enemy. Opening the enemy's scene, we'll change the root node
name to match the enemy. Import their sprite sheet and set it as the atlas
in the inspector. All of the sprites
for the Sprite sheet are the same as the first enemy, so there's nothing to fix
in terms of animations. In my room contents scene, I'll remove the two
enemies and add in this new enemy then edit the terrain so they are in a raised terrace with a ledge on either side
of different heights. I want this enemy to stop
at the higher ledge, wait for a moment, then turn around and walk in
the other direction. But then they should walk
off of the shorter ledge. In regards to walls, they should act the same
way as the previous enemy. Since this behavior
is so similar, it would be good to
use the same script. So I'll add a timer note
as a child of the enemy. Rename it, attach the
pace script to it, and set its one shot
property to true. So this enemy should behave
exactly like the blind mouse. Now we just need
to figure out when the enemy is walking
towards a ledge, which we can do using
a new type of node, a cast two D node. We'll name this
node edge detector. Then move it a short distance to the character's right and up. The ray has a
collision mask which is already set to
layer one for terrain. Expanding the collide
with section, our terrain is a physics body, so the ray will collide with it. If using a ray cast
node to detect areas, you'll need to check on areas
here before it will work. The ray cast is represented
in the editor as an arrow, but will be invisible
while the game is running, just like our
collision area nodes. It fires a ray starting
from its position in the direction of the arrow and reports whether or
not it hits something. In this case, whether
or not it hits terrain. So if it hits terrain, the enemy will continue
walking forward, and if it doesn't the
character needs to stop. The distance to the right
of the character will determine how far ahead they
will look for the ledge. The distance up from the
ground won't have much effect as long as it's above the ground and below the height
of the character. The target position should point down in order to
detect the ground, but how far it shoots
the ray will affect what it considers to be a
ledge and what it doesn't. I'll set mine to be 64
pixels down or two tiles. This means that if
the enemy encounters a ledge that is
only one tile high, it will still walk off. But if it's two or
more tiles high, the ray will not hit terrain, and therefore, the
enemy will stop. Unlike collision areas, however, the ray cast node does not emit any signals
we can react to. So we'll have to attach our
own script to this node. I'll put it in the
enemy scripts folder. We can start by declaring
our own signal, which we will want to admit
when a ledge is detected and pass along the
direction of the ledge in relation to the
enemy as an integer. It's not quite as
simple as whether or not the ray is hitting
something, though. Similar to how we made
the Coyote timer only start on the exact frame when the character
walks off of a ledge, we only want to detect
a ledge once during the exact frame when the ray stops colliding
with terrain. So we'll need a Boolean variable for whether or not the ray hit terrain last frame and another four if it hit
terrain this frame. During the physics process, we'll set was colliding to the previous value
of is colliding, then I colliding to the current value
returned by the ray cast. If the ray was colliding last frame but is not
colliding this frame, this is the exact
moment when we have detected a ledge and we
will emit our signal. The direction of the
ledge can just be the sign of the
position of the node. Let's connect our ledge
detected signal to the pace script and call a function named
wait and turnaround. We can copy these three
lines from above, which tell the enemy to stop, changes the pacing direction
and starts the timer. Then replace them with
a call to our function. But we'll need to pass
the opposite of the walls normal X as the
direction argument to match what is being
passed by the edge detector. The ledge detector sends the
direction towards the ledge. So here we need to give the
direction towards the wall. The new pacing
direction is therefore the opposite of the
direction parameter. This will work as long as the enemy is
moving to the right, but we will need to reposition the cast node to the enemy's
left when they turn around. Fortunately, we have
already emitted a signal every time a
character changes direction. So all we have to do is connect this signal to the edge
detector to react to it. Let's rename it on enemy
changed direction. Since this gives us
their new direction, we'll just set the
nodes position to be the absolute value of its position multiplied
by negative one if direction is less than zero
or positive one otherwise. Tracing this changed
direction signal omission back to the character script. I originally wrote
this by passing the move direction variable
as the signal parameter. However, this does not work in the context that we are
using this enemy character, since the value of move
direction will not necessarily match the direction we are telling them to face. So rather than passing
move direction, it is more accurate to
pass negative one if telling the character to face left or positive one for right. Now, the ray cast dende will always be in front
of the character, whether they're
facing left or right. Let's turn on visible
collision shapes so we can see the ray cast while the
game is running and hit play. The ray cast is drawn in red when it is colliding
with something. Our enemy approaches
the first ledge. The ray cast turns blue to indicate that it isn't
hitting anything, and the enemy stops
for a short time, then turns around and
goes the other way. When approaching
the shorter ledge, the ray cast is long enough that it still
hits the terrain, so the enemy walks off and continues in
the same direction. They eventually hit the
invisible wall on the far edges of the room and turn around
as the previous enemy does. We now have a second enemy type which behaves differently
from the first, but we need much more variety
to make our game fun. I'll see you in the next video.
53. 7-4 Flying: Hello, friends. In this video, we'll add a flying enemy
which bounces around a room. Let's start by importing a new sprite sheet
for the flying enemy. Then duplicating one of
the existing enemy scenes. After opening the
new enemy scene, we'll rename the root node to match and swap out the atlas with the
proper sprite sheet. These sprites are
32 pixels square, unlike the other enemies so far. So I'll adjust the Y position of the Sprite now to
lower it slightly. This enemy doesn't have separate idle and
walking animations, so I'll remove the
walk animation and edit the idle animation to have the correct
number of frames. Then edit the keyed values for the idle animation to be
the correct pixel height. Now the idle animation
looks correct. Turning on the visibility of the character body's
collision shape, I'll rotate it 90 degrees so
the capsule is horizontal, which is a closer
match to the shape of this enemy and adjust its
radius and height as needed. Then do the same thing
with the Hurt Box. And the contact damage hit box, making sure that it fits the
size and shape of the enemy, so any damage inflicted on
the player will feel fair. As for the Animation
Tree state machine, since there's only one
animation right now, it only needs to have an
idle state transitioning from start to idle. In regards to the script attached to this
enemy's root node, we don't need this
enemy to walk or jump, but we still need this enemy to be able to face
left or right, have acceleration
and deceleration, be affected by gravity and
terminal velocity, et cetera. So there's enough
benefit to still use the same character script here rather than starting
from scratch. But there's enough difference in the behavior of
an enemy that can fly that we should write a new subclass which
inherits from character. So let's create a new
script named flying Enemy, inheriting from character, and save it in the enemy
scripts folder. Our flying enemy will
need a direction to fly. Unlike walking or running, which is represented as a float, since it only needs to
worry about the X axis, a flying direction can be
any direction in to D space, so we'll store it
in a vector two. We'll also need a
boolean variable to know whether or not this
enemy is currently flying. We can then override
the physics process to include a new check to see
if the enemy is flying, and if so, call a flying
physics function. If the enemy is not flying, then we can call the
super class definition of the physics
process as normal. You may also want to export
a flying speed as a float if you want to have
an enemy that has different walking
and flying speeds. I'll give it a default value
of 100 pixels per second. I'll hold Control or command and click on
this function call to the superclass physics
process to go directly to its declaration so we can take a look at
what it contains. Note that the move and
slide function call is inside the superclass definition
of our physics process. This function must
be called to apply the character body
to Dnodes velocity and check for collisions. We also still want
our flying enemy to face the direction
they're flying. So we'll need to do that in
the flying physics function. I'll copy this block
of code and return to the flying enemy script and paste it into my flying physics. Rather than checking the
value of move direction, we'll need to check the value
of fly direction dot X. We'll also need to
still call the move and slide function if
the enemy is flying. We can then apply similar
logic we used for air control on our
non flying characters to make our flying enemy fly. If flying direction has a value, we will move the
enemy's velocity toward fly direction
multiplied by fly speed at a rate of acceleration multiplied by air control
multiplied by Delta. Or if no flying
direction is given, we can move their velocity
toward vector two dot zero at a rate of deceleration multiplied by airbrakes
multiplied by Delta. O. When we were editing
only Velocity dot X, which is a float, a
primitive data type, we had to call Godot move toward function in
order to edit it. But since a vector
two is a class, it can have its own functions. Holding control or command and clicking on the class
name brings us to the documentation
for that class where we can view all
of its properties and functions if we need to. So here we have the option of calling velocity
dot move toward, which differs from
Godot function move toward in that the
first parameter is already known as
the vector itself. You may also want to
export different values for flying acceleration,
deceleration, air control, and air brakes
for your flying enemies if you want them to
use different values while they are not flying. We'll also need a
public function that tells this enemy to fly, accepting a vector two parameter for the direction to fly. And another for when
to stop flying. This will control the is flying variable and set the fly
direction at the same time. Remember to attach this
flying enemy script to the root node of the
enemy in its own scene. In the room contents scene, just like our existing enemies have a separate node
telling them how to behave, we will do the same thing
with our new flying enemy. I'll delete this
enemy from the scene and add two of these
new flying enemies. Then I'll edit the terrain
to enclose one of them inside of a box of terrain so we can see how it
behaves a little easier. I want these bats to fly in a diagonal direction and
bounce off of any wall, ceiling, or floor they
come in contact with. So attaching a basic
node to both of them, I'll name it ricochet. Then attach a script to this node and save it in
the enemy scripts folder. This script will need
an exported variable for the initial fly direction. I'll use vector 21, which is down into the right. We'll also need a
reference to the enemy whose behavior we are
controlling set to this nodes, parent using the on ready tag. Overriding the ready function, it would be a good idea to
normalize the fly direction. Normalizing a vector will make sure that it has a
length of exactly one. Since a vector with a value
of 11 is longer than one, this would result in the bat flying faster than
its flying speed. But if we normalize the vector, we are only using the vector's
direction, not its length. Overriding the process function, we can tell the enemy to fly in the desired
fly direction. But before we do that, we'll need to check if this enemy is currently in contact with
any wall, ceiling or floor. We can then set the
fly direction dot X or dot and Y to the appropriate
value in any of these cases. For the ceiling,
the value will be the absolute value of
fly direction dot Y, making it always
positive, which is down. And the floor is the opposite, multiplying the absolute value by negative one, which is up. For walls, we can simply
multiply the absolute value of fly direction dot X by the
sign of the wall normal dot X, which points away from the wall. I'll attach the ricochet
script to the other enemy, too, then hit play. When we run the game, we can see that the bat bounces around, ricocheting off of
every wall, ceiling, and floor it touches, creating an interesting obstacle
for the player to avoid. We now have a flying
enemy to fight, but we should at least have one more enemy type for our
players to engage with. I'll see you in the
next video. And
54. 7-5 Crawl: Hello, friends. In this video, we'll make an enemy that can
crawl along any surface, including walls and ceilings. Let's start by importing a
sprite sheet for this enemy. Then duplicate the flying enemy. After opening the
new enemy scene, I'll rename the root node. Then swap out the atlas of the Sprite two D
nodes atlas texture for the appropriate
Sprite sheet. In the enemy's Animation Player, I'll update the idle
animation to have the correct number
of frames that are provided by
the Sprite sheet. Updating the keyed
region values as needed. Since we want this
enemy to crawl on not just the floor but also
the walls and ceiling, it would be helpful if
we can rotate the enemy. With the enemy's
origin at its feet, the way we've made
every other enemy, rotating them will pivot
everything about this point. To make things a little easier, it would be better to make the origin point of crawling enemies be in their center to make
these rotations look smoother. So I'll center the
sprite and all of the collision shapes to be
centered on the origin point. Now when we rotate this enemy, it will remain in the
same relative position. I'll quickly adjust
the Hurt Box and hit box to better fit
the shape of this enemy. For the character
body's collision shape, however, I'll use a circle
rather than a capsule. Since this enemy can rotate, the collision shape being a circle means that
rotations will not result in any changes in what this enemy
is colliding with. If you're using strict
90 degree rotations, a square collision
shape would also work, but would require
some extra steps when it comes to convex corners. So I'll stick to using a circle. In the room contents scene, I'll remove the bats and
replace them with two snails. The goal is to have these
snails crawl around both the inside and outside
surface of this box. Just like we created
a new script inheriting from character
for flying enemies, we'll write another for crawling enemies and save it in
the enemy scripts folder. Then replace the script attached to the enemy
with this new script. I'll slow down my
snail by setting its walk speed to only
32 pixels per second. First, we need to know the direction towards
the surface that this enemy is currently
crawling on as a vector two. Then also the forward crawling direction as another vector two. We'll also need to handle our own collisions here
rather than relying on the character body's
move and slide function to do all of the physics for us. So we can store collision
data in another variable of type kinematic collision two D. Overriding the
physics process, we'll check if this enemy is currently stuck to a surface, and if so, call the physics
process for crawling. If they are not
stuck to a surface, we can call the superclass
physics process. This works because a vector two with its default values of zero, zero will be treated
by the if statement as false and any
other value as true. If they aren't
stuck to a surface, we'll also want to check if this enemy can
stick to a surface. Calling a slightly different function than move and slide, we'll call move and collide. This function accepts the motion of the character as
the first parameter, which is their velocity
multiplied by Delta. The function returns a
kinematic collision two D, which we can assign
to our variable. Much the same way our vector two can be used in
an if statement, so can this collision data. If no collision occurred, it will be considered as false. And if a collision did occur, it will be considered true. If this enemy did
collide with something, then we'll assume
they want to stick to it and set the value
of stuck to surface. We can access the collisions normal by calling the
get normal function. This is a vector pointing
away from the surface that the character collided with
at the point of collision. Since we want the value of this variable to be
toward the surface, not away from it, we
can simply rotate it by 180 degrees calling
the rotated function. But rotations in Godot
don't use degrees. They use radians, and 180 degrees is the
same as Pi radians. Oh If the enemy didn't
collide with any surface, then we will call the
superclass physics process. But the superclass
physics process is also applying the
character's velocity when it calls move and slide. Following this code, the
character's velocity is being applied twice every frame as long as they are not
touching any surface. We can fix this by passing a second optional parameter to the move and
collide function, which is a Boolean named test. If true, this function
will only check for collisions but will not
actually move the character. Now that our enemy will automatically stick
to any surface, we can get into the physics of crawling along that surface. First, we'll apply gravity toward the surface that
the enemy is stuck to, since we didn't do it for the test for collisions earlier, and this will also help keep the enemy on the surface
they are crawling along. Calling move and collide, passing the direction
toward the surface, multiplied by gravity,
multiplied by Delta, our collision data will now hold the collision
with the surface. We can then branch based on whether or not a
collision happened. In case the angle of
the surface changed, we can update the direction
toward the crawling surface, then calculate the
forward direction, which is just the
direction toward the surface rotated 90 degrees. If Pi is 180 degrees, then Pi divided by
two is 90 degrees. However, the direction
of rotation will depend on whether the enemy
is facing left or right. If they are facing left, then we need to
rotate clockwise. If they are facing
right, we rotate counterclockwise to get their forward facing
direction as a vector two. And if these values have changed since the
previous frame, we'll also need to update the rotation of the enemy
to draw them the right way. Since the enemy is facing to the right in their scene
with a rotation of zero, all we have to do is use the forward direction then
call the angle function. The angle function
returns the angle between any vector two and
the positive X axis. But since we're using
negative X scaling to flip the character
when they face left, we'll also have to correct
for this in the rotation, rotating another 180 degrees or Pi radians if the
enemy is facing left. Now that we have the enemy
stuck to the surface, and we know which direction
they want to crawl, then if the enemy is
being told to move, we can finally use the
move and collide function again to move them in
their forward direction, multiplied by movement
speed, multiplied by Delta. If there is any collision as
a result of this movement, that means they have collided
with a concave corner. All we have to do here is recalculate the direction
toward the new surface. The next physics frame
will handle the rest. If there was no collision, then the enemy is
smoothly crawling along a flat or convex surface. If there was no move
direction given, they will remain still. Going all the way back to
the first if statement, if the gravity toward the surface did not
result in a collision, that means the enemy has somehow lost contact with the surface
they were crawling on. So we should call a
function which will reset them back to a
normal orientation. I'll name it unstick and make it public so
other scripts can call it. This function will reset the direction to
the surface back to vector 20 and the
rotation to zero, returning them to a state of behavior that is just
like any other enemy. There's still one thing missing from our crawling
physics, though, which is telling the enemy to face the direction
they are moving, since that was being done in the superclass physics process. Rather than copying and
pasting the same code here, let's move this into a private function in
the character class. Then we can call this function in both this character script, and also the crawling
enemy script. Rather than creating
a separate node and script to just tell
this enemy to crawl, I'll export an integer variable
for the Krall direction. Then override the process
function and do it here. For testing purposes, I'll also override the
input function to have the down arrow force them to unstick from whatever
surface they're crawling on. When we run the game, the
snail crawls around this box, turning at both the concave and convex corners to climb on the walls and again to
crawl on the ceiling. Pressing the down arrow
makes them unstick from the surface and fall to gravity
as any other enemy would. We now have our
final enemy type, one that can crawl on
walls and ceilings. But our enemies should be more aggressive towards
the player character. I'll see you in the next video.
55. 7-6 Sight: Hello, friends. In this video, we'll give some of our
enemies the ability to perceive and react to the presence of the
player character. In the room contents scene, I'll remove the snails
and the box of terrain. Replace it with a
block on two sides, then put a mouse note inside that will pace back and
forth between them. I'll add the timer node, attach the pace script and set its one shot
property to true. In order for this enemy to act aggressively toward
the player character, they need a way to detect them. We'll need to use multiple
tools to achieve this. First, we'll add an area
two D node to our enemy. And a collision shape
node for the area. This area will represent the entire range of the
enemy's field of view. You may want to use
a square rotated 45 degrees a capsule or a circle. If you use a collision polygon two D node instead of a
collision shape two D node, you can actually create
a custom polygon rather than using preset shapes. Simply click in the editor to draw each vertex of
the shape you want. This allows us to make
a triangle or a fan, which is a better representation
of a field of view. Expanding the polygon
property in the inspector, we can see the positions
of each vertex, edit and rearrange
them if necessary. Whatever shape you
choose to use, make sure your shape
has only convex angles. If your polygon is concave, Godot will divide it into multiple smaller
convex shapes and double the cost of collision
calculations as a result. If you do decide to use a normal symmetrical
collision shape, however, flipping the enemy left and right is much easier, since you can just
use the same method we used with the
edge detector cast, setting its position property anytime the enemy
changes direction. With the collision
polygon two D node, however, the process is a
little more complicated. We can't just move its position, and we shouldn't change
its scale property either. If we open the documentation
for this node, it specifically warns against this stating that it will
likely not behave as expected. If we do decide to use these collision polygons for non symmetrical shapes
that we need to flip, we'll have to manually edit the positions of each of
the vertices in our script. The field of view will not
be sufficient on its own, though, since there is nothing stopping it from
going through walls. If we want the enemy to detect the player with an
unobstructed line of sight, we'll need another node to check that a ray cast two D node. You'll want to
position this up near the enemy's eyes where they would actually be
observing from, but keep it centered
horizontally. I'll rename this
to line of sight. The line of sight will
be colliding with both the terrain and the
player character's body, both of which are bodies. This will allow the terrain
to block their line of sight, even if the player character is within their field of view. With the field of view selected, we'll only need this
to be monitoring, masking the player's body. I'll remove the shape node and just use the polygon
as my collision area. Then give both it and
the cast unique names. Attaching a script to the
field of view area to dende we'll save it in
the enemy scripts folder. Let's grab references
to the shape or polygon node and the line of sight node using the ready tag. What I would like from this
setup is for the enemy to react to the player
character after they have both entered
the field of view and the enemy has a direct
line of sight to the player. If the player
character both exits the field of view and the
line of sight is broken, then I would like the enemy to return to their normal behavior. Consider how we
want this node to communicate with other
nodes through signals. We'll need this node
to report two things when the enemy has acquired a target and when they
have lost their target. These will be the exact moments when we want to change
their behavior from their default pacing back
and forth and instead tell them to start acting more
aggressively or vice versa. We can connect the
body entered and body exited signals from the
field of view to the script. And this quite easily tells us when the
player character enters and exits the field of view. We'll need to store this in
a variable named target. Since the parameter
is a no two D, we'll also set the variable type as a no two D to
keep things simple. We also need a
boolean variable for whether or not the enemy
is currently targeting, whether the target is
currently within the field of view and whether the enemy currently has line of
sight to the target. When a body enters
the field of view, then set is in field of view to true and set target to be body. When the body leaves
the field of view, we can set it back to false. Remember that we are only
masking the player character, so we already know that this body is the
player character. Overriding the physics process, if there is no target yet, we can simply return since
there's nothing to do. Now we can assume that
the player character has entered the field
of view at least once. Next, we'll point the line
of sight at the target. We can create a vector
pointing from A to B by subtracting A from B. So the target position
of the raycast will be the target's global position
minus our global position. Then the value of is in line
of sight is going to be true if what the collider is hitting is the
same as the target. If the ray cast hits
terrain or doesn't hit anything at all,
this will be false. Note that when the
physics process happens, collisions have already
been calculated by Godot. So when we point the line of sight at the player
character in the line above, the collision data that results will not be available
until next frame. What we are checking here are the collision results
from the previous frame. If for some reason
you absolutely need a cast to
update immediately, you can call its force
RycastUdate function. But with the game running
at 60 frames per second, the delay will not be noticeable and this will perform
just fine as it is. Now all we have to do is emit our signals at the
correct times. If the enemy is not
currently targeting, but the target is in
their field of view, and they have line of
sight to the target, then the target
has been acquired. So we'll set is targeting to true and emit the
target acquired signal, passing the target
as an argument. And in the complete
opposite scenario, if they are currently targeting, but the target is not
in their field of view and they do not
have line of sight, then we have lost the target. In this case, it would
also be a good idea to set the value of
target back to Null, which will stop the physics
process from checking anything further until they
re enter the field of view. Let's also print out when these events happen so
we can test them out. The last thing we
need to do is flip our collision polygon when
the enemy changes direction, which we can do by connecting the changed direction
signal to our script. If you're using a
symmetrical shape, all you need to do is reposition it to the other side
of the character. But I'm using a polygon, which means I'll need to edit
the individual vertices. I'll start by getting
the existing array from the polygon and
storing it in a variable. The type is a packed
vector two array. I can then iterate through
each of the vertices and use the same strategy to
flip their exposition to the other side of the character if they
are facing left. The array of
vertices provided by the polygon here is just a copy, not the actual array itself. So when we edit its values, it doesn't actually affect
the collision polygon. To make the changes
actually work, we need to overwrite the existing array
with the edited copy. Let's turn on visible
collision shapes from the debug menu
and try it out. We can see our enemy pacing
back and forth between the two walls and the collision polygon flips
when they turn around. Entering the field of view, the ray cast node starts
tracking our position, but the enemy has
not seen yet since their line of sight is
blocked by the terrain. If we jump up, the line
of sight is no longer obstructed and we get the
target acquired message output. Leaving the enemy's field of view and also their
line of sight, the target lost
message pops up and the cast is no longer
tracking our position. We now have our enemy able to detect the player
character's presence, but they still need
to react to it. I'll see you in the next video.
56. 7-7 Aggression: Hello, friends. In this video, we'll have the enemy chase
and attack the player. Starting in the enemy's scene, we'll need to add an
attack for this enemy. Duplicating the idle animation, I'll quickly keyframe
the proper sprites for the enemy's
attack animation. This attack animation
doesn't really communicate how much of an area the
attack is going to hit. It only shows the
enemy's movement itself. We'll need a separate
sprite for that. I'll make a new
Sprite folder for effect sprites and import a sprite sheet for this
enemy's attack effect. We'll add an Animated Sprite to denode as a child of
the enemy's Sprite, so it will automatically flip horizontally when the
enemy turns around. Then create a new Sprite frames resource and add the sprites
from the Sprite sheet. As always, I'll add a blank sprite to the
end of the animation, set its frame rate to 12 frames per second and not looping. I'll put this animation
on its first sprite, which accurately shows the
size and shape of the attack. Then position it in
front of the enemy. This makes it much easier to add a hit box to this attack. So let's duplicate
the existing hit box. I'll rename the
original hit box to contact damage and leave
this new one as hit box. Now we can make this hit box as collision shape match
the attack effect. Unlike the Animated Sprite, we shouldn't use the
negative scaling trick to flip our hit box. So we'll need to connect the enemy's changed
direction signal to the hit boox script and
manually reposition it. As we've done before, we'll just grab a reference to
the collision shape. And set its position to its
absolute value multiplied by negative one if direction is less than zero or
positive one otherwise. In this enemy's default state, it will not be attacking, so this hit box
will not be active. So let's turn off its monitoring
property and visibility. Then switch back to
the attack animation where we can key frame these properties to
turn them on during the attack animation
and back off again. This will also add
both of them to the reset track with
their default values. And we can also tell the attack effect to
play at the same time. In the room contents scene, we currently have this enemy's
normal behavior pacing back and forth and a way for the enemy to see
the player character. When this node emits the
target acquired signal, we will want to change the enemy's behavior from
pacing to be more aggressive. And when they lose
track of their target, they should resume
their original pacing. But we may not necessarily
want this to apply to all enemies or other characters that are using the pace script. So we should create
a separate node for aggressive behavior. I'll use another timer node
and name it aggression. Then attach a
script to this node and save it in the
enemy scripts folder. Just like with the pace script, we'll need a reference to
the enemy that this script is controlling using
the ready tag. We'll also need to store a
reference to the target in a variable that this enemy is
acting aggressive towards. And we'll also need to know the direction towards the
target as a vector two, as well as the distance
to the target as a float. Knowing these values
will allow the enemy to approach their target to get into attack range
before attacking. But we'll also need to specify this enemy's attack range as an exported variable,
which would be a float. If you want, you
can also specify a minimum attack range too. If we change the type of the attack range
to a vector two, we can use the X value as the minimum range and
the Y as the maximum. I'll set its default value
to be at least 32 pixels away from the player and up
to a maximum of 64 pixels. So if the enemy is more than 64 pixels away from the
player character, I want the enemy to
run toward them. Or if the enemy is less than 32 pixels away from the
player character, I want the enemy to
run away from them. But within that range,
the enemy will attack. Overriding the process function, we can ignore Delta, and if there is no target for
this enemy, we can return. This shouldn't happen, but it's never a bad idea to
have a failsafe. We can calculate
the direction to the target by subtracting
our enemy's position from the target's position
and then get the distance by taking that same vector and
calculating its length. If the distance to the target is greater than our
maximum attack range, then this enemy should move
toward the target setting its move direction to the sign of direction to target dot X. If the distance to the target is less than our minimum
attack range, then this enemy should
move away from the target, doing the exact opposite. If both of these
conditions were false, then the target is
within attack range. So the enemy should
prepare to attack. I'll make sure the enemy stops moving by setting its
move direction to zero and also face towards the target just in
case they aren't already. I don't want the enemy
to attack right away, since my attack animation is very short, it
isn't telegraphed. So I'll start the timer here and wait for it to time
out before attacking. The one shot property
of this timer should be set to true to
only count down once. The wait time property
will be the time that the enemy remains within the attack range
before attacking. Connecting the timeout
signal to the script, here is where I'll tell
my enemy to attack. It's also a good idea to
face toward the target again in case something has changed since the timer was started. I only want to start the timer if it is currently stopped. I'll also stop the timer anytime the target is
outside of attack range. This should be sufficient
for our behavior. Now we just need to switch
back and forth between our two behavior scripts
at the appropriate times. We don't want our aggression script active when the
enemy first spawns, so we'll set its process
mode to disabled. Selecting the field of
view in the scene tree, we'll connect the target acquired signal to the
aggression script, and we can rename this
function if we want to. To keep things consistent between my different
behavior scripts, I'll name it zoom. This function will set
the value of target, change the process
mode to inherit, and also tell the enemy to run, which will make
them start acting aggressively towards the target. But we also need to
pause the pacing script. So we'll also connect the
same signal to the pace node. And this time call a
function named pause, but this signal
provides an argument, and we don't need it in order
to pause this behavior. We can also imagine a scenario where we would want to
pause this behavior under other circumstances
and wouldn't want to provide an arbitrary and
unnecessary argument to call the pause function. So we will unbind the argument
provided by the signal, so the connected function
has no arguments. Here, all we need to do is set the process mode to disabled. When the enemy sees a target, they will pause their
pacing behavior and resume their
aggressive behavior. So when they lose their target, we can do the opposite, connecting the signal to the scripts and calling
the opposite functions. Setting their process mode
to inherit to activate them or disabled to
deactivate them. When activating the
pacing behavior, we will also want to
tell the enemy to walk. Now in the character script, we'll need to trigger
the attack animation when we tell the
enemy to attack. To do that, we'll
need a reference to the Animation Tree
state machine playback. But this will cause an error in the player character
script if we have another variable
of the same name, since the player character
inherits from character. So there would be an
animation variable inherited from character, but also another one declared
in player character, which is not allowed, but we're using it for
the same purpose, so we can actually just remove this one from the player
character script. Back in the character
script, when told to attack, we just need to
tell this playback to travel to the
attack animation. In the Animation Tree, we'll add the attack animation
and transition to it from Idle with Advance Enabled and return to idle at the
end of the animation. If you don't want
the enemy to be able to move or change
direction while attacking, then we can add quick
checks to see if the animation playback's
current node is attack. And either set move direction to zero or return out of
the function entirely. Now, when we run the game, once the enemy has us in both their field of view
and their line of sight, their behavior switches from pacing to being more aggressive. They will chase after us and
attack after a brief pause. We now have a much
more aggressive enemy for our player character
to contend with, but our player character
can't really fight back. I'll see you in
the next section.
57. 8-1 Combo: Hello, friends. In this video, we'll give the player
character attacks of their own so they can fight
back against our enemies. We've already given the
mouse Knight an attack, and the process will
be very similar. However, our player character
has two different attacks. We can copy some
of the nodes from the mouse night scene
to save some time, the hit box and the
Animated Sprite two D node. Switching over to
the character scene, we can paste these
nodes into the scene, then reorganize them as needed. The Animated Sprite two D node can be sorted under the
character's Sprite, so it will automatically flip to match which direction
the character is facing. I'll rename it to attack effect. I know which specific
animated sprite this is. The hit box, I'll generally keep sorted next to the
Hurt Box as a sibling. For the player
characters hit box to deal damage to the
enemy's Hurt Box, we'll need to make sure that the collision layers and
masks are set accordingly. Since my player character
is on collision layer nine and their Hurt Box is
on collision layer ten, I'll then have my enemies
on collision layer 17 and their Hurt boxes will
be on collision layer 18. Opening the project settings, I'll add the enemy Hurt Box to my two D physics layer
names as layer 18. Then remember to edit
all of my existing enemy scenes to put their Hurt
boxes on this layer too. The player characters hit
box can then be set to mask layer 18 in order
to cause damage to them. In order to flip the hit box, we'll need to reconnect
the signal in this scene, since that signal
connection will not have copied over
from the other scene. But the function we wrote before in the Hit box
script that we want to connect this signal to is named on enemy
changed direction. This won't matter,
but if you want to be consistent with how you
name things in your game, you may want to change
this function name to on character
changed direction. This will add another
function to the script, and we can easily just move the code to this new function
and delete the old one, since it will just
do the same thing. However, we now have to
go into the enemies scene and edit their signal connection to connect to the new function. The earlier in your project you can identify these problems, the less time and
effort you will need to spend on
refactoring later. Back in the player
character scene, we'll select the Animation
Player node from the scene hierarchy and
add a new animation. We'll name this attack
one since there are two different attack animations and we'll need to
differentiate them. After adding the
character's Sprite frames. I'll copy the keyframes to the other tracks for the
character's clothing. We can also turn on the hit
box's monitoring property. And the visibility, too, if you like to see it turning
on and off in debug mode. Taking a look at
the attack effect, this doesn't match
the attack animation. So I'll import the proper
effect sprites for this attack. Then add a new animation to
the Sprite frames resource. I'll name it attack one, turn off looping,
set the frame rate, and add an empty
frame at the end. Now the attack animation
can tell the attack effect, Animated Sprite two D node to
play the attack one effect. The keyframed function
call is just play, which has been working
fine as long as our Animated Sprite two D node only has one animation to play. But now we have two
different animations, and we need to specify
which one we want to play. Clicking on the keyframe, we can edit it in the inspector. Expanding the arguments section, we can see that
the play function actually has three arguments, all of which are optional. We are only concerned
with the first argument, the name of the animation, which we can type
into the text field. If you're curious, the other
arguments are for adjusting the animation speed or playing
the animation backwards. The spelling must match the name of the
animation exactly. I'll rename the
default animation to attack two to match what
I put in the animation. Duplicating the
attack one animation, we can also make an
attack two animation. After changing the
character's sprite frames, I'll copy the keyframes
to the other tracks. Then change the argument
being passed to the attack effect to play attack two instead
of attack one. I selecting the Animation Tree node, we can add the attack one
and attack two animations. Then add a transition from
movement to attack one, then from attack
one to attack two. Both of these will have their advanced mode set to enabled, meaning we will trigger these transitions from our script. Adding transitions,
leaving either of the attack animations
returning back to movement, these will have their
switch modes set to at end. The transition from
attack one to attack two will also have its switch
mode set to at end. This will allow us
to transition to attack one if the attack
button is pressed, and then from attack
one to attack two if the attack button
was pressed a second time during the first
attack animation. To add an attack
button to our game, we'll need to go into
the project settings, the input Map tab and add a new input
event for attacking. I'll use the X key
on the keyboard and the left face button on my
controller, which is also X. Then in the player script, we can add another I
block for attacks, buffering the input the same way we have with
every other action. Since the button to trigger the first attack and the
second attack is the same, the players script
should only be concerned with telling
the character to attack, not which specific
attack to perform. In the player character script, we can add this attack function, returning a boolean just like
any other buffered input. This won't be locked and
doesn't have any cost, unlike the cast function. If the character's
animation playback is currently in the
movement state, then I'll allow them to attack. If successful, then we can tell the animation
playback to travel to the attack one
animation and return true or return false if the character was busy
doing something else. There's an error because
the attack function is overriding the attack function
of the character class, but the signature
does not match. This means that either
the parameters or the return type are different,
which is not allowed. Holding control or command and clicking on the
function brings us to the superclass definition
of this function where we can see that it
returns void, not a boolean. In order to use this
inheritance structure with our enemies and
our player character, while still allowing
buffered inputs, we'll need to have the
superclass attack function also return a boolean. We'll just return true to
satisfy this requirement, and returning to the
player character script, the error is solved. But what about the
second attack? We can add an else if
block and check if the animation playback is currently in the
attack one animation. Then tell it to travel to the attack two
animation instead. Let's turn on visible collision
shapes in the Debug menu, then hit Play and try it out. Pressing the attack button, our character plays the
attack One Animation. The hit box is activated, and the effect animation plays. Pressing the attack
button again during the first attack also
plays the second attack. Switching back to the editor, we can open the remote tree to see our current game running. Exploring the scene tree until
we find the mouse night, we can select its Hurt Box and view its properties
in the inspector, including their current health. Back in the game, we can
attack our enemy and watch as each attack hits them for one health each time
until they reach zero. Come on. We now have our character able
to attack enemies, but our characters and enemies
don't react to being hit. I'll see you in the next video.
58. 8-2 Hit: Hello, friends. In this video, we'll make our characters
react to being hit by attacks. Starting in the player
character scene, selecting the
Animation Player node, we'll need to add a new
animation for being hit. To make things easier, I'll duplicate the
dash animation to create the hit animation. After updating the
Sprite keyframes, I'll copy and paste them into the other tracks to also animate
the character's clothes. Because I duplicated
the dash animation, this hit animation now also includes
invincibility frames. This is very helpful for
preventing characters from taking damage from multiple hit boxes in rapid succession, which often feels unfair
to the player and also causes the character
to repeatedly take damage from
persistent hit boxes, like environmental
hazards, for example, because a new collision
will be triggered when the Hurt Box is reactivated
at the end of the animation, if the Hurt Box is still
overlapping with the hit box. We also have the
method track calling the end dash function,
which if you recall, from the way it was implemented
in the earlier section, must be called if the
dash was interrupted. It would be a good idea to move this function call to the
start of the hit animation. Let's take a look
at this function in the player character script. Depending on which frames you turn the Hurt Box on or off, it may be possible for the
character to get hit while the dash animation is playing and therefore interrupt
the dash animation. Meaning that the end dash
function would never be called, the character's gravity
will never be reset, and the dash Cooldown
never started. Obviously, this is problematic, but we can easily fix it. All we really need
to do is track when the character is dashing
with a Boolean variable. When the dash starts,
we'll set it to true. Then when the end dash
function is called, we'll check if the character
is dashing first and if so, do all of the same things. Then set is dashing
to false after. Now we can call this
function anytime we need to regardless of whether or
not the character is dashing. If the character isn't dashing, the function just
won't do anything. Selecting the
Animation Tree node and taking a look at
the state machine, I would like the hit
animation to be able to interrupt any of these
existing action animations, as well as any jumping or
movement animations, too. Therefore, it would
be a good idea to use another state machine. So I'll save this state machine like I did with
the previous ones. I'll name it actions and save it in my character's folder
along with the others. Then replace this
state machine with a new one and load the actions
state machine as a node. Okay. You can then add the hit animation to
this state machine. Adding a transition from start to actions makes it
the default state. Then a transition
from actions to hit with its advanced
mode set to enabled, and another from hit back to actions with its switch
mode set to at end. This allows us to trigger the animation from
any existing state, play at once, then return
back to our other states. In the character script, since I want this to
apply to all characters, not just my player character, we'll need to make
a new variable to hold the playback for
this new state machine. We can duplicate the
existing variable and rename it to something
like Hurt animations. The path to this
playback property is parameters Playback, since it is the root
of the Animation Tree. If we select the
Animation Tree in the scene tree and expand
its parameters section, we can see the playbacks for
each of our state machines. So the action state machine
playback property path has changed, and we
need to update it. To better clarify the use of this playback in relation
to the Hurt animations, I'll name this playback
action animations. Then use the replace
feature to change all instances of this
variable with its new name. Since the player character
script inherits this variable, I'll also need to
replace all instances of the old name
with the new name in that script, as well. This will work for
the player character, but in order for it to
work for the enemies, we will also need to update their Animation
Tree state machines to match this new format. I'll only update my mouse night since that enemy is
in my starting room, and the process will be
the same for every enemy. I'll make the hit animation
in the Animation Player node. Then save the Animation
Tree state machine as a project resource. Replace it with a
new state machine. Add the old state
machine as a node, add the hit animation, add the transitions with the same advance
and switch modes. Rename it actions to match what the script
will be looking for. Now this enemy has the
same animation structure as the player character, so the scripts will
work the same, having two separate
playbacks for playing the hit animation and
another for actions. Once the state
machine is set up, the saved state machine
can be deleted. This will make the state
machine localized to the scene rather than
a shared resource between multiple scenes. Now that we have access to
our state machine playback, we need to trigger
the animation. We'll start in the
Hurt Box script. We have a signal that is emitted when the
character's health changes, which we are using to
update the HUD element. But this signal is
also emitted when we initialize the character's
health at the start, as well as anytime the
character would recover health. So it's not the best signal
to use for receiving damage. We'll declare a new
signal for that. If you want to have
the character react to different amounts
or types of damage, you may want to give
this signal parameters. I'll add a parameter
for the direction that the damage is coming
from, as a factor two. But we can't get that
information from here. We only have access
to the Hurt Box and not the hit box
that caused the damage. So switching over to
the Hit box script, here we have access to both
the hit box and the Hurt Box. So we'll add this as another argument passed through
the take damage function. We can calculate the
direction from A to B by subtracting A from B, then normalize the result. Normalizing we'll ignore
the distance between A and B by setting the
length of the vector to one. Back in the Hurt Box script, we'll add the
direction parameter. When our Herd box
receives damage, we'll emit the signal and pass along the
direction with it. Our character script can
now react to this signal. I'll rename the
connected function to on damage received
for simplicity. Okay. We'll first tell the Hurt animation playback
to travel to the hit state. Then also add a force to the character body in the
direction of the damage. Since the direction
is normalized, it will have a
length of one pixel. We can multiply this
by any number we want to alter how impactful the
knockbck force will feel. You may even want
to export this as a new variable so it can be tweaked for each
individual character. We'll also need to make this signal connection in
all of our character scenes. Et's try it out.
My character can now attack the enemy and the
enemy reacts to being hit by both playing and animation and a knock back force
is also applied. The same also happens to my character when the
enemy attacks me. We now have our characters
reacting to receiving damage, but our combat doesn't
feel very juicy. I'll see you in the next video.
59. 8-3 Juice: Hello, friends. In this video, we'll add some
particle effects to our combat to make it
feel more impactful. Starting in our player
character scene, we'll add a new type
of node to our scene, a particle emitter node. Godot has two different
types, CPU and GPU. The CPU particle node will render particles
the same way we are drawing our characters sprites using the device's
central processor, which is less efficient but more compatible with older devices. While a GPU particle
node will delegate drawing these particles entirely to the device's graphics card, which is more efficient but
only on modern devices. The GPU particle node can handle much higher numbers of particles without sacrificing performance. I would only recommend
using CPU particles if you're specifically targeting phone or tablet for your game. Otherwise, you should
use GPU particles. Adding a GPU Particles two D
node to our character scene, I'll just name it particles, move it up to be
in the center of the character and take a look at its properties
in the inspector. Before a particle emitter
will draw anything, it needs a process material. This is a resource that tells
the emitter how to spawn particles and how the particles should behave after
they've spawned. There are two different options
for our process material, a particle process material
or a Shadum material. The particle process
material will be created as a project resource that we can edit right here
in the inspector, while a shader
material will allow us to write a Shader script
to do the same thing. Shader scripts are
not written in GDScript like all of
our scripts so far. They are written in GD shader, which will not be
covered in this course. So we'll use a particle process material
resource instead. Click on the resource to view
and edit its properties. Now that our particle
emitter node has a process material, it spawns individual
white pixels from its origin point
that fall due to gravity. To make the particles
easier to see, I'll hide my character sprite. Before we get into each
individual property, it helps to turn
off gravity first, which is under the
acceleration section. And since these
particles are going to be used for impact
during combat, I'll set their
explosiveness to one. I have a sprite sheet I would like to use
for my particles, so I'll import that
into my effects folder, then assign it as my particle
emitters texture property. But this just draws the entire sprite sheet
for every particle. To animate a particle, we have to give it
a custom material under the properties
inherited from Canvas item. We'll make a new
Canvas item material then turn on its particles
animation property. Now we can change the number of animation frames
horizontally and vertically in the sprite sheet. The particles are now drawing
only the first frame. Expanding the animation section of the particle
process material, we can set the animation
speed for the particles. I want my particles to
have a consistent speed, so I'll set both the
minimum and maximum to one. A curve can also be
used here to alter the randomization of each
particle's animation speed. And the same values can
also be provided to give each particle its
own animation offset. Expanding the span section, we can edit the position where
the particles will spawn, their rotation angle and
their initial velocity. We can change the emission shape from a single point to be a variety of other shapes and change the size of
the shape if we want to. Point emission shapes are usually the most appropriate
for combat particles. Angle allows randomization of the rotation of the particles. To make it completely random, just set max to 360. Since I'm using pixel art, I do not want my
particles rotated. The initial velocity will make our particles feel
more explosive. The amount of force
will be randomized for each particle
between min and max, making the particles
feel less uniform. The spread will
create a fan shape. Maxing it out at 180 degrees, the direction will be
completely random. The animated velocity section contains variables
you can use to apply a variety of different constant velocities to your particles over
their lifetime. Angular will make them rotate. Directional will
continue to move them in whatever direction
they're already moving. Orbital will make them orbit
around the spawn point. Radio will move them away
from the spawn point. And the velocity limit uses a curve to limit the
velocity of each particle. The accelerations
section contains similar variables that you can use to affect each
particle's velocity gradually over its lifetime, which will make them
feel more realistic. Gravity is a constant force in one direction for all particles. Linear is the direction that the particle
is already moving. Radial is away from
the spawn point. And tangential is
similar to orbital. Damping is a useful
force that will cause particles to decelerate
over their lifetime, which is useful for explosive
or impact particles like we're currently making. Particle attractors are
not available for two D, so this setting
won't do anything. Expanding the display section, we can change the way
each particle is drawn. Scale will make the particles
randomize their own size. If we add a scale curve, this will change
how each particle changes its scale
over its lifetime. We'll go over the
curve editor soon. Scale over velocity will
instead make the scale of particles relative to their velocity rather than
their lifetime. This is particularly useful for making dust clouds
that dissipate. Color curves has multiple
different properties, starting with a base
color for all particles. Color ramp will allow us to make a gradient with each particle progressing from the
color to the left to the color on the
right over its lifetime. This color is multiplied by the base color to
produce the result. Color initial ramp
will randomize the starting color of each
particle across the gradient. The emission curve
can be used to make three D particles emit different amounts of light
over their lifetime, which isn't used in two D. The Alpha curve is
useful for only editing the Alpha of your
particles over their lifetime. Let's create a new curve
texture resource here. This curve texture resource requires another curve
resource of its own. Clicking on this opens
a curve editor we can easily use to create a
wide variety of effects. The X axis of this graph is time with zero being
when the particle is spawned and one being the end of the lifetime of the
particle. No 1 second. The Y axis is the
value we are editing, which is the particle
colors Alpha, one being opaque and
zero being invisible. We can edit the minimum and
maximum values for our axis, but we don't need to
do it in this context. Simply clicking on
the graph will add a point that we can
drag around as needed. I want my particles to start opaque and fade to invisible
over their lifetime. This is great for
making particles fade away instead of
just disappearing. Tangents are
automatically applied to the points to create a curve. Selecting any point
on the graph, we can edit the tangents to create any kind of
curve that we need. We can also manually
edit the points and tangents by expanding the
points section at the bottom. Hue variation can be used to randomize the hue of
your particle colors, which makes them
look less uniform. The rest of the
properties aren't very applicable to our tot project, but I'll give you a brief
summary of what they're for. Turbulence can be applied to simulate how air currents
would affect your particles. You can edit the
noise pattern used to calculate the turbulence
when it is turned on. Particles can be stopped
or killed by collisions. Collisions here are not the same as collisions we have been using so far because the particles are being handled by
the graphics card. They can only collide with light occluders, not
physics objects. And if your particles scale, then their collision radius
can also be scaled with them. And a sub emitter can be used to create effects
like fireworks, spawning additional
particle omissions. To make my particles match
my game's other frame rates, I'll set the particles
lifetime to be eight frames divided by
12 frames per second. Now that the particle
emitter is set up, we should set its one
shot property to true. Now it will only emit
particles in one single burst, which we can trigger
through our script. I'll turn my character
sprite back on, then open my character script. Giving the particles
node a unique name, we can store it in a variable
with the on ready tag. Then when the character
takes damage, we can tell the snow to emit particles by setting its
emitting property to true. Note that this will not work
if used in rapid succession, since the particle emitter will already be
emitting particles. So just setting emitting to true will not spawn more particles. For times like this, you'll
want to either spawn a new GPU particles node for each hit or call the
restart function instead. Restart will force the
particle emitter to stop its current emission and start over with a new
set of particles. Remember to copy this node into your enemy
scenes as needed. Et's try it out.
Attacking the enemy, they now emit particles
when they get hit. And the player character also emits particles when
they receive damage. We now have particle effects
triggered by our attacks, but there's more
we can do to make combat feel even more crunchy. I'll see you in the next video.
60. 8-4 Shake: Hello, friends. In this video, we'll add a camera
shake that can make combat feel
even more impactful. Starting in the camera script, we'll need a variable that
we can use to shake it, which will be a vector two. While there are a variety of different ways we can create
a camera shake effect, I think it makes
sense to use a tween, since it is a property change that happens over
a specific time. So let's declare a new tween
variable named shake tween. Then also declare a
new function that will be publicly accessible
that can shake the camera. First, checking if the camera is already shaking by checking if the shake tween already
exists and if so, kill it. We can then create a new tween. There's no real need
for special transition or easing types for this. We don't want to tween a property evenly from
one value to another, but rather just call a method multiple times
over a short duration. We'll need to declare the
function we'll be calling, but I'll name it set shake
as a private function. Then we need to
provide an argument to pass to the function from the beginning to the
end of the tween. This will be the intensity of the shaking effect
measured in pixels. We can accept this
as a parameter of the shake function so that any script which
wishes to trigger a camera shake can
specify the intensity. Then over the duration
of the camera shake gradually reduce the
intensity down to zero. You may optionally
want to instead make this another
parameter if you want to allow camera shakes that have a consistent or
increasing intensity. The final argument is
the duration of between, which we can also make
into another parameter. Next, we'll write the
set shake function, which accepts the
intensity as a parameter. This function will be called every frame for the
duration of the shake with the intensity gradually
changing from the value provided in the original
function call to zero. So all we have to do is set the value of shake
to a new vector two. If we start with
vector two dot right, then rotate it to a
random direction. A random rotation angle can be a random float
between zero and tau. We can then multiply the
results by intensity. If for any reason your
shake tween does not conclude with setting the
value of shake to zero, then you'll need to do that
at the end of the tween. We can connect the shake
tweens finished signal to the set shake function and
bind zero as the intensity. This will ensure that the
value of shake anytime after the camera has finished shaking will be
reset back to zero. To make the shake vector
actually move the camera, all we have to do is add it to the camera's position in
our process function. A in our games scene, we can now connect any
signals we want to trigger a camera shake to our
camera's shake function. Like, for example, when
the player character attacks an enemy or when
they receive damage. In the player character script, we can add signals
for these events. Then back in the game scene, we can connect these signals to the camera's shake function. In order to add arguments
to a signal connection, we need to expand the
advanced section. We can then select
float from the list of argument types since
the intensity parameter is a float in our script. Then click on the Add button. The shake also needs a duration, which is also a float type. So we can click the Add button again to add another float. The values we provide
for Argument one will change the intensity of the
shake measured in pixels, and Argument two will be the duration of the
shake in seconds. I'll have my player characters attack cause a camera shake with an intensity of four pixels over a
quarter of a second. But when they receive damage, I'll shake the camera
with an intensity of eight pixels over
half of a second. Switching over to the
player character scene, we need to emit these signals
at the appropriate times, starting with our Hurt boxes
damage received signal, which is already
connected to a function named on damage received
in the character script. If we override this function in the player character script, we can call the
superclass function, then emit the signal. Next, we can select
the hit box and add another signal connection
to the on area entered signal and have
it call a new function. Let's name this
on attack landed. This doesn't need
to be concerned with the other area node, so we can use the
advanced section to unbind this argument
from the signal connection. Since we unbound
the area argument, the function has no parameters. All this function needs to do is emit the signal to
cause the camera shake. No let's try it out. If I attack the mouse Knight, this triggers the hit
boxes are entered signal, which will bubble up through
the player character to the game scene and
trigger a camera shake. Letting the mouse Knight
hit the player character triggers the Hurt box's
damaged received signal, which causes a more
intense camera shake. We now have our camera
shaking in response to the player character attacking enemies and receiving damage, but our characters don't die
when they run out of health. I'll see you in
the next video. I
61. 8-5 Death: Hello, friends. In this video, we'll have our characters die when they run out of health. Starting in the
mouse night scene, selecting the
Animation Player node, I'll quickly make a
new dead animation by duplicating the
hit animation. After changing the sprite to the correct frame in
the Sprite sheet, it's a good idea to turn off
the Hurt Box so they can no longer receive damage and
not turn it back on again. The same can be done for all of the other area to Di nodes, turning off contact damage
and the hit box two, so the dead enemy can no longer deal damage to
the player character. Selecting the
Animation Tree node, we can add this dead animation
to the state machine and automatically transition to it if this character is dead. Unlike other animations, there's no need for a transition
out of the dead state. Now we just need to add this Is dead variable to the
character script. I'll put it at the top of
the script as a boolean. And also declare a
died signal two, just in case we
ever want anything to react to certain
characters dying. Then write a public function
named die that will set the value of this variable
to true and emit the signal. Anywhere in this script
where a character could perform any action that shouldn't be allowed
after they die, we can just check the value of the is dead variable first. So if the character is
told to face a direction, but they're dead, they
should ignore that request. The same way we ignore it
if they are attacking. In a similar fashion, if this character
is told to jump, but they are dead,
they should not jump. Same with attacking. And in the physics process, I'll also set their
move direction to zero if they are dead, so they will never
move while dead, but will be affected by physics. In the Hurt Box script, we already created the dyed
signal and emitted it when the character runs
out of health when we wrote the script in
the earlier section. But in order to play
a death animation and not a hit animation
when the character receives damage and
dies as a result, we need to make sure we only emit the dyed signal
at that time. So we'll move the damage received signal
into an se block, giving the dyed signal priority. All we have to do is connect the Hurt Box's dyed signal to the enemy's die function
to trigger the animation. Either in the enemy's scene or in the room contents scene, wherever you have your
enemy's AI nodes, you may also want to
connect the enemy's did signal to the free
function of these nodes. Alternatively, you can do this
in the scripts themselves. Overriding the ready function, enemy died can be
connected to ure. This will save you from
having to connect the signal manually hundreds of times
across the entire project. Let's try it out. If I attack the mouse
Knight five times, its health reaches
zero, and it dies, which plays the death animation, and it stops all actions. I can also touch it
without taking any damage. Now for the player character, I don't actually have death animation sprites
for this character. What I will do instead is
reuse the hit animation, duplicating it to create
a death animation, since I know that when
this character dies, the game scene will fade
out and reset anyway. Like the enemy,
it's a good idea to also turn off all hurt
boxes and hit boxes. The only real change I will make here is to have the
animation loop. In the Animation Tree, I'll add the dead animation, the same as before, with an automatic transition
if the character is dead. But this time, also add
a transition out of the dead state under the condition that they
are no longer dead, since the player character will be revived after they die. After connecting the Hurt Box's died signal to the
character script, I'll switch over to the
player character script. We'll need to write a
revive function that can be used to bring the player
character back to life, setting the I dead
variable back to false. We can also tell the Hurt Box to recover full health
at the same time. To add more to the player
character's death animation, I'll override the die function in the player character script. I'll start by calling
the superclass function, then do some other things specific to the
player character. Reusing the particle
effects node, I'll set its one shot
property to false, its explosiveness to zero. And it's emitting
property to true, turning the instantaneous burst of particles into
a steady stream. On top of that, I'll also tween the character's modulation color to have an increased intensity. After creating a new tween, I'll tween this node's
modulation color to a new color with RGB
values of 255 each. Then in the revive function, I'll set all of these back
to their normal values. A in the game scene, the camera can also react to the player's death with another even more intense camera shake. The game manager will
also need to react to the player character's death in order to reset everything. So we can connect the player
character's dD signal to the game manager script. Let's name this
function on Player DD. When the player dies, first, we can disable the input handler so they can no longer
control the character. I'll wait 1 second first to let my character's
death animation play, which can be done by telling the scene tree to
create a timer. We can get the scene
tree anytime using the get tree function and call
the create timer function, which accepts an argument
for the length of the timer. This returns the timer already started so we can await
its timeout signal. This will pause the execution of this function at this line
for exactly 1 second. After fading to black, we'll need to reposition the player character to where
we want them to respond. Since we don't have
proper systems in place to handle this yet, I'll just put them at
the game scene's origin and leave a comment that this needs to be
implemented later. We can now revive the player
character, fade back in, and re enable the input handler so the player can
continue playing. If you want to
display some text, you can add a label node
to the Canvas layer. I'll name Mind game over
and give it a unique name. Then populate it with
some text, center it. Give it a larger font size. And anchor it to the
center of the screen. Before fading to black, I'll also create
a tween that will fade the label's
Alpha up to one. I'll modulate its Alpha to zero, so it is invisible by default. Without any await,
this will happen at the same time as the
screen is fading to black. The label will need
to be drawn in front of the fade in
order to be visible. After repositioning
the player character, I'll then wait another 1 second before fading at the text. But I'll make it quicker
than it faded in. After reviving the
player character, I'll display a different message as the game fades back in. I'll duplicate the existing
game over message, name this one revive, and change the text. Before the fade to clear, I'll use another tween to
make this text fade in. Wait another 1 second, then fade the text back out. Let's try it out. I'll let the Mouse night kill
my player character. We now have our
characters dying and the game manager reacting to the player character's death, but our game doesn't
have a boss in it yet. I'll see you in the next video.
62. 8-6 Boss 1: Hello, friends. In this video, we'll build a boss for our game that's more complex
than our other enemies. I'll start by creating a new asset folder to hold
the assets for this boss. Then import the sprites. I'll then create a
new scene in my enemy scenes folder named Mac Mouse
with a root node type of CharacterBody two D. This boss is comprised of multiple
different sprite components that I'll need to assemble
and layer one over another. First, I'll add a
normal no two D that I can use to flip all of the boss' visual
components when they turn around and name it flip. Then add multiple sprites
for the various components. I'll need a sprite for
the armor plating, which will be an atlas texture. And after populating
the texture, I'll select the appropriate
region from the sprite sheet. This will be offset
up by 48 pixels. Duplicating this sprite node, I'll rename this one to be gears and change its type
to Animated Sprite two D. Giving the node a
Sprite frames resource, I'll add the gear sprites
to the animation. Then change its animation
speed to 12 frames per second. This should be drawn
behind the armor plating, so I'll reorder the nodes. Duplicating the armor
plating sprite node again, I'll make another sprite for the background behind the gears. After making the atlas
texture unique to this node, I can then select a
different region to draw and reorder this node to be
drawn behind the others. This background is also
meant to clip the gears so that anything outside
the body will not be drawn. We can do this by
putting the gears as a child of the
background node. Then selecting the background under the visibility section, change clip children from
disabled to clip and draw. Now the gears only
draw inside the body. There's also a wind up
key which is animated. So I'll duplicate my
gears node to make another Animated Sprite
tout node and name it key. After making the Sprite frames resource
unique to this node, I'll replace the gear Sprite frames with the
key sprite frames. I'll then reposition
the key to be at the appropriate position at the back of the Mc mouse's body. I'll need more Animated Sprite two D nodes for the wheels, so I'll duplicate the key
to make an animated wheel. After making the Sprite
frames resource unique, I'll change out the
key Sprite frames for the wheel sprite frames. Then put the wheel at a good position and offset
it up to be on the ground. O. To draw the wheels behind the body, I'll reorder them to be above
the body in the scene tree. I'll duplicate the
front wheel to make a second wheel
and reposition it. The last visual component of this boss is a barrel that
extends from its mouth, which will just be
an ordinary sprite. After assigning the
sprite texture, I'll move it to be around the Mc mouse's mouth and layer it behind
the body sprites. I When this barrel fires projectiles, I want it to be protruding out
from the Mc mouse's mouth, so it will have an offset
of negative 16 pixels. I'll add an area Tutti
node at this position, which will be the projectile. Then give it an Animated
Sprite Tutti node with Sprite frames resource and add the
two fireball sprite frames. After setting the frame rate, I'll also have this
auto play by default. This will need a
collision shape, which will just be a circle. In order to work
as a projectile, it will need the projectile
script attached. The projectile script expects the projectile to have
an impact animation, so I'll add one using the same impact sprites
as my other projectile. This will need to
be monitoring and masking the player's
Hurt Box layer. We can connect the area entered
and body entered signals to the projectile script to trigger the impact
animation and damage. O. With the fireball assembled, we can save it as its own scene in the projectile's folder. I'll add a marker Toti
node as a child of the fireball to act as the location where the
projectiles will spawn, just like we did with
the player character. Then reorder the
marker Tutti node to be a sibling to the fireball, maintaining its
relative position and delete the fireball. Then reset the barrels offset back to its
default position. In addition to flipping
the body left and right, I also want the body of the Mc mouse to rumble
but not the wheels. So I'll put everything
I want to rumble as children of another
no two D named body. Anytime the Mc mouse is moving, I want dust and rocks to be kicked up from the
ground by the wheels. So I'll add a GP particle
two D node for the rocks. Then assign a rock sprite as the texture and make a
particle process material. Okay. Make the rocks
kick up from the ground, all I really need to do is change the velocity direction to up and give each rock a random velocity 32-64
pixels per second. For dust, I'll start
by duplicating the rock particle
emitter and rename it. Then assign the same
particle sprite sheet I used for attack impacts as
the texture property. Since this is a sprite sheet which is intended
to be animated, this node will need a
canvas item material with its particles animation
property turned on with the correct
number of frames. After making the particle
process material unique, I'll keep the velocity the same, but remove gravity and add enough damping to counteract
the initial velocity. Under display, I'll add an animation speed
of one to animate the particles and an Alpha curve to make them fade
away over time. I want this drawn in front, so I'll set its order
property to one. This should be sufficient, so I'll duplicate the
two particle nodes and add them to the other wheel. I don't want them
emitting by default, so I'll group select all of them and turn their emitting
properties off. Just like every other enemy, this one will need a collision
shape to collide with the environment plus hurt
boxes and hit boxes. Since this enemy has
more of a unique shape, I'll use a collision polygon rather than a collision shape. Adding a few vertices
to the polygon to get the general shape
of the boss' body, I'll need to make sure that the collision polygon makes
contact with the ground. I also don't really want to bother with flipping
this polygon, so I'll make it symmetrical. This will suffice for collisions
with the environment, but will also allow the player character to stand
on top of the enemy, too. For the Hurt Box,
I'm a little less concerned with
matching the shape of the enemy so closely, so I'll use an ordinary capsule. If you want to combine multiple standard shapes to
make a more complex Hurt Box, you can simply add more
collision shape nodes. I'll add a second capsule above the first one to
cover more of the body. I also don't want to bother with flipping
these Hurt boxes, so it helps that they
are symmetrical. The area two D node
will emit its signals as if all of the shapes were
combined into one shape. But keep in mind that if
the shapes are far apart, you may have something
enter one collision shape, then exit it, then enter another collision shape
causing multiple collisions. I'll also add a separate Hurt
Box for the wind up key. Giving this key its
own Hurt Box means it doesn't need to be
connected to the boss' health, but can emit signals when
the player attacks it. This can just have a simple
circle collision shape, but will need to be flipped. So I'll make sure that
the exposition of the shape node is what is determining which side
the Hurt Box is on. And finally, this
enemy needs a hit box. This will be used when the
Mc mouse charges forward. I want this to hurt the
player character if they are in front or
on top of the boss. So I'll use a capsule rotated
90 degrees that extends beyond the body's
collision polygon in front of it and above
it, but not behind it. Selecting the character
body root node, the enemy will need
to be considered terrain in order for the player
character to stand on it, but also exist on
the enemy layer too and mask only the terrain. None of the Hurt boxes
or hit boxes will need to be monitoring or
monitorable by default. We'll turn them on
through scripts or animations when they
need to be turned on. But the Hurt boxes can be set to the enemy Hurt Box
collision layer, and the hit boxes can be
set to mask the player Hurt Box collision layer so that when they are
turned on, they will work. We now have a boss
assembled for our game, but it doesn't have any
scripts controlling it yet. I'll see you in the next video.
63. 8-7 Boss 2: Hello, friends. In this video, we'll give our boss some
more complex behaviors than our normal enemies. Actual progression
mechanics will be covered in the next section. But for now, it will
help to think about how our boss should exist in
relation to its arena. So let's put this boss enemy in a simple arena with a
wall on either side. Generally speaking, bosses in Metroidvania trap the
player in an arena, and once the player enters, they must defeat the boss before they are
allowed to leave. This is an event which will
need to be triggered by the player character entering
the Area two D node, so we'll add one to the scene
and name it boss Encounter. Then give it a collision shape. I'll use a rectangle and make it cover a very
large area so that the player can't get around it and have it monitor for the
player character's body. This will need a
script attached, but I'll create it from
the file system tab in a new enemy scripts
folder for bosses, which we can name
Boss Encounter, extending Area two D. This script will
be responsible for controlling the overall
flow of the boss encounter, including when it
starts, when it ends, and any phase transitions, things like title cards, changing music, et cetera. We'll need to give
this a class name so we can extend it later. To react to the player character entering this boss encounter, we can go ahead and write the
on body entered function, which will accept
a parameter named body of type no two D
and return nothing. Player character enters
this boss encounter, first, we should turn off
all future collisions with this area by using set deferred to set the monitoring
property to false. Remember that when a function is called by a collision event, we shouldn't alter our collision objects at the same time, so we defer that until after the collisions
have been handled. We can then close the arena, trapping the player inside and perform any
initialization required. But we'll get to that later. For now, we'll declare an
exported variable that holds the boss enemy itself and one for the player
character as the target. When the player character
enters the boss encounter, we can set the value of
target to be the body, as well as a local phase
variable as an integer. Then add a start function,
an end function, and also a phase
transition function, accepting the phase
as a parameter and updating the variable
to its new value. In the start function,
we can set phase to one and call our decision making function to
start the fight. We'll also need a function which tells the boss
what to do next. For now, I'll call the Start function when the player character
enters the boss arena. Make sure you save this script so the class name is registered. Now that we have the
basic structure in place, let's attach a new script
to our boss encounter node. I'll name this after my boss, Neck mouse encounter extending boss encounter and save it
in the appropriate folder. With the script attached, we can assign the boss enemy
to the exported variable, set the collision mask to
the player character layer, and connect the body
entered signal. Since the on body
entered function already exists in the boss
encounter superclass, we don't need to worry
about it in this script. However, we will override the decision making
function and give this particular boss encounter its own algorithm for deciding
what it will do next. Structuring the boss encounter
in this way makes it easier to alter a
boss's behavior based on which phase it
is currently in or add environmental hazards
to the arena in reaction to the boss' actions
or synchronize the behavior of multiple
bosses in a single encounter. Any new boss encounters we want to create can also
extend the boss encounter class and provide their own unique implementations
for each specific boss. First, it's a good idea
to check if the boss is dead before letting it
decide what to do next. So if they are already dead, then we can just return. It's also a good idea to space
out a boss's actions with a brief pause awaiting a
timer's timeout signal. We can make it less predictable by randomizing the
timer's duration and even make the cadence of the fight increase
with each phase by dividing by the phase number. And no matter what the
boss decides to do, they should probably
face towards the target. So we can call a function to
tell the boss to face left if the target's global exposition
is less than their own. Using a match statement
on the phase variable, we can easily provide different action options
for different phases. Since my boss is so simple, I'll opt to increase
the intensity of actions by phase rather
than introduce new ones. With another match statement
on a random integer 0-1, I'll create an even chance to decide between two
different actions. Either the boss will move to a random position in the arena, which will be the
global position of this boss encounter node plus a random number between negative 300
and positive 300, or the boss will shoot
fireballs at the player. I'll randomize the number of fireballs based on
the current phase. I also want the boss to
have a third action, which is to charge at the player character
at a higher speed, but only under
certain conditions. So if the Mc mouse is ready to charge, then it will charge. If not charging, then the
action will be randomized. And I'll give it the sign of the target's global
position minus the Mc Mouse's global
position as the direction. Planning out what
actions you want your boss to use
and when gives you exactly which public
variables and functions you will need to
write in their scripts. So we can see from this script, our boss' script needs
an is dead variable, a ready to charge variable, and also face left, charge, move to position, and shoot fireballs functions. This is an easy way to approach encapsulation when
writing complex scripts. We can write the script that
needs to interface with it first so we already know
exactly what we need. Encapsulation also helps us
achieve abstraction, too. Since we only focus on what needs to be publicly accessible, we can also assume that everything else
should be private. Next, let's attach a script to the McMus' root node and save it in the
enemy scripts folder. We can declare all of the
variables and functions accepting the
matching parameters from the Boss encounter script. If we declare a signal and name it something
like finished, then we can emit this
signal from all of the actions which were selected from the
decision making function. For now, let's just print out what we were expecting
the boss to do. We can first check if this
enemy is dead, and if so, emit the finished signal immediately and return
so nothing will happen. Then wait for 1 second before emitting the
finished signal. Then selecting the boss enemies node in
the content scene, which we can child to the boss encounter itself if we want to and connect the
finished signal to the decision making function. Now every time this boss enemy completes any of the actions, they will automatically
decide what to do next. This should be sufficient
to demonstrate the flow of our boss battle.
So let's try it out. We can see from the output
every second or two, the enemy is making a decision
about what to do next. We now have our boss enemy being controlled by a boss
encounter script, but we still need
to implement how everything works internally
within the boss scene. I'll see you in the
next video. And
64. 8-8 Boss 3: Hello, friends. In this video, I'll go over how I wrote
my boss's functions so you can compare my methods to the way you
implemented yours. First, I attached
the Hit box and Hurt Box scripts to the boss's hit boox and
Hurt Boxes respectively. Then started on the
boss enemy scripts. I declared signals for anything that would
be used to start or end the boss fight or
cause a phase transition. Another for when
the boss changes directions so I know when
to flip things around, and a couple more for knowing
when the boss has arrived at its intended destination
or crashed into a wall. Then there's also the signal, which will be omitted when the boss' current decided
action is completed, and it needs to decide
what it will do next. I connected the change
direction signal to the Hit box and the wind up keys Hurt Box so they
can be flipped around. The Hurt Box script didn't
actually have this function, so I copied it from
the Hit boox script. However, the wind up keys
Hurt Box is behind the boss, so I needed to add
an exported variable to reverse the direction
for this specific case. Back in the Mac Mouse's script, I needed a reference to the boss's body in order
to make it rumble. So this gets stored
in a variable, since I'll be using
it very frequently. And, of course,
I'll need to know if the boss is alive or dead. I would like my boss fight to start with the Mcmuse turned off and have the player attack the wind up key multiple
times to start it up. So I declared a variable for
whether or not the engine is currently started plus a
speed for the wind up key, starting it at one
eighth of normal speed, and a tween I can use to alter the speed
gradually over time. To create this effect, I had the wind up keys animation
playing automatically, but with a speed scale of zero and its Hurt Box
monitorable by default. I then connected its damaged
signal to my boss script, calling a function
named on key attack. To ensure the key's
Hurt Box never dies, I call it its recover function, since this isn't a reflection
of the boss' health, merely another target for the
player character to attack. After setting the wind up keys animation speed
scale to double, I used the tween to
gradually reduce it back to its expected value
over the course of 1 second. If the engine has
already been started, then attacking the wind up
key will cause the Mac mouse to charge toward the
player character as their next action. Otherwise, I doubled
the key speed and checked if it had reached
a value of at least one. With the default
value of one eighth, this will happen
after the third time the wind up key is attacked. When that happens, I called a function to start
the Mc mouse's engine. All this function does is tell the gears animation
to start playing, start a rumble timer, which is set to one
12th of a second, so the engine is
started variable to true and emit the signal. Every one 12th of a second, when the rumble timer times out, I moved the Y position of
the body to a random integer between negative one
and positive one so the body rumbles up and down. Making the boss face left was very similar
to how it was done with the player character
with the exception that this boss enemy is
facing left by default. To make the boss drive around and charge toward
the player character, I used many of the same
locomotion variables in the character script
just with different names, and also included the
boss' destination and the distance to their
destination as floats, so I can use them
to determine where the boss needs to go in
the physics process. I also included gravity
just in case and set the movement speed to be the driving speed by default. I saved the wheels in an array so I can
synchronize them easily. While this may seem unnecessary
for only two wheels, writing it this way will work
for any number of wheels. I used a public
variable to say whether or not this Mc mouse
has had its wind up key attacked and is ready to
perform a charging attack and an integer to
count the number of times that they have
crashed into a wall. When the boss encounter script decides that the Mc
mouse should move to a position within
the boss arena and provides the position to
move to as a parameter, quickly checking if the boss is dead so that the
request can be ignored, I stored the destination, set the move direction, the move speed, and
is moving to true. I called a private function
to animate all of the wheels, then await my arrived signal before emitting the
finished signal. The arrived signal will be emitted from the
physics process later. The argument passed to spinning the wheels controls how
fast they will animate, as well as whether the animation will play forward or in reverse. So if the Mac mouse
is facing left and moving left or facing
right and moving right, I'll play the animation
with a speed of one. Otherwise, a speed of negative one plays the
animations backwards. Charging at the player character does mostly the same things. However, I want the Mc mouse to always face
toward the direction that it is charging and spin
the wheels at double speed. Using a short pause, this will telegraph
the charge to the player so they can react
and get out of the way. Since the charge is an attack, the hit box has its monitoring
property set to true. After setting is
charging to true, I await the signal which will be emitted after the
Mcmuse has crashed into a wall and use another brief pause to keep the boss stunned
for a short moment. Remember to connect
the hit boxes area entered signal to
actually deal damage. Spinning the wheels
and accepting the direction of the
spin as a parameter, I iterated through
each of the wheels in my wheels array and
told each to play their default animation using the absolute value
of direction as the animation speed
and whether or not the direction
was negative to determine if the animation
should be played in reverse. I also started the rock and dust particle
emitters attached to each wheel at the same time if the value of direction was
anything other than zero. Otherwise, they
will be turned off. Now for the physics process, which will make both of
these actions possible, if the boss is already dead, then it cannot willingly move, so its move direction
will be set to zero. I then applied acceleration or deceleration to the
boss' X velocity, as well as gravity to
its Y velocity before calling the move and
slide function to apply the velocity to
the character body. If the boss is
currently driving, I'll need to check if it has
arrived at its destination, or if it is charging and is
currently touching a wall, then I know it has crashed. To know if the boss has arrived at its
intended destination, I first need to know the
distance to its destination, which is the absolute value of the destination minus the boss' current
global exposition. Reversing the jump
height formula, we can determine if
the boss is close enough to the destination
to start decelerating. If distance is
less than velocity squared divided by
deceleration, divided by two. When this is true, I stop
spinning the wheels, set move direction
to zero is driving to false and emitted
the arrived signal. When the boss
crashes into a wall, I did most of the same things, but also added velocity away from the wall so they
will bounce off of it. And turned off the hit box. I also counted the number
of times that this happens, and on the third crash, damaged the Mc mouse's armor plating so they will
enter the second phase. When the armor is damaged
from crashing too many times, I increased the speed of
the wind up key to two. To damage the armor plating, all I needed to do was
change the region of the sprite sheet being drawn
by the armor sprite node. I also doubled the speed of
both the wind up key and the gear animations as well as the rumble timer to make the boss scene even
more unstable. And I turned the
monitorable property of the Hurt Box to true so the player character
can now attack the Mc mouse's body
to deal damage to it. Then emitted the signal to
trigger the phase transition. To shoot the fireballs, I needed a packed scene
for the fireballs, as well as the new fireballs, I would be instantiating the marker for where
to instantiate them, plus the barrel and a
tween for moving it. When the boss is told to
shoot a number of fireballs, I first awaited
extending the barrel, then looped a number of times, pausing a quarter of a
second before each shot, then shooting a fireball. After shooting all
of the fireballs, I awaited retracting the barrel before emitting the
finished signal. Extending or
retracting the barrel was a simple tween tweening the barrels offset X to the desired number over
a quarter of a second. Shooting a single
fireball is much the same as with the player
character's projectiles. Just adding it as a sibling putting it at the
correct position and firing in the forward
facing direction. In my projectile script, when telling a Hurt
Box to take damage, I'll also need to provide
the direction of the damage. Finally, when the boss
runs out of health, I use set deferred to
turn off the hit box and both Hurt boxes and stop all of the animations before emitting the dyed signal to end
the boss encounter. In the room content scene, I connected the engine started signal to the boss
encounters start function and the died signal
to end the encounter two. I also connected the
armor damaged signal to the set phase function, binding two as an argument to trigger the
transition to phase two. I commented out the call
to the start function so I could trigger the
start of the battle to happen after the
engine is started. Yes. Oh. We now have our boss performing
a complex sequence of actions with escalating
intensity over multiple phases. But our game lacks any kind
of meaningful progression. I'll see you in the
next section. And
65. 9-1 Save Point: Hello, friends. In this section, we'll turn this two
D platform into a Metroidvania by properly tracking the player's progress. We'll start by adding a
save point to our game. I've moved my boss
encounter into its own room and moved it
off to the side of my map in a separate volcano region and also made a new save room for my forest
region on the left. I left my player
character up here. I'll just reset their
position back to the origin. Let's start in the players
save data where we will need to store the location where the player last
saved their game. How you store this information will vary based on the
structure of your game. But for my game, I'll
use an array of strings. While it might be
more efficient to store save points as numbers, this is only a single variable, so the impact will
be negligible. It also helps to keep the save file human readable
for debugging purposes. The first element of the array will be the
name of the region, and the second will be
the name of the room. This pattern follows the
structure of my scene tree. When I load the
players save data, all I have to do is find the region that matches
the first string, then find the room that
matches the second string and position the
player character at the save point in that room. Note that this save point should exist in the game
at the time when the save data is
being loaded and no room contents have
been loaded yet. Therefore, the save Point
object should be in the room scene rather than
the rooms contents scene. I'll add a node two D to my save room scene and
name it savepoint. To keep things simple, I'll place this node to be at the position where I want
to spawn the character. Since we will need
to have multiple of these save
points in our game, it should be saved
as its own scene. We can then edit
it in isolation. How you want to activate your
save point is up to you. I'll just use a simple
collision event. So anytime the player
touches a save point, their progress will be saved. So I'll add an area two D node
to the scene and offset it up off the ground 96 pixels so the player has
to jump to touch it. I've imported some assets
for my save point, which is a book and also an effect sprite that I'll draw behind the book
to make it glow. I'll add a Sprite two D no to the area node to
draw the glow first. Then add an Animated
Sprite two D for the book. Give it a Sprite frames resource and populate it with
the book sprites. Playing the animation forwards
and then again backwards. The area needs a
collision shape, so I'll give it a circle and
use the radius of the glow, which is 32 pixels. Since this is in the room scene, not the room contents scene, it will need to be ordered in front of the room's contents. It will need to monitor for
the player character's body. A attaching a script to the save point, we can then connect
the area's body entered signal to the script. When the player character
touches the save point, we'll set the value
of last saved at in their save data to
be a new array with the first element of the
array being the name of the region and the second
element, the name of the room. We can automate these names
using the on ready tag, since the parent
of the save point will always be the
room it is in, and the parent of the room
will always be the region. The nature of the scene tree requiring siblings
to have unique names will ensure that this
is sufficient to determine exactly which
room they last saved in. We can also grab a reference
to the Animated Sprite two D node and tell it to play its animation
when this happens. Don't forget to tell
the file global node to save the game anytime you want to make
something permanent. Switching over to the
game manager script, we'll need to load the
player's last saved position. After all of the
initialization is done, we'll call a function to
position the player character. If the player's save data has
a value for last saved at, then we can get the region by its name since we know it is
a child of the world node. And then do the same
to retrieve the room, knowing that the room is
a child of the region. We can then position
the player character at the global position of the
save point in that room. This will trigger the
player entering that room, load its contents, and
fade in as we expect. If the player hasn't
reached a safe point yet, I'll put them at the
origin to start the game. A problem arises when the
player dies, however, since we will also want to put the player character at the last saved position at that time. Both the death event sequence
and the player entering a room will be manipulating
the fade at the same time. There are multiple ways
to solve this problem, but to keep things simple, I'll check if the
player character is changing rooms when they die. If they die and respawn
in the same room, then the player entered
room will not be triggered, so the death sequence
can handle the fade. Otherwise, I'll replace it with a simple pause
for 1 second. To know if reposition the player character
has changed rooms, I'll make that function
return a boolean. And return if the
new room is the same as the current room or
false as a default. Et's try it out. When
I first load my game, my character starts at the default position
in my start room. If I go over to the left, I'll find the save point. Jumping into the save point
triggers saving my game. If I close the game
and play again, now my player character
starts at the save point. To make my save point
a little more magical, I'll have it float up and down. I'll export a couple of variables for the
oscillation range and speed with a default of 16 pixels and one
oscillation per second. The thing that will be
moving is the area node, which will also move the
sprites as it's children. I'll need to know its
initial Y position and how much time has passed. Overriding the process function, I'll set T to be
itself plus Delta multiplied by oscillation speed wrapped between zero and tau. This will allow the oscillation
to repeat infinitely without ever getting
to the maximum of a floating point number. The area's Y position
can then be set to its initial position
plus sine of T multiplied by the
oscillation range. To draw the save
point on the map, we can simply add
a Sprite two D to the save point scene and give it the texture
we want to draw. Scale it up really big so it will be visible
to the map camera, then hide it so it isn't in the way and attach
the map icon script. It will need to be
on visibility layer two, not layer one, and all of its ancestors
in the scene tree must be visible on both
layer one and layer two. I want to test out the
death problem too, so I'll add a simple
input function to my player character script. So when I press the space
bar, the character dies. Now when I run the game, the save point is
floating up and down, and I can see it on the map. When I press the space bar, the character dies and
responds at the save point. If I move to another room, I can do it again and respond back at the
save point once more. We now have a save point
that will remember the player's last saved position and load them at the
correct location. In the next video,
we'll add the ability unlocking items that
define a Metroidvania. I'll see you in the next video.
66. 9-2 Metroidvania: Hello, friends. In this video, we'll add items for
the player to collect that will unlock their
Metroidvania abilities. I'll start by creating a new scenes folder
named Upgrades. Then duplicate my
save Point scene, name it Gem, and put it in
the new Upgrades folder. Then open it up to edit it. Unlike the save point, I don't want this to
be drawn on my map, so I'll delete the map icon. There's no need for this to
be offset from its origin, so I'll reset the
area nodes position. Then make the area two
D node, the root node, rename it GEM and delete
the old Save Point node two D. I'll replace the book Sprite frames with new ones
drawing a spinning gem Then have this
animation autoplay and loop at 12
frames per second. We can then attach a new
script for our upgrades. Reconnecting the Area
two D nodes body entered signal to the script, we can unlock the
player characters new ability when they
touch this upgrade. To do that, we'll
export a variable of type Enums dot abilities, referencing the Enums
script we created earlier. This creates a convenient drop down in the inspector that will let us select one of our abilities for this
upgrade item to unlock. But remember that an enumeration is just an integer with a name. In my game, the gem will unlock the ability for the
player character to shoot projectiles. Back in the script, when the player character's
body enters this area, we can set the value of file dot data dot Abilities unlocked indexed at
ability to true. Then save the game to make
this upgrade permanent. To make this feel
more impactful, we'll probably want
to do a lot of fancy stuff before
and after this, then eventually remove
this node when we're done. We can ensure that only
a single collision event will ever happen with this node by using set
deferred to turn off its monitoring property
after the first collision. In my save rooms contents scene, I'll put this somewhere
so I can test it out. A however, I wanted to float up and down
like the save Point does. Rather than copy and paste to the code from one
script into another, we should make it
a component that can be attached to
any node two D, much like the enemy
behavior scripts. Back in the gem scene, I'll add just a normal node
and name it float. Then copy and paste to the code from the save Point
script into this script. Instead of an area tote, this will just be the
float node's parent, although it must
be a no tutti or some ancestor of a no tutti
in order to have a position. We can then copy this node into the Save Point scene and remove the code from
the Save Point script. These three scripts
are much more reusable when they are written to only serve a single purpose, as well as being easier
to understand and debug. Another thing we
should do is not have this upgrade item B here if the player has already
unlocked this ability. Overriding the ready function, we'll check the value of abilities unlocked in the
players save data to see if this ability is already unlocked and queue this
node to be freed if it has. Now let's try it out. When I run the game, I can see my upgrade item
floating there. Pressing the button to
shoot projectiles does not work since I have not
unlocked this ability yet. Jumping up to grab
the upgrade item, it disappears, and I can
now shoot projectiles. Closing the game
then playing again, the upgrade item is not there since I have
already collected it, and I can still
shoot projectiles because the ability was
permanently unlocked. Now we want to make
collecting this item feel a lot more
impactful to the player. In order to test this, we'll need to delete
our save data. In the project menu, select Open user data folder to open this folder in
your file manager, where we can see save dot
s, and we can delete it. Now when we run the game, the player has no
last saved position and no abilities unlocked. I'll just go back to
the save room and hit my Save Point,
then close the game. If you want to try to
make the event sequence for collecting the
item by yourself, you'll probably want to add particle effects or other
nodes to achieve your result. Pause the video here, and then we'll go
over what I did. After implementing
my event sequence, here is what my
results look like. First, I declared a constant
color bright white. This is not just white but
has a maximum intensity, meaning it will make other
colors near white, too. When the player character
touches the item, I immediately stop
the floating up and down and freeze the
player character by disabling the processing. I then started a particle
emitter emitting the particles from around
the item moving inward. Then tween the item to the player character's position plus halfway up to be around their body center while also tweening the colors of both the item and the
character to bright white. Adding parallel to
a tween function means that these will all happen at the same time
rather than waiting for one to finish before
starting the next one. I then unlock the ability and save the game
just as before. And after the
ability is unlocked, hide all of the visual elements, stop the inward particles, start an outward
burst of particles, and update the
character's sprite to include the new item. After tweening the player character's color
back to normal white, I then turn the player
character's processing back on and remove this
item from the scene tree. The inward particles are reusing the same sprites from the
boss' dust particles. They are emitting
from a sphere with a radius of 32 pixels. The particles have a
negative radial velocity to draw them toward the
center point, no gravity. Alpha curving 1-0 and animated. Outward burst particles
are mostly the same except it's a one shot
burst with explosiveness. Emitting from a point with maximum spread and enough damping to cancel out
the initial velocity. We now have collectible
items in our game that will give the player new
ways to explore the world. But our game would
be a lot more fun to explore if it had shortcuts. I'll see you in the next video. And
67. 9-3 Shortcut: Friends. In this video, we'll add a shortcut that the
player can unlock to make exploration of our game
smoother and more rewarding. I'll be using the second room to the right of my start room to build my shortcut in the
second room content scene. I've divided the room
into two distinct halves, and I'll place my shortcut
in the middle so that the player cannot get through this room if they
entered from the left. Starting with a static body to denode to provide
collision detection, and a Sprite Toti
node to draw it. I'll use an atlas texture and populate my forest
terrain as the atlas, then use the three vertical
tiles to draw a wall. Adding a collision shape as a box and covering the wall
with terrain collision, the player will not be
able to get through. We can then add another
Sprite tutti node using an atlas texture, and I've already imported a new sprite sheet
for a wooden brace. I'll use the first
undamaged sprite and position it on the
right side of the wall. So if the player enters this
room from the right side, they will be able to
attack this brace, damage it, and destroy the
wall to open the shortcut. The brace will need
a Hurt Box area two D node with a collision
shape two D node child. I'll use another box shape
and have it cover the sprite. O. It would be a good idea to put this on a different collision
layer so we can carefully control
what can damage it. I'll use layer two
for my shortcuts. Don't forget to name
your physics layers in the project settings so you can remember
what they're for. We'll need to have our
characters male attack hit box mask this layer in order for them to be able
to damage the brace. As well as any projectiles
they can shoot. Back in the room contents scene, we should make this
shortcut its own scene, since we'll probably have
multiple of them in our game. This shortcut will be specifically made only
for the forest region. So I'll save it in that folder, and I'll need to make other shortcut scenes for
my other regions. Opening up this scene, we can attach the
Hurt Box script to the Hurt Box node and also attach a new script to the root node for
the shortcuts behavior. The main purpose of this script will be to save when
this shortcut is destroyed and keep it destroyed when loading
this room's contents. To do that, we'll use
something commonly called a flag by exporting a string variable
for the flag's name. In the room contents scene, we will need to specify a
flag name for this shortcut. We can name this flag
anything we want, but it must be unique for
every shortcut in our game. I'll name mine Forest
second Room shortcut. Switching over to
the data script, we will need a new
exported variable to hold our flags
in a dictionary. Using the string as a key and the value
will be a boolean. Back in the shortcut script, when this shortcut is destroyed, we can set the value of file dot data dot flags with the key being our flag name
and set the value to true. Then save the game to make this permanent and after
doing some visual stuff, remove the shortcut
from the scene tree. Overriding the ready function, we can check the
value of our flag to see if the shortcut
has already been destroyed and remove it from
the scene tree if it was. However, if the shortcut
has not been destroyed yet, the dictionary will not have any value for this
flag name yet. So we actually need to check if the value exists first
before trying to access it. Let's connect the Hurt box's
damage received signal to the shortcuts
destroy function, unbinding the argument. I'll move the second
room to be to the left of the save room
so I can test it out. Entering this room
from the right, the wall prevents my character
from passing through. I can attack the
brace to destroy it, which destroys the wall, and I can pass through. Closing the game and
running it again, the shortcut remains open. However, there's
a small problem. I'll rearrange my rooms so the second room is
back on the right, then delete my safe file
to restore the shortcut. Running the game again. The wall does prevent my character
from proceeding. However, I can still break the shortcut open
from this side. So we'll need to
prevent our character from being able to
attack through walls. Opening up the character scene, we can add a cast two
D node to the hit box. We'll use the same strategy we used for enemy aggression to check if there is a clear line from the hit box
to the Hurt Box. To do this, the ray should be originating from the
character's center, and we'll need to mask
everything that this hit box can hit plus anything that can
obstruct it like terrain, including both bodies and areas. The ray may also originate inside of some of
these colliders, so it will need to
be able to hit areas and bodies from inside
their shapes, too. In the Hit box script, we can grab a reference to this cast node with
an on ready tag, but I'll change the node path
to use get node or null. This way, not every hit box in our game needs to have
a raycast attached. When a collision occurs, we'll check if the
ray exists first, then tell the ray to point at the Hurt Box that was hit by the hit box and force it
to update immediately. If the ray cast is
hitting something and that something is anything
other than the area, that means that there is
something else in the way. Returning will prevent the
damage from occurring. If we run the game now, attacking the shortcut from the wrong side will be blocked
by the walls collider. If I move this room to the left side and
enter from the right, I can attack the brace and
destroy the wall since the ray fired from
the character's hit box is not obstructed. If you want, you can pause
here and add particle effects, shake and even require multiple attacks to break the brace before
destroying the wall. Now that I've added my effects, here's what my
results look like. After one attack, the wall
emits dust particles, shakes, and the brace
is visibly damaged. After a second attack, the brace shatters
into several pieces, the wall is removed, and the
particles are emitted again. To achieve this effect, I added a GPU
particles to the node emitting the same dust
particles used previously, but this time emitting
from a box shape. I added a debris
node as a folder containing three different
pieces as rigid bodies. And a shake timer which will emit a signal 12
times per second. Connecting the Hurt box's
damaged received signal to an damaged received function, I restarted the
particle emitter, then checked if this is the first time it
is being damaged. If already damaged, I turn off collision detection on the Hurt Box and destroyed the wall. Otherwise, set damage to
true, update the brace, sprite to draw the
damaged version, then start the shake timer
and stop it 1 second later. The shake timer's timeout signal calls this shake function, which just offsets the wall by one pixel in a
random direction. When the wall is destroyed, I updated the save
data as before, but also scattered some debris, hide both the wall
and the brace. Then wait for the particles to finish before removing
the entire scene. To scatter the debris,
I made it visible, then unfreeze all of
the pieces and add impulse forces to them equal to their positions
relative to the parent. We now have a shortcut that
the player can break open, making backtracking much easier as they explore our world. But the entire map is plainly visible for them to see
from the start of the game. I'll see you in the next video.
68. 9-4 Map: Hello, friends. In this video, we'll hide our map
from the player and only reveal rooms as
the player enters them. Starting in our
save data script, we can use the same
strategy as we did with the save point to track which rooms the
player has entered. We'll export a new variable
named map of type dictionary. The keys of this dictionary
will be the names of regions, and the values will be
another dictionary. The keys of the
second dictionary will be the names of
rooms in that region, and the values will be booleans tracking whether or not the
player has entered that room. If we look in any
of our room scenes, the map will be drawn by
some form of Tu Di node. In my game, they are being drawn by a Tile Map layer node. Whatever the node is, toggling its visibility is
fairly trivial. We'll describe a reference to this node in our room script. You can use the ready tag if the map node is uniform
for all rooms across your entire project or an export tag if you
want more flexibility, but you'll have to manually
assign it for every room. We'll also need each room to know the name of
the region it's in, which we can automate
with an ready tag, getting the name of the room's parent node
in the scene tree. We can then write a function
to update the visibility of the map based on whether or not the player has
entered this room before. Checking file dot data
dot Map indexed at the name of the region gives us another
dictionary for the region, which we can index at
the name of the room. This will be a boolean
for whether or not the player has entered
this room previously. And if it is true, we will set the maps visibility to true. However, just like with
the flag dictionary, we need to be careful
not to try to access dictionary entries
which do not exist. But this provides an extra
opportunity in this case, since we can use the absence of the dictionary entry as
additional information. For example, we can
check if the map dictionary has an
entry for this region. If it doesn't, that means the player has never
entered this region before. Likewise, if the room
entry doesn't exist, then the player has no knowledge that this room even exists. We'll first need to check if the region entry exists
in the map dictionary. If it doesn't the player should not be able
to see this map, and we can just return
out of this function. Then do the same thing with the room entry in the
region dictionary. If the entry does not
exist, I'll hide the map. However, if it does exist, then we can set
the visibility to true and check the
value of the boolean. If the boolean is true, then I'll set the maps modulate
Alpha to full opacity. But if it is false, I'll set
it to be semi transparent. This means I can differentiate
rooms on the map the player knows about from ones that they have
previously entered. Alternatively, you may want to use a different node to draw these different states and toggle the visibility of
each individual node. We can initialize the visibility of the map during
the ready function. We can write another function to discover a room and
reveal its map. To create our two
different map states, we can accept a
boolean parameter for whether this room is being
discovered by entering it. All this function
needs to do is set the value of file
dot data dot Map, indexed at the region name, indexed at the room's name
to the value of Entered. But if this value already exists and has been set to true, we don't want to set
it back to false. We can use the logic
operation or to set it to false only if
it isn't already true. Then call the Update
Map visibility function to start drawing it accordingly. If the region does not yet
exist in the Map dictionary, then this is the first room the player has discovered
in this region. This would be a good opportunity to do something interesting, like display a
title card or play a shortcut scene to introduce the player to the new region. I'll call a function not yet written for the region
named Discover. This will be responsible for initializing the map dictionary
entry for the region. Switching over to the
game manager script, when the player character
enters a new room, we can discover that room with an argument of true to
draw it opaque on the map. In the main game scene, we can attach a script
to our game's regions. Writing the discover function, this will be called
the first time a player enters any
room in this region. So we can start by creating the map dictionary entry for this region in the
player save data. I'll also take this
opportunity to add the region to the player's map by calling
a reveal Map function. This will just iterate through every room in this
region and discover it, but with an argument of false, so it will be drawn
semi transparent. This script will need to be
attached to all regions. In each of my region scenes, I'll also add a label node to display the name of
that region on the map. For this to be readable
on the map camera, it will need to have a
very large font size. I'll set the font size to 256. Then set the font color to
match the color of the rooms. To make it stand out more, I'll also add a shadow
and an outline. It will need to be
on visibility layer two for the map
camera to see it. I'll copy this label note into my volcano region and change its text color
to match that region. Adding a canvas layer to
the region scene two, we can make a simple
title card to display when the player
first enters each region. With another label node, I'll display the name
of this region in the center of the screen
with a large font size. I'll make this invisible
by default and only display it when the player enters this region
for the first time. In the region script, I'll animate this
display with a tween, tweening the Alpha of the
label node up to one, waiting 1 second, then
tweening it back down to zero. It will need to have an Alpha of zero to start and
be made visible. This can be copied into my other regions and
updated with their names. When the region is discovered, I'll save the game and make
the map label visible. This will also need to be initialized in the
ready function based on whether or
not the region entry exists in the Map dictionary. Now that regions have
children that are not rooms, I'll need to check
if the children are actually rooms
before calling their discover functions.
Let's try it out. When the game starts, the player enters the forest
region automatically. So the title card for the
forest region is displayed. It's map data is revealed, but only the start
room is opaque since that is the only
room I have entered. As I enter each room, the rooms change from
transparent to opaque. I have removed the shortcut from the other room to
make things easier. The volcano region has
not been entered yet, so it is not
revealed on the map. And when I enter
the volcano region, it displays its title card and its rooms are
added to the map. Although I haven't
built anything appropriate for this region yet. Returning to the forest region, the region has already
been discovered, and so it does not
display its title card. We now have a system
for revealing our game's map gradually as the player
explores our world. But the regions of
our game would feel a lot more distinct with
some background music. I'll see you in the next video.
69. 9-5 Music: Hello, friends. In this video, we'll add some background music that will change with
each region in our game. This will not only
help emphasize the unique look and
feel of each region, but also provide an extra
sense of progression too. When you import background
music into your game project, select the files in
the File System tab, then open the Import tab and
change loop mode to forward. Otherwise, your music will
only play once and then stop. This will require a
reimport of the assets. In each of our region scenes, we could put an audio
stream player node to play these music tracks. But that would require us to manage which one is
playing at any given time, which is kind of a hassle. Typically, we won't want to have more than one background music track playing at a time anyway. So it would be easier to only have one audio
stream player node dedicated to playing
background music that we can access from
anywhere in our game. But each region
still needs to know which background music
should be played for it. In our region script, we can export a variable to hold the background music for this
region as an audio stream. Then populate the
exported variable for each region in their
individual scenes with different music files. We can then create
a global script to manage our background music, inheriting from
Audio stream Player. Then open the project settings, the Globals tab, and add this script to the
list of global nodes. Returning to the script, we really just need
two public functions, one which will tell
the script to play a music track and accept the
audio stream as a parameter, and another to stop
playing music. But stopping music suddenly
would be kind of jarring. It's fairly common
to fade music out by reducing its volume
gradually before stopping. Starting with the
play track function, we can set the stream
being played to the track parameter and call the audio stream players play function to
start playing it. But if something is already playing, we need
to stop it first. So if this script is already playing something and what it is playing is the same as what we want to play, we
can just return. If it's not the same, then we can await fading out
to the volume first, and after that, we can switch to the new track
and fade back in. To fade the volume gradually, we will need a tween and a function for
tweening the volume, accepting the final value for
the volume as a parameter. After checking if the tween already exists and killing it, we'll create a new
tween and tween the volume to its final
value over some duration. I'll just use 1 second. Volume is usually
measured in decibels, which is not a linear
unit of measurement. So tweening the volume in
decibels is not recommended. Instead, we use the
linear volume value, which is a simple float
ranging from zero for mute to one
for normal volume. We can return the tween finished signal so
we can await it. To stop playing music, we just need to wait for
the volume to reach zero, then call stop and return
the finished signal. And after switching tracks, we should fade the volume back up to one so we can
hear the new music. Now we just need to tell this node which music
track to play and when. For simplicity, I'll assume that when a room's
contents are loaded, that I want to play the music that is associated
with that region. So calling music play Track, I'll pass the region, which is the parent of
the room dot Music. If told to play the same music
that is already playing, the music global node will
just ignore the request. So there's no need to check
what's already playing. Let's try it out.
Running the game, I start in the forest region, and so the forest
music theme plays. Changing rooms, the music does not change as long as I am
still in the forest region. But if I transition to
the volcano region, the forest region
theme fades out and the volcano region
theme plays instead. There will probably be
times when you want to play a specific music
track temporarily, such as during a cut
scene or a boss fight. So let's add two more
functions to our music script. One to override the
current music track and another to revert back
to the previous music track. We'll need a variable to hold the current region's
background music. When told to override the background music
with a new music track, we'll first store the region's background
music in our variable. Then wait for the fade out
and play our new track. After the event has completed, we can revert back to our Region's background
music by fading out, then playing the
old music track. In our boss encounter script, we can export a variable to hold a special music track
for our boss music. Then in the Mc Mouse
boss encounter, we can give this boss fight
its own special music track. The music global
node can be told to play this music when the
boss encounter starts. And reverts back to the region's background music
when the fight is over. And finally, this music node should be told which
bus it belongs to on the Audio Bus so we can control its volume in other
settings more easily. So overriding the
ready function, we can manually set the
value of bus to be music. We now have a global
music system that we can use to play any music
we need for our game, but our boss encounter
still needs some work. I'll see you in the next video.
70. 9-6 Final Boss: Hello, friends. In this video, we'll complete the boss
encounter with an arena and stop the boss from
respawning once it is defeated. I have rearranged my
rooms so that the player will start in the save room
right next to the boss room. Opening the boss rooms contents, I've gone ahead and built up the Boss room's content scene with multiple Parallax layers. In order for the Tile Map layer two D node to be able
to Parallax scroll, I had to create a new
script inheriting from Tile Map layer without
the auto scroll. Since the Tile Map
layer two D node does not have the region
wrecked property, it can't use the same method if we wanted it to auto scroll. Each of the Tile Map
layer to Di nodes have a different scroll scale and modulate more toward
the background color. To keep track of which bosses
the player has defeated, let's open up the
data script and add a new variable named
Boss is defeated. An alternative to using a
dictionary is an array. Anytime a boss is defeated, we can just append its
name to this array. In the boss encounter script, we can override the
ready function to check if this boss has
already been defeated. If file dot data dot boss is defeated has an entry
matching the boss's name, then the player has
already defeated this boss and we can remove
this boss encounter. When the boss is defeated, we can append the boss' name to the array of bosses defeated. Then save the game to
make it permanent. Unlike dictionaries, arrays
can have duplicates. So when you're storing
information in this way, you may want to check if the entry already exists
before adding it. Removing the boss encounter node from the scene
tree at this time will also remove the boss itself, which I
don't want to do. But if the player were to leave the room and come back in, the boss will be removed
by the ready function. This will be sufficient to
make the boss stay defeated, but the encounter itself
could use some improvements. The first thing I want to do
when the boss battle begins is shut a door locking the
player inside the arena. Just like with the shortcut, I used a static body to de node, reusing the same wall
sprite as the terrain. And a rectangle collision shape. However, the sprite and
the collision shape are positioned far above the
door's origin point, so the door is open by default. When told to close, the door repositions
the collision shape, then tweens the sprites
position to match, and opening the door returns the collision shape
and the sprite back to their
original positions. Giving this door a unique name, we can close it in the boss' encounter
script when the battle starts and open it
when the battle ends. Remember to call the
superclass functions to also do everything else in the
start and end functions, too. I also want to make
the boss fight harder by dropping flaming
boulders from the ceiling. So I've made another scene
for the flaming boulders, which are a rigid
body two D node. This will just automatically
handle the gravity for me. They are being
drawn and animated by an Animated
Sprite two D node, looping through the
imported sprites at 12 frames per
second automatically. The rigid body is on a new
collision layer set aside specifically for hazards and
is not masking anything. So they will not collide with anything,
including the terrain. They will just fall from above the arena to below the arena, and there is a hit box attached, monitoring for the player
character's Hurt Box. Don't forget to connect to the area entered
signal to deal damage. In my Mc mouse encounter script, I've exported a variable to hold this flaming boulder
as a packed scene, which I can instantiate as a new flaming
boulder rigid body. When told to spawn
flaming boulders, I looped a number of times randomly between the
phase number and the phase number
times two so that the number of boulders will
increase in the second phase. Instantiate a new boulder, I added it to the scene
and positioned it above the arena at a random position between the left
and right walls. To make the crash event
even more impactful, I'll also add a camera shake. Godot makes it pretty easy to access the current
camera from anywhere. First, we just need to call
the Get Viewport function, then call the viewports
Get camera two D function. We can then call our shake
function to shake the camera. I'll give it an intensity of two pixels for a
quarter of a second. I can then connect the Mc
mouse's crashed signal to the encounter script to
trigger the spawning of the flaming boulders in
response to this happening. Since the boulders
are rigid bodies, they will fall naturally
due to gravity. Then I can clean them
up after they have fallen below the bottom
of the arena using an area two D. This
garbage collection area is just a rectangular area that covers the
bottom of the arena, far enough below that
anything entering this area will not be
visible to the camera. The area node is monitoring and masking the hazards layer. Its body entered signal
connects back to the Mc mouse counter script to remove them from
the scene tree. With the changes made to
our character's hit box, requiring the cast from the hit box to the Hurt
Box to be unobstructed, I had to reorganize my
Mac mouse's key Hurt Box. So now the Hurt Box is
determining the position and the collision shape is at
its local origin position. Since the Area two D node is determining the
position itself, the script no longer needs to reference the collision
shape and instead flips its own position in response to the character
changing directions. Let's try it out. I'll enter my boss room and find my
boss enemy sitting there. I can attack the key to wind it up and start the
boss encounter. This triggers the start
of my boss fight, which changes the music and shuts the door on the
arena, trapping me inside. As I fight the boss, anytime it crashes into a wall, it triggers a camera shake and drops flaming boulders
from the ceiling. While we can't see
what happens to the flaming boulders as
they drop below the camera, they are being cleaned up by
the garbage collection area. When the boss enters
its second phase, the number of flaming
boulders spawned increases, and defeating the boss
ends the encounter, changing the music back to the volcano region music
and opening the door. Leaving the room
and coming back, the boss encounter
is no longer there. We now have
everything we need to build an entire
Metroidvania game. In the next video, we'll
go over our next steps. I'll see you in the next video.
71. 9-7 Final: Hello, friends. Congratulations
on completing the course. You now have all the
tools and skills necessary to build a
basic Metroidvania game, complete with unlockable
abilities that will gradually give the
player access to more of the world to explore
a map that shows the player's current location and save points to keep
track of their progress. All that's left to do
now is repeat many of the lessons taught throughout the course to create more of it. More rooms, filling
out more regions with more enemies to fight and
more shortcuts to unlock. Follow these steps to get more out of this
course and continue learning more skills
you can use to build your own
Metroidvania game. Step one. If this is your first
major game project, you should be able to
build a game following this template with at least
three different regions, each with multiple rooms containing different
enemy types, platforming challenges, and these regions should feel
distinct from each other. Following Metroidvania
game design principles, your game should demonstrate
to the player obstacles that they're unable to overcome before unlocking the
ability to do so. The critical path
through the game should require
backtracking through previous areas and
give the player multiple new opportunities to explore with their
new abilities. Try to make a game that
takes at least 10 minutes to complete from start to finish without being
too repetitive. Step two, after you're done with your game and
want more of a challenge, you may want to think of something that you
want to change. Maybe you don't like the way
the wall jumping works or you want to add recoil force to the player
character's attack. Try to come up with a way of adjusting the mechanic
to function the way you would prefer and change the way it is
implemented in the game. Try to avoid making
something entirely new yet. Just focus on making adjustments
to what's already there. Step three. Once you're able to make changes to the mechanics
that already exist, your next step should be to add a new mechanic that uses
the tools already provided. Some things you
may want to add to the game might be a
collectible item that increases the player's
maximum health or a new spell that heals
the player character. Adding these mechanics to the game requires no new skills, but requires that you
learn how to integrate multiple existing systems
together in a new way. You may also want to look
for some new assets to use for your new mechanics
or try creating your own. Step four. Now that you can make existing
mechanics work the way you want and integrate them
together with new systems, it's time to design and build a new mechanic
from scratch. This could be an entirely
new Metroidvania ability that allows the player to
access more of the world, adding NPCs for the player to talk to with a
dialogue system, making enemies drop money
for the player to collect and allowing them to spend that money in a shop to
upgrade their weapon. Before you start, make sure you take some time to
think about all of the existing systems that will need to communicate
with the new mechanic. Consider which parts of
your design should be abstracted or what can be inherited from existing scripts. Step five. If you can create an
entirely new system and successfully integrate
it into this game, then you should be ready to
make a new game from scratch. Either find a
different asset pack online or create your
own assets to use as you build a completely
new Metroidvania game from the ground up entirely
of your own design. You're welcome to share your
work on our discord server as you continue to learn and grow your skills
with this project. Step six. Once your game is ready, you can share it on
platforms like Ich to get player feedback and even sell the game when
it's complete. You probably won't make very much money
selling games on IH, but the feedback will be invaluable for improving your
game development skills. If you have a game that
has performed well on Ich, then you could try selling it on steam to reach a much
larger audience. But the bar for
quality for a game to be successful on
steam is much higher. And you'll need to
have a market strategy if you want your game to
be noticed by players. Good luck on completing
your project. I look forward to
seeing your work.