Game Development with Godot & GDScript Module 9 – Classes, memory and custom resources
Custom resources and data-driven design
Define Godot custom resources with exported fields, save and load them as .tres files, and build an item database that designers can edit in the Inspector.
What you will learn
- Define Resource classes with exported fields for items, enemies and abilities
- Save and load .tres files with ResourceSaver and ResourceLoader
- Use resource_local_to_scene and duplicate() to avoid shared-state bugs
- Build an item database that designers can edit in the Inspector, checked at start-up
Before you start
What you’ll learn · Before you start
What you will learn
- Define Resource classes with exported fields for items, enemies and abilities
- Save and load .tres files with ResourceSaver and ResourceLoader
- Use resource_local_to_scene and duplicate() to avoid shared-state bugs
- Build an item database that designers can edit in the Inspector, checked at start-up
Before you start
On this page
A game with 40 items does not need 40 scripts. It needs one ItemData class and 40 small files that designers edit in the Inspector, which the game loads as typed objects. That is data-driven design: Designing a game so that what differs between items, enemies or levels lives in data files rather than in code, so that one script handles them all and designers change the data without touching code.: what differs between items lives in data, and one script handles them all. In Godot the data files are custom resource: A Resource class of your own, such as class_name ItemData with extends Resource and @export fields. Each object of it can be saved as a .tres file, edited in the Inspector and loaded as a typed object., and this page makes, saves and loads them on Godot 4.7.2.
A Resource class with exported fields is a data type the editor knows
Custom resources start as a script: class_name ItemData and extends Resource, with an @export on every field that
should be saved and shown in the Inspector. With a class name, the Resources page notes, the type also appears in the
editor’s dialog for new resources. make_sword.gd fills one in code and saves it with ResourceSaver:
class_name ItemData
extends Resource
# One item of the game as data. Every @export field is saved in the item's .tres file and shown in the Inspector;
# durability is not exported, so it is neither saved nor shown.
enum Rarity { COMMON, RARE, EPIC }
@export var id := ""
@export var display_name := ""
@export_range(0, 9999, 1, "or_greater") var price := 0
@export var rarity := Rarity.COMMON
@export_file("*.svg", "*.png") var icon_path := ""
@export var stats: Dictionary[String, int] = {}
@export_group("Durability")
@export var max_durability := 100
var durability := 100 extends SceneTree
# Makes an ItemData in code, as the Inspector would, and saves it as items/sword.tres.
func _initialize() -> void:
var sword := ItemData.new()
sword.id = "sword"
sword.display_name = "Iron sword"
sword.price = 120
sword.rarity = ItemData.Rarity.RARE
sword.icon_path = "res://icons/sword.svg"
sword.stats = {"attack": 12, "speed": 3}
sword.durability = 40
DirAccess.make_dir_recursive_absolute("res://items")
print("save: ", error_string(ResourceSaver.save(sword, "res://items/sword.tres")))
quit() from session import files, run
# The project's import registers ItemData; make_sword.gd saves the item, and cat shows the file Godot wrote.
files("project.godot", "item_data.gd", "make_sword.gd")
run("""
godot --headless --quiet --import
godot --headless --no-header --script make_sword.gd
cat items/sword.tres
""") Output
$ godot --headless --quiet --import
$ godot --headless --no-header --script make_sword.gd
save: OK
$ cat items/sword.tres
[gd_resource type="Resource" script_class="ItemData" format=3]
[ext_resource type="Script" path="res://item_data.gd" id="1_es5ut"]
[resource]
script = ExtResource("1_es5ut")
id = "sword"
display_name = "Iron sword"
price = 120
rarity = 1
icon_path = "res://icons/sword.svg"
stats = Dictionary[String, int]({
"attack": 12,
"speed": 3
})
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 save_session.py
Step through what reached the file:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- An object of the class. make_sword.gd makes an ItemData with new(), as the Inspector does when a designer creates a new resource. Every field starts at the default its declaration gives.
- Its data. The script sets id, display_name, price 120, rarity RARE, the icon path and two stats, and also durability to 40, a variable that is not exported.
- The saver. ResourceSaver.save() writes a path that ends in .tres in the text format, which reads well in version control. It returned OK.
- What it is. The file starts by naming its type and script_class, ItemData, then the script it needs as an ext_resource, item_data.gd. Loading the file will run that script.
- What it holds. Under [resource] come the exported values that differ from their defaults: price 120, rarity 1, the stats as a typed Dictionary. durability was never written, and max_durability, still at its default, neither.
A .tres file: The text format of a saved resource: a header with its type and script, the script as an ext_resource, then the exported values that differ from their defaults. Exported games may store resources as binary .res files instead. is plain text that version control can compare line by line. It held the
exported values that differ from their defaults, the stats as a typed Dictionary[String, int], and the path of the
script. durability, which is not exported, was not saved. Export hints shape the Inspector: @export_range gives a
slider, @export_file a file picker for .svg and .png, and @export_group folds max_durability under
Durability. The docs’ advice for a custom resource’s _init() is to give every parameter a default value, or the
Inspector cannot create one. This is the core of data-driven design: a new item is a new file, not new code.
Load resource files only from your own project
A resource file names the script it uses, and loading the file runs that script: every load of sword.tres runs
item_data.gd. Keep .tres and .res files for data that ships with the game, under res://. A file from a
player or a download could name any script; the FileAccess reference warns that objects decoded from untrusted
sources can contain code that gets executed. Store saves and mods as plain data, such as JSON or ConfigFile, which
the module on files and saving covers.
Checkpoint
Check: Custom resources and what a .tres file holds
load() reads a resource once and hands everyone the same object
use_sword.gd loads sword.tres twice, as a shop and a bag would, then once more without the cache:
extends SceneTree
# Loads items/sword.tres twice, then once more without the cache.
func _initialize() -> void:
var shop_sword: ItemData = load("res://items/sword.tres")
var bag_sword: ItemData = load("res://items/sword.tres")
print("%s: %d gold, %s, stats %s, durability %d" % [shop_sword.display_name, shop_sword.price, ItemData.Rarity.keys()[shop_sword.rarity], shop_sword.stats, shop_sword.durability])
print("loaded twice, same object: ", shop_sword == bag_sword, ", cached: ", ResourceLoader.has_cached("res://items/sword.tres"))
var fresh: ItemData = ResourceLoader.load("res://items/sword.tres", "", ResourceLoader.CACHE_MODE_IGNORE)
print("with CACHE_MODE_IGNORE, same object: ", fresh == shop_sword)
quit() from session import files, run
files("project.godot", "item_data.gd", "make_sword.gd", "use_sword.gd")
run("""
godot --headless --quiet --import
godot --headless --no-header --script make_sword.gd
godot --headless --no-header --script use_sword.gd
""") Output
$ godot --headless --quiet --import
$ godot --headless --no-header --script make_sword.gd
save: OK
$ godot --headless --no-header --script use_sword.gd
Iron sword: 120 gold, RARE, stats { "attack": 12, "speed": 3 }, durability 100
loaded twice, same object: true, cached: true
with CACHE_MODE_IGNORE, same object: false
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 load_session.py
Step through the three loads:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The first load. load("res://items/sword.tres") asks the ResourceLoader for the file. Nothing is cached under that path yet.
- Read once. The loader reads the file, makes an ItemData with its script and its values, and keeps it in the cache under the path.
- The object. shop_sword gets the object: Iron sword, 120 gold, RARE, and durability 100, the default, because the file holds no durability.
- The second load. bag_sword loads the same path. The cache has it, so the loader returns the same object without reading the file: same object, true.
- Same object back. shop_sword and bag_sword are two names for one ItemData, just as two variables can hold one Array.
- Without the cache. ResourceLoader.load() with CACHE_MODE_IGNORE reads the file again and returns a new object: same object, false.
The first load() read the file and cached the object under its path; the second returned that same object, as the
Resource reference describes, and CACHE_MODE_IGNORE read the file again into a new one. The loaded sword had
durability 100, its default, because the file held none. Sharing is what makes custom resources cheap, and it decides
the rest of this page. A change to a loaded resource is seen by everything that loaded it, so a resource every owner
changes needs a copy: duplicate() makes one, and resource_local_to_scene: A setting of a resource, Local To Scene in the Inspector. When it is on, every instance of a scene that uses the resource gets its own copy, so a change made through one instance stays in that instance. gives
each instance of a scene its own. An item database loads a folder of such files at start-up and checks them, because
export hints do not check data. Gameplay code then asks the database for an item by its id instead of loading files
itself.
Key idea
A custom resource is a typed data file: its exported fields are what Godot saves and the Inspector edits, and
load() gives every caller the one cached object.
Checkpoint
Check: Loading resources and the cache
Shared state in loaded resources, reproduced and fixed Pro: opens with a Pro, Premium or Ultimate pass
A loaded resource is one object for the whole game, which is a bug the moment two owners change it. This part reproduces that bug, then fixes it three ways and shows where each one stops working.
- One cached sword, two owners, and three kinds of copy
- Local to the scene: a copy for every instance
5 runnable examples (GD, Python) · about 4 minutes
Opening this part with your pass…
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
An item database designers can edit, checked at start-up Pro: opens with a Pro, Premium or Ultimate pass
The Inspector edits resources, and it does not check them: a designer can save two items with one id or a negative price. This part builds the database that loads a folder of items by id, and shows why it must check what it loads.
In this part:
- A folder of items, indexed by id
- Export hints shape the Inspector, and checks protect the data
4 runnable examples (GD, Python) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
A lab: a validator for a folder of items and loot tables Pro: opens with a Pro, Premium or Ultimate pass
The database of the previous part checks one rule. This lab writes the validator that a team runs before every build, on items and loot tables.
In this part:
- Six problems in a folder, and the checker that knows them
4 runnable examples (GD, Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise
These questions cover the first two sections: what a custom resource saves, and what load() gives back. Practice
opens with whichever of the two your earlier answers handled less well, and explains every answer straight away:
Practice that adapts to you
Practise this lesson
8 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: what is a custom resource in Godot, and when is it better than a Dictionary or a JSON file?
A custom resource is a script that extends Resource, usually with a class_name and @export fields, such as an
ItemData with an id, a price and an icon path. Each object of it can be saved as a .tres file, edited in the
Inspector and loaded as a typed object, so code writes item.price and the editor checks the type. A Dictionary or
JSON has no type and no Inspector, which makes them better for data from outside the game, such as saves and mods.
Use custom resources for data that ships with the game and that designers edit.
-
Mid to senior
An item database scans res://items with DirAccess and loads every file ending in .tres. In the editor the shop lists 40 items; in the exported game it lists none. What happened, and how do you build the list so it works in both?
-
Mid to senior
Designers edit 300 item resources in the Inspector, and every week a mistake reaches a playtest: two items with one id, a negative price, an icon that was renamed, a loot table whose weights do not match its entries. Design a check that stops these before a build, and say where it runs.
Key takeaways
- A custom resource is a script that extends Resource, with
@exporton every field to save and edit; a.tresfile stores those fields as text, with the script it needs. load()reads a resource once and returns the same cached object to every caller;CACHE_MODE_IGNOREreads the file again.- Load resource files only from your own project, because a resource file names a script that runs when it loads.
Terms in this lesson
- .tres file
- The text format of a saved resource: a header with its type and script, the script as an ext_resource, then the exported values that differ from their defaults. Exported games may store resources as binary .res files instead.
- custom resource
- A Resource class of your own, such as class_name ItemData with extends Resource and @export fields. Each object of it can be saved as a .tres file, edited in the Inspector and loaded as a typed object.
- data-driven design
- Designing a game so that what differs between items, enemies or levels lives in data files rather than in code, so that one script handles them all and designers change the data without touching code.
- resource_local_to_scene
- A setting of a resource, Local To Scene in the Inspector. When it is on, every instance of a scene that uses the resource gets its own copy, so a change made through one instance stays in that instance.
References
- Resources (Godot Engine)
- Resource class reference (Godot Engine)
- ResourceSaver class reference (Godot Engine)
- ResourceLoader class reference (Godot Engine)
- GDScript exported properties (Godot Engine)
- DirAccess class reference: folders in an exported project (Godot Engine)
- FileAccess class reference: get_var() and untrusted data (Godot Engine)
- @GDScript class reference: load() and preload() (Godot Engine)
- Godot 4.5 release notes: duplicate_deep() (Godot Engine)
Report a problem with this lesson
Kept only in this browser. Your Learn progress