Basic · Write CharacterData, then three characters
32 minYou are building: a custom Resource type called CharacterData, and three .tres files that use it. After this lesson, a fourth character is a file, not a code change.
1 · What a Resource is
A Node lives in a scene and does things. A Resource is just saved data — a texture, a sound, a material. You can invent your own kinds.
Because your script declares class_name, Godot registers it engine-wide. It then appears in the Create Resource dialog and in the Inspector like a built-in type.
2 · The script
Create res://scripts/character_data.gd and type this in. Read every line as you go.
class_name CharacterData
extends Resource
@export_group("Identity")
@export var id: String = ""
@export var display_name: String = "Character"
@export_multiline var description: String = ""
@export var accent_color: Color = Color(0.6, 0.7, 1.0)
@export_group("Model")
## Your imported character scene. Leave empty for a placeholder capsule.
@export var model_scene: PackedScene
@export var model_scale: float = 1.0
@export var model_offset: Vector3 = Vector3.ZERO
@export var model_yaw_offset_deg: float = 0.0
@export var head_height: float = 1.55
@export_group("Animation Names")
@export var anim_idle: String = "idle"
@export var anim_walk: String = "walk"
@export var anim_run: String = "run"
@export var anim_jump: String = "jump"
@export var anim_fall: String = "fall"
@export_group("Stats")
@export var max_health: int = 100
@export var walk_speed: float = 4.5
@export var run_speed: float = 8.0
@export var jump_velocity: float = 7.0
@export var attack_damage: int = 123 · What each group is for
- Identity — what the player reads on the select screen, plus the id the code uses.
- Model — everything about getting a 3D model to stand correctly inside the capsule. Lessons 6 and 7 use all of it.
- Animation Names — the clip names inside the model. Lesson 12 reads them.
- Stats — numbers the player scene reads.
max_healthandattack_damageare unused for now, and deliberately there for when you add combat.
@export_group is cosmetic, and it earns its keep: it turns a flat wall of twenty fields into four collapsible sections in the Inspector.
4 · Make three characters
- Right-click
res://resources/characters/→ Create New → Resource… - Search for CharacterData and pick it. If it is not listed, save your script and reload the project.
- Save it as
01_knight.tres. - Fill it in:
- Repeat for
02_mage.tres(idmage) and03_rogue.tres(idrogue). Give the rogue the highestrun_speed.
5 · The field that travels
Never change an id later
id is the only part of a character that crosses the network. Everything else is looked up locally from it. Rename display_name as often as you like — but renaming "knight" to "paladin" after players have saved characters leaves those saves pointing at nothing.
6 · Why the files are numbered
In the next lesson the game finds characters by listing the folder and sorting the names. So 01_, 02_, 03_ is how you control their order on the select screen. Rename files to reorder it.
Done when: three .tres files sit in resources/characters/, each opens in the Inspector with four collapsible groups, and each has a different id.
Challenge 1 · A fourth character 🟢 Easy
8 minMake 04_ranger.tres — or a character of your own from the plan you wrote in lesson 1.
- A unique id, in lower-case, with no spaces.
- A description of two sentences that would help a player choose.
- Stats that match the description. A fast character should not also be the strongest.
Then rename the files so your new character appears second in the folder listing.
Done when: four resources exist, ids are all different, and the FileSystem dock lists them in your chosen order.
Challenge 2 · Read one from code 🟡 Medium
12 minA resource is useless until something reads it. Prove yours loads.
- Make a scene with a
Noderoot, saved asres://scenes/data_test.tscn. - Attach this script:
extends Node
func _ready() -> void:
var knight: CharacterData = load(
"res://resources/characters/01_knight.tres")
print(knight.display_name, " runs at ", knight.run_speed)
print("description: ", knight.description)- Run it with F6.
- Now open
01_knight.tresin a text editor. Find the line that storesrun_speed.
Answer in one sentence: what has the .tres file got in it that tells Godot to use your script?
Done when: both lines print, and you can point to the line in the file that links it to character_data.gd.
Challenge 3 · Guard the data 🟠 Harder
14 minTwo characters with the same id would be a nasty bug — the wrong one would load, on some machines only.
Write a small editor tool that checks for it. Make res://scripts/check_characters.gd:
@tool
extends EditorScript
## Run with File → Run in the script editor.
func _run() -> void:
var seen := {}
var dir := DirAccess.open("res://resources/characters")
for file in dir.get_files():
if not file.ends_with(".tres"):
continue
var data: CharacterData = load(
"res://resources/characters/".path_join(file))
if data.id.is_empty():
push_warning("%s has no id" % file)
elif seen.has(data.id):
push_error("duplicate id '%s' in %s and %s"
% [data.id, seen[data.id], file])
else:
seen[data.id] = file
print("checked %d characters" % seen.size())- Open it in the script editor and choose File → Run.
- Deliberately give two characters the same id. Run again and read the error.
- Fix the id.
- Extend the tool: also warn when
run_speedis lower thanwalk_speed.
Done when: the tool prints a count on good data, reports the duplicate you planted, and catches a swapped speed.
Extra · Data-driven thinking 🔴 Open-ended
no limit- Design a second resource type of your own —
ItemData,SpellDataorMapData. Write only the script and one example file. You will needMapDatafor real in lesson 23. - Add
@export_rangeto the speed fields so the Inspector shows a slider instead of a number box. Look up the syntax in the docs first. - Write down three things a game could store as data rather than code, and one thing that genuinely has to be code.
- Give each character an
accent_colorthat suits it, then arrange the four colours on screen. Do they look like one set?