Этот фреймворк сделан под вдохновением от LeoECS Lite, с упором на те же принципы:
- независимость от игровых движков,
- возможность использовать фреймворк в любых программах на C#,
- минимизация выделения памяти,
- быстродействие.
Подробнее о расширениях можно прочитать тут.
Для использования фреймворка в проекте достаточно скачать архив со страницы релизов и добавить исходники в проект.
Представляет собой обычный int
. Выступает в качестве идентификатора для доступа к данным.
Представляет собой контейнер с данными. Наличие логики внутри компоненты допустимо, однако не рекомендуется.
public struct SampleComponent
{
public int SomeData1;
public bool SomeData2;
}
Компоненты обязательно должны быть структурами, но не классами.
Представляет собой класс, не содержащий никаких данных (кроме, возможно, вспомогательных для функционирования) и занимающийся исключительно обработкой отфильтрованных сущностей.
public class SampleSystem : IInitSystem, IRunSystem, IPostRunSystem, IDisposeSystem
{
public void Init(IEcsWorld world)
{
// Do something...
}
public void Run(IEcsWorld world)
{
// Do something...
}
public void PostRun(IEcsWorld world)
{
// Do something...
}
public void Dispose(IEcsWorld world)
{
// Do something...
}
}
Для работы с фреймворком необходимо создать корневой объект типа IEcsWorld
:
var world = new EcsWorld();
Он представляет собой контейнер для сущностей, пулов и фильтров.
Пулы компонент можно добавлять вручную, с помощью функции BindPool
:
world
.BindPool<SomePool1>()
.BindPool<SomePool2>();
Также пул будет автоматически создаваться, если еще нет, при запросе через GetPool
:
var pool = world.GetPool<SomePool3>();
Пулы, которые объявлены в фильтрах, но не созданы заранее, будут добавлены в мир при сборке фильтра.
Создание сущности производится с помощью двух методов:
var entityId = world.NewEntityId();
// OR
var entity = world.NewEntity();
Оба метода равнозначны, однако имеют разный возвращаемый тип. Метод NewEntityId()
возвращает числовой идентификатор сущности,
по которому сущность можно добавлять в пулы, получать по нему значение и удалять из пулов. Однако для этого сначала нужно получить пул из мира:
var pool = world.GetPool<SampleComponent>();
var entityId = world.NewEntityId();
ref var component = ref pool.Add(entityId);
Метод NewEntity()
, в свою очередь, возвращает более удобочитаемую обертку над сущностью, благодаря которой код можно немного упростить:
var entity = world.NewEntity();
ref var component = ref entity.AddComponent<SampleComponent>();
При этом из обертки можно получить идентификатор с помощью entity.GetId()
, а по идентфикатору можно получить обертку с помощью world.GetEntityById(entityId)
.
Представляет собой контейнер для сущностей, отфильтрованных по наличию или отсутствию определенных компонент:
public class SampleFiltersSystem : IInitSystem
{
private EcsFilter _filter = null!;
public void Init(IEcsWorld world)
{
_filter = new EcsFilter(world);
_filter.Inc<SampleComponent>().Exc<PlayerComponent>().Register();
}
}
Для того, чтобы фильтр начал обрабатываться ECS миром, его нужно в нем зарегистрировать с помощью метода
Register()
.
Фильтр достаточно собрать один раз и закешировать, пересборка для обновления списка сущностей не нужна. Фильтры поддерживают любое количество требований к компонентам, но один и тот же компонент не может быть в списках "include" и "exclude".
Фильтр реализует интерфейсы IEnumerable
и IEnumerator
. Благодаря этому по сущностям в фильтре можно пройтись с помощью цикла foreach
:
foreach (int entityId in _filter)
{
ref var addComponent = ref _pool.Add(entityId);
ref var getComponent = ref _pool.Get(entityId);
var hasComponent = _pool.Has(entityId);
_pool.Remove(entityId);
}
ВАЖНО! Метод
Current
возвращает объект типаobject
, поэтому в цикле обязательно приведение к типуint
.
Также в фильтре можно изменить тип возвращаемого объекта:
_filter.EnumerateAsEntity();
foreach (Entity entity in _filter)
{
ref var addComponent = ref entity.AddComponent<SampleComponent>();
ref var getComponent = ref entity.GetOrAddComponent<SampleComponent>();
var hasComponent = entity.HasComponent<SampleComponent>();
entity.RemoveComponent<SampleComponent>();
}
Тогда можно будет работать сразу с оберткой для сущности.
Чтобы вернуться к типу int
:
_filter.EnumerateAsEntityId();
Системы конвейерно обрабатывают данные ECS мира. Это значит, что они активируются все и именно в той последовательности, в которой они были добавлены в EcsWorld
.
Для вызова систем используется один из методов мира:
world.Init();
world.Run();
world.Dispose();
Системы, реализующие интерфейс
IPostRunSystem
будут вызваны в соответствующем порядке, после отработки всехIRunSystem
. Все вызовы происходят за один вызовworld.Run()
.
Фреймворк не является потокобезопасным.