В этом уроке добавим в нашу игру еду и возможность для главного героя ее собирать, чтобы утолить голод, выведем всю важную информацию на экран и научимся работать с префабами и интерфейсом. По традиции, напомню, что если интересна эта тема и не хочется пропустить продолжение, то подпишитесь на канал и давайте начнём!
Еда для главного героя
Мне кажется, что самым очевидным источником еды в лесу являются грибы и если в реальной жизни они могут быть ядовиты и вызвать даже летальные последствия, то в нашей игре давайте пока ограничимся допущением, что все грибы съедобны и даже в сыром виде. Мы могли бы обойтись без этих объяснений, раскидав по всему лесу яблоки или бургеры, но тогда нам бы пришлось объяснять, почему по лесу разбросана еда. :) А так вроде логично - грибы в лесу растут!
К тому же, среди префабов ёлок и камней, я как раз заметил префаб гриба.
Теперь просто перетащим его на нашу сцену.
Учимся работать с префабами
Давайте распакуем префаб, в окне иерархии кликнув на название объекта нашего гриба с закрашенной иконкой и выбрав Prefab - Unpack Completely.
Теперь давайте переименуем его в просто Mushroom. Создадим в окне проекта папку Prefabs. Сюда мы будем помещать созданные нами префабы. Теперь из окна иерархии просто перетащим наш Mushroom в эту папку. Таким образом мы снова создали префаб и теперь гриб на сцене будет зависеть от настроек этого префаба в нашей папке. Все грибы на сцене, которые будут созданы на основе этого префаба будут иметь общие настройки.
Давайте добавим еще пару грибов на основе нашего нового префаба, перетащив префаб на сцену, либо просто продублировав имеющийся гриб в окне Иерархии с помощью сочетания клавиш CTRL-D. Расставим их так, как нам нравится. Если изменить масштаб в настройках префаба, в папке Prefabs, то все грибы на сцене меняют свой размер.
Теперь все наши грибы стандартизированы и обладают общими свойствами. Если мы захотим какой-то из них сделать побольше, то мы можем изменить настройку scale конкретного экземпляра. Этот гриб сохранит эту индивидуальную настройку и на неё префаб уже не будет влиять. При этом все остальные параметры продолжат зависеть от префаба.
Если мы захотим добавить какой-то компонент на все наши грибы, то для этого достаточно добавить его только на префаб.
Как объекты в Unity взаимодействуют между собой
Время писать код! Давайте создадим новый скрипт Mushroom. И повесим его на префаб нашего гриба. Он соответственно появится и у всех экземпляров. Пропишем в скрипте поле foodValue - это будет ценность еды или другими словами, то сколько процентов сытости она добавит. А еще добавим метод OnTriggerEnter(). Для этого просто начнём писать название метода и VisualStudio сама начнёт предлагать варианты.
Кликнем на нужном нам предложенном методе и нажмём клавишу TAB. Вуаля! Метод пропишется автоматически! А все потому-что это стандартный метод доставшийся нам в наследство от класса MonoBehaviour.
Метод OnTriggerEnter(), как было написано в подсказке - вызывается, когда коллайдер входит в триггер. Минутка новых слов! Коллайдер - это компонент, отвечающий за столкновение объекта с другими объектами. А триггер это его вариация, которая не даёт столкновений, но регистрирует попадание других коллайдеров в свою зону.
Используя эти компоненты мы можем сделать так, что когда коллайдер игрока попадёт в триггер гриба, то мы выполним подбор этого гриба игроком.
Для этого сделаем проверку, что этот коллайдер принадлежит игроку, а то мало ли какие коллайдеры еще будут шастать по нашему лесу.
Компонент Collider висит на игровом объекте, поэтому мы можем легко от компонента обратиться к объекту с помощью обращения gameObject. Для проверки наличия других компонентов на этом объекте лучше использовать метод TryGetComponent(), т.к. он в случае отрицательного результата не даст нам ошибку, а в случае положительного в локальную переменную player запишет ссылку на компонент Player.
Таким образом, если на игровом объекте, чей коллайдер попал в наш триггер, окажется компонент Player, то метод TryGetComponent() вернёт значение true и выполнится код, который мы пропишем в теле оператора if.
Безопасное изменение параметров игрока
С точки зрения безопасности будет правильнее не давать грибу управлять сытостью игрока напрямую, а дать доступ к специальному методу изменения сытости. Мало ли, что какой-то коварный гриб решит сделать с нашей сытостью? Вдруг решит задать ей значение минус 10000 и наш персонаж за свою жизнь уже не сможет почувствовать себя сытым.
Создадим метод изменения сытости в скрипте Game и будем вызывать в скрипте Mushroom.
Метод ChangeFood() в качестве входного параметра будет принимать целое число как положительное, так и отрицательное. После изменения параметра сытости, мы проверим не стало ли значение меньше нуля, чтобы избежать отрицательного значения сытости и если да, то зададим значение 0, как минимально возможное. Если же наш герой решит съесть все грибы в лесу, то его желудок не должен быть бесконечным и наестся больше максимального значения мы ему тоже не дадим.
Теперь если игрок войдёт в триггер гриба, то его сытость увеличится на значение foodValue этого гриба, но не превышая максимальную сытость в 100.
Сам же гриб, во избежание бесконечного источника пропитания, из игры надо убрать. Есть два основных способа. Можно удалить объект с помощью метода Destroy(gameObject), либо отключить его с помощью метода gameObject.SetActive(false). В первом случае объект безвозвратно исчезнет с нашей сцены, а во втором просто станет неактивным и его можно будет как-нибудь потом еще раз использовать. Например "зареспавнить", т.е. возродить.
Воспользуемся вторым вариантом, т.к. создавать объекты на сцене гораздо ресурсозатратнее, чем их включать и выключать.
Почти всё готово осталось добавить на объект гриба триггер, а на игрока коллайдер. Зайдём в настройки префаба гриба, дважды кликнув на него в окне проекта и добавим компонент SphereCollider. Нажмём в окне компонента Edit Collider и увидим зелёные контуры сферы вокруг нашего гриба.
Эту сферу неплохо бы центрировать относительно гриба, поэтому давайте выставим координаты центра (Center) по Y равными 0.2, а радиус сделаем чуть побольше, например 0.7, чтобы гриб было проще собрать.
Чтобы этот коллайдер стал триггером, нам достаточно поставить всего лишь одну галочку возле поля Is Trigger.
На объект игрока же коллайдер вешать не придётся, т.к. на нём висит компонент CharacterController, который в себе уже содержит коллайдер.
Запускаем в режиме Play и проверяем. Ура! Работает! Если подойти достаточно близко к грибу, то он исчезает, а в поле Food в инспекторе мы видим, что сытость увеличилась на 20.
Создаём интерфейс и выводим параметры игрока на экран
Чтобы игрок мог понимать, что наш главный герой проголодался или его жизненные силы иссякают, нужно вывести эту информацию на экран.
UI (User Interface) - это пользовательский интерфейс - всё то, что даёт нам информацию об игре и помогает с игрой взаимодействовать. Индикаторы здоровья, меню игры, кнопки, сообщения - в общем, всё то что мы видим поверх изображения игры. В Unity это отдельный слой с 2D-объектами, который называется Canvas (что переводится как холст).
Предлагаю на этом "холсте" нарисовать индикаторы здоровья и сытости. Для этого сначала создадим сам Canvas. В меню выберем GameObject - UI - Canvas:
В окне иерархии вместе с объектом Canvas появится еще и EventSystem - это специальный объект с компонентами отвечающими за взаимодействие курсора и интерфейса (наведение, клики и т.д.).
Для отображения текста нам понадобится объект TextMeshPro, который можно найти в меню создания объекта UI - Text - TextMeshPro.
Причём, если мы впервые захотим добавит в наш проект объект типа TextMeshPro, то Unity предложит нам сначала установить этот плагин,
В проекте появится Text Mesh Pro и можно сразу создать папку Plugins, чтобы скидывать туда все подобные плагины.
Давайте создадим объекты для интерфейса. Кликнув правой клавишей на Canvas создадим внутри пустой объект, который назовём Indicators и который будет являться "папкой" для других объектов, а внутри него создадим два объекта Text - TextMeshPro и 2 объекта Image. Это будут 2 текстовых блока для отображения значений здоровья и сытости, а также иконки для них в виде изображений (Image). Сразу их переименуем, чтобы понимать что есть что.
Иконки у нас это объекты Image, а текст это объекты TextMeshPro. Давайте перейдём в режим 2D - в нём работать в интерфейсом удобнее. И дважды кликнув на Canvas в окне иерархии, сфокусируем окно сцены на нём.
Настройка интерфейса под разные разрешения экрана
Предлагаю разместить иконки в верхнем правом углу экрана, а текст сдвинуть относительно иконок вправо. Включим отображение вспомогательных элементов (иконка в виде сферы с точками, крайняя справа в окне сцены) - это позволит нам видеть якоря. Якоря (Anchors) - это важная штука в настройке интерфейса - с помощью них можно привязать элементы интерфейса к конкретной части экрана вне зависимости от разрешения экрана и его ориентации. Давайте выберем в настройках родительского объекта Indicators позицию Top-Left (сверху и слева) и она автоматически выставит якоря.
Теперь если в окне Game мы выберем другое разрешение экрана, например с вертикальной ориентацией, то наши иконки останутся на том же месте относительно верхнего угла.
Изменения текста интерфейса из кода
Для взаимодействия нашего кода и интерфейса создадим отдельный скрипт и назовём его UI. Он будет содержать в себе ссылки на текстовые объекты и метод для их изменения. Вот только VisualStudio знать не знает ни про какие TextMeshPro. Не знает, но догадывается.
Если кликнуть во всплывающей подсказке на "показать возможные решения", то первым будет вариант подключить нужную библиотеку с помощью строчки using TMPro.
Кликаем по предложенному решению и VisualStudio сама добавит в наш код нужную строчку. После этого ошибка пропадёт, а мы сможем в инспекторе поставить в качестве значения этих полей наши текстовые блоки. Для этого сам скрипт UI давайте повесим, например, на объект Canvas.
Теперь давайте напишем в скрипте UI методы для изменения этих текстовых блоков - UpdateHealth и UpdateFood. Они будут получать значения и вписывать их в поле text текстового блока. Значения мы будем получать в виде целых чисел (тип int), а поле text имеет тип string, поэтому просто так записать число в текстовое поле не получится. Для этого нужно воспользоваться функцией ToString(), которая превращает числовое значение в текстовые символы.
Теперь давайте в скрипте Game сделаем ссылку на UI и будем вызывать методы обновления интерфейса каждый раз, когда параметры будут изменяться. Для этого давайте создадим метод ChangeHealth() для изменения значения параметра health, по аналогии с методом ChangeFood(), чтобы не менять значения параметров напрямую, как нам вздумается, а только в соответствии с заданными нами правилами:
- Здоровье и сытость не могут быть меньше 0
- Здоровье и сытость не могут быть больше 100 (пока так)
- Каждый раз когда изменяется значение параметров, то и интерфейс должен обновляться
Но давайте еще и изменим наш метод Hunger(), чтобы он не забывал обновлять интерфейс. Вот только раз уж метод Hunger() сам контролирует правильность изменения голода, то чтобы избежать лишних и ненужных проверок, оставим ему возможность менять параметры напрямую и лишь добавим методы обновления интерфейса. Маленькая, но оптимизация! :)
Также давайте пропишем стандартный метод в Start(), который срабатывает один раз при запуске скрипта, чтобы при старте игры наши индикаторы приняли начальные значения. Мы можем их также задать и в настройках текстовых блоков, но когда мы будем делать сохранения, то при их загрузке значения параметров могут быть самые разные и их непременно нужно будет обновить.
Для наглядности в текстовых блоках пропишем значения, например 999, чтобы в режиме редактирования видеть сколько места займёт трёхзначное число.
Теперь не забыв в настройках объекта Game указать ссылку на скрипт UI, который мы повесили на объект Canvas, запустим режим Play.
Работает! "Девятки" превратились в первоначальное значения здоровья и сытости - 100. Сытость начала понемногу менять свое значение в интерфейсе в реальном времени.
Добавляем в интерфейс изображения
Теперь осталось только найти картинки для иконок. Они могут быть в любом формате - например JPEG или PNG. Их можно нарисовать самому, найти в поисковике или скачать набор иконок из UnityAssetStore.
Для иконки здоровья подойдёт изображение "сердечка". Давайте поищем готовый вариант, желательно в формате png, чтобы был без фона.
Ну, а для иконки сытости можно либо изображение еды, либо изображение желудка. Мы можем просто скачать файл картинки, открыть папку нашего проекта в браузере, найти там папку Assets и скопировать его туда. Для картинок давайте создадим папку Images.
Нужно еще обязательно в настройках изображений выбрать Texture Type -> Sprite, чтобы мы могли использовать их для интерфейса.
Теперь в настройках компонента Image, который висит на наших иконках (пока это белые квадраты) можно выбрать изображение и оно автоматически примет размер, который указан в объекте иконки.
Что-то иконка желудка мне не понравилась. :) Пусть лучше в качестве иконки сытости будет изображение гриба. Теперь это всё больше похоже на настоящую игру! Симулятор грибника какой-то! :)
Ну что ж, у нас даже появился простейший игровой цикл. Мы ходим по лесу и чтобы не умереть с голоду, едим грибы. А едим грибы, чтобы продолжать ходить по лесу! :) В игре вырисовывается какой-то смысл. :)
Отлично! Мы научились делать подбираемые предметы, научились работать с префабами, сделали механику восполнения сытости, научились работать с интерфейсом и управлять им из кода. Но на 3 грибах долго не проживешь и еще не понятно умрешь раньше от голода или от скуки! Поэтому в следующей части подумаем, как сделать источник еды восполняемым и как сделать выживание в лесу более увлекательным!
Чтобы не пропустить продолжение, не забудь подписаться на канал, а чтобы поддержать мой труд и придать мне больше мотивации поскорее сделать следующий урок, можно поставить лайк или оставить комментарий. :)
Первый урок из этой серии:
Вся подборка статей из этой серии: