Найти в Дзене
Сергей Эланд

Индикатор здоровья, шрифты и браузерные нюансы. Создание игры на Unity и изучение C# (Часть 16)

Прежде чем закапываться в глубь программирования, давайте научимся сначала создавать основные механики! Например, такую популярную штуку как индикатор здоровья персонажа, известный также как "полоска хп". :) А ещё расскажу про интересную особенность браузерных игр из-за которой наши гениальные задумки могут не работать! Усаживайтесь поудобнее, наливайте себе большую кружку печенек с чаем и поехали в мир игростроения! :) Индикатор здоровья или полоска ХП У нас уже в проекте есть цифровой индикатор здоровья, как в легендарном Doom - что еще нужно для счастья? Иногда нужно больше наглядности и вообще может оказаться, что игрок гуманитарий и ему эти наши цифры вообще ни о чём не говорят! :) Надо очки здоровья (health points, они же hp, они же хп) отобразить визуально! Индикатор здоровья в виде убывающей полоски - решение популярное и всем знакомое - вспомните Warcraft или любую схватку с боссом (не в вашем офисе, конечно - там о здоровье босса можно только догадываться). Давайте изучим оп
Оглавление

Прежде чем закапываться в глубь программирования, давайте научимся сначала создавать основные механики! Например, такую популярную штуку как индикатор здоровья персонажа, известный также как "полоска хп". :) А ещё расскажу про интересную особенность браузерных игр из-за которой наши гениальные задумки могут не работать! Усаживайтесь поудобнее, наливайте себе большую кружку печенек с чаем и поехали в мир игростроения! :)

Индикатор здоровья или полоска ХП

У нас уже в проекте есть цифровой индикатор здоровья, как в легендарном Doom - что еще нужно для счастья? Иногда нужно больше наглядности и вообще может оказаться, что игрок гуманитарий и ему эти наши цифры вообще ни о чём не говорят! :)

Сто "сердечков" игроками может быть понято поразному!
Сто "сердечков" игроками может быть понято поразному!

Надо очки здоровья (health points, они же hp, они же хп) отобразить визуально! Индикатор здоровья в виде убывающей полоски - решение популярное и всем знакомое - вспомните Warcraft или любую схватку с боссом (не в вашем офисе, конечно - там о здоровье босса можно только догадываться). Давайте изучим опыт наших коллег игроделов из компании Blizzard!

В игре World of Warcraft каких только индикаторов нет и всё ради удобства игрока!
В игре World of Warcraft каких только индикаторов нет и всё ради удобства игрока!

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

В кадре есть ещё и другие индикаторы здоровья, статично висящие в левом верхнем углу. Это мы уже понимаем как работает - элементы интерфейса висят на канвасе (объект canvas) всегда в одном и том же месте (если конечно мы не задумаем, что они должны перемещаться).

Итак, что мы имеем? Индикаторы как элемент интерфейса и индикаторы в игровом пространстве, над теми объектами, состояние которых они показывают.

Простой способ сделать индикатор здоровья

Начнём с того, как вообще сделать индикатор в виде полоски. Это довольно просто, если воспользоваться таким готовым элементом UI, как Slider. Давайте его создадим, кликнув на нашем Canvas, или еще лучше на специальном объекте-папке Indicators (это просто пустой объект, который мы сделали родителем для всех индикаторов, либо еще один Canvas, который будет дочерним).

Чтобы создать слайдер, жмём правой кнопкой на любом объекте канваса, выбираем в появившемся меню UI, а там находим заветный пункт Slider
Чтобы создать слайдер, жмём правой кнопкой на любом объекте канваса, выбираем в появившемся меню UI, а там находим заветный пункт Slider

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

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

Примерно также слайдер будет выглядеть в Unity, когда мы его только создали. Перетащим его к нашему цифровому индикатору, не забывая о том, что с канвасом и элементами интерфейса мы работаем в режиме 2D.

У компонента Slider много настроек, но не пугайтесь, нам понадобятся далеко не все. :)
У компонента Slider много настроек, но не пугайтесь, нам понадобятся далеко не все. :)

Слайдер состоит из фоновой полоски, изменяющейся полоски значения и ручки "ТудаСюдашки". Здоровье у нас изменяется из кода, а потому ручку можно сразу удалить. Раскрываем в иерархии объект Slider и безжалостно удаляем Handle Slide Area за ненадобностью.

В этом объекте настроена зона перемещения ручки и внутрь вожен сам объект ручки.
В этом объекте настроена зона перемещения ручки и внутрь вожен сам объект ручки.

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

-7

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

По умолчанию значение value изменяется от 0 до 1, что соответствует полному исчезновению и полному заполнению белой полоски.
По умолчанию значение value изменяется от 0 до 1, что соответствует полному исчезновению и полному заполнению белой полоски.

Стильно, модно, молодёжно! Но что-то какие-то цвета недостаточно модные, да и полоска тонковата. Давайте всё это настроим. Толщину слайдера изменим параметром Height в компоненте Rect Transform, что на объекте Slider.

Все дочерние объекты (Background, Fill Area) настроены так, что принимают размеры родительского объекта Slider, поэтому просто регулируем его размеры.
Все дочерние объекты (Background, Fill Area) настроены так, что принимают размеры родительского объекта Slider, поэтому просто регулируем его размеры.

Цвет фона настраиваем в объекте Background, в его компоненте Image. А цвет заполнения в найденном внутри объекта Fill Area, объекте Fill, также в компоненте Image.

Классический зелёный индикатор здоровья. :)
Классический зелёный индикатор здоровья. :)

С помощью инструмента Rect Tool мы можем легко подогнать размеры полосок, так чтобы они соответствовали друг другу. Fill Area и Fill лучше подравнять под Background. По умолчанию они почему-то вкривь и вкось. :)

-11

Изменение полоски здоровья из кода

Теперь давайте запрограммируем изменение этого индикатора вместе с изменением параметра здоровья. Для изменения цифрового индикатора здоровья мы сделали специальный метод в классе UI под названием UpdateHealth(). Туда и добавим логику изменения параметров слайдера.

Во-первых добавим ссылку на Slider и прокинем его в инспекторе. Во-вторых зададим максимальное значение слайдера maxValue равное максимальному значению здоровья персонажа maxHealth. А в-третьих передадим значение здоровья персонажа health в поле value нашего слайдера. Правда наш класс UI знать не знает про существование maxHealth.

Если написать неизвестное классу имя переменной, то он конечно же ничего не поймёт и будет грязно ругаться.
Если написать неизвестное классу имя переменной, то он конечно же ничего не поймёт и будет грязно ругаться.

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

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

Красивое! Вот только теперь все классы, что вызывали метод UpdateHealth() начнут неистово ругаться, мол они не в курсе, что там еще какие-то параметры подавать надо. Причём находясь в VisualStudio в классе UI мы этого не узнаем, но перейдя например к редактированию класса Player обнаружим ошибку.

Как исправлять ошибки в коде

Внизу окна Visual Studio отображаются ошибки редактируемого класса, но не всего проекта.
Внизу окна Visual Studio отображаются ошибки редактируемого класса, но не всего проекта.

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

Ссылок из класса на класс, многовато, но пока приемлемо :)
Ссылок из класса на класс, многовато, но пока приемлемо :)

В классе Game тоже будут ошибки, для исправления которых, нужно указать второй параметр при вызове UpdateHealth(). Но если вдруг мы забудем отследить эти ошибки, то Unity всё равно нам покоя не даст и обо всём напомнит в консоли.

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

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

Двойной клик на ошибке в консоли Unity сразу перекидывает нас в VisualStudio, причём в нужный класс и на строчку, на которой обнаружена эта ошибка!
Двойной клик на ошибке в консоли Unity сразу перекидывает нас в VisualStudio, причём в нужный класс и на строчку, на которой обнаружена эта ошибка!

Добавим второй аргумент и ошибка пропадёт.

Не так уж и много ссылок :)
Не так уж и много ссылок :)
Тут уже через две ссылки game и ui - ссылка на ссылку :) Но это пока не криминально. :)
Тут уже через две ссылки game и ui - ссылка на ссылку :) Но это пока не криминально. :)

Протестируем, не забыв прокинуть ссылку на слайдер в инспекторе и увидим, что всё работает. Полоска здоровья меняется вместе с количеством здоровья.

-20

Составной индикатор здоровья

Теперь вопрос нужно ли нам 2 индикатора рядом - цифровой и визуальный? Можно конечно и так оставить, если нужно, а можно воспользоваться опытом всё той же малоизвестной, начинающей компании Blizzard и поместить текст количества здоровья внутрь индикатора.

Для этого давайте сделаем полоску здоровья потолще, а текстовый объект HP Text перетащим в объект Slider, сделав его дочерним. А еще давайте в настройках текстового компонента Text Mesh Pro, в самом его низу поставим галочку возле Outline, чтобы сделать обводку текста. С помощью настроек Dilate и Thickness можно её поднастроить так, как нам нравится.

Компонент Text Mesh Pro даёт много полезных настроек для текста.
Компонент Text Mesh Pro даёт много полезных настроек для текста.

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

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

Чтобы выводить здоровье в таком формате, давайте доработаем наш метод UpdateHealth().

Если мы складываем числовые значения со строковыми, то числовые автоматически преобразуются в строки. В кавычках у нас автоматически значение типа string.
Если мы складываем числовые значения со строковыми, то числовые автоматически преобразуются в строки. В кавычках у нас автоматически значение типа string.

Вот как это будет выглядеть в игре.

Теперь наш индикатор здоровья стал одновременно и более наглядным и более информативным.
Теперь наш индикатор здоровья стал одновременно и более наглядным и более информативным.

Настройка и смена шрифтов в Unity

Поскольку по умолчанию у нас на всех шрифтах используется один и тот же шаблон шрифтов Font Asset и один файл настроек материала Material Preset, то все текстовые надписи в игре теперь с чёрной обводкой. При желании мы можем в настройках компонента текста найти этот Font Asset или Material Preset, кликнуть по одному из них и увидеть эти файлы в проекте.

Файлы шаблонов шрифтов для компонентов TextMeshPro и файлы настроек материалов к ним.
Файлы шаблонов шрифтов для компонентов TextMeshPro и файлы настроек материалов к ним.

Настройки шрифта хранятся в шаблоне шрифта Font Asset. Мы можем, продублировать этот файл и сделать один с нашей обводкой, а второй без.

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

В шаблонах шрифтов естественно можно выбрать не только настройки, но и поменять сами шрифты скачанные из интернета, используя любые стандартные форматы, типа .ttf. Для этого надо кликнуть на сам файл шаблона и найти там строчку Source Font File.

Однако в Unity шрифты не берутся напрямую из исходного файла. Для них нужно сгенерировать так называемый "атлас". Просто выбрав другой Source Font File мы ничего не добьёмся. Но и нажав Update Atlas Texture, мы скорее всего получим не совсем то, что хотели. Не знаю почему, но русские буквы в некоторых версиях Unity упорно не хотят генерироваться. Но решение есть!

Смена файлов шрифтов происходит не в настройках Text Mesh Pro, а в настройках шаблонов для него (Font Asset).
Смена файлов шрифтов происходит не в настройках Text Mesh Pro, а в настройках шаблонов для него (Font Asset).

Рабочее решение, которым я сам пользуюсь намного проще. Находим в окне проекта наш шрифт и жмём на него правой кнопкой мыши, выбирая 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, а вторых нет поля для ссылки на текстовый элемент отображающий здоровье.

Класс Enemy пока не знает, о существовании слайдеров и hpText тоже для него непонятный набор букв
Класс Enemy пока не знает, о существовании слайдеров и hpText тоже для него непонятный набор букв

Наш враг знать не знает ни про какой UI. Для решения этой проблемы наводим курсор на Slider и кликаем на "Показать возможные решения", среди которых будет вариант добавить using UnityEngine.UI.

Чтобы работать со слайдерами нужно подключить библиотеку UI
Чтобы работать со слайдерами нужно подключить библиотеку UI

Выбираем этот вариант и проблема решена. А для решения второй проблемы просто прописываем поле типа TMP_Text. Тут тоже потребуется добавление еще одного пространства имён.

Чтобы работать с объектами TextMeshPro нужно подключить библиотеку TMPro
Чтобы работать с объектами TextMeshPro нужно подключить библиотеку TMPro

Сейчас будет немного сложно. :) Снова Объектно-Ориентированное Программирование (ООП). Напомню, что мы для общего развития в прошлых уроках попробовали использовать наследование. Класс врага Enemy у нас наследует функционал AnimalMove, а тот наследует функционал класса Unit.

Здоровье врагов, как и здоровье всех объектов класса Unit, мы меняем в виртуальном методе ChangeHealth(), поэтому в классе Enemy давайте его переопределим и к базовой логике добавим обновление индикатора. Ну и в методе Start() индикатор тоже будем обновлять, что в начале игры, он принял нужные стартовые значения.

При каждом изменении здоровья врага, его индикатор тоже будет обновляться.
При каждом изменении здоровья врага, его индикатор тоже будет обновляться.

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

Теперь мы можем даже узнать сколько у врага всего было здоровья.
Теперь мы можем даже узнать сколько у врага всего было здоровья.

В браузерных играх не работает await Task.Delay()

В завершении статьи маленькое, но важное открытие. После того, как мы соберём билд игра и запустим её в браузере, мы увидим, что многое у нас не работает, или работает как-то странно. А всё потому-что технология браузерных игр WebGL не дружит почему-то именно с оператором await Task.Delay(). На всех других платформах всё будет работать как надо, а тут нет.

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

-40

Но в браузерном варианте волк никуда не пойдёт. Всё застопорится на строчке await Task.Delay(500). Вместо ожидания 500 миллисекунд, непонятно, что вообще происходит. Какое-то вечное ожидание. Да и такое впечатление, что иногда даже работает, а иногда нет.

В видео ещё одного англоязычного блогера я узнал, что можно эту проблему решить, если использовать вместо Task.Delay(), оператор Task.Yield(). Он работает в данной ситуации, как ожидание следующего кадра.

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

Это решение как-то само пришло ко мне - наверное потому-что оно довольно очевидное. :) Главное было узнать какие ожидания в браузере работают, а как нет. :)
Это решение как-то само пришло ко мне - наверное потому-что оно довольно очевидное. :) Главное было узнать какие ожидания в браузере работают, а как нет. :)

Респект и уважуха всем, кто дочитал до конца! :) Надеюсь, что статья была для вас полезна, а если так, то буду рад вашим подпискам, лайкам и комментариям.

Вот полная подборка статей по разработке игр на Unity и еще подборка статей про C# для Unity.

Учимся делать игры и программировать на C# | Сергей Эланд | Дзен
Основы языка С# для создания игр на Unity | Сергей Эланд | Дзен
-42