wrote a prioritized scene preloader for Godot 4, tired of chaining awaits in level transitions

181 views 0 replies

The standard pattern for async scene loading in Godot 4 is: call ResourceLoader.load_threaded_request(), poll status in _process, do something with the result. Fine for one place. Once you have a loading screen, a background prewarmer, and three different spots in the game that need scenes at different times, you end up with a nest of awaits and signals that are genuinely hard to trace when something loads in the wrong order or fires twice.

So I wrote a small autoload that wraps threaded loading with a priority queue and a plain callback model. Call ScenePreloader.request() from anywhere, pass a Callable, and it fires when the resource is ready. Same path requested twice gets deduplicated. Both callbacks fire on load. Already-cached paths return synchronously.

# ScenePreloader.gd — add as Autoload
extends Node

enum Priority { HIGH, NORMAL, LOW }

var _queue: Array[Dictionary] = []
var _cache: Dictionary = {}

func request(path: String, callback: Callable, priority: Priority = Priority.NORMAL) -> void:
    if _cache.has(path):
        callback.call(_cache[path])
        return
    for entry in _queue:
        if entry.path == path:
            entry.callbacks.append(callback)
            return
    _queue.append({ "path": path, "callbacks": [callback], "priority": priority })
    _queue.sort_custom(func(a, b): return a.priority < b.priority)
    ResourceLoader.load_threaded_request(path)

func _process(_delta: float) -> void:
    var i := 0
    while i < _queue.size():
        var entry := _queue[i]
        match ResourceLoader.load_threaded_get_status(entry.path):
            ResourceLoader.THREAD_LOAD_LOADED:
                var res := ResourceLoader.load_threaded_get(entry.path)
                _cache[entry.path] = res
                for cb: Callable in entry.callbacks:
                    cb.call(res)
                _queue.remove_at(i)
            ResourceLoader.THREAD_LOAD_FAILED:
                push_error("ScenePreloader: failed — " + entry.path)
                _queue.remove_at(i)
            _:
                i += 1

Usage:

# High priority — jumps to front of queue
ScenePreloader.request("res://scenes/level_2.tscn", func(s): get_tree().change_scene_to_packed(s), ScenePreloader.Priority.HIGH)

# Background prewarm, low priority
ScenePreloader.request("res://scenes/credits.tscn", func(_s): pass, ScenePreloader.Priority.LOW)

The priority sorting matters most at transition moments. When a player hits a door, I want the next scene at the front of the queue, not competing with background preloads. The cache is intentionally unbounded for now, which is fine at my project scale, but you'd want some eviction strategy if you're cycling through a lot of unique scenes.

One gap: no progress reporting. If you need a loading bar you'd have to poll load_threaded_get_status() with the Array variant separately and wire that up yourself. Haven't needed it yet. Anyone doing something similar? Mostly curious if there's a cleaner approach to the deduplication check than the linear scan I'm doing right now.

Moonjump
Forum Search Shader Sandbox
Sign In Register