Understanding Godot’s resource system for data storage and asset management
Resources are the foundation of Godot’s data storage system. They serve as serializable containers for data and assets, from textures and sounds to custom game data and scene files.
A Resource is the base class for all Godot-specific resource types. Resources are reference-counted objects that can be saved to disk, loaded, and shared between multiple nodes. They inherit from RefCounted, which means they’re automatically freed when no longer referenced.
Resources are primarily data containers. Unlike nodes, they don’t have a position in the scene tree and don’t receive processing callbacks.
# Loaded when the script is parsedconst PLAYER_SCENE = preload("res://characters/player.tscn")const EXPLOSION_SOUND = preload("res://sounds/explosion.wav")const COIN_TEXTURE = preload("res://sprites/coin.png")func _ready(): # Resource is already loaded and ready to use var player = PLAYER_SCENE.instantiate() add_child(player)
Use preload() for resources you know you’ll need immediately. The path must be a constant string literal.
func load_level(level_number: int): # Path can be dynamic var path = "res://levels/level_%d.tscn" % level_number var scene = load(path) if scene: var instance = scene.instantiate() add_child(instance) else: print("Failed to load level ", level_number)
func load_resource_with_cache(): # Check if already cached if ResourceLoader.has_cached("res://player.tscn"): print("Resource is cached") # Load resource var resource = ResourceLoader.load("res://player.tscn") # Check resource type if resource is PackedScene: print("Loaded a scene")func threaded_load(): # Start loading in background ResourceLoader.load_threaded_request("res://large_scene.tscn") # Check load status while true: var status = ResourceLoader.load_threaded_get_status("res://large_scene.tscn") if status == ResourceLoader.THREAD_LOAD_LOADED: var resource = ResourceLoader.load_threaded_get("res://large_scene.tscn") print("Loading complete!") break elif status == ResourceLoader.THREAD_LOAD_FAILED: print("Loading failed") break await get_tree().process_frame
The engine maintains a global cache of loaded resources. The same resource path will return the same resource instance, saving memory but potentially causing unexpected shared state.
var enemy_scene = preload("res://enemies/goblin.tscn")func spawn_enemy(position: Vector2): # Create a new instance var enemy = enemy_scene.instantiate() enemy.position = position add_child(enemy)func spawn_multiple_enemies(): # One PackedScene can create many instances for i in range(10): var enemy = enemy_scene.instantiate() enemy.position = Vector2(i * 100, 0) add_child(enemy)
func save_scene_to_file(): # Create a scene var root = Node2D.new() var sprite = Sprite2D.new() var collision = CollisionShape2D.new() # Build hierarchy root.add_child(sprite) root.add_child(collision) # Set ownership (crucial for saving) sprite.owner = root collision.owner = root # Pack into PackedScene var packed_scene = PackedScene.new() var result = packed_scene.pack(root) if result == OK: # Save to disk var error = ResourceSaver.save(packed_scene, "res://generated_scene.tscn") if error == OK: print("Scene saved successfully") # Clean up the temporary nodes root.free()
Only nodes with their owner property set will be saved when packing a scene. This prevents temporary or runtime-generated nodes from being saved unintentionally.
You can create your own resource types for game data:
# weapon_stats.gdextends Resourceclass_name WeaponStats@export var weapon_name: String = "Sword"@export var damage: int = 10@export var attack_speed: float = 1.0@export var range: float = 100.0@export var icon: Texture2D@export var sound: AudioStreamfunc get_dps() -> float: return damage * attack_speed
Using custom resources:
# weapon.gdextends Node2D@export var stats: WeaponStatsfunc _ready(): if stats: print("Weapon: ", stats.weapon_name) print("DPS: ", stats.get_dps()) # Use the weapon's icon if stats.icon: $Sprite2D.texture = stats.iconfunc attack(): if stats and stats.sound: $AudioStreamPlayer.stream = stats.sound $AudioStreamPlayer.play()
Why Use Custom Resources?
Custom resources provide several benefits:
Data Separation: Game data is separate from game logic
Reusability: The same resource can be used by multiple nodes
Editor Integration: Can be edited in the Inspector
Serialization: Automatically saved and loaded with scenes
Type Safety: Define clear data structures with type checking
The resource_local_to_scene property makes each scene instance get its own copy:
# health_resource.gdextends Resourceclass_name HealthResource@export var max_health: int = 100var current_health: int = 100func _init(): # This resource will be unique per scene instance resource_local_to_scene = truefunc take_damage(amount: int): current_health = max(0, current_health - amount)
# player.gdextends CharacterBody2D@export var health: HealthResourcefunc _ready(): # Each player instance has its own health resource print("Health: ", health.current_health)func take_damage(amount: int): health.take_damage(amount) # Only this instance's health changes
When resource_local_to_scene is true, each scene instance gets a duplicate of the resource. This is perfect for data that should be instance-specific.
# item.gdextends Resourceclass_name Item@export var item_name: String@export var description: String@export var icon: Texture2D@export var stack_size: int = 1@export var value: int = 0func use(target: Node) -> bool: # Override in derived resources return false
# consumable_item.gdextends Itemclass_name ConsumableItem@export var health_restore: int = 0@export var mana_restore: int = 0func use(target: Node) -> bool: if target.has_method("heal"): target.heal(health_restore) if target.has_method("restore_mana"): target.restore_mana(mana_restore) return true
# inventory.gdextends Resourceclass_name Inventorysignal item_added(item: Item, quantity: int)signal item_removed(item: Item, quantity: int)signal inventory_changed@export var max_slots: int = 20var items: Dictionary = {} # item_path: quantityfunc add_item(item: Item, quantity: int = 1) -> bool: if items.size() >= max_slots and not has_item(item): return false var path = item.resource_path if path.is_empty(): # Generate a unique key for runtime items path = str(item.get_instance_id()) items[path] = items.get(path, 0) + quantity item_added.emit(item, quantity) inventory_changed.emit() emit_changed() return truefunc remove_item(item: Item, quantity: int = 1) -> bool: var path = item.resource_path if path.is_empty(): path = str(item.get_instance_id()) if not items.has(path): return false items[path] -= quantity if items[path] <= 0: items.erase(path) item_removed.emit(item, quantity) inventory_changed.emit() emit_changed() return truefunc has_item(item: Item) -> bool: var path = item.resource_path if path.is_empty(): path = str(item.get_instance_id()) return items.has(path)func get_item_count(item: Item) -> int: var path = item.resource_path if path.is_empty(): path = str(item.get_instance_id()) return items.get(path, 0)
# player.gdextends CharacterBody2D@export var inventory: Inventoryfunc _ready(): if not inventory: inventory = Inventory.new() inventory.item_added.connect(_on_item_added) inventory.inventory_changed.connect(_update_ui)func pickup_item(item: Item, quantity: int = 1): if inventory.add_item(item, quantity): print("Picked up ", quantity, "x ", item.item_name) else: print("Inventory full!")func use_item(item: Item): if inventory.has_item(item): if item.use(self): inventory.remove_item(item, 1)func _on_item_added(item: Item, quantity: int): print("Added to inventory: ", item.item_name)func _update_ui(): # Update inventory UI pass