Skip to content

General scripting paradigms

Created: 2020-05-12 08:41:41 -0700 Modified: 2020-05-21 18:11:56 -0700

This note is about scripting paradigms that should pertain to all languages allowed by Godot, but the examples will be in GDScript since that’s what I used.

Suppose there’s a UI that should only listen to player characters, and you have a factory somewhere creating all characters. You could have the CharacterFactory emit a signal like character_created(character: Character), then have something listen for that and add signals to the Character itself, e.g. character.health_changed.connect([...]).

Without the signal on the factory, it would typically be hard to get access to the Character to be able to set signals up.

Getting access to some other node or script in the scene tree

Section titled Getting access to some other node or script in the scene tree

Suppose you have a scene tree like this:

  • root
    • Game
    • Map

…and you want to get access to “Game” from “Map”. Here are many different ways of doing it:

  • Go from the root down through the scene tree: $"/root/Game"
  • Use a relative path: get_parent().get_node("Game")
  • Consider using autoload so that you have a singleton
  • Inject Game into Map when initializing it
var map_scene:PackedScene = load("res://scenes/Map.tscn")
var map = map_scene.instance()
map.init(game)
  • Set a group:
game.gd
add_to_group("game")
# map.gd
get_tree().get_nodes_in_group("game")[0]
  • Timers are easy to set up, but be careful because they’re in seconds, not milliseconds
func _ready():
set_up_ai_timer()
func set_up_ai_timer():
var timer = Timer.new()
add_child(timer)
timer.wait_time = 1
while true:
ai_tick()
timer.start()
yield(timer, "timeout")
func ai_tick():
print("Hello world")
  • They suggest that if you don’t need logic every single frame, consider yielding to a timer (reference)
# Infinitely loop, but only execute whenever the Timer fires.
# Allows for recurring operations that don't trigger script logic
# every frame (or even every fixed frame).
while true:
my_method()
$Timer.start()
yield($Timer, "timeout")

Suppose you’re in a character-select scene and want to swap to a gameplay scene.

Note: the reference link talks about nuances like swapping vs. hiding vs. deleting so that you could, for example, keep processing a scene even though it’s not showing. However, for the methods below, I’m just talking about a high-level view of how to switch scenes.

  • Method #1: make a “Main” class that keeps track of all of this for you. It would have code to add the new scene as a child and free the old scene.
  • Method #2: make use of change_scene from the scenes themselves. E.g. TitleScreen.gd would have get_tree().change_scene(“res://Gameplay.tscn”)

Method #1’s code looks like this:

main.gd
extends Node
var CharacterSelectScene = load("scenes/CharacterSelect.tscn")
var GameplayScene = load("scenes/Gameplay.tscn")
var character_select_scene_name = "CharacterSelect"
var gameplay_scene_name = "Gameplay"
func setup_up_character_select_scene():
var character_select_scene = CharacterSelectScene.instance()
add_child(character_select_scene)
character_select_scene.name = character_select_scene_name
character_select_scene.connect("selected_character", self, "_on_character_selected")
func _on_character_selected(character_class):
get_node(character_select_scene_name).queue_free()
setup_gameplay_scene()
func setup_gameplay_scene():
var gameplay_scene = GameplayScene.instance()
add_child(gameplay_scene)
gameplay_scene.name = gameplay_scene_name
func _ready():
setup_up_character_select_scene()

Method #2 doesn’t need the concept of a Main scene. Instead, make CharacterSelect.tscn your primary scene that runs when you start your game.

CharacterSelect.gd
extends Node
func _unhandled_input(_e):
if Input.is_action_just_pressed("ui_accept"):
get_tree().change_scene('res://Gameplay.tscn')
  • Pros
    • There’s no need for tracking names
    • No need for a signal to connect
    • No need to explicitly call free (reference, reference2)
  • Cons
    • If you want to initialize the scene you’re changing to; there are only hacky ways for doing so (reference), e.g.
var gameplay_scene = GameplayScene.instance()
gameplay_scene.init(selected_character_class)
$"/root".call_deferred("add_child", gameplay_scene) # this only needs to be deferred if called from particular functions like _ready
get_tree().current_scene = gameplay_scene
get_node("/root").remove_child(self)
self.queue_free()
  • changescene exists on a SceneTree, you end up changing _everything in the tree, so if you wanted a wrapper node at the top of your tree, you would have to include it in every scene that you’re changing to.

Preloading will try loading as soon as the game runs. You can tell that’s the case by doing this:

Gameplay.gd
func _some_function_that_never_gets_called():
var scene = preload("res://scenes/Gameplay.tscn")
foo # this is an error

Here, if you replace “preload” with “load”, the syntax error in Gameplay.gd will never actually be hit.