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.
Unit Health
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.
In our 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.
Health Bar
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 Rect.size
to (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 #414141
(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 ColorRect
s we attached as children to the Health
node, so let’s get those references with the onready
keyword.
# 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 current_hp
or 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: _calculate_color()
and _calculate_scale()
.
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)
The _calculate_scale()
function is much more straightforward — all we have to do is set the scale to the result of _get_hp_percent()
!
func _calculate_scale():
_hp_bar.rect_scale.x = _get_hp_percent()
func _get_hp_percent():
return float(current_hp) / float(max_hp)
Verifying
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 tool
script.
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_bar
and _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 current_hp
and 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
Inside the unit.gd
script let’s define a custom signal
called hp_changed
.
# 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 Health
node.
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 unit_actions.gd
script.

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.
Leave a Reply