Как развить игровое состояние сущности-компонента в пошаговой игре?

9

До сих пор системы компонентов сущностей, которые я использовал, работали в основном как artemis Java:

  • Все данные в компонентах
  • Независимые системы без состояний (по крайней мере, в той степени, в которой они не требуют ввода при инициализации), итерирующие по каждой сущности, которая содержит только компоненты, в которых заинтересована конкретная система
  • Все системы обрабатывают свои объекты одним кликом, а затем все начинается заново.

Теперь я пытаюсь применить это к пошаговой игре впервые, с множеством событий и ответов, которые должны произойти в установленном порядке относительно друг друга, прежде чем игра сможет двигаться дальше. Пример:

Игрок А получает урон от меча. В ответ на это броня А срабатывает и снижает получаемый урон. Скорость движения А также уменьшается в результате ослабления.

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

События также могут запускать другие события. Уменьшение урона от меча с помощью доспехов может привести к разрушению меча (это должно произойти до завершения уменьшения урона), что, в свою очередь, может вызвать дополнительные события в ответ на него, по существу, рекурсивную оценку событий.

В целом, это, кажется, приводит к нескольким проблемам:

  1. Множество потраченных впустую циклов обработки: большинство систем (за исключением тех вещей, которые всегда работают, например, рендеринга) просто не имеют ничего стоящего, если не «их очередь» работать, и проводят большую часть времени в ожидании входа в игру. действительное рабочее состояние. Это засоряет каждую такую ​​систему чеками, размер которых увеличивается по мере добавления новых состояний в игру.
  2. Чтобы выяснить, может ли система обрабатывать сущности, присутствующие в игре, им необходимо каким-то образом отслеживать другие несвязанные состояния сущности / системы (система, ответственная за нанесение урона, должна знать, была ли применена броня). Это либо запутывает системы с множеством обязанностей, либо создает необходимость в дополнительных системах без какой-либо иной цели, кроме сканирования коллекции объектов после каждого цикла обработки и связи с группой слушателей, сообщая им, когда можно что-то делать.

Вышеупомянутые два пункта предполагают, что системы работают с одним и тем же набором объектов, которые в конечном итоге изменяют состояние, используя флаги в своих компонентах.

Другим способом решения этой проблемы является добавление / удаление компонентов (или создание совершенно новых объектов) в результате единой системной работы по улучшению состояния игр. Это означает, что всякий раз, когда система действительно имеет соответствующий объект, она знает, что ей разрешено ее обрабатывать.

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

Это то, с чем мне придется жить? Каждый пример ECS, который я видел, был в реальном времени, и действительно легко увидеть, как в таких случаях работает цикл «одна итерация на игру». И я все еще нуждаюсь в этом для рендеринга, он просто кажется неподходящим для систем, которые останавливают большинство аспектов себя каждый раз, когда что-то происходит

Есть ли какой-то шаблон проектирования для продвижения состояния игры вперед, который подходит для этого, или я должен просто вывести всю логику из цикла и вместо этого запускать ее только при необходимости?

Aeris130
источник
Вы действительно не хотите опрашивать, чтобы событие произошло. Событие происходит только тогда, когда оно происходит. Разве Артемида не позволяет системам общаться друг с другом?
Сидар
Это так, но только соединяя их, используя методы.
Aeris130

Ответы:

3

Мой совет здесь основан на прошлом опыте в проекте RPG, где мы использовали систему компонентов. Я скажу, что я ненавидел работать в этом игровом коде, потому что это был код спагетти. Так что я не предлагаю здесь много ответов, просто перспектива:

Логика, которую вы описываете для обработки урона от меча игроку ... кажется, что одна система должна отвечать за все это.

Где-то есть функция HandleWeaponHit (). Он получит доступ к ArmorComponent сущности игрока, чтобы получить соответствующую броню. Он получит доступ к оружейному компоненту атакующего оружия, чтобы, возможно, разрушить оружие. После вычисления окончательного урона, он коснется компонента MovementComponent для игрока, чтобы добиться снижения скорости.

Что касается потраченных впустую циклов обработки ... HandleWeaponHit () должен запускаться только при необходимости (при обнаружении удара мечом).

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

Эрик Андерсандер
источник
Если вы сделаете это таким образом, функция hit () всплывет, поскольку добавлено больше поведения. Допустим, есть враг, который падает со смеху каждый раз, когда меч поражает цель (любую цель) в пределах ее прямой видимости. Должен ли HandleWeaponHit действительно отвечать за его запуск?
Aeris130
1
У вас плотно запутанная боевая последовательность, так что да, удар отвечает за запуск эффектов. Не все должно быть разбито на маленькие системы, пусть эта единственная система справится с этим, потому что это действительно ваша «Боевая система», и она обрабатывает ... Бой ...
Патрик Хьюз
3

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

Я не уверен, что это нарушает концепции ECS, но что если:

  • Добавьте EventBus, чтобы позволить системам выпускать / подписываться на объекты событий (на самом деле чистые данные, но не компонент, я думаю)
  • Создать компоненты для каждого промежуточного состояния

Пример:

  • UserInputSystem запускает событие Атака с помощью [DamageDealerEntity, DamageReceiverEntity, Skill / Weapon used info]
  • CombatSystem подписывается на нее и рассчитывает вероятность уклонения для DamageReceiver. Если уклонение заканчивается неудачей, оно вызывает событие Damage с теми же параметрами
  • DamageSystem подписан на такое событие и, таким образом, срабатывает
  • DamageSystem использует силу, урон BaseWeapon, его тип и т. Д. И записывает его в новый IncomingDamageComponent с помощью [DamageDealerEntity, FinalOutgoingDamage, DamageType] и присоединяет его к объекту / объекту получателя ущерба.
  • DamageSystem запускает OutgoingDamageCalculated
  • ArmorSystem запускается им, подбирает сущность получателя или выполняет поиск по этому аспекту IncomingDamage в сущностях, чтобы подобрать IncomingDamageComponent (последний может быть лучше для нескольких атак с разбросом) и вычисляет броню и нанесенный ей урон. Опционально запускает события для разрушения меча
  • ArmorSystems удаляет IncomingDamageComponent в каждой сущности и заменяет его на DamageReceivedComponent с окончательными вычисленными числами, которые повлияют на HP и снижение скорости от ран
  • ArmorSystems отправляет событие IncomingDamageCalculated
  • Система скорости подписывается и пересчитывает скорость
  • HealthSystem подписан и снижает фактический HP
  • так далее
  • Как-то убирать

Плюсы:

  • Система запускает друг друга, предоставляя промежуточные данные для сложных цепных событий
  • Разделение с помощью EventBus

Минусы:

  • Я чувствую, что смешиваю два способа передачи вещей: в параметрах событий и во временных компонентах. это может быть слабое место. Теоретически, чтобы все было однородно, я мог бы запускать просто перечисление событий без данных, чтобы системы могли найти подразумеваемые параметры в компонентах сущности по аспектам ... Хотя не уверен, что все в порядке
  • Не уверен, как узнать, все ли потенциально заинтересованные SystemsHave обработали IncomingDamageCalculated, чтобы его можно было очистить и позволить следующему повороту произойти. Может быть, какие-то проверки обратно в CombatSystem ...
Сергей Яковлев
источник
2

Публикация решения, на котором я наконец остановился, аналогична решению Яковлева.

По сути, я закончил тем, что использовал систему событий, поскольку нашел очень интуитивно понятным следовать его логике по очереди. Система в конечном итоге отвечала за игровые юниты, которые придерживались пошаговой логики (игрок, монстры и все, с чем они могут взаимодействовать), задачи в реальном времени, такие как рендеринг и опрос ввода, были размещены в другом месте.

Системы реализуют метод onEvent, который принимает событие и объект в качестве входных данных, сигнализируя, что объект получил событие. Каждая система также подписывается на события и объекты с определенным набором компонентов. Единственной точкой взаимодействия, доступной для систем, является одноэлементный диспетчер сущностей, используемый для отправки событий сущностям и для извлечения компонентов из конкретной сущности.

Когда менеджер объекта получает событие, связанное с объектом, которому он отправляется, он помещает событие в конец очереди. Пока в очереди есть события, главное событие извлекается и отправляется каждой системе, которая подписывается на событие и интересуется набором компонентов объекта, получающего событие. Эти системы могут, в свою очередь, обрабатывать компоненты объекта, а также отправлять дополнительные события менеджеру.

Пример: игрок получает урон, поэтому объект игрока получает событие с уроном. DamageSystem подписывается на события повреждения, отправленные любому объекту с компонентом работоспособности, и имеет метод onEvent (сущность, событие), который уменьшает работоспособность в компоненте сущностей на величину, указанную в событии.

Это облегчает вставку системы брони, которая подписывается на события повреждения, отправленные сущностям с компонентом брони. Его метод onEvent уменьшает урон в событии на количество брони в компоненте. Это означает, что указание порядка, в котором системы получают события, влияет на игровую логику, поскольку система брони должна обработать событие повреждения до того, как система повреждения заработает.

Однако иногда система должна выйти за пределы принимающей организации. Чтобы продолжить мой ответ Эрику Ундерсандеру, было бы тривиально добавить систему, которая обращается к игровой карте и ищет объекты с FallsDownLaughingComponent в пределах x пространств объекта, получающего повреждение, а затем отправляет им FallDownLaughingEvent. Эта система должна была быть запланирована для получения события после системы повреждений, если событие повреждения не было отменено в тот момент, ущерб был нанесен.

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

Игрок перемещается, запрашивая событие перемещения, отправляемое объекту игрока и принимаемое системой движения.

В очереди: движение

Если движение разрешено, система корректирует положение игроков. Если нет (игрок пытался переместиться в препятствие), он помечает событие как отмененное, заставляя менеджера сущности отказаться от него, а не отправлять в последующие системы. В конце списка систем, заинтересованных в событии, находится TurnFinishedSystem, которая подтверждает, что игрок потратил свой ход на перемещение персонажа, и что его / ее ход закончен. Это приводит к тому, что событие TurnOver отправляется объекту игрока и помещается в очередь.

В очереди: TurnOver

Теперь скажите, что игрок наступил на ловушку, наносящую урон. TrapSystem получает сообщение о движении до TurnFinishedSystem, поэтому сначала отправляется событие повреждения. Теперь очередь вместо этого выглядит так:

В очереди: повреждение, поворот

Пока все в порядке, событие повреждения будет обработано, и затем ход закончится. Однако что делать, если в ответ на ущерб отправляются дополнительные события? Теперь очередь событий будет выглядеть так:

В очереди: Повреждение, Поворот, ResponseToDamage

Другими словами, поворот завершится до того, как будут обработаны ответы на повреждения.

Чтобы решить эту проблему, я использовал два метода отправки событий: отправить (событие, сущность) и ответить (событие, eventToRespondTo, сущность).

Каждое событие ведет запись предыдущих событий в цепочке ответов, и всякий раз, когда используется метод response (), отвечаемое событие (и каждое событие в его цепочке ответов) заканчивается в начале цепочки в событии, используемом для ответить с. Событие начального движения не имеет таких событий. Последующий ответ об ущербе имеет событие движения в своем списке.

Кроме того, массив переменной длины используется для хранения нескольких очередей событий. Всякий раз, когда менеджер получает событие, оно добавляется в очередь по индексу в массиве, который соответствует количеству событий в цепочке ответов. Таким образом, начальное событие движения добавляется в очередь в [0], а повреждения, а также события TurnOver добавляются в отдельную очередь в [1], так как они оба были отправлены как ответы на движение.

Когда отправляются ответы на событие повреждения, эти события будут содержать как само событие повреждения, так и движение, помещая их в очередь по индексу [2]. Пока индекс [n] имеет события в своей очереди, эти события будут обрабатываться, прежде чем перейти к [n-1]. Это дает порядок обработки:

Движение -> Урон [1] -> ResponseToDamage [2] -> [2] пусто -> TurnOver [1] -> [1] пусто -> [0] пусто

Aeris130
источник