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.
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")
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)
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)
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.
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
.tres files are text. Diff them. Merge them..tres can be loaded into many scenes. Change the file, they all update..duplicate() gives you a copy if you need a per-instance mutation.# 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.
| Class | In tree? | Memory | Use for |
|---|---|---|---|
Node | Yes | Manual (queue_free) | Things that tick, render, take input |
Resource | No | Ref-counted | Data. Saveable. Shareable. |
RefCounted | No | Ref-counted | Transient helper objects |
Object | No | Manual (free) | You almost never use this directly. |
queue_free() it. Resources and RefCounteds handle themselves. When in doubt: queue_free() when a node's life is over.
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.
scripts/card_data.gd in your project with the CardData code above.cards.cards/, right-click โ New โ Resource. Pick CardData. Fill in letter: "A", value: 1. Save as a_tile.tres.a_tile.tres in a text editor. See how it serialized.print(load("res://cards/a_tile.tres").describe()). Run it.You now know enough to do the exercise.