Table of contents
Fig. 01. Overview of state of the game at the end of this tutorial part
This should be a fun one. We’ll get to add a new state in our FSM, we will get to see (very simply) what the singleton OOP pattern is and how it’s useful in Godot through autoloaded scripts, a bit about sound and music and removing objects from the scene tree with a bit of procedural generated feedback for the user!
music (autoloaded scripts & singletons)¶
We’ll start off with this part as it is arguably the easiest. First, the singleton pattern is considered a harmful pattern that should be avoided (at all costs!). Well, more or less, it has it’s uses although very limited. Basically a singleton is a class that can be instantiated once and only once. No more than one object of the same type can exist at any given moment. I’ll leave it to the masters to explain more about it and why and where this might be used in a general sense. Just follow the links.
As for Godot, the engine uses the singleton pattern for autoloaded scripts, i.e. scripts that are not attached to the nodes from the scene tree available through the editor, but which are loaded automatically by Godot at the start of the game. They can be accessed from any script and they have the useful property that they are loaded once and only once at the start of the game. This means that even if we transition to other scenes or reload the current scene, the autoloaded script doesn’t reload. So in a sense it behaves like the
Globals object. Also, if you were wondering, this
Globals is also a singleton. But autoloaded scripts have the added benefit that they can execute any code, not just store data, compared to
Globals. And since the autoloaded scripts are in fact nodes (they must explicitly inherit from a base node type - such as
Node for example) they expose all of the regular functionality:
For our purpose, we’ll be using them for the background music since we don’t want the music to restart every time the player finishes the level and triggers the scene reload. So using autoloaded scripts let’s us play music without interruption even through scene changes. Quite useful.
Fig. 02. Autoloaded scripts window with the
StreamPlayer.gd script ready for action
Alright. To instruct Godot to load up these singletons when the game starts we need to access the project settings (
Scene > Project Settings), switch to the
AutoLoad tab, give the appropriate path and fill in the
Node Name: text box with something simple since this will be the variable name of this node accessible from anywhere and finally hitting the
Add button. Fig. 02 shows this window with the
StreamPlayer.gd script ready to be autoloaded by Godot.
The contents of the
SamplePlayer.gd script is very simple and pretty much self-explanatory:
extends StreamPlayer func _ready(): self.set_stream(preload('res://Assets/Audio/music.ogg')) self.set_loop(true) self.play()
So, as mentioned, the autoloaded scripts have to actually extend a certain node type. In this case we use the
StreamPlayer node which is useful for playing, as you might have guessed, music.
Fig. 03. Scene tree at run time. Notice how the
sp node is attached to
root, at the same level with our
The only notable part of this script is the use of the
preload() function. This, along with the
load() function are used to load resources in Godot from code, such as images, audio files, fonts, even other scripts etc.
load(), gets the resource into Godot at during script parsing, which basically means that it will load the resource into memory when the game starts, unlike
load() which will load it dynamically during execution. This has another implication.
preload() can only be used with constant expressions since it needs to know what to load at parse time.
Finally, I’d like to show you exactly what happens with the autoloaded script at runtime. Fig. 03 shows part of the scene tree that is generated at run time. As you can see our autoloaded script is there, the
sp node located at the same level with our
Game node. I remind you that the panel showing this scene tree at run time is located in the bottom
Debugger panel, under the
Remote Inspector tab and is very useful!
Let’s have some fun with this one shall we? In the original Unity tutorial the player knows that he/she attached the bushes only once, at the first attack (which we call chop here… for reasons). The sprite changes to a damaged look of the same bush in order to give feedback to the player but only on this first attack. It seems rather boring so let’s add a bit more life to it through the use of some procedurally generated displacement (as in Fig. 01 - check when the bushes are chopped by the player).
Alright, let’s get the script out of the way first. Create a new script called
Obstacle.gd (you know by now where to save it). This script will be attached to the
Area2D nodes under the
Obstacles collection. We’ll go over it section by section as it introduces a few new ideas. First, let’s declare the variables and what to inherit from:
1 2 3 4 5 6 7 8 9 10 11
extends "Area2D.gd" const SPRITE_PATH = 'res://Assets/Sprites/TileSet/%s%s.png' export var sprite = '' export var hp = 3 onready var __parent = self.get_node('..') onready var __pos_start = self.__parent.get_pos() var __sprite_damage_set = false var __time = 0 var __time_total = 0.1 var __amplitude = 3
First thing to note is that we extend from
Area2D.gd, the script we wrote a while back for the generic
Area2D nodes attached to different objects, such as the walls, player, enemy etc. which includes the functionality that dynamically calculates the size of the collision shape at execution time based on the tile size.
Next there’s a bunch of variables & a constant we’ll be needing.
SPRITE_PATH is a string that points to the appropriate sprite image file, but what are those funny looking symbols like
%s? That’s part of string formatting. Those familiar with the
printf C/C++ function should already know a bit about this. The neat thing about it is that the strings can be formatted and assigned to variables, not just for printing purposes. At this point I urge you to visit the godot docs for string formatting to get a better understanding of how this works as I’ll not go over the details.
We then have a couple of variables that are made available in the
sprite is a
String that we’ll be using to define the PNG file name (without extension) for the appropriate sprite. For example, this
Sprite property will have the value
obstacle03 for the
TileSet/Obstacles/3/Area2D node (as
Obstacle.gd will be attached to these
Area2D nodes as we’ll see later). Notice how the number in
obstacle03 matches the number from the sprite nodes under the
hp is, as you imagine, the hit points of our obstacles. This is also exported to the
Inspector panel for easy experimentation, furthermore, with this, different obstacles can have different hit points if we choose so.
By now you should know what the
__parent variable is for, I’m not going to cover it again. Next, we’ll get the position of our obstacle as it was placed during the board creation step. We’ll need this when making the bush (obstacle) shake when it was chopped by the player. This will add a nice feedback effect that will let the player know what it is he/she is attacking.
__sprite_damage_set is… a flag variable. I did mention that I hate flag variables, didn’t I? Well this one is no different, but in this case is really harmless. We’ll be using it for checking if the damaged image of the bush was already loaded so we won’t load it needlessly each time the player chops a bush.
The last three variables,
__amplitude are used for the shake effect. With
__time we’ll keep track of the elapsed time since the player attacked the bush (in seconds),
__time_total is the animation time duration (in seconds) and
__amplitude is the amount the bush will jump around relative to the initial position (
__pos_start), in pixels.
func _ready(): self.__parent.get_texture().load(SPRITE_PATH % [self.sprite, ''])
_ready() function we simply get the texture resource of the parent
Sprite node (remember that
Area2D is under a
Sprite node in our design) and using the
load() method it loads the appropriate PNG file from the disk by creating the string value based on
sprite values. Just for clarification purposes, let’s say we have the following string:
'%s%s.PNG. Then by using the string formatting rules from Godot, we could for examples say
var p = '%s%s.PNG' % ['texture01', 'dmg'] resulting in
p having the value
texture01-dmg.PNG. The first
%s is replaced by
texture01, while the second
%s is replaced by
dmg. In our code, we replace the first
%s with the value of the
sprite variable (which we export and set in the
Inspector panel), while the second
%s is replaced by the empty string
1 2 3 4 5 6 7 8
func take_damage(damage): self.set_process(true) self.__time = 0 self.hp -= damage if self.hp <= 0: self.__parent.queue_free() if not self.__sprite_damage_set: self.__parent.set_texture(load(SPRITE_PATH % [self.sprite, '_dmg']))
Next we have a “public” function,
take_damage which takes a
damage parameter and which returns either
false as we shall see. Its purpose is to reset
0 and check if
hp is below or equal to
0. If it is, then we remove the object from the scene tree, if not and if player attacks this object for the first time, then we switch the sprite texture to the damaged version. It also starts
_process which will drive the procedurally generated shake animation feedback and it will return
false if the object isn’t to be removed from the scene tree and
true otherwise. Let’s go over it line by line:
- turn on processing for this node so we can drive the shake animation
__timeis reset to
damage(which is given on function call) is subtracted from
hpis less than or equal to
0then we remove the parent node (of this
Area2D, so the
Sprite) from the scene tree.
queue_free()is a function that instructs Godot to remove the node only when it’s safe to do so, unlike
free()for example which would remove it immediately
- if damage sprite was not set (verified by checking
__sprite_damage_set) then set the damaged texture on the parent
Spritenode. As you can see, here we format the string with
% [self.sprite, '_dmg']instead of just
% [self.sprite, '']
If you go to the
Assets/Sprites/TileSet folder you’ll see that for every
obstacle0*.png file there’s a
obstacle0*_dmg.png file. Check them out if you don’t remember what they are for, but basically
obstacle0*_dmg.png is the damaged version of
obstacle0*.png and we load that in line
10 when the bush is attacked by the player the first time.
Finally, we have the procedural shake:
1 2 3 4 5 6 7
func _process(delta_time): self.__time += delta_time self.__parent.set_pos(self.__pos_start + Vector2(randf(), randf()) \ * 2 * self.__amplitude - Vector2(1, 1) * self.__amplitude) if self.__time >= self.__time_total: self.__parent.set_pos(self.__pos_start) self.set_process(false)
Fig. 04. Scene tree after setting the
obstacle group on the
Area2D nodes under
Inside of the
_process() function we increase
delta_time to keep track of the elapsed time since processing was turned on in
take_damage(). Next comes the important part. Line
3 basically defines the shake effect. Every frame, the parent
Sprite is set to a random position relative to the initial position (
__pos_start) by generating a random 2D vector with
y components both having values between
[-__amplitude, __amplitude], that is to say, if
__amplitude has the value of
3 (which it has interestingly in our case) then the components will have values inside the interval
[-3, 3]. Let’s see how this is done:
- to the
Vector2(randf(), randf()) * 2 * self.__amplitude.
randf()is a function that returns a pseudo-random number in the interval
[0, 1]so this vector will have components with values between
2 * self.__amplitude, so
[0, 6]in our case, then we
Vector2(1, 1) * self.__amplitudewhich is basically the vector
Vector2(slef.__amplitude, self.__amplitude)so in our case:
Vector2(3, 3). Subtracting this vector gives us a final value of a vector with components inside the interval
[-3, 3]as mentioned before. If we could write
[0, 6] - [3, 3]for an interval math then that’s exactly what this does, resulting in the
So for every passed frame, the bush jumps in a square around
__pos_start depending on
__amplitude. This goes on until
__time runs out, i.e. is more than
__time_total when we set the position back to
__pos_start and turn off processing.
Now that we have the script ready, we need to modify the obstacles form our
TileSet scene. So open
TileSet.tscn if it isn’t opened yet and select all of the
Area2D nodes under
Obstacles and select
Load from the
Script property menu from the
Inspector panel and search for the
Obstacle.gd script and select it.
This next part has to be done for each
Area2D under the
Obstacles node. Select them one by one and in the
Node panel (the tab near the
Inspector) select the
Groups tab and add
obstacle as the group name. Do this for all of the other
Area2D nodes under
Obstacles. We need this because the player object interacts with the outer walls as well as the inner bushes and we need a way to distinguish between them.
At the end of it all you should have the scene tree as in Fig. 04.
Before moving on be sure to add
randomize() to the
ready() function in
Game.gd! It’s pretty important if we want to get different random sequences and for a brief explanation check the gotcha above. In all honesty, for this procedural shake effect it isn’t that important, but it’s good to be aware of it.
adding the chop state, transition & SFX¶
A bit of node set-up first for preparing the SFX for different actions including chopping, walking, etc.
Game.tscn should be already opened, if not open it, we’ll be adding a
SamplePlayer node under the
Game node. This will be responsible for storing and playing the SFXs. Note that this is different from the
SamplePlayer is optimized to work with a library of sound effects (short duration sounds), while the
StreamPlayer is for… streaming music from disk or memory for example.
After adding the
StreamPlayer under the
Game node, select
New SampleLibrary under the menu from the
Samples property from the
Inspector panel. Next select
Edit from the same menu or press the
> symbol for this
Inspector property to edit the
SampleLibrary resource. At this point a panel should open at the bottom of the screen called
SampleLibrary (Fig. 05).
SoundLibrary panel with the SFX already added
Open Sample File(s) (the only button on this
SampleLibrary panel) and navigate to
Assets/Audio and select all of the
*.wav files, finally press
Open. At this point we have all of the SFX files loaded in the
SoundLibrary resource set to be used by the
SamplePlayer node. We can also preview the sounds by pressing the for the appropriate row.
Fig. 06. New
Chopping state & possible transitions
Now we’re ready to add the
Chopping state and add the appropriate transitions (Fig. 06).
Chopping will only happen when the player tries to move on a tile populated with a node from the
Obstacles list (so for example not the walls). So we’ll have a transition from
Chopping (but not a transition from
Moving) and we’ll also have a transition from
Idle which will take place after the chop animation finishes. With this, we’re ready to move on to the code. First, let’s modify the
Moving.gd script so it includes the transition to the new
Chopping state and play the SFX when moving to a new tile (walking):
- add a new variable at the top like so:
onready var __sample_player = self.get_node('/root/Game/SamplePlayer')
ifstatement on line
enter()function will now become:
if entity.ray_casts[delta_xy].is_colliding(): var collider = entity.ray_casts[delta_xy].get_collider() var state_name if entity.is_in_group('player'): if collider.is_in_group('obstacle'): collider.take_damage(entity.damage) state_name = 'Chopping' else: state_name = 'IdlePlayer' elif entity.is_in_group('enemy'): state_name = 'IdleEnemy' entity.transition_to(self.__parent.get_node(state_name)) else: self.__sample_player.play('footsteps%02d' % int(rand_range(1, 3)))
This is pretty self-explanatory. The only notable piece of code is the
collider.take_damage(entity.damage) line, where we call the
take_damage() method from the
Obstacle.gd script. Basically, if the player wants to move to a tile occupied by an bush the
take_damage() method is called on the bush, passing in the damage amount is should take, which is stored in the player script. Which reminds me. Open up the
Actor.gd script and add
export var damage = 1 at the top, this is the
entity.damage variable basically, so our modification to
Moving.gd wouldn’t work without it!
Finally, note that, if the player object doesn’t collide with anything and the move transition takes place, (the top
else branch) we play either
footsteps02 from the
SoundLibrary. Check out the documentation for string formatting if you haven’t done so in order to understand the
self.__sample_player.play('footsteps%02d' % int(rand_range(1, 3))) line! Also you might want to check
rand_range() in the docs. Figure this one out! It isn’t that complicated :). I know you can do it!
Now the last piece of the puzzle, the
Chopping.gd script and the
Chopping state! In the
Game.tscn scene, add a new node (of type
States and add a script to it. Name it
Chopping.gd and save it under
res://Scripts/States. Now let’s go over it:
extends 'Base.gd' export var energy_cost = 5 onready var __sample_player = self.get_node('/root/Game/SamplePlayer') var __time var __time_total
I thought it should be appropriate to add a cost for stopping & chopping the bushes. That’s what the
energy_cost is for. We’ll also need a reference to the
SamplePlayer to play the chopping sound. Finally we’ll need to play the chopping animation, but we need to figure out for how long. Since we’re not using the
AnimationPlayer for this we’ll need to calculate ourselves the duration of the animation and stop the process manually. Should be fun, we have all the tools! There are a number of things happening in the
enter() function so let’s go over it:
1 2 3 4 5 6 7 8 9 10 11
func enter(entity): entity.set_process_input(false) entity.set_fixed_process(true) self.__sample_player.play('chop%02d' % int(rand_range(1, 3))) var animation = 'chop' entity.play(animation) var sprite_frames = entity.get_sprite_frames() var frame_count = sprite_frames.get_frame_count(animation) var animation_speed = sprite_frames.get_animation_speed(animation) self.__time = 0 self.__time_total = frame_count/animation_speed
- We turn off input processing so we don’t process the input while playing the animation
- next we turn on the fixed processing since we’ll need it for the animation
- play the chop sound from the
SoundLibrary(just like we did for the footsteps)
- we’ll need to pass around the name of the animation so let’s store it in the
- here start playing the chop animation
- get the
SpriteFramesresource from our
AnimatedSpritenode. We’ll need it to get some information that will help with calculating the duration of the animation
- get the number of frames in our animation
- and the animation speed which is the FPS (frames per second) basically, for the given animation
- reset the elapsed time to
- and finally calculate the animation duration time based on the number of frames and FPS
Our last part is the
update() function which is self-explanatory so I’m not even gonna go over it. You should understand it by now on your own! It is a very good exercise so give it a try. If you don’t understand something, change it, play with it, try it out and see what brakes, what works!
1 2 3 4 5 6 7 8 9 10
func update(entity, delta_time): self.__time += delta_time if self.__time >= self.__time_total: var state_name if entity.is_in_group('player'): entity.energy -= self.energy_cost state_name = 'IdlePlayer' elif entity.is_in_group('enemy'): state_name = 'IdleEnemy' entity.transition_to(self.__parent.get_node(state_name))
Alright I lied, there’s one last part :). We need to play the sound when the player picks up the items (soda & fruits). Very simple, in
onready var __sample_player = self.get_node('/root/Game/SamplePlayer') at the top variable list & at the end of
self.__sample_player.play('%s%02d' % [self.get_groups(), int(rand_range(1, 3))]). Yes, exactly the same stuff we’ve been using for playing the other sounds.
I hope this was an interesting read and that you learned a little about procedural (random) processes, how to improve things by providing a little feedback to the player, about singletons and autoloaded scripts in Godot. Finally we covered the
SamplePlayer and playing music with the
StreamPlayer and why it’s useful to have the
StreamPlayer in an autoloaded script. All in all I think we covered a lot of ground!
As usual, let me know what you think about these tutorial by leaving a comment! See you in the next part where we’ll implement the “brain” for our AI enemy!