Создание системы бонусов в Unity

Создание системы бонусов в Unity

На что бы была похожа игра Sonic The Hedgehog без золотых колец и скоростных ботинок, Super Mario без грибов или Pac-Man без мигающих точек? Все эти игры стали бы намного скучнее!

Бонусы (power-ups) — это важнейший компонент игрового процесса, потому что они добавляют новые уровни комплексности и стратегии, побуждая игрока к действию.

В этом туториале вы научитесь следующему:

  • Конструировать и собирать систему бонусов с возможностью многократного применения в других играх.
  • Использовать в игре систему связи на основе сообщений.
  • Реализовывать всё это в игре с видом сверху, использующей ваши собственные бонусы!

Для повторения действий туториала вам потребуется Unity 2017.1 или более новой версии, поэтому обновите свою версию Unity, если ещё этого не сделали.

Приступаем к работе

Игра, над которой мы будем работать — это двухмерная аркада с видом сверху, где игрок пытается увернуться от врагов; она немного похожа на Geometry Wars, но без стрельбы (и без коммерческого успеха). Наш герой в шлеме должен уворачиваться от врагов, чтобы добраться до выхода; при столкновении с врагами его здоровье уменьшается. Когда здоровье заканчивается, наступает game over.

Скачайте заготовку проекта для этого туториала и извлеките её в нужную папку. Откройте проект в Unity и изучите папки проекта:

  • Audio: содержит файлы звуковых эффектов игры.
  • Materials: материалы игры.
  • Prefabs: содержит префабы (Prefabs) игры, в том числе игровое поле, игрока, врагов, частицы и бонусы.
  • Scenes: здесь находится основная сцена игры.
  • Scripts: содержит скрипты игры на C# с подробными комментариями. Можете исследовать эти скрипты, если хотите лучше освоиться в них перед началом работы.
  • Textures: исходные изображения, используемые для игры и экрана заставки.

Вы увидите, что в игре пока нет бонусов. Поэтому уровень пройти сложно и игра кажется немного скучноватой. Наша задача — добавить бонусы и оживить игру. Когда игрок подбирает бонус, на экране появляется цитата из известной серии фильмов. Посмотрим, сможете ли вы узнать её. Ответ будет в конце туториала!

Цикл жизни бонуса

У бонуса есть цикл жизни, состоящий из нескольких отдельных состояний:

  • Первая стадия — это создание, которое выполняется в игровом процессе или на этапе разработки, когда вы вручную располагаете GameObject бонусов в сцене.
  • Далее идёт режим привлечения внимания, когда бонусы могут становиться анимированными или иным способом привлекать внимание игрока.
  • Стадия сбора — это действие по подбиранию бонуса, которое вызывает срабатывание звуков, систем частиц или других спецэффектов.
  • Подбор бонуса ведёт к выполнению полезной нагрузки, при которой бонус «делает своё дело». Полезной нагрузкой может быть всё, что угодно, от скромной прибавки здоровья до наделения игрока какими-то потрясающими сверхспособностями. Этап полезной нагрузки также приводит к срабатыванию проверки срока действия. Можно настроить бонус так, чтобы он переставал действовать после определённого времени, после того, как игрока заденет враг, после нескольких применений или после выполнения любого другого условия игрового процесса.
  • Проверка срока действия приводит к стадии завершения. Стадия завершения уничтожает бонус и становится концом цикла.

Создание простого бонуса-звёздочки

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

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

Создайте новый Sprite, назовите его PowerUpStar и расположите прямо над героем в точке (X:6, Y:-1.3). Чтобы сцена была упорядоченной, сделайте спрайт дочерним элементом пустого GameObject PowerUps в сцене:

Теперь зададим внешний вид спрайта. Введите для Transform Scale значения (X:0.7, Y:0.7), в компоненте Sprite Renderer назначьте слоту Sprite звезду, а для Color выберите бледно-коричневый цвет (R:211, G:221, B:200).

Добавьте компонент Box Collider 2D, поставьте флажок Is Trigger и измените Size на (X:0.2, Y:0.2):

Мы только что создали первый бонус! Запустите игру, чтобы убедиться, что всё выглядит хорошо. Бонус появляется, но когда вы пытаетесь его поднять, то ничего не происходит. Чтобы исправить это, нам потребуются скрипты.

Отделяем игровую логику от иерархии классов

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

На схеме выше показан класс PowerUp в качестве родительского класса. Он содержит независящую от игры логику, поэтому мы можем повторно использовать её «как есть» почти в любом другом проекте. В проекте туториала уже есть родительский класс. Родительский класс управляет циклом жизни бонуса, различными состояниями, которые может иметь бонус, обрабатывает коллизии, подбирание бонуса, полезные нагрузки, сообщения и завершение действия бонуса.

Родительский класс реализует простой конечный автомат, отслеживающий цикл жизни бонуса. Нам нужно реализовать подкласс и значения инспектора для каждого нового бонуса, и на этом всё!

Проверочный список кодирования бонусов

Примечание: для создания скрипта бонуса мы должны создать подкласс класса PowerUp и обеспечить выполнение всех пунктов списка. В туториале мы несколько раз будем обращаться к этому списку, так что держите его под рукой!

  1. Реализовать PowerUpPayload , чтобы запустить полезную нагрузку.
  2. Опционально: реализовать PowerUpHasExpired , чтобы убрать полезную нагрузку из предыдущего этапа.
  3. Вызвать PowerUpHasExpired при истечении срока действия бонуса. Если срок действия истекает сразу же, поставьте в инспекторе флажок ExpiresImmediately, потому что в этом случае нет необходимости вызывать PowerUpHasExpired .
Создаём первый скрипт для бонуса

Добавьте новый скрипт к GameObject PowerUpStar, назовите его PowerUpStar и откройте в редакторе.

Добавьте следующий код, замените бОльшую часть начального boilerplate-кода Unity, оставив только конструкции using в начале.

Код довольно короткий, но его достаточно, чтобы реализовать логику звезды! Скрипт соответствует всем пунктам контрольного списка:

  1. PowerUpPayload даёт игроку немного здоровья, вызывая playerBrain.SetHealthAdjustment . Родительский класс PowerUp уже позаботился о получении ссылки на playerBrain . То, что у нас есть родительский класс, означает, что нам придётся вызывать base.PowerUpPayload , чтобы обеспечить выполнение всей базовой логики перед нашим кодом.
  2. Нам не нужно реализовывать PowerUpHasExpired , потому что прибавление здоровья не отменяется.
  3. Срок действия этого бонуса завершается сразу же, поэтому нам снова не нужно ничего писать; достаточно поставить флажок ExpiresImmediately в инспекторе. Настало подходящее время, чтобы сохранить метод и вернуться в Unity для внесения изменений в инспекторе.
Создание первого бонуса в сцене

После сохранения и возврата в Unity GameObject StarPowerUp будет выглядеть в инспекторе следующим образом:

Введите значения инспектора следующим образом:

  • Power Up Name: Star
  • Explanation: Recovered some health…
  • Power Up Quote: (I will become more powerful than you can possibly imagine)
  • Expires Immediately: флажок поставлен
  • Special Effect: перетащите префаб из папки проекта Prefabs/Power Ups/ParticlesCollected
  • Sound Effect: перетащите аудиоклип из папки проекта Audio/power_up_collect_01
  • Health Bonus: 40

Завершив с параметрами PowerUpStar, перетащите его в папку дерева проекта Prefabs/Power Ups, чтобы создать префаб.

Используйте новый префаб, чтобы добавить несколько звёзд в правой части сцены.

Запустите сцену и проведите героя к первому бонусу. Подбор бонуса сопроводят замечательные звуковые эффекты и частицы. Отлично!

Связь на основе сообщений

Следующий создаваемый нами бонус требует фоновой информации. Чтобы получить эту информации, нам нужно, чтобы GameObjects научились обмениваться между собой данными.

Например, когда мы подбираем бонус, UI должен знать, какую информацию нужно отобразить. Когда здоровье игрока меняется, полоска энергии должна знать, каким стал обновлённый уровень здоровья. Это можно реализовать множеством способом, но в руководстве Unity перечислено несколько механизмов.

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

Мы можем сделать GameObjects на передающими сообщения, получающими сообщения, или и тем, и другим:

В левой части представленной выше схемы представлены передатчики сообщений. Можно считать их объектами, «кричащими», когда происходит что-то интересное. Например, если игрок передаёт сообщение «Меня задели». В правой части схемы показаны слушатели сообщений. Как понятно из названия, они слушают сообщения. Слушатели не обязаны слушать все сообщения; они слушают только те сообщения, на которые хотят реагировать.

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

Можно понять, что при наличии множества передатчиков и множества слушателей, между левой и правой сторонами должно быть множество пересекающихся линий. Чтобы упростить нам жизнь, в Unity имеется компонент EventSystem, который располагается посередине:

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

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

Этапы создания связи на основе сообщений

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

  • Передатчики определяют сообщение, которое они хотят передать, как интерфейс C#.
  • Затем передатчики передают сообщения слушателям, хранящимся в списке слушателей.

Если вкратце, то интерфейс определяет сигнатуры метода. Любой класс, реализующий интерфейс, обещает предоставить функционал этим методам.

Более чётко это можно увидеть на примере. Посмотрите на код в файле IPlayerEvents.cs:

У этого интерфейса C# есть методы для OnPlayerHurt и OnPlayerReachedExit . Это сообщения, которые может отправить игрок. Теперь посмотрите на метод SendPlayerHurtMessages в файле PlayerBrain.cs. Строки, помеченные числами в следующем фрагменте кода, описаны ниже:

Представленный выше метод обрабатывает отправку сообщения OnPlayerHurt . Цикл foreach обходит всех слушателей, хранящихся в списке EventSystemListeners.main.listeners и вызывает для каждого слушателя ExecuteEvents.Execute , который отправляет сообщения.

Пройдёмся по комментариям с числами:

  1. EventSystemListeners.main.listeners — это список GameObjects, глобально видимый в объекте синглтона EventSystemListeners . Любой GameObject, который хочет слушать все сообщения, должен находиться в этом списке. Добавлять GameObjects в этот список можно, присваивая GameObject метку Listener в инспекторе или вызывая EventSystemListeners.main.AddListener .
  2. ExecuteEvents.Execute — это предоставляемый Unity метод, отправляющий сообщение GameObject. Тип в угловых скобках — это имя интерфейса, содержащего сообщение, которое мы хотим отправить.
  3. Здесь определяется GameObject, которому нужно отправить сообщение и null для дополнительной информации события в соответствии с примером синтаксиса из руководства Unity.
  4. Лямбда-выражение. Это сложная концепция C#, которую мы не будем рассматривать в этом туториале. Если вкратце, то лямбда-выражение позволяет передавать методу код как параметр. В нашем случае код содержит сообщение, которое мы хотим отправить ( OnPlayerHurt ) вместе с необходимыми ему параметрами ( playerHitPoints ).
  • IPlayerEvents : используется для сообщений, когда игрока задевают или он добирается до выхода.
  • IPowerUpEvents : используется для сообщений, когда подбирается бонус или его действие заканчивается.
  • IMainGameEvents : используется для сообщений, когда игрок побеждает или проигрывает.

Бонус увеличения скорости

Теперь, когда мы знаем о связи на основе сообщений, мы воспользуемся ею и будем слушать сообщение!

Мы создадим бонус, дающий игроку дополнительную скорость до момента, пока он с чем-нибудь не столкнётся. Бонус будет распознавать столкновение игрока «прослушивая» игровой процесс. А конкретнее, бонус будет слушать игрока, передающего сообщение «I am hurt».

Чтобы слушать сообщение, нам нужно выполнить следующие шаги:

  1. Реализовать соответствующий интерфейс C#, чтобы указать, что должен слушать слушающий GameObject.
  2. Сделать так, чтобы сам слушающий GameObjects находился в списке EventSystemListeners.main.listeners .

Добавьте Box Collider 2D и измените его Size на (X:0.2, Y:0.2). В компоненте Sprite Renderer назначьте для Sprite значение fast и измените его цвет так, как мы делали со звездой. Не забудьте также поставить флажок Is Trigger. После этого GameObject должен выглядеть примерно так:

Добавьте этому GameObject новый скрипт PowerUpSpeed и вставьте в скрипт следующий код:

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

  1. PowerUpPayload . Вызывает метод base , чтобы обеспечить вызов родительского класса, затем придаёт игроку ускорение. Заметьте, что родительский класс определяет playerBrain , в котором содержится ссылка на игрока, собравшего бонус.
  2. PowerUpHasExpired. Нам нужно убрать данное игроку ускорение, а затем вызвать метод base .
  3. Последний пункт контрольного списка — вызов PowerUpHasExpired после завершения срока действия бонуса. Позже мы реализуем это с помощью прислушивания к сообщениям игрока.

Примечание: если вы работаете в Visual Studio, то можете навести мышь на элемент IPlayerEvents после его ввода и выбрать опцию меню Implement interface explicitly. При этом создастся заготовка метода.

Добавляйте или изменяйте методы, пока они не будут выглядеть следующим образом, и убедитесь, что они всё ещё являются частью класса PowerUpSpeed:

Метод IPlayerEvents.OnPlayerHurt вызывается каждый раз, когда игрок получает урон. Это часть «прислушивания к вещаемым сообщениям». В этом методе мы сначала делаем так, чтобы бонус реагировал только после подбора. Затем код вызывает PowerUpHasExpired в родительском классе, который будет обрабатывать логику истечения срока действия.

Сохраните этот метод и вернитесь в Unity, чтобы внести нужные изменения в инспекторе.

Создание бонуса увеличения скорости в сцене

GameObject SpeedPowerUp теперь будет выглядеть в инспекторе следующим образом:

Введите в инспекторе следующие значения:

  • Power Up Name: Speed
  • Explanation: Super fast movement until enemy contact
  • Power Up Quote: (Make the Kessel run in less than 12 parsecs)
  • Expires Immediately: снять флажок
  • Special Effect: ParticlesCollected (тот же, что и для звезды)
  • Sound Effect power_up_collect_01 (тот же, что и для звезды)
  • Speed Multiplier: 2

Настроив параметры бонуса Speed нужным вам образом, перетащите его в папку дерева проекта Prefabs/Power Ups, чтобы создать префаб. Мы не будем использовать его в демо-проекте, но для завершённости стоит это сделать.

Запустите сцену и переместите героя, чтобы собрать бонус скорости, после чего он получит дополнительную скорость, которая будет сохраняться до контакта с врагом.

Отталкивающий бонус

Следующий бонус позволяет игроку отталкивать объекты со своего пути нажатием клавиши P; количество его применений ограничено. Вы уже знакомы с этапами создания бонуса, поэтому чтобы не загромождать текст, я рассмотрю только самые интересные части кода, а затем создам префаб. Этому бонусу не нужно слушать никакие сообщения.

Найдите и изучите в иерархии проекта префаб PowerUpPush. Откройте его скрипт под названием PowerUpPush и изучите код.

Вы увидите знакомые методы, которые мы уже рассматривали. Все интересные действия отталкивающего бонуса происходят в методе Update:

Вот, что происходит в этом коде:

  1. Скрипт должен выполняться только для собранных бонусов.
  2. Скрипт должен выполняться, только если осталось количество использований.
  3. Скрипт должен выполняться, только когда игрок нажимает на P.
  4. Скрипт запускает выполнение красивого эффекта частиц вокруг игрока.
  5. Скрипт отталкивает врагов от игрока.
  6. Бонус используется ещё раз.
  7. Если число использований закончилось, то срок действия бонуса заканчивается.

Дополнительное задание: бонус неуязвимости

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

Советы и рекомендации для выполнения этого задания:

  1. Sprite: найдите спрайт в папке проекта Textures/shield
  2. Power Up Name: Invulnerable
  3. Power Up Explanation: You are Invulnerable for a time
  4. Power Up Quote: (Great kid, don't get cocky)
  5. Coding: пройдитесь по тому же контрольному списку, который мы использовали для бонуса-звезды и увеличения скорости. Вам понадобится таймер, контролирующий срок действия бонуса. Метод SetInvulnerability в PlayerBrain будет включать и отключать неуязвимость.
  6. Effects: проект содержит эффект частиц для красивого эффекта пульсации вокруг игрока, пока он остаётся неуязвимым. Префаб находится в Prefabs/Power Ups/ParticlesInvuln. Можно сделать его дочерним экземпляром игрока, пока он неуязвим.

Создайте новый Sprite, назовите его PowerUpInvuln и поместите в (X:-0.76, Y:1.29). Для Scale задайте значения X:0.7, Y:0.7. Этот GameObject не будет никого слушать, срок его действия будет просто истекать после заданного времени, поэтому нет необходимости давать ему метку в инспекторе.

Добавьте Box Collider 2D и измените Size на X = 0.2, Y = 0.2. В компоненте Sprite Renderer назначьте Sprite значение shield и выберите цвет, как мы это раньше делали с другими бонусами. Убедитесь, что флажок Is Trigger поставлен. После этого GameObject должен выглядеть примерно так:

Добавьте к этому GameObject новый скрипт PowerUpInvuln и вставьте в него следующий код:

Снова проверьте пункты контрольного списка кода. Скрипт выполняет эти пункты следующим образом:

📎📎📎📎📎📎📎📎📎📎