WorldType

Тип-тег-идентификатор мира, служит для изоляции статических данных при создании разных миров в одном процессе

  • Представлен в виде пользовательской структуры без данных с маркер интерфейсом 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>.Entity.New<Position>();

Второй способ - чуть более удобный, использовать статические импорты или статические алиасы (придется писать в каждом файле)

using static FFS.Libraries.StaticEcs.World<WT>;

public struct WT : IWorldType { }

Create(WorldConfig.Default());
CalculateEntitiesCount();

var entity = Entity.New<Position>();

Трейтий способ - самый удобный, использовать типы-алиасы в корневом неймспейсе (не требуется писать в каждом файле)

Везде в примерах будет использован именно этот способ

public struct WT : IWorldType { }

public abstract class W : World<WT> { }

W.Create(WorldConfig.Default());
W.CalculateEntitiesCount();

var entity = W.Entity.New<Position>();

Основные операции:

// Определяем ID мира
public struct WT : IWorldType { }

// Регестрируем типы - алиасы
public abstract class World : World<WT> { }

// Создание мира с дефолтной конфигурацие
W.Create(WorldConfig.Default());
// Или кастомной
W.Create(new() {
            // Указывает независимый мир или зависимый (Подробнее в разделе "Чанк")
            Independent = true   
            // Базовый размер всех разновидностей типов компонентов (количество типов компонент)
            BaseComponentTypesCount = 64                        
            // Базовый размер всех разновидностей типов тегов (количество типов тегов)
            BaseTagTypesCount = 64,                             
            // Режим работы многопоточной обработки 
            // (Disabled - потоки не создаются, MaxThreadsCount - создается максимально доступное количество потоков, CustomThreadsCount - указанное количество потоков)
            ParallelQueryType = ParallelQueryType.Disabled,
            // Количество потоков при ParallelQueryType.CustomThreadsCount
            CustomThreadCount = 4,
            // Строгий режим работы Query по умолчанию, дополнительно в разделе "Запросы"
            DefaultQueryModeStrict = true
        });

W.Entity.    // Доступ к сущности для MainWorldType (ID мира)
W.Context.   // Доступ к контексту для MainWorldType (ID мира)
W.Components.// Доступ к компонентам для MainWorldType (ID мира)
W.Tags.      // Доступ к тегам для MainWorldType (ID мира)
W.Events.    // Доступ к событиям

// Инициализация мира
W.Initialize(baseEntitiesCapacity = 4096);
// Инициализация мира с загрузкой сохраненных ранее идентификаторов
W.InitializeFromGIDStoreSnapshot(snapshot);
// Инициализация мира из сохраненных данных
W.InitializeFromWorldSnapshot(snapshot);

// Уничтожение и очистка данных мира
W.Destroy();

// true если мир инициализирован
bool initialized = W.IsInitialized();

// true если мир независимый
bool independent = W.IsIndependent();

// количество созданных сущностей в мире (активных + незагруженных)
int entitiesCount = W.CalculateEntitiesCount();

// количество загруженных сущностей в мире
int loadedEntitiesCount = W.CalculateLoadedEntitiesCount();

// текущая емкость для сущностей
int entitiesCapacity = W.CalculateEntitiesCapacity();

// Уничтожает всех сущностей в мире
W.DestroyAllEntities();

Кластер:

Кластер - это множество чанков сущностей, сущности принадлежащие одному кластеру сгруппированы и располагаются в памяти сегментировано Кластер представлен значением ushort 0-65535, по умолчанию при инициализации мира создается один кластер с идентификатором 0 и все сущности по умолчанию создаются в нем.


Основные операции:

// Регистрация кластера, может быть вызван после создания или после инициализации мира
const ushort NPC_CLUSTER = 1;
const ushort ENVIRONMENT_CLUSTER = 2;
W.RegisterCluster(NPC_CLUSTER);
W.RegisterCluster(ENVIRONMENT_CLUSTER);

// Проверить зарегистрирован ли кластер
bool clusterIsRegistered = W.ClusterIsRegistered(NPC_CLUSTER);

// Включить или отключить кластер, сущности из отключенных кластеров не попадают в итерацию
W.SetActiveCluster(ENVIRONMENT_CLUSTER, false);

// Проверить включен ли кластер
bool active = W.ClusterIsActive(ENVIRONMENT_CLUSTER);

// Освободить кластер, все сущности в кластере будут удалены, все чанки и идентификатор кластера освобождены (Будет ошибка если кластер не зарегистрирован)
W.FreeCluster(ENVIRONMENT_CLUSTER);

// Освободить кластер если он зарегистрирован
bool free = W.TryFreeCluster(ENVIRONMENT_CLUSTER);

// Уничтожить все сущности в кластере
W.DestroyAllEntitiesInCluster(NPC_CLUSTER);

// Сделать снимок кластера, который хранит все данные сущностей в этом кластере
// Существуют перегрузки метода, для записи на диск, сжатию и тд
// Больше примеров в разделе "сериализация"
byte[] clusterSnapshot = W.Serializer.CreateClusterSnapshot(NPC_CLUSTER);

// Выгрузить кластер из памяти, все чанки компонентов и тегов будут удалены,
// сущности будут помечены как незагруженные и сохранится только информации об идентификаторах, сущности не будут получены в запросах
W.UnloadCluster(NPC_CLUSTER);

// Загрузить из снимка кластера сущности в мир
W.Serializer.LoadClusterSnapshot(clusterSnapshot);

// Получить все чанки в кластере (включая пустые чанки где нет загруженных сущностей)
ReadOnlySpan<uint> chunks = W.GetClusterChunks(NPC_CLUSTER);

// Получить все чанки в кластере в которых как минимум одна сущность загружена
ReadOnlySpan<uint> loadedChunks = W.GetClusterLoadedChunks(NPC_CLUSTER);

// При создании сущности можно передать идентификатор кластера (по умолчанию сущность создается в дефолтном кластере W.DEFAULT_CLUSTER = 0)
var npc = W.Entity.New(clusterId: W.DEFAULT_CLUSTER);

// Попытаться создать сущность в кластере, если мир зависим и в нем не осталось свободных идентификаторов сущностей то вернутся false
var created = W.Entity.TryNew(out var ent, clusterId: ENVIRONMENT_CLUSTER);

// Для всех перегрузок добавлен опциональный параметр идентификатора кластера
W.Entity.New(
    new Position(),
    new Name(),
    clusterId: NPC_CLUSTER
);

// Получить кластер сущности
ushort entityClusterId = npc.ClusterId();

// Получить кластер сущности у EntityGID
ushort gidClusterId = npc.Gid().ClusterId;

Чанк:

Чанк - это группировка сущностей размером 4096, весь мир состоит из чанков. Чанк всегда принадлежит какому-либо кластеру.
Мир может быть зависимым или независимым, параметр устанавливается в конфигурации мира при создании W.Create(new() { Independent = true })
Независимый мир по умолчанию управляет идентификаторами сущностей и всеми чанками автоматически, создает новые чанки при создании сущностей W.Entity.New() когда требуется.
Зависимый мир при создании не имеет идентификаторов сущностей и чанков доступных для создания сущностей через W.Entity.New(), миру необходимо указать какие чанки доступны. Далее мы рассмотрим примеры.


Основные операции:

// Найти свободный чанк, не принадлежащий никакому кластеру
// Для независимого мира в случае отсутствия свободного чанка будет создан новый
// Для зависимого мира в случае отсутствия свободного чанка будет ошибка
EntitiesChunkInfo chunkInfo = W.FindNextSelfFreeChunk();
uint chunkIdx = chunkInfo.ChunkIdx; // индекс чанка
// chunkInfo.EntitiesFrom - первый идентификатор сущности в кластере
// chunkInfo.EntitiesCapacity - размер чанка (всегда 4096)

// Попробовать найти свободный чанк, не принадлежащий никакому кластеру
// Для независимого мира в случае отсутствия свободного чанка будет создан новый (результат всегда true)
// Для зависимого мира в случае отсутствия свободного чанка результат будет false
bool hasFreeChunk = W.TryFindNextSelfFreeChunk(out EntitiesChunkInfo info);

// Зарегистрировать свободный чанк в кластере (Если чанк уже зарегистрирован будет ошибка)
W.RegisterChunk(chunkIdx, clusterId: NPC_CLUSTER);
// Зарегистрировать свободный чанк в кластере и присвоить тип владения (подробности ниже) (Если чанк уже зарегистрирован будет ошибка)
W.RegisterChunk(chunkIdx, owner: ChunkOwnerType.Self, clusterId: NPC_CLUSTER);

// Попытаться зарегистрировать свободный чанк в кластере (Если чанк уже зарегистрирован вернется false)
bool chunkRegistered = W.TryRegisterChunk(chunkIdx, NPC_CLUSTER);

// Проверить зарегистрирован ли чанк
bool registered = W.ChunkIsRegistered(chunkIdx);

// Получить идентификатор кластера которому принадлежит чанк
ushort chunkClusterId = W.GetChunkClusterId(chunkIdx);

// Изменить кластер чанка, все сущности внутри чанка будут принадлежать другому кластеру
W.ChangeChunkCluster(chunkIdx, ENVIRONMENT_CLUSTER);

// Проверить есть ли сущности в чанке (активные + незагруженные)
bool hasEntitiesInChunk = W.HasEntitiesInChunk(chunkIdx);

// Проверить есть ли загруженные сущности в чанке
bool hasLoadedEntitiesInChunk = W.HasLoadedEntitiesInChunk(chunkIdx);

// Освободить чанк, все сущности в чанке будут удалены, дентификатор чанка будет освобожден
W.FreeChunk(chunkIdx);

// Уничтожить все сущности в чанке
W.DestroyAllEntitiesInChunk(chunkIdx);

// Сделать снимок чанка, который хранит все данные сущностей в этом чанке
// Существуют перегрузки метода, для записи на диск, сжатию и тд
// Больше примеров в разделе "сериализация"
byte[] chunkSnapshot = W.Serializer.CreateChunkSnapshot(chunkIdx);

// Выгрузить чанк из памяти, все компонентов и теги будут удалены,
// сущности будут помечены как незагруженные и сохранится только информации об идентификаторах, сущности не будут получены в запросах
W.UnloadChunk(chunkIdx);

// Загрузить из снимка чанка сущности в мир
W.Serializer.LoadChunkSnapshot(chunkSnapshot);

// При создании сущности можно передать индекс чанка (без указания, выбор чанка определяется миром)
var entity = W.Entity.New(chunkIdx: chunkIdx);

// Попытаться создать сущность в чанке, если чанк полон вернется false
var created = W.Entity.TryNew(out var ent, chunkIdx: chunkIdx);


// Проверить владельца чанка
// ChunkOwnerType.Self - значит что чанк управляется данным миром, только чанки с Self владением используются для создания сущностей через Entity.New()
//     - независимый мир по умолчанию имеет все чанки с Self владением
// ChunkOwnerType.Other - значит что чанк не управляется данным миром, сущности созданные через Entity.New() никогда не будут созданы в этих чанках
//     - зависимы мир по умолчанию имеет все чанки с Other владением
ChunkOwnerType owner = W.GetChunkOwner(chunkIdx);

// Изменить тип владения чанка
// Если владение меняется с Other на Self то чанк становится доступен для создания сущностей через Entity.New()
// Если владение меняется с Self на Other то чанк становится недоступен для создания сущностей через Entity.New()
W.ChangeChunkOwner(chunkIdx, ChunkOwnerType.Other);
 
// Создание сущностей через Entity.New(gid) доступно для чанков только с типом владения Other
// Создание сущностей через Entity.New(chunkIdx) доступно для чанков только с типом владения Self

Примеры применения кластеров и чанков:

Кластеры могут использоваться для любой пользовательской логики, например:

  • Разные кластеры могут определять разные типы сущностей, например кластер юнитов, кластер игрового окружения, кластер предметов, кластер эффектов
    • Это позволяет уменьшить потребление и фрагментацию памяти, ускорить итерацию, и помогает в сериализации мира и игровой логике
    • Например при большой игровой карте, которая подгружается и выгружается по мере движения игрока, разные кластеры сильно экономят память
  • Другой пример это использование кластеров для разных игровых уровней, можно загружать\выгружать кластеры при смене уровня
  • Также идентификатор кластера может определять игровую сессию, в сочетании с параллельной итерацией возможно в рамках одного мира создать эмуляцию мультимиров

Управление чанками может использоваться например для:

  • Стриминга мира, можно загружать и выгружать чанки в процессе игры
  • Пользовательского управления идентификаторами сущностей
  • Быстрого выделения и очистки большого количества сущностей, как арена-память для временных сущностей

Управление владением чанков может использоваться для клиент-серверных взаимодействий, например:

// На стороне сервера в Independent мире
// Находим свободный чанк и регистрируем
EntitiesChunkInfo chunkInfo = W.FindNextSelfFreeChunk();
// Устанавливаем тип владения чанка на Other, таким образом сервер никогда не будет создвать сущности в этом диапазоне идентификаторов
W.RegisterChunk(chunkInfo.ChunkIdx, ChunkOwnerType.Other);

// Отправляем идентификатор чанка на клиент

// На стороне клиента в Dependent мире
// Получаем идентификатор чанка от сервера
W.RegisterChunk(ChunkIdxFromServer, ChunkOwnerType.Self);

// теперь на клиенте доступно 4096 свободных идентификаторов для сущностей
// и можно создавать клиентские сущности через W.Entity.New()
// например для UI или VFX

// Аналогично можно использовать для p2p сетевых форматов
// где есть один Independent хост и N Dependent клиентов