godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 69 forks source link

Emit a signal when `ResourceLoader.load_threaded_request()` finishes #10386

Open torbenvanassche opened 1 month ago

torbenvanassche commented 1 month ago

Describe the project you are working on

I am working on a project where i want to have adjacent rooms load into memory when you enter a room. For this I am using the ResourceLoader.load_threaded_request. This allows me to dynamically load the nodes I need and clean up the ones I don't.

Describe the problem or limitation you are having in your project

The problem is that there is no way of knowing when something has finished loading, other than manually checking. The only way this is currently possible is by periodically checking the status of ResourceLoader.load_threaded_get_status. This makes it a little tricky to manage the data and its loading.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Having a signal would prevent the need for the periodic check, I would just be able to connect the signal to add the node directly, rather than polling every X seconds to see if the asset is ready, then instantiating it if it is.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

In terms of how it works on the engine side, I have little insight to offer. However, as a user I could see the following work:

ResourceLoader.load_threaded_request(packed_scene.resource_path, type_string(typeof(PackedScene)))

Where you can then have signal load_threaded_finished(data: Variant) so you can work with the data you receive directly

If this enhancement will not be used often, can it be worked around with a few lines of script?

I wrote some code that works around the issue with a Timer but as I mentioned above, this is not optimal:

class_name SceneCache extends Node

var cached_scenes: Array[SceneInfo] = [];
var loading_queue: Array[SceneInfo] = [];

func _init():
    var timer = Timer.new();
    timer.timeout.connect(_check_progress)
    timer.wait_time = 0.1;

    Manager.instance.orphan_timers.add_child(timer)
    timer.start();

func queue(scene_info: SceneInfo):
    loading_queue.append(scene_info);
    var error = ResourceLoader.load_threaded_request(scene_info.packed_scene.resource_path, type_string(typeof(PackedScene)))
    if error:
        loading_queue.erase(scene_info)
        Debug.err(error)

func _check_progress():
    for loading in loading_queue:
        if ResourceLoader.load_threaded_get_status(loading.packed_scene.resource_path) == ResourceLoader.THREAD_LOAD_LOADED:
            loading.node = ResourceLoader.load_threaded_get(loading.packed_scene.resource_path).instantiate();
            cached_scenes.append(loading)
            loading_queue.erase(loading)
            loading.cached.emit(loading);

func get_from_cache(scene_info: SceneInfo) -> Node:
    if cached_scenes.has(scene_info):
        return cached_scenes[cached_scenes.find(scene_info)].node;
    else:
        queue(scene_info)
        return null;

func is_cached(scene_info: SceneInfo):
    if loading_queue.has(scene_info) || cached_scenes.has(scene_info):
        return ResourceLoader.load_threaded_get_status(scene_info.packed_scene.resource_path)
    else:
        return -1;

Is there a reason why this should be core and not an add-on in the asset library?

I think this could be useful, not just for me, but for a lot of things in the engine as a whole. Personally, I feel this is something that should exist and I was surprised that it doesn't yet.

americoperez49 commented 1 month ago

Here is one example of how I might handle this situation. You could probably combine this with signals that you create yourself to do whatever you need to do

extends Node

var thread:Thread = Thread.new()

@onready var sprite_2d:Sprite2D= $"../Sprite2D" as Sprite2D
@onready var mona_lisa:Sprite2D = $"../mona_lisa"  as Sprite2D

func _ready():
    thread.start(thread_func)

func thread_func(): 
    var mona_lisa_texture = load("res://mona.png")
    var i = 0
    var j = 10000

    while i != 30000000: # LONG WHILE LOOP TO SIMULATE LOADING TIME
        i += 1
        if i % j == 0: 
            print(i)
    call_deferred("done")
    return mona_lisa_texture

func _process(delta):
    sprite_2d.position.x += 2

func done():
    print("All finished, destroying thread objects")
    mona_lisa.texture = thread.wait_to_finish()