# LeoECS Proto LeoECS Proto — это легковесный Entity Component System (ECS) фреймворк для C#, разработанный с акцентом на производительность и минимальное использование памяти. Фреймворк обеспечивает нулевые или минимальные аллокации во время выполнения, не имеет зависимостей от игровых движков и может использоваться как в Unity (2021.2+), так и в кастомных движках. Требуется поддержка C#9. Основные компоненты архитектуры включают: сущности (ProtoEntity) — идентификаторы для наборов компонентов, компоненты — структуры данных без логики, системы — классы с логикой обработки сущностей, пулы (ProtoPool) — контейнеры для компонентов, аспекты (IProtoAspect) — группировки пулов, миры (ProtoWorld) — изолированные контейнеры сущностей, и итераторы (ProtoIt) — фильтры сущностей по компонентам. ## ProtoWorld — Создание и управление миром Мир является контейнером для всех сущностей. Каждый экземпляр мира уникален и изолирован от других миров. Мир требует хотя бы один аспект при создании. ```csharp using Leopotam.EcsProto; // Определение аспекта с пулами компонентов class GameAspect : IProtoAspect { public ProtoPool PositionPool; public ProtoPool VelocityPool; public ProtoPool HealthPool; public void Init (ProtoWorld world) { world.AddAspect (this); PositionPool = new (); world.AddPool (PositionPool); VelocityPool = new (); world.AddPool (VelocityPool); HealthPool = new (); world.AddPool (HealthPool); } public void PostInit () { } public ProtoWorld World () => null; } // Компоненты struct Position { public float X, Y, Z; } struct Velocity { public float Dx, Dy, Dz; } struct Health { public int Current, Max; } // Создание мира с конфигурацией var config = new ProtoWorld.Config { Entities = 1024, // Начальная ёмкость сущностей RecycledEntities = 256, // Ёмкость переработанных сущностей Pools = 64 // Количество пулов }; ProtoWorld world = new (new GameAspect (), config); // Работа с миром // ... // Очистка всех сущностей без уничтожения мира world.Clear (); // Уничтожение мира (обязательно вызывать когда мир больше не нужен) world.Destroy (); ``` ## ProtoPool — Работа с компонентами Пул является контейнером для компонентов и предоставляет API для добавления, запроса и удаления компонентов на сущностях. ```csharp using Leopotam.EcsProto; struct PlayerData { public string Name; public int Score; } struct Enemy { } // Компонент-маркер для фильтрации class GameAspect : IProtoAspect { public ProtoPool PlayerPool; public ProtoPool EnemyPool; public void Init (ProtoWorld world) { world.AddAspect (this); PlayerPool = new (); world.AddPool (PlayerPool); EnemyPool = new (); world.AddPool (EnemyPool); } public void PostInit () { } public ProtoWorld World () => null; } // Создание мира и получение аспекта ProtoWorld world = new (new GameAspect ()); GameAspect aspect = (GameAspect) world.Aspect (typeof (GameAspect)); // NewEntity() — создание сущности с компонентом ref PlayerData player = ref aspect.PlayerPool.NewEntity (out ProtoEntity playerEntity); player.Name = "Игрок1"; player.Score = 0; // Add() — добавление компонента к существующей сущности ref Enemy enemyMarker = ref aspect.EnemyPool.Add (playerEntity); // Has() — проверка наличия компонента bool hasEnemy = aspect.EnemyPool.Has (playerEntity); // true // Get() — получение компонента (вызывает исключение если отсутствует в DEBUG) ref PlayerData playerRef = ref aspect.PlayerPool.Get (playerEntity); playerRef.Score += 100; // Del() — удаление компонента (сущность удаляется автоматически при удалении последнего компонента) aspect.EnemyPool.Del (playerEntity); // Перебор всех сущностей с компонентом через пул foreach (ProtoEntity entity in aspect.PlayerPool) { ref PlayerData data = ref aspect.PlayerPool.Get (entity); Console.WriteLine ($"Игрок: {data.Name}, Счёт: {data.Score}"); } // Len() — количество компонентов в пуле int playerCount = aspect.PlayerPool.Len (); world.Destroy (); ``` ## ProtoEntity — Управление сущностями Сущность — это идентификатор для набора компонентов. Сущности не могут существовать без компонентов и автоматически уничтожаются при удалении последнего компонента. ```csharp using Leopotam.EcsProto; struct Transform { public float X, Y, Z; } struct Renderable { public string Model; } class WorldAspect : IProtoAspect { public ProtoPool TransformPool; public ProtoPool RenderPool; public void Init (ProtoWorld world) { world.AddAspect (this); TransformPool = new (); world.AddPool (TransformPool); RenderPool = new (); world.AddPool (RenderPool); } public void PostInit () { } public ProtoWorld World () => null; } ProtoWorld world = new (new WorldAspect ()); WorldAspect aspect = (WorldAspect) world.Aspect (typeof (WorldAspect)); // Создание сущности через пул ref Transform transform = ref aspect.TransformPool.NewEntity (out ProtoEntity entity); transform.X = 10f; transform.Y = 0f; transform.Z = 5f; // Добавление второго компонента ref Renderable render = ref aspect.RenderPool.Add (entity); render.Model = "player_model.obj"; // Клонирование сущности со всеми компонентами ProtoEntity clonedEntity = world.CloneEntity (entity); // Копирование компонентов с одной сущности на другую ref Transform newTransform = ref aspect.TransformPool.NewEntity (out ProtoEntity targetEntity); world.CopyEntity (entity, targetEntity); // Скопирует Transform и Renderable // Получение поколения сущности для безопасного сохранения ссылок short generation = world.EntityGen (entity); // Количество компонентов на сущности ushort componentsCount = world.ComponentsCount (entity); // Удаление сущности (все компоненты удаляются автоматически) world.DelEntity (entity); world.Destroy (); ``` ## ProtoIt — Итераторы для фильтрации сущностей Итераторы позволяют фильтровать сущности по наличию или отсутствию компонентов. Они должны создаваться один раз при инициализации. ```csharp using Leopotam.EcsProto; struct Position { public float X, Y; } struct Velocity { public float Dx, Dy; } struct Static { } // Маркер для статичных объектов class PhysicsAspect : IProtoAspect { public ProtoWorld _world; public ProtoPool PositionPool; public ProtoPool VelocityPool; public ProtoPool StaticPool; public ProtoIt MovableIt; public ProtoItExc DynamicIt; public void Init (ProtoWorld world) { _world = world; world.AddAspect (this); PositionPool = new (); world.AddPool (PositionPool); VelocityPool = new (); world.AddPool (VelocityPool); StaticPool = new (); world.AddPool (StaticPool); // Создание итераторов (без инициализации) MovableIt = new (new [] { typeof (Position), typeof (Velocity) }); DynamicIt = new ( new [] { typeof (Position), typeof (Velocity) }, // Include new [] { typeof (Static) } // Exclude ); } public void PostInit () { // Инициализация итераторов в PostInit MovableIt.Init (_world); DynamicIt.Init (_world); } public ProtoWorld World () => _world; } ProtoWorld world = new (new PhysicsAspect ()); PhysicsAspect aspect = (PhysicsAspect) world.Aspect (typeof (PhysicsAspect)); // Создание сущностей для теста ref Position pos1 = ref aspect.PositionPool.NewEntity (out ProtoEntity e1); aspect.VelocityPool.Add (e1); ref Position pos2 = ref aspect.PositionPool.NewEntity (out ProtoEntity e2); aspect.VelocityPool.Add (e2); aspect.StaticPool.Add (e2); // Эта сущность статичная // ProtoIt — итерация по сущностям с Position И Velocity foreach (ProtoEntity entity in aspect.MovableIt) { ref Position pos = ref aspect.PositionPool.Get (entity); ref Velocity vel = ref aspect.VelocityPool.Get (entity); pos.X += vel.Dx; pos.Y += vel.Dy; } // ProtoItExc — итерация с исключением (Position И Velocity, НО НЕ Static) foreach (ProtoEntity entity in aspect.DynamicIt) { // Только динамические объекты ref Position pos = ref aspect.PositionPool.Get (entity); Console.WriteLine ($"Динамический объект: ({pos.X}, {pos.Y})"); } // Вспомогательные методы итераторов int count = aspect.MovableIt.LenSlow (); // Количество сущностей bool isEmpty = aspect.MovableIt.IsEmptySlow (); // Проверка на пустоту var (firstEntity, ok) = aspect.MovableIt.FirstSlow (); // Первая сущность if (ok) { ref Position firstPos = ref aspect.PositionPool.Get (firstEntity); } world.Destroy (); ``` ## IProtoSystems — Группа систем Группа систем определяет порядок выполнения и управляет жизненным циклом систем. Поддерживается указание веса для нелинейного порядка выполнения. ```csharp using Leopotam.EcsProto; using System; using System.Collections.Generic; struct Position { public float X, Y; } struct Velocity { public float Dx, Dy; } class GameAspect : IProtoAspect { public ProtoWorld _world; public ProtoPool PositionPool; public ProtoPool VelocityPool; public ProtoIt MovementIt; public void Init (ProtoWorld world) { _world = world; world.AddAspect (this); PositionPool = new (); world.AddPool (PositionPool); VelocityPool = new (); world.AddPool (VelocityPool); MovementIt = new (new [] { typeof (Position), typeof (Velocity) }); } public void PostInit () => MovementIt.Init (_world); public ProtoWorld World () => _world; } // Система инициализации class InitSystem : IProtoInitSystem { GameAspect _aspect; public void Init (IProtoSystems systems) { ProtoWorld world = systems.World (); _aspect = (GameAspect) world.Aspect (typeof (GameAspect)); // Создание начальных сущностей for (int i = 0; i < 100; i++) { ref Position pos = ref _aspect.PositionPool.NewEntity (out ProtoEntity entity); pos.X = i * 10f; pos.Y = 0; ref Velocity vel = ref _aspect.VelocityPool.Add (entity); vel.Dx = 1f; vel.Dy = 0.5f; } } } // Система движения class MovementSystem : IProtoInitSystem, IProtoRunSystem { GameAspect _aspect; public void Init (IProtoSystems systems) { _aspect = (GameAspect) systems.World ().Aspect (typeof (GameAspect)); } public void Run () { foreach (ProtoEntity entity in _aspect.MovementIt) { ref Position pos = ref _aspect.PositionPool.Get (entity); ref Velocity vel = ref _aspect.VelocityPool.Get (entity); pos.X += vel.Dx; pos.Y += vel.Dy; } } } // Система очистки class CleanupSystem : IProtoDestroySystem { public void Destroy () { Console.WriteLine ("Очистка ресурсов..."); } } // Использование ProtoWorld world = new (new GameAspect ()); IProtoSystems systems = new ProtoSystems (world); systems .AddSystem (new InitSystem (), -1) // Вес -1: выполнится первой .AddSystem (new MovementSystem ()) // Вес 0: по умолчанию .AddSystem (new CleanupSystem (), 100) // Вес 100: очистка в конце .Init (); // Игровой цикл for (int frame = 0; frame < 60; frame++) { systems.Run (); } // Очистка systems.Destroy (); world.Destroy (); ``` ## IProtoModule — Модульная архитектура Модули позволяют разделить код на независимые части с собственными системами, сервисами и аспектами. ```csharp using Leopotam.EcsProto; using System; struct PhysicsBody { public float Mass; } struct AIAgent { public int State; } // Аспект модуля физики class PhysicsAspect : IProtoAspect { public ProtoPool BodyPool; public void Init (ProtoWorld world) { world.AddAspect (this); BodyPool = new (); world.AddPool (BodyPool); } public void PostInit () { } public ProtoWorld World () => null; } // Модуль физики class PhysicsModule : IProtoModule { int _weight; public PhysicsModule (int weight = 0) => _weight = weight; public void Init (IProtoSystems systems) { systems .AddSystem (new PhysicsSystem (), _weight) .AddService (new PhysicsService ()); } public IProtoAspect[] Aspects () => new IProtoAspect[] { new PhysicsAspect () }; public Type[] Dependencies () => null; // Нет зависимостей class PhysicsSystem : IProtoRunSystem { public void Run () { // Физические расчёты } } class PhysicsService { public float Gravity = 9.81f; } } // Модуль ИИ с зависимостью от физики class AIModule : IProtoModule { public void Init (IProtoSystems systems) { systems.AddSystem (new AISystem ()); } public IProtoAspect[] Aspects () => null; public Type[] Dependencies () => new [] { typeof (PhysicsModule) }; class AISystem : IProtoRunSystem { public void Run () { // Логика ИИ } } } // Главный аспект мира class MainAspect : IProtoAspect { public PhysicsAspect Physics; public void Init (ProtoWorld world) { world.AddAspect (this); Physics = new PhysicsAspect (); Physics.Init (world); } public void PostInit () => Physics.PostInit (); public ProtoWorld World () => null; } // Использование ProtoWorld world = new (new MainAspect ()); IProtoSystems systems = new ProtoSystems (world); systems .AddModule (new PhysicsModule (1)) // Сначала физика .AddModule (new AIModule ()) // Потом ИИ (зависит от физики) .Init (); ``` ## Сервисы — Внедрение зависимостей Сервисы позволяют передавать экземпляры классов во все системы для доступа к общим ресурсам. ```csharp using Leopotam.EcsProto; using System; using System.Collections.Generic; // Интерфейс сервиса interface IConfigService { float GetDifficulty (); } // Реализация сервиса class ConfigService : IConfigService { public float Difficulty = 1.0f; public float GetDifficulty () => Difficulty; } // Сервис ресурсов class ResourceService { public string AssetsPath = "Assets/Resources/"; public Dictionary Cache = new (); } // Система использующая сервисы class GameSystem : IProtoInitSystem, IProtoRunSystem { IConfigService _config; ResourceService _resources; public void Init (IProtoSystems systems) { var services = systems.Services (); _config = services[typeof (IConfigService)] as IConfigService; _resources = services[typeof (ResourceService)] as ResourceService; Console.WriteLine ($"Сложность: {_config.GetDifficulty ()}"); Console.WriteLine ($"Путь к ресурсам: {_resources.AssetsPath}"); } public void Run () { // Использование сервисов в игровом цикле } } // Настройка и использование ProtoWorld world = new (new SimpleAspect ()); ConfigService configService = new () { Difficulty = 2.5f }; ResourceService resourceService = new () { AssetsPath = "Data/" }; IProtoSystems systems = new ProtoSystems (world); systems // Регистрация сервиса с переопределением типа (интерфейс) .AddService (configService, typeof (IConfigService)) // Регистрация сервиса по его типу .AddService (resourceService) .AddSystem (new GameSystem ()) .Init (); class SimpleAspect : IProtoAspect { public void Init (ProtoWorld world) => world.AddAspect (this); public void PostInit () { } public ProtoWorld World () => null; } ``` ## IProtoHandlers — Обработчики компонентов Обработчики позволяют настраивать поведение компонентов при сбросе, копировании и сериализации. ```csharp using Leopotam.EcsProto; using System; using System.IO; // Компонент с пользовательскими обработчиками struct NetworkEntity : IProtoHandlers { public int NetworkId; public byte[] Data; public void SetHandlers (IProtoPool pool) { pool.SetResetHandler (OnReset); pool.SetCopyHandler (OnCopy); pool.SetSerializeHandler (OnSerialize); pool.SetDeserializeHandler (OnDeserialize); } static void OnReset (ref NetworkEntity c) { // Вызывается при создании нового компонента и при удалении c.NetworkId = -1; c.Data = null; // Важно очищать ссылки для предотвращения утечек памяти } static void OnCopy (ref NetworkEntity src, ref NetworkEntity dst) { // Вызывается при CopyEntity или pool.Copy dst.NetworkId = src.NetworkId + 1000; // Новый ID для копии dst.Data = src.Data != null ? (byte[]) src.Data.Clone () : null; } static void OnSerialize (ref NetworkEntity c, Stream writer) { Span buf = stackalloc byte[sizeof (int)]; BitConverter.TryWriteBytes (buf, c.NetworkId); writer.Write (buf); // Запись данных... } static void OnDeserialize (ref NetworkEntity c, Stream reader) { Span buf = stackalloc byte[sizeof (int)]; reader.Read (buf); c.NetworkId = BitConverter.ToInt32 (buf); // Чтение данных... } } class NetworkAspect : IProtoAspect { public ProtoPool NetworkPool; public void Init (ProtoWorld world) { world.AddAspect (this); NetworkPool = new (); world.AddPool (NetworkPool); } public void PostInit () { } public ProtoWorld World () => null; } // Использование сериализации ProtoWorld world = new (new NetworkAspect ()); NetworkAspect aspect = (NetworkAspect) world.Aspect (typeof (NetworkAspect)); ref NetworkEntity netEntity = ref aspect.NetworkPool.NewEntity (out ProtoEntity entity); netEntity.NetworkId = 42; netEntity.Data = new byte[] { 1, 2, 3 }; using var stream = new MemoryStream (); // Сериализация bool written = aspect.NetworkPool.Serialize (entity, stream); // Десериализация stream.Position = 0; bool read = aspect.NetworkPool.Deserialize (entity, stream); world.Destroy (); ``` ## IProtoEventListener — События мира Слушатели событий позволяют реагировать на изменения в мире. Требует директиву `LEOECSPROTO_WORLD_EVENTS`. ```csharp // Добавьте LEOECSPROTO_WORLD_EVENTS в директивы компилятора #define LEOECSPROTO_WORLD_EVENTS using Leopotam.EcsProto; using System; class WorldEventListener : IProtoEventListener { public void OnEntityCreated (ProtoEntity entity) { Console.WriteLine ($"Создана сущность: {entity}"); } public void OnEntityChanged (ProtoEntity entity, ushort poolId, bool added) { string action = added ? "добавлен" : "удалён"; Console.WriteLine ($"Компонент {poolId} {action} на сущности {entity}"); } public void OnEntityDestroyed (ProtoEntity entity) { Console.WriteLine ($"Уничтожена сущность: {entity}"); } public void OnWorldResized (int capacity) { Console.WriteLine ($"Мир изменил размер: {capacity}"); } public void OnWorldDestroyed () { Console.WriteLine ("Мир уничтожен"); } } struct TestComponent { public int Value; } class TestAspect : IProtoAspect { public ProtoPool Pool; public void Init (ProtoWorld world) { world.AddAspect (this); Pool = new (); world.AddPool (Pool); } public void PostInit () { } public ProtoWorld World () => null; } // Использование ProtoWorld world = new (new TestAspect ()); WorldEventListener listener = new (); world.AddEventListener (listener); TestAspect aspect = (TestAspect) world.Aspect (typeof (TestAspect)); // Вызовет OnEntityCreated и OnEntityChanged ref TestComponent c = ref aspect.Pool.NewEntity (out ProtoEntity entity); // Вызовет OnEntityChanged и OnEntityDestroyed aspect.Pool.Del (entity); // Вызовет OnWorldDestroyed world.Destroy (); ``` ## Интеграция с Unity Пример полной интеграции фреймворка с игровым движком Unity, включая разделение систем по Update и FixedUpdate. ```csharp using Leopotam.EcsProto; using UnityEngine; // Компоненты struct TransformRef { public Transform Value; } struct RigidbodyRef { public Rigidbody Value; } struct MoveInput { public float Horizontal, Vertical; } // Аспект class UnityAspect : IProtoAspect { public ProtoWorld _world; public ProtoPool TransformPool; public ProtoPool RigidbodyPool; public ProtoPool InputPool; public ProtoIt MovementIt; public ProtoIt PhysicsIt; public void Init (ProtoWorld world) { _world = world; world.AddAspect (this); TransformPool = new (); world.AddPool (TransformPool); RigidbodyPool = new (); world.AddPool (RigidbodyPool); InputPool = new (); world.AddPool (InputPool); MovementIt = new (new [] { typeof (TransformRef), typeof (MoveInput) }); PhysicsIt = new (new [] { typeof (RigidbodyRef), typeof (MoveInput) }); } public void PostInit () { MovementIt.Init (_world); PhysicsIt.Init (_world); } public ProtoWorld World () => _world; } // Система ввода class InputSystem : IProtoInitSystem, IProtoRunSystem { UnityAspect _aspect; ProtoIt _it; public void Init (IProtoSystems systems) { _aspect = (UnityAspect) systems.World ().Aspect (typeof (UnityAspect)); _it = new (new [] { typeof (MoveInput) }); _it.Init (systems.World ()); } public void Run () { float h = Input.GetAxis ("Horizontal"); float v = Input.GetAxis ("Vertical"); foreach (ProtoEntity entity in _it) { ref MoveInput input = ref _aspect.InputPool.Get (entity); input.Horizontal = h; input.Vertical = v; } } } // Система физического движения (для FixedUpdate) class PhysicsMovementSystem : IProtoInitSystem, IProtoRunSystem { UnityAspect _aspect; float _speed = 5f; public void Init (IProtoSystems systems) { _aspect = (UnityAspect) systems.World ().Aspect (typeof (UnityAspect)); } public void Run () { foreach (ProtoEntity entity in _aspect.PhysicsIt) { ref RigidbodyRef rb = ref _aspect.RigidbodyPool.Get (entity); ref MoveInput input = ref _aspect.InputPool.Get (entity); Vector3 velocity = new Vector3 (input.Horizontal, 0, input.Vertical) * _speed; rb.Value.velocity = velocity; } } } // MonoBehaviour точка входа public class EcsStartup : MonoBehaviour { ProtoWorld _world; IProtoSystems _updateSystems; IProtoSystems _fixedUpdateSystems; void Start () { _world = new (new UnityAspect ()); // Системы для Update _updateSystems = new ProtoSystems (_world); _updateSystems .AddSystem (new InputSystem ()) .Init (); // Системы для FixedUpdate _fixedUpdateSystems = new ProtoSystems (_world); _fixedUpdateSystems .AddSystem (new PhysicsMovementSystem ()) .Init (); // Создание игровых сущностей var aspect = (UnityAspect) _world.Aspect (typeof (UnityAspect)); var playerGO = GameObject.Find ("Player"); ref TransformRef transform = ref aspect.TransformPool.NewEntity (out ProtoEntity player); transform.Value = playerGO.transform; ref RigidbodyRef rb = ref aspect.RigidbodyPool.Add (player); rb.Value = playerGO.GetComponent (); aspect.InputPool.Add (player); } void Update () { _updateSystems?.Run (); } void FixedUpdate () { _fixedUpdateSystems?.Run (); } void OnDestroy () { _updateSystems?.Destroy (); _updateSystems = null; _fixedUpdateSystems?.Destroy (); _fixedUpdateSystems = null; _world?.Destroy (); _world = null; } } ``` ## Заключение LeoECS Proto идеально подходит для разработки игр и симуляций, где требуется высокая производительность обработки большого количества игровых объектов. Фреймворк особенно эффективен в сценариях с тысячами сущностей, частым добавлением и удалением компонентов, а также при необходимости модульной архитектуры кода. Поддержка модулей позволяет легко разделять функциональность на независимые части и переиспользовать их в разных проектах. Основные паттерны интеграции включают: использование аспектов для группировки связанных пулов компонентов, создание отдельных групп систем для разных частот обновления (Update/FixedUpdate), применение сервисов для внедрения зависимостей, и использование модулей для организации кода в крупных проектах. Фреймворк может быть интегрирован в любой игровой движок на C# или использоваться автономно в серверных приложениях и симуляциях.