Level transitions
Autoload, scene changes, completion signals and repeated requests.
Levels and autoload
Requires two saved documents, such as asset://Levels/Level01.rscene and asset://Levels/Level02.rscene. Each level needs its own camera and lighting. Assign the first as the project's main document.
Create Scripts/LevelRouter.cs. Under Project Settings → Globals → Autoload, add the script with the name LevelRouter. An autoload object lives outside the current scene and survives level changes.
LevelRouter.cs
using System.Threading.Tasks;
using Reckfell;
public partial class LevelRouter : Entity
{
public bool IsChanging { get; private set; }
public async Task<Error> OpenLevel(string path)
{
if (IsChanging)
return Error.Busy;
if (!IsInsideTree())
return Error.Unconfigured;
if (!ResourceLoader.Exists(path, "PackedEntity"))
return Error.FileNotFound;
IsChanging = true;
EntityTree tree = GetTree();
try
{
Error error = tree.ChangeSceneToFile(path);
if (error != Error.Ok)
return error;
await ToSignal(tree, EntityTree.SignalName.SceneChanged);
return Error.Ok;
}
finally
{
IsChanging = false;
}
}
}Route all project scene changes through this object. Until one transition completes, another request returns Busy. finally releases the guard after either a successful change or a returned loading error.
A transition button
In the first level, add a CanvasLayer containing a Control with Full Rect layout, then a child Button. Set the button's Text to Next level, Position to (24, 24) and Size to (240, 48), with top-left anchors. This hierarchy places the button over the 3D world.
Attach OpenLevelButton.cs to the button. After building, set Level Path to asset://Levels/Level02.rscene.
using Reckfell;
public partial class OpenLevelButton : Button
{
[Export] public string LevelPath { get; set; } = "asset://Levels/Level02.rscene";
public override void _Ready()
{
Pressed += Open;
}
private async void Open()
{
var router = GetEntityOrNull<LevelRouter>("/root/LevelRouter");
if (router == null)
{
ReckfellApi.PushError("Add LevelRouter to Autoload.");
return;
}
Error error = await router.OpenLevel(LevelPath);
if (error != Error.Ok && error != Error.Busy)
ReckfellApi.PushError($"Level change failed: {error}");
}
}Press F5 and click the button: the second level replaces the first. Code after await does not access the button, which belonged to the old level and has already been deleted.
Transition order
| Point | Tree state |
|---|---|
ChangeSceneToFile returns Ok | The old scene is already outside the tree; CurrentScene is temporarily null |
| End of frame | The old tree is deleted; the new tree is added and runs _Ready() |
SceneChanged signal | The new CurrentScene is available and OpenLevel() completes |
Do not access the new tree immediately after ChangeSceneToFile: await SceneChanged or initialize objects in their _Ready(). References to old-level objects become invalid. LevelRouter stays at /root/LevelRouter.
Keep cross-level state in a separate autoload. Restoring it after closing the game requires disk persistence; autoload state lasts only for the current run.
Errors and limits
| Symptom | Check |
|---|---|
Add LevelRouter to Autoload | The autoload name must be exactly LevelRouter |
FileNotFound | Check the asset:// path, case and saved document |
CantOpen / CantCreate | The file cannot load as PackedEntity or cannot instantiate; read the first Output error |
| New level is black | Check its camera, Current, light and object positions |
| Access error after a transition | Code is using an object deleted with the old level |
ChangeSceneToFile loads synchronously and may pause on large levels. Scene replacement does not implement open-world section streaming. Use EntityTree.ReloadCurrentScene() to restart the current document, with the same deferred transition completion.
Reference: EntityTree, ResourceLoader, Button.