Adding Juice to a Zelda-Style Heart System with Python/Godot
Previously, I created a tutorial walking you through how to create a basic Zelda-Style Heart System in Godot. This project allowed you to create a grid of hearts that increase and decrease in value to show different sprites just like Zelda. While this works perfectly fine, it’s still stagnant and doesn’t quite feel right. It’s time to take things to the next level by adding what game developers love to call juice . In this tutorial, we’ll be adding two animations to our hearts: pulsing and flashing. By the end of this tutorial, the active heart—the furthest heart along in the array—will play a heartbeat animation and all non-empty hearts will flash while the player is considered to be at low health.
Before You Begin
This tutorial is a follow-up to a previous tutorial I created, which you can find here . In that tutorial, we created the Heart HUD which displays 4-part hearts in a grid, mirroring the Zelda-like style. Prior to completing this tutorial, you should ensure you have the basic components of that tutorial set up.
Complete Project Download
If you just want to comb through the finished project, you can grab it here .
Adding Animations
For our juice in the system, we need two animations and two animation players, each with a unique animation capable of playing independently from or conjunction with the other.
Adding the Animation Nodes
-
In the lower left-hand sidebar, in the
FileSystem
tab, double-click the
player_ux_heart.tscn scene. The
player_ux_heart scene will open.
-
In the upper left-hand sidebar, in the
Scene
tab, right-click the
Heart node, then click
Add Child Node… The Create New Node window will appear.
-
In the
Create New Node
window, in the
Search
field, type “AnimationPlayer”. The
AnimationPlayer node will be highlighted in the list of matches.
-
Double-click the
AnimationPlayer node in the list of matches. The
AnimationPlayer node will appear as a child of the
Heart node.
-
Right-click the new
AnimationPlayer node, then click
Duplicate . A copy of the node will appear with the name
AnimationPlayer2 .
-
Right-click
AnimationPlayer , then select
Rename . You will be prompted to rename the node.
- In the node’s name field, rename it “FlashAnim”.
-
Repeat steps 6-7 to rename
AnimationPlayer2 to “PulseAnim”.
Setting Up the Flash Animation
The Flash animation is an animation that will play on any hearts that are not currently empty while the player’s health is considered to be low. For this tutorial, we’ll set the heart up to flash red, but you can set it up to flash any color you wish.
-
In the upper left-hand sidebar, in the
Scene
tab, double-click the
FlashAnim . The Animation window will open at the bottom of the Godot project window.
-
In the
Animation
window, click
Animation
, then
New.. . The Create New Animation window will appear.
- In the Create New Animation window, name your animation “heart_flash”, then click OK .
-
In the
Scene
tab, select
HeartTexture .
-
In the right-hand sidebar, in the
Inspector
tab, click the
Visibility
dropdown, then click the
key icon
next to Self Modulate .
-
In the window that appears, ensure
Create RESET Track(s)
is checked, then click
Create
.
self_modulate
will be added as a track to the
heart_flash
animation in the
Animation
window for
HeartTexture .
-
In the
Animation
window, in the
self_modulate
track, right-click and select
Insert Key… . A white box will appear.
- Click the new white box that appeared and, in the right-hand sidebar, in the Inspector tab, change the Time value to “1.0”.
- In the Animation window, in the self_modulate track, right-click between the two white boxes and select Insert Key… . A third white box will appear.
- Click the center white box and change its Time value to “0.5”, then change its Value in the AnimationTrackKeyEdit section to the color you want the heart to flash. The color red is used in the example for this tutorial.
-
In the
Animation
window, next to the
Animation Length
field in the top right corner, click the
loop icon
to allow the animation to play on a loop.
-
(Optional) In upper left corner of the
Animation
window, click
Play to test your animation.
Setting Up the Pulse Animation
The Pulse animation will play perpetually, but only on the currently-active heart (the highest value heart that is not empty). This is an animation meant to resemble a heartbeat. This animation also serves to make the active heart slightly larger (0.05%) than the other hearts.
- At the top of the Godot project editor, click Project , then Project Settings… . The Project Settings window will appear.
- In the Project Settings window, change the following settings:
- GUI/Common/Snap Controls to Pixels : Uncheck this box.
- Rendering/2D/Snap 2D Transforms to Pixel : Uncheck this box.
- Rendering/2D/Snap 2D Vertices to Pixel : Uncheck this box.
-
In the upper left-hand sidebar, in the
Scene
tab, double-click the
PulseAnim . The Animation window will open at the bottom of the Godot project window.
-
In the
Animation
window, click
Animation
, then
New… . The Create New Animation window will appear.
- In the Create New Animation window, name your animation “heart_pulse”, then click OK .
-
In the
Scene
tab, select
HeartTexture .
-
In the right-hand sidebar, in the
Inspector
tab, click the
Transform
dropdown, then click the
key icon
next to Scale .
-
In the window that appears, ensure
Create RESET Track(s)
is checked, then click
Create
.
scale
will be added as a track to the
heart_pulse
animation in the
Animation
window for
HeartTexture .
-
In the
Animation
window, in the
self_modulate
track, right-click and select
Insert Key… . A small white diamond will appear.
- Click this second white diamond and, in the right-hand sidebar, in the Inspector tab, change the Time value to “1.0”.
- Repeat steps 6-7 to create new keys with the following time values in the scale track:
- 0.4
- 0.55
- 0.8
- For each key, change the Value field in its animationTrackKeyEdit section of the Inspector tab to correspond with the following:
- 0.0: 1.05
- 0.4: 1.15
- 0.55: 1.05
- 0.8: 1.15
- 1.0: 1.05
-
In the
Animation
window, next to the
Animation Length
field in the top right corner, click the
loop icon
to allow the animation to play on a loop.
-
(Optional) In upper left corner of the
Animation
window, click
Play to test your animation. You should see the heart beating.
Writing the Code
Now we need to actually tell our heart texture to play these animations based on predetermined variables. We need to figure out which heart is active, whether that heart is empty, and whether our player is at full health. After that, we’ll call a function for pulsing and flashing the relevant hearts.
Before writing the code that determines when these functions will be called, we’re going to create two blank functions for the heart_gui.gd . Blank functions provide a reference to functions we can call throughout our code. Later on, we’ll set up these functions to actually play their corresponding animations.
Adding the Placeholder Functions
- In the left-hand sidebar, in the FileSystem tab, click Scripts , then double-click heart_gui.gd . The heart_gui.gd script will open.
- In heart_gui.gd , add the following blank functions at the bottom of the #region FUNCTIONS section:
func flash(flash_state : bool): pass func pulse(pulse_state : bool): pass
Note : You may get a yellow error for the time being, since neither _flash_state nor _pulse_state are used in the function, right now. You can ignore this, as it will be resolved once we write the functions.
Making the Heart Pulse
First, let’s focus on making the active heart pulse. This will require determining which heart in the index is active . This is a straightforward line of code: all we need to do is divide our current health by 4 (ensuring we are calculating actual hearts instead of hit points), then subtract one to ensure we aren’t grabbing an empty heart.
Setting Up the Heart Pulse
- In the left-hand sidebar, in the FileSystem tab, click Scripts , then double-click heart_grid.gd . The heart_grid.gd script will open.
- At the top of the update_hearts function, where the variables are written, add the following variable:
- At the bottom of the update_hearts function, after calling heart.update(heart_fill_value) , add the following code within the for i in range(hearts.size()): statement:
var active_heart_index = int(ceil(cur_health / 4.0)) -1
var pulse : bool = (i ==active_heart_index and heart_fill_value > 0) heart.pulse(pulse)
These few lines of code communicate to each heart when the hearts are updated, checking to see if the heart is active and ensuring the heart is not empty, then passing that value as a boolean to the pulse function. Now, all that’s remaining for the pulse functionality is setting up the animation via code.
Making the Heart Pulse Animation Play
- Open the heart_gui.gd .
- At the top of the script, in the #region VARIABLES section, under the @export_category(“Heart Components”) category, add the following export variable:
- At the bottom of the #region FUNCTIONS section, replace the pass section of the pulse function with the following code:
@export var pulse_anim : AnimationPlayer
if not pulse_anim: return if pulse_state: if pulse_anim and pulse_anim.current_animation != “heart_pulse”: pulse_anim.play(“heart_pulse”) elif pulse_anim.is_playing or sprite.scale != Vector2.ONE: sprite.scale = Vector2.ONE pulse_anim.play(“RESET”)
This code both plays the pulse animation and ensures hearts reset to their default scale of 1x1 if they are somehow stuck at a larger scale due to their animation being interrupted.
Connecting the Pulse Animation Player
-
In the
player_ux_heart scene, in the left-hand sidebar, click
Heart in the Scene tab.
- In the right-hand sidebar, in the Inspector tab, you will see the Pulse Anim field appear in the Heart Components category.
-
Click the
Pulse Anim
field, then select
PulseAnim in the dropdown that appears.
PulseAnim will populate the field.
Testing the Heart Pulse
-
In the left-hand sidebar, in the
FileSystem
tab, click
Scenes
, then double-click the
test.tscn to open it.
-
In the Godot editor, in the top menu bar, click the
Run Current Scene icon
to run the test scene. The (DEBUG) window will appear.
- You should see the furthest heart in the grid array playing the pulse animation.
- Using your debug keys, decrease the health value until the active heart changes to verify the animation change functions as expected.
Making the Hearts Flash
With the active heart pulsing, it’s time to add heart flashing while health is low. This functionality will cause the heart_flash animation to play on all hearts that are not empty while the player’s health is at a predetermined low-health threshold. Determining this threshold is entirely up to you—it could be based on a flat number of hearts, such as having only 1 heart remaining (in which case, the value would be 4 health), or it could be more complex like the one in this tutorial. How you determine what constitutes ‘low health’ depends entirely on your game. This tutorial features a scaling low HP threshold based on a system where the player begins with 3 hearts and can end the game with up to 20 hearts.
Setting Up the Heart Flash
- In the left-hand sidebar, in the FileSystem tab, click Scripts , then double-click heart_grid.gd . The heart_grid.gd script will open.
- In the update_hearts function, after the hearts variable is created, add the following variables:
- At the bottom of the update_hearts function, after calling heart.update(heart_fill_value) , add the following code within the for i in range(hearts.size()): statement:
var max_hp : int = int(hearts.size() * 4) var low_hp_threshold : int = clampi(int(floor(max_hp * 0.25)), 4, 15) var hp_low : bool = (cur_health <= low_hp_threshold and cur_health > 0)
var flash : bool = (hp_low and heart_fill_value > 0) heart.flash(flash)
The formula used for the low_hp_threshold variable in this tutorial is designed for a 20-heart maximum system. However, when determining the value of the low_health_threshold , it sets it to a value equal to 1/4 of the player’s maximum hit points as long as it is within the range of 4 hit points (1 heart) and 15 hit points (3 and 3/4 hearts). This keeps the player from being warned too early or too late that their health is low.
With that calculated, we then determine whether we are at low health every time our hearts are updated. That information is passed to the hearts as a boolean value, telling the hearts whether they should or should not be flashing after their fill values are updated.
Making the Heart Flash Animation Play
- Open the heart_gui.gd .
- At the top of the script, in the #region VARIABLES section, under the @export_category(“Heart Components”) category, add the following export variable:
- At the bottom of the #region FUNCTIONS section, replace the pass section of the flash function with the following code:
@export var flash_anim: AnimationPlayer
if not flash_anim: return if flash_state: if flash_anim and flash_anim.current_animation != “heart_flash”: flash_anim.play(“heart_flash”) elif flash_anim.is_playing() or sprite.modulate != Color.WHITE: sprite.modulate = Color.WHITE flash_anim.stop()
This code both plays the flash animation and ensures hearts reset to their default coloring if they are somehow stuck at mid-flash due to their animation being interrupted.
Connecting the Flash Animation Player
-
In the
player_ux_heart scene, in the left-hand sidebar, click
Heart in the Scene tab.
- In the right-hand sidebar, in the Inspector tab, you will see the Flash Anim field appear in the Heart Components category.
-
Click the
Flash Anim
field, then select
FlashAnim in the dropdown that appears.
FlashAnim will populate the field.
Testing the Heart Flash
-
In the left-hand sidebar, in the
FileSystem
tab, click
Scenes
, then double-click the
test.tscn to open it.
-
In the Godot editor, in the top menu bar, click the
Run Current Scene icon
to run the test scene. The (DEBUG) window will appear.
- Using your debug keys, decrease the health value until it reaches your low_health_threshold value. All of the hearts that are not currently empty should begin playing the flash animation, including the active heart.
Conclusion
And that’s it for adding juice to the Zelda-Style heart system! Or… is it? There’s still so much more you could do to improve the field of this system. Animations, sound effects, and more can be added to enhance the feel of our hearts and make things even… juicier. Have a great idea for how to improve this system? Already executed your plan? Feel free to share!