Signals, Resources, and Godot's Data Model

Module 1 ยท Lesson 3 ยท ~40 min ยท Godot 4.x

Two concepts that will define your code architecture: signals (how nodes communicate) and resources (how data is stored, shared, and serialized). If you nail these two, you'll write idiomatic Godot from day one.

Signals: pub/sub built into every node

A signal is an emitted event other nodes can listen to. Every built-in node type has some: Button has pressed, Timer has timeout, Area2D has body_entered. You also declare your own.

# Declaring a signal โ€” goes at the top of the script.
signal card_played(card: Card, target: Node)
signal health_changed(new_hp: int)
signal duel_finished

# Emitting โ€” from anywhere in your node.
func play_card(card: Card) -> void:
    card_played.emit(card, self)

# Connecting a listener โ€” usually in _ready of the parent/owner.
func _ready() -> void:
    $Button.pressed.connect(_on_button_pressed)
    $Timer.timeout.connect(_on_timer_timeout)

func _on_button_pressed() -> void:
    print("clicked")

func _on_timer_timeout() -> void:
    print("tick")
If you've wired up event emitters in Node.js or observed a ViewModel in SwiftUI, you know this. The differences: signals are first-class language constructs, you can type their arguments, and the editor has a UI to connect them without writing code (which you'll sometimes use but mostly shouldn't).

Why signals matter for architecture

Signals let children talk to parents without children knowing the parent exists. A Card emits card_played. Whoever is listening โ€” the Hand, the CombatManager, an AudioPlayer โ€” handles it. The Card has no imports, no coupling. You can instance the same Card scene in a menu or a battle scene and it just works.

The rule of thumb: parents know about children (by owning them); children talk to parents via signals.

# BAD โ€” Card reaches up and knows too much.
func play() -> void:
    get_parent().get_parent().combat_manager.register_play(self)

# GOOD โ€” Card announces, the rest of the system listens.
func play() -> void:
    card_played.emit(self)

Lambdas and Callables

Signal handlers are Callable values. You can use named methods or lambdas:

$Button.pressed.connect(func(): print("clicked"))
$Button.pressed.connect(_on_button_pressed)

# You can disconnect too.
$Button.pressed.disconnect(_on_button_pressed)

# Bind extra args into the callable.
$Button.pressed.connect(_on_card_chosen.bind(card_index))
func _on_card_chosen(index: int) -> void:
    print("chose card ", index)

await: signals as coroutines

One of the most delightful features. You can await a signal like it's a Promise:

func play_turn_sequence() -> void:
    show_card()
    await get_tree().create_timer(0.5).timeout  # wait half a second
    apply_damage()
    await animation_finished
    next_turn()

This lets you write sequential-looking code for inherently async game flow. You'll use this in turn-based combat (Module 5) heavily โ€” draw card, wait for animation, apply effect, wait for confirmation, end turn.

Resources: the data class you're missing

A Resource is a serializable, shareable, reference-counted data object. Think of it as a hybrid between a data class and a JSON file. You write the class in code, then create instances in the editor, which saves them as .tres (or .res) files.

# scripts/card_data.gd
class_name CardData
extends Resource

@export var letter: String = "A"
@export var value: int = 1
@export var rarity: int = 0
@export var art: Texture2D

# Methods work. Just data + behavior.
func describe() -> String:
    return "%s (%d pts)" % [letter, value]

Now in the editor: FileSystem โ†’ right-click โ†’ New โ†’ Resource โ†’ CardData. Fill in the Inspector. Save as cards/a_tile.tres. You've just created a data file, no JSON parser required.

In code:

# Load at runtime.
var card_data: CardData = load("res://cards/a_tile.tres")

# Preload at parse time โ€” faster, errors early.
const A_TILE := preload("res://cards/a_tile.tres")

# Create in memory without saving.
var dynamic_card := CardData.new()
dynamic_card.letter = "Z"
dynamic_card.value = 10
If you've worked in Rails, Resources are like fixtures but type-checked. If you've worked in Unity, they're Unity's ScriptableObjects โ€” same idea, same superpower. Designers edit data in the Inspector, programmers read it in code, and nobody has to touch a JSON schema.

Why Resources are a superpower

  1. Designer-friendly. You edit values in the Inspector. No code changes to tweak balance.
  2. Version-control friendly. .tres files are text. Diff them. Merge them.
  3. Shareable. One .tres can be loaded into many scenes. Change the file, they all update.
  4. Instanceable. .duplicate() gives you a copy if you need a per-instance mutation.
  5. Nestable. A Resource can have a property whose type is another Resource. Entire data structures, editable in the Inspector.
# scripts/enemy_data.gd
class_name EnemyData
extends Resource

@export var name: String
@export var max_hp: int
@export var deck: Array[CardData]   # yes, a typed array of another Resource type
@export var ai_strategy: int        # enum index

This lets you define a new enemy without writing code. In the editor you drag card .tres files into the deck array on an enemy.tres. Ship it.

RefCounted vs Node vs Resource

ClassIn tree?MemoryUse for
NodeYesManual (queue_free)Things that tick, render, take input
ResourceNoRef-countedData. Saveable. Shareable.
RefCountedNoRef-countedTransient helper objects
ObjectNoManual (free)You almost never use this directly.
Memory gotcha Nodes are not reference-counted. If you remove a node from the tree and nothing else holds a reference, it leaks unless you queue_free() it. Resources and RefCounteds handle themselves. When in doubt: queue_free() when a node's life is over.

The mental model you're building

That triangle โ€” nodes + signals + resources โ€” is 80% of Godot game architecture. The remaining 20% is game-specific systems you'll build on top (turn manager, combat, UI state), and you'll build every one of them using these three primitives.

Do this now

  1. Create scripts/card_data.gd in your project with the CardData code above.
  2. In the FileSystem dock, right-click your project folder, create a folder called cards.
  3. In cards/, right-click โ†’ New โ†’ Resource. Pick CardData. Fill in letter: "A", value: 1. Save as a_tile.tres.
  4. Open a_tile.tres in a text editor. See how it serialized.
  5. In a test scene's script, do print(load("res://cards/a_tile.tres").describe()). Run it.

You now know enough to do the exercise.