Skip to content

UpgradeData Resource

What you'll learn

  • The Resource base class — Godot's container for data without behavior — and how extends Resource differs from extends Node.
  • The @export annotation, which exposes a property to the Inspector for editing without code.
  • How a .tres file is an instance of a Resource subclass: pure data, saved as text, diff-friendly, hot-reloadable.
  • The difference between preload(...) (parse-time, path is a literal) and load(...) (runtime, path can be variable) — and why preload is the default for known assets.
  • How to define UpgradeData once, then author a Blessing and a Sermon as two .tres files in the Inspector — without writing a second script.

How it applies

  • Designer-editable balance. Cost values, multipliers, descriptions, icons, and unlock thresholds live in .tres files editable in the Inspector. A designer or QA tester adjusts a Blessing's cost from 50.0 to 60.0 by 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 UpgradeData references (or their id strings), not a copy of each upgrade's fields. The fields live in the .tres files; 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 .tres into a watched folder; the upgrade list (M4.2) iterates the folder and picks up the new entry. The modder writes zero .gd code if the existing UpgradeData fields cover their case. This is the "data-driven content" pattern most idle and roguelike games rely on.
  • Localization. A display_name: String field 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 UpgradeData with UpgradeData.new(), set fields directly, and pass it to the purchase code. The test does not need a .tres file 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. A Resource is not a node. It has no children, no transform, no input handling.
  • Reference-counted lifetime. When the last reference to a Resource goes out of scope, the engine frees it automatically. No queue_free(), no manual delete.
  • Built-in disk serialization. Every Resource knows 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:

class_name UpgradeData
extends Resource

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.

@exportInspector-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 plain Color.
  • @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; multiple preload calls 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. Returns null on 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.

  1. In the FileSystem dock, navigate to scripts/. Right-click → New → Script. Name it upgrade_data.gd. Click Create. The script editor opens with a generated stub.
  2. Replace the body. The full code-fragment ceiling for this chapter, in one fragment:
    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
    
    Twelve @export lines, one fragment, unsplittable. Two of the lines use the constrained @export_enum form to limit effect_type to 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).
  3. Confirm the FileSystem dock shows scripts/upgrade_data.gd. The class is now registered project-wide as UpgradeData. To verify: open any other script and start typing Up — the autocomplete should suggest UpgradeData. (Optional check; close without saving if you tested in another file.)
  4. Author the first .tres. Navigate to resources/ in the FileSystem dock — the folder you created in M1.2. If a sub-folder upgrades/ does not exist, right-click resources/New → Folder → name upgrades. Click into upgrades/.
  5. 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. Type UpgradeData in the search. The class appears (because of class_name). Double-click it. A "Save As" dialog asks for the file path; type blessing_of_might.tres and confirm. The new file appears in the FileSystem dock.
  6. Click blessing_of_might.tres to select it. The Inspector (right side) now shows the resource's fields — twelve fields with the defaults you set in the script. Edit them:
  7. Id: blessing_of_might
  8. Display Name: Blessing of Might
  9. Description: Doubles your Light per click.
  10. Cost: 50.0
  11. Effect Type: click (dropdown — pick the click option)
  12. Multiplier: 2.0
  13. Unlock Resource: light
  14. Unlock Threshold: 25.0 No Ctrl+S needed for .tres edits 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.
  15. Author the second .tres for variety. Right-click upgrades/New → Resource… → UpgradeData. Save as sermon_of_dawn.tres. Edit:
  16. Id: sermon_of_dawn
  17. Display Name: Sermon of Dawn
  18. Description: Doubles passive Light income.
  19. Cost: 200.0
  20. Effect Type: tick
  21. Multiplier: 2.0
  22. Unlock Resource: light
  23. Unlock Threshold: 100.0
  24. Open the .tres files 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.
  25. Demonstrate preload. Open scripts/light.gd (or any existing script). Add at the top of the file, after extends Node:
    const BLESSING_OF_MIGHT: UpgradeData = preload("res://resources/upgrades/blessing_of_might.tres")
    
    Save. The script reloads; BLESSING_OF_MIGHT is now a project-global constant of type UpgradeData. 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).
  26. Press F5. Nothing visibly changed — UpgradeData is data, not behavior, and no UI yet displays it. The .tres files exist and are referenceable; M4.2 will iterate them onto the screen.

Optional sanity check. Edit blessing_of_might.tres in a text editor: change cost = 50.0 to cost = 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 value 75.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 _process or _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 @export a property is invisible to the Inspector even on a Resource or scene-instanced Node. 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 null on failure rather than parse-erroring. Use for resources whose path is determined at runtime — saved-game references, modded content, dynamic asset selection.
### Ask Claude (side-channel) Try one of these in a Claude Code session if a piece did not land: - `Show me the .tres file contents for blessing_of_might.tres after I edit cost from 50 to 75. What changes? What stays the same?` - `Walk me through @export_range syntax — what each of the three numbers (min, max, step) controls, and what the editor widget looks like.` - `Compare extends Resource with Unity ScriptableObject — what's the same, what differs in lifecycle and serialization?`