UpgradeData Resource¶
What you'll learn
- The
Resourcebase class — Godot's container for data without behavior — and howextends Resourcediffers fromextends Node. - The
@exportannotation, which exposes a property to the Inspector for editing without code. - How a
.tresfile is an instance of aResourcesubclass: pure data, saved as text, diff-friendly, hot-reloadable. - The difference between
preload(...)(parse-time, path is a literal) andload(...)(runtime, path can be variable) — and whypreloadis the default for known assets. - How to define
UpgradeDataonce, then author a Blessing and a Sermon as two.tresfiles in the Inspector — without writing a second script.
How it applies
- Designer-editable balance. Cost values, multipliers, descriptions, icons, and unlock thresholds live in
.tresfiles editable in the Inspector. A designer or QA tester adjusts a Blessing's cost from50.0to60.0by clicking a field; the change is one diff in one file. Engineering does not gate balance changes; balance changes do not require a recompile. - Save-format simplicity. When M7 saves the player's purchased upgrades, it saves the list of
UpgradeDatareferences (or theiridstrings), not a copy of each upgrade's fields. The fields live in the.tresfiles; the save just records "the player owns these." A balance update that changes a Blessing's effect propagates to existing saves automatically. - Modding seam. A modder adding a new Blessing drops a new
.tresinto a watched folder; the upgrade list (M4.2) iterates the folder and picks up the new entry. The modder writes zero.gdcode if the existingUpgradeDatafields cover their case. This is the "data-driven content" pattern most idle and roguelike games rely on. - Localization. A
display_name: Stringfield is a translation key by default. The Inspector can hold the English string; M-late localization can lookup the key in a translation table at display time. The data shape carries the locale-independent identity (id); the locale-dependent fields (display_name,description) are swapped per language. - Test isolation. A unit test for "purchasing a Blessing applies its multiplier" can construct a synthetic
UpgradeDatawithUpgradeData.new(), set fields directly, and pass it to the purchase code. The test does not need a.tresfile on disk; the same class shape works in-memory. The data type and the file format are decoupled.
Concepts¶
Resource — data without behavior¶
Resource is Godot's base class for data-only objects. Unlike Node, a Resource has:
- No
_process, no_ready, no scene-tree position. Resources don't run per-frame logic. They sit in memory, fields read by callers, fields written by the Inspector. - No
add_child/parent. AResourceis not a node. It has no children, no transform, no input handling. - Reference-counted lifetime. When the last reference to a
Resourcegoes out of scope, the engine frees it automatically. Noqueue_free(), no manual delete. - Built-in disk serialization. Every
Resourceknows how to save itself as.tres(text) or.res(binary). The Inspector's "Save As…" button writes the current field values to disk in either format.
Use a Resource for anything that is configuration rather than behavior: upgrade definitions, theme presets, audio bus configurations, level descriptors, dialogue trees. Use a Node for anything that runs over time: the Light counter, the Tick autoload, UI Labels.
Example
You declare class_name UpgradeData extends Resource, give it five fields, and create three .tres files for three different upgrades. The class definition is one .gd file with no methods (just @export declarations); the data lives in three .tres files. Compare to a hypothetical extends Node design where each upgrade is a node in a scene — N nodes for N upgrades, scene-tree clutter, no Inspector preview of the data outside the scene. The Resource design separates "what an upgrade is" (the class) from "the specific upgrades the project ships with" (the .tres files).
extends Resource + class_name¶
The two-line preamble:
Together they say: "this script defines a class called UpgradeData, and it is a subclass of Resource." After saving the script, the project's Inspector knows about a new class — when you create a new resource via FileSystem dock → right-click → New Resource…, UpgradeData appears in the list of creatable types.
class_name is the same directive M2.2 introduced for Light. The difference is the parent class. Light extended Node because it lived in the scene tree and emitted signals over time; UpgradeData extends Resource because it lives in a .tres file and is read-only at runtime.
Example
You forget class_name. The .gd file still parses, the class still extends Resource, but the FileSystem dock's "New Resource" dialog does not list it — you cannot create .tres instances through the editor without class_name. The fix is the one-line addition. Without class_name you can still construct instances in code (var u = preload("res://scripts/upgrade_data.gd").new()), but the editor-driven authoring flow this chapter relies on is closed off.
@export — Inspector-editable fields¶
@export is the annotation that makes a property editable in the Inspector:
@export var cost: float = 100.0
@export var display_name: String = "Blessing of Might"
@export var click_multiplier: float = 2.0
The default value (right of the =) is what the field shows when a new .tres is created. Once authored in the Inspector, the value is stored in the .tres file and overrides the default for that instance.
@export has variant forms that customize the editor widget:
@export_range(0.0, 1000.0, 5.0)— slider with min, max, step.@export_enum("Blessing", "Sermon", "Relic")— dropdown.@export_color— color picker instead of plainColor.@export_file("*.png", "*.webp")— file picker, filtered by extension.
For numeric fields where any reasonable value is allowed, plain @export var x: float is enough. For fields where the designer needs guard rails, the constrained variants pay for themselves quickly.
Example
You write @export var cost: float. The Inspector shows a free-form number field — −500.0 is acceptable, 100000000.0 is acceptable. A designer accidentally types a negative cost; the upgrade can be "purchased" for negative Light, granting Light. The fix is @export_range(0.0, 1000000.0, 1.0) var cost: float = 100.0 — the slider clamps to the valid range, and free-form entry into the field still respects the bounds. Constraints in the data type save bug-hours later.
.tres files — Resource instances on disk¶
A .tres file is one instance of a Resource subclass, stored as text. The Inspector's Save action writes the current field values to disk; the FileSystem dock's "New Resource…" button creates a fresh instance with default values.
The file format is human-readable INI-like:
[gd_resource type="UpgradeData" script_class="UpgradeData" load_steps=2 format=3 uid="uid://abc123"]
[ext_resource type="Script" uid="uid://xyz" path="res://scripts/upgrade_data.gd" id="1_blessing"]
[resource]
script = ExtResource("1_blessing")
id = "blessing_of_might"
display_name = "Blessing of Might"
cost = 50.0
click_multiplier = 2.0
Diff-friendly: a balance change is one line in one file. Source-control-friendly: merges resolve like text. Hot-reloadable: editing a .tres while the game runs causes the editor (and, with the right setup, the running game) to pick up the new values without a restart.
The binary alternative .res is identical in semantics but smaller on disk and faster to load. Use .res for shipping builds with very large numbers of resources; use .tres during development and for any resource a human might edit.
Example
A balance pass updates twenty Blessing costs. With .tres, the diff in source control is twenty single-line cost edits, immediately reviewable. With a per-upgrade extends Node design, the same change is twenty scene-file edits, harder to review and prone to merge conflicts on unrelated scene metadata. The .tres format optimizes for "tweak a value in a file."
preload() versus load()¶
GDScript has two functions for loading resources from res:// paths:
preload(path_literal)runs at parse time (when the script is compiled). The path must be a string literal — no variables. The result is cached; multiplepreloadcalls of the same path return the same in-memory object. Used for resources known at edit time: a specific upgrade, a specific scene, a specific texture.load(path_string)runs at the call site. The path can be a variable; the runtime resolves it when the line executes. Returnsnullon failure rather than parse-erroring. Used for resources whose path is computed at runtime: save-file references, mod content, dynamic asset selection.
const BLESSING_OF_MIGHT: UpgradeData = preload("res://resources/upgrades/blessing_of_might.tres")
var dynamic_upgrade: UpgradeData = load(some_runtime_path)
preload is the default for any path you can write as a literal. The parse-time check catches typos at script-load time; the cached result avoids redundant disk reads. load is the escape hatch for paths the script cannot know in advance.
Example
You rename blessing_of_might.tres to blessing_of_strength.tres without updating the preload line. The script fails to compile with "Could not preload resource" — the error fires when the script is loaded, before any function runs. With load, the same rename produces null from the call (because the path no longer exists), and downstream code crashes when it dereferences null — a runtime error two function calls deep, harder to diagnose. The parse-time-vs-runtime distinction is also an early-vs-late error distinction, and earlier errors are cheaper.
Walkthrough¶
You will perform these in your own Godot editor. Coming in, the project has Light, Tick, and GameState autoloads, with the click-counter and Honor-resource UI from M3.3.
- In the FileSystem dock, navigate to
scripts/. Right-click → New → Script. Name itupgrade_data.gd. Click Create. The script editor opens with a generated stub. - Replace the body. The full code-fragment ceiling for this chapter, in one fragment:
Twelve
class_name UpgradeData extends Resource @export var id: String = "" @export var display_name: String = "" @export var description: String = "" @export var cost: float = 100.0 @export_enum("click", "tick") var effect_type: String = "click" @export var multiplier: float = 2.0 @export var unlock_resource: String = "light" @export var unlock_threshold: float = 0.0@exportlines, one fragment, unsplittable. Two of the lines use the constrained@export_enumform to limiteffect_typeto two valid string values via a dropdown — this is the M4.3 effect-application target. The remaining lines are unconstrained because the values are open-ended (any string id, any positive cost, any multiplier). Save (Ctrl+S). - Confirm the FileSystem dock shows
scripts/upgrade_data.gd. The class is now registered project-wide asUpgradeData. To verify: open any other script and start typingUp— the autocomplete should suggestUpgradeData. (Optional check; close without saving if you tested in another file.) - Author the first
.tres. Navigate toresources/in the FileSystem dock — the folder you created in M1.2. If a sub-folderupgrades/does not exist, right-clickresources/→ New → Folder → nameupgrades. Click intoupgrades/. - Right-click in the
upgrades/folder pane → New → Resource…. The "Create New Resource" dialog opens with a search box and a tree of class types. TypeUpgradeDatain the search. The class appears (because ofclass_name). Double-click it. A "Save As" dialog asks for the file path; typeblessing_of_might.tresand confirm. The new file appears in the FileSystem dock. - Click
blessing_of_might.tresto select it. The Inspector (right side) now shows the resource's fields — twelve fields with the defaults you set in the script. Edit them: Id:blessing_of_mightDisplay Name:Blessing of MightDescription:Doubles your Light per click.Cost:50.0Effect Type:click(dropdown — pick theclickoption)Multiplier:2.0Unlock Resource:lightUnlock Threshold:25.0NoCtrl+Sneeded for.tresedits in 4.6 — the Inspector auto-saves on focus loss. Click out of the field; the asterisk on the file name (if it appeared) clears.- Author the second
.tresfor variety. Right-clickupgrades/→ New → Resource… → UpgradeData. Save assermon_of_dawn.tres. Edit: Id:sermon_of_dawnDisplay Name:Sermon of DawnDescription:Doubles passive Light income.Cost:200.0Effect Type:tickMultiplier:2.0Unlock Resource:lightUnlock Threshold:100.0- Open the
.tresfiles in a text editor (outside Godot — File Explorer → right-click → Open With → Notepad/VS Code). Each file is a few lines: a[gd_resource ...]header, an[ext_resource ...]block referencing the script, and a[resource]block with each field's value. The format is human-readable; a balance change is a one-line edit in this file. Close without saving. - Demonstrate
preload. Openscripts/light.gd(or any existing script). Add at the top of the file, afterextends Node: Save. The script reloads;BLESSING_OF_MIGHTis now a project-global constant of typeUpgradeData. Hover the constant — the editor's tooltip shows the field values from the.tres. Remove the line afterward; M4.3's purchase flow will introduce the proper way to reference upgrades (an Array, not per-upgrade constants). - Press
F5. Nothing visibly changed —UpgradeDatais data, not behavior, and no UI yet displays it. The.tresfiles exist and are referenceable; M4.2 will iterate them onto the screen.
Optional sanity check. Edit
blessing_of_might.tresin a text editor: changecost = 50.0tocost = 75.0. Save. Switch back to Godot — the editor's FileSystem dock highlights the file, and the Inspector (if the file is selected) shows the new value75.0. The hot-reload picks up disk changes automatically; you can balance-tune in a text editor outside the Godot session if you prefer.
Self-check quiz¶
Q1 — You write class_name UpgradeData extends Resource, but no @export annotations on any field. The script saves cleanly. You then create a .tres of this type via the FileSystem dock. What does the Inspector show?
A. All declared fields, editable.
B. No fields — without @export, none of them appear in the Inspector.
C. Only fields whose types are built-in (String, float, int) — typed Object fields are hidden.
D. The Inspector errors with "Resource has no exposed properties."
Reveal answer
B — @export is the gate to Inspector visibility. A property declared without @export exists on the class and is reachable from code (upgrade.id = "foo"), but the Inspector does not display it. This is intentional: many Resource subclasses have internal fields (caches, computed values, references) that should not be authored in the editor. @export is the annotation that says "this field is part of the editing surface." A is wrong: the un-exported fields are there, just hidden. C and D are fabricated rules that do not exist in Godot.
Q2 — You write var u: UpgradeData = preload('res://upgrades/typo_filename.tres'). The file does not exist (typo). When does the error fire?
A. The first time the line u.cost runs (lazy evaluation).
B. At script load — the entire script fails to compile and the editor flags the line.
C. At _ready() of whichever node owns the script.
D. Silently — preload returns null, and u is null until something dereferences it.
Reveal answer
B — preload is checked at script-load time. The compiler resolves the path during script parsing; a missing file is a compile error before any function in the script runs. This is the parse-time-versus-runtime tradeoff preload buys: typos and renames fail loudly the moment the script loads, not later when a field is accessed. load(...) (runtime) would behave like option D — return null, defer the failure to dereference. The textbook prefers preload for known paths because the early failure is cheaper to diagnose. A imagines lazy evaluation that does not exist. C confounds load timing with _ready timing.
Q3 — You design UpgradeData with @export var click_multiplier: float = 1.0 and @export var output_multiplier: float = 1.0 as separate fields. Later you decide all upgrades multiply one of click or tick income, never both. What is the cheapest way to enforce this in the editor?
A. Replace the two fields with one @export_enum("click", "tick") var effect_type: String plus one @export var multiplier: float.
B. Add a _validate_property virtual method that hides one field based on the value of the other.
C. Document the constraint in description; rely on designers to honor it.
D. Add an assert(click_multiplier == 1.0 or output_multiplier == 1.0) in a _setup method.
Reveal answer
A — collapse the two fields into a typed-enum + one multiplier. The data shape now expresses the constraint: an upgrade has one effect type, picked from a dropdown of valid choices, plus one multiplier. The editor widget is two fields instead of three (one of which would always be 1.0); the data model can no longer express the invalid state. B works (and _validate_property is real) but is more code than the schema redesign and still leaves both fields present in memory. C is a documentation-as-enforcement antipattern — it works until it doesn't. D moves the check from author time to runtime, which is later than necessary. The lesson: when two fields of a Resource are mutually exclusive, replace them with one enum-tagged choice. The data model carries the invariant.
Integration question¶
Q4 — open
In M2.2 you wrote Light as extends Node and explained that Resource was "for upgrade definitions, coming in M4." Now UpgradeData extends Resource is in place. Compare the two: a Light instance lives in /root/Light and emits value_changed; an UpgradeData instance lives in res://resources/upgrades/blessing_of_might.tres and has no signals. Why is the choice between Node and Resource not arbitrary, and what would break if you swapped them — extends Resource for Light, extends Node for UpgradeData?
Reveal expected answer
Light needs to do things over time: subscribe to Tick.tick, run _on_tick, emit value_changed whenever its setter runs. Node provides the lifecycle hooks (_ready, _process if used, signal connection) and the /root placement. Resource does not — it has no _ready, no _process, no scene-tree position, no built-in signals beyond changed. If Light extends Resource, the autoload registration would still work (autoloads can be Resources), but the script could not connect to Tick in _ready (no _ready), could not be reached by Tick.tick.connect(...) from outside (no Callable for a Resource method without an instance reference at runtime). Light would have to be polled, not driven. UpgradeData, conversely, does not need lifecycle — it is read by the purchase code and the upgrade list UI, never tickle by time. Making it extends Node would force every upgrade to occupy a slot in some scene, run a _ready that does nothing, accept signal connections it does not emit. The pattern: Node for runtime behavior, Resource for inert data. The chapter's wrong-direction examples — Light extends Resource, UpgradeData extends Node — both compile, both attach somewhere, but each fights its own base class for the affordances the other provides.
What's next¶
Two .tres files now exist in resources/upgrades/ but the game looks identical — there is no UI to display them and no purchase logic to consume them. M4.2 builds the dynamic UpgradeList that iterates the array and renders one row per upgrade; M4.3 wires the purchase guard that turns clicking a row into Light spent and effect applied.
Glossary¶
Glossary
Resource- Godot's base class for data-only objects: pure fields, no node lifecycle, no
_processor_ready. Saved/loaded as.tres(text) or.res(binary). Reference-counted; freed when no references remain. Use for upgrade data, theme presets, audio bus configs — anything edited in the Inspector but not running per-frame logic. @export- GDScript annotation that exposes a property to the Inspector for editing. Without
@exporta property is invisible to the Inspector even on aResourceor scene-instancedNode. Variants —@export_range,@export_file,@export_enum,@export_color— constrain the editor widget. Stored as part of the resource/scene file when set. load(path)- GDScript built-in that loads a resource at the call site (runtime). Path can be a variable. Returns
nullon failure rather than parse-erroring. Use for resources whose path is determined at runtime — saved-game references, modded content, dynamic asset selection.