G1.6 — Custom Resources & .tres¶
What you'll learn
- Distinguish a
Resource(shareable data) from aNode(a thing in the tree). - Define a custom
Resourcewithclass_name+@export, and save/edit it as a.tres. - Reference a resource from a node via an exported slot, and share one
.tresacross many nodes. - Use
.duplicate()to give one instance its own copy of otherwise-shared data.
How it applies
- Hardcoded data needs a programmer for every tweak. Stats, item definitions, and tuning baked
into scripts cannot be balanced by anyone but a coder. A custom
Resourceturns that data into Inspector-editable.tresfiles a designer can adjust without touching code — the data/code seam at the level of whole data objects. - Shared resources are shared — mutating one affects all. A
Resourceis a reference type (L1.5). If fifty enemies reference the same stat.tresand one lowers its health at runtime, all fifty change. That is either exactly what you want (tune once, all follow) or a bug (one elite was supposed to differ), and.duplicate()is the switch between them. - Confusing Resources with Nodes wastes effort. Trying to add a
Resourceto the scene tree, or using aNodewhere you just need a bag of data, fights the engine. Resources are data; nodes are participants in the tree. - Authored data and saved data are different files. A
.tresunderres://is authored content, read-only at runtime in an export (G1.1). Runtime save state belongs underuser://, not written back into ares://resource — the game books' save chapters depend on this split.
Try it first
Every goblin should share one base stat block — health, damage, speed — that you can retune in a single place and have all goblins follow. But one elite goblin needs +50% health without changing the others. Before reading on: how would you model data that is shared by default but overridable per instance? What type holds the data, and what do you do for the elite?
Concepts¶
Resource versus Node¶
You have spent G1.1–G1.5 on nodes — things that live in the tree, draw, and run. A Resource
is the other half: a data container. It does not live in the tree or do anything on its own; it
holds values, can be saved to a file (.tres), and — crucially — can be shared between nodes.
Godot's built-in resources include textures, audio streams, and fonts; you can define your own.
The rule of thumb: if it acts in the scene, it is a node; if it is data that nodes read, it is a resource.
Defining a custom Resource¶
A custom resource is a script that extends Resource, named with class_name, exposing @export
fields:
class_name StatBlock
extends Resource
@export var max_health: int = 10
@export var damage: int = 1
@export var move_speed: float = 100.0
Because it has class_name (L2.1), StatBlock appears in the editor's Create Resource dialog;
because its fields are @exported (L2.2), they are editable in the Inspector. You can now create
.tres files of this type and a designer can fill in the numbers — no code edit per stat change.
Creating and editing a .tres¶
In the FileSystem dock, right-click → New Resource, choose your StatBlock type, and save it
as, say, res://data/goblin_stats.tres. Select it and the Inspector shows your exported fields, ready
to edit. That file is authored data, versioned and shipped under res://.
Referencing a resource from a node¶
A node holds a resource through an exported slot of that type:
extends Node2D
@export var stats: StatBlock # an empty slot in the Inspector...
func _ready() -> void:
print(stats.max_health) # ...drag goblin_stats.tres into it in the editor
In the Inspector, the stats field is a slot you drag a .tres into (or create one inline). Several
goblin scenes can point their stats slot at the same goblin_stats.tres, and now one edit to that
file retunes every goblin — the answer to the "Try it first" prompt's first half.
Sharing versus copying: the per-instance override¶
Because a Resource is a reference type, every node pointing at goblin_stats.tres shares the one
object. Tuning the file in the editor is fine (it retunes all, as intended). But mutating it at
runtime mutates it for everyone — and that is the second half of the prompt: the elite goblin needs
+50% health without affecting the rest. The fix is .duplicate() (L1.5):
func _ready() -> void:
stats = stats.duplicate() # this goblin now owns a private copy
stats.max_health = int(stats.max_health * 1.5) # safe: only the elite changes
Example
Shared-by-default, overridable-per-instance, in two lines:
# all goblins reference goblin_stats.tres → tuning the file retunes all (shared)
@export var stats: StatBlock
# the elite, in _ready, breaks off its own copy before changing it:
stats = stats.duplicate()
stats.max_health = 60
Without the duplicate(), stats.max_health = 60 would rewrite the shared goblin_stats object
and turn every goblin into a 60-HP elite — the aliasing defect from L1.5, now wearing a resource.
.duplicate() is the deliberate "give this one its own data" move; use .duplicate(true) if the
resource contains nested resources or arrays that must also be independent.
Authored data is not save data¶
A .tres under res:// is authored content — read-only at runtime in an exported build (G1.1). Do
not write runtime progress back into it. Runtime save state goes under user://, typically as JSON or
a separately-managed save resource; the game books devote whole modules to this. Here the point is only
the split: .tres files are the designer's data, loaded at runtime; the player's changing state is
saved elsewhere.
Walkthrough¶
- Create
res://stat_block.gdwithclass_name StatBlock,extends Resource, and a couple of@exportfields (max_health,damage). Save. - In the FileSystem dock, right-click → New Resource, pick
StatBlock, save asres://goblin_stats.tres. Select it and set the fields in the Inspector. - Make a
Node2Dscene with a script that has@export var stats: StatBlock. Select the node and draggoblin_stats.tresinto thestatsslot. In_ready,print(stats.max_health); run with F6. - Demonstrate sharing and the override: instance the goblin twice (both pointing at the same
.tres). In_ready, on one of them only, dostats = stats.duplicate()then changestats.max_health, and print both goblins' values to confirm only the duplicated one changed.
Optional sanity check
Remove the .duplicate() and instead directly set stats.max_health = 999 in one goblin's
_ready. Print the other goblin's stats.max_health — it is also 999, because both share the
one resource object. Put .duplicate() back and the other goblin is unaffected. That is
reference-sharing versus copying, on a resource.
Self-check quiz¶
Q1 — What is the difference between a Node and a Resource?
A. None; they are interchangeable.
B. A Node lives in the scene tree and acts; a Resource is shareable data that does not live in the tree.
C. A Resource can have children; a Node cannot.
D. A Node is saved to disk; a Resource is not.
Reveal answer
B. Nodes participate in the tree (draw, run, receive callbacks); resources are data objects
that nodes reference and that can be saved as .tres. A is false — they have distinct roles. C
inverts it (nodes have children, resources do not). D is backwards — resources are exactly the
things saved as .tres/.res.
Q2 — Fifty enemies reference the same stats.tres. One does stats.max_health = 1 at runtime (no duplicate). What happens?
A. Only that enemy's health changes.
B. All fifty change, because they share one resource object (a reference type); mutating it mutates it for everyone.
C. It errors — resources are immutable at runtime.
D. The .tres file on disk is rewritten.
Reveal answer
B. A Resource is a reference type, so all references point at one object; mutating it at
runtime affects every holder — the aliasing rule from L1.5. A would require a .duplicate()
first. C is false — resource fields are mutable at runtime. D is false — in-memory mutation does
not write the file (and res:// is read-only in an export anyway).
Q3 — How do you let designers tune an enemy's stats without editing code?
A. Hardcode the stats as const in the script.
B. Define a custom Resource with @export fields, save instances as .tres, and reference them from the node — the Inspector then edits the data.
C. Put the stats in an autoload.
D. Write the stats to user:// at runtime.
Reveal answer
B. A custom Resource with exported fields gives Inspector-editable .tres data files that
designers adjust without touching code — the data/code seam. A keeps the data in code (the
opposite of the goal). C is for global runtime state, not per-enemy authored data. D is for
runtime save state, not authored tuning.
Integration question¶
Q4 — open
Pull the G1 module together. You are building a wave of enemies: a single enemy.tscn (G1.3)
instanced many times by a spawner, each enemy referencing a shared enemy_stats.tres custom
Resource (this chapter), with a global Game autoload (G1.5) tracking the kill count, and the
enemy's nodes addressed by @onready/unique names rather than absolute paths (G1.4). The designer
wants most enemies to share one stat block (tune once, all follow) but the wave's final boss to have
double health, and the kill count must survive the player retrying the level. Explain which G1
mechanism handles each requirement, and where .duplicate() versus a shared reference is the right
call.
Reveal expected answer
One enemy design, many copies: the enemy is a scene (enemy.tscn) instanced by the spawner
with instantiate() + add_child (G1.3); editing the scene updates all instances. Shared,
retunable stats: a custom Resource (enemy_stats.tres) referenced through each enemy's
exported stats slot — every enemy points at the one object, so tuning the .tres in the
Inspector retunes all of them (shared reference is correct here). The boss's double health:
the boss must not share, so in its _ready it breaks off its own copy —
stats = stats.duplicate() then stats.max_health *= 2 — which changes only the boss and leaves
the shared block intact (this is where .duplicate() is the right call, the L1.5 aliasing rule
applied to a resource). Kill count surviving a retry: the count lives in the Game autoload
(G1.5), which persists across scene reloads, rather than on the level scene which is freed on
retry. Robust references: the enemy reaches its own child nodes by @onready/unique names
(G1.4), not absolute paths, so restructuring the enemy scene does not break it. Each requirement
maps to exactly one G1 mechanism — scene/instancing for reuse, custom Resource for shared data,
.duplicate() for the per-instance exception, autoload for persistence, and stable addressing
for maintainability — which together are the project model the game books assume from page one.