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 клиентов

This site uses Just the Docs, a documentation theme for Jekyll.