-
Notifications
You must be signed in to change notification settings - Fork 0
Background loading
Godot's ResourceLoader allows users to retrieve resources asynchronously; however, it requires users to periodically poll the loader since it does not trigger a callback when the loading is complete. This is both cumbersome and requires users to track the request's state making background loading unappealing for games developed in short timeframes.
To make background loading more appealing, ProtoJam provides a convenient wrapper to this functionality. The BackgroundResourceLoader functions like Godot's own loader but returns AsyncResourceHandle objects which emit a signal when the resource is ready.
Before using BackgroundResourceLoader, it's recommended to first start its background thread. This will happen automatically on the first load request but starting it manually allows you to catch any potential errors and avoid hitches during gameplay. To start the thread, simply make a call to BackgroundResourceLoader.start(). An optional thread priority may be provided. This operation can fail so you should always check the error code before making any requests. It's best to start the loader from your main scene so you can be sure it's initialized before any requests are submitted.
Once the loader is running, simply make a call to BackgroundResourceLoader.load_async(resource_path, resource_type) to request a resource. This function will return a new AsyncResourceHandle to monitor each request. A null handle will be returned if the request cannot be started for any reason.
It is best-practice to call BackgroundResourceLoader.stop() before exiting the game or performing a soft restart. This ensures the background thread is shut-down and cleaned-up properly. It is recommended to call this from the main scene's Node._notification(what: int) function when a NOTIFICATION_WM_CLOSE_REQUEST notification is received as a node's exit/exiting signals are not emitted when the engine is closed.
Once you have an AsyncResourceHandle, it's recommended to immediately connect the AsyncResourceHandle.ready(success) signal. This signal will be emitted when the resource is loaded or when an error has been encountered. The success parameter indicates if the resource was successfully loaded.
While you're waiting for the signal to be emitted, calls to AsyncResourceHandle.get_status() and AsyncResourceHandle.get_progress() may be made to watch the loader's progress.
Once loading completes successfully, AsyncResourceHandle.get_resource() will return the loaded resource.
This example demonstrates starting and stopping the loader from the main scene.
main.gd
class_name Main
extends Node
func _ready() -> void:
# Start by initializing the background loading service; we only need to do this once
var loader_error: Error = BackgroundResourceLoader.start()
if Error.OK != loader_error:
# Close the game if the loader won't start; it's unlikely we can recover from this
# Alternatively, you could choose to load your resources synchronously using `ResoureLoader.load()` if this fails
push_error("Failed to start background resource loader; error code %d" % loader_error)
NodeUtils.quit_gracefully(-1)
func _notification(what: int) -> void:
match what:
NOTIFICATION_WM_CLOSE_REQUEST:
# The game is closing; shutdown the loader
BackgroundResourceLoader.stop()This short example demonstrates a level loading its background music asynchronously.
level.gd
class_name Level
extends Node
# The path (or UID) to the resource you want to load (a .ogg file in the case of music)
const _MUSIC_PATH: String = "res://track_1.ogg"
func _ready() -> void:
# Make a request to load the music track as a stream
var music_handle: AsyncResourceHandle = BackgroundResourceLoader.load_async(_MUSIC_PATH, "AudioStreamOggVorbis")
# If the request was accepted, monitor the loader's progress
if null != music_handle:
# Wait for the loader to finish without blocking the main thread
# await allows the rest of the game to continue while we wait
var success: bool = await music_handle.ready
# If the track was loaded successfully, fetch it and start playing
if success:
var player: AudioStreamPlayer = AudioStreamPlayer.new()
player.stream = music_handle.get_resource()
player.autoplay = true
add_child(player)Note
ProtoJam's GlobalMusicPlayer is the preferred way to load and play background music. This is for demonstration purposes only.
This more advanced example demonstrates a game loading its level asynchronously with a progress bar overlay.
loading_screen.gd
class_name LoadingScreen
extends CanvasLayer
# The resource handle to monitor the progress of
var resource_handle: AsyncResourceHandle = null
# A background to obscure the screen
var _background: ColorRect = ColorRect.new()
# A bar to display loading progress
var _progress_bar: ProgressBar = ProgressBar.new()
func _ready() -> void:
_background.color = Color.BLACK
_background.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(_background)
_progress_bar.set_anchors_preset(Control.PRESET_HCENTER_WIDE)
add_child(_progress_bar)
func _process(delta: float) -> void:
# Keep updating the progress bar until the resource is ready
if is_instance_valid(resource_handle) and not resource_handle.is_ready():
_progress_bar.ratio = resource_handle.get_progress()game.gd
class_name Game
extends Node
# The path (or UID) to the resource you want to load (a .tscn file in the case of packed scenes)
const _LEVEL_PATH: String = "res://level.tscn"
func _ready() -> void:
# Cover the screen with a loading screen until the level is ready
var loading_screen: LoadingScreen = LoadingScreen.new()
add_child(loading_screen)
# Start loading the level
_load_level(loading_screen)
func _load_level(loading_screen: LoadingScreen) -> void:
# Make a request to load the level as a packed scene
var level_handle: AsyncResourceHandle = BackgroundResourceLoader.load_async(_LEVEL_PATH, "PackedScene")
# If the request was accepted, start monitoring the handle; otherwise, reload the game
if null != level_handle:
level_handle.ready.connect(_on_level_loaded.bind(loading_screen), CONNECT_ONE_SHOT | CONNECT_APPEND_SOURCE_OBJECT )
loading_screen.resource_handle = level_handle
else:
push_error("Failed to load level \"%s\"; loading failed" % _LEVEL_PATH)
get_tree().reload_current_scene()
func _on_level_loaded(success: bool, loading_screen: LoadingScreen, level_handle: AsyncResourceHandle) -> void:
# Once the request is finished, check its status and instance the level
if success:
var level_scene: PackedScene = level_handle.get_resource()
var level: Node = level_scene.instantiate()
add_child(level)
loading_screen.queue_free()
else:
push_error("Failed to load level \"%s\"; loading failed" % level_handle.get_path())
get_tree().reload_current_scene()Note
ProtoJam's NodeSwapper is the preferred way to handle more complex cases like switching between menus and transition animations. This example demonstrates a simpler use case where switching content is not required.
Contributions are always welcome! Check out the contributing guide to get started.
Made with ❤️ for humans by humans.