disclaimer

Although I have quite a bit of programming experience in high level languages, I may not be qualified to teach you anything about game development as I am learning myself and I probably do not have a sense of how code should be written just yet. So please keep in mind that I am very new at this as well and don’t take it as the definite way to do it. It is only my way

overview

project files

Before you continue make sure you have the project files from the last part as you will need them here if you want to follow along!

overview 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: _init, _ready, _process etc.

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.

autoloaded scripts 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.

live scene tree Fig. 03. Scene tree at run time. Notice how the StreamPlayer sp node is attached to root, at the same level with our Game node!

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. preload(), unlike 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!

destructible obstacles

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 Inspector panel: sprite and hp. 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 Obstacle node. 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, __time, __time_total and __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, ''])

In the _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_PATH and 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 true or false as we shall see. Its purpose is to reset __time to 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:

  1. turn on processing for this node so we can drive the shake animation
  2. __time is reset to 0
  3. damage (which is given on function call) is subtracted from hp
  4. if hp is less than or equal to 0 then 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
  5. if damage sprite was not set (verified by checking __sprite_damage_set) then set the damaged texture on the parent Sprite node. 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)

obstacles Area2D scene tree Fig. 04. Scene tree after setting the obstacle group on the Area2D nodes under Obstacles

Inside of the _process() function we increase __time with 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 x and 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:

  1. to the self.__pos_start variable we
  2. add 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 0 and 2 * self.__amplitude, so [0, 6] in our case, then we
  3. subtract Vector2(1, 1) * self.__amplitude which 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 [-3, 3] interval

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.

random gotcha, get it?… random :)

OK, you should know something about working with random numbers in Godot. If you use for example randf() repeatedly and for example plot their return value, we’d notice that every time we run the game we get the same exact sequence of numbers. What is happening? Shouldn’t these things be random numbers? Different each time we run the game? Well, no!

The randomize() function sets the random number generator seed built inside Godot to a random value (based on the hardware clock and some funky math magic). This ensures that every time we play a new game, a random sequence of numbers is generated when using functions such as randi(), randf(), rand_range() etc. If we won’t use this randomize() function then Godot will generate the exact same sequence of numbers when calling randi() etc. functions. Why would this be useful you ask? Well for debugging purposes. By generating the same “random” sequence of numbers every time you can debug certain parts of the algorithm easier. So in the end the random number generators are not really random, they’re more pseudo-random number generators and this is a good thing! As we want to have predictability when debugging our algorithms.

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 StreamPlayer. 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 bottom panel Fig. 05. SoundLibrary panel with the SFX already added

Select the 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.

chopping state 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 Moving to Chopping (but not a transition from Chopping to Moving) and we’ll also have a transition from Chopping to 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):

  1. add a new variable at the top like so: onready var __sample_player = self.get_node('/root/Game/SamplePlayer')
  2. the if statement on line 30 from the 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 footstesp01 or 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 Node) under 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

  1. We turn off input processing so we don’t process the input while playing the animation
  2. next we turn on the fixed processing since we’ll need it for the animation
  3. play the chop sound from the SoundLibrary (just like we did for the footsteps)
  4. we’ll need to pass around the name of the animation so let’s store it in the animation_name variable
  5. here start playing the chop animation
  6. get the SpriteFrames resource from our AnimatedSprite node. We’ll need it to get some information that will help with calculating the duration of the animation
  7. get the number of frames in our animation
  8. and the animation speed which is the FPS (frames per second) basically, for the given animation
  9. reset the elapsed time to 0 (__time)
  10. 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 Item.gd add onready var __sample_player = self.get_node('/root/Game/SamplePlayer') at the top variable list & at the end of __on_area_enter() add self.__sample_player.play('%s%02d' % [self.get_groups()[0], int(rand_range(1, 3))]). Yes, exactly the same stuff we’ve been using for playing the other sounds.

closing remarks

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!

project files

Get the zipped finished project for this part with annotated scripts and see if you could follow along! Or if just want to consult them.