Lazy loading Godot scenes with InstancePlaceholder
Tue Aug 27 2024
Huh?
So imagine you have a monster of a level your player has to be loaded into. It has all kinds of cool stuff in it for the player to experience and discover; there's just one problem: it takes ages to load, and waiting sucks. Well, there's one way to make it faster—by not loading everything at once but loading parts only when we need them!
There are things that are most likely out of the players view from the start that we don't need to load right away, like enemies, foliage, or buildings.
You can probably imagine some ways you could manage that with Godot. Instead of having your optional thing in the scene from the start, when a certain event happens, you just load()
it, then instantiate()
it, then call add_child()
on the level node with your new instance. But what if there was a less "manual" way?
Enter InstancePlaceholder
Godot comes with this nifty thing called InstancePlaceholder. What it does is, instead of loading a child scene right away, it loads a "marker" node in its place, and then all you need to do is call create_instance
on it, and it will spawn that child scene instance right where it was supposed to go originally.
Using InstancePlaceholder
has multiple advantages: the initial load of the scene goes considerably faster since you don't have to load everything at once, so we get to render things to the player faster. We load stuff dynamically, but in the editor we can see and work with the complete level, which makes it easy to iterate, and using InstancePlaceholder
doesn't force you to change your workflow; you can design the level as you would normally and then save separate parts as child scene instances when you are closer to being done.
OK, how about a practical example?
Practical Placeholdery
Let's start a new Godot project, and we'll do something dead-simple and useless, but the main idea is to just show how it works. I'll post the project, so you can download it and don't have to follow along.
I made a simple setup with a Control
node as the root, then added a VBoxContainer
containing some text and a public domain image of a cute little rabbit. Saved all of that as main.tscn
and set it as the main scene.
The goal is to make the VBoxContainer
part of the scene load only when we press the button. In an actual game, you'd use different logic to load a placeholder, like checking for a condition, e.g., "the player is about to go into this room; load it", user input, or maybe even something like a VisibleOnScreenNotifier2D
signal.
First of all, we will save the VBoxContainer
as a separate child scene. We can do so by just right-clicking it and clicking "Save Branch as Scene." Call the scene whatever you like; it literally does not matter as we won't be using the file path anywhere.
Now we need to mark the VBoxContainer
child scene as a placeholder. When you right-click it again, you will notice some new menu items, one of them being a "Load as Placeholder" checkbox. Tick it:
After these steps, when you launch the main scene, you will notice that the text and the image no longer appear, even though you can see them in the editor. If the VBoxContainer
node contained more than just a label and an image as its children, you'd also notice a decrease in the load time. That is because instead of the VBoxContainer
scene, Godot loaded an InstancePlaceholder
.
And now we need to write a script for the button so it loads the actual VBoxContainer
scene. Add a script to the root Control
scene:
And replace it with the following contents:
extends Control
var v_box_container: VBoxContainer
func _ready() -> void:
$Button.pressed.connect(_on_button_pressed)
func _on_button_pressed() -> void:
v_box_container = $VBoxContainer.create_instance()
Now, when you launch the project and press the button, the text and the bunny show up! We've successfully deferred loading a part of our main scene.
The only line that really matters here is the create_instance()
call. Which tells the InstancePlaceholder
to load the child scene, the place of which it's put instead, and add it as a sibling next to itself. You can also tell it to replace itself by passing true
as the first argument to create_instance()
, but then you are robbing yourself of another nice upside of using instance placeholders: being able to delete the child scene and call create_instance()
again.
Basically, you can save RAM by deleting a child scene created from a placeholder when it's not needed (e.g., it goes out of view) and instantiating it again when you need it again. There is no reason to keep it around once it is loaded if it is not used.
Using our example, it would be as simple as:
v_box_container.queue_free()
v_box_container = null
# some time later, by pressing the button again, we execute the same again ...
v_box_container = $VBoxContainer.create_instance()
Keep in mind that calls to create_instance()
aren't indempotent, as in, every time you perform this call, an instance of the child scene will be added as a sibling to the InstancePlaceholder
, so you need to take care not creating more instances than you need, but that can also work as a way to implement a mob spawner!
Basically, InstancePlaceholder
has multiple applications, and I've shown you the most useless one, but hopefully that will spark some ideas on how you can improve the load times of your levels or other types of complex scenes.
You can download the project from here, but we are not done with this yet!
Background loading
Well, you might notice that if you start putting large child scenes as placeholders and load them later, you will notice the game choke up. That's quite understandable since, under the hood, Godot does something like load()
-ing the child scene, instantiate()
-ing it, and adding it to the active scene tree, and that all takes plenty of CPU cycles, especially if there's a lot to load.
So we're back to our original issue. Waiting still sucks. Now we're just doing some more of it a bit later instead of waiting a whole lot right away.
Threads to the rescue. So, how about we do the waiting without the game being locked up? All we need to do is let the main thread draw all the pretty things for us while we employ other threads to do background loading.
Thankfully, we don't need to do anything special to do threaded background loading. Godot provides the ResourceLoader
singleton, which makes loading PackedScene
files in the background pretty straightforward, we'll just need to perform a few adjustments to our InstancePlaceholder
example. Open main.gd
in the editor and replace its contents with the following:
extends Control
var v_box_container: VBoxContainer
var path: String
@onready var placeholder: InstancePlaceholder = $VBoxContainer
func _ready() -> void:
set_process(false)
path = placeholder.get_instance_path()
$Button.pressed.connect(_on_button_pressed, CONNECT_ONE_SHOT)
func _process(_delta: float) -> void:
var status: int = ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.ThreadLoadStatus.THREAD_LOAD_LOADED:
return
set_process(false)
_create_instance()
func _on_button_pressed() -> void:
ResourceLoader.load_threaded_request(path, "", true)
set_process(true)
func _create_instance() -> void:
v_box_container = placeholder.create_instance()
As you see, the script has grown a little bit. First of all, we have added a bit of state since we'll need to use that in a couple places—a reference to the InstancePlaceholder
as well as a string path
. In the _ready()
function, we tell the node not to run its _process()
function by calling set_process(false)
, and we store the path of the PackedScene
replaced by the InstancePlaceholder
in the path
variable.
Now onto the interesting bits: when the button is pressed, instead of asking the placeholder to place an instance right away, which would result in the main thread being blocked for larger scenes, we call load_threaded_request
of the ResourceLoader
singleton. The first argument is the scene path, which we got from the InstancePlaceholder
. It can be anything that you would ordinarily pass to load()
or preload()
. Then the type hint argument, which we just leave as empty—the default. And the third argument is the usage of subthreads for loading the scene, which is beneficial for scenes with more dependencies as those can be loaded in parallel, therefore reducing the loading time. The documentation mentions that using subthreads can cause slowdowns on the main thread, but in my personal experience I haven't really noticed a negative impact. But in case you do, just flip the third argument to false
or just remove it, since false
is the default.
Pressing the button also enables processing. So now onto the _process()
function. In every tick of the engine, we ask ResourceLoader
if our scene is loaded. We're sort of being like a child on a car ride, saying, "Are we there yet?" only really, really fast. And since this call isn't heavy on the CPU, the game just continues running while the scene is loaded in the background. As soon as the ResourceLoader
tells us that the scene is loaded, we stop processing and call _create_instance()
.
In a usual scenario, we would just use ResourceLoader.load_threaded_get()
to get our PackedScene
so we can instantiate()
it and add it to the scene tree, but we have the InstancePlaceholder
for that, so all we do is, just like before, ask InstancePlaceholder
to do all that for us by calling create_instance()
. Only now this call returns faster since we have already put the PackedScene
in the cache of Godot by using ResourceLoader
. Basically, the benefit of "preloading" the scene is that the instance placeholder needs less time on the main thread for the instance creation since it doesn't need to load and parse the scene from disk; we did that part for it. This results in a smoother experience for the player as the main thread can just continue with regular game input processing and rendering instead of spending time on loading.
Conclusion
There are multiple things that we can improve here; for example, we aren't doing any error checking during the _process()
function, and we could also use another "worker" thread that could monitor the status of the loading process instead of the _process()
function.
Using threaded loading before instantiating an InstancePlaceholder
is absolute overkill for displaying a picture after clicking a button, but I wanted to use the simplest possible example I could think of to just show you how to get this going. Now we rely on a hardcoded placeholder reference, but you can use your newfound knowledge to create a more dynamic system that can work with multiple instance placeholders when your levels grow in complexity, and you could benefit from loading only a small bit ahead of time and loading other bits on demand. What I'm trying to say is—build something cool! And when you do, tag me on X or mastodon!