When switching the main scene of your game (e.g. going to a new
level), you might want to show a loading screen with some indication
that progress is being made. The main load method
(ResourceLoader::load or just load from GDScript) blocks your
thread while the resource is being loaded, so it’s not good. This
document discusses the ResourceInteractiveLoader class for smoother
load screens.

The ResourceInteractiveLoader class allows you to load a resource in
stages. Every time the method poll is called, a new stage is loaded,
and control is returned to the caller. Each stage is generally a
sub-resource that is loaded by the main resource. For example, if you’re
loading a scene that loads 10 images, each image will be one stage.

Use this method to advance the progress of the load. Each call to
poll will load the next stage of your resource. Keep in mind that
each stage is one entire “atomic” resource, such as an image, or a mesh,
so it will take several frames to load.

Returns OK on no errors, ERR_FILE_EOF when loading is finished.
Any other return value means there was an error and loading has stopped.

The function goto_scene is called from the game when the scene
needs to be switched. It requests an interactive loader, and calls
set_process(true) to start polling the loader in the _process
callback. It also starts a “loading” animation, which can show a
progress bar or loading screen, etc.

funcgoto_scene(path):# game requests to switch to this sceneloader=ResourceLoader.load_interactive(path)ifloader==null:# check for errorsshow_error()returnset_process(true)current_scene.queue_free()# get rid of the old scene# start your "loading..." animationget_node("animation").play("loading")wait_frames=1

_process is where the loader is polled. poll is called, and then
we deal with the return value from that call. OK means keep polling,
ERR_FILE_EOF means load is done, anything else means there was an
error. Also note we skip one frame (via wait_frames, set on the
goto_scene function) to allow the loading screen to show up.

Note how we use OS.get_ticks_msec to control how long we block the
thread. Some stages might load fast, which means we might be able
to cram more than one call to poll in one frame; some might take way
more than your value for time_max, so keep in mind we won’t have
precise control over the timings.

func_process(time):ifloader==null:# no need to process anymoreset_process(false)returnifwait_frames>0:# wait for frames to let the "loading" animation show upwait_frames-=1returnvart=OS.get_ticks_msec()whileOS.get_ticks_msec()<t+time_max:# use "time_max" to control how much time we block this thread# poll your loadervarerr=loader.poll()iferr==ERR_FILE_EOF:# Finished loading.varresource=loader.get_resource()loader=nullset_new_scene(resource)breakeliferr==OK:update_progress()else:# error during loadingshow_error()loader=nullbreak

Some extra helper functions. update_progress updates a progress bar,
or can also update a paused animation (the animation represents the
entire load process from beginning to end). set_new_scene puts the
newly loaded scene on the tree. Because it’s a scene being loaded,
instance() needs to be called on the resource obtained from the
loader.

funcupdate_progress():varprogress=float(loader.get_stage())/loader.get_stage_count()# Update your progress bar?get_node("progress").set_progress(progress)# ... or update a progress animation?varlength=get_node("animation").get_current_animation_length()# Call this on a paused animation. Use "true" as the second argument to force the animation to update.get_node("animation").seek(progress*length,true)funcset_new_scene(scene_resource):current_scene=scene_resource.instance()get_node("/root").add_child(current_scene)

If you have a mutex to allow calls from the main thread to your loader
class, don’t lock the former while you call poll on the latter. When a
resource is done loading, it might require some resources from the
low-level APIs (VisualServer, etc), which might need to lock the main
thread to acquire them. This might cause a deadlock if the main thread
is waiting for your mutex while your thread is waiting to load a
resource.

You can find an example class for loading resources in threads here:
resource_queue.gd. Usage is as follows:

funcstart()

Call after you instance the class to start the thread.

funcqueue_resource(path,p_in_front=false)

Queue a resource. Use optional argument “p_in_front” to put it in
front of the queue.

funccancel_resource(path)

Remove a resource from the queue, discarding any loading done.

funcis_ready(path)

Returns true if a resource is done loading and ready to be retrieved.

funcget_progress(path)

Get the progress of a resource. Returns -1 on error (for example if the
resource is not on the queue), or a number between 0.0 and 1.0 with the
progress of the load. Use mostly for cosmetic purposes (updating
progress bars, etc), use is_ready to find out if a resource is
actually ready.

funcget_resource(path)

Returns the fully loaded resource, or null on error. If the resource is
not done loading (is_ready returns false), it will block your thread
and finish the load. If the resource is not on the queue, it will call
ResourceLoader::load to load it normally and return it.

# Initialize.queue=preload("res://resource_queue.gd").new()queue.start()# Suppose your game starts with a 10 second cutscene, during which the user can't interact with the game.# For that time, we know they won't use the pause menu, so we can queue it to load during the cutscene:queue.queue_resource("res://pause_menu.tres")start_cutscene()# Later, when the user presses the pause button for the first time:pause_menu=queue.get_resource("res://pause_menu.tres").instance()pause_menu.show()# when you need a new scene:queue.queue_resource("res://level_1.tscn",true)# Use "true" as the second argument to put it at the front# of the queue, pausing the load of any other resource.# to check progressifqueue.is_ready("res://level_1.tscn"):show_new_level(queue.get_resource("res://level_1.tscn"))else:update_progress(queue.get_process("res://level_1.tscn"))# when the user walks away from the trigger zone in your Metroidvania game:queue.cancel_resource("res://zone_2.tscn")

Note: this code, in its current form, is not tested in real world
scenarios. Ask punto on IRC (#godotengine on irc.freenode.net) for help.