Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. GDScript
  3. GDScript Resources Practice
GDScript33 exercises

GDScript Resources Practice

Custom Resource classes, @export arrays, load/save .tres, and how to fix shared-mutation bugs with duplicate() and local_to_scene (including array caveats). Tested with Godot 4.3+.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Define `UpgradeData` as a custom `Resource` class for upgrade config (include both `extends` and `class_name`).

On this page
  1. 1Quick Symptoms → Fixes
  2. 2Resources vs Nodes
  3. 3Creating a Custom Resource
  4. 4Loading Resources (load vs preload)
  5. 5Saving and Loading .tres Files
  6. 6The Shared Reference Bug
  7. Fix 1: Treat Resources as Immutable (Recommended)
  8. Fix 2: Duplicate at the Mutation Boundary
  9. 7resource_local_to_scene (Useful, but Not Magic)
  10. 8Deep-Duplicate Caveat (Read This If You Nest Resources)
  11. 9Debugging: Are We Sharing the Same Resource?
  12. 10Editor Updates: emit_changed()
Quick Symptoms → FixesResources vs NodesCreating a Custom ResourceLoading Resources (load vs preload)Saving and Loading .tres FilesThe Shared Reference Bugresource_local_to_scene (Useful, but Not Magic)Deep-Duplicate Caveat (Read This If You Nest Resources)Debugging: Are We Sharing the Same Resource?Editor Updates: emit_changed()

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.

Related GDScript Topics
GDScript ExportsGDScript Arrays & LoopsGDScript Timers & Signals

SymptomFix
Two enemies share HPTreat the Resource as a definition, store runtime state separately (or duplicate() at spawn)
"Local to scene" didn't helpExplicit duplicate() when you need per-instance mutation
Inventory item edits affect every itemDuplicate at the mutation boundary (when you add to inventory/equip)
duplicate(true) didn't deep-copy nested ResourcesKeep nested subresources as direct properties, not inside Arrays/Dicts (see caveat below)

Ready to practice?

Start practicing GDScript Resources with spaced repetition

Resources are lightweight data assets (definitions, configs, items). Nodes are for behavior in the scene tree. If it doesn't need _process or _ready, it should be a Resource.

ResourcesNodes
Data containersBehavior in scene tree
Shared by defaultInstanced per scene
No _process, no lifecycleFull lifecycle
Great for .tres assetsGreat 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.


Resources are shared by default. Treat them as immutable definitions—store runtime state in separate vars. Only duplicate() at the mutation boundary when you need per-instance state.

Resources are shared by default. If two enemies reference the same stats Resource and one modifies it, both see the change:

# BUG: Editing enemy_a's stats affects enemy_b!
enemy_a.stats.hp -= 10

Fix 1: Treat Resources as Immutable (Recommended)

var current_hp: int

func _ready() -> void:
    current_hp = stats.max_hp  # Copy the value, don't modify the Resource

Fix 2: Duplicate at the Mutation Boundary

func _ready() -> void:
    stats = stats.duplicate() as EnemyStats

When to duplicate: At the exact moment you need unique state—not earlier. For inventories, that's when an item is added/equipped.


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_scene is 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) does NOT deep-copy Resources inside Arrays or Dictionaries. Manually duplicate nested resources, or keep subresources as direct properties.

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:

  1. Keep nested subresources as direct properties, not inside arrays/dicts
  2. 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

Prompt

Check Your Understanding: Why use a Resource instead of a Node for data?

What a strong answer looks like

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

Define a custom Resource with class_name and @export fieldsExport Resource references and arrays in Node scriptsLoad/preload Resources and use them as immutable definitionsDuplicate resources at the mutation boundary for per-instance stateSave/load .tres data with ResourceSaver and load()Use emit_changed() in setters for custom resources that drive editor tools

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

Custom upgrade Resource
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
Load/preload patterns
# 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/load .tres file
# Save config to user directory
ResourceSaver.save(settings, "user://settings.tres")

# Load it back
var settings := load("user://settings.tres") as GameSettings
Duplicate for mutable state
var stats: EnemyStats

func _ready() -> void:
	# Each instance gets its own copy
	stats = base_stats.duplicate() as EnemyStats
Manual deep copy (array workaround)
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
emit_changed() for editor tools
extends Resource
class_name EnemyStats

@export var max_hp: int = 100:
	set(value):
		max_hp = value
		emit_changed()  # Updates Inspector previews

GDScript Resources Sample Exercises

Example 1Difficulty: 2/5

Declare an exported variable called upgrade of type UpgradeData.

@export var upgrade: UpgradeData
Example 2Difficulty: 2/5

Get 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)
Example 3Difficulty: 1/5

What does this code print?

+ 30 more exercises

Quick Reference
GDScript Resources Cheat Sheet →

Copy-ready syntax examples for quick lookup

Practice in Build a Game

Roguelike: Part 3: The Dungeon BreathesBuild a GameRoguelike: BSP DungeonsBuild a Game

Further Reading

  • GDScript Dictionary map() and map_in_place12 min read
  • Facade Pattern in Godot 4 GDScript: Taming "End Turn" Spaghetti12 min read

Related Design Patterns

Builder Pattern

Start practicing GDScript Resources

Free daily exercises with spaced repetition. No credit card required.

← Back to GDScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.