Recursivity для DragonECS

November 12, 2024 · View on GitHub

Version License Discord QQ

Recursivity для DragonECS

Readme Languages:

Русский

English(WIP)

Упрощает обработку событий симулируя поведение рекурсии, но в рамках стандартной структуры ECS. Гарантирует для событий которые должны быть обработаны в рамках одного кадра, что они будут обработаны всеми системами ровно один раз, в независимости от места появления.

Warning

Проект в стадии разработки. API может меняться.

Оглавление


Установка

Семантика версионирования - Открыть

Окружение

Обязательные требования:

  • Зависимость: DragonECS
  • Минимальная версия C# 7.3;

Опционально:

  • Игровые движки с C#: Unity, Godot, MonoGame и т.д.

Протестировано:

  • Unity: Минимальная версия 2020.1.0;

Установка для Unity

Поддерживается установка в виде Unity-модуля в при помощи добавления git-URL в PackageManager или ручного добавления в Packages/manifest.json:

https://github.com/DCFApixels/DragonECS-Recursivity.git
  • В виде исходников

Пакет так же может быть добавлен в проект в виде исходников.


Инициализация

Для обработки событий используется процесс IOn<T>.ToRun() где T это тип компонента-события. Процесс IOn<T>.ToRun() контролирует специальная система, которую необходимо инициализировать в пайплайне.

_world = new EcsDefaultWorld();
_pipeline = EcsPipeline.New()
    // ...
    // Инициализация системы контролирующей процесс IOn<DamageEvent>.ToRun().
    // Аргумент maxLoops устанавливает лимит на количество выполнений за один кадр.
    .AddOn<DamageEvent>(maxLoops: 100)
    // Добавление систем обрабатывающих этот процесс
    .Add(new SomeDamageSystem())
    .Add(new SomeReturnDamageAbilitySystem())
    // ...
    .Inject(_world)
    .BuildAndInit();

За запуск всех систем которые выполняются рекурсивно, включая управляющих процессом IOn<T>.ToRun(), ответственна система рекурсии. По умолчанию система рекурсии добавляется на слой EcsRecursivityConsts.RECURSIVE_LAYER, а слой EcsRecursivityConsts.RECURSIVE_LAYER вставляется после EcsConsts.BASIC_LAYER.


Обработка событий

Системы с IOn<T>.ToRun() будут выполняться до тех пор пока в мире остается хотя бы один компонент-событие T.

С точки зрения замедления производительности влияние не высоко по нескольким причинам:

  • Системы с IOn<T>.ToRun() не выполняются вовсе если в мире нет ни одного компонента T.
  • В инициализации .AddOn<DamageEvent>(maxLoops) Можно выставить лимит(maxLoops) вызова IOn<T>.ToRun(), тогда при достижении лимита, оставшиеся события будут обработаны в следующем кадре.

Имеется защита от бесконечного зацикливания в виде глобального лимита в 100_000 повторений.

Ниже приведен пример системы обрабатывающей события. В примере реализована система применения урона к здоровью и система способности возврата урона атакующему, что-то вроде шипов.

Используемые в примере компоненты
using DCFApixels.DragonECS;
public struct Health : IEcsComponent
{
    public float points;
}
public struct DamageEvent : IEcsComponent
{
    public entlong source;
    public entlong target;
    public float points;
}
public struct ReturnDamageAbility : IEcsComponent
{
    public float multiplier;
}

Этот пример имеет некоторые проблемы, но как пример достаточно нагляден для понимания работы.

// Система которая применяет полученный урон к здоровью.
public class SomeApplyDamageSystem : IOn<DamageEvent>, IEcsInject<EcsDefaultWorld>
{
    class EventAspect : EcsAspect
    {
        public EcsPool<DamageEvent> damageEvents = Inc;
    }
    class Aspect : EcsAspect
    {
        public EcsPool<Health> healths = Inc;
    }

    EcsDefaultWorld _world;

    // targetEntities содержит все сущности с компонентом DamageEvent. 
    // Сущности из этого же списка в конце цикла будут автоматически отчищены от компонента DamageEvent.
    void IOn<DamageEvent>.ToRun(EcsSpan targetEntities)
    {
        var a = _world.GetAspect<Aspect>();
        // Итерироваться нужно по targetEntities, 
        // так можно гарантировать что системы будут обрабатывать каждое событие один раз.
        foreach (var ee in targetEntities.Where(out EventAspect ea))
        {
            ref var damageEvent = ref ea.damageEvents.Get(ee);
            // Извлечение ID сущности с проверкой что она не была удалена.
            // И проверка на соответствие аспекту Aspect.
            if (damageEvent.target.TryGetID(out int e) && 
                _world.IsMatchesMask(a, e))
            {
                ref var health = ref a.healths.Get(e);
                health.points -= damageEvent.points;
            }
        }
    }

    public void Inject(EcsDefaultWorld obj) => _world = obj;
}
// Система которая делает возвратный урон.
public class SomeReturnDamageAbilitySystem : IOn<DamageEvent>, IEcsInject<EcsDefaultWorld>
{
    class EventAspect : EcsAspect
    {
        public EcsPool<DamageEvent> damageEvents = Inc;
    }
    class Aspect : EcsAspect
    {
        public EcsPool<ReturnDamageAbility> returnDamageAbilities = Inc;
    }

    EcsDefaultWorld _world;

    void IOn<DamageEvent>.ToRun(EcsSpan targetEntities)
    {
        var a = _world.GetAspect<Aspect>();
        // Итерируемся по targetEntities.
        foreach (var ee in targetEntities.Where(out EventAspect ea))
        {
            ref var damageEvent = ref ea.damageEvents.Get(ee);
            if (damageEvent.target.TryGetID(out int targetE) &&
                damageEvent.source.TryGetID(out int sourceE) &&
                _world.IsMatchesMask(a, targetE))
            {
                ref var returnDamageAbility = ref a.returnDamageAbilities.Get(targetE);

                // Создание события возвратного урона,
                // которое будет обработано в следующем цикле.
                int newEE = _world.NewEntity(ea);
                ref var newDamageEvent = ref ea.damageEvents.Get(newEE);
                newDamageEvent.target = damageEvent.source;
                newDamageEvent.source = damageEvent.target;
                newDamageEvent.points = damageEvent.points * returnDamageAbility.multiplier;
            }
        }
    }

    public void Inject(EcsDefaultWorld obj) => _world = obj;
}