Overview
When developing games with Godot Engine, managing dependencies between scripts and scenes is an unavoidable topic. Especially if you proceed with design without understanding the reference counting mechanism that handles automatic memory freeing, you'll face the problem of "cyclic references (Cyclic Reference)" which becomes a source of unexpected memory leaks and bugs.
This article explains the basics of dependencies and cyclic references in Godot, points out common pitfalls beginners fall into, and introduces design patterns for writing clean, maintainable code using Godot-specific powerful tools like WeakRef and signals, with concrete GDScript code examples.
By reading this article, you'll be able to solve the following challenges:
- Understand why cyclic references are a problem and prevent memory leaks.
- Properly manage dependencies between nodes and resources, reducing code coupling.
- Learn how to effectively work with Godot's memory management system (reference counting).
Core Concepts
What are Dependencies
Dependencies in Godot refer to situations where one script or scene uses the functionality (methods, properties, etc.) of another script or scene.
For example, if PlayerStatus.gd managing player HP calls a method in HUD.gd to update the UI, PlayerStatus.gd is dependent on HUD.gd.
Godot's Memory Management and Reference Counting
Godot objects are mainly classified into two types:
Nodederived classes (objects belonging to scene tree):- Usually automatically freed when removed from scene tree (
queue_free()).
- Usually automatically freed when removed from scene tree (
RefCountedderived classes (resources, etc.):- Memory is managed using a mechanism called reference counting.
- Automatically freed from memory when the number of variables referencing this object reaches 0.
Resources (Resource) and containers like Array and Dictionary also internally use reference counting. Cyclic reference problems mainly occur between these reference-counted objects.
Note: For references only between
Nodes in the scene tree, they're typically destroyed together withqueue_free()from the parent, so it's slightly different from typical "reference-counted cyclic reference leaks". The "cyclic reference memory leaks" discussed in this chapter mainly concern objects managed by reference counting likeRefCounted/Resource/Array/Dictionary.
What are Cyclic References
Cyclic references refer to a state where two or more objects reference each other.
- Object A references Object B.
- Object B references Object A.
In this state, both A and B have reference counts of 1 or more, and even if no one externally references A or B anymore, reference counts never reach 0. As a result, these objects remain in memory, causing memory leaks.
Common Pitfalls
Here are two typical patterns where beginners easily fall into cyclic references.
Mutual References Between Resources
Resources (Resource) are representative examples of reference-counted objects. For example, if Item and ItemDrop resources each have properties referencing the other type:
Item.gd
# Item.gd (Resource)
extends Resource
class_name Item
@export var drop_data: ItemDrop # References ItemDrop
ItemDrop.gd
# ItemDrop.gd (Resource)
extends Resource
class_name ItemDrop
@export var item_data: Item # References Item
In this state, if you try to mutually set these resources in the editor, Godot may detect the cyclic reference and error when saving resources. Also, if you try to mutually load with preload, unexpected errors (like scripts becoming empty) may occur due to script loading order issues.
Mutual References Between Objects Outside Scene Tree
Even for Node derived classes that aren't RefCounted, you need to be careful about cyclic references when holding RefCounted objects through containers like Array or Dictionary.
For example, if you create two custom data classes (inheriting RefCounted) and design them to reference each other, even when nodes are freed from the scene tree, the data class instances remain in memory.
Best Practices and Examples
Here we introduce three main design patterns to avoid cyclic references and keep dependencies clean.
Using WeakRef (Weak References)
The most direct way to break cyclic references is to use weak references that don't affect reference counts. Godot provides the WeakRef class for this.
WeakRef holds a reference to an object but doesn't increase that object's reference count. This allows referenced objects to be freed when they should be.
Example of Breaking Cyclic Reference with WeakRef
Design so Object A strongly references B, and B weakly references A.
ObjectA.gd
# ObjectA.gd
extends RefCounted
class_name ObjectA
var object_b: ObjectB # Strong reference
func set_b(b: ObjectB):
object_b = b
ObjectB.gd
# ObjectB.gd
extends RefCounted
class_name ObjectB
var object_a_ref: WeakRef # Weak reference
func set_a(a: ObjectA):
object_a_ref = weakref(a) # Create WeakRef
func get_a() -> ObjectA:
# Check if reference is valid before getting
var a = object_a_ref.get_ref()
if is_instance_valid(a):
return a
return null
Since ObjectB weakly references ObjectA, when all strong references to ObjectA are gone, ObjectA is freed. At that point, object_a_ref held by ObjectB becomes an invalid reference (null).
When calling
get_a()afterObjectAhas been freed,object_a_refis already invalid, soget_ref()returnsnull. Therefore, callers must always do existence checks withis_instance_valid()or similar.
Loose Coupling with Signals
The most Godot-like way to break dependencies between nodes is using signals.
Signals are a mechanism for nodes to broadcast that "something happened." This means the event emitter doesn't need to know who receives the event (receivers).
Example of Breaking Dependencies with Signals
Consider informing the UI when player HP changes.
Player.gd (Emitter)
# Player.gd
extends CharacterBody3D
signal hp_changed(new_hp: int) # Define signal
var hp: int = 100:
set(value):
hp = value
hp_changed.emit(hp) # Emit signal when HP changes
HUD.gd (Receiver)
# HUD.gd
extends Control
func _ready():
# Get reference to Player node
var player = get_tree().get_first_node_in_group("player")
if player:
# Connect signal
player.hp_changed.connect(_on_player_hp_changed)
func _on_player_hp_changed(new_hp: int):
# UI update processing
print("Player HP updated: ", new_hp)
In this design, Player.gd doesn't know about HUD.gd's existence. Player just emits signals, and HUD subscribing (connect) to those signals enables the functionality. This makes dependencies always one-directional "HUD → Player", completely eliminating mutual references and strong dependencies.
One-directional Dependencies with Autoload (Singleton)
For functionality that needs global access throughout the game (e.g., game settings, data management, scene transitions), the best practice is to implement it as a singleton using the Autoload feature.
How to register Autoload:
- Open "Project" → "Project Settings" → "Autoload" tab
- Specify the script file in "Path" (e.g.,
res://scripts/GameManager.gd) - Enter any name in "Node Name" (e.g.,
GameManager) - Click the "Add" button
Registered Autoloads are automatically added to the scene tree root and can be globally accessed from anywhere using the registered name (in the example above, GameManager). This means other nodes only need to depend on Autoload one-directionally, eliminating the need for Autoload to depend on other nodes.
Example of Using Autoload
Create an Autoload called GameManager to manage game state.
GameManager.gd (Autoload)
# GameManager.gd
extends Node
class_name GameManager
var score: int = 0
func add_score(amount: int):
score += amount
print("Current Score: ", score)
AnyNode.gd (Dependent Node)
# AnyNode.gd
extends Node
func _on_enemy_killed():
# Just access GameManager, GameManager doesn't know about AnyNode
GameManager.add_score(100)
In this pattern, all nodes depend on GameManager, but GameManager doesn't depend on individual nodes, so there's no worry about cyclic references.
Note: Autoload is very convenient, but stuffing everything into it tends to create a "giant do-everything class". Split by role into
GameManager/AudioManager/SaveDataManagerand design with awareness of "what is this singleton responsible for".
Summary
The key to dependency management in Godot Engine is understanding the reference counting mechanism and consciously avoiding cyclic references.
| Problem | Solution | Godot Implementation |
|---|---|---|
| Memory leaks from mutual references | Use references that don't increase reference counts | Use WeakRef class |
| Strong coupling between nodes | Break dependencies with event-based communication | Use signals |
| Access to global functionality | Establish one-directional dependencies | Use Autoload (singleton) |
By applying these design patterns, your Godot project will be more stable, easier to extend, and best of all, remain clean without worrying about memory leaks.
As a next step, we recommend reading the "Best Practices" chapter in Godot's official documentation to learn more advanced design principles.