Производительность, нулевые или минимальные аллокации, минимизация использования памяти, отсутствие зависимостей от любого игрового движка - это основные цели данного фреймворка.
ВАЖНО! РАЗРАБОТКА ДАННОГО ПРОЕКТА ПРЕКРАЩЕНА, ВМЕСТО НЕГО СЛЕДУЕТ ИСПОЛЬЗОВАТЬ EcsProto, либо EcsLite.
ВАЖНО! Не забывайте использовать
DEBUG-версии билдов для разработки иRELEASE-версии билдов для релизов: все внутренние проверки/исключения будут работать только вDEBUG-версиях и удалены для увеличения производительности вRELEASE-версиях.
ВАЖНО! LeoEcs-фрейморк не потокобезопасен и никогда не будет таким! Если вам нужна многопоточность - вы должны реализовать ее самостоятельно и интегрировать синхронизацию в виде ecs-системы.
- Социальные ресурсы
- Установка
- Основные типы
- Инъекция данных
- Специальные типы
- Интеграция с движками
- Статьи
- Проекты, использующие LeoECS
- Расширения
- Лицензия
- ЧаВо
Поддерживается установка в виде unity-модуля через git-ссылку в PackageManager или прямое редактирование Packages/manifest.json:
"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git", По умолчанию используется последняя релизная версия. Если требуется версия "в разработке" с актуальными изменениями - следует переключиться на ветку develop:
"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git#develop", Код так же может быть склонирован или получен в виде архива со страницы релизов.
Является контейнером для данных пользователя и не должен содержать логику (допускаются минимальные хелперы, но не куски основной логики):
struct WeaponComponent { public int Ammo; public string GunName; }Сама по себе ничего не значит и не существует, является исключительно контейнером для компонентов. Реализована как EcsEntity:
// NewEntity() используется для создания новых сущностей в контексте мира. EcsEntity entity = _world.NewEntity (); // Get() возвращает существующий на сущности компонент. Если компонент не существовал - он будет добавлен автоматически. // Следует обратить внимание на "ref" - компоненты должны обрабатываться по ссылке. ref Component1 c1 = ref entity.Get<Component1> (); ref Component2 c2 = ref entity.Get<Component2> (); // Del() удаляет компонент с сущности. Если это был последний компонент - сущность будет удалена автоматически. Если компонент не существовал - ошибки не будет. entity.Del<Component2> (); // Replace() выполняет замену компонента новым экземпляром. Если старый компонент не существовал - новый будет добавлен без ошибки. WeaponComponent weapon = new WeaponComponent () { Ammo = 10, GunName = "Handgun" }; entity.Replace (weapon); // Replace() позволяет выполнять "чейнинг" создания компонентов: EcsEntity entity2 = world.NewEntity (); entity2.Replace (new Component1 { Id = 10 }).Replace (new Component2 { Name = "Username" }); // Любая сущность может быть скопирована вместе с компонентами: EcsEntity entity2Copy = entity2.Copy (); // Любая сущность может "передать" свои компоненты другой сущности (сама будет уничтожена): var newEntity = world.NewEntity (); entity2Copy.MoveTo (newEntity); // все компоненты с "entity2Copy" переместятся на "newEntity", а "entity2Copy" будет удалена. // Любая сущность может быть удалена, при этом сначала все компоненты будут автоматически удалены и только потом энтити будет считаться уничтоженной. entity.Destroy ();ВАЖНО! Сущности не могут существовать без компонентов и будут автоматически уничтожаться при удалении последнего компонента на них.
Является контейнером для основной логики для обработки отфильтрованных сущностей. Существует в виде пользовательского класса, реализующего как минимум один из IEcsInitSystem, IEcsDestroySystem, IEcsRunSystem (и прочих поддерживаемых) интерфейсов:
class UserSystem : IEcsPreInitSystem, IEcsInitSystem, IEcsRunSystem, IEcsDestroySystem, IEcsPostDestroySystem { public void PreInit () { // Будет вызван один раз в момент работы EcsSystems.Init() и до срабатывания IEcsInitSystem.Init(). } public void Init () { // Будет вызван один раз в момент работы EcsSystems.Init() и после срабатывания IEcsPreInitSystem.PreInit(). } public void Run () { // Будет вызван один раз в момент работы EcsSystems.Run(). } public void Destroy () { // Будет вызван один раз в момент работы EcsSystems.Destroy() и до срабатывания IEcsPostDestroySystem.PostDestroy(). } public void PostDestroy () { // Будет вызван один раз в момент работы EcsSystems.Destroy() и после срабатывания IEcsDestroySystem.Destroy(). } }Все поля ecs-систем, совместимые c EcsWorld и EcsFilter<T> будут автоматически инициализированы валидными экземплярами соответствующих типов:
class HealthSystem : IEcsSystem { // Поля с авто-инъекцией. EcsWorld _world; EcsFilter<WeaponComponent> _weaponFilter; }Экземпляр любого кастомного типа (класса) может быть инъецирован с помощью метода EcsSystems.Inject():
class SharedData { public string PrefabsPath; } ... SharedData sharedData = new SharedData { PrefabsPath = "Items/{0}" }; EcsSystems systems = new EcsSystems (world); systems .Add (new TestSystem1 ()) .Inject (sharedData) .Init ();Каждая система будет просканирована на наличие полей, совместимых по типу с последующей инъекцией:
class TestSystem1 : IEcsInitSystem { // Поле с авто-инъекцией. SharedData _sharedData; public void Init() { var prefabPath = string.Format (_sharedData.Prefabspath, 123); // prefabPath = "Items/123" к этому моменту. } }ВАЖНО! Для инъекции подходят только нестатичные public/private-поля конечного класса системы, либо public/protected-поля базовых классов. Все остальные поля будут проигнорированы!
Является контейнером для хранения отфильтрованных сущностей по наличию или отсутствию определенных компонентов:
class WeaponSystem : IEcsInitSystem, IEcsRunSystem { // Поля с авто-инъекцией. EcsWorld _world; // Мы хотим получить все сущности с компонентом "WeaponComponent" // и без компонента "HealthComponent". EcsFilter<WeaponComponent>.Exclude<HealthComponent> _filter; public void Init () { _world.NewEntity ().Get<WeaponComponent> (); } public void Run () { foreach (int i in _filter) { // Сущность, которая точно содержит компонент "WeaponComponent". ref EcsEntity entity = ref _filter.GetEntity (i); // Get1() позволяет получить доступ по ссылке на компонент, // указанный первым в списке ограничений фильтра ("WeaponComponent"). ref WeaponComponent weapon = ref _filter.Get1 (i); weapon.Ammo = System.Math.Max (0, weapon.Ammo - 1); } } }Любые компоненты из Include-списка ограничений фильтра могут быть получены через вызовы EcsFilter.Get1(), EcsFilter.Get2() и т.д - нумерация идет в том же порядке, что и в списке ограничений.
Если в компоненте нет данных и он используется исключительно как флаг-признак для фильтрации, то компонент может реализовать интерфейс IEcsIgnoreInFilter - это поможет уменьшить потребление памяти фильтром и немного увеличить производительность:
struct Component1 { } struct Component2 : IEcsIgnoreInFilter { } class TestSystem : IEcsRunSystem { EcsFilter<Component1, Component2> _filter; public void Run () { foreach (var i in _filter) { // Мы можем получить компонент "Component1". ref var component1 = ref _filter.Get1 (i); // Мы не можем получить "Component2" - кеш внутри фильтра не существует и будет выкинуто исключение. ref var component2 = ref _filter.Get2 (i); } } }ВАЖНО!: Фильтры поддерживают до 6
Include-ограничений и 2Exclude-ограничений. Чем меньше ограничений в фильтре - тем он быстрее работает.
ВАЖНО! Нельзя использовать несколько фильтров с одинаковым списком ограничений, но выставленных в разном порядке - в
DEBUG-версии будет выкинуто исключение с описанием конфликтующих фильтров.
ВАЖНО! Один и тот же компонент не может быть в списках "Include" и "Exclude" одного фильтра одновременно.
Является контейнером для всех сущностей и фильтров, данные каждого экземпляра уникальны и изолированы от других миров.
ВАЖНО! Необходимо вызывать
EcsWorld.Destroy()у экземпляра мира если он больше не нужен.
Является контейнером для систем, которыми будет обрабатываться EcsWorld-экземпляр мира:
class Startup : MonoBehaviour { EcsWorld _world; EcsSystems _systems; void Start () { // Создаем окружение, подключаем системы. _world = new EcsWorld (); _systems = new EcsSystems (_world) .Add (new WeaponSystem ()); _systems.Init (); } void Update () { // Выполняем все подключенные системы. _systems.Run (); } void OnDestroy () { // Уничтожаем подключенные системы. _systems.Destroy (); // Очищаем окружение. _world.Destroy (); } }Экземпляр EcsSystems может быть использован как обычная ecs-система (вложена в другую EcsSystems):
// Инициализация. EcsSystems nestedSystems = new EcsSystems (_world).Add (new NestedSystem ()); // Нельзя вызывать nestedSystems.Init() здесь, // "rootSystems" выполнит этот вызов автоматически. EcsSystems rootSystems = new EcsSystems (_world).Add (nestedSystems); rootSystems.Init (); // В цикле обновления нельзя вызывать nestedSystems.Run(), // "rootSystems" выполнит этот вызов автоматически. rootSystems.Run (); // Очистка. // Нельзя вызывать nestedSystems.Destroy() здесь, // "rootSystems" выполнит этот вызов автоматически. rootSystems.Destroy ();Любая IEcsRunSystem система (включая вложенные EcsSystems) может быть включен или выключен из списка обработки:
class TestSystem : IEcsRunSystem { public void Run () { } } EcsSystems systems = new EcsSystems (_world); systems.Add (new TestSystem (), "my special system"); systems.Init (); var idx = systems.GetNamedRunSystem ("my special system"); // "state" будет иметь значение "true", все системы включены по умолчанию. var state = systems.GetRunSystemState (idx); // Выключаем систему по ее индексу. systems.SetRunSystemState (idx, false);Проверено на Unity 2020.3 (не зависит от нее) и содержит asmdef-описания для компиляции в виде отдельных сборок и уменьшения времени рекомпиляции основного проекта.
Интеграция в Unity editor содержит шаблоны кода, а так же предоставляет мониторинг состояния мира.
Для использования фреймворка требуется C#7.3 или выше.
Каждая часть примера ниже должна быть корректно интегрирована в правильное место выполнения кода движком:
using Leopotam.Ecs; class EcsStartup { EcsWorld _world; EcsSystems _systems; // Инициализация окружения. void Init () { _world = new EcsWorld (); _systems = new EcsSystems (_world); _systems // Системы с основной логикой должны // быть зарегистрированы здесь, порядок важен: // .Add (new TestSystem1 ()) // .Add (new TestSystem2 ()) // OneFrame-компоненты должны быть зарегистрированы // в общем списке систем, порядок важен: // .OneFrame<TestComponent1> () // .OneFrame<TestComponent2> () // Инъекция должна быть произведена здесь, // порядок не важен: // .Inject (new CameraService ()) // .Inject (new NavMeshSupport ()) .Init (); } // Метод должен быть вызван из // основного update-цикла движка. void UpdateLoop () { _systems?.Run (); } // Очистка. void Destroy () { if (_systems != null) { _systems.Destroy (); _systems = null; _world.Destroy (); _world = null; } } }- Интеграция в редактор Unity
- Поддержка Unity uGui
- Поддержка многопоточности
- Unity Physx events support
Фреймворк выпускается под двумя лицензиями, подробности тут.
В случаях лицензирования по условиям MIT-Red не стоит расчитывать на персональные консультации или какие-либо гарантии.
Я хочу знать - существовал ли компонент на сущности до вызова Get() для разной инициализации полученных данных. Как я могу это сделать?
Если не важно - существовал компонент ранее и просто нужна уверенность, что он теперь существует достаточно вызова EcsEntity.Get<T>().
Если нужно понимание, что компонент существовал ранее - это можно проверить с помощью вызова EcsEntity.Has<T>().
Я хочу одну систему вызвать в MonoBehaviour.Update(), а другую - в MonoBehaviour.FixedUpdate(). Как я могу это сделать?
Для разделения систем на основе разных методов из MonoBehaviour необходимо создать под каждый метод отдельную EcsSystems-группу:
EcsSystems _update; EcsSystems _fixedUpdate; void Start () { var world = new EcsWorld (); _update = new EcsSystems (world).Add (new UpdateSystem ()); _update.Init (); _fixedUpdate = new EcsSystems (world).Add (new FixedUpdateSystem ()); _fixedUpdate.Init (); } void Update () { _update.Run (); } void FixedUpdate () { _fixedUpdate.Run (); }Мне нравится как работает автоматическая инъекция данных, но хотелось бы часть полей исключить. Как я могу сделать это?
Для этого достаточно пометить поле системы атрибутом [EcsIgnoreInject]:
// Это поле будет обработано инъекцией. EcsFilter<C1> _filter1; // Это поле будет проигнорировано инъекцией. [EcsIgnoreInject] EcsFilter<C2> _filter2;Компоненты поддерживают кастомную настройку значений через реализацию интерфейса IEcsAutoReset<>:
struct MyComponent : IEcsAutoReset<MyComponent> { public int Id; public object LinkToAnotherComponent; public void AutoReset (ref MyComponent c) { c.Id = 2; c.LinkToAnotherComponent = null; } }Этот метод будет автоматически вызываться для всех новых компонентов, а так же для всех только что удаленных, до помещения их в пул.
ВАЖНО! В случае применения
IEcsAutoResetвсе дополнительные очистки/проверки полей компонента отключаются, что может привести к утечкам памяти. Ответственность лежит на пользователе!
ВАЖНО!: Компоненты, реализующие
IEcsAutoResetне совместимы с вызовамиentity.Replace(). Рекомендуется не использоватьentity.Replace()или любые другие способы полной перезаписи компонентов.
Я использую компоненты как "события", которые живут 1 цикл, а потом удаляются в конце отдельной системой. Получается много лишнего кода, есть ли более простой способ?
Для автоматической очистки компонентов, которые должны жить один цикл, место их очистки может быть зарегистрировано в общем списке систем внутри EcsSystems:
struct MyOneFrameComponent { } EcsSystems _update; void Start () { var world = new EcsWorld (); _update = new EcsSystems (world); _update .Add (new CalculateSystem ()) // Все "MyOneFrameComponent" компоненты будут // удалены здесь. .OneFrame<MyOneFrameComponent> () // Здесь можно быть уверенным, что ни один // "MyOneFrameComponent" не существует. .Add (new UpdateSystem ()) .Init (); } void Update () { _update.Run (); }Мне нужен больший контроль над размерами кешей, которые мир использует в момент создания. Как я могу сделать это?
Мир может быть создан с явным указанием EcsWorldConfig-конфигурации:
var config = new EcsWorldConfig () { // Размер по умолчанию для World.Entities. WorldEntitiesCacheSize = 1024, // Размер по умолчанию для World.Filters. WorldFiltersCacheSize = 128, // Размер по умолчанию для World.ComponentPools. WorldComponentPoolsCacheSize = 512, // Размер по умолчанию для Entity.Components (до удвоения). EntityComponentsCacheSize = 8, // Размер по умолчанию для Filter.Entities. FilterEntitiesCacheSize = 256, }; var world = new EcsWorld(config);Мне нужно больше чем 6-"Include" и 2-"Exclude" ограничений для компонентов в фильтре. Как я могу сделать это?
ВАЖНО! Это приведет к модификации исходного кода фреймворка и несовместимости с обновлениями.
Необходимо воспользоваться кодогенерацией EcsFilter классов и заменить содержимое файла EcsFilter.cs.
ВАЖНО! Так делать не рекомендуется из-за падения производительности.
Для активации этого функционала следует добавить LEOECS_FILTER_EVENTS в список директив комплятора, а затем - добавить слушатель событий:
class CustomListener: IEcsFilterListener { public void OnEntityAdded (in EcsEntity entity) { // Сущность добавлена в фильтр. } public void OnEntityRemoved (in EcsEntity entity) { // Сущность удалена из фильтра. } } class MySystem : IEcsInitSystem, IEcsDestroySystem { readonly EcsFilter<Component1> _filter = null; readonly CustomListener _listener = new CustomListener (); public void Init () { // Подписка слушателя на события фильтра. _filter.AddListener (_listener); } public void Destroy () { // Отписка слушателя от событий фильтра. _filter.RemoveListener (_listener); } }










