GDScript (Godot scripting)
Created: 2020-04-21 10:23:13 -0700 Modified: 2020-10-27 08:06:45 -0700
My opinion about GDScript
Section titled My opinion about GDScriptIn 2020, it was really rough around the edges. Functions couldn’t be passed around or bound with arguments, types were sort of lacking, etc.
In 2024, I feel like there are a ton of annoyances that arise from writing GDScript in VSCode, which isn’t entirely the fault of Godot. E.g. I’ll switch between files in VSCode, which triggers a save, which then causes an error and makes Godot take the focus. Or I’ll save a file with a syntax error and it’ll delete all of the spaces in strings.
There are some things I don’t like about the language itself:
- I still don’t like that there aren’t proper generics. There’s no way to write types for a
Dictionary
. - I don’t like using spaces/tabs to control flow, but maybe that’s a VSCode problem as well since I would sometimes paste code (or maybe Copilot would) and it would have incorrect whitespace
So my stance is still to prefer C#.
I originally thought that C# didn’t support hot reloading, but then I learned that it does. See C sharp in Godot.
- They highly suggest that you go through the reference (and they say it takes a few minutes)
- For visibility, everything is public. By convention, the underscore indicates that something is private (reference).
- Use “print” to log values:
- If you opened Godot from a command prompt, it logs there.
- There’s no explicit garbage collection. It sounds like there’s just reference counting that leads to deallocation in something like an internal destructor (imagine a smart pointer in C++). You can manually call this yourself with
.queue_free
(reference, reference2). - Format strings just like in Python (reference):
"Number: %d String: %s" % [number, string]
- To specify a precision (like toFixed in JavaScript), use
%.1f
- To specify a precision (like toFixed in JavaScript), use
- Ternary is like Python with
if
/else
:var color = 'red' if life < 5 else 'green'
- They have literals for node paths:
@"Node/Label"
- NodePath$NodePath
- shorthand forget_node("NodePath")
- Note that both forms (
$
andget_node
) will error if you try getting a node that doesn’t exist, so considerhas_node
orget_node_or_null
instead. - There’s a
get_tree
function to return theSceneTree
that contains the current node (reference). This is where the$
syntax helps. - If you have a Scene that looks like this:
- TileMap
- Sprite
- Camera
- Then from Camera, to access Sprite, you would path it like this:
/root/TileMap/Sprite
. This is an absolute path (reference).- You can also do
$"."
as the root node.
- You can also do
- TileMap
- Note that both forms (
%NodePath
- shorthand for getting a unique node. See more here. (I would write more on this, but I didn’t thoroughly test it).- For observers/events, use signals (reference)
- For an exported array variable of a specific type, you can do this (reference):
- You can provide types at declaration, then the variable is forced to always be that type.
- You can coerce types with the
as
keyword - Constants (
const
) always infer their types, although you can still specify them if you want - You can specify function return types this way:
- Godot uses radians. If you want degrees, use
deg2rad()
andrad2deg()
. - “Lifecycle” events
_ready
(reference): called when the node and all of its children enter the active scene_init
(reference): called when the object is initialized. This is for the constructor. This does indeed allow for arguments. See the notes below on constructing a script or scene._process
(reference): called every time that a frame is drawn. This is done after the physics step on single-threaded games._physics_process
(reference): runs before each physics step at a fixed time interval (60 per second by default). This is where you would do something like controlling a character._enter_tree
(reference): called when the node enters the Scene Tree. In general, they say it’s better to use _ready since _enter_tree is called before the children enter._exit_tree
(reference): called when this node exits the Scene Tree. This is called after all children have already exited as well._unhandled_input
(reference): input is handled in a particular way, so this only gets hit with input that hasn’t already been handled.
- To make a script like “Constants” or “Util” available everywhere (keyword: “singleton”), you have to use AutoLoad (reference). This forces the script into the scene tree.
Constructing a script or scene (or calling set_script
)
Section titled Constructing a script or scene (or calling set_script)-
When instancing scenes, you cannot pass arguments (reference), so you should make your own
init()
function: -
When instancing scripts, you can pass arguments via new() to
_init
(reference): -
When calling
set_script
, the docs say that_init
is automatically called (reference). Thus, you have to rely on your own init function without the underscore if you want to parameterize initialization:
Basic code snippets
Section titled Basic code snippets- Create a node (reference):
- Delete a node (reference)
- Random integer from 0 to n-1:
- Functions can be passed around as lambdas:
- Dictionaries (reference)
- Check if key exists (note, you could just use
dict.get("prop_that_may_not_exist", default_value)
):
- Check if key exists (note, you could just use
- Remove a pair from the dictionary:
- Get the current scene’s tree without having to use
$
orget_node()
: - Find all children of type
- Find a child by name:
- Find first child of type - same code as above, but
break
after handling the first match - Comparing the type of a value (reference)
- Note that
TYPE_*
is an enum, so the types themselves areint
s. In Godot 4, it’s possible to haveenum
s be their own types, e.g.var id: AbilityIds = AbilityIds.FIREBALL
.
- Note that
Advanced code snippets
Section titled Advanced code snippets-
Checking if a tile in a tilemap has a collision shape set:
-
Note: what this code is doing is saying “if there are no shapes set on this tile, then it must be walkable”. I’m nearly positive that “shapes” in this case represent collision shapes, and I have it set that tiles are either completely walkable or completely unwalkable, so I only need to check if they exist or not.
-
Draw individual pixels (not very performant compared to a shader):
-
Converting coordinate systems
- I had a scene tree with something like Arena → Player → Shield, and I wanted to make sure that something like a Shield would be constrained to the boundaries of the Arena without having to obey physics (otherwise I could have just used move_and_collide or something). In order to do this, I needed the Shield to be in the coordinate space of the Arena, which can be accomplished through
shield.get_relative_transform_to_parent(arena)
. Here’s the full code that I wrote:
- I had a scene tree with something like Arena → Player → Shield, and I wanted to make sure that something like a Shield would be constrained to the boundaries of the Arena without having to obey physics (otherwise I could have just used move_and_collide or something). In order to do this, I needed the Shield to be in the coordinate space of the Arena, which can be accomplished through
Dynamically create an AtlasTexture (reference)
Section titled Dynamically create an AtlasTexture (reference)Bouncing a KinematicBody2D off of a collision point:
Section titled Bouncing a KinematicBody2D off of a collision point:First, a diagram of what we’re trying to accomplish:
- Red vector: the normalized input vector
- Blue vector: the normalized vector after colliding with the square
What’s really happening though is that the input vector gets split at the point of collision into two parts: “travel” (solid line below) and “remainder” (dotted line below):
The “travel” vector is in the same direction as the input vector. The “remainder” needs to be redirected in the bounce direction:
Here’s the code I wrote (explanation below):
- We first call
move_and_collide
withtest_only=true
(the last argument) set totrue
, meaning our position will remain unchanged bymove_and_collide
. - We immediately add
collision.travel
toposition
, making sure not to multiply bydelta
because we already took it into account during the call tomove_and_collide
. - We use Godot’s built-in
bounce
method to get the velocity we should have after the collision. - If we had simply added
collision.travel + remainder
, then we’d attempt to move along a third path that may have a different collider (represented in green below) in the way:
- It’s possible that the bounce results in another collision, in which case you probably want to recurse and bounce again. In my case, I just ignore it and let it get handled on the next
_physics_process
.
Types and type-checking
Section titled Types and type-checkingBasics
Section titled Basics- It’s a very good idea to enable warnings for missing types:
- If you ever want to ignore a particular warning just for a single line, annotate the line like
@warning_ignore("UNSAFE_PROPERTY_ACCESS")
(no comment character needed)
- If you ever want to ignore a particular warning just for a single line, annotate the line like
- There aren’t exactly “generics”, but you can define arrays like this:
var names: Array[String] = ["Alice", "Bob", "Carol"]
- These don’t exist yet for dictionaries as of Wed 02/21/2024 (reference)
Defining a “struct” (reference)
Section titled Defining a “struct” (reference)Note: as of 03/21/2024, there is a proposal to add “proper” structs into Godot.
My goal was to have calling code like this:
The “struct” is ProjectileParams, which has the code below. Note that by having “class_name ProjectileParams”, I don’t need to load, preload, or AutoLoad the class despite using it from a different file.
We can’t have static typing on get_optional_value
, get_required_value
, value
, or default_value
in Godot 3.2 because there’s no “any” type. In Godot 4, you can use the Variant
type, e.g.:
Also, you don’t technically need the “typeof” checking since it’ll fail when you try to assign to the typed member variables if the types don’t match, in which case the code gets cleaner:
You can further clean this up by making get_optional_value
and get_required_value
static.
Godot / GDScript integration
Section titled Godot / GDScript integration- Scripts are attached to nodes (i.e. they’re not nodes themselves). You can add with the quick button in the first screenshot or by right-clicking like in the second one:
- If you drag a node to the code editor, it’ll type the node path for you into the script.
- If you export a variable, you’ll be able to modify it in the “Script variables” section of the Inspector.
- You can write tool scripts to run directly in the editor itself, e.g. to draw a cannon ball’s trajectory directly in the editor (reference)
- Clicking the script icon next to a node will directly open its script in the editor
Tweens
Section titled TweensBasic example
Section titled Basic exampleHere’s how you can fade out music:
Here’s how to tween a nested property:
Chaining tweens
Section titled Chaining tweensIt’s not built-in as of 7/10/20 (reference). Instead, here’s some code I used to make an entity flash red when hit:
Playing sounds
Section titled Playing soundsBasics
Section titled Basics- First, the Godot docs suggest that you just let each individual scene manage its own sounds as opposed to having a singleton SoundManager or something (reference)
- There is now a
max_polyphony
property (reference) which automatically handles what happens when you try playing too many sounds at once through an audio player. - Godot has functions to convert from linear to logarithmic values for decibels:
linear2db
anddb2linear
(these are inMathf
in C#). - Just like how Godot has groups, sounds and music can be classified into buses. It’s AudioStreamPlayer that has this property. Once you have a bus, you can individually control volume levels, toggle mute, add effects like reverb, etc. Clicking “Audio” at the bottom of Godot will let you modify them:
- With either code snippet above, you may want to set the “bus” on the audio_stream_player. You can add buses by clicking an AudioStreamPlayer in the scene tree from within the editor, then clicking “Audio” at the bottom of Godot. Buses are identified by strings.
Code to control sounds
Section titled Code to control soundsThis code will add a sound scene and free it when it’s finished playing, so it requires that your AudioStreamPlayer exists in its own scene:
This code will read directly from a .wav file so that you don’t need a scene (don’t use this without reading the code after this snippet):
That code works from Godot, but it doesn’t work from an HTML5 export thanks to File.new() (reference). Instead, change “File.new()” to use “load”
Troubleshooting
Section titled TroubleshootingThe GDScript language server can’t work properly! The open workspace is different from the editor’s. (reference)
Section titled The GDScript language server can’t work properly! The open workspace is different from the editor’s. (reference)Apparently this is caused by opening Godot after Visual Studio Code, so just click the “reload” button in the error dialog itself.
Editing a script in Visual Studio Code does not update the running game
Section titled Editing a script in Visual Studio Code does not update the running gameTo be clear, the problem is this:
- You have a node with a script
- You run your game
- You edit the script from Visual Studio Code
- The game, while running, does not reflect those updates
I’m not sure if this is supposed to work based on this GitHub issue, but the workaround is to open the script in Godot itself and then save it from Godot.
Creating a function with a signal doesn’t actually add any code when using an external editor (reference)
Section titled Creating a function with a signal doesn’t actually add any code when using an external editor (reference)I.e. using this dialog won’t actually create code unless you’re using the internal editor:
The code it would normally create is just an overridden function like this at the top level:
Note: even if you make the code yourself in the receiver, you still need to connect the source with its signal to the receiver or else nothing will happen. Alternatively, you can write the connect function in _ready():
Tool script can’t access singletons (reference)
Section titled Tool script can’t access singletons (reference)I had a tool script that did something like this:
The problem was with “Constants.sprite_info”; it didn’t exist until I added “tool” to the top of “Constants”.
Tool scripts don’t refresh the contents of an “export” var’s type when the type is a singleton (reference, reference2)
Section titled Tool scripts don’t refresh the contents of an “export” var’s type when the type is a singleton (reference, reference2)My tool script has this:
export(Constants.SpriteNames) var sprite_name setget set_sprite
My Constants script has this:
enum SpriteNames {WARRIOR, MAGE, ARCHER}
If I update the enum, then the only way to get the tool script to update is based on whether I’m using the external editor or not:
- With external editor, I think you have to just delete your singleton from Project Settings and then add it again.
- With the internal editor, I think you need to save the file with the enum in it, close your scene, then reopen it
Can’t find a particular node in the scene tree that should exist
Section titled Can’t find a particular node in the scene tree that should existIf you can’t find it, then it almost certainly doesn’t exist on the frame that you’re trying to use it. General things to look into:
- Is the node named what you expect it to be at the time that you’re looking? E.g. I had a Gameplay scene whose root node was just named “Node”, so I needed to rename it in the scene for me to be able to find it with ”$/root/Main/Gameplay”. To help figure this out, set a breakpoint on your _ready function or wherever you’re looking for the node and inspect the remote scene.
start: Timer was not added to the SceneTree. Either add it or set autostart to true.
Section titled start: Timer was not added to the SceneTree. Either add it or set autostart to true.5/19/2020 (Godot 5.2.1) - I tried forever to get timers to work through pure code without being part of the scene tree, but I couldn’t figure it out. I don’t have any reference links anymore.
Cyclical script references (reference)
Section titled Cyclical script references (reference)The error looks like this: Parser Error: the class “Arena” was found in global scope, but its script couldn’t be loaded.
Sometimes, simply putting a type on a variable will cause a cyclical reference error:
var arena # Fine
var arena:Arena # Causes an error
The only solution that I’ve found is to simply remove the type, which of course destroys static type-checking. However, this sometimes seems to be indicative of a false positive (at least in 3.2.2) where there’s some other, completely unrelated error in your script that needs to be fixed, and the type itself is fine.
Can’t find a class whose class_name
you definitely set
Section titled Can’t find a class whose class_name you definitely setJust restart Godot.
”X hides global script class”
Section titled ”X hides global script class”When this happened to me, I had been making a very simple scene with a very simple script, and I couldn’t figure out what was different from what I normally do. I tried everything, but the only thing that worked was deleting the scene and the script and just recreating them entirely. The script contents ended up being identical, but it worked after the deletion.
I think the cause may be duplicating a script with a class_name
when using an external editor…?