Skip to content

G1.6 — Custom Resources & .tres

What you'll learn

  • Distinguish a Resource (shareable data) from a Node (a thing in the tree).
  • Define a custom Resource with class_name + @export, and save/edit it as a .tres.
  • Reference a resource from a node via an exported slot, and share one .tres across 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 Resource turns that data into Inspector-editable .tres files 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 Resource is a reference type (L1.5). If fifty enemies reference the same stat .tres and 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 Resource to the scene tree, or using a Node where 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 .tres under res:// is authored content, read-only at runtime in an export (G1.1). Runtime save state belongs under user://, not written back into a res:// 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

  1. Create res://stat_block.gd with class_name StatBlock, extends Resource, and a couple of @export fields (max_health, damage). Save.
  2. In the FileSystem dock, right-click → New Resource, pick StatBlock, save as res://goblin_stats.tres. Select it and set the fields in the Inspector.
  3. Make a Node2D scene with a script that has @export var stats: StatBlock. Select the node and drag goblin_stats.tres into the stats slot. In _ready, print(stats.max_health); run with F6.
  4. Demonstrate sharing and the override: instance the goblin twice (both pointing at the same .tres). In _ready, on one of them only, do stats = stats.duplicate() then change stats.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.