-
-
Notifications
You must be signed in to change notification settings - Fork 95
Корень Композиции
Класс 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.