Retain a level in memory while switching
A two-room host: detaching without deletion, preserved fields, reentry and cache cleanup.
Choose a strategy
| Strategy | Retained data | Responsibility |
|---|---|---|
| EntityTree.ChangeSceneToFile | Data outside the replaced root | Recreate level state from its file/save data |
| Detach and retain an Entity | The same objects and in-memory fields | Explicitly free detached roots |
| Hide a CanvasItem | The object remains in the active hierarchy | Separately define processing, input, audio and physics |
Ordinary EntityTree replacement immediately detaches the old root and deletes it when deferred replacement finishes. It is not a free cache of previous levels. Hide changes visibility rather than lifecycle. ProcessMode.Disabled is not a universal stop-everything operation either: account for explicit child modes and physics-object DisableMode.
Run two retained rooms
Add both C# files at the root of a 1.0.0 project and create Rooms/A.rscene and Rooms/B.rscene at the shown paths. Attach RetainedLevelHost to an Entity2D in a separate level, set a 640×420 viewport, build and run with F6. Press +1, switch to B, then return to A: Value remains, Enter increases and Ready stays 1.
RetainedRoom2D.cs
using Reckfell;
public partial class RetainedRoom2D : Entity2D
{
[Export] public int Value { get; set; }
public int Entries { get; private set; }
public int ReadyCalls { get; private set; }
public override void _EnterTree() => Entries++;
public override void _Ready() => ReadyCalls++;
public void Increase() { Value++; QueueRedraw(); }
public override void _Draw()
{
DrawRect(new Rect2(60, 150, 500, 170), new Color(.10f, .17f, .38f));
bool ru = TranslationServer.GetLocale().StartsWith("ru");
DrawString(ThemeDB.FallbackFont, new Vector2(84, 204), Name + (ru ? " · значение: " : " · value: ") + Value, fontSize: 23);
DrawString(ThemeDB.FallbackFont, new Vector2(84, 254), "Enter: " + Entries + " · Ready: " + ReadyCalls, fontSize: 19);
}
}Rooms/A.rscene
[reckfell_tree format=3]
[ext_resource type="Script" path="asset://RetainedRoom2D.cs" id="Room"]
[entity name="RoomA" type="Entity2D"]
script = ExtResource("Room")
Value = 10Rooms/B.rscene
[reckfell_tree format=3]
[ext_resource type="Script" path="asset://RetainedRoom2D.cs" id="Room"]
[entity name="RoomB" type="Entity2D"]
script = ExtResource("Room")
Value = 20RetainedLevelHost.cs
using System.Collections.Generic;
using System.Linq;
using Reckfell;
public partial class RetainedLevelHost : Entity2D
{
public const string RoomA = "asset://Rooms/A.rscene";
public const string RoomB = "asset://Rooms/B.rscene";
public RetainedRoom2D? ActiveRoom { get; private set; }
public int CachedCount => _rooms.Count;
private readonly Dictionary<string, RetainedRoom2D> _rooms = new();
private bool _switching;
public Error SwitchTo(string path)
{
if (!IsInsideTree()) return Error.Unconfigured;
if (_switching) return Error.Busy;
if (path != RoomA && path != RoomB) return Error.InvalidParameter;
_switching = true;
try
{
if (!_rooms.TryGetValue(path, out var next))
{
var packed = ResourceLoader.Load<PackedEntity>(path);
if (packed is null || !packed.CanInstantiate()) return Error.CantOpen;
Entity instance = packed.Instantiate();
if (instance is not RetainedRoom2D room) { instance.Free(); return Error.InvalidData; }
next = room;
_rooms.Add(path, next);
}
if (next == ActiveRoom) return Error.Ok;
if (ActiveRoom is not null) RemoveChild(ActiveRoom);
ActiveRoom = next;
AddChild(next);
next.QueueRedraw();
return Error.Ok;
}
finally { _switching = false; }
}
public override void _Ready()
{
bool ru = TranslationServer.GetLocale().StartsWith("ru");
AddChild(new Label { Position = new Vector2(28, 24), Text = ru ? "Комнаты: состояние сохраняется в памяти" : "Rooms: state stays in memory" });
foreach (var item in new[] { ("A", RoomA), ("B", RoomB) })
{
string path = item.Item2;
var button = new Button { Text = item.Item1, Position = new Vector2(item.Item1 == "A" ? 28 : 104, 82) };
button.Pressed += () => { Error error = SwitchTo(path); if (error != Error.Ok) ReckfellApi.PrintErr(error); };
AddChild(button);
}
var increase = new Button { Text = "+1", Position = new Vector2(180, 82) };
increase.Pressed += () => ActiveRoom?.Increase();
AddChild(increase);
Error initial = SwitchTo(RoomA);
if (initial != Error.Ok) ReckfellApi.PrintErr(initial);
}
public override void _ExitTree()
{
foreach (var pair in _rooms.ToArray())
{
if (pair.Value == ActiveRoom) continue;
pair.Value.Free();
_rooms.Remove(pair.Key);
}
}
}The host intentionally accepts only these two rooms and RetainedRoom2D. A new instance is prepared before detaching the previous one. An inactive room stays in the dictionary without a Parent and receives no ordinary tree processing. These are the same native objects, not a Pack-based reconstruction.
Ownership and exit
RemoveChild does not free a room. A C# reference also does not automatically manage an ordinary Entity lifetime. The host must Free detached instances; normal parent-tree deletion releases the active child. Here _ExitTree clears the inactive cache even when the host is temporarily detached. Its active room remains a child and enters again when the host returns.
Reentry invokes _EnterTree/_ExitTree without automatically repeating ordinary _Ready. Rooms with external subscriptions must restore and disconnect them symmetrically. A detached object can still receive a direct call or an external service signal if you keep such a connection.
The host does not assign every room to CurrentScene: EntityTree current level remains the containing root. ReloadCurrentScene targets that root and can recreate the host and its cache. Use EntityTree for standard full replacement.
When to choose something else
The cache consumes memory: live rooms may retain meshes, textures and other resources. This example has no memory budget, eviction, background streaming or disk persistence. Do not free rooms outside the host or treat it as a finished seamless-world system. Choose from project measurements rather than assuming manual replacement is always faster.