WorldType
Тип-тег-идентификатор мира, служит для изоляции статических данных при создании разных миров в одном процессе
- Представлен в виде пользовательской структуры без данных с маркер-интерфейсом
IWorldType - Каждый уникальный
IWorldTypeполучает полностью изолированное статическое хранилище
Пример:
public struct MainWorldType : IWorldType { }
public struct MiniGameWorldType : IWorldType { }
World
Точка входа в библиотеку, отвечающая за доступ, создание, инициализацию, работу и уничтожение данных мира
- Представлен в виде статического класса
World<T>параметризованногоIWorldType
Так как тип-идентификатор
IWorldTypeопределяет доступ к конкретному миру, есть три способа работы с библиотекой:
Первый способ — полное обращение:
public struct WT : IWorldType { }
World<WT>.Create(WorldConfig.Default());
World<WT>.CalculateEntitiesCount();
var entity = World<WT>.NewEntity<Default>();
Второй способ — статические импорты:
using static FFS.Libraries.StaticEcs.World<WT>;
public struct WT : IWorldType { }
Create(WorldConfig.Default());
CalculateEntitiesCount();
var entity = NewEntity<Default>();
Третий способ — тип-алиас в корневом неймспейсе:
Везде в примерах будет использован именно этот способ
public struct WT : IWorldType { }
public abstract class W : World<WT> { }
W.Create(WorldConfig.Default());
W.CalculateEntitiesCount();
var entity = W.NewEntity<Default>();
Жизненный цикл
Create() → Регистрация типов → Initialize() → Работа → Destroy()
WorldStatus:
NotCreated— мир не создан или уничтоженCreated— структуры выделены, доступна регистрация типовInitialized— мир полностью готов к работе, доступны операции с сущностями
Создание мира:
// Определяем идентификатор мира
public struct WT : IWorldType { }
public abstract class W : World<WT> { }
// Создание мира с конфигурацией по умолчанию
W.Create(WorldConfig.Default());
// Или с пользовательской конфигурацией (все параметры опциональны — незаданные значения берутся из умолчаний)
W.Create(new WorldConfig {
// Независимый мир (управляет чанками самостоятельно) или зависимый (требует ручного управления чанками)
Independent = true,
// Начальная ёмкость для типов компонентов (по умолчанию — 64)
BaseComponentTypesCount = 64,
// Начальная ёмкость для кластеров (минимум 16, по умолчанию — 16)
BaseClustersCapacity = 16,
// Количество потоков для параллельных запросов (по умолчанию — 0, однопоточный)
// 0 — потоки не создаются
// WorldConfig.MaxThreadCount — все доступные потоки CPU
// N — указанное количество потоков
ThreadCount = 4,
// Количество итераций ожидания потока перед блокировкой (по умолчанию — 256)
WorkerSpinCount = 256,
// Включить отслеживание создания сущностей для фильтра Created (по умолчанию — false)
TrackCreated = true,
});
WorldConfig предоставляет фабричные методы:
WorldConfig.Default()— стандартные настройки (однопоточный, независимый)WorldConfig.MaxThreads()— все доступные потоки CPU Все параметры опциональны — незаданные значения берутся изWorldConfig.Default().
Регистрация типов:
W.Create(WorldConfig.Default());
// Регистрация компонентов, тегов и событий — только между Create() и Initialize()
W.Types()
.EntityType<Bullet>()
.Component<Position>()
.Component<Velocity>()
.Tag<IsPlayer>()
.Event<OnDamage>();
// Инициализация мира
W.Initialize();
Регистрация типов (.Component<T>(), .Tag<T>(), .EntityType<T>()) доступна только в состоянии Created — после Create() и до Initialize(). Регистрация событий (.Event<T>()) доступна также после инициализации.
Авторегистрация типов:
Вместо ручной регистрации каждого типа можно использовать автоматическое сканирование сборок. RegisterAll() находит все структуры, реализующие ECS-интерфейсы, в одной или нескольких сборках и регистрирует каждую через соответствующий Register*-API.
W.Create(WorldConfig.Default());
// Беспараметрная форма — сканируется сборка, в которой объявлена IWorldType-структура `WT`
// (берётся как typeof(WT).Assembly). Никакого stack walking.
W.Types().RegisterAll();
// Явная форма — сканируются только переданные сборки. Первая сборка обязательна,
// поэтому пустой вызов синтаксически невозможен.
W.Types().RegisterAll(typeof(MyGame).Assembly, typeof(MyPlugin).Assembly);
// Можно комбинировать с ручной регистрацией (fluent-цепочка)
W.Types()
.RegisterAll()
.Component<SpecialComponent>();
W.Initialize();
Как определяется сканируемая сборка
| Перегрузка | Сканируемые сборки |
|---|---|
RegisterAll() | typeof(TWorld).Assembly — сборка, в которой объявлена ваша IWorldType-структура (в примерах — WT, не класс-алиас W : World<WT>, а сама структура) |
RegisterAll(Assembly first, params Assembly[] rest) | Ровно те сборки, которые вы передали — сборка TWorld не добавляется неявно |
Беспараметрная форма намеренно использует typeof(TWorld).Assembly и никогда не вызывает Assembly.GetCallingAssembly(). Благодаря этому она корректно работает на всех рантаймах, включая:
- .NET Framework / .NET Core / .NET 5+
- Mono и Unity Mono
- Unity IL2CPP
- Unity WebGL
- NativeAOT
На IL2CPP/WebGL/NativeAOT Assembly.GetCallingAssembly() возвращает ненадёжный результат, потому что stack walking там урезан или отсутствует — поэтому реализация берёт сборку через generic-параметр. Пока ваша IWorldType-структура (WT) живёт в той же сборке, что и ваши ECS-типы, беспараметрной формы достаточно.
Мульти-сборочный сценарий
Если IWorldType-структура и ECS-типы лежат в разных сборках (например, WT объявлен в общей «core»-сборке, а компоненты — в игровой), используйте явную перегрузку и перечислите все сборки с ECS-типами:
W.Types().RegisterAll(
typeof(WT).Assembly, // core-сборка с IWorldType-структурой
typeof(Position).Assembly, // геймплейная сборка с компонентами
typeof(AiPlugin).Assembly // ещё одна сборка-плагин
);
Обнаруживаемые интерфейсы
| Интерфейс | Регистрация |
|---|---|
IComponent | Types().Component<T>() |
ITag | Types().Tag<T>() |
IEvent | Types().Event<T>() |
ILinkType | Оборачивается в Link<T> и регистрируется как компонент |
ILinksType | Оборачивается в Links<T> и регистрируется как компонент |
IMultiComponent | Оборачивается в Multi<T> и регистрируется как компонент |
IEntityType | Types().EntityType<T>() |
- Сборка самого фреймворка StaticEcs всегда исключается из сканирования.
- Абстрактные типы и открытые generic-определения пропускаются.
- Структура, реализующая несколько интерфейсов (например, и
IComponent, иIMultiComponent), регистрируется для каждого применимого интерфейса. - Тип
Defaultпропускается при регистрации типов сущностей — он уже зарегистрирован миром. RegisterAll()ищет статическое поле или свойство соответствующего типа конфига внутри каждой структуры и использует его, если найдено. Иначе используется конфигурация по умолчанию. Правила поиска:IComponent— ищетComponentTypeConfig<T>(предпочитает имяConfig)IEvent— ищетEventTypeConfig<T>(предпочитает имяConfig)ITag— ищетTagTypeConfig<T>(предпочитает имяConfig)IEntityType— ищетbyte(предпочитает имяId)
- Поддерживаются и поля (field), и свойства (property).
- Должен вызываться в фазе
Created— послеW.Create()и доW.Initialize().
Инициализация:
// Стандартная инициализация (baseEntitiesCapacity — начальная ёмкость для сущностей)
W.Initialize(baseEntitiesCapacity: 4096);
// После инициализации можно загрузить ранее сохранённый снимок:
// — только идентификаторы сущностей (версии EntityGID)
W.Serializer.RestoreFromGIDStoreSnapshot(snapshot);
// — или полное состояние мира (сущности и все их данные)
W.Serializer.LoadWorldSnapshot(snapshot);
RestoreFromGIDStoreSnapshot восстанавливает только метаданные идентификаторов сущностей (версии GID). LoadWorldSnapshot восстанавливает полное состояние мира, включая все сущности и их данные. Оба метода требуют, чтобы мир уже был инициализирован.
Уничтожение:
// Уничтожить мир и освободить все ресурсы
W.Destroy();
Основные операции
// Текущий статус мира
WorldStatus status = W.Status;
// true если мир инициализирован
bool initialized = W.IsWorldInitialized;
// true если мир независимый
bool independent = W.IsIndependent;
// Количество сущностей в мире (активные + незагруженные)
uint entitiesCount = W.CalculateEntitiesCount();
// Количество загруженных сущностей
uint loadedCount = W.CalculateLoadedEntitiesCount();
// Текущая ёмкость для сущностей
uint capacity = W.CalculateEntitiesCapacity();
// Уничтожить все сущности в мире (мир остаётся инициализированным)
W.DestroyAllLoadedEntities();
Подробнее о создании сущностей и операциях с ними — см. Сущность.
Подробнее о ресурсах мира — см. Ресурсы.
Кластер
Кластер — это группа чанков сущностей для пространственной сегментации мира. Сущности одного кластера сгруппированы и располагаются в памяти сегментировано.
- Представлен значением
ushort(0–65535) - По умолчанию при инициализации мира создаётся кластер с идентификатором 0
- Все сущности по умолчанию создаются в кластере 0
- Кластер можно отключить — сущности из отключённых кластеров не попадают в итерацию
Кластеры предназначены для пространственной группировки: уровни, зоны карты, игровые комнаты. Для логической группировки (юниты, снаряды, эффекты) используйте entityType.
Основные операции:
// Регистрация кластеров (можно вызывать после Create() или после Initialize())
const ushort LEVEL_1_CLUSTER = 1;
const ushort LEVEL_2_CLUSTER = 2;
W.RegisterCluster(LEVEL_1_CLUSTER);
W.RegisterCluster(LEVEL_2_CLUSTER);
// Проверить зарегистрирован ли кластер
bool registered = W.ClusterIsRegistered(LEVEL_1_CLUSTER);
// Включить или отключить кластер — сущности из отключённых кластеров не попадают в итерацию
W.SetActiveCluster(LEVEL_2_CLUSTER, false);
// Проверить включён ли кластер
bool active = W.ClusterIsActive(LEVEL_2_CLUSTER);
// Уничтожить все сущности в кластере
W.DestroyAllEntitiesInCluster(LEVEL_1_CLUSTER);
// Освободить кластер — все сущности удаляются, чанки и идентификатор освобождаются
W.FreeCluster(LEVEL_2_CLUSTER);
// Безопасное освобождение — вернёт false если кластер не зарегистрирован
bool freed = W.TryFreeCluster(LEVEL_2_CLUSTER);
Снимки и выгрузка кластеров:
// Создать снимок кластера (хранит все данные сущностей)
// Существуют перегрузки для записи на диск, сжатия и т.д.
byte[] snapshot = W.Serializer.CreateClusterSnapshot(LEVEL_1_CLUSTER);
// Выгрузить кластер из памяти
// Данные компонентов и тегов удаляются, сущности помечаются как незагруженные
// Сохраняется только информация об идентификаторах, сущности не попадают в запросы
ReadOnlySpan<ushort> clusters = stackalloc ushort[] { LEVEL_1_CLUSTER };
W.Query().BatchUnload(EntityStatusType.Any, clusters: clusters);
// Загрузить кластер из снимка
W.Serializer.LoadClusterSnapshot(snapshot);
Чанки кластера:
// Получить все чанки в кластере (включая пустые)
ReadOnlySpan<uint> chunks = W.GetClusterChunks(LEVEL_1_CLUSTER);
// Получить чанки, в которых есть хотя бы одна загруженная сущность
ReadOnlySpan<uint> loadedChunks = W.GetClusterLoadedChunks(LEVEL_1_CLUSTER);
Создание сущностей в кластере:
// При создании сущности можно указать кластер (по умолчанию — кластер 0)
struct UnitType : IEntityType { }
var entity = W.NewEntity<UnitType>(clusterId: LEVEL_1_CLUSTER);
// Для всех перегрузок доступен параметр clusterId
W.NewEntity<UnitType>(
new UnitType(), // экземпляр типа сущности (может содержать данные для OnCreate)
clusterId: LEVEL_1_CLUSTER
);
// Получить кластер сущности
ushort entityClusterId = entity.ClusterId;
// Получить кластер из EntityGID
ushort gidClusterId = entity.GID.ClusterId;
Чанк
Чанк — это блок на 4096 сущностей. Весь мир состоит из чанков. Каждый чанк принадлежит какому-либо кластеру.
- Независимый мир (
Independent = true) — управляет чанками автоматически, создаёт новые при необходимости - Зависимый мир (
Independent = false) — не имеет чанков для создания сущностей черезNewEntity(), необходимо явно указать какие чанки доступны
Основные операции:
// Найти свободный чанк, не принадлежащий никакому кластеру
// Независимый мир: если нет свободного — создаст новый
// Зависимый мир: если нет свободного — ошибка
EntitiesChunkInfo chunkInfo = W.FindNextSelfFreeChunk();
uint chunkIdx = chunkInfo.ChunkIdx;
// chunkInfo.EntitiesFrom — первый идентификатор сущности в чанке
// chunkInfo.EntitiesCapacity — размер чанка (всегда 4096)
// Безопасный вариант (вернёт false если нет свободных чанков)
bool found = W.TryFindNextSelfFreeChunk(out EntitiesChunkInfo info);
// Зарегистрировать чанк в кластере
W.RegisterChunk(chunkIdx, clusterId: LEVEL_1_CLUSTER);
// Зарегистрировать чанк с указанием типа владения
W.RegisterChunk(chunkIdx, owner: ChunkOwnerType.Self, clusterId: LEVEL_1_CLUSTER);
// Безопасная регистрация (вернёт false если чанк уже зарегистрирован)
bool registered = W.TryRegisterChunk(chunkIdx, clusterId: LEVEL_1_CLUSTER);
// Проверить зарегистрирован ли чанк
bool isRegistered = W.ChunkIsRegistered(chunkIdx);
// Получить кластер чанка
ushort clusterId = W.GetChunkClusterId(chunkIdx);
// Переместить чанк в другой кластер
W.ChangeChunkCluster(chunkIdx, LEVEL_2_CLUSTER);
// Проверить наличие сущностей в чанке
bool hasEntities = W.HasEntitiesInChunk(chunkIdx); // активные + незагруженные
bool hasLoaded = W.HasLoadedEntitiesInChunk(chunkIdx); // только загруженные
// Уничтожить все сущности в чанке
W.DestroyAllEntitiesInChunk(chunkIdx);
// Освободить чанк — все сущности удаляются, идентификатор освобождается
W.FreeChunk(chunkIdx);
Снимки и выгрузка чанков:
// Создать снимок чанка
byte[] snapshot = W.Serializer.CreateChunkSnapshot(chunkIdx);
// Выгрузить чанк из памяти (данные удаляются, сущности помечаются как незагруженные)
ReadOnlySpan<uint> chunks = stackalloc uint[] { chunkIdx };
W.Query().BatchUnload(EntityStatusType.Any, chunks);
// Загрузить чанк из снимка
W.Serializer.LoadChunkSnapshot(snapshot);
Создание сущностей в конкретном чанке:
// Создать сущность в указанном чанке
struct UnitType : IEntityType { }
var entity = W.NewEntityInChunk<UnitType>(chunkIdx: chunkIdx);
// Безопасный вариант (вернёт false если чанк заполнен)
bool created = W.TryNewEntityInChunk<UnitType>(out var entity, chunkIdx: chunkIdx);
// Не-дженерик вариант (тип сущности известен в runtime как byte)
byte entityTypeId = EntityTypeInfo<UnitType>.Id;
var entity = W.NewEntityInChunk(entityTypeId, chunkIdx: chunkIdx);
Владение чанками (ChunkOwnerType)
Тип владения определяет, как мир использует чанк для создания сущностей:
ChunkOwnerType.Self— чанк управляется данным миром. Сущности, создаваемые черезNewEntity(), размещаются в этих чанках- Независимый мир по умолчанию имеет все чанки с
Selfвладением
- Независимый мир по умолчанию имеет все чанки с
ChunkOwnerType.Other— чанк не управляется данным миром.NewEntity()никогда не будет размещать сущности в этих чанках- Зависимый мир по умолчанию имеет все чанки с
Otherвладением
- Зависимый мир по умолчанию имеет все чанки с
// Получить тип владения чанка
ChunkOwnerType owner = W.GetChunkOwner(chunkIdx);
// Изменить тип владения
// Self → Other: чанк становится недоступен для NewEntity()
// Other → Self: чанк становится доступен для NewEntity()
W.ChangeChunkOwner(chunkIdx, ChunkOwnerType.Other);
Создание сущностей через NewEntityByGID<TEntityType>(gid) доступно только для чанков с владением Other. Создание сущностей через NewEntityInChunk<TEntityType>(chunkIdx) доступно только для чанков с владением Self.
Клиент-серверный пример:
// === Серверная сторона (Independent мир) ===
// Находим свободный чанк и регистрируем с владением Other
// Сервер не будет создавать свои сущности в этом диапазоне идентификаторов
EntitiesChunkInfo chunkInfo = WServer.FindNextSelfFreeChunk();
WServer.RegisterChunk(chunkInfo.ChunkIdx, ChunkOwnerType.Other);
// Отправляем идентификатор чанка клиенту
// === Клиентская сторона (Dependent мир) ===
// Получаем идентификатор чанка от сервера
// Регистрируем с владением Self — теперь доступно 4096 слотов для сущностей
WClient.RegisterChunk(chunkIdxFromServer, ChunkOwnerType.Self);
// Клиент может создавать сущности через NewEntity()
// Например, для UI или VFX
var vfx = WClient.NewEntity<VfxType>();
// Аналогично работает для P2P:
// один Independent хост + N Dependent клиентов
Примеры применения кластеров и чанков
Кластеры:
- Уровни и зоны карты — разные кластеры для разных частей игрового мира. При движении игрока можно загружать и выгружать кластеры, экономя память
- Игровые уровни — загрузка/выгрузка кластеров при смене уровня
- Игровые сессии — идентификатор кластера определяет сессию. В сочетании с параллельной итерацией возможна эмуляция мультимиров в рамках одного мира
Чанки:
- Стриминг мира — загрузка и выгрузка чанков в процессе игры
- Пользовательское управление идентификаторами — контроль над распределением EntityGID
- Арена-память — быстрое выделение и очистка большого количества временных сущностей
Владение чанками:
- Клиент-серверное взаимодействие — сервер выделяет диапазоны идентификаторов клиентам
- P2P сетевые форматы — один Independent хост и N Dependent клиентов