GDScript Resources Cheat Sheet
Quick-reference for Godot 4 resources and data-driven design. Each section includes copy-ready snippets for item stats, level configs, and asset management.
Resource Basics
Resources are data containers that can be saved to disk and shared between nodes. Textures, audio, scenes, and custom data all extend Resource.
# Everything in Godot is either a Node (in the tree) or a Resource (data)
# Common resources:
var tex: Texture2D # images, sprites
var sound: AudioStream # audio files
var scene: PackedScene # .tscn files
var font: Font # font files
var material: Material # shaders, materials# Resources use RefCounted — automatically freed when no references remain
var tex := load("res://icon.svg")
# tex is freed when nothing references it anymoreload() vs preload()
preload() loads at parse time (compile time). load() loads at call time (runtime). Use preload for known assets, load for dynamic paths.
# Loaded when the script is parsed — no runtime delay
const SwordScene := preload("res://scenes/weapons/sword.tscn")
const HitSound := preload("res://audio/sfx/hit.ogg")
const EnemySprite := preload("res://sprites/goblin.png")# Loaded when this line executes — path can be a variable
var weapon_type := "axe"
var scene: PackedScene = load("res://scenes/weapons/%s.tscn" % weapon_type)
# Load from user data
var save := load("user://saves/slot1.tres")# preload: known at write-time, small/medium assets
const BulletScene := preload("res://scenes/bullet.tscn")
# load: path computed at runtime, large assets, user content
func load_level(level_name: String) -> PackedScene:
return load("res://levels/%s.tscn" % level_name)preload() cannot use variables — the path must be a string literal. load() accepts any expression.
Custom Resource Classes
Create your own Resource subclasses for game data — item stats, enemy configs, ability definitions.
class_name ItemData
extends Resource
@export var name: String
@export var icon: Texture2D
@export var damage: int
@export var rarity: int # 0=common, 1=rare, 2=epic
@export_multiline var description: String# 1. Create item_data.gd with class_name ItemData
# 2. Right-click in FileSystem > New Resource > ItemData
# 3. Fill in fields in the Inspector
# 4. Save as .tres file (e.g. res://data/items/iron_sword.tres)# weapon.gd
@export var item_data: ItemData
func get_damage() -> int:
return item_data.damage
func get_display_name() -> String:
return item_data.name@export with Resources
Expose resource slots in the Inspector so designers can assign data without touching code.
@export var weapon_stats: ItemData
@export var hit_sound: AudioStream
@export var death_effect: PackedScene@export var inventory: Array[ItemData] = []
@export var loot_table: Array[ItemData] = []
func drop_random_loot() -> ItemData:
return loot_table.pick_random()# The Inspector shows a dropdown filtered to this type
@export var character_class: CharacterClassData
@export var skill_tree: SkillTreeData
# Nested resources
@export var equipment: EquipmentSlots # Another custom ResourcePackedScene as Resource
PackedScene is just a Resource that holds a node tree. instantiate() creates live nodes from it.
@export var enemy_scenes: Array[PackedScene] = []
func spawn_random_enemy(pos: Vector2) -> void:
var scene := enemy_scenes.pick_random()
var enemy := scene.instantiate()
enemy.global_position = pos
add_child(enemy)# data/wave_config.gd
class_name WaveConfig
extends Resource
@export var enemy_scene: PackedScene
@export var count: int
@export var spawn_delay: float
# spawner.gd
@export var waves: Array[WaveConfig] = []
func run_wave(config: WaveConfig) -> void:
for i in range(config.count):
var enemy := config.enemy_scene.instantiate()
add_child(enemy)
await get_tree().create_timer(config.spawn_delay).timeoutThreaded Loading
Load large assets in the background to avoid frame drops. Essential for level transitions.
var next_scene_path := "res://levels/world_2.tscn"
func start_loading() -> void:
ResourceLoader.load_threaded_request(next_scene_path)
func _process(delta: float) -> void:
var status := ResourceLoader.load_threaded_get_status(next_scene_path)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
# Update loading bar
var progress: Array = []
ResourceLoader.load_threaded_get_status(next_scene_path, progress)
$LoadingBar.value = progress[0] * 100
ResourceLoader.THREAD_LOAD_LOADED:
var scene := ResourceLoader.load_threaded_get(next_scene_path)
get_tree().change_scene_to_packed(scene)func load_scene_async(path: String) -> PackedScene:
ResourceLoader.load_threaded_request(path)
while true:
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
return ResourceLoader.load_threaded_get(path)
await get_tree().process_frameSaving Resources
Use ResourceSaver to write resource data to disk. Perfect for save files and user-generated content.
# save_data.gd
class_name SaveData
extends Resource
@export var player_name: String
@export var level: int
@export var inventory: Array[String] = []
@export var position: Vector2
# game_manager.gd
func save_game() -> void:
var data := SaveData.new()
data.player_name = player.name
data.level = current_level
data.inventory = player.get_inventory_names()
data.position = player.global_position
ResourceSaver.save(data, "user://saves/save_01.tres")func load_game() -> void:
if ResourceLoader.exists("user://saves/save_01.tres"):
var data: SaveData = load("user://saves/save_01.tres")
player.name = data.player_name
current_level = data.level
player.global_position = data.position# .tres = text format (human-readable, good for version control)
ResourceSaver.save(data, "res://data/config.tres")
# .res = binary format (smaller, faster to load)
ResourceSaver.save(data, "res://data/config.res")Use .tres during development (readable diffs). Switch to .res for release builds if size matters.
Shared vs Unique Resources
Resources are shared by default. Multiple nodes referencing the same .tres edit the SAME data. Use duplicate() or make_unique() to avoid this.
# Two enemies reference the same ItemData .tres
# Modifying one affects both!
@export var stats: EnemyStats
func buff() -> void:
stats.damage += 10 # BUG: ALL enemies using this stats get buffedfunc _ready() -> void:
# Create a unique copy so changes are per-instance
stats = stats.duplicate()
func buff() -> void:
stats.damage += 10 # Only this enemy is buffed# In the Inspector, check "Local to Scene" on a sub-resource
# This makes each scene instance get its own copy automatically
# Or set it in code:
@export var stats: EnemyStats
func _ready() -> void:
stats.resource_local_to_scene = trueduplicate() is explicit and clear. local_to_scene is convenient for Inspector-assigned resources.
Common Resource Patterns
Data-driven design patterns using custom Resources.
class_name AbilityData
extends Resource
@export var name: String
@export var icon: Texture2D
@export var cooldown: float
@export var mana_cost: int
@export var damage: int
@export var effect_scene: PackedScene
# ability_system.gd
@export var abilities: Array[AbilityData] = []
func use_ability(index: int) -> void:
var ability := abilities[index]
if mana >= ability.mana_cost:
mana -= ability.mana_cost
spawn_effect(ability.effect_scene)class_name DialogueLine
extends Resource
@export var speaker: String
@export var text: String
@export var portrait: Texture2D
@export var choices: Array[String] = []
class_name DialogueSequence
extends Resource
@export var lines: Array[DialogueLine] = []class_name LootEntry
extends Resource
@export var item: ItemData
@export_range(0, 100) var drop_chance: float
class_name LootTable
extends Resource
@export var entries: Array[LootEntry] = []
func roll() -> ItemData:
for entry in entries:
if randf() * 100.0 < entry.drop_chance:
return entry.item
return nullCommon Resource Pitfalls
Quick fixes for the most frequent resource bugs in Godot 4.
# BUG: preload requires a string literal
# const scene = preload(path_variable) # ERROR
# FIX: use load() for dynamic paths
var scene = load(path_variable)# BUG: all enemies share the same stats resource
# Changing one changes them all
# FIX: duplicate in _ready
func _ready() -> void:
stats = stats.duplicate(true) # true = deep duplicate# BUG: load() returns null if file doesn't exist
var data = load("user://saves/save.tres")
data.level # null access crash!
# FIX: check existence first
if ResourceLoader.exists("user://saves/save.tres"):
var data = load("user://saves/save.tres")
else:
create_new_save()Always check ResourceLoader.exists() before loading from user:// — the file may not exist on first run.
Can you write this from memory?
Define `UpgradeData` as a custom `Resource` class for upgrade config (include both `extends` and `class_name`).