Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 15 minutes
  • Examples run with godot 4.7.2-stable and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: A Resource class with exported fields is a data type the editor knows · load() reads a resource once and hands everyone the same object. With a pass: 3 in-depth parts and 3 interview answers, about 10 minutes.

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

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:

item_data.gd: an item as data GD · item_data.gd
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
make_sword.gd: one item, saved as a .tres file GD · make_sword.gd
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()
An ItemData saved, and the file Godot wrote, Godot 4.7.2 Python · save_session.py
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:

Interactive · stage by stage From ItemData.new() to sword.tres Step through the save. The sub-label of each stage says what reached the file.
From ItemData.new() to sword.tres, step 5 of 5. Showing ItemData.new(), Set the fields, ResourceSaver.save(), Header and script, Exported values. In focus: Exported values.ItemData.new()every field at its defaultSet the fieldsprice 120, durability 40ResourceSaver.save().tres: the text formatHeader and scriptscript_class="ItemData"Exported valuesdurability left out

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. The saver. ResourceSaver.save() writes a path that ends in .tres in the text format, which reads well in version control. It returned OK.
  4. 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.
  5. 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

  1. Question 1 of 2 make_sword.gd set price to 120, durability to 40 and left max_durability at its default of 100. Which of these lines did sword.tres hold?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • script = ExtResource(…), the script item_data.gd
    • price = 120

    A .tres file holds the exported values that differ from their defaults, and the path of the script.

  2. Question 2 of 2 Players ask for a mod folder where they drop their own .tres item files. What is the risk?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A resource file names the script it uses, and loading the file runs that script, so a file from anyone could run any code

    Keep resource files for data that ships with the game, and read player files as plain data such as JSON.

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:

use_sword.gd: two loads of one file, and one without the cache GD · use_sword.gd
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()
Three loads of sword.tres, Godot 4.7.2 Python · load_session.py
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:

Interactive · message by message Three loads of items/sword.tres Step through the loads of use_sword.gd. The cache's sub-label says what it holds.
Three loads of items/sword.tres, step 6 of 6. Messages so far: use_sword.gd to ResourceLoader: load(…sword.tres); ResourceLoader to items/sword.tres: read the file; ResourceLoader to use_sword.gd: the sword; use_sword.gd to ResourceLoader: load(…sword.tres); ResourceLoader to use_sword.gd: the same sword; use_sword.gd to ResourceLoader: CACHE_MODE_IGNORE.use_sword.gdResourceLoadercache: the sworditems/sword.tresload(…sword.tres)read the filethe swordload(…sword.tres)the same swordCACHE_MODE_IGNORE

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first load. load("res://items/sword.tres") asks the ResourceLoader for the file. Nothing is cached under that path yet.
  2. 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.
  3. The object. shop_sword gets the object: Iron sword, 120 gold, RARE, and durability 100, the default, because the file holds no durability.
  4. 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.
  5. Same object back. shop_sword and bag_sword are two names for one ItemData, just as two variables can hold one Array.
  6. 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

  1. Question 1 of 2 shop_sword and bag_sword both come from load("res://items/sword.tres"). What is shop_sword == bag_sword?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: true, because the second load returned the object the first one cached

    The Resource reference: subsequent loads using its path return the cached reference.

  2. Question 2 of 2 The sword was saved with durability 40 set in code, and durability is not exported. What durability does the loaded sword have?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 100

    The file holds no durability, so the loaded object starts from the declaration's default, 100, as the run printed.

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.

In this part:

  • 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…

This part opens with any MySmartCoPilot 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.

  1. Question 1 of 8 What makes ItemData a custom resource?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: Its script extends Resource; class_name and @export fields make it a type with saved, editable fields

    Resources are data containers: they do nothing on their own, and they save and load with their fields.

  2. Question 2 of 8 A designer edits durability in the Inspector, but the field does not appear. What is missing?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: An @export on durability, which both shows it and saves it

    @export both shows a field in the Inspector and saves it in the .tres file; durability, the one field of item_data.gd without it, was neither shown nor written to sword.tres.

  3. Question 3 of 8 A custom resource's _init(p_id) takes one parameter without a default. What does the Resources page warn about?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: Creating and editing the resource in the Inspector can fail; every parameter should have a default value

    The Object reference adds that an object whose _init() has required parameters can only be made with new().

  4. Question 4 of 8 Why save item resources as .tres in a team project?

    Choose one answer.

    Show the answer: Question 4 of 8

    Answer: It is a text format, so version control can show and merge the changes line by line

    Exported games may convert text resources to binary; the source stays readable in the project.

  5. Question 5 of 8 What does load() do the second time it is given the same path?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: It returns the object it cached the first time, without reading the file

    The cache entry goes when nothing references the resource any more; the next load reads the file again.

  6. Question 6 of 8 Which call gives an object read freshly from sword.tres, separate from the cached one?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: ResourceLoader.load("res://items/sword.tres", "", ResourceLoader.CACHE_MODE_IGNORE)

    CACHE_MODE_IGNORE neither reads from the cache nor stores into it, for the file and its sub-resources.

  7. Question 7 of 8 What is the difference between preload() and load() for a resource file?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: preload() reads the file when the script is compiled and needs a constant path; load() reads it when its line runs

    A database that reads a whole folder needs load(), because the paths are known only at run time.

  8. Question 8 of 8 A save system writes each player's progress with ResourceSaver.save() to user://save.tres and loads it with load(). Why should it change, and to what?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: Anyone can edit or swap that file, and loading it runs whatever script it names; save progress as plain data such as JSON

    Resources are for data that ships with the game; data from players is read as plain values and checked.

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.

  1. Fresher to mid

    Two heroes equip swords loaded from sword.tres. When one sword loses durability, so does the other, and once a hero's sword breaks, the shop shows its sword as broken too. Why, and how do you fix it without copying every field of every item?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A custom resource is a script that extends Resource, with @export on every field to save and edit; a .tres file 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_IGNORE reads 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.