Сохранить уровень в памяти при переключении
Хост двух комнат: отсоединение без удаления, сохранённые поля, повторный вход и освобождение кэша.
Выбрать стратегию
| Стратегия | Что сохраняется | За что отвечать |
|---|---|---|
| EntityTree.ChangeSceneToFile | Данные вне заменяемого корня | Повторно создать состояние уровня из файла/сохранения |
| Отсоединить и удерживать Entity | Те же объекты и их поля в памяти | Явно освободить отсоединённые корни |
| Скрыть CanvasItem | Объект остаётся в активной иерархии | Отдельно определить обработку, ввод, звук и физику |
Обычная смена EntityTree отсоединяет старый корень сразу, а освобождает его при завершении отложенной смены. Она не является бесплатным кэшем предыдущих уровней. Hide меняет видимость, но не заменяет жизненный цикл. ProcessMode.Disabled тоже не универсальный метод остановить всё: учитывайте явные режимы детей и DisableMode физических объектов.
Запустить две сохраняемые комнаты
Добавьте оба C#-файла в корень проекта 1.0.0 и создайте Rooms/A.rscene, Rooms/B.rscene по указанным путям. Прикрепите RetainedLevelHost к Entity2D отдельного уровня, задайте viewport 640×420, соберите и запустите F6. Нажмите +1, перейдите B, вернитесь A: Value сохранится, Enter увеличится, Ready останется 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);
}
}
}Хост намеренно принимает только эти две комнаты и тип RetainedRoom2D. Новый экземпляр подготавливается до отключения прежнего. Неактивная комната остаётся в словаре без Parent и не получает обычную обработку дерева. Это те же native-объекты, а не пересоздание через Pack.
Владение и выход
RemoveChild не освобождает комнату. C#-ссылка тоже не является автоматическим управлением lifetime обычной Entity. Хост обязан вызвать Free для отсоединённых экземпляров; активного ребёнка освобождает обычное удаление родительского дерева. В примере _ExitTree освобождает неактивный кэш даже при временном удалении хоста. Активный ребёнок остаётся его ребёнком и при возврате хоста входит повторно.
Повторный вход вызывает _EnterTree/_ExitTree, но не обычный _Ready. Комнаты с внешними подписками должны восстанавливать и снимать их симметрично. Отсоединённый объект всё ещё может получить прямой метод или сигнал от внешней службы, если вы оставили такую связь.
Хост не назначает CurrentScene каждой комнате: текущим уровнем EntityTree остаётся содержащий его корень. ReloadCurrentScene относится к этому корню и может пересоздать хост вместе с кэшем. Для штатной полной замены используйте EntityTree.
Когда этот подход не подходит
Кэш занимает память: ссылки на меши, текстуры и прочие ресурсы могут удерживаться живыми комнатами. Здесь нет ограничения памяти, вытеснения, background streaming или сохранения состояния на диск. Не освобождайте комнату снаружи хоста и не используйте его как готовую систему бесшовного мира. Выбирайте стратегию по измерениям проекта, а не по обещанию, что ручная смена всегда быстрее.