Can you write this from memory?
Define `UpgradeData` as a custom `Resource` class for upgrade config (include both `extends` and `class_name`).
Resources are Godot's lightweight data assets: upgrades, items, enemy definitions, wave configs, and tuning knobs you want editable in the inspector—without hard-coding values in scripts.
This page focuses on the stuff that breaks real projects: shared-state bugs, safe mutation patterns, and the "deep duplicate" gotchas that still catch experienced developers.
| Symptom | Fix |
|---|---|
| Two enemies share HP | Treat the Resource as a definition, store runtime state separately (or duplicate() at spawn) |
| "Local to scene" didn't help | Explicit duplicate() when you need per-instance mutation |
| Inventory item edits affect every item | Duplicate at the mutation boundary (when you add to inventory/equip) |
duplicate(true) didn't deep-copy nested Resources | Keep nested subresources as direct properties, not inside Arrays/Dicts (see caveat below) |
| Resources | Nodes |
|---|---|
| Data containers | Behavior in scene tree |
| Shared by default | Instanced per scene |
| No _process, no lifecycle | Full lifecycle |
| Great for .tres assets | Great for scenes |
Mental model: Resource = definition (immutable). Node = behavior. Runtime state = separate vars or component. When you compose multiple Resources to configure a game entity, you're applying the builder pattern—assembling a complex object step by step.
For a printable quick reference of Resource patterns, see the GDScript resources cheat sheet.
# upgrade_data.gd
extends Resource
class_name UpgradeData
@export var id: String = ""
@export var display_name: String = ""
@export var icon: Texture2D
@export var damage_bonus: float = 0.0
@export var cooldown_multiplier: float = 1.0
With class_name, this shows up in "New Resource" and can be exported:
# In player script
@export var upgrades: Array[UpgradeData] = []
See @export annotations for range constraints and Inspector organization.
Use preload() when the file is known at compile time. Use load() when the path is dynamic (mods, user content).
# Compile-time: faster, checked by editor
const FIRE_UPGRADE := preload("res://data/upgrades/fire.tres")
# Runtime: for dynamic paths
func load_upgrade(path: String) -> UpgradeData:
return load(path) as UpgradeData
Note: preload() is resolved at parse time—the resource is already in memory when the script loads. load() fetches from disk at runtime.
Resources are great for config/save data. They serialize all @export properties automatically:
# Save
ResourceSaver.save(save_data, "user://savegame.tres")
# Load
var save_data := load("user://savegame.tres") as SaveGameData
For more complex save systems, see the GDQuest resource-based save tutorial.
Setting resource_local_to_scene = true tells Godot to duplicate the Resource per scene instance. However:
- Only works for exported resources in the Inspector, not runtime-created ones
- Broken with Arrays/Dicts — if the Resource is inside an array,
local_to_sceneis ignored (GitHub Issue #71243, still open in 4.3) - Can cause unexpected behavior with nested resources
When in doubt: duplicate() explicitly where you need unique state.
duplicate(true) is supposed to deep-copy subresources, but it doesn't work for subresources inside Arrays/Dictionaries. This is a confirmed bug with 100+ reactions, targeted for Godot 4.5.
# inventory.gd
extends Resource
@export var items: Array[ItemData] = []
# BUG: items inside the array are NOT duplicated
var copy := inventory.duplicate(true) # items still share references!
Workarounds:
- Keep nested subresources as direct properties, not inside arrays/dicts
- Manually duplicate nested resources when creating copies:
func deep_copy_inventory(inv: Inventory) -> Inventory:
var copy := inv.duplicate() as Inventory
copy.items = []
for item in inv.items:
copy.items.append(item.duplicate() as ItemData)
return copy
print(enemy_a.stats.get_instance_id())
print(enemy_b.stats.get_instance_id())
# Same ID = same object = shared state risk
# Check where a resource came from
print(stats.resource_path) # Empty if created at runtime
If you're building editor tooling or want Inspector previews to update when a Resource changes, call emit_changed() from setters:
extends Resource
class_name EnemyStats
@export var max_hp: int = 100:
set(value):
max_hp = value
emit_changed() # Notifies editor and dependent objects
This is essential for custom resources that drive tool scripts or @tool editor plugins.
When to Use GDScript Resources
- Upgrade systems, item definitions, enemy stats, wave configs
- Tunable gameplay parameters you want editable by non-programmers
- Save/load systems where you want automatic serialization
- Reducing giant scripts by moving data into inspector-editable assets
Check Your Understanding: GDScript Resources
Check Your Understanding: Why use a Resource instead of a Node for data?
Resources are lightweight, serializable data assets you can reuse across scenes and edit in the inspector. Nodes are for behavior in the scene tree. Using Resources for definitions keeps gameplay logic clean and lets you iterate on values without touching code.
What You'll Practice: GDScript Resources
Common GDScript Resources Pitfalls
- Mutating a shared Resource at runtime—unexpected global changes; use duplicate() for per-instance state
- Expecting duplicate(true) to deep-copy Resources inside Arrays/Dicts—it doesn't; manually duplicate nested resources
- Relying on resource_local_to_scene for array elements—it's ignored; use explicit duplicate()
- Using Resources for behavior (logic)—keep behavior in Nodes/scripts; keep Resources as data
- Forgetting that preload() happens at parse time—can't use variables in the path
GDScript Resources FAQ
Why did changing a Resource affect multiple enemies?
Resources are shared by default: if multiple instances reference the same Resource, mutating it changes it everywhere. Treat Resources as immutable definitions, or duplicate() them for per-instance state.
Why didn't duplicate(true) fully deep-copy my nested Resources?
Deep duplication has a known bug: subresources inside Arrays/Dictionaries are NOT duplicated (GitHub Issue #74918). Either keep nested resources as direct properties (not in arrays/dicts), or manually duplicate each nested resource.
Why didn't resource_local_to_scene help with my array of Resources?
local_to_scene is ignored for Resources inside Arrays/Dictionaries (GitHub Issue #71243). Use explicit duplicate() instead when you need per-instance state for array elements.
What's the difference between load() and preload()?
preload() resolves at parse time—the resource is in memory when the script loads. load() fetches from disk at runtime. Use preload for known assets, load for dynamic paths (mods, user content).
How do I save/load a Resource to disk?
Use ResourceSaver.save(resource, "user://path.tres") to save and load("user://path.tres") to load. Resources serialize all @export properties automatically. Great for config/save data.
How do I make a custom Resource show up in "New Resource…"?
Give it a class_name and extend Resource. Godot will list it in the resource creation dialog and allow it in @export type hints.
How do I know if two variables point to the same Resource?
Compare get_instance_id(): if enemy_a.stats.get_instance_id() == enemy_b.stats.get_instance_id(), they share the same object. Same ID = shared state risk.
When should I call emit_changed() on a Resource?
Call emit_changed() from setters when building @tool scripts or editor plugins that need to update when Resource properties change. Not needed for runtime-only code.
Resource vs Node—when should it be data?
If it doesn't need _process, _ready, or to exist in the scene tree, it should probably be a Resource. Nodes are for behavior; Resources are for data.
GDScript Resources Syntax Quick Reference
extends Resource
class_name UpgradeData
@export var id: String = ""
@export var display_name: String = ""
@export var damage_bonus: float = 0.0
@export var cooldown_multiplier: float = 1.0# Compile-time (faster, editor-checked)
const FIRE := preload("res://data/upgrades/fire.tres")
# Runtime (for dynamic paths)
func load_upgrade(path: String) -> UpgradeData:
return load(path) as UpgradeData# Save config to user directory
ResourceSaver.save(settings, "user://settings.tres")
# Load it back
var settings := load("user://settings.tres") as GameSettingsvar stats: EnemyStats
func _ready() -> void:
# Each instance gets its own copy
stats = base_stats.duplicate() as EnemyStatsfunc deep_copy_inventory(inv: Inventory) -> Inventory:
var copy := inv.duplicate() as Inventory
copy.items = []
for item in inv.items:
copy.items.append(item.duplicate() as ItemData)
return copyextends Resource
class_name EnemyStats
@export var max_hp: int = 100:
set(value):
max_hp = value
emit_changed() # Updates Inspector previewsGDScript Resources Sample Exercises
Declare an exported variable called upgrade of type UpgradeData.
@export var upgrade: UpgradeDataGet the 'damage' value from the stats dictionary with a default of 0, storing it in a variable called dmg.
var dmg = stats.get("damage", 0)What does this code print?
+ 30 more exercises