For this part we’re going to implement a unit’s current health, as well as a way to lower and raise it and display it. This will act as our basic health system going forward for when units fight each other.
First of all, we know we want our units each to have their own health because we want them to be able to take damage and be defeated. We currently have a
health variable defined in our
UnitData, but that shouldn’t act as the unit’s current health because we’re treating it as an immutable resource (at run-time). Instead we’re going to have to create an instance variable on our
Unit class that will keep track of their current health, and use
UnitData.health as the “max health” instead.
unit.gd script let’s create a new variable
health, and initialize it in the
_ready() function, and also create a
take_damage() function that will reduce the health by the given amount when called, and then let’s also create a
heal() function while we’re at it that does the opposite.
var hp: int func _ready(): self.hp = unit_data.health func take_damage(amount: int): self.hp -= amount func heal(amount: int): # We don't want to overheal! self.hp = [hp + amount, unit_data.health].min()
Easy peasy! Now we just need a way to indicate what a unit’s current health is. The most common way to do that is by using a health bar, so that’s what we’ll do.
Open up the
Unit scene and create a new child
Node2D and name it “Health.” I’m going to position it at
(-10, 6) so that the health bar will appear under the unit, but feel free to position it anywhere you want. The point that you put it at will be the top left corner of the health bar, so keep that in mind.
Now as a child of the Health node create a
ColorRect node and set the
(20, 3). (You can also set the
Control.margin for the same effect.) This is going to be the “background” of the health bar, so I’m going to make it a neutral gray color
rgb(65, 65, 65)).
Then as a child of the
ColorRect node we just created, create another
ColorRect and give it the same with as the parent, but one less point vertically, so it will be
(20, 2). This
ColorRect will be the color of our health bar, so I’m going to set it to a green color for right now.
When you’re all finished you should have something that looks similar to this.
Coding the bar
In order to update this health bar when the unit’s current health changes we’ll need to make a simple script to control the bar color and bar width. Let’s create a new script called
hp_bar.gd and assign it to the
Health node, then open it in your editor of choice.
The first things we’ll want a reference to are the two
ColorRects we attached as children to the
Health node, so let’s get those references with the
# I named the background bar "HpBar" and the color bar "HpColor" # for easy reference onready var _hp_bar = $HpBar onready var _hp_color = $HpBar/HpColor
Personally I like the health bar to have 3 distinct colors, one for when the unit is near or at full health, one for around 50% health, and another for when the unit is close to that deep dark sleep from whence no traveler returns… So I’m going to create three exported
Color variables in my script that will represent these states respectively. (These colors were ones I liked when tweaking the color wheel)
export(Color) var healthy := Color("#20ea40") export(Color) var wounded := Color("#eae220") export(Color) var critical := Color("#e74322")
The last thing we’ll need to know is how full we should display the health bar. For that we’ll need the ratio of what the current health value is over what the maximum health value is. Once we calculate that all we need to do is set the X scale of the background bar to that value.
So let’s do that in the code! Make a function in the script and name it something like
_update_hp_bar(). This is the function that we’ll run whenever the
max_hp changes. In order to make the code more readable I’m also going to create two more functions that will handle the change to both color and scale separately:
Calculating the color
Because we’ve identified three separate colors we want the health bar’s color to swap between, we’re going to need to set cutoff points for where those colors should be activated. As far as the actual color that’s displayed is concerned, we can go about it in two ways:
- Lerp (linearly-interpolate) between the colors depending on the current percent health
- Just set the color at the corresponding cutoff point
Method #2 is much more straightforward but I like the look of the health bar gradually changing color instead of abruptly changing color, so we’re going to implement the first method.
# The cutoff values can be whatever value you decide between 0 and 1, # mine are .95, .5, and .2 func _calculate_color(): var percent := _get_hp_percent() var cutoff_diff: float if percent >= HEALTHY_CUTOFF: _hp_color.color = healthy elif percent < CRITICAL_CUTOFF: _hp_color.color = critical elif percent >= WOUNDED_CUTOFF: cutoff_diff = HEALTHY_CUTOFF - WOUNDED_CUTOFF # This line means: "depending on the value given, return a color # between 'wounded' and 'healthy' where 1.0 == 'healthy' and 0.0 == 'wounded'" _hp_color.color = wounded.linear_interpolate(healthy, (percent - WOUNDED_CUTOFF) / cutoff_diff) else: cutoff_diff = WOUNDED_CUTOFF - CRITICAL_CUTOFF _hp_color.color = critical.linear_interpolate(wounded, (percent - CRITICAL_CUTOFF) / cutoff_diff)
_calculate_scale() function is much more straightforward — all we have to do is set the scale to the result of
func _calculate_scale(): _hp_bar.rect_scale.x = _get_hp_percent() func _get_hp_percent(): return float(current_hp) / float(max_hp)
In order to test that our script behaves how we want without needing to run the scene, guess what we’re going to do?? That’s right, say it with me now! Make a
tool script! Well, we’ll actually be updating our
hp_bar.gd script to a
Once we’ve done that, let’s also add a setter method using
setget for all of our exported variables so that we can see updates in real-time when changing these values. Here are what all of those functions will look like:
func _set_max_hp(newVal): max_hp = newVal if max_hp < current_hp: current_hp = max_hp _update_hp_bar() func _set_hp(newVal): if newVal > max_hp: newVal = max_hp current_hp = newVal _update_hp_bar() func _set_healthy_color(newVal): healthy = newVal _calculate_color() func _set_wounded_color(newVal): wounded = newVal _calculate_color() func _set_critical_color(newVal): critical = newVal _calculate_color()
Also, because we set our
_hp_color variables as
onready variables, we need to check in our
_calculate_color() function whether or not it has been set, and if not, to set it.
func _calculate_color(): # I ran into some weird errors when running this code but this # check seemed to fix it if not is_inside_tree(): return if not _hp_color: _hp_color = $HpBar/HpColor # ... func _calculate_scale(): if not is_inside_tree(): return if not _hp_bar: _hp_bar = $HpBar
And that’s it! When you return to the editor you should be able to see your changes happen in real-time!
Syncing With The Unit
Now that we have a working health bar, let’s make it work in tandem with the unit.
The simple way to do this would be to get a reference to the
Health node from our
unit.gd script and just update the
Health.current_hp whenever the unit’s HP changes, but that creates redundancy (having two variables representing the same thing) and coupling (adding an explicit dependency to
Unit means the
unit.gd script can’t run in isolation without modification). Instead we’re going to follow the Godot pattern of utilizing signals.
First we’ll get rid of the
max_hp variables inside the
hp_bar.gd script. That script will not store any outward-facing state besides what it has direct control over (the colors). Instead we’ll create a listener method in the script called
_on_hp_changed(), which we will connect to a
signal from the
Unit it’s attached to, as well as create a private variable
_percent to keep reference of the percent to show.
# hp_bar.gd var _percent := 1.0 func _on_hp_changed(newHp, maxHp): if maxHp == null: _percent = 1.0 else: _percent = float(newHp) / float(maxHp) _update_hp_bar() # Also update all references in this script from the old _get_hp_percent() # to reference _percent instead
unit.gd script let’s define a custom
# Emits with parameters: newHp, maxHp? signal hp_changed
And let’s also make a setter function that emits
hp_changed whenever the unit’s hp… changes.
var hp: int setget _update_hp # ... func _update_hp(newValue): if newValue == null or newValue == hp: return hp = newValue if unit_data: emit_signal("hp_changed", newValue, unit_data.health) else: emit_signal("hp_changed", newValue, null)
Connecting the signal
Now that all of the code is set up we can go back to the editor and connect the
hp_changed signal to the
_on_hp_changed() function on our
To connect a signal in the editor, click on the node that emits the signal you want to listen for, click on the “Node” tab (to the right of the “Inspector” tab), find the signal you’re looking for in the list of signals displayed there, and double-click on it. This will bring up a dialog that looks like the following image. Select the
Health node and change the “Receiver Method” to “_on_hp_changed” and click “Connect.”
Back in the
MovementDemo scene we can verify if all of our code is put together correctly. Playing the scene you should see that pressing the “Attack” button and then clicking on a unit will reduce its health by however much you specified in the
And there we go! Two actions down! I probably won’t be writing a post or two for each separate action option from here on out, but I will give a summary of what I did.
As promised, the code for this post (and the previous two posts) can be found here.