Leopotam.EcsLite 1.0.1

dotnet add package Leopotam.EcsLite --version 1.0.1                
NuGet\Install-Package Leopotam.EcsLite -Version 1.0.1                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Leopotam.EcsLite" Version="1.0.1" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Leopotam.EcsLite --version 1.0.1                
#r "nuget: Leopotam.EcsLite, 1.0.1"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Leopotam.EcsLite as a Cake Addin
#addin nuget:?package=Leopotam.EcsLite&version=1.0.1

// Install Leopotam.EcsLite as a Cake Tool
#tool nuget:?package=Leopotam.EcsLite&version=1.0.1                

LeoEcsLite - Легковесный C# Entity Component System фреймворк

Производительность, нулевые или минимальные аллокации, минимизация использования памяти, отсутствие зависимостей от любого игрового движка - это основные цели данного фреймворка.

ВАЖНО! Не забывайте использовать DEBUG-версии билдов для разработки и RELEASE-версии билдов для релизов: все внутренние проверки/исключения будут работать только в DEBUG-версиях и удалены для увеличения производительности в RELEASE-версиях.

ВАЖНО! LeoEcsLite-фрейморк не потокобезопасен и никогда не будет таким! Если вам нужна многопоточность - вы должны реализовать ее самостоятельно и интегрировать синхронизацию в виде ecs-системы.

Содержание

Социальные ресурсы

discord

Установка

В виде unity модуля

Поддерживается установка в виде unity-модуля через git-ссылку в PackageManager или прямое редактирование Packages/manifest.json:

"com.leopotam.ecslite": "https://github.com/Leopotam/ecslite.git",

По умолчанию используется последняя релизная версия. Если требуется версия "в разработке" с актуальными изменениями - следует переключиться на ветку develop:

"com.leopotam.ecslite": "https://github.com/Leopotam/ecslite.git#develop",

В виде исходников

Код так же может быть склонирован или получен в виде архива со страницы релизов.

Основные типы

Сущность

Сама по себе ничего не значит и не существует, является исключительно контейнером для компонентов. Реализована как int:

// Создаем новую сущность в мире.
int entity = _world.NewEntity ();

// Любая сущность может быть удалена, при этом сначала все компоненты будут автоматически удалены и только потом энтити будет считаться уничтоженной. 
world.DelEntity (entity);

// Компоненты с любой сущности могут быть скопированы на другую. Если исходная или целевая сущность не существует - будет брошено исключение в DEBUG-версии.
world.CopyEntity (srcEntity, dstEntity);

ВАЖНО! Сущности не могут существовать без компонентов и будут автоматически уничтожаться при удалении последнего компонента на них.

Компонент

Является контейнером для данных пользователя и не должен содержать логику (допускаются минимальные хелперы, но не куски основной логики):

struct Component1 {
    public int Id;
    public string Name;
}

Компоненты могут быть добавлены, запрошены или удалены через компонентные пулы.

Система

Является контейнером для основной логики для обработки отфильтрованных сущностей. Существует в виде пользовательского класса, реализующего как минимум один из IEcsInitSystem, IEcsDestroySystem, IEcsRunSystem (и прочих поддерживаемых) интерфейсов:

class UserSystem : IEcsPreInitSystem, IEcsInitSystem, IEcsRunSystem, IEcsDestroySystem, IEcsPostDestroySystem {
    public void PreInit (IEcsSystems systems) {
        // Будет вызван один раз в момент работы IEcsSystems.Init() и до срабатывания IEcsInitSystem.Init().
    }
    
    public void Init (IEcsSystems systems) {
        // Будет вызван один раз в момент работы IEcsSystems.Init() и после срабатывания IEcsPreInitSystem.PreInit().
    }
    
    public void Run (IEcsSystems systems) {
        // Будет вызван один раз в момент работы IEcsSystems.Run().
    }

    public void Destroy (IEcsSystems systems) {
        // Будет вызван один раз в момент работы IEcsSystems.Destroy() и до срабатывания IEcsPostDestroySystem.PostDestroy().
    }
    
    public void PostDestroy (IEcsSystems systems) {
        // Будет вызван один раз в момент работы IEcsSystems.Destroy() и после срабатывания IEcsDestroySystem.Destroy().
    }
}

Совместное использование данных

Экземпляр любого кастомного типа (класса) может быть одновременно подключен ко всем системам:

class SharedData {
    public string PrefabsPath;
}
...
SharedData sharedData = new SharedData { PrefabsPath = "Items/{0}" };
IEcsSystems systems = new EcsSystems (world, sharedData);
systems
    .Add (new TestSystem1 ())
    .Init ();
...
class TestSystem1 : IEcsInitSystem {
    public void Init(IEcsSystems systems) {
        SharedData shared = systems.GetShared<SharedData> (); 
        string prefabPath = string.Format (shared.PrefabsPath, 123);
        // prefabPath = "Items/123" к этому моменту.
    } 
}

Специальные типы

EcsPool

Является контейнером для компонентов, предоставляет апи для добавления / запроса / удаления компонентов на сущности:

int entity = world.NewEntity ();
EcsPool<Component1> pool = world.GetPool<Component1> (); 

// Add() добавляет компонент к сущности. Если компонент уже существует - будет брошено исключение в DEBUG-версии.
ref Component1 c1 = ref pool.Add (entity);

// Has() проверяет наличие компонента на сущности.
bool c1Exists = pool.Has (entity);

// Get() возвращает существующий на сущности компонент. Если компонент не существует - будет брошено исключение в DEBUG-версии.
ref Component1 c1 = ref pool.Get (entity);

// Del() удаляет компонент с сущности. Если компонента не было - никаких ошибок не будет. Если это был последний компонент - сущность будет удалена автоматически.
pool.Del (entity);

// Copy() выполняет копирование всех компонентов с одной сущности на другую. Если исходная или целевая сущность не существует - будет брошено исключение в DEBUG-версии.
pool.Get (srcEntity, dstEntity);

ВАЖНО! После удаления, компонент будет помещен в пул для последующего переиспользования. Все поля компонента будут сброшены в значения по умолчанию автоматически.

EcsFilter

Является контейнером для хранения отфильтрованных сущностей по наличию или отсутствию определенных компонентов:

class WeaponSystem : IEcsInitSystem, IEcsRunSystem {
    public void Init (IEcsSystems systems) {
        // Получаем экземпляр мира по умолчанию.
        EcsWorld world = systems.GetWorld ();
        
        // Создаем новую сущность.
        int entity = world.NewEntity ();
        
        // И добавляем к ней компонент "Weapon".
        var weapons = world.GetPool<Weapon>();
        weapons.Add (entity);
    }

    public void Run (IEcsSystems systems) {
        EcsWorld world = systems.GetWorld ();
        // Мы хотим получить все сущности с компонентом "Weapon" и без компонента "Health".
        // Фильтр может собираться динамически каждый раз, а может быть закеширован где-то.
        var filter = world.Filter<Weapon> ().Exc<Health> ().End ();
        
        // Фильтр хранит только сущности, сами даные лежат в пуле компонентов "Weapon".
        // Пул так же может быть закеширован где-то.
        var weapons = world.GetPool<Weapon>();
        
        foreach (int entity in filter) {
            ref Weapon weapon = ref weapons.Get (entity);
            weapon.Ammo = System.Math.Max (0, weapon.Ammo - 1);
        }
    }
}

Дополнительные требования к отфильтровываемым сущностям могут быть добавлены через методы Inc<>() / Exc<>().

ВАЖНО! Фильтры поддерживают любое количество требований к компонентам, но один и тот же компонент не может быть в списках "include" и "exclude".

EcsWorld

Является контейнером для всех сущностей, компонентых пулов и фильтров, данные каждого экземпляра уникальны и изолированы от других миров.

ВАЖНО! Необходимо вызывать EcsWorld.Destroy() у экземпляра мира если он больше не нужен.

EcsSystems

Является контейнером для систем, которыми будет обрабатываться EcsWorld-экземпляр мира:

class Startup : MonoBehaviour {
    EcsWorld _world;
    IEcsSystems _systems;

    void Start () {
        // Создаем окружение, подключаем системы.
        _world = new EcsWorld ();
        _systems = new EcsSystems (_world);
        _systems
            .Add (new WeaponSystem ())
            .Init ();
    }
    
    void Update () {
        // Выполняем все подключенные системы.
        _systems?.Run ();
    }

    void OnDestroy () {
        // Уничтожаем подключенные системы.
        if (_systems != null) {
            _systems.Destroy ();
            _systems = null;
        }
        // Очищаем окружение.
        if (_world != null) {
            _world.Destroy ();
            _world = null;
        }
    }
}

ВАЖНО! Необходимо вызывать IEcsSystems.Destroy() у экземпляра группы систем если он больше не нужен.

Интеграция с движками

Unity

Проверено на Unity 2020.3 (не зависит от нее) и содержит asmdef-описания для компиляции в виде отдельных сборок и уменьшения времени рекомпиляции основного проекта.

Интеграция в Unity editor содержит шаблоны кода, а так же предоставляет мониторинг состояния мира.

Кастомный движок

Для использования фреймворка требуется C#7.3 или выше.

Каждая часть примера ниже должна быть корректно интегрирована в правильное место выполнения кода движком:

using Leopotam.EcsLite;

class EcsStartup {
    EcsWorld _world;
    IEcsSystems _systems;

    // Инициализация окружения.
    void Init () {        
        _world = new EcsWorld ();
        _systems = new EcsSystems (_world);
        _systems
            // Дополнительные экземпляры миров
            // должны быть зарегистрированы здесь.
            // .AddWorld (customWorldInstance, "events")
            
            // Системы с основной логикой должны
            // быть зарегистрированы здесь.
            // .Add (new TestSystem1 ())
            // .Add (new TestSystem2 ())
            
            .Init ();
    }

    // Метод должен быть вызван из
    // основного update-цикла движка.
    void UpdateLoop () {
        _systems?.Run ();
    }

    // Очистка окружения.
    void Destroy () {
        if (_systems != null) {
            _systems.Destroy ();
            _systems = null;
        }
        if (_world != null) {
            _world.Destroy ();
            _world = null;
        }
    }
}

Статьи

Проекты, использующие LeoECS Lite

С исходниками

Расширения

Лицензия

Фреймворк выпускается под двумя лицензиями, подробности тут.

В случаях лицензирования по условиям MIT-Red не стоит расчитывать на персональные консультации или какие-либо гарантии.

ЧаВо

В чем отличие от старой версии LeoECS?

Я предпочитаю называть их лайт (ecs-lite) и классика (leoecs). Основные отличия лайта следующие:

  • Кодовая база фреймворка уменьшилась в 2 раза, ее стало проще поддерживать и расширять.
  • Лайт не является порезанной версией классики, весь функционал сохранен в виде ядра и внешних модулей.
  • Отсутствие каких-либо статичных данных в ядре.
  • Отсутствие кешей компонентов в фильтрах, это уменьшает потребление памяти и увеличивает скорость перекладывания сущностей по фильтрам.
  • Быстрый доступ к любому компоненту на любой сущности (а не только отфильтрованной и через кеш фильтра).
  • Нет ограничений на количество требований/ограничений к компонентам для фильтров.
  • Общая линейная производительность близка к классике, но доступ к компонентам, перекладывание сущностей по фильтрам стал несоизмеримо быстрее.
  • Прицел на использование мультимиров - нескольких экземпляров миров одновременно с разделением по ним данных для оптимизации потребления памяти.
  • Отсутствие рефлексии в ядре, возможно использование агрессивного вырезания неиспользуемого кода компилятором (code stripping, dead code elimination).
  • Совместное использование общих данных между системами происходит без рефлексии (если она допускается, то рекомендуется использовать расширение ecslite-di из списка расширений).
  • Реализация сущностей вернулась к обычныму типу int, это сократило потребление памяти. Если сущности нужно сохранять где-то - их по-прежнему нужно упаковывать в специальную структуру.
  • Маленькое ядро, весь дополнительный функционал реализуется через подключение опциональных расширений.
  • Весь новый функционал будет выходить только к лайт-версии, классика переведена в режим поддержки на исправление ошибок.

Я хочу одну систему вызвать в MonoBehaviour.Update(), а другую - в MonoBehaviour.FixedUpdate(). Как я могу это сделать?

Для разделения систем на основе разных методов из MonoBehaviour необходимо создать под каждый метод отдельную IEcsSystems-группу:

IEcsSystems _update;
IEcsSystems _fixedUpdate;

void Start () {
    EcsWorld world = new EcsWorld ();
    _update = new EcsSystems (world);
    _update
        .Add (new UpdateSystem ())
        .Init ();
    _fixedUpdate = new EcsSystems (world);
    _fixedUpdate
        .Add (new FixedUpdateSystem ())
        .Init ();
}

void Update () {
    _update?.Run ();
}

void FixedUpdate () {
    _fixedUpdate?.Run ();
}

Меня не устраивают значения по умолчанию для полей компонентов. Как я могу это настроить?

Компоненты поддерживают установку произвольных значений через реализацию интерфейса IEcsAutoReset<>:

struct MyComponent : IEcsAutoReset<MyComponent> {
    public int Id;
    public object LinkToAnotherComponent;

    public void AutoReset (ref MyComponent c) {
        c.Id = 2;
        c.LinkToAnotherComponent = null;
    }
}

Этот метод будет автоматически вызываться для всех новых компонентов, а так же для всех только что удаленных, до помещения их в пул.

ВАЖНО! В случае применения IEcsAutoReset все дополнительные очистки/проверки полей компонента отключаются, что может привести к утечкам памяти. Ответственность лежит на пользователе!

Меня не устраивают значения для полей компонентов при их копировании через EcsWorld.CopyEntity() или Pool<>.Copy(). Как я могу это настроить?

Компоненты поддерживают установку произвольных значений при вызове EcsWorld.CopyEntity() или EcsPool<>.Copy() через реализацию интерфейса IEcsAutoCopy<>:

struct MyComponent : IEcsAutoCopy<MyComponent> {
    public int Id;

    public void AutoCopy (ref MyComponent src, ref MyComponent dst) {
        dst.Id = src.Id * 123;
    }
}

ВАЖНО! В случае применения IEcsAutoCopy никакого копирования по умолчанию не происходит. Ответственность за корректность заполнения данных и за целостность исходных лежит на пользователе!

Я хочу сохранить ссылку на сущность в компоненте. Как я могу это сделать?

Для сохранения ссылки на сущность ее необходимо упаковать в один из специальных контейнеров (EcsPackedEntity или EcsPackedEntityWithWorld):

EcsWorld world = new EcsWorld ();
int entity = world.NewEntity ();
EcsPackedEntity packed = world.PackEntity (entity);
EcsPackedEntityWithWorld packedWithWorld = world.PackEntityWithWorld (entity);
...
// В момент распаковки мы проверяем - жива эта сущность или уже нет.
if (packed.Unpack (world, out int unpacked)) {
    // "unpacked" является валидной сущностью и мы можем ее использовать.
}

// В момент распаковки мы проверяем - жива эта сущность или уже нет.
if (packedWithWorld.Unpack (out EcsWorld unpackedWorld, out int unpackedWithWorld)) {
    // "unpackedWithWorld" является валидной сущностью и мы можем ее использовать.
}

Я хочу добавить реактивности и обрабатывать события изменений в мире самостоятельно. Как я могу сделать это?

ВАЖНО! Так делать не рекомендуется из-за падения производительности.

Для активации этого функционала следует добавить LEOECSLITE_WORLD_EVENTS в список директив комплятора, а затем - добавить слушатель событий:

class TestWorldEventListener : IEcsWorldEventListener {
    public void OnEntityCreated (int entity) {
        // Сущность создана - метод будет вызван в момент вызова world.NewEntity().
    }

    public void OnEntityChanged (int entity) {
        // Сущность изменена - метод будет вызван в момент вызова pool.Add() / pool.Del().
    }

    public void OnEntityDestroyed (int entity) {
        // Сущность уничтожена - метод будет вызван в момент вызова world.DelEntity() или в момент удаления последнего компонента.
    }

    public void OnFilterCreated (EcsFilter filter) {
        // Фильтр создан - метод будет вызван в момент вызова world.Filter().End(), если фильтр не существовал ранее.
    }

    public void OnWorldResized (int newSize) {
        // Мир изменил размеры - метод будет вызван в случае изменения размеров кешей под сущности в момент вызова world.NewEntity().
    }

    public void OnWorldDestroyed (EcsWorld world) {
        // Мир уничтожен - метод будет вызван в момент вызова world.Destroy().
    }
}
...
var world = new EcsWorld ();
var listener = new TestWorldEventListener ();
world.AddEventListener (listener);

Я хочу добавить реактивщины и обрабатывать события изменения фильтров. Как я могу это сделать?

ВАЖНО! Так делать не рекомендуется из-за падения производительности.

Для активации этого функционала следует добавить LEOECSLITE_FILTER_EVENTS в список директив комплятора, а затем - добавить слушатель событий:

class TestFilterEventListener : IEcsFilterEventListener {
    public void OnEntityAdded (int entity) {
        // Сущность добавлена в фильтр.
    }

    public void OnEntityRemoved (int entity) {
        // Сущность удалена из фильтра.
    }
}
...
var world = new EcsWorld ();
var filter = world.Filter<C1> ().End ();
var listener = new TestFilterEventListener ();
filter.AddEventListener (listener);
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Leopotam.EcsLite:

Package Downloads
Undine.LeopotamEcsLite

Undine bindings for LeopotamEcsLite framework

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on Leopotam.EcsLite:

Repository Stars
Doraku/Ecs.CSharp.Benchmark
Benchmarks of some C# ECS frameworks.
Version Downloads Last updated
1.0.1 705 1/29/2024
1.0.0 608 9/30/2022