Skip to content

GDScript (Godot scripting)

Created: 2020-04-21 10:23:13 -0700 Modified: 2020-10-27 08:06:45 -0700

In 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#.

However, there are some benefits of GDScript:

  • Hot-reloading the code
  • They highly suggest that you go through the reference (and they say it takes a few minutes)
  • The only types that are passed by reference are Array and Dictionary. Even something like a PoolByteArray is passed by value.
  • For visibility, everything is public. By convention, the underscore indicates that something is private (reference).
  • Use “print” to log values:
    print("%s %s" % [scale, position])
    print("You can also log a ", msg, " with commas like ", this_msg)
    • 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).
  • For contiguous memory, use an Array. For non-contiguous memory, use a specifically typed array like PoolByteArray, PoolColorArray, etc.
  • Format strings just like in Python (reference): "Number: %d String: %s" % [number, string]
    • To specify a precision (like toFixed in JavaScript), use %.1f
  • 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 for get_node("NodePath")
      • Note that both forms ($ and get_node) will error if you try getting a node that doesn’t exist, so consider has_node or get_node_or_null instead.
      • There’s a get_tree function to return the SceneTree 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.
  • %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):
    export(Array, Constants.MonsterType) var monster_types
  • You can provide types at declaration, then the variable is forced to always be that type.
    var my_vector2: Vector2 # just declaration
    var my_node: Node = Sprite.new() # declaration and initialization
    var my_vector2 := Vector2() # inferred type with ":=" as long as the assigned value has a defined 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:
    func my_int_function() -> int:
    return 0
  • Godot uses radians. If you want degrees, use deg2rad() and rad2deg().
  • ”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:

    var instanced_scene = SomeScene.instance()
    instanced_scene.init(param_1, param2)
  • When instancing scripts, you can pass arguments via new() to _init (reference):

    Projectile.gd
    func _init(velocity, damage):
    print(velocity)
    # In another script…
    var projectile_script = preload("res://scripts/Projectile.gd")
    projectile_script.new(Vector2(5, 4), 10)
  • 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:

var projectile = ProjectileScene.instance()
projectile.set_script(projectile_script)
projectile.init(params)
  • Create a node (reference):
    var s
    func _ready():
    s = Sprite.new() # Create a new sprite!
    add_child(s) # Add it as a child of this node.
  • Delete a node (reference)
    func _someaction():
    s.free() # Immediately removes the node from the scene and frees it (as opposed to queue_free())
  • Random integer from 0 to n-1:
    randi() % n
  • Functions can be passed around as lambdas:
var desc_function: Callable = func(level: int) -> String: return "Adds %d damage" % [level]
desc_function.call(5)
# You can bind arguments if you want:
var on_mouse_enter: Callable = Callable(self._on_skill_hover).bind(skill_id)
button.mouse_entered.connect(on_mouse_enter)
  • Dictionaries (reference)
    • Check if key exists (note, you could just use dict.get("prop_that_may_not_exist", default_value)):
    if "life" in stats: dosomething()
    if stats.has("life"): dosomething()
  • Remove a pair from the dictionary:
    dict.erase("some_key")
  • Get the current scene’s tree without having to use $ or get_node():
    get_tree()
  • Find all children of type
    for child in get_children():
    if child is CollisionShape2D:
    # do something here
    • Find a child by name:
# The "true" is whether to search recursively
# The "false" is whether to search for owned nodes (see documentation)
var button: SkillTreeButton = find_child("SkillTreeButton_%d" % skill_id, true, false)
  • Find first child of type - same code as above, but break after handling the first match
  • Comparing the type of a value (reference)
    typeof(some_value) == TYPE_OBJECT
    • Note that TYPE_* is an enum, so the types themselves are ints. In Godot 4, it’s possible to have enums be their own types, e.g. var id: AbilityIds = AbilityIds.FIREBALL.
  • Checking if a tile in a tilemap has a collision shape set:

    var tile_id:int = tile_map.get_cell(tile_x, tile_y)
    var is_tile_walkable:bool = tile_map.tile_set.tile_get_shape_count(tile_id) == 0
  • 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):

    extends Sprite
    var image:Image = Image.new()
    var image_texture:ImageTexture = ImageTexture.new()
    func _ready():
    image.create(200, 200, false, Image.FORMAT_RGBA8)
    set_texture(image_texture)
    func draw_square(color:Color):
    image.lock()
    for draw_x in range(50):
    for draw_y in range(50):
    image.set_pixel(draw_x, draw_y, color)
    image.unlock()
    image_texture.create_from_image(image)
  • 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:
    # This function is in the Arena class, and the descendant_node is something like
    # a Shield.
    func clamp_position(descendant_node: Node2D, unscaled_size: Vector2) -> Vector2:
    var t:Transform2D = descendant_node.get_relative_transform_to_parent(self)
    # The origin represents the center of our node. This is also why we scale
    # the size down by 2—so that the node will only brush up against the edge.
    var relative_origin:Vector2 = t.get_origin()
    var relative_scale:Vector2 = t.get_scale()
    var arena_size:Vector2 = get_local_size()
    var half_scaled_size:Vector2 = unscaled_size * relative_scale / 2
    var x_local_to_arena:float = clamp(relative_origin.x, half_scaled_size.x, arena_size.x - half_scaled_size.x)
    var y_local_to_arena:float = clamp(relative_origin.y, half_scaled_size.y, arena_size.y - half_scaled_size.y)
    return Vector2(x_local_to_arena, y_local_to_arena)
var spritesheet_texture:Texture = preload("res://assets/sprites/characters.png")
var atlas_texture:AtlasTexture = AtlasTexture.new()
atlas_texture.atlas = spritesheet_texture
atlas_texture.region = Rect2(Vector2(32, 64), Vector2(16, 16))
icon.texture = atlas_texture # "icon" is a TextureRect in my case

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):

var collision:KinematicCollision2D = move_and_collide(velocity * delta, true, true, true)
if collision != null:
if collision.collider is TileMap:
position += collision.travel
velocity = velocity.bounce(collision.normal)
var remainder:Vector2 = collision.remainder.bounce(collision.normal)
var _ignored = move_and_collide(remainder * delta)
else:
position += velocity * delta
  • We first call move_and_collide with test_only=true (the last argument) set to true, meaning our position will remain unchanged by move_and_collide.
  • We immediately add collision.travel to position, making sure not to multiply by delta because we already took it into account during the call to move_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.
  • It’s a very good idea to enable warnings for missing types:
    • 500
    • 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)
  • 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)

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:

func fire_projectile(firer:Node2D) -> void:
var projectile_scene:PackedScene = preload("res://scenes/Arrow.tscn")
var projectile:KinematicBody2D = projectile_scene.instance()
add_child(projectile)
var projectile_params:ProjectileParams = ProjectileParams.new({
"firer": firer,
"target_position": projectile.get_global_mouse_position()
})
projectile.init(projectile_params)

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.

extends Object
class_name ProjectileParams
var firer:Node2D
var target_position:Vector2
var message:String
# This function is automatically invoked by Godot when you call ".new()" on
# ProjectileParams. We use it here because ProjectileParams is like a C struct
# moreso than it is a script to control a scene.
# We take in a Dictionary rather than named parameters because Godot 3.2 doesn't
# support specifying only certain intermediate parameters and not others.
func _init(params:Dictionary) -> void:
firer = get_required_value(params, "firer", TYPE_OBJECT)
target_position = get_required_value(params, "target_position", TYPE_VECTOR2)
message = get_optional_value(params, "message", TYPE_STRING, "nothing")
# This indicates that a property is optional.
# params - the dictionary of parameters passed into this class
# property_name - the name of the property you want to fetch.
# expected_type - the type of the property, if it exists, and if not, then this
# has to match the type of default_value.
# default_value - the value to use if the property doesn't exist.
# Returns: the property, if it exists, otherwise default_value
func get_optional_value(params:Dictionary, property_name:String, expected_type:int, default_value = null):
if params.has(property_name):
var value = params.get(property_name)
assert(typeof(value) == expected_type)
return value
assert(typeof(default_value) == expected_type)
return default_value
# This indicates that a property is required.
# params - the dictionary of parameters passed into this class
# property_name - the name of the property you want to fetch.
# expected_type - the type of the property that you're fetching
# Returns: the property
func get_required_value(params:Dictionary, property_name:String, expected_type:int):
assert(params.has(property_name))
var value = params.get(property_name)
assert(typeof(value) == expected_type)
return value

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.:

# NOTE: this is built-in to Godot as array.pick_random(), so this is just to demonstrate
func random_array_element(array: Array) -> Variant:
return array[randi() % array.size()]

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:

extends Object
class_name ProjectileParams
var firer:Node2D
var target_position:Vector2
var message:String
# This function is automatically invoked by Godot when you call ".new()" on
# ProjectileParams. We use it here because ProjectileParams is like a C struct
# moreso than it is a script to control a scene.
# We take in a Dictionary rather than named parameters because Godot 3.2 doesn't
# support specifying only certain intermediate parameters and not others.
func _init(params:Dictionary) -> void:
firer = get_required_value(params, "firer")
target_position = get_required_value(params, "target_position")
message = get_optional_value(params, "message", "nothing")
# This indicates that a property is optional.
# params - the dictionary of parameters passed into this class
# property_name - the name of the property you want to fetch.
# default_value - the value to use if the property doesn't exist.
# Returns: the property, if it exists, otherwise default_value
func get_optional_value(params:Dictionary, property_name:String, default_value = null):
if params.has(property_name):
return params.get(property_name)
return default_value
# This indicates that a property is required.
# params - the dictionary of parameters passed into this class
# property_name - the name of the property you want to fetch.
# Returns: the property
func get_required_value(params:Dictionary, property_name:String):
assert(params.has(property_name))
return params.get(property_name)

You can further clean this up by making get_optional_value and get_required_value static.

  • 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.
export var speed = 400

  • 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

Here’s how you can fade out music:

var audio_tween: Tween
func _fade_out_music() -> void:
if audio_tween != null:
audio_tween.stop() # make sure we don't have multiple tweens running
var audio: AudioStreamPlayer = $AudioStreamPlayer
audio_tween = get_tree().create_tween()
audio_tween.tween_property(audio, "volume_db", -80, 5) # fade over 5 seconds
audio_tween.tween_callback(audio.stop)

Here’s how to tween a nested property:

modulate.a = 0.0 # make transparent
tween.tween_property(self, "modulate:a", 1.0, 5) # fade in over 5 seconds

It’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:

func animate_hit_flash() -> void:
var time:float = 0.11
var from_color:Color = Color.white
var to_color:Color = Color(1.0, 0.3, 0.3, 1.0)
var _ignored = hit_flash_tween.interpolate_property(animated_sprite, "modulate", from_color, to_color, time, Tween.TRANS_QUINT, Tween.EASE_OUT)
_ignored = hit_flash_tween.start()
yield(hit_flash_tween, "tween_all_completed")
_ignored = hit_flash_tween.interpolate_property(animated_sprite, "modulate", to_color, from_color, time, Tween.TRANS_QUINT, Tween.EASE_OUT)
_ignored = hit_flash_tween.start()
  • 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.
  • 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.

This 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:

static func add_sound_to_node(scene_path:String, node:Node) -> void:
var packed_scene:PackedScene = load(scene_path)
var audio_stream_player:AudioStreamPlayer = packed_scene.instance()
audio_stream_player.autoplay = true
var _ignored = audio_stream_player.finished.connect(audio_stream_player.queue_free)
node.add_child(audio_stream_player)

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):

static func add_sound_to_node_by_sound_file(sound_path:String, node:Node) -> void:
var audio_stream_player:AudioStreamPlayer = AudioStreamPlayer.new()
var audio_stream:AudioStreamSample = AudioStreamSample.new()
var file:File = File.new()
var _ignored = file.open(sound_path, File.READ)
var buffer:PoolByteArray = file.get_buffer(file.get_len())
file.close()
audio_stream.data = buffer
audio_stream.format = AudioStreamSample.FORMAT_16_BITS
audio_stream_player.autoplay = true
audio_stream_player.stream = audio_stream
_ignored = audio_stream_player.finished.connect(audio_stream_player.queue_free)
node.add_child(audio_stream_player)

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”

static func add_sound_to_node_by_sound_file(sound_path:String, node:Node) -> void:
var audio_stream_player:AudioStreamPlayer = AudioStreamPlayer.new()
var audio_stream_sample:AudioStreamSample = load(sound_path)
audio_stream_player.autoplay = true
audio_stream_player.stream = audio_stream_sample
audio_stream_player.bus = "Sounds"
var _ignored = audio_stream_player.finished.connect(audio_stream_player.queue_free)
node.add_child(audio_stream_player)

The 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 game

To be clear, the problem is this:

  1. You have a node with a script
  2. You run your game
  3. You edit the script from Visual Studio Code
  4. 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:

func _on_Player_body_entered(body):
pass # Replace with function body.

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():

func _ready():
var button_node = $Button
button_node.pressed.connect(self._on_Button_pressed)

I had a tool script that did something like this:

tool
extends Sprite
class_name SpritesheetSprite
var DungeonTexture = load("res://assets/sprites/DungeonCrawl_ProjectUtumnoTileset.png")
export(Constants.SpriteNames) var sprite_name setget set_sprite
func _init(name = null):
if name != null:
sprite_name = name
func set_sprite(new_sprite):
sprite_name = new_sprite
print("Set sprite to a new sprite " + str(new_sprite))
initialize_sprite()
func initialize_sprite():
if sprite_name == null:
return
var tilesize = Constants.tilesize
set_region(true)
var sprite_info = Constants.sprite_info[sprite_name]
var tile_x = sprite_info.tile_x
var tile_y = sprite_info.tile_y
set_region_rect(Rect2(tile_x * tilesize, tile_y * tilesize,tilesize,tilesize))
self.texture = DungeonTexture
self.centered = false
func _ready():
initialize_sprite()

The problem was with “Constants.sprite_info”; it didn’t exist until I added “tool” to the top of “Constants”.

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 exist

If 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.

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 set

Just restart Godot.

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…?