What You’ll Learn in This Hour:
The relationship of objects and scripts
Basics of GDScript
Signals and groups
Many games shine because of their unique or well-polished game mechanics. Most behaviors need manual programming, which can often be
quite complex. Godot features a scripting system with which you can implement those systems, and it encourages you to apply many battle-
tested patterns of software engineering.
This fourth hour will show you how to use GDScript, Godot’s custom scripting language, to give nodes and objects custom behaviors. It
shows how to create scripts, attach them to objects and nodes, and make them interface with other objects. We will see how you can use
predefined and custom signals to decouple parts of your code and use groups to identify different objects that are related in your game code.
Node and Script Relationship
We learned about the Scene Tree and nodes in Hour 2, “Scene System.” Now we will learn how to add your own code to these nodes using
Godot’s scripting interface.
We learned there are different node types, like Node2D or Control. These different types follow the model of single inheritance. That means
every node type has a parent type from which it inherits all properties and behaviors. This is a common model in the world of object-oriented
programming. The root type for things that can be part of the Scene Tree is Node. A simple node does nothing special; it’s just the basic
skeleton an object needs to be part of the Scene Tree.
Node2D inherits Node, so every Node2D can be used where a node could be used. Often, you’ll find a notation like this:
Node2D < Node
That means Node2D inherits Node. Sprite inherits Node2D, so the inheritance order would look like this:
Sprite < Node2D < Node
You will find that notation inGodot documentation.
Node is not the end of the inheritance chain. The very root type for all n
o
de types (except core types) is object. Everything inherits an
object, directly or indirectly. Because this is the base type, Godot uses this type as the starting point for the scripting system. Every object can
have a script attached to it. A script is some kind of object that can give additional behaviors to its owner (see Figure 4.1).
FIGURE 4.1
Object-script relationship.
The way your script can add behaviors to objects works as follows: the object gets asked to do task x. If the object has a script attached to it, it
asks the script if it can do task x. If so, the script gets called to do said task (see Figure 4.2). If the object doesn’t have a script attached, it sees
if it can handle it on its own.
FIGURE 4.2
Method call delegated to a script.
Introduction to GDScript
The Godot engine features its own custom scripting language called GDScript. Godot supports other ways of scripting, like VisualScript,
Mono/C#, and NativeScripts. GDScript is the most tightly integrated language of those listed above, as it was made specifically for Godot.
Many people love it for its ability to quickly prototype games.
The Basics of GDScript
GDScript’s syntax closely resembles that of Python. Every object has a type, and those types consist of class hierarchies. One GDScript file
represents one “subclass.” This means that everything you declare in the scope of the file (such as variables and functions) is part of the class
definition, and it’s not available globally to other files. Variables in this scope are called “members,” and functions are called “methods.” To
use the code in a GDScript file, you need to set that script as the object script.
A GDScript file might look like the following in Listing 4.1:
LISTING4.1 Sample GDScript File
Click here to view code image
# button_counter.gd
extends Node2D
111111111
22222222signal count_updated(count)
# a variable to hold the number of times the button has been pressed
# since this is declared in the file scope it becomes a class member
var press_count = 0
# The maximum number of button presses allowed
const MAX_PRESSES = 42
# connect the “pressed” signal to a user defined method
func _ready():
$Button.connect(“pressed”, self, “on_button_pressed”)
# this gets called when Button gets pressed
func on_button_pressed():
if press_count + 1 <= MAX_PRESSES:
press_count += 1
emit_signal(“count_updated”, press_count)
$Button.text = str(press_count)
else:
$Button.text = “No more presses allowed.”
TIP
Whitespace-sensitive Syntax
GDScript uses whitespace (space and tab character) to know which code belongs to which scope or block. This is similar to Python and
many other languages, like Haskell, CoffeeScript, or F#. This technique is called Offside-Rule.
Other languages like C, C++, C#, Java, and many others use Braces to make that distinction.
Getting Started
To attach a script, use the Attach Node Script dialog, as shown in Figure 4.3.
FIGURE 4.3
The Attach Node Script dialog.
TRY IT YOURSELF
Attaching a Script to a Node
The easiest way to add a Script to a node is to use the context menu in the scene dock:
1. In the Scene dock, click on a node to select it.
2. Click the Attach Script button.
3. The language should be set to GDScript.
4. Set a Path where the script should be saved.
5. Click the Create button.
Now the editor should show you the script that already contains some code to get you started. You can use the arrow keys to navigate around
the text editor, type letters to insert them, and delete characters. All the common shortcuts and features are available.
TIP
Virtual Methods
Many node- and object-types have Virtual Methods, which a coder is supposed to override. By overriding a virtual method, you can give
Godot an “entry point” to your custom code. A few of those virtual methods are presented here, but there are many more depending on the
node-/object-type you’re dealing with.
Life-cycle callbacks are special virtual methods that are conventionally prefixed with an underscore. They are called automatically by the
engine when a certain event happens (such as when the node is added to the Scene Tree and when the user provides keyboard or joystick
input).
_ready Method
When a node enters the Scene Tree, it first “sets up” all of its children. ThenGodot tries to call a method named “_ready,” which you can define
in a script. This function takes no argument, and you can use it to set up an object or a default state for your script.
The famous “Hello World.” program inGDScript might look like what is shown in Listing 4.2.
LISTING4.2 Hello World Program
111111111
22222222Click here to view code image
extends Node
func _ready():
print(“hello, world.”)
If you modify the script to look like Listing 4.2 and press “Ctrl + S” to save the script, you can start the scene like you learned in Hour 1,
“Introducing the Godot Engine,” and see “hello, world.” in the output tab on the bottom of the editor (see Figure 4.4).
FIGURE 4.4
Output of the “Hello World” program from Listing 4.2.
Listing 4.2 shows the statement print(“hello, world.”). That’s the syntax to call methods and built-in functions. In this case, the script
calls the print function with one argument. That one argument is a string (a string is text enclosed by double quotes).
_process Method and Using Input.is_action_pressed()
In the last chapter, Hour 3, “2D Graphics,” we learned more about the Sprite node.
Every node has a method _process that a Script can override. The _process method gets called on every new frame drawn byGodot.
In this example, we will take a Sprite node and make it move left or right based on user input.
We set up our scene like this and set the icon.png that is included in every new project as the texture of that Sprite (Figure 4.5).
FIGURE 4.5
Scene and Sprite setup for the next example.
The _ready method did not take any arguments, but the _process method does take one argument, delta. It’s the time in seconds since the last
frame was drawn. This is particularly useful to implement frame rate-independent behaviors. We’ll see how that works out in practice now.
Listing 4.3 shows the GDScript file that’s attached to the Sprite node.
LISTING4.3 Script Attached to the Sprite Node
Click here to view code image
extends Sprite
const MOVEMENT_SPEED = 50 # pixels per second
# notice that you can leave out _ready if you don’t need it
func _process(delta):
var input_direction = 0 # 0 is no movement, 1 is right, -1 is left
if Input.is_action_pressed(“ui_left”):
input_direction = -1
elif Input.is_action_pressed(“ui_right”):
input_direction = 1
# now move the sprite around
position.x += input_direction MOVEMENT_SPEED delta
Here you can see that _process takes one argument, which we called “delta.”
We define a constant named movement_speed with the value 50. The value of a constant cannot be changed, and constants are only meant to
give a name to constant values (to avoid the use of “magic numbers” in the code).
It also shows a few new concepts. The first is the declaration and initialization of a local variable using the var name = expression
syntax.
This code is using “if” statements. It works just like many other languages; it executes some code based on the condition. If there’s an else
branch, the code under it gets executed when the condition is false. elif is just a short version of else: if . . ..
TIP
Input Singleton
We learned about the method call syntax when _ready was introduced. In the previous example, some method calls are prefixed by Input.
This means they aren’t methods on the current object (where this code executes), but in a different class or object. In this case, it’s the Input
singleton. We will learn more about singletons in Hour 6, “More Scripting,” and more about Input in Hour 7, “Handling Input.”
If one of those keys is pressed, we change the local variable that we created previously. Make sure to indent the code that should only be
executed conditionally. The last line accesses the “position” property of the Sprite and increments the x component of that position. The
position of a Node2D is a Vector2. A Vector2 is a core type and consists of two float components, x and y.
A Vector2 can be used to represent a position or a direction. In more complicated games, you can use vectors to perform linear algebra,
111111111
22222222which simplifies many tasks in game creation. When the “position” property of an object that inherits Node2D gets modified, the actual position
in the scene will get changed.
So, this line moves the Sprite on the X axis in different directions depending on the input.
TIP
Complex Computations and _process
Try to avoid complex computations in _process. _process runs on every frame the engine renders; if _process is still busy following an
algorithm, the engine won’t be able to start working on the next frame.
This means if you have huge computations on every, or some, frames, the frame rate will drop.
_input and Basic Input Events
In the previous example, we used Input.is_action_pressed(. . .) in every frame to check if a key was pressed. Statistically
speaking, it’s much more common that a key is not pressed than that it is pressed. So, it might be wasteful to check for key presses all the
time. That’s what the _input(event) method is used for: it gets called whenever new events are created.
We won’t go into too much detail here, because Hour 7 will talk about this in greater detail.
If we wanted to translate the above code to use _input instead of asking for key presses every frame, we could come up with the code in
Listing 4.4.
LISTING4.4 Shifting to use _input
Click here to view code image
extends Sprite
export var movement_speed = 50 # pixels per second
var input_direction = 0 # 0 is no movement, 1 is right, -1 is left
func _process(delta):
# move the sprite
position.x += input_direction movement_speed delta
func _input(event):
# we test if the key was pressed. This will be one event
# note that this method doesn’t have the “just” word, be
c
ause
# events received in the _input callback happens only once per press
if event.is_action_pressed(“ui_left”):
input_direction = -1
elif event.is_action_pressed(“ui_right”):
input_direction = 1
# we also test if the key was released, this is also just one event
elif event.is_action_released(“ui_left”):
input_direction = 0
elif event.is_action_released(“ui_right”):
input_direction = 0
The Sprite gets moved, but the user input only gets processed when it happens (but not all of the time).
TIP
Exporting Variables
Listing 4.4 uses a member variable called movement_speed. By prefixing the variable definition with export, this variable will show up as a
property in the Property Inspector. An exported variable needs a proper default value, which will be shown in the Property Inspector.
That makes tweaking values a lot easier, and can be done by level designers or artists without having to go into the code to change values. If
you use scene instancing, every instance will have its own set of exported variables—so you don’t step on other people’s toes!
Interfacing with Other Nodes
There are some methods that can be overridden so that Godot calls your code, but all of the previous code just acts on the node to which the
script was attached! Bigger and more complex scenes require scripts to interface with other nodes in the scene.
To get a reference to a node inside the tree, you can use the get_node method. get_node is part of all nodes (since it needs access to the
Scene Tree), and it takes a path relative to the current node that points to the node you want to reference (see Figure 4.6).
FIGURE 4.6
The scene setup for the get_node path example.
For example, to get a reference to the parent node, you’d use var parent = get_node(“..”). As you can see, it’s using unix-style
paths. To access a child node called A, you would do get_node(“A”), to get the child B of A, you would do get_node(“A/B”), and to
111111111
22222222get the sibling node C, you would do get_child(“../C”).
Listing 4.5 shows an example in which a script moves a child node instead of itself. Note the single project shown in Figure 4.7.
FIGURE 4.7
The Godot Project Manager with a single project.
The GDScript shown in Listing 4.5 moves the child node called “Sprite.”
LISTING4.5 Sprite Child Node
Click here to view code image
extends Node2D
const MOVEMENT_SPEED = 50 # pixels per second
var sprite_node
func _ready():
sprite_node = get_node(“Sprite”)
func _process(delta):
var input_direction = 0 # 0 is no movement, 1 is right, -1 is left
if Input.is_action_pressed(“ui_left”):
input_direction = -1
elif Input.is_action_pressed(“ui_right”):
input_direction = 1
# notice that the child is moved here
sprite_node.position.x += input_direction MOVEMENT_SPEED delta
Because get_node is such a common operation, there is an alternative syntax. It’s $Path instead of get_node(“Path”). Table 4.1
shows how these syntaxes relate.
TABLE 4.1 Alternative Syntaxes
get_node(“A”)
$A
get_node(“A/B”)
$A/B
get_node(“A/space in name”)
$”A/space in name”
get_node(“..”)
$ “..”
get_node(“ ../C”)
$ “../C”
Calling User-Defined Methods on Other Objects
In bigger scenes, it’s really useful to call methods from scripts that are attached to other nodes. To show how that can be done, let’s consider
the following scene in Figure 4.8.
FIGURE 4.8
The script on “A” calls methods defined in the script on “B.” See Listing 4.6.
LISTING4.6 The Output
Click here to view code image
# expected output
B prints: This is a test.
B prints: Hello from A
Listing 4.7 shows the script on A.
LISTING4.7 The AScript
Click here to view code image
# A.gd
extends Node
func _ready():
$”../B”.test_method(“This is a test.”)
get_node(“../B”).test_method(“Hello from A”)
As expected, these calls go through the method defined in the script on B, shown in Listing 4.8.
111111111
22222222LISTING4.8 The B Script
Click here to view code image
# B.gd
extends Node
func test_method(arg):
print(“B prints: “, arg)
As you can see, calling functions when you know what to call is pretty straightforward, but sometimes you need to call methods on other
objects, which you don’t know about. This is done via signals, which is introduced in the next section.
Signals and Groups
Let’s now take a look at signals and groups.
Signals
Signals and groups are two mechanisms to reduce coupling in game code. Signals are an implementation of the observer design
pattern. The base concept is that an object notifies other objects that are interested in some actions (see Figure 4.9). The coder doesn’t know
when and where these actions might be of interest, so it’s not possible to just hard-code these connections to other objects. That’s where the
concept of signals helps.
FIGURE 4.9
An object emits signals; other objects connect to those signals.
An object can emit multiple possible signals. When a signal is emitted, all the “listeners” (or observers) will get notified (or more specifically, a
method gets called). It can also be seen as providing a callback to the object that is emitting the signal.
This means there are two sides a signal: emitting it and somebody “listening.”
Reacting to Signals
One simple example is detecting if a Button was pressed (see Figure 4.10 and Listing 4.9).
FIGURE 4.10
GDScript code that connects a method to a signal on the child node “Button.”
LISTING4.9 GDScript Detecting if a Button Was Pressed
Click here to view code image
extends Node2D
func _on_Button_pressed():
print(“Button got pressed”)
func _ready():
$Button.connect(“pressed”, self, “_on_Button_pressed”)
To “listen” to a signal (or provide a callback), the signal needs to be connected to a method of an object. connect expects the name of the
signal as the first argument, the object to which to connect the signal as the second argument, and the name of the method on that object as the
third argument. The fourth argument of this method is optional, and it expects an array of bindings, which will be sent as additional parameters
to the connected methods. This can be useful if you connect multiple signals to the same method (like one that handles the pressing of many
buttons), because you can add an extra parameter to identify the sender.
Instead of connecting the method to a signal in code, we can use the Godot Editor to do the same thing (Figure 4.11).
FIGURE 4.11
The Godot Editor provides the functionality to connect signals via a graphical user interface.
TRY IT YOURSELF
Connecting a Method to a Signal
To connect a method of an object to a signal on another object, you can use the editor:
1. In the Scene dock, click a node to select it.
2. In the Node dock, click Signals.
3. Double-click the signal to which you want to connect a method.
4. A new Dialog Window will pop up.
111111111
222222225. Select the object in the Scene Tree on the left that you want to connect to the signal.
6. Enter the name of the method that will react to the signal.
7. Click the Connect button.
It is possible to connect a signal to a method defined on an object other than self.
Click here to view code image
$VisibilityNotifier2D.connect(“exit_screen”, some_bullet, “queue_free”)
Sometimes, connecting signals to built-in methods, like “queue_free,” can be a real timesaver. This code will cause a “bullet” object to be
automatically destroyed after it leaves the screen thanks to the VisibilityNotifier2D Node.
Emitting Signals and Defining Custom Signals
In the previous section, we showed how to connect and react to signals. In a game project, it often is advantageous to define your own signals
to reduce coupling between objects.
One example is to use signals in order to perform some action after another action was performed. The “key_collected” animation on the
Player shouldn’t play before the “chest_opened” animation on the Chest has finished. Signals are also very useful for managing scene
changes, achievement systems, game-progress systems, and many more things to reduce complexity in the code base.
There are two ways to define a custom signal inGDScript: using the signal keyword and using Object.add_user_signal(). The
most preferred way is to use the signal keyword, as it will enable the editor to connect methods to user-defined signals just like they do to built-
in signals.
To define a signal in a script, you need to write the “signal signature” so the editor knows about it and can offer functionality like auto-
completion.
Here is an example of the syntax used to define a custom signal inGDScript:
Click here to view code image
extends Node
signal test_signal(some_argument, another_argument)
Signals can pass arguments to all the methods connected to them; these arguments’ names are written comma-separated in between the
parentheses of the signal declaration.
You can find the user-defined signals in the same place as the built-in signals, in the Node dock under Signals (See Figure 4.12).
FIGURE 4.12
User-defined signals will show up in the Node dock.
To emit a signal, the method Object.emit_signal(signal_name, arguments) is used. By calling this method, all the methods
connected to that signal will be called too.
Listing 4.10 is pseudo code showing how custom signals can be used and emitted.
LISTING4.10 Pseudo Code Using CustomSignals
Click here to view code image
extends KinematicBody2D
# user defined signal
signal jumped()
# more code . . .
func _ready():
self.connect(“jumped”, self, “_on_jumped”)
# more code . . .
func _input(event):
# more code . . .
if event.is_action_pressed(“jump”) and can_jump():
velocity.y = -JUMP_VELOCITY
emit_signal(“jumped”)
# more code . . .
111111111
22222222func _on_jumped():
$SamplePlayer2D.play(“jump_sound”)
jump_count += 1
Groups
Groups are a way to group objects together. An object can be checked for “membership” in a group, which makes groups useful to distinguish
different types or game objects (for example, “bullet hits enemy” and “bullet hits ally”).
There are two ways to add an object to a group. It can be done in the Node dock in the editor or done in code.
To add an object to a group using the editor, the Node dock needs to be used. It has a category, “Groups,” in which all the current user-defined
groups are visible and editable (Figure 4.13).
FIGURE 4.13
The Groups tab in the Node dock showing a few groups added to a node.
TRY IT YOURSELF
Adding a Node to Group
To add a node to a group, follow the following steps:
1. In the Scene dock, click a node to select it.
2. In the Node dock, click Groups.
3. Enter the name of the group.
4. Click the Add button.
To remove a node from a group, click the Icon next to the group name.
Adding an object to a group via code is really straightforward. It is done using the Object.add_to_group() method, which takes the
name of the group as the only argument.
Click here to view code image
object.add_to_group(“shootable”)
A classic example is to detect if enemies entered a certain area of the map or if the hero got hit by a bullet (or a flying cat).
There’s also a method in the Scene Tree that allows you to call a method in every member of a group. This is like the reverse of signals:
instead of emitting for anyone who’s observing it, this will broadcast the information to everyone in the group. You can use this by calling
get_tree().call_group(“group_name”, “method_name”). Additional arguments will be sent as parameters for the methods.
Note that this can generate script errors if some members of the group don’t have the specified method.
To show how groups can be effectively used, the next example makes use of the Area2D node, which hasn’t been introduced yet. To use it
correctly, PhysicsBodies have to be set up and CollisionShapes need to be created. This will get explained in greater detail in Hour 8,
“Physics System,” about the Physics System inGodot. But a vague understanding should be enough to read the example.
An area can be used to detect if PhysicsBodies (or other areas) have entered (or “collided”) a certain area. In this example, the Area2D is a
child of a Player node, is used to detect bullets from either allies or non-allies, and emits appropriate signals.
Listing 4.11 shows example code about signals that can be used to allow for easier composition with other mechanisms, like achievement or
statistic systems.
LISTING4.11 Achievement Systems
Click here to view code image
extends KinematicBody2D # Player
signal shot(damage)
signal shot_by_friendly(ally)
signal died()
var health = 250.0
func _on_Area_entered(object):
if object.is_in_group(“bullet”):
# we got hit by a bullet
# note: all objects of type bullet have a damage property
emit_signal(“shot”, object.damage)
if object.owner.is_in_group(“ally”):
111111111
22222222# we got shot by one of our allies
emit_signal(“shot_by_friendly”, object.owner)
health -= object.damage
if health < 0:
emit_signal(“died”)
# destroy the bullet
object.queue_free()
func _ready():
$Area2D.connect(“area_entered”, self, “_on_Area_entered”)
self.connect(“died”, self, “on_death”)
self.connect(“shot”, self, “on_shot”)
func on_died():
# implement dying here
self.queue_free()
func on_shot(damage):
$SamplePlayer2D.play(“hit_sound”)
Summary
In this chapter, you learned how Scripts can be used to add custom code to an object, how to create scripts, and how to overwrite the virtual
methods _ready, _process, and _input to set up your object and interface with user input.
You also learned about signals and groups, which can be used to notify objects interested in some of the actions your nodes can perform. You
saw how groups can be used to differentiate nodes and objects in certain groups, making it easy to implement different behaviors depending
on the type of group the current object is dealing with.
This was just a basic introduction to some of the concepts. In Hour 6, we discover more concepts facilitated inGodot to make scripting easier.
But GDScript is a fully fledged language, and there’s more to it than what would fit in one or two chapters, so always be on the lookout to learn
more about it!
Q&A
Q. What happens if you emit a signal with the wrong number of arguments?
A. Godot will show an error that the arguments don’t match.
Q. Is get_node an expensive operation?
A. It depends on the complexity and length of the path. But try to avoid using get_node in _process and save a reference in a variable instead
if you can.
Q. Can an object connect to the same signal multiple times?
A. Yes, it’s useful sometimes to trigger different actions when one signal gets emitted. However, if you connect the same signal to the same
object and method, the engine will output an error.
Q. Is there a limit on how many groups to which an object can be part?
A. Practically no. Of course, there are some limits, like memory constraints, but for the everyday needs of game developers, it will never
become a problem.
Workshop
Answer the following questions to make sure you understand the content of this hour.
Quiz
1. What type does every class type inherit from?
2. Can every object be part of the Scene Tree?
3. If your game runs at 60 frames per second, how often will _process be called?
4. If your game runs at 60 frames per second, how often will _input get called?
5. What is the difference between const and var?
Answers
1. Every class type directly or indirectly inherits from the Object class.
2. No; only objects of Node type can be part of the Scene Tree.
3. On every frame, so every 1/60 seconds = 16 ms.
4. _input only gets called when there are input events to process, so it’s independent from the frame rate.
111111111
222222225. var variables can be mutated, while const values can’t be changed, as they are immutable.
Exercises
Try to make a button that spawns Sprites that fall down and increase a counter when they leave the screen:
1. Place a Button and a Label Node in the scene.
2. Create a new scene containing only the Sprite, and save it in the project folder with the name “sprite.tscn”.
3. Connect a method to the Button’s “pressed” signal. By calling load(“res://sprite.tscn”).instance(), you get a reference
to the node representing the scene.
4. Add the Sprite node to the scene by using add_child.
5. Add a script to the Sprite scene. It should move the Sprite down (Tip: postion.y =+ speed delta) and emit a custom signal when
position.y is greater than a certain value. Then use queue_free() to delete the Sprite.
6. Connect a method to the custom signal on all of the spawned Sprites. If it’s emitted, increase a counter.
*7. To show the number in the label, you can use $Label.text = str(counter).