A Safer Way to Access Godot Resources at Runtime

I currently have a few types of resources in my game, such as InventoryItem, and Upgrade that store basic data and textures for these concepts. I have found several use cases where trying to access specific resources or resource values has caused me problems. Most of these use cases arise when needed during dynamic access at runtime to these resources, meaning that they are not embedded into a node or resource's @export
property. Resources attached to a property are managed well by Godot's editor.

Problem Cases
Checking a resource property id or value
func on_upgrade_acquired(upgrade:Upgrade, _upgrade_level:UpgradeLevel):
match upgrade.id:
"axe:
add_new_cursor(axe_cursor_scene)
"pickaxe":
add_new_cursor(pickaxe_cursor_scene)
❌Hardcoded value may requires updates over time. Will be forgotten
❌Requires remembering resource ids
Getting all resources defined in my project
var upgrades:Array[Upgrade]
var resource_paths:PackedStringArray = ResourceLoader.list_directory("res://resources/upgrade/upgrades/")
for resource_path in resource_paths:
var upgrade = ResourceLoader.load(target_upgrade_folder + resource_path) as Upgrade
upgrades.append(upgrade)
❌Hardcoded value may requires updates over time. Will be forgotten
❌All resources must be in the same folder.
Accessing a specific resource
# CHEATS
if event.is_action_pressed("give_wood"):
inventory_manager.add_items(load("res://scenes/objects/resource/data/wood.tres"), 1000)
❌Hardcoded value may requires updates over time. Will be forgotten
❌Requires remembering resource ids
Partial Solutions
I attempted trying to solve this a few ways.
Managing Enum or Constants
class_name InventoryItem extends Resource
enum ID {
WOOD = 0,
STONE = 1,
MUSHROOM = 2,
}
✅Editor auto completion
❌Need to update with every resource change
ResourceManager Singletons that Load All Resources at Startup
@export_dir var target_item_folder:String = "res://scenes/objects/resource/data/"
var item_reference:Dictionary[StringName, InventoryItem]
func populate_item_reference():
var resource_paths:PackedStringArray = ResourceLoader.list_directory(target_upgrade_folder)
for resource_path in resource_paths:
var item = ResourceLoader.load(target_item_folder + resource_path) as InventoryItem
item_reference[item.id] = item
✅Easy access to any resource if you know the name.
❌Hardcoded value may requires updates over time. Will be forgotten
❌Custom script for each resource type.
Putting These Together
# CHEATS
if event.is_action_pressed("give_wood"):
inventory_manager.add_items(inventory_manager.item_reference.get(InventoryItem.ID.WOOD), 1000)
We get some very good improvement here. If "wood" changes or is removed, all I need to do is updated the enum. The enum also gives us some nice code completion, so we don't need to memorize string values.
We can do better though.
The Solution So Far: Code Generation
After a small refactor broke a bunch of my resource lookup behavior due to moving some folders I came up with a new solution. I wanted the following:
- code auto completion in the Godot editor
- access to any resource with ease
- type safety
- caching
- protection from bugs introduced by refactoring.
I came to a pretty good solution. ResourceReferenceGenerator
This is a editor/build time tool that will generate a resource reference script alongside your custom resource script giving you everything I listed above. While there's definitely room for improvement, it has worked extremely well so far. I'll post the full code below, but I'll walk through the features here.
func _run():
ResourceReferenceGenerator.create_reference_script(InventoryItem, "id")
Given a resource class name, and property representing a unique id, as well as an optional target path, the ResourceReferenceGenerator will create a reference script granting easy access to every resource of the type specified that exists as a .tres file in your project. (This could easily be expanded to support .res as well)
Example of a Generated ResourceRef script
#DO NOT EDIT: Generated using ResourceReferenceGenerator
@tool
class_name InventoryItemRef extends Object
const MUSHROOM = "mushroom"
const STONE = "stone"
const WOOD = "wood"
static var resource_paths:Dictionary[StringName, String] = {
"mushroom": "res://scenes/objects/resource/data/mushroom.tres",
"stone": "res://scenes/objects/resource/data/stone/stone.tres",
"wood": "res://scenes/objects/resource/data/wood/wood.tres"
}
static func getr(id: String, cache_mode: ResourceLoader.CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE) -> InventoryItem:
if !id:
return null
var path:String = resource_paths.get(id, "") as String
if !path:
return null
return ResourceLoader.load(resource_paths.get(id), "", cache_mode)
static func getrall(cache_mode: ResourceLoader.CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE) -> Array[InventoryItem]:
var result_array: Array[InventoryItem] = []
for resource_path in resource_paths.values():
result_array.append(ResourceLoader.load(resource_path, "", cache_mode))
return result_array
static func getr_mushroom(cache_mode: ResourceLoader.CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE) -> InventoryItem:
return getr("mushroom", cache_mode)
static func getr_stone(cache_mode: ResourceLoader.CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE) -> InventoryItem:
return getr("stone", cache_mode)
static func getr_wood(cache_mode: ResourceLoader.CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE) -> InventoryItem:
return getr("wood", cache_mode)
Using these reference scripts let's see how they solve our above problems.
Checking a resource property id or value
func on_upgrade_acquired(upgrade:Upgrade, _upgrade_level:UpgradeLevel):
match upgrade.id:
UpgradeRef.AXE:
add_new_cursor(axe_cursor_scene)
UpgradeRef.PICKAXE:
add_new_cursor(pickaxe_cursor_scene)
✅Editor auto completion
✅Parse errors protect against invalid references
Getting all resources defined in my project
var all_upgrades = UpgradeRef.getrall()
✅No hard coded file paths
✅Automatic caching support via underlying usage of ResourceLoader
✅No complex singleton for every resource type
✅Resources can exist anywhere in the project
Accessing a specific resource
# Dynamic Access
var item = InventoryItemRef.getr(item_id)
# Direct Access
var wood_item = InventoryItemRef.getr_wood()
✅Editor auto completion
✅Parse errors protect against invalid references
The Code, And How to Use It
Add the following two files into your project. Mine are in "res://addons/dewlap_tools/"
. Then create a new script that extends EditorScript. Read more about editor runnable code in the Godot docs.
In this example, let's says you have an Enemy resource and Weapon resource. Both must have a String
property called id
with unique values. you MUST have your resource script define with a class_name, for example class_name Enemy
.
@tool
extends EditorScript
func _run():
ResourceReferenceGenerator.create_reference_script(Enemy, "id")
ResourceReferenceGenerator.create_reference_script(Weapon, "id")
Then with the file open in the Godot editor, select File -> Run
. EnemyRef and WeaponRef will be created and available to use in your project. Just remember to re-run this every time you add/remove resources.
Combining With Last Week's CSV Generation.
I have an example combining generation of resources from CSV then creating the resource reference script in my git repo.
Check out last weeks post for more info on generating resources from CSVs.

Advanced Feature: Caching
When retrieving a resource via the ResourceRef scripts, there is an optional parameter cache_mode
. The underlying implementation uses the built in Godot ResourceLoader, which support several cache modes.
static func getr(id: String, cache_mode: ResourceLoader.CacheMode = ResourceLoader.CacheMode.CACHE_MODE_REUSE)
This solution means there should be no need to implement custom caching, and the behavior should be clear based on existing documentation.
Limitations
- Requires rerunning the generator with resource changes
- Possibly solved by some editor hooks
- Does not handle locally defined resource or dynamically created resources.
- Only supports .tres file types.
- .res support easy to add.
Let me know if you think this is helpful or not and if there's any other ways to handle the above problems.