2023-01-12 20:49:14 +01:00
|
|
|
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
Background loading
|
|
|
|
==================
|
|
|
|
|
|
|
|
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
|
2023-01-12 19:43:03 +01:00
|
|
|
(`ResourceLoader::load` or just `load` from GDScript) blocks your
|
2022-03-18 17:46:08 +01:00
|
|
|
thread, making your game appear frozen and unresponsive while the resource is being loaded. This
|
2023-01-12 19:43:03 +01:00
|
|
|
document discusses the alternative of using the `ResourceInteractiveLoader` class for smoother
|
2022-03-18 17:46:08 +01:00
|
|
|
load screens.
|
|
|
|
|
|
|
|
ResourceInteractiveLoader
|
|
|
|
-------------------------
|
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
The `ResourceInteractiveLoader` class allows you to load a resource in
|
|
|
|
stages. Every time the method `poll` is called, a new stage is loaded,
|
2022-03-18 17:46:08 +01:00
|
|
|
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.
|
|
|
|
|
|
|
|
Usage
|
|
|
|
-----
|
|
|
|
|
|
|
|
Usage is generally as follows
|
|
|
|
|
|
|
|
Obtaining a ResourceInteractiveLoader
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
.. code-block:: cpp
|
|
|
|
|
2023-01-12 20:47:54 +01:00
|
|
|
Ref( ResourceInteractiveLoader> ResourceLoader::load_interactive(String p_path);
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
This method will give you a ResourceInteractiveLoader that you will use
|
|
|
|
to manage the load operation.
|
|
|
|
|
|
|
|
Polling
|
|
|
|
~~~~~~~
|
|
|
|
|
|
|
|
.. code-block:: cpp
|
|
|
|
|
|
|
|
Error ResourceInteractiveLoader::poll();
|
|
|
|
|
|
|
|
Use this method to advance the progress of the load. Each call to
|
2023-01-12 19:43:03 +01:00
|
|
|
`poll` will load the next stage of your resource. Keep in mind that
|
2022-03-18 17:46:08 +01:00
|
|
|
each stage is one entire "atomic" resource, such as an image, or a mesh,
|
|
|
|
so it will take several frames to load.
|
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
Returns `OK` on no errors, `ERR_FILE_EOF` when loading is finished.
|
2022-03-18 17:46:08 +01:00
|
|
|
Any other return value means there was an error and loading has stopped.
|
|
|
|
|
|
|
|
Load progress (optional)
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
To query the progress of the load, use the following methods:
|
|
|
|
|
|
|
|
.. code-block:: cpp
|
|
|
|
|
|
|
|
int ResourceInteractiveLoader::get_stage_count() const;
|
|
|
|
int ResourceInteractiveLoader::get_stage() const;
|
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
`get_stage_count` returns the total number of stages to load.
|
|
|
|
`get_stage` returns the current stage being loaded.
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
Forcing completion (optional)
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
.. code-block:: cpp
|
|
|
|
|
|
|
|
Error ResourceInteractiveLoader::wait();
|
|
|
|
|
|
|
|
Use this method if you need to load the entire resource in the current
|
|
|
|
frame, without any more steps.
|
|
|
|
|
|
|
|
Obtaining the resource
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
.. code-block:: cpp
|
|
|
|
|
2023-01-12 20:47:54 +01:00
|
|
|
Ref( Resource> ResourceInteractiveLoader::get_resource();
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
If everything goes well, use this method to retrieve your loaded
|
|
|
|
resource.
|
|
|
|
|
|
|
|
Example
|
|
|
|
-------
|
|
|
|
|
|
|
|
This example demonstrates how to load a new scene. Consider it in the
|
2023-01-12 19:29:11 +01:00
|
|
|
context of the `doc_singletons_autoload` example.
|
2022-03-18 17:46:08 +01:00
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
First, we set up some variables and initialize the `current_scene`
|
2022-03-18 17:46:08 +01:00
|
|
|
with the main scene of the game:
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
var loader
|
|
|
|
var wait_frames
|
|
|
|
var time_max = 100 # msec
|
|
|
|
var current_scene
|
|
|
|
|
|
|
|
|
|
|
|
func _ready():
|
|
|
|
var root = get_tree().get_root()
|
|
|
|
current_scene = root.get_child(root.get_child_count() -1)
|
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
The function `goto_scene` is called from the game when the scene
|
2022-03-18 17:46:08 +01:00
|
|
|
needs to be switched. It requests an interactive loader, and calls
|
2023-01-12 20:57:31 +01:00
|
|
|
`set_process(true)` to start polling the loader in the `process`
|
2022-03-18 17:46:08 +01:00
|
|
|
callback. It also starts a "loading" animation, which could show a
|
|
|
|
progress bar or loading screen.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func goto_scene(path): # Game requests to switch to this scene.
|
|
|
|
loader = ResourceLoader.load_interactive(path)
|
|
|
|
if loader == null: # Check for errors.
|
|
|
|
show_error()
|
|
|
|
return
|
|
|
|
set_process(true)
|
|
|
|
|
|
|
|
current_scene.queue_free() # Get rid of the old scene.
|
|
|
|
|
|
|
|
# Start your "loading..." animation.
|
|
|
|
get_node("animation").play("loading")
|
|
|
|
|
|
|
|
wait_frames = 1
|
|
|
|
|
2023-01-12 20:57:31 +01:00
|
|
|
`process` is where the loader is polled. `poll` is called, and then
|
2023-01-12 19:43:03 +01:00
|
|
|
we deal with the return value from that call. `OK` means keep polling,
|
|
|
|
`ERR_FILE_EOF` means loading 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.
|
2022-03-18 17:46:08 +01:00
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
Note how we use `OS.get_ticks_msec` to control how long we block the
|
2022-03-18 17:46:08 +01:00
|
|
|
thread. Some stages might load fast, which means we might be able
|
2023-01-12 19:43:03 +01:00
|
|
|
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
|
2022-03-18 17:46:08 +01:00
|
|
|
precise control over the timings.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func _process(time):
|
|
|
|
if loader == null:
|
|
|
|
# no need to process anymore
|
|
|
|
set_process(false)
|
|
|
|
return
|
|
|
|
|
|
|
|
# Wait for frames to let the "loading" animation show up.
|
|
|
|
if wait_frames > 0:
|
|
|
|
wait_frames -= 1
|
|
|
|
return
|
|
|
|
|
|
|
|
var t = OS.get_ticks_msec()
|
|
|
|
# Use "time_max" to control for how long we block this thread.
|
2023-01-12 20:47:54 +01:00
|
|
|
while OS.get_ticks_msec() ( t + time_max:
|
2022-03-18 17:46:08 +01:00
|
|
|
# Poll your loader.
|
|
|
|
var err = loader.poll()
|
|
|
|
|
|
|
|
if err == ERR_FILE_EOF: # Finished loading.
|
|
|
|
var resource = loader.get_resource()
|
|
|
|
loader = null
|
|
|
|
set_new_scene(resource)
|
|
|
|
break
|
|
|
|
elif err == OK:
|
|
|
|
update_progress()
|
|
|
|
else: # Error during loading.
|
|
|
|
show_error()
|
|
|
|
loader = null
|
|
|
|
break
|
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
Some extra helper functions. `update_progress` updates a progress bar,
|
2022-03-18 17:46:08 +01:00
|
|
|
or can also update a paused animation (the animation represents the
|
2023-01-12 19:43:03 +01:00
|
|
|
entire load process from beginning to end). `set_new_scene` puts the
|
2022-03-18 17:46:08 +01:00
|
|
|
newly loaded scene on the tree. Because it's a scene being loaded,
|
2023-01-12 19:43:03 +01:00
|
|
|
`instance()` needs to be called on the resource obtained from the
|
2022-03-18 17:46:08 +01:00
|
|
|
loader.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func update_progress():
|
|
|
|
var progress = float(loader.get_stage()) / loader.get_stage_count()
|
|
|
|
# Update your progress bar?
|
|
|
|
get_node("progress").set_progress(progress)
|
|
|
|
|
|
|
|
# ...or update a progress animation?
|
|
|
|
var length = 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)
|
|
|
|
|
|
|
|
|
|
|
|
func set_new_scene(scene_resource):
|
|
|
|
current_scene = scene_resource.instance()
|
|
|
|
get_node("/root").add_child(current_scene)
|
|
|
|
|
|
|
|
Using multiple threads
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
ResourceInteractiveLoader can be used from multiple threads. A couple of
|
|
|
|
things to keep in mind if you attempt it:
|
|
|
|
|
|
|
|
Use a semaphore
|
|
|
|
~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
While your thread waits for the main thread to request a new resource,
|
2023-01-12 19:43:03 +01:00
|
|
|
use a `Semaphore` to sleep (instead of a busy loop or anything similar).
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
Not blocking main thread during the polling
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
If you have a mutex to allow calls from the main thread to your loader
|
2023-01-12 19:43:03 +01:00
|
|
|
class, don't lock the main thread while you call `poll` on your loader class. When a
|
2022-03-18 17:46:08 +01:00
|
|
|
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.
|
|
|
|
|
|
|
|
Example class
|
|
|
|
-------------
|
|
|
|
|
|
|
|
You can find an example class for loading resources in threads here:
|
2023-01-12 20:47:54 +01:00
|
|
|
:download:`resource_queue.gd ( files/resource_queue.gd )`. Usage is as follows:
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func start()
|
|
|
|
|
|
|
|
Call after you instance the class to start the thread.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func queue_resource(path, p_in_front = false)
|
|
|
|
|
|
|
|
Queue a resource. Use optional argument "p_in_front" to put it in
|
|
|
|
front of the queue.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func cancel_resource(path)
|
|
|
|
|
|
|
|
Remove a resource from the queue, discarding any loading done.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func is_ready(path)
|
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
Returns `true` if a resource is fully loaded and ready to be retrieved.
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func get_progress(path)
|
|
|
|
|
|
|
|
Get the progress of a resource. Returns -1 if there was an error (for example if the
|
|
|
|
resource is not in the queue), or a number between 0.0 and 1.0 with the
|
|
|
|
progress of the load. Use mostly for cosmetic purposes (updating
|
2023-01-12 19:43:03 +01:00
|
|
|
progress bars, etc), use `is_ready` to find out if a resource is
|
2022-03-18 17:46:08 +01:00
|
|
|
actually ready.
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
func get_resource(path)
|
|
|
|
|
2023-01-12 19:43:03 +01:00
|
|
|
Returns the fully loaded resource, or `null` on error. If the resource is
|
|
|
|
not fully loaded (`is_ready` returns `false`), it will block your thread
|
2022-03-18 17:46:08 +01:00
|
|
|
and finish the load. If the resource is not on the queue, it will call
|
2023-01-12 19:43:03 +01:00
|
|
|
`ResourceLoader::load` to load it normally and return it.
|
2022-03-18 17:46:08 +01:00
|
|
|
|
|
|
|
Example:
|
|
|
|
~~~~~~~~
|
|
|
|
|
|
|
|
::
|
|
|
|
|
|
|
|
# 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 progress.
|
|
|
|
if queue.is_ready("res://level_1.tscn"):
|
|
|
|
show_new_level(queue.get_resource("res://level_1.tscn"))
|
|
|
|
else:
|
|
|
|
update_progress(queue.get_progress("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. If you run into any issues, ask for help in one of
|
2023-01-12 20:39:50 +01:00
|
|
|
`Godot's community channels ( https://godotengine.org/community )`.
|