Прежде чем закапываться в глубь программирования, давайте научимся сначала создавать основные механики! Например, такую популярную штуку как индикатор здоровья персонажа, известный также как "полоска хп". :) А ещё расскажу про интересную особенность браузерных игр из-за которой наши гениальные задумки могут не работать! Усаживайтесь поудобнее, наливайте себе большую кружку печенек с чаем и поехали в мир игростроения! :)
Индикатор здоровья или полоска ХП
У нас уже в проекте есть цифровой индикатор здоровья, как в легендарном Doom - что еще нужно для счастья? Иногда нужно больше наглядности и вообще может оказаться, что игрок гуманитарий и ему эти наши цифры вообще ни о чём не говорят! :)
Надо очки здоровья (health points, они же hp, они же хп) отобразить визуально! Индикатор здоровья в виде убывающей полоски - решение популярное и всем знакомое - вспомните Warcraft или любую схватку с боссом (не в вашем офисе, конечно - там о здоровье босса можно только догадываться). Давайте изучим опыт наших коллег игроделов из компании Blizzard!
Что мы видим на этом скриншоте? Огромный человек-корова своим топориком, пытается отдубасить агрессивную кошку, у которой над головой серая полоска. Полоска серая видимо потому, что здоровья у пумы осталось настолько мало, что его и не видно. Эта полоска перемещается вместе с кошкой - над её головой. В дальней части кадра видно, что еще одна котейка, с полной красной полоской здоровья, крадётся к какому-то токсичному зелёному козлу.
В кадре есть ещё и другие индикаторы здоровья, статично висящие в левом верхнем углу. Это мы уже понимаем как работает - элементы интерфейса висят на канвасе (объект canvas) всегда в одном и том же месте (если конечно мы не задумаем, что они должны перемещаться).
Итак, что мы имеем? Индикаторы как элемент интерфейса и индикаторы в игровом пространстве, над теми объектами, состояние которых они показывают.
Простой способ сделать индикатор здоровья
Начнём с того, как вообще сделать индикатор в виде полоски. Это довольно просто, если воспользоваться таким готовым элементом UI, как Slider. Давайте его создадим, кликнув на нашем Canvas, или еще лучше на специальном объекте-папке Indicators (это просто пустой объект, который мы сделали родителем для всех индикаторов, либо еще один Canvas, который будет дочерним).
Слайдер - это удобная штука, чтобы легко менять значение какого-либо параметра перетаскиванием его ручки влево-вправо. Посмотрите на любой регулятор громкости, например, в YouTube под видео.
Примерно также слайдер будет выглядеть в Unity, когда мы его только создали. Перетащим его к нашему цифровому индикатору, не забывая о том, что с канвасом и элементами интерфейса мы работаем в режиме 2D.
Слайдер состоит из фоновой полоски, изменяющейся полоски значения и ручки "ТудаСюдашки". Здоровье у нас изменяется из кода, а потому ручку можно сразу удалить. Раскрываем в иерархии объект Slider и безжалостно удаляем Handle Slide Area за ненадобностью.
Чтобы компонент не ругался, что мы его лишили всего нажитого непосильным трудом, а конкретно ручки, выберем в выпадающем меню пункт None.
В настройках компонента есть параметры, которые удобно регулируются такими же слайдерами. Давайте подёргаем параметр Value, чтобы увидеть, как будет изменяться наша будущая полоска здоровья.
Стильно, модно, молодёжно! Но что-то какие-то цвета недостаточно модные, да и полоска тонковата. Давайте всё это настроим. Толщину слайдера изменим параметром Height в компоненте Rect Transform, что на объекте Slider.
Цвет фона настраиваем в объекте Background, в его компоненте Image. А цвет заполнения в найденном внутри объекта Fill Area, объекте Fill, также в компоненте Image.
С помощью инструмента Rect Tool мы можем легко подогнать размеры полосок, так чтобы они соответствовали друг другу. Fill Area и Fill лучше подравнять под Background. По умолчанию они почему-то вкривь и вкось. :)
Изменение полоски здоровья из кода
Теперь давайте запрограммируем изменение этого индикатора вместе с изменением параметра здоровья. Для изменения цифрового индикатора здоровья мы сделали специальный метод в классе UI под названием UpdateHealth(). Туда и добавим логику изменения параметров слайдера.
Во-первых добавим ссылку на Slider и прокинем его в инспекторе. Во-вторых зададим максимальное значение слайдера maxValue равное максимальному значению здоровья персонажа maxHealth. А в-третьих передадим значение здоровья персонажа health в поле value нашего слайдера. Правда наш класс UI знать не знает про существование maxHealth.
Нам нужна либо ссылка на класс Player, который расскажет сколько нынче у игрока максимально может быть здоровья, либо можно передать максимальное здоровье в качестве второго входного параметра. Предлагаю второй вариант.
Красивое! Вот только теперь все классы, что вызывали метод UpdateHealth() начнут неистово ругаться, мол они не в курсе, что там еще какие-то параметры подавать надо. Причём находясь в VisualStudio в классе UI мы этого не узнаем, но перейдя например к редактированию класса Player обнаружим ошибку.
Как исправлять ошибки в коде
Но над классом есть раскрывающийся список ссылок, с помощью которого мы сможем узнать, где ещё мы этим методом пользуемся в нашем проекте. Кликаем на него и во всплывающем окне видим, что этот метод, мы вызываем ещё и из класса Game.
В классе Game тоже будут ошибки, для исправления которых, нужно указать второй параметр при вызове UpdateHealth(). Но если вдруг мы забудем отследить эти ошибки, то Unity всё равно нам покоя не даст и обо всём напомнит в консоли.
Удобно, что можно дважды кликнуть на появившейся ошибке и попасть в нужный скрипт сразу на нужную строчку.
Добавим второй аргумент и ошибка пропадёт.
Протестируем, не забыв прокинуть ссылку на слайдер в инспекторе и увидим, что всё работает. Полоска здоровья меняется вместе с количеством здоровья.
Составной индикатор здоровья
Теперь вопрос нужно ли нам 2 индикатора рядом - цифровой и визуальный? Можно конечно и так оставить, если нужно, а можно воспользоваться опытом всё той же малоизвестной, начинающей компании Blizzard и поместить текст количества здоровья внутрь индикатора.
Для этого давайте сделаем полоску здоровья потолще, а текстовый объект HP Text перетащим в объект Slider, сделав его дочерним. А еще давайте в настройках текстового компонента Text Mesh Pro, в самом его низу поставим галочку возле Outline, чтобы сделать обводку текста. С помощью настроек Dilate и Thickness можно её поднастроить так, как нам нравится.
Поскольку при обновлении индикатора здоровья мы в качестве второго параметра передаём максимальное здоровье, то и его можно вывести, также как и в Варкрафте. Вот в таком виде.
Чтобы выводить здоровье в таком формате, давайте доработаем наш метод UpdateHealth().
Вот как это будет выглядеть в игре.
Настройка и смена шрифтов в Unity
Поскольку по умолчанию у нас на всех шрифтах используется один и тот же шаблон шрифтов Font Asset и один файл настроек материала Material Preset, то все текстовые надписи в игре теперь с чёрной обводкой. При желании мы можем в настройках компонента текста найти этот Font Asset или Material Preset, кликнуть по одному из них и увидеть эти файлы в проекте.
Настройки шрифта хранятся в шаблоне шрифта Font Asset. Мы можем, продублировать этот файл и сделать один с нашей обводкой, а второй без.
В шаблонах шрифтов естественно можно выбрать не только настройки, но и поменять сами шрифты скачанные из интернета, используя любые стандартные форматы, типа .ttf. Для этого надо кликнуть на сам файл шаблона и найти там строчку Source Font File.
Однако в Unity шрифты не берутся напрямую из исходного файла. Для них нужно сгенерировать так называемый "атлас". Просто выбрав другой Source Font File мы ничего не добьёмся. Но и нажав Update Atlas Texture, мы скорее всего получим не совсем то, что хотели. Не знаю почему, но русские буквы в некоторых версиях Unity упорно не хотят генерироваться. Но решение есть!
Рабочее решение, которым я сам пользуюсь намного проще. Находим в окне проекта наш шрифт и жмём на него правой кнопкой мыши, выбирая Create - TextMeshPro - Font Asset. Либо нажать сочетание клавиш SHIFT + CTRL + F12. Шаблон шрифта создаётся автоматически и со всеми русскими буквами. Нам остаётся его только выбрать в настройках нужного нам текстового блока.
Индикатор здоровья над головой персонажа
Чтобы сделать отображение каких либо интерфейсов в игровом пространстве нам понадобятся дополнительные объекты типа Canvas. Создадим один такой в объекте нашего волка. Выберем в его параметре Render Mode значение World Space, а также обнулим его координаты и Rotation.
Давайте продублируем полоску здоровья игрока и перетащим её на канвас волка. Также обнулим его координаты, вращение и масштаб (scale).
Масштаб её всё равно великоват. Поэтому scale самого канваса я обычно уменьшаю до 0.01, а нужную ширину и высоту канваса выставляю на глаз, так чтобы её хоть было видно. Например, 500 на 300.
Вот только индикатор развёрнут в противоположную от камеры сторону, как и сам канвас. Мы могли бы его развернуть вручную, но вот не задача этот неугомонный волк всё время будет вертеться туда-сюда, а с ним и индикатор здоровья.
Нужно сделать так, чтобы холст всё время был развёрнут в сторону камеры.
Постоянный поворот элементов интерфейса, находящихся в пространстве, к камере
Давайте создадим специальный скрипт LookAtCamera, в котором специально обученный метод LookAt() из класса Transform будет хитрым образом вычислять направление поворота к нашей камере. Для этого само собой нам потребуется ссылка компонент Transform нашей камеры.
Этот скрипт надо повесить на канвас и прокинуть ссылку. Теперь совсем другое дело!
Изменения значений индикатора здоровья врагов из кода
Осталось только сделать так, чтобы при изменении здоровья волка, индикатор менял свои значения. Мы можем использовать наши наработки и взять скрипт, написанный для игрока. Скопируем его в класс Enemy и увидим пару ошибок. Во-первых в классе Enemy у нас нет подключенного пространства имён UI, а вторых нет поля для ссылки на текстовый элемент отображающий здоровье.
Наш враг знать не знает ни про какой UI. Для решения этой проблемы наводим курсор на Slider и кликаем на "Показать возможные решения", среди которых будет вариант добавить using UnityEngine.UI.
Выбираем этот вариант и проблема решена. А для решения второй проблемы просто прописываем поле типа TMP_Text. Тут тоже потребуется добавление еще одного пространства имён.
Сейчас будет немного сложно. :) Снова Объектно-Ориентированное Программирование (ООП). Напомню, что мы для общего развития в прошлых уроках попробовали использовать наследование. Класс врага Enemy у нас наследует функционал AnimalMove, а тот наследует функционал класса Unit.
Здоровье врагов, как и здоровье всех объектов класса Unit, мы меняем в виртуальном методе ChangeHealth(), поэтому в классе Enemy давайте его переопределим и к базовой логике добавим обновление индикатора. Ну и в методе Start() индикатор тоже будем обновлять, что в начале игры, он принял нужные стартовые значения.
Прокидываем ссылки, тестируем и радуемся тому, что теперь можем следить за изменением здоровья нашего врага. :)
В браузерных играх не работает await Task.Delay()
В завершении статьи маленькое, но важное открытие. После того, как мы соберём билд игра и запустим её в браузере, мы увидим, что многое у нас не работает, или работает как-то странно. А всё потому-что технология браузерных игр WebGL не дружит почему-то именно с оператором await Task.Delay(). На всех других платформах всё будет работать как надо, а тут нет.
Например, метод движения нашего волка выглядит так и обычно прекрасно работает.
Но в браузерном варианте волк никуда не пойдёт. Всё застопорится на строчке await Task.Delay(500). Вместо ожидания 500 миллисекунд, непонятно, что вообще происходит. Какое-то вечное ожидание. Да и такое впечатление, что иногда даже работает, а иногда нет.
В видео ещё одного англоязычного блогера я узнал, что можно эту проблему решить, если использовать вместо Task.Delay(), оператор Task.Yield(). Он работает в данной ситуации, как ожидание следующего кадра.
А раз у нас есть ожидание следующего кадра, то мы можем использовать время между кадрами Time.deltaTime, чтобы получить время. Вот таким нехитрым способом, с помощью цикла for с переменной типа float, суммируем время между кадрами, пока оно не станет больше, чем пол секунды, т.е. нужные нам 500 миллисекунд.
Респект и уважуха всем, кто дочитал до конца! :) Надеюсь, что статья была для вас полезна, а если так, то буду рад вашим подпискам, лайкам и комментариям.
Вот полная подборка статей по разработке игр на Unity и еще подборка статей про C# для Unity.