Loading a prefab in the background
Request a resource, poll its status and instantiate entities at the right time.
Setup
Create asset://Props/Crate.rprefab with an Entity3D root and a child MeshInstance3D using BoxMesh. In the main level, create an Entity3D named Objects and an ordinary Entity with BackgroundPrefab.cs. Assign Objects to ParentForInstance, build and run.
The script calls Begin() from _Ready(). Once loaded, an instance appears under Objects, Busy becomes false, Progress becomes 1 and LastError is Ok. A visible cube also needs a camera and lighting; loading the resource does not create them.
BackgroundPrefab.cs
using Reckfell;
public partial class BackgroundPrefab : Entity
{
[Export(PropertyHint.File, "*.rprefab,*.rscene")]
public string ResourcePath { get; set; } = "asset://Props/Crate.rprefab";
[Export] public Entity ParentForInstance { get; set; } = null!;
public bool Busy { get; private set; }
public float Progress { get; private set; }
public Error LastError { get; private set; } = Error.Ok;
public Entity? LastInstance { get; private set; }
private string _requestedPath = "";
public override void _Ready() => Begin();
public Error Begin()
{
if (Busy)
return Error.Busy;
if (!IsInsideTree() || IsQueuedForDeletion() || !ReckfellObject.IsInstanceValid(ParentForInstance) ||
!ParentForInstance.IsInsideTree() || ParentForInstance.IsQueuedForDeletion())
return Error.Unconfigured;
if (!ResourcePath.StartsWith("asset://", System.StringComparison.Ordinal) ||
!ResourceLoader.Exists(ResourcePath, "PackedEntity"))
return Error.FileNotFound;
_requestedPath = ResourcePath;
Progress = 0;
LastInstance = null;
LastError = ResourceLoader.LoadThreadedRequest(_requestedPath, "PackedEntity");
Busy = LastError == Error.Ok;
return LastError;
}
public override void _Process(double delta)
{
if (!Busy)
return;
using var progress = new Reckfell.Collections.Array();
var status = ResourceLoader.LoadThreadedGetStatus(_requestedPath, progress);
if (progress.Count != 0)
Progress = Mathf.Clamp((float)progress[0].AsDouble(), 0, 1);
if (status == ResourceLoader.ThreadLoadStatus.InProgress)
return;
Busy = false;
if (status == ResourceLoader.ThreadLoadStatus.InvalidResource)
{
LastError = Error.InvalidData;
return;
}
Resource resource = ResourceLoader.LoadThreadedGet(_requestedPath);
if (status != ResourceLoader.ThreadLoadStatus.Loaded || resource is not PackedEntity prefab)
{
LastError = Error.CantOpen;
return;
}
if (!ReckfellObject.IsInstanceValid(ParentForInstance) || !ParentForInstance.IsInsideTree() ||
ParentForInstance.IsQueuedForDeletion())
{
LastError = Error.Unconfigured;
return;
}
LastInstance = prefab.Instantiate();
ParentForInstance.AddChild(LastInstance);
Progress = 1;
LastError = Error.Ok;
}
public override void _ExitTree()
{
if (!Busy)
return;
Busy = false;
// Collect the outstanding request; this may wait for its completion.
_ = ResourceLoader.LoadThreadedGet(_requestedPath);
LastError = Error.Unavailable;
Progress = 0;
}
}Why three calls
LoadThreadedRequest() starts loading a resource. Poll LoadThreadedGetStatus() once per frame, not in a waiting loop. When populated, the progress array contains a fraction from 0 to 1, not a remaining-time estimate.
Only after Loaded does the code collect the result with LoadThreadedGet(), instantiate and attach on the main thread. An early Get() can block until completion. Failed also needs its request result collected; that failed result cannot be used as a prefab. Collecting consumes the request token, so a subsequent status can be InvalidResource.
Lifetime and restarting
Begin() returns Busy during loading instead of issuing a second request. _requestedPath captures the path; editing the exported property does not change the active operation. Another successful Begin() creates another instance; the previous one stays in the game.
If the parent disappears before completion, the result is collected without instantiating. When the loader itself leaves the tree, _ExitTree() collects any outstanding request without creating an entity. This fallback may wait for loading and delay exit; this API has no cancellation call. For a normal loading screen, keep the loader in a persistent root until completion before changing levels.
This loads one resource and dependencies; it is not a world-streaming system. Instantiation, _Ready() and graphics preparation can still occupy the main thread. useSubThreads: true is not a speed guarantee either; measure the particular resource.
Errors
| Result | Cause |
|---|---|
Unconfigured | The loader or assigned parent is outside the tree |
FileNotFound | Wrong path or no PackedEntity resource exists |
Busy from Begin() | Wait for the current operation |
InvalidData | No active request exists for the captured path |
CantOpen | Loading failed or returned another type |
Unavailable | The loader left the tree before handling the result |
Changing the current level is separate: level transitions.