0 Концепции Svelto.ECS
Grigory edited this page 6 years ago

Сущности

Первый шаг после создания пустого корня композиции и экземпляра класса EnginesRoot должен идентифицировать объекты, с которыми вы хотите работать в первую очередь. Логично начать с Сущности Player. Сущность Svelto.ECS не следует путать с Игровым Объектом (GameObject) Unity. Если вы читали другие статьи, связанные с ECS, то могли видеть, что во многих из них сущности часто описываются как индексы. Вероятно, это худший способ ввести концепцию ECS. Хоть это справедливо и для Svelto.ECS, в нем это скрыто. Я хочу, чтобы пользователь Svelto.ECS представлял, описывал и идентифицировал каждую сущность с точки зрения языка предметной области игры (Game Design Domain language). Сущность в коде должна быть объектом, описанным в дизайн-документе игры. Любая другая форма определения сущности приведет к надуманному способу адаптации ваших старых представлений к принципам Svelto.ECS. Следуйте этому основополагающему правилу, и вы не ошибетесь. Класс сущности сам по себе не существует в коде, но вы все равно должны определять его не абстрактно.

Движки

Следующий шаг - подумать о том, какое поведение задать Сущности. Каждое поведение всегда моделируется внутри Движка, нельзя добавлять логику в любые другие классы внутри приложения Svelto.ECS. Мы можем начать с передвижения персонажа игрока и определить класс PlayerMovementEngine. Название движка должно быть очень узконаправленным, поскольку чем оно конкретнее, тем вероятней, что Движок будет следовать Правилу Единственной Ответственности (Single Responsibility Rule). Правильное именование классов в Svelto.ECS имеет фундаментальное значение. И цель не только в том, чтобы четко показать ваши намерения, но и в том, чтобы помочь вам самим «увидеть» их.

По этой же причине важно, чтобы ваш движок находился в очень специализированном пространстве имен. Если вы определяете пространства имен в соответствии с структурой папок, приспосабливайтесь к понятиям Svelto.ECS. Использование конкретных пространств имен помогает обнаружить ошибки проектирования, когда сущности используются внутри несовместимых пространств имен. Например, не предполагается, что какой-либо объект-враг будет использоваться внутри пространства имен игрока, если целью не стоит нарушить правила, связанные с модульностью и слабой связанностью объектов. Идея состоит в том, что объекты определенного пространства имен могут использоваться только внутри него самого или родительского пространства имен. Применяя Svelto.ECS гораздо сложнее превратить ваш код в спагетти, где зависимости внедряются направо и налево, а это правило поможет вам ещё выше поднять планку качества кода, когда зависимости правильно абстрагируются между классами.

В Svelto.ECS абстракция выдвигается на несколько рубежей, но ECS по существу способствует абстрагированию данных из логики, которая должна обрабатывать данные. Сущности определяются их данными, а не их поведением. Движки таком случае - это место, где можно поместить совместное поведение одинаковых сущностей, чтобы движки всегда могли работать с набором сущностей.

Svelto.ECS и парадигма ECS позволяют кодеру достичь одного из святых граалей чистого программирования, которым является идеальная инкапсуляция логики. Двигатели не должны иметь публичных функций. Единственные публичные функции, которые должны существовать, - это те, которые необходимы для реализации интерфейсов фреймворка. Это приводит к забыванию инъекции зависимостей и помогает избежать плохого кода, возникающего при использовании инъекции зависимостей без инверсии контроля. Движки НИКОГДА не должны внедряться ни в один другой движок или какой-либо другой тип класса. Если вы думаете, что хотите внедрить движок, вы просто сделаете принципиальную ошибку дизайна кода.

По сравнению с Unity MonoBehaviours, движки уже показывают первое огромное преимущество, которое представляет собой возможность доступа ко всем состояниям сущностей данного типа из той же области кода. Это означает, что код может легко использовать состояние всех объектов непосредственно из того же места, где будет выполняться логика общего объекта. Кроме того, отдельные движки могут обрабатывать одни и те же объекты, чтобы движок мог изменять состояние объекта, в то время как другой движок мог его прочитать, эффективно используя два движка для коммуникации через одни и те же данные сущности. Пример можно увидеть глядя на движки PlayerGunShootingEngine и PlayerGunShootingFxsEngine. В этом случае два движка находятся в одном пространстве имен, поэтому они могут совместно использовать одни и те же данные сущности. PlayerGunShootingEngine определяет, был ли поврежден игрок (враг), и записывает значение lastTargetPosition компонента IGunAttributesComponent (который является компонентом PlayerGunEntity). PlayerGunShootFxsEngine обрабатывает графические эффекты оружия и считывает позицию цели выбранной игроком. Это пример взаимодействия между движками посредством опроса данных (data polling). Позже в этой статье я покажу, как позволить механизму общаться между ними посредством проталкивания данных (Data pushing) или привязки данных (Data binding). Логично, что движки никогда не должны хранить состояние.

Движки не должны знать, как взаимодействовать с другими движками. Внешняя связь происходит через абстракцию, а Svelto.ECS решает связь между движками тремя разными официальными способами, но об этом я расскажу позже. Лучшие движки - это те, которые не требуют каких-либо внешних коммуникаций. Эти движки отражают хорошо инкапсулированное поведение и обычно работают через логический цикл. Циклы всегда моделируются с помощью задач Svelto.Task внутри приложений Svelto.ECS. Поскольку движение игрока должно обновляться каждый физический тик, было бы естественно создать задачу, выполняемую в каждое физическое обновление. Svelto.Tasks позволяет запускать каждый тип IEnumerator на нескольких типах планировщиков. В этом случае мы решили создать задачу на PhysicScheduler, которая позволяет обновить позицию игрока:

            public PlayerMovementEngine(IRayCaster raycaster, ITime time)
            {
                _rayCaster = raycaster;
                _time = time;
                _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine().SetEnumerator(PhysicsTick()).SetScheduler(StandardSchedulers.physicScheduler);
            }

            protected override void Add(PlayerEntityView entityView)
            {
                _taskRoutine.Start();
            }

            protected override void Remove(PlayerEntityView entityView)
            {
                _taskRoutine.Stop();
            }

            IEnumerator PhysicsTick()
            {   
		//Я предпологаю, что сущность игрока уже создана
		//и добавлена EnginesRoot когда этот код запускается.
		//Я предполагаю, что в массиве сущностей есть только одна сущность игрока.
                var _playerEntityViews = entityViewsDB.QueryEntityViews<PlayerEntityView>();
                var playerEntityView = _playerEntityViews[0];

                while (true)
                {   
                    Movement(playerEntityView);
                    Turning(playerEntityView);

                    yield return null; //Не забудьте использовать yield, или войдете в бесконечный цикл!
                }
            }

Задачи Svelto.Tasks могут выполняться напрямую или через объекты ITaskRoutine. Я не буду здесь много говорить о Svelto.Tasks, поскольку я написал для него другие статьи. Причина, по которой я решил использовать подпрограмму задачи вместо того, чтобы запускать реализацию IEnumerator напрямую, довольно дискреционная. Я хотел показать, что можно запустить цикл, когда объект игрока добавлен в движок и остановить его при его удалении. Однако для этого нужно знать, когда объект добавляется и удаляется.

Svelto.ECS вводит обратные вызовы для добавления и удаления, чтобы знать, когда определенные сущности добавляются или удаляются. Это нечто уникальное в Svelto.ECS, но этот подход следует использовать с умом. Я часто видел, что этими обратными вызовами злоупотребляют, так как во многих случаях их достаточно, чтобы запрашивать сущности. Даже наличие ссылки на сущность в качестве поля движка должно рассматриваться больше как исключение, чем правило.

Только когда эти обратные вызовы должны быть использованы, движок должен наследоваться либо от SingleEntityViewEngine, либо от MultiEntitiesViewEngine <EntityView1, ..., EntityViewN>. Опять-таки использование этих данных должно быть редким, и они никоим образом не намерены сообщать, какие объекты будет обрабатывать движок.

Движки чаще всего реализуют интерфейс IQueryingEntityViewEngine. Это позволяет получить доступ к базе данных сущностей и извлекать данные из нее. Помните, что вы всегда можете запросить какой-либо объект изнутри движка, но в тот момент, когда вы запрашиваете сущность, которая несовместима с пространством имен, где находится движок, вы должны понимать, что уже делаете что-то неправильно. Движки никогда не должны предполагать, что сущности доступны, и должны работать над набором объектов. Не следует предполагать, что в игре всегда будет только один игрок, как я делаю в примере кода. В EnemyMovementEngine находится очень общий подход к тому, как запрашивать объекты:

            public void Ready()
            {
                Tick().Run();
            }

            IEnumerator Tick()
            {
                while (true)
                {
                    var enemyTargetEntityViews = entityViewsDB.QueryEntityViews<EnemyTargetEntityView>();

                    if (enemyTargetEntityViews.Count > 0)
                    {
                        var targetEntityView = enemyTargetEntityViews[0];

                        var enemies = entityViewsDB.QueryEntityViews<EnemyEntityView>();

                        for (var i = 0; i < enemies.Count; i++)
                        {
                            var component = enemies[i].movementComponent;

                            component.navMeshDestination = targetEntityView.targetPositionComponent.position;
                        }
                    }

                    yield return null;
                }
            }

В этом случае основной цикл движка запускается непосредственно на предопределенном планировщике. Tick().Run() показывает самый короткий способ запуска IEnumerator с Svelto.Tasks. IEnumerator будет продолжать уступать следующему кадру, пока не будет найдена хотя бы одна цель Enemy. Поскольку мы знаем, что всегда будет только одна цель (другое нехорошее предположение), я выбираю первую доступную. В то время как цель Enemy Target может быть только одной (хотя могло быть и больше!), Врагов много, и движок все-таки заботится о логике движения для всех. В этом случае я схитрил, поскольку на самом деле я использую Unity Nav Mesh System, поэтому все, что мне нужно сделать, это просто установить точку назначения в NavMesh. Честно говоря, я никогда не использовал код Unity NavMesh, поэтому я даже не уверен, как он работает, этот код просто унаследован от оригинальной демонстрации Survival.

Обратите внимание, что компонент никогда не предоставляет напрямую зависимость Navmesh Unity. Компонент Сущности, как я расскажу позже, должен всегда выставлять типы значений. В этом случае это правило также позволяет сохранить код под контролем, так как тип значения поля navMeshDestination может быть позже реализован без использования Unity Nav Mesh.

Для завершения параграфа, относящегося к движкам, обратите внимание на то, что нет такого понятия, как слишком маленький движок. Следовательно, не бойтесь писать движок содержащий нескольких строк кода, ведь вы не можете писать логику где-то еще, и вам нужно, чтобы ваши движки следовали правилу единой ответственности.

Представления сущности

До этого мы ввели концепцию Движка и абстрактное определение Сущности, давайте теперь определим, что такое Представление сущности. Я должен признать, что из 5 концепций, на которых построен Svelto.ECS, Представления сущностей, вероятно, являются самыми запутанными. Ранее названные Узлом (Node) (название, взятое из ECS фреймворка Ash), я понял, что название “Узел” ничего не значило. EntityView также может вводить в заблуждение, поскольку программисты обычно ассоциируют представления с концепцией, исходящей из шаблона Модель Представление Контроллер (Model View Controller), однако в Svelto.ECS используется View, потому что EntityView - это то, как Движок видит Сущность. Мне нравится описывать его так, поскольку это кажется наиболее естественным, но я мог бы также назвать его EntityMap, поскольку EntityView отображает компоненты сущности, к которым должен обращаться движок. Эта схема концепций Svelto.ECS должна немного помочь:

Я предлагаю начинать работу с Движка, и сейчас мы находимся на правой стороне этой схемы. Каждый движок имеет собственный набор EntityViews. Движок может повторно использовать совместимые с пространством имен EntityViews, но чаще всего Движок определяет его EntityViews. Движок не заботится о том, действительно ли определена сущность Player, он констатирует тот факт, что ему нужен PlayerEntityView для работы. Написание кода зависит от потребностей Движка, вы не должны создавать сущность и её поле, прежде чем поняли как их использовать. В более сложном сценарии имя EntityView могло бы быть еще более конкретным. Например, если нам пришлось бы писать сложные движки для обработки логики игрока и рендеринга графики игрока (или анимации и т. д.), Мы могли бы иметь PlayerPhysicEngine с PlayerPhysicEntityView, а также PlayerGraphicEngine с PlayerGraphicEntityView или PlayerAnimationEngine с PlayerAnimationEntityView. Можно использовать более конкретные имена, такие как PlayerPhysicMovementEngine или PlayerPhysicJumpEngine (и т. д.).

Компоненты

Мы поняли, что движки моделируют поведение для набора данных сущностей, и мы понимаем, что движки не используют сущности напрямую, а используют компоненты сущности через представления сущностей. Мы поняли, что EntityView - это класс, который может содержать ТОЛЬКО открытые (public) компоненты сущностей. Я также намекнул, что компоненты сущностей всегда являются интерфейсами, поэтому давайте дадим лучшее определение:

Сущности представляют собой набор данных, а компоненты сущностей - это способ доступа к этим данным. Если вы еще этого не заметили, определение компонентов сущности как интерфейсов является еще одной довольно уникальной особенностью Svelto.ECS. Обычно компоненты в других фреймворках являются объектами. Использование интерфейсов вместо этого позволяет значительно сократить код. Если вы следуете принципу «Разделение интерфейса» (Interface Segregation Principle), написав небольшие интерфейсы компонентов, даже с одним свойством каждый, вы заметите, что начали повторно использовать интерфейсы компонентов внутри разных сущностей. В нашем примере ITransformComponent повторно используется во многих представлениях сущности. Использование компонентов в качестве интерфейсов также позволяет им реализовывать одни и те же объекты, что во многих случаях позволяет упростить связь между сущностями, которые видят одну и ту же сущность с помощью разных представлений сущностей (или одной и той же, если это возможно).

Поэтому в Svelto.ECS компонент сущности всегда является интерфейсом, и этот интерфейс используется только через поле EntityView внутри Движка. Интерфейс компонента сущности затем реализуется так называемым «Имплементором». Теперь мы начинаем определять саму Сущность, и находимся в левой части вышеприведенной схемы.

Компоненты должны всегда хранить значимые типы, и поля всегда являются свойствами. Исключения могут быть сделаны только для того, чтобы писать сеттеры и геттеры в качестве методов для использования ключевого слова ref, когда необходима оптимизация. Это не означает, что код ориентирован на данные (data oriented), но он позволит создавать код для тестов, поскольку логика движка не должна обрабатывать ссылки на внешние зависимости. Кроме того, это мешает кодерам обманывать фреймворк и использовать публичные функции (которые могут включать логику!) случайных объектов. Единственная причина, по которой можно было почувствовать необходимость использования ссылок внутри интерфейсов компонентов сущностей, - это иметь дело с зависимостями третьих сторон, такими как объекты Unity. Тем не менее, пример Survival, показывает, как справиться с этим, оставляя тестовый код движков без необходимости заботиться о зависимостях Unity.

Дескрипторы Сущности

Именно здесь Дескрипторы сущностей приходят на помощь, чтобы собрать все вместе. Мы знаем, что движки могут получать доступ к данным Сущности через Компоненты, которые хранятся в Представлениях сущности. Мы знаем, что движки являются классами, EntityView - это классы, которые содержат только Компоненты сущности и что Компоненты являются интерфейсами. Хотя я дал абстрактное определение Сущности, мы не видели ни одного класса, который фактически представляет собой Сущность. Это соответствует концепции объектов, являющихся идентификаторами внутри современной системы ECS. Однако без правильного определения Сущности это заставит кодеров идентифицировать Сущности с Представлениями сущностей, что было бы катастрофически неправильным. Представления сущностей - это способ, которым несколько Движков могут видеть одну и ту же Сущность, но они не являются Сущностями. Сама Сущность всегда должна рассматриваться как набор данных, определенных через Компоненты сущности, но даже это - слабое определение. Экземпляр EntityDescriptor дает возможность кодеру правильно определять свои Сущности независимо от движков, которые будут обрабатывать их. Поэтому в случае с Сущностью Player нам понадобится PlayerEntityDescriptor. Этот класс будет использоваться для создания Сущности, и хотя то, что он действительно делает, является чем-то совершенно другим, тот факт, что пользователь может писать BuildEntity(), помогает очень просто визуализировать Сущности для построения и сообщить о намерениях другим кодерам.

Однако то, что действительно делает EntityDescriptor, - это создает список EntityViews !!! На ранних этапах разработки фреймворка я разрешал кодерам создавать этот список EntityViews вручную, что приводило к очень уродливому коду, поскольку он больше не мог визуализировать то, что на самом деле происходило.

Вот как выглядит PlayerEntityDescriptor:

    using Svelto.ECS.Example.Survive.Camera;
    using Svelto.ECS.Example.Survive.HUD;
    using Svelto.ECS.Example.Survive.Enemies;
    using Svelto.ECS.Example.Survive.Sound;

    namespace Svelto.ECS.Example.Survive.Player
    {
        public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView,
            EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView>
        {

        }
    }

Дескрипторы сущностей (и Имплементоры) являются единственными классами, которые могут использовать идентификаторы из нескольких пространств имен. В этом случае PlayerEntityDescriptor определяет список EntityViews для создания экземпляра и внедрения в движок при создании PlayerEntity.

EntityDescriptorHolder

EntityDescriptorHolder является расширением для Unity и должен использоваться только в определенных случаях. Наиболее распространенным является создание своего рода полиморфизма, хранящего информацию о Сущности для построения Unity GameObject. Таким образом, один и тот же код может использоваться для создания нескольких типов Сущностей. Например, в Robocraft мы используем единую фабрику кубов, которая строит все кубы из которых состоят машины. Тип куба для сборки хранится в префабе самого куба. Это хорошо, пока имплементоры одинаковы между кубами или найдены в GameObject как MonoBehaviour’s. Создавать Сущности напрямую предпочтительнее, поэтому используйте EntityDescriptorHolders только тогда, когда вы правильно поняли принципы Svelto.ECS, иначе существует риск злоупотребления ими. Эта функция из примера показывает, как использовать класс:

void BuildEntitiesFromScene(UnityContext contextHolder)
{
    //EntityDescriptorHolder - это специальный класс Svelto.ECS созданный,
    //чтобы динамично извлекать данные сущности из игровых объектов.
    //Игровые объекты могут содеражть всю информацию необходимую для создания сущности.
    //Это позволяет создать своего рода полиморфный код, который может быть переиспользован
    //для создания разных типов сущностей
    IEntityDescriptorHolder[] entities = contextHolder.GetComponentsInChildren<IEntityDescriptorHolder>();

    //Это достаточно общий паттерн в Svelto.ECS, добавленный, чтобы автоматически
    //создавать сущности из объектов представленных в сцене.
    //Я предпочитаю избегать этот способ и создавать сущности напрямую.
    //Вам следует избегать использования EntityDescriptorHolder,
    //когда это не необходимо
    for (int i = 0; i < entities.Length; i++)
    {
        var entityDescriptorHolder = entities[i];
        var entityDescriptor = entityDescriptorHolder.RetrieveDescriptor();
        _entityFactory.BuildEntity
        (((MonoBehaviour) entityDescriptorHolder).gameObject.GetInstanceID(),
        entityDescriptor,
        (entityDescriptorHolder as MonoBehaviour).GetComponentsInChildren<IImplementor>());
    }
}

Обратите внимание, что в этом примере я использую менее предпочтительную, не обобщенную функцию BuildEntity. Я поясню это. В этом случае имплементоры являются классами MonoBehaviour присоединенными к GameObject. Это не очень хорошая практика. Я должен был удалить этот код из примера, но оставил, чтобы показать вам этот особенный случай. Имплементоры, как мы увидим дальше, должны быть классами MonoBehaviours только тогда, когда это необходимо!

Имплементоры

Прежде чем создавать свою сущность, давайте определим последнюю концепцию в Svelto.ECS, которой является Имплементор. Как мы знаем, Компоненты сущности это всегда интерфейсы, а интерфейсы C# должны быть реализованы. Объект, реализующий эти интерфейсы, называется «Имплементором». У Имплементоров есть несколько важных характеристик:

  • Возможность отвязать количество объектов для сборки от количества компонентов сущности, необходимых для определения данных сущности.
  • Возможность обмениваться данными между разными Компонентами, поскольку Компоненты предоставляют данные через свойства, разные свойства Компонента могут возвращать одно и то же поле реализации. Возможность создания заглушки интерфейса компонента сущности. Это важно для того, чтобы оставить тестируемым код движка.
  • Действуют как мост между Движками Svelto.ECS и сторонними (third party) платформами. Эта характеристика имеет фундаментальное значение. Если вам нужен Unity, чтобы общаться с движками, вам не нужно использовать неудобные обходные пути, просто создайте имплементор как наследник Monobehaviour. Таким образом, вы можете использовать внутри имплементора обратные вызовы Unity, такие как OnTriggerEnter / OnTriggerExit, и изменять данные в соответствии с обратным вызовом Unity. Не следует использовать логику внутри этого обратного вызова, за исключением установки данных Компонентов сущности. Вот пример:
        public class EnemyTriggerImplementor : MonoBehaviour, IImplementor, IEnemyTriggerComponent, IEnemyTargetComponent
        {
            public event Action<int, int, bool> entityInRange;

            bool IEnemyTriggerComponent.targetInRange { set { _targetInRange = value; } }
            bool IEnemyTargetComponent.targetInRange { get { return _targetInRange; } }

            void OnTriggerEnter(Collider other)
            {
                if (entityInRange != null)
                    entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), true);
            }

            void OnTriggerExit(Collider other)
            {
                if (entityInRange != null)
                    entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), false);
            }

            bool    _targetInRange;
        }

Помните, что степень разбиения ваших EntityViews, Компонентов сущностей и Имплементоров полностью зависит от вас. Чем мельче они разбиты, тем проще их повторно использовать.

Создание Сущностей

Предположим, что мы создали наши Движки, добавили их в EnginesRoot, создали их Представления, которым нужны Компоненты в качестве интерфейсов, которые будут реализованы внутри Имплементоров. Настало время создать нашу первую Сущность. Сущность всегда создается через экземпляр Фабрики сущностей (Entity Factory), созданный EnginesRoot через функцию GenerateEntityFactory. В отличие от экземпляра EnginesRoot экземпляр IEntityFactory можно внедрять и передавать. Объекты могут быть построены внутри корня композиции или динамически внутри фабрик, поэтому для последнего случая необходимо передать IEntityFactory через параметр.

IEntityFactory идет с несколькими похожими функциями. В рамках этой статьи я пропущу объяснение функций PreallocateEntitySlots и BuildMetaEntity, чтобы сосредоточиться на наиболее часто используемых функциях BuildEntity и BuildEntityInGroup.

Лучше всегда использовать BuildEntityInGroup, но для примера Survival на не понадобится, поэтому давайте посмотрим, как обычный BuildEntity используется в примере:

IEnumerator IntervaledTick()
{
	
//Одно важное замечание: Никогда не создавайте имплементоры в виде MonoBehaviour только для хранения данных. 
//Данные всегда должны извлекаться через сервисный уровень независимо от источника данных. 
//Выгоды многочисленны, в том числе тот факт, что для изменения источника данных потребуется изменить только код сервиса. 
//В этом простом примере я не использую Сервисный слой, но в целом идея ясна.
//Также обратите внимание, что я загружаю данные только один раз для каждого запуска приложения, 
//вне основного цикла. Вы всегда можете использовать этот трюк, если данные, которые вам нужны, 
//не будут изменяться.
    var enemiestoSpawn = ReadEnemySpawningDataServiceRequest();

    while (true)
    {               
	//Svelto.Tasks позволяет использовать стандартные yield операторы Unity, но они могут снижать производительность.
	//Поэтому самым быстрым решением будет использование собственных перечислителей. Честно говоря, разница минимальна,
    //но лучше этим не злоупотреблять.
        yield return _waitForSecondsEnumerator;

        if (enemiestoSpawn != null)
        {
            for (int i = enemiestoSpawn.Length - 1; i >= 0 && _numberOfEnemyToSpawn > 0; --i)
            {
                var spawnData = enemiestoSpawn[i];

                if (spawnData.timeLeft <= 0.0f)
                {
		    //Ищем случайный индекс между нулем и количеством точек для спавна
                    int spawnPointIndex = Random.Range(0, spawnData.spawnPoints.Length);

		    //Создаем экземпляр префаба врага в случайно выбранной точке.
                    var go = _gameobjectFactory.Build(spawnData.enemyPrefab);
								
		    //Здесь я поленился и извлек данные напрямую из MonoBehaviour.
		    //Я не создаю имплементор для этой цели.
                    var data = go.GetComponent<EnemyAttackDataHolder>();

		    //Здесь мы используем смесь из MonoBehaviour имплеметоров и
		    //нормальных имплементоров:
                    List<IImplementor> implementors = new List<IImplementor>();
                    go.GetComponentsInChildren(implementors);
                    implementors.Add(new EnemyAttackImplementor(data.timeBetweenAttacks, data.attackDamage));
								
		    //В этом примере каждый вид врага генерирует одинаковый список EntityViews, 
		    //поэтому я всегда использую одинаковый EntityDescriptor. 
		    //Однако, если разные враги должны создавать разные EntityView 
		    //для разных движков, это был бы хороший пример, где EntityDescriptorHolder 
		    //мог бы использоваться для использования своего рода полиморфизма, 
		    //который описывается в моих статьях.
                    _entityFactory.BuildEntity<EnemyEntityDescriptor>(
                    go.GetInstanceID(), implementors.ToArray());

                    var transform = go.transform;
                    var spawnInfo = spawnData.spawnPoints[spawnPointIndex];

                    transform.position = spawnInfo.position;
                    transform.rotation = spawnInfo.rotation;

                    spawnData.timeLeft = spawnData.spawnTime;
                    numberOfEnemyToSpawn--;
                }

                spawnData.timeLeft -= 1.0f;
            }
        }
    }
}

Не забудьте прочитать все комментарии в этом примере, они помогут еще лучше понять концепции Svelto.ECS. Из-за простоты примера я не использую BuildEntityInGroup, который применяется в более сложных проектах. В Robocraft каждый движок, который обрабатывает логику функциональных кубов, обрабатывает логику ВСЕХ функциональных кубов этого конкретного типа в игре. Однако, часто необходимо знать, к какому транспортному средству принадлежат кубы, поэтому использование группы для каждой машины поможет разбить кубы одного и того же типа по машинам, где идентификатор машины - это идентификатор группы. Это позволяет нам реализовывать классные штуки, такие как запуск одной задачи Svelto.Tasks на машину внутри одного и того же движка, который может работать параллельно с использованием многопоточности.

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

Никогда не создавайте Имплементоры в виде MonoBehaviour только для хранения данных. Данные всегда должны извлекаться через Сервисный слой независимо от источника данных. Выгоды многочисленны, в том числе тот факт, что для изменения источника данных потребуется изменить только код сервиса. В этом простом примере я не использую Сервисный слой, но в целом идея ясна. Также обратите внимание, что я загружаю данные только один раз для каждого запуска приложения, вне основного цикла. Вы всегда можете использовать этот трюк, если данные, которые вам нужны, никогда не изменяются.

Первоначально я считывал данные непосредственно из MonoBehaviour, как это сделал бы хороший ленивый кодер. Это заставило меня создать Имплементор в виде MonoBehaviour только для чтения сериализованных данных. Это приемлемо, если мы не хотим абстрагировать источник данных, однако намного лучше сериализовывать информацию в json-файл и считывать её по запросу к сервису, чем читать эти данные из Компонента сущности.