Part 02 — Definitions and instances
You will build: the definition/state split as a reusable shape — a Resource for the
kind, a container for the instance, the rule that decides which field goes where — and you'll
run the 3D farm's mutation prediction to see what "shared" actually means.
You'll learn: the never-mutate-a-.tres rule and why it's a physics law, not a style
one · Resource vs RefCounted vs Node as the three containers · the acceptance test
that a new kind is a file.
Why this exists#
Part 01 moved numbers to their homes. This part moves kinds — a crop, a weapon upgrade,
an enemy species — to a shape where the kind is a file and the instance is a value. The 3D
farm's stage 05 is the worked example in full; this part is the general form, because the
same triple (definition / state / view) shows up in the farming game's CropData, the
survivor's WeaponUpgrade, and any game where "add a thing" should be "add a file."
Build it#
A — The triple, in the 3D farm's words#
The 3D farm has three types doing three jobs (stage 05), and the jobs are the pattern:
| Type | Container | Job | Lifetime |
|---|---|---|---|
CropData | Resource (a .tres) | the kind: id, name, days_per_stage, stage_meshes | shared, immutable, loaded once |
Plot | RefCounted | the instance: which kind, current stage, days_grown | per planted cell, mutable, freed by reference count |
Crop | Node (a scene) | the view: shows stage_meshes[stage] | per planted cell, disposable, rebuilt from the Plot |
The rule from the overview, applied field by field:
days_per_stage— could two turnips disagree? No. Every turnip takes one day per stage. → definition, onCropData.days_grown— could two turnips disagree? Yes. Planted a day apart, they're a day apart forever. → state, onPlot.stage_meshes— the look of the kind, per stage. Two turnips show the same meshes. → definition.stage— which mesh this turnip is showing, derived from itsdays_grown. → state.
The test is the same as part 01's, one level up: part 01 asked "what is this number," this
part asks "what is this field," and the answer is always "could two of the same kind
disagree." A field that answers yes is state; no is definition. The 3D farm's stage 05
checkpoint makes it a drill: "name a field that would break if it lived on the wrong side
(days_grown on CropData would make every turnip the same age)."
B — The never-mutate rule, as physics#
The rule: never mutate a loaded .tres at runtime. It reads like a style preference until
you see why it's a law. Godot shares loaded resources by reference — ten turnips don't hold
ten copies of turnip.tres; they hold ten pointers to the one CropData in memory.
Mutate days_per_stage on the loaded resource and every turnip in the field changes tempo
at once — not because the code is wrong, but because there is one resource and ten owners.
The 3D farm's stage 05 runs the prediction that makes it visible:
Change
turnip.tres'sstage_meshes[1]to a cube. Save. What does the running game show — and what will the next run show?
The running game shows nothing (the loaded resource is in memory; the file is a different object now). The next run shows every turnip's middle stage as a cube — every one, because they share the definition. The two tenses are the whole rule: the file is not the runtime, and the runtime is not private. A mutation you make at runtime is invisible until the next load (when it's gone — you mutated memory, not the file) or visible to every instance at once (when you've found the one code path that writes the shared resource). Neither tense is the one you want, which is why the rule is "never," not "carefully."
The corollary is the one that catches the bug: if every instance of a kind changes at once, you mutated the definition. The farming game's stage 04 stuck-list has the fingerprint — "all crops share one look after planting two kinds → you mutated the shared definition somewhere." The data version is the same: a per-instance field that behaves per-kind is a definition that got written to.
C — The three containers, and which one a thing is#
The split's fields need homes, and Godot offers three. The choice is a lifetime question, not a feature one:
Resource— shared, loadable from a.tres, inspectable, immutable by convention. The definition's container. AResourceis the only container Godot serializes to a file you can edit in the inspector, which is the whole reason the definition is one: the kind is content, and content is a file.RefCounted— private data, freed when the last reference drops, not a scene node. The state's container when the state has no scene presence. APlotis aRefCountedbecause it's data with a lifetime and no transform, no process, no reason to be in the tree — the 3D farm's stage 04 argued it: "if you find yourself wanting a plot to do something, that want is the central-tick decision resurfacing." ARefCountedthat starts doing things is aNodeyou didn't mean to build.Node— a scene presence, a lifetime bound to the tree, a process callback. The view's container, and the only container that can be a child of something and die with it. ACropis aNodebecause it's shown, and shown things have to be in the tree to be shown.
The mistake is reaching for the wrong one by habit: a Plot as a Node (state with a scene
presence it doesn't need — the 3D farm's stage 04 decision 3, option B, "the tree is the
map," the temptation the dictionary beats); a CropData as a RefCounted (a definition you
can't edit in the inspector — the kind is no longer content, it's code again); a Crop view
as a Resource (a thing that's shown with no scene presence — it can't be shown). The
container is the lifetime; the lifetime is the job; pick the job first.
D — The id, and the display name#
Every definition needs a machine name and a human name, and they're different fields for a reason:
@export var id: StringName = &"" # the machine name — keyed, compared, saved
@export var display_name: String = "New Crop" # the human name — shown, never branched on
id is a StringName because it's looked up constantly (part 03's catalogue is a
Dictionary[StringName, CropData] — StringNames hash by identity, the cheapest key there is)
and saved (the 3D farm's stage 07 save holds the crop's id, never its display name — a
renamed display doesn't orphan a save; a renamed id would). display_name is a String
because it's shown and nothing else — code never branches on it. The bug the split
prevents: a game that matches on "Turnip" (the display name) to decide growth, and breaks
the day someone localizes the name to "Rüben" or renames it for flavour. The id is the
contract; the display name is the label on the box.
E — The acceptance test#
The 3D farm's stage 05 states it as the design's pass/fail: add a third crop with zero code
changes. A parsnip.tres (id, name, days_per_stage, meshes), one entry in the farm's
crop_catalogue (part 03), and the parsnip plants, grows on its own tempo, and harvests —
with no .gd file edited. If the parsnip needs a line of code, the split has a hole, and the
hole is a field on the wrong side of the rule (a per-instance fact on the definition, or a
per-kind fact hardcoded in a controller). Find the line; the line is the leak.
The survivor's stage 07 runs the same test on upgrades: a third WeaponUpgrade .tres
applies through the same apply_upgrade with no new match branch as long as its
upgrade_type is one the enum already has — the fence part 04 names. A new kind of
upgrade (one the enum doesn't list) is a new enum value and a new branch, which is code, by
design: the set of upgrade kinds is closed, and the set of upgrade instances is open.
F — Commit#
git add . && git commit -m "data 02: definition/state split, three containers, the id"
Checkpoint — definition of done#
- You can name, for your game's central kind, which fields are definition and which are state, and why (the "could two disagree" test, per field)
- The mutation prediction ran: you changed a definition and saw the two tenses (the running game unchanged, the next run every-instance-changed) — and you can say which code path would have made the running game change (a write to the shared resource)
- Each of your three types is on the container its job requires, and you can say what the wrong container would have cost (part C's three mistakes, named for your game)
- The id is a
StringName, the display name is aString, and nomatch/ifbranches on the display name — grep for it; the absence is the check - The acceptance test passed: a new kind added as a file, zero code, and you can name the hole that would have made it need a line
- Zero warnings; committed
Stretch (no instructions)#
A definition with a derived field: the 3D farm's CropData.stage_count() (the farming
game's stage 04 left it as a blank — stage_textures.______). The stage count is derived
from stage_meshes.size(), and the question is whether it's a field or a method. Make it a
method, and say why a stored stage_count field would have been a second source of truth
(the definition's own version of the survivor's stage 05 bug — a number that can disagree
with the number it's derived from).
If you get stuck#
- Every instance of a kind changes at once when one should → the mutation fingerprint (part
B). Find the write to the shared
Resource; the fix is a copy (crop_data.duplicate()) if the instance legitimately needs its own, or a field moved to thePlotif it was state all along. - A
Plotis "doing things" (ticking itself, emitting its own day signal) → the container reached forNodebehaviour while staying aRefCounted. The 3D farm's stage 04 answer: the farm ticks the plots (the central tick), and thePlotis data again. A state container that schedules itself is the per-entity-listener option the central tick beat. - The new kind needs a code line, and the line is a
matchon the id → the id is being used as a behaviour selector, not a lookup key. A kind that behaves differently by id is a kind that needs a different definition field (a flag, a parameter) — the behaviour is data on the definition, not a branch on the id. The branch is the leak. - Two definitions with the same id → part 03's duplicate check. For now, the
idis the contract and a duplicate is a silent overwrite in the catalogue (the last one wins); the warning that names both is part 03's, and it's the difference between "a kind I can't find" and "two kinds with the same name."