0 Корень Композиции
Grigory редактировал эту страницу 6 лет назад

Composition Root и EnginesRoot

Класс Main - это Корень композиции приложения. Корень композиции - это то место, где создаются и внедряются зависимости (я много об этом рассказывал в своих статьях). Корень композиции принадлежит контексту, но контекст может иметь более одного корня композиции. Например, Фабрика (Factory) является корнем композиции. В приложении может быть более одного контекста, но это продвинутый сценарий, и в этом примере мы его рассматривать не будем.

Прежде чем погрузиться в код, давайте познакомимся с первыми правилами языка Svelto.ECS. ECS - это аббревиатура Сущность Компонент Система (Entity Component System). Инфраструктура ECS была хорошо проанализирована в статьях многими авторами, но в то время как основные концепции являются общими, реализации сильно различаются. Прежде всего, нет стандартного способа решения некоторых проблем, возникающих при использовании ECS-ориентированного кода. Именно в отношении этого вопроса я прилагаю большую часть своих усилий, но об этом я расскажу позже или в следующих статьях. В основе теории лежат понятия Сущности, Компонентов (сущностей) и Систем. Хотя я понимаю, почему исторически использовалось слово Система, я с самого начала не считал его достаточно интуитивно понятным для этой цели, поэтому я использовал Движок как синоним Системы, и вы, в зависимости от ваших предпочтений, можете применять один из этих терминов.

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

Чтобы создавать и внедрять зависимости, нам нужен, по крайней мере, один корень композиции. Да, в одном приложении вполне может существовать более одного EnginesRoot, но мы не будем касаться этого в текущей статье, которую я стараюсь максимально упростить. Вот как выглядит корень композиции с созданием движков и внедрением зависимостей:

void SetupEnginesAndEntities()
{
    //Engines Root это ядро Svelto.ECS. Вы НИКОГДА не должны внедрять EngineRoot
    //Как есть, поэтому Composition Root должен содержать ссылку на него, или он 
    //будет собран сборщиком мусора.
    //UnitySumbmissionEntityViewScheduler - это планировщик, который используется EnginesRoot, чтобы знать
    //когда внедрять EntityViews. Вы не должны использовать свой, если не знаете, что вы
    //делаете или если вы не работаете с Unity.
				
    _enginesRoot = new EnginesRoot(new UnitySumbmissionEntityViewScheduler());
    //Engines root никогда не должен содержать что либо, кроме самого контекста, чтобы избежать утечек памяти.
    //Именно поэтому создаются EntityFactory и EntityFunctions.
    //EntityFactory может быть внедрен в фабрики (Или движки используемые как фабрики),
    //чтобы динамически создавать сущности.
    _entityFactory = _enginesRoot.GenerateEntityFactory();
				
    //Класс EntityFunctions содержит набор утилитарных функций для выполнения на сущностях,
    //включая удаление сущности. Я пока не предумал более подходящего названия
    var entityFunctions = _enginesRoot.GenerateEntityFunctions();
				
    //GameObjectFactory позволяет создавать Unity GameObject без использования статического
    //метода GameObject.Instantiate. Хотя это кажется лишним усложнением
    // важно, чтобы двигатели были тестируемыми, а не
    // связанными с ссылками на жесткие зависимости (почитайте мои статьи, чтобы понять
    // как работает инъекция зависимостей и почему разрешать зависимости
    // с статическими классами и синглтонами - ужасная ошибка)
    GameObjectFactory factory = new GameObjectFactory();
				
    //Паттерн наблюдатель один из 3 официальных способов коммуникации в Svelto.ECS.
    //Он должен использоваться для коммуникации между движками в очень специфических случаях.
    //Это не предпочтительное решение и в основном используется для коммуникации между устаревшим и сторонним кодом.
    var enemyKilledObservable = new EnemyKilledObservable();
    var scoreOnEnemyKilledObserver = new ScoreOnEnemyKilledObserver(enemyKilledObservable);
    //ISequencer один из 3 официальных путей доступных в Svelto.ECS 
    //для коммуникации. Они используются для решения двух специфических задач:
    //1) Указать строгий порядок выполнения между движками (Логика движков выполняется
    //горизонтально, а не вертикально, Я рассказывал об этом
    //в своих статьях). 2) Отфильтровать токен данных, переданный как параметр через
    // движки. ISequencer не основной способ для коммуникации
    //между движками
    Sequencer playerDamageSequence = new Sequencer();
    Sequencer enemyDamageSequence = new Sequencer();

    //Обертка для статических классов Unity.
    //В дальнейшем может использоваться для тестирования.
    IRayCaster rayCaster = new RayCaster();
    ITime time = new Others.Time();

    //Движки игрока. ВСЕ зависимости должны быть установлены в этом месте
    //through constructor injection.
    var playerHealthEngine = new HealthEngine(entityFunctions, playerDamageSequence);
    var playerShootingEngine = new PlayerGunShootingEngine(enemyKilledObservable, enemyDamageSequence, rayCaster, time);
    var playerMovementEngine = new PlayerMovementEngine(rayCaster, time);
    var playerAnimationEngine = new PlayerAnimationEngine();

    //Движки врагов
    var enemyAnimationEngine = new EnemyAnimationEngine();
    var enemyHealthEngine = new HealthEngine(entityFunctions, enemyDamageSequence);
    var enemyAttackEngine = new EnemyAttackEngine(playerDamageSequence, time);
    var enemyMovementEngine = new EnemyMovementEngine();
    var enemySpawnerEngine = new EnemySpawnerEngine(factory, _entityFactory);

    //Интерфейс и звуковые движки
    var hudEngine = new HUDEngine(time);
    var damageSoundEngine = new DamageSoundEngine();
				
				
    //Реализация Sequencer очень проста, но позволяет выполнять 
    //сложную конкатенацию, включая петли и условное ветвление.
    playerDamageSequence.SetSequence(
        new Steps //Последовательность шагов, является словарем!
        { 
            { //Первый шаг
                enemyAttackEngine, //Этот шаг выполняется только через функцию Next этого движка
                    new To //этот шаг может привести только к одной ветви
                    { 
                        playerHealthEngine, //Это единственный движок который будет вызван при срабатывании функции Next
                    }  
            },
            { //Второй шаг
                playerHealthEngine, //Этот шаг выполняется только через функцию Next этого движка
                    new To //Этот шаг может разветвиться на два пути
                    { 
                        {  DamageCondition.damage, new IStep[] { hudEngine, damageSoundEngine }  }, //Эти движки будут вызваны когда функция Next будет вызвана с условием DamageCondition.damage
                        {  DamageCondition.dead, new IStep[] { hudEngine, damageSoundEngine, 
                           playerMovementEngine, playerAnimationEngine, enemyAnimationEngine }  }, //Эти движки будут вызваны когда функция Next будет вызвана с условием DamageCondition.dead
                    }  
            }  
        });

    enemyDamageSequence.SetSequence(
        new Steps
        { 
            { 
                playerShootingEngine, 
                new To
                { 
                    enemyHealthEngine,
                }  
            },
            { 
                enemyHealthEngine, 
                new To
                { 
                    {  DamageCondition.damage, new IStep[] { enemyAnimationEngine, damageSoundEngine }  },
                    {  DamageCondition.dead, new IStep[] { enemyMovementEngine, 
                       enemyAnimationEngine, playerShootingEngine, enemySpawnerEngine, damageSoundEngine }  },
                }  
            }  
        });

    //Главный шаг, чтобы заставить движки работать
    //Движки игрока
    _enginesRoot.AddEngine(playerMovementEngine);
    _enginesRoot.AddEngine(playerAnimationEngine);
    _enginesRoot.AddEngine(playerShootingEngine);
    _enginesRoot.AddEngine(playerHealthEngine);
    _enginesRoot.AddEngine(new PlayerInputEngine());
    _enginesRoot.AddEngine(new PlayerGunShootingFXsEngine());
    //Движки врагов
    _enginesRoot.AddEngine(enemySpawnerEngine);
    _enginesRoot.AddEngine(enemyAttackEngine);
    _enginesRoot.AddEngine(enemyMovementEngine);
    _enginesRoot.AddEngine(enemyAnimationEngine);
    _enginesRoot.AddEngine(enemyHealthEngine);
    //Остальные движки
    _enginesRoot.AddEngine(new CameraFollowTargetEngine(time));
    _enginesRoot.AddEngine(damageSoundEngine);
    _enginesRoot.AddEngine(hudEngine);
    _enginesRoot.AddEngine(new ScoreEngine(scoreOnEnemyKilledObserver));

Этот код — из примера Survival, который теперь прокомментирован и соответствует почти всем правилам хороших практик, которые я предлагаю применять, в том числе использование платформонезависимой и тестируемой логики движков. Комментарии помогут вам понять большинство из них, но проект такого размера может быть сложен для понимания, если вы новичок в Svelto.