Сериализация
Сериализация — механизм создания бинарных снимков мира целиком или отдельных сущностей, кластеров, чанков. Для бинарной сериализации используется StaticPack.
Настройка компонентов
Для поддержки сериализации компонентов необходимо:
- Указать
Guidпри регистрации (стабильный идентификатор типа) - Реализовать хуки
WriteиReadна компоненте
Хуки Write и Read обязательны для сериализации через EntitiesSnapshot (для всех типов компонентов, включая unmanaged). Для снимков мира/кластера/чанка non-unmanaged типы также всегда используют эти хуки.
Unmanaged компонент:
public struct Position : IComponent, IComponentConfig<Position> {
public float X, Y, Z;
public ComponentTypeConfig<Position> Config() => new(
guid: new Guid("b121594c-456e-4712-9b64-b75dbb37e611")
);
public void Write<TWorld>(ref BinaryPackWriter writer, World<TWorld>.Entity self)
where TWorld : struct, IWorldType {
writer.WriteFloat(X);
writer.WriteFloat(Y);
writer.WriteFloat(Z);
}
public void Read<TWorld>(ref BinaryPackReader reader, World<TWorld>.Entity self, byte version, bool disabled)
where TWorld : struct, IWorldType {
X = reader.ReadFloat();
Y = reader.ReadFloat();
Z = reader.ReadFloat();
}
}
W.Types().Component<Position>();
Non-unmanaged компонент (содержит ссылочные поля):
public struct Name : IComponent, IComponentConfig<Name> {
public string Value;
public ComponentTypeConfig<Name> Config() => new(
guid: new Guid("531dc870-fdf5-4a8d-a4c6-b4911b1ea1c3")
);
public void Write<TWorld>(ref BinaryPackWriter writer, World<TWorld>.Entity self)
where TWorld : struct, IWorldType {
writer.WriteString16(Value);
}
public void Read<TWorld>(ref BinaryPackReader reader, World<TWorld>.Entity self, byte version, bool disabled)
where TWorld : struct, IWorldType {
Value = reader.ReadString16();
}
}
W.Types().Component<Name>();
Блочное копирование для unmanaged типов:
Для снимков мира/кластера/чанка unmanaged компоненты автоматически сериализуются блоком памяти вместо поэлементных вызовов Write/Read.
UnmanagedPackArrayStrategy<T> выполняет прямое копирование памяти — значительно быстрее поэлементной сериализации. Работает только для unmanaged типов. При несовпадении версий (миграция данных) система автоматически переключается на хуки Read. Стратегия по умолчанию определяется автоматически: UnmanagedPackArrayStrategy<T> для unmanaged типов, StructPackArrayStrategy<T> в остальных случаях.
Блочная сериализация сегментов для Multi и Links:
Мульти-компоненты и Links хранят значения в общем сегментном хранилище. Стратегии блочной сериализации сегментов применяются автоматически для unmanaged типов значений. Для переопределения GUID или другой конфигурации реализуйте соответствующий интерфейс на типе:
// Мульти-компонент с кастомной конфигурацией
public struct Item : IMultiComponent, IMultiComponentConfig<Item> {
public int Id;
public ComponentTypeConfig<W.Multi<Item>> Config<TWorld>()
where TWorld : struct, IWorldType => new(
guid: new Guid("...")
);
public IPackArrayStrategy<Item> ElementPackStrategy()
=> new UnmanagedPackArrayStrategy<Item>();
}
W.Types().Multi<Item>();
// Links с кастомной конфигурацией
public struct MyLinkType : ILinksType, ILinksConfig<MyLinkType> {
public ComponentTypeConfig<W.Links<MyLinkType>> Config<TWorld>()
where TWorld : struct, IWorldType => new(
guid: new Guid("...")
);
}
W.Types().Links<MyLinkType>();
Полная конфигурация:
public struct Position : IComponent, IComponentConfig<Position> {
public float X, Y, Z;
public ComponentTypeConfig<Position> Config() => new(
guid: new Guid("b121594c-456e-4712-9b64-b75dbb37e611"),
version: 1, // версия схемы данных для миграции (по умолчанию — 0)
noDataLifecycle: true // отключить управление данными фреймворком (по умолчанию — false)
// стратегия сериализации определяется автоматически: UnmanagedPackArrayStrategy<T> для unmanaged, StructPackArrayStrategy<T> в остальных случаях
);
// ... хуки Write/Read ...
}
W.Types().Component<Position>();
Настройка тегов
Теги настраиваются через реализацию ITagConfig<T>:
public struct IsPlayer : ITag, ITagConfig<IsPlayer> {
public TagTypeConfig<IsPlayer> Config() => new(
guid: new Guid("3a6fe6a2-9427-43ae-9b4a-f8582e3a5f90")
);
}
public struct IsDead : ITag, ITagConfig<IsDead> {
public TagTypeConfig<IsDead> Config() => new(
guid: new Guid("d25b7a08-cbe6-4c77-bd8e-29ce7f748c30")
);
}
W.Types()
.Tag<IsPlayer>()
.Tag<IsDead>();
Полная конфигурация:
public struct Poisoned : ITag, ITagConfig<Poisoned>,
ITrackableAdded, ITrackableDeleted {
public TagTypeConfig<Poisoned> Config() => new(
guid: new Guid("A1B2C3D4-...") // стабильный идентификатор для сериализации (по умолчанию — автоматически из имени типа)
);
}
W.Types().Tag<Poisoned>();
Отслеживание изменений включается реализацией интерфейсов-маркеров (ITrackableAdded, ITrackableDeleted) на самом типе — см. Отслеживание изменений.
Все типы автоматически получают стабильный GUID, вычисленный из имени типа. Для переопределения реализуйте ITagConfig<T> на структуре тега с пользовательским guid.
Настройка событий
Для событий реализуется IEventConfig<T> — аналогично компонентам:
public struct OnDamage : IEvent, IEventConfig<OnDamage> {
public float Amount;
public EventTypeConfig<OnDamage> Config() => new(
guid: new Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")
);
public void Write(ref BinaryPackWriter writer) {
writer.WriteFloat(Amount);
}
public void Read(ref BinaryPackReader reader, byte version) {
Amount = reader.ReadFloat();
}
}
W.Types().Event<OnDamage>();
Снимок мира (World Snapshot)
Сохраняет полное состояние мира: все сущности, компоненты, теги, события и состояние отслеживания изменений.
Снимок мира сохраняет тик и всю историю трекинга. В поток записываются CurrentTick, CurrentLastTick и все TrackingBufferSize + 1 слотов истории — для фильтров AllAdded<T> / AllChanged<T> / AllDeleted<T>, методов HasAdded/HasChanged/HasDeleted на сущности, а также мирового трекинга создания (HasCreated). После загрузки tick-based запросы (включая использующие fromTick) возвращают те же результаты, что и до сохранения.
Конфигурация должна совпадать при загрузке. Значения TrackingBufferSize и TrackCreated целевого мира должны быть равны тем, что были при сохранении. Любое несовпадение приводит к StaticEcsException. Эти значения задаются в WorldConfig при создании мира — менять их между сохранением и загрузкой нельзя.
Каждый снимок начинается с 2-байтового заголовка версии формата (FormatVersion = 2) и 8-байтового размера снимка. Загрузка снимка, созданного несовместимой версией, приводит к StaticEcsException с понятным сообщением.
Сохранение и загрузка после инициализации:
byte[] worldSnapshot = W.Serializer.CreateWorldSnapshot();
W.Destroy();
CreateWorld();
W.Initialize();
// Все существующие сущности и события удаляются перед загрузкой
W.Serializer.LoadWorldSnapshot(worldSnapshot);
Дополнительные параметры:
// Сохранение в файл
W.Serializer.CreateWorldSnapshot("path/to/world.bin");
// С GZIP сжатием
byte[] compressed = W.Serializer.CreateWorldSnapshot(gzip: true);
// Фильтрация по кластерам
W.Serializer.CreateWorldSnapshot(clusters: new ushort[] { 0, 1 });
// Стратегия записи чанков
W.Serializer.CreateWorldSnapshot(strategy: ChunkWritingStrategy.SelfOwner);
// Без событий
W.Serializer.CreateWorldSnapshot(writeEvents: false);
// Без кастомных данных
W.Serializer.CreateWorldSnapshot(withCustomSnapshotData: false);
// Загрузка из файла (gzip определяется автоматически)
W.Serializer.LoadWorldSnapshot("path/to/world.bin");
// Загрузка сжатых данных (gzip определяется автоматически)
W.Serializer.LoadWorldSnapshot(compressed);
Все компоненты и теги автоматически получают стабильный Guid, вычисленный из имени типа. Вы можете переопределить Guid через конфигурацию для обеспечения стабильности при переименовании типов.
Снимок сущностей (Entities Snapshot)
Позволяет сохранять и загружать отдельные сущности с гранулярным контролем.
Сохранение сущностей:
// Создаём писатель сущностей
using var writer = W.Serializer.CreateEntitiesSnapshotWriter();
// Записываем конкретные сущности
foreach (var entity in W.Query().Entities()) {
writer.Write(entity);
}
// Или записываем все сущности сразу
// writer.WriteAllEntities();
// Создаём снимок
byte[] snapshot = writer.CreateSnapshot();
// Или сохраняем в файл
// writer.CreateSnapshot("path/to/entities.bin");
Запись с одновременной выгрузкой:
using var writer = W.Serializer.CreateEntitiesSnapshotWriter();
// Записать и выгрузить — экономит память при стриминге
foreach (var entity in W.Query().Entities()) {
writer.WriteAndUnload(entity);
}
// Или все сущности сразу
// writer.WriteAndUnloadAllEntities();
byte[] snapshot = writer.CreateSnapshot();
Загрузка сущностей (entitiesAsNew):
Параметр entitiesAsNew определяет, как загружаются сущности:
entitiesAsNew: false(по умолчанию) — сущности восстанавливаются в те же слоты (тот же EntityGID). Если слот уже занят — ошибка в DEBUG.entitiesAsNew: true— сущности загружаются в новые слоты с новыми EntityGID. Связи между сущностями (Link, Links) могут указывать на неверные сущности.
// Загрузка в оригинальные слоты
W.Serializer.LoadEntitiesSnapshot(snapshot, entitiesAsNew: false);
// Загрузка как новые сущности
W.Serializer.LoadEntitiesSnapshot(snapshot, entitiesAsNew: true);
// С колбеком для каждой загруженной сущности
W.Serializer.LoadEntitiesSnapshot(snapshot, entitiesAsNew: true, onLoad: entity => {
Console.WriteLine($"Loaded: {entity.PrettyString}");
});
Сохранение связей между сущностями (GID Store):
Чтобы корректно загружать сущности с entitiesAsNew: false, нужно сохранить хранилище глобальных идентификаторов:
// 1. Сохраняем сущности и GID Store
using var writer = W.Serializer.CreateEntitiesSnapshotWriter();
writer.WriteAllEntities();
byte[] entitiesSnapshot = writer.CreateSnapshot();
byte[] gidSnapshot = W.Serializer.CreateGIDStoreSnapshot();
W.Destroy();
// 2. Восстанавливаем мир с GID Store
CreateWorld();
W.Initialize();
W.Serializer.RestoreFromGIDStoreSnapshot(gidSnapshot);
// Новые сущности не займут слоты сохранённых
var newEntity = W.NewEntity<Default>();
newEntity.Set(new Position { X = 1 });
// 3. Загружаем сущности в оригинальные слоты — все связи корректны
W.Serializer.LoadEntitiesSnapshot(entitiesSnapshot, entitiesAsNew: false);
GID Store хранит информацию обо всех выданных идентификаторах. Это гарантирует, что новые сущности не займут слоты выгруженных сущностей, и все связи (Link, Links, EntityGID в данных) останутся корректными.
Хранилище GID (GID Store)
// Сохранить GID Store
byte[] gidSnapshot = W.Serializer.CreateGIDStoreSnapshot();
// С GZIP сжатием
byte[] gidCompressed = W.Serializer.CreateGIDStoreSnapshot(gzip: true);
// В файл
W.Serializer.CreateGIDStoreSnapshot("path/to/gid.bin");
// Со стратегией записи чанков
W.Serializer.CreateGIDStoreSnapshot(strategy: ChunkWritingStrategy.SelfOwner);
// Фильтрация по кластерам
W.Serializer.CreateGIDStoreSnapshot(clusters: new ushort[] { 0, 1 });
// Восстановление GID Store в уже инициализированном мире
// Все сущности удаляются, состояние сбрасывается
CreateWorld();
W.Initialize();
W.Serializer.RestoreFromGIDStoreSnapshot(gidSnapshot);
Снимки кластеров и чанков
Кластер:
// Сохранить кластер
byte[] clusterSnapshot = W.Serializer.CreateClusterSnapshot(clusterId: 1);
// С данными для загрузки как новые сущности
byte[] clusterWithEntities = W.Serializer.CreateClusterSnapshot(
clusterId: 1,
withEntitiesData: true // необходимо для entitiesAsNew при загрузке
);
// Выгрузить кластер из памяти
ReadOnlySpan<ushort> clusters = stackalloc ushort[] { 1 };
W.Query().BatchUnload(EntityStatusType.Any, clusters: clusters);
// Загрузить кластер из снимка
W.Serializer.LoadClusterSnapshot(clusterSnapshot);
// Загрузить как новые сущности в другой кластер
W.Serializer.LoadClusterSnapshot(clusterWithEntities,
new EntitiesAsNewParams(entitiesAsNew: true, clusterId: 2)
);
Чанк:
// Сохранить чанк
byte[] chunkSnapshot = W.Serializer.CreateChunkSnapshot(chunkIdx: 0);
// Выгрузить чанк из памяти
ReadOnlySpan<uint> unloadChunks = stackalloc uint[] { 0 };
W.Query().BatchUnload(EntityStatusType.Any, unloadChunks);
// Загрузить чанк из снимка
W.Serializer.LoadChunkSnapshot(chunkSnapshot);
По умолчанию снимки кластеров и чанков не хранят данные идентификаторов сущностей (только данные компонентов). Если нужно загружать их как новые сущности (entitiesAsNew: true), при создании снимка укажите withEntitiesData: true.
Снимки кластеров и чанков не сохраняют данные отслеживания изменений. В отличие от снимка мира, эти частичные снимки предназначены для стриминга и миграции, когда у целевого мира свой независимый тик и своё состояние трекинга. Загрузка снимка кластера или чанка не изменяет CurrentTick, CurrentLastTick и историю трекинга целевого мира — восстанавливаются только сущности, компоненты и теги. Если нужна согласованная история трекинга через частичные снимки — используйте снимок мира.
Комплексный пример стриминга:
void PrintCounts(string label) {
Console.WriteLine($"{label} — Всего: {W.CalculateEntitiesCount()} | Загружено: {W.CalculateLoadedEntitiesCount()}");
}
// Сохраняем отдельные сущности
using var writer = W.Serializer.CreateEntitiesSnapshotWriter();
foreach (var entity in W.Query().Entities()) {
writer.WriteAndUnload(entity);
}
byte[] entitiesSnapshot = writer.CreateSnapshot();
PrintCounts("После выгрузки сущностей"); // Всего: 2 | Загружено: 0
// Создаём кластер и наполняем его
const ushort ZONE_CLUSTER = 1;
W.RegisterCluster(ZONE_CLUSTER);
struct ZoneEntityType : IEntityType { }
W.NewEntities<ZoneEntityType>(count: 2000, clusterId: ZONE_CLUSTER);
PrintCounts("После создания кластера"); // Всего: 2002 | Загружено: 2000
// Сохраняем и выгружаем кластер
byte[] clusterSnapshot = W.Serializer.CreateClusterSnapshot(ZONE_CLUSTER);
ReadOnlySpan<ushort> zoneClusters = stackalloc ushort[] { ZONE_CLUSTER };
W.Query().BatchUnload(EntityStatusType.Any, clusters: zoneClusters);
PrintCounts("После выгрузки кластера"); // Всего: 2002 | Загружено: 0
// Создаём чанк и наполняем его
var chunkIdx = W.FindNextSelfFreeChunk().ChunkIdx;
W.RegisterChunk(chunkIdx, clusterId: 0);
for (int i = 0; i < 100; i++) {
W.NewEntityInChunk<ZoneEntityType>(chunkIdx: chunkIdx);
}
PrintCounts("После создания чанка"); // Всего: 2102 | Загружено: 100
// Сохраняем и выгружаем чанк
byte[] chunkSnapshot = W.Serializer.CreateChunkSnapshot(chunkIdx);
ReadOnlySpan<uint> unloadChunks = stackalloc uint[] { chunkIdx };
W.Query().BatchUnload(EntityStatusType.Any, unloadChunks);
PrintCounts("После выгрузки чанка"); // Всего: 2102 | Загружено: 0
// Сохраняем GID Store и пересоздаём мир
byte[] gidSnapshot = W.Serializer.CreateGIDStoreSnapshot();
W.Destroy();
CreateWorld();
W.Initialize();
W.Serializer.RestoreFromGIDStoreSnapshot(gidSnapshot);
// Загружаем в любом порядке
W.Serializer.LoadClusterSnapshot(clusterSnapshot);
PrintCounts("После загрузки кластера"); // Всего: 2102 | Загружено: 2000
W.Serializer.LoadEntitiesSnapshot(entitiesSnapshot);
PrintCounts("После загрузки сущностей"); // Всего: 2102 | Загружено: 2002
W.Serializer.LoadChunkSnapshot(chunkSnapshot);
PrintCounts("После загрузки чанка"); // Всего: 2102 | Загружено: 2102
Миграция данных
Версионирование компонентов:
Параметр version в хуке Read позволяет мигрировать данные между версиями схемы:
public struct Position : IComponent, IComponentConfig<Position> {
public float X, Y, Z;
public ComponentTypeConfig<Position> Config() => new(
guid: new Guid("b121594c-456e-4712-9b64-b75dbb37e611"),
version: 1 // была версия 0, теперь 1
);
public void Write<TWorld>(ref BinaryPackWriter writer, World<TWorld>.Entity self)
where TWorld : struct, IWorldType {
writer.WriteFloat(X);
writer.WriteFloat(Y);
writer.WriteFloat(Z);
}
public void Read<TWorld>(ref BinaryPackReader reader, World<TWorld>.Entity self, byte version, bool disabled)
where TWorld : struct, IWorldType {
X = reader.ReadFloat();
Y = reader.ReadFloat();
// В версии 0 не было Z — задаём значение по умолчанию
Z = version >= 1 ? reader.ReadFloat() : 0f;
}
}
// Регистрация
W.Types().Component<Position>();
Миграция удалённых типов:
Если компонент, тег или событие было удалено из кода, по умолчанию данные пропускаются автоматически. Для кастомной обработки:
// Миграция удалённого компонента
W.Serializer.SetComponentDeleteMigrator(
new Guid("guid-удалённого-компонента"),
(ref BinaryPackReader reader, W.Entity entity, byte version, bool disabled) => {
// Прочитать ВСЕ данные и выполнить кастомную логику
}
);
// Миграция удалённого тега
W.Serializer.SetMigrator(
new Guid("guid-удалённого-тега"),
(W.Entity entity) => {
// Кастомная логика
}
);
// Миграция удалённого события
W.Serializer.SetEventDeleteMigrator(
new Guid("guid-удалённого-события"),
(ref BinaryPackReader reader, byte version) => {
// Прочитать ВСЕ данные и выполнить кастомную логику
}
);
При добавлении новых типов старые снимки загружаются корректно — новые компоненты просто отсутствуют на загруженных сущностях.
Колбеки
Глобальные колбеки:
// Вызываются при всех типах снимков (World, Cluster, Chunk, Entities)
// Перед созданием снимка
W.Serializer.RegisterPreCreateSnapshotCallback(param => {
Console.WriteLine($"Создание снимка типа: {param.Type}");
});
// После создания снимка
W.Serializer.RegisterPostCreateSnapshotCallback(param => {
Console.WriteLine($"Снимок создан: {param.Type}");
});
// Перед загрузкой снимка
W.Serializer.RegisterPreLoadSnapshotCallback(param => {
Console.WriteLine($"Загрузка снимка: {param.Type}, AsNew: {param.EntitiesAsNew}");
});
// После загрузки снимка
W.Serializer.RegisterPostLoadSnapshotCallback(param => {
Console.WriteLine($"Снимок загружен: {param.Type}");
});
Фильтрация по типу снимка:
W.Serializer.RegisterPreCreateSnapshotCallback(param => {
if (param.Type == SnapshotType.World) {
Console.WriteLine("Сохранение мира");
}
});
Колбеки для каждой сущности:
// После сохранения каждой сущности
W.Serializer.RegisterPostCreateSnapshotEachEntityCallback((entity, param) => {
Console.WriteLine($"Сохранена: {entity.PrettyString}");
});
// После загрузки каждой сущности
W.Serializer.RegisterPostLoadSnapshotEachEntityCallback((entity, param) => {
Console.WriteLine($"Загружена: {entity.PrettyString}");
});
Кастомные данные в снимках
Глобальные кастомные данные:
// Добавить произвольные данные в снимок (например, данные систем или сервисов)
W.Serializer.SetSnapshotHandler(
new Guid("57c15483-988a-47e7-919c-51b9a7b957b5"), // уникальный guid типа данных
version: 0,
writer: (ref BinaryPackWriter writer, SnapshotWriteParams param) => {
writer.WriteDateTime(DateTime.Now);
},
reader: (ref BinaryPackReader reader, ushort version, SnapshotReadParams param) => {
var savedTime = reader.ReadDateTime();
Console.WriteLine($"Время сохранения: {savedTime}");
}
);
Кастомные данные для каждой сущности:
W.Serializer.SetSnapshotHandlerEachEntity(
new Guid("68d26594-1a9b-48f8-b2de-71c0a8b068c6"),
version: 0,
writer: (ref BinaryPackWriter writer, W.Entity entity, SnapshotWriteParams param) => {
// Записать дополнительные данные для сущности
},
reader: (ref BinaryPackReader reader, W.Entity entity, ushort version, SnapshotReadParams param) => {
// Прочитать дополнительные данные для сущности
}
);
Сериализация событий
// Сохранить события
byte[] eventsSnapshot = W.Serializer.CreateEventsSnapshot();
// С GZIP сжатием
byte[] eventsCompressed = W.Serializer.CreateEventsSnapshot(gzip: true);
// В файл
W.Serializer.CreateEventsSnapshot("path/to/events.bin");
// Загрузить события
W.Serializer.LoadEventsSnapshot(eventsSnapshot);
// Из файла
W.Serializer.LoadEventsSnapshot("path/to/events.bin");
При использовании CreateWorldSnapshot события сохраняются автоматически (если не указано writeEvents: false). Отдельная сериализация событий нужна при использовании EntitiesSnapshot.
Сериализация ресурсов
IResource имеет четыре опциональных метода с дефолтной реализацией. Переопределите Guid(), чтобы подключить ресурс к автоматической сериализации; остальные нужны только если тип не unmanaged.
public interface IResource {
public Guid? Guid() => null;
public byte Version() => 0;
public void Write(ref BinaryPackWriter writer) {}
public void Read(ref BinaryPackReader reader, byte version) {}
}
Правила валидации (проверяются при первом SetResource)
- Ресурсы без
Guid(дефолтnull) молча не попадают в снапшот. - Если
Guidне пуст, тип не unmanaged (ссылочный тип или struct, содержащий ссылки) и нет одного изWrite/Read— выбрасываетсяStaticEcsException. - Дубликат
Guidмежду двумя singleton-ресурсами разных типов ассертится в DEBUG.
Выбор формата
- Unmanaged struct без
Write/Read— фреймворк пишет/читаетUnsafe.SizeOf<T>()сырых байт напрямую изResources<TWorld, T>.Valueили из box-а именованного ресурса. - Не-unmanaged тип, либо несовпадение версии для unmanaged — вызывается
Read(ref reader, savedVersion)для миграции; на сохранение —Write(ref writer).
Примеры
// Unmanaged singleton-ресурс — Write/Read не требуются
public struct GameSettings : IResource {
public float MasterVolume;
public bool Vsync;
public Guid? Guid() => new("11111111-2222-3333-4444-555555555555");
}
// Не-unmanaged ресурс — Write/Read обязательны
public class AssetCache : IResource {
public Dictionary<string, byte[]> Items = new();
public Guid? Guid() => new("22222222-3333-4444-5555-666666666666");
public byte Version() => 1;
public void Write(ref BinaryPackWriter writer) {
writer.WriteInt(Items.Count);
foreach (var kvp in Items) {
writer.WriteString16(kvp.Key);
writer.WriteByteArray(kvp.Value);
}
}
public void Read(ref BinaryPackReader reader, byte version) {
Items.Clear();
var count = reader.ReadInt();
for (var i = 0; i < count; i++) {
var key = reader.ReadString16();
Items[key] = reader.ReadByteArray();
}
}
}
Что попадает в снапшот
- Singleton-ресурсы (
SetResource<T>(value, …)) — ключ —GuidтипаT. - Именованные ресурсы (
SetResource<T>(key, value, …)) — ключ —GuidтипаTплюс строковый ключ.
WorldSnapshot автоматически включает обе группы (между событиями и пользовательскими SnapshotHandlers). Чтобы сохранить или загрузить только ресурсы, есть отдельный API по образцу событий:
// Сохранить
byte[] snapshot = W.Serializer.CreateResourcesSnapshot();
W.Serializer.CreateResourcesSnapshot("resources.bin", gzip: true);
// Загрузить
W.Serializer.LoadResourcesSnapshot(snapshot);
W.Serializer.LoadResourcesSnapshot("resources.bin");
При загрузке записи, Guid которых не зарегистрирован сейчас, молча пропускаются (как и для удалённых компонентов или событий) — добавление или удаление типа ресурса между save и load forward-совместимо.
Сериализация систем
ISystem имеет те же четыре опциональных метода, что и IResource. Переопределите Guid(), чтобы подключить систему к сериализации.
public interface ISystem {
public void Init() { }
public void Update() { }
public bool UpdateIsActive() => true;
public void Destroy() { }
public Guid? Guid() => null;
public byte Version() => 0;
public void Write(ref BinaryPackWriter writer) {}
public void Read(ref BinaryPackReader reader, byte version) {}
}
Правила валидации (проверяются при Add<TSystem>)
- Системы без
Guidмолча не попадают в снапшот. - Любая система, объявляющая
Guid, обязана переопределить иWrite, иRead— независимо от лэйаута. Unmanaged fast-path не применяется: инстансы систем хранятся упакованными вSystemData.System, фреймворк всегда вызывает хуки. Их отсутствие выбрасываетStaticEcsExceptionизAdd. - Дубликат
Guidвнутри одной группыSystems<TSystemsType>ассертится в DEBUG.
Пример
public class SpawnerSystem : ISystem {
private int _nextId;
private float _accumulator;
public Guid? Guid() => new("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
public byte Version() => 1;
public void Update() { /* логика спавна */ }
public void Write(ref BinaryPackWriter writer) {
writer.WriteInt(_nextId);
writer.WriteFloat(_accumulator);
}
public void Read(ref BinaryPackReader reader, byte version) {
_nextId = reader.ReadInt();
_accumulator = reader.ReadFloat();
}
}
Systems<TSystemsType>.Create принимает явный Guid группы
GameSys.Create(baseSize: 64); // Guid = typeof(GameSystems).GuidFromAQN()
GameSys.Create(baseSize: 64, snapshotGuid: new("…stable-pipeline-guid…")); // явный, переживает переименования namespace-а
Группа регистрируется в реестре снапшотов мира на Create и снимается на Destroy. WorldSnapshot обходит все зарегистрированные группы и пишет по секции на каждую; при загрузке секции с незарегистрированным Guid молча пропускаются.
Отдельный API
Зеркалирует Create/LoadEventsSnapshot. Обходит все зарегистрированные Systems<TSystemsType>-группы (вместе с их scoped-ресурсами):
// Сохранить
byte[] snapshot = W.Serializer.CreateSystemsSnapshot();
W.Serializer.CreateSystemsSnapshot("systems.bin", gzip: true);
// Загрузить
W.Serializer.LoadSystemsSnapshot(snapshot);
W.Serializer.LoadSystemsSnapshot("systems.bin");
Каждая секция группы содержит её scoped-ресурсы (singleton + named), затем все системы внутри неё, объявляющие Guid.
Пользовательский GUID для стабильности
Все типы автоматически получают стабильный Guid, вычисленный из имени типа (assembly-qualified name). При переименовании или перемещении типа автоматический GUID изменится — что нарушит совместимость с существующими снимками. Чтобы этого избежать, задайте фиксированный GUID:
// Пример: сохранение всех сущностей
using var writer = W.Serializer.CreateEntitiesSnapshotWriter();
writer.WriteAllEntities();
byte[] snapshot = writer.CreateSnapshot();
byte[] gidSnapshot = W.Serializer.CreateGIDStoreSnapshot();
byte[] eventsSnapshot = W.Serializer.CreateEventsSnapshot();
Сжатие (GZIP)
Все методы создания снимков поддерживают GZIP сжатие через gzip: true. Все методы загрузки (LoadWorldSnapshot, LoadClusterSnapshot, LoadChunkSnapshot, LoadEventsSnapshot, LoadResourcesSnapshot, LoadSystemsSnapshot, RestoreFromGIDStoreSnapshot) автоматически определяют gzip по байтовой последовательности — передавайте байты или путь напрямую без флага.
// Мир — gzip определяется автоматически при загрузке
byte[] snapshot = W.Serializer.CreateWorldSnapshot(gzip: true);
W.Serializer.LoadWorldSnapshot(snapshot);
// Кластер — gzip определяется автоматически при загрузке
byte[] cluster = W.Serializer.CreateClusterSnapshot(1, gzip: true);
W.Serializer.LoadClusterSnapshot(cluster);
// Чанк — gzip определяется автоматически при загрузке
byte[] chunk = W.Serializer.CreateChunkSnapshot(0, gzip: true);
W.Serializer.LoadChunkSnapshot(chunk);
// GID Store
byte[] gid = W.Serializer.CreateGIDStoreSnapshot(gzip: true);
// События — gzip определяется автоматически при загрузке
byte[] events = W.Serializer.CreateEventsSnapshot(gzip: true);
W.Serializer.LoadEventsSnapshot(events);
// Ресурсы — gzip определяется автоматически при загрузке
byte[] resources = W.Serializer.CreateResourcesSnapshot(gzip: true);
W.Serializer.LoadResourcesSnapshot(resources);
// Системы — gzip определяется автоматически при загрузке
byte[] systems = W.Serializer.CreateSystemsSnapshot(gzip: true);
W.Serializer.LoadSystemsSnapshot(systems);
// Файлы — для мира/кластера/чанка/событий/ресурсов/систем gzip определяется автоматически при загрузке
W.Serializer.CreateWorldSnapshot("world.bin", gzip: true);
W.Serializer.LoadWorldSnapshot("world.bin");