Section 6: Combat System
In this section, we added a combat system for players and enemies to attack each other and take damage.
Section Intro - Combat System
There are no notes for this lecture.
Player Combo Attacks
In this lecture, we added two animations for player attacks and created the player attack state. There are no notes for this lecture.
Combo Counters
In this lecture, we tracked the player attacks using a counter. When we play the animation, we're combining the ANIM_ATTACK
constant and comboCounter
field to generate the name for the animation to play.
Next, we subscribed to a signal called AnimationFinished
from the animation player node. This signal gets emitted when the animation is finished playing.
When our method gets called, we're wrapping the comboCounter field. Something to keep in mind is that the Mathf.Wrap
method's maximum threshold argument wraps the value when the threshold is met or exceeded. So, the max threshold is incremented by 1 so that the value doesn't get wrapped too early.
Resetting the Combo Counter With a Timer
In this lecture, we decided to reset the combo counter if the player stops attacking after a few seconds. We used the timer node's Timeout
signal for this event. During subscription, we used a lambda function.
Lambda functions are just shorthand solutions for defining methods. If the signal gets emitted, the function gets executed. In this method, we're setting the comboCounter
field to 1
.
The timer gets started every time we exit the attack state. By doing so, the timer resets itself even if it's still running.
Hitboxes and Hurtboxes
In this lecture, we took the time to set up hitboxes and hurtboxes with Area3D
nodes.
Hitboxes - Cause damage. (Ex: Swords, Arrows, Floor Spikes)
Hurtbox - Take damage. (Ex: Players, Enemies)
During this process, we subscribed to the AreaEntered
signal on the Area3D
node. Unlike the BodyEntered
signal, this signal gets emitted when another area has been detected.
Next, we printed a message. For this example, we used string interpolation, which allows us to inject variable values into a string. First, the string must start with the $
character. Next, we must wrap the variable with a pair of {}
characters. Other than that, everything else is the same. For demonstration purposes, here's an example of string interpolation and string concatenation.
Resources
Custom Stat Resources
In this lecture, we created a custom resource. Resources are a feature in Godot for storing any kind of data that we'd like. We can create a resource by inheriting from the Resource
class. During this process, you'll have to make sure the class is partial.
In this example, we're creating a resource for storing stats. The first property will be the type of stat, and the second property will be a value. For the type of stat, we're using an Enum.
Enums are a feature for storing a list of values. They're helpful when you don't want to make typos in your program by relying on strings. We can define an enum with the enum
keyword followed by an identifier. Inside the enum, we can provide a list of comma-separated values.
Understanding LINQ
In this lecture, we used LINQ to help us find a result from an array. To grab a single stat, we defined a method in the Character
class called GetStatResource
.
This method will accept a specific stat and then return a resource. All resources are stored in a field called stats
on the Character
class. This field is an array. To search through an array, we can use a regular foreach
and if
but there's a feature in C# called Linq to help us filter and sort through collections. First, we must import the namespace to use it.
Next, we can use the Where
method to loop through the array. On each iteration, the function passed into the Where
method will be called where we must return a boolean. If true
, the current item in the loop is stored in the results. Otherwise, the element gets discarded from the new array.
Lastly, we're chaining the FirstOrDefault
method since we're only interested in the first result from the array. If nothing is found, null
is returned by our method.
Property Getters and Setters
In this lecture, we learned how to create flexible getter and setter accessors. We can update the get
and set
keywords to functions to refine their behavior. We did so with the StatValue
property since we want to be able to set it from outside the class.
To use this syntax, both the get
and set
keywords must be converted into functions. For the get
keyword, we're returning a field called _statValue
. A few things worth noting about this:
Behind the scenes, C# creates a field for you using auto-properties. However, if you plan on using this syntax, you are responsible for creating a field that will actually store the value.
It's common practice for a private field that a property will use to start with an
_
character and be camelcased.
In the set
accessor, we're clamping the value with the Mathf.Clamp
method. This method makes sure that a numeric value stays within a range. It accepts the value to clamp, a minimum and maximum threshold.
In this example, we're using the value
keyword, which represents the value that the property is being set to from external sources.
Animation Method Tracks
In this lecture, we learned about method tracks, which are a feature to call methods from a specific frame in your animation. There are no notes for this lecture.
Moving Nodes By Local Position
In this lecture, we moved a node by its local position. We have a hitbox that needs to be moved left or right depending on where the enemy is facing. In addition, it needs to be relative to the player. To move a node by its local positon, we can modify a node's Position
property.
In this example, we're setting the Position
property to a variable called newPosition
. To calculate the position, we used the constants Vector3.Left
and Vector3.Right
. These constants store the direction of left or right, respectively. They're available on the Vector3
class by default.
Toggling Collision Shapes
In this lecture, we learned how to disable collision shapes to prevent them from detecting characters. We can do so by toggling the Disabled
property. We defined a method that will take care of setting this property when called. It accepts a boolean as an argument.
Working on the Enemy Attack State
In this lecture, we worked on the enemy attack state. There are no notes for this lecture.
Moving the Enemy Hitbox
In this lecture, we moved the enemy's hitbox to the player's position so that it'll always hit them when the enemy enters the attack state. In the EnterState
method, we grabbed the player by using the Area3D
method called GetOverlappingBodies
, which returns a list of bodies inside the area node.
Afterward, we toggled the hitbox when the PerformHit
method was called from the animation player node.
Finishing the Enemy Attack State
In this lecture, we finished the attack state by determining whether the enemy should attack again or transition away. To perform this check, we waited for the animation to finish from the AnimationFinished
signal.
There are a few things worth mentioning about this bit of code.
We're checking if the area node has any bodies by using the
GetOverlappingBodies
method.If not, we're checking if the chase area has any bodies. If the player is there, we'll transition into the chase state. Otherwise, we'll transition into the return state.
If the player is still in the attack area, we'll perform the attack animation again.
The target position gets updated so that the hitbox will get moved when the attack lands.
Lastly, we're flipping the enemy in the direction of the player when the attack starts.
Character Death States
In this lecture, we prepared the death states for both characters. To notify each character that their death state should be played, we created a field with the Action
type. The Action
type allows us to store functions/methods in a variable.
This action will be used for when a stat reaches zero. We can run any method that gets stored in the field by calling the Invoke
method.
We're using the ?
operator to check if there's any methods stored to begin with. Otherwise, C# would throw errors if attempted to call a method that didn't exist. It can be a simple way to avoid errors.
During this process, we had to update our state machine if it attempted to transition into a state it's already in.
In this example, we're checking if the currentState
field is a specific data type. If it is, that means we're already in the state and we don't have to transition into it again.
Last updated