A Safer Way to Access Godot Resources at Runtime

Code saying get_wood pointing to a pixelated log

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.

Screenshot of a stone and axe upgrade in the Godot 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

🔗
Find the latest versions of my personal Godot scripts on my Github

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.

Generate Godot Resources From CSV
🔗Find the latest versions of my personal Godot scripts on my Github I’m going a bit more technical than usual with this post as I’m going to be sharing some of this information with others anyways. Two birds with one stone and all of that... Resources are data containers. They

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.