Overview
When developing games with Godot Engine, managing dependencies between scripts and scenes is an unavoidable topic. In particular, if you design without understanding the reference counting mechanism, you'll face memory leaks from "cyclic references."
This article explains the cyclic reference problem and introduces design patterns utilizing WeakRef, signals, and Autoload.
Godot's Memory Management and Cyclic References
Reference Counting System
Godot objects are mainly classified into two types:
| Type | Examples | Memory Management |
|---|---|---|
Node derived classes | CharacterBody2D, etc. | Released when removed from scene tree (queue_free()) |
RefCounted derived classes | Resource, Array, Dictionary | Auto-released when reference count reaches 0 |
Memory leaks from cyclic references primarily occur with RefCounted derived classes.
How Cyclic References Cause Memory Leaks
Object A ---> Object B
^ |
|_______________|
When A and B reference each other, even after external references are gone, both reference counts remain at 1 or higher. As a result, they're never freed from memory.
Common Mistakes and Best Practices
| Common Mistake | Best Practice |
|---|---|
| Parent references child, child also directly references parent | Child should notify parent via get_parent() or signals |
| Item resource references Player, Player also references Item | Resources hold data only, Player passes as argument when using |
| SFX, UI, etc. directly modify Autoload internals | Singleton provides modification methods, others just call them |
| Mutual references for temporary effects | Use WeakRef for one side as weak reference, or notify effect completion via signal |
Solution 1: WeakRef (Weak Reference)
The most direct way to break cyclic references. Uses weak references that don't increment the reference count.
Practical Example: Equipment and Character
# Equipment.gd (RefCounted)
class_name Equipment
extends RefCounted
var owner_ref: WeakRef # Weak reference
func get_owner() -> Character:
if owner_ref:
var ref = owner_ref.get_ref()
if ref:
return ref
return null
# Character.gd (Node)
class_name Character
extends CharacterBody3D
var equipped_item: Equipment # Strong reference
func equip(item: Equipment):
if equipped_item:
unequip()
equipped_item = item
equipped_item.owner_ref = weakref(self)
func unequip():
if equipped_item:
equipped_item.owner_ref = null
equipped_item = null
When Character is freed, Equipment's owner_ref automatically becomes invalid (null).
Solution 2: Loose Coupling with Signals
Signals are the most Godot-like way to manage dependencies.
Practical Example: Player HP and UI
# Player.gd (Signal emitter)
extends CharacterBody3D
signal hp_changed(current_hp: int, max_hp: int)
var max_hp: int = 100
var hp: int = max_hp:
set(value):
hp = clamp(value, 0, max_hp)
hp_changed.emit(hp, max_hp)
func take_damage(amount: int):
self.hp -= amount
# HUD.gd (Signal receiver)
extends Control
@onready var hp_bar: ProgressBar = $HPBar
func _ready():
var player = get_tree().get_first_node_in_group("players")
if player:
player.hp_changed.connect(_on_player_hp_changed)
func _on_player_hp_changed(current_hp: int, max_hp: int):
hp_bar.max_value = max_hp
hp_bar.value = current_hp
Player doesn't know HUD exists - this is the beauty of loose coupling. The dependency is always one-way: "HUD → Player".
Solution 3: Autoload (Singleton)
Functionality shared across the entire game should be made into a singleton with Autoload.
Event Bus Pattern
Using Autoload as a global event bus enables communication across scenes.
# EventBus.gd (Autoload)
extends Node
signal enemy_defeated(position: Vector3, score_value: int)
signal item_collected(item_id: String)
# Enemy.gd (Signal emitter)
func die():
EventBus.enemy_defeated.emit(self.global_position, 100)
queue_free()
# ScoreManager.gd (Signal receiver)
func _ready():
EventBus.enemy_defeated.connect(_on_enemy_defeated)
func _on_enemy_defeated(position: Vector3, score_value: int):
Global.score += score_value
Enemy doesn't know ScoreManager exists. It just posts to the "bulletin board."
Summary
| Pattern | Primary Use | Benefits | Caveats |
|---|---|---|---|
| WeakRef | When mutual references are needed | Directly breaks cycles | Must check for null |
| Signals | Loose coupling between objects | Dependencies are one-way | Hard to track when many connections |
| Autoload | Global state management, event bus | Accessible from anywhere | Avoid making it a "do-everything" object |
Next steps:
- Observer pattern: The classic design pattern behind signals
- Using ResourceLoader: Load resources only when needed to optimize memory