【Godot】Dependency Management in Godot: Design Patterns to Avoid Cyclic References

Created: 2025-12-10

Design patterns to prevent memory leaks from cyclic references. Learn how to use WeakRef, signals, and Autoload.

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:

  1. Node derived classes (objects belonging to scene tree):
    • Usually automatically freed when removed from scene tree (queue_free()).
  2. RefCounted derived 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 with queue_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 like RefCounted/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() after ObjectA has been freed, object_a_ref is already invalid, so get_ref() returns null. Therefore, callers must always do existence checks with is_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:

  1. Open "Project" → "Project Settings" → "Autoload" tab
  2. Specify the script file in "Path" (e.g., res://scripts/GameManager.gd)
  3. Enter any name in "Node Name" (e.g., GameManager)
  4. 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/SaveDataManager and 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.

ProblemSolutionGodot Implementation
Memory leaks from mutual referencesUse references that don't increase reference countsUse WeakRef class
Strong coupling between nodesBreak dependencies with event-based communicationUse signals
Access to global functionalityEstablish one-directional dependenciesUse 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.