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