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

Делаем свою игру на Unity и изучаем язык программирования C# (Часть 7)

Создадим систему появления грибов в случайных местах и придумаем разные виды грибов. Бегать по всему лесу и сажать грибы для нашего героя, дело хлопотное и неблагодарное - пускай матушка-природа сама занимается воспроизведением собственных ресурсов, ну а мы ей в этом поможем! Не забудьте подписаться на канал и вперёд в сказку! Водку купим по дороге! Создаём список игровых объектов Списки и массивы - это очень нужные элементы в программировании. Хороший программист - это ленивый программист, он не будет заниматься каждым объектом отдельно, а объединит всё в какие-нибудь группы и сделает так, чтобы эта группа обрабатывалась целиком и желательно автоматически, вообще без его участия. Вы уже возможно догадываетесь на что я намекаю) Если мы хотим, чтоб в нашей игре было много грибов, они появлялись в случайных местах и будучи съеденными, со временем опять где-то вырастали, то нам нужен их список! Давайте создадим скрипт ItemHandler, который будет отвечать за появление грибов и вообще их вся
Оглавление

Создадим систему появления грибов в случайных местах и придумаем разные виды грибов. Бегать по всему лесу и сажать грибы для нашего героя, дело хлопотное и неблагодарное - пускай матушка-природа сама занимается воспроизведением собственных ресурсов, ну а мы ей в этом поможем! Не забудьте подписаться на канал и вперёд в сказку! Водку купим по дороге!

Создаём список игровых объектов

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

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

Давайте создадим скрипт ItemHandler, который будет отвечать за появление грибов и вообще их всячески контролировать. Объявим в нём список объектов типа Mushroom (List<Mushroom>), под очень говорящим названием mushrooms.

Наконец-то нам стала нужна стандартная библиотек System.Collections.Generic, которая как раз и содержит в себе логику работы списков.
Наконец-то нам стала нужна стандартная библиотек System.Collections.Generic, которая как раз и содержит в себе логику работы списков.

Давайте специально для скрипта ItemHandler создадим отдельный пустой объект внутри объекта Game и повесим его туда. Поскольку мы сделали список публичным, то мы можем видеть его в инспекторе. Если тыкнуть на стрелочку рядом с названием списка в инспекторе, то он раскроется и мы увидим все "ноль" грибов, которые в нём содержаться. Ну то есть ничего не увидим, т.к. изначально он пустой.

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

Поскольку в треугольных скобках при объявлении списка мы указали тип Mushroom, то ничего кроме экземпляров "грибного" скрипта в этом списке хранить не получится. Но грибов сколько угодно! Давайте перетащим из окна иерархии грибы прямо на название списка в инспекторе. По одному. Если хотим перетащить все грибы сразу, то прежде чем их выделить с помощью клик+CTRL, нам нужно заблокировать окно инспектора с помощью иконки замка. И не забыть потом разблокировать, а то окно инспектора перестанет меняться и это может вызвать замешательство и даже панику! :)

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

Теперь когда все наши грибы помещены в список мы можем обращаться к ним через этот список по номерам элементов: mushrooms[0], mushrooms[1], mushrooms[2]. Очень удобно! Если не считать, что счёт идёт с нуля - к этому надо привыкнуть.

Появление грибов в случайных местах

Чтобы грибы в начале игры появлялись в случайных местах нам понадобится полезная функция Random.Range(). В качестве аргументов этой функции мы указываем диапазон в виде 2 чисел через запятую и каждый раз будем получать случайное число из этого диапазона. Зададим каждую координату позиции гриба с помощью рандома, а чтобы не делать это для каждого гриба отдельно нам понадобится наш список mushrooms и цикл for. Создадим метод SpawnMushrooms(), который будет рандомно расставлять грибы в начале игры.

Дробные числа типа float указываются с помощью буквы f. В нашем случае это нужно для того, чтобы функция Random.Range выдавала нам дробные числа тоже. Поэтому хотя бы одна граница интервала должна быть с f.
Дробные числа типа float указываются с помощью буквы f. В нашем случае это нужно для того, чтобы функция Random.Range выдавала нам дробные числа тоже. Поэтому хотя бы одна граница интервала должна быть с f.

Цикл for будет выполнять всё, что мы в него пропишем, для каждого значения i, которое меньше, чем количество элементов в нашем списке mushrooms. Значение i должно быть меньше, чем количество элементов потому-что счёт в списке идёт от 0 и третьего элемента там не будет, хотя количество элементов 3.

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

Теперь пропишем вызов нашего нового метода в методе Start() и давайте проверим, как оно работает.

Внутри метода можно объявлять локальные переменные, которые будут существовать только пока метод не выполнится до конца. Внутри же цикла эти локальные переменные вообще существуют для каждого элемента i отдельно.
Внутри метода можно объявлять локальные переменные, которые будут существовать только пока метод не выполнится до конца. Внутри же цикла эти локальные переменные вообще существуют для каждого элемента i отдельно.

Запускаем режим Play и чтобы долго грибы не искать, переходим в окно сцены, выделяем все грибы в иерархии, отдаляем камеру и сразу видим где они все находятся. Работает - грибы появились в случайных местах!

В окне сцены можно свободно перемещать камеру с помощью WSAD и поворачивать её мышью с зажатой правой клавишей.
В окне сцены можно свободно перемещать камеру с помощью WSAD и поворачивать её мышью с зажатой правой клавишей.

Автоматическое появление новых объектов

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

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

Создадим в скрипе ItemHandler асинхронный метод для респауна, который будет еще и таймером. Для этого перед методом нужно написать async, а внутри использовать хотя бы одну асинхронную функцию, например await Task.Delay(). И снова VisualStudio не понимает, что мы от него хотим, но догадливо предлагает в качестве возможного решения библиотеку System.Threading.Tasks. Берём! Кто мы такие, чтоб отказываться! :)

Для работы с асинхронными методами нам нужна специальная библиотека.
Для работы с асинхронными методами нам нужна специальная библиотека.

Добавим переменную respawnTime, а в методе пропишем её в качестве аргумента для асинхронного метода задержки Task.Delay(). После того как время задержки истечёт, включаем объект гриба.

Для удобства и наглядности методы можно сворачивать и разворачивать нажав на "-" или "+"
Для удобства и наглядности методы можно сворачивать и разворачивать нажав на "-" или "+"

Остаётся добавить перемещение гриба на новое случайное место. У нас уже есть логика случайного размещения грибов в начале игры - давайте ей воспользуемся и вынесем её в отдельный метод RandomPosition(), который на вход будет получать ссылку на гриб, который нужно переместить. А еще надо учесть, что Task.Delay() на вход принимает время в миллисекундах, поэтому respawnTime надо умножить на 1000, иначе грибы будут появляться практически моментально.

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

Ну, а в методе RespawnTimer() вызовем метод RandomPosition для собранного гриба сразу как только асинхронная задержка равная respawnTime закончится. Теперь осталось только в скрипте самого гриба получить ссылку на ItemHandler и вместо отключения гриба вызвать RespawnTimer, который его и отключит, и переместит, и включит. Для начала сделаем ссылку на ItemHandler в скрипте Game.

Мы могли бы сделать ссылку и в скрипте Player, но по логике не игрок должен управлять появлением предметов, а сама игра
Мы могли бы сделать ссылку и в скрипте Player, но по логике не игрок должен управлять появлением предметов, а сама игра

Теперь учитывая, что в скрипте Mushroom мы в момент взаимодействия с игроком, получаем на него ссылку, а сам игрок уже имеет ссылку на скрипт Game, то мы можем из скрипта Mushroom обратится к нужному методу через вот такую цепочку ссылок: player.game.itemHandler.RespawnTimer()

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

В момент поедания гриба, скрипт запустит таймер для его нового появления
В момент поедания гриба, скрипт запустит таймер для его нового появления

Не забывая в инспекторе проставить ссылку скрипта Game на скрипт ItemHandler, запускаем и проверяем. Если все ссылки везде указаны, то все заработает и грибы на 5 секунд будут выключаться и появляться в другом месте. Теперь наш главный герой сможет практически бесконечно ходить по лесу и искать новые (или хорошо забытые старые) грибы.

Разные версии одного вида объектов

Как я и обещал, давайте сделаем разные виды грибов, дабы разнообразить флору нашего сказочного леса и сделать выживание главного героя более интересным. Предлагаю сделать ядовитый гриб и МегаГриб-3000 Дефинитив Эдишн Плюс. Второй для удобства будем называть МегаГриб и это будет отсылка к крутой аптечке из Quake 2 под названием MegaHealth.

Применим 2 разных подхода для их создания. Ядовитость сделаем в качестве еще одного параметра гриба, а МегаСвойства мы сделаем уникальными для конкретного вида гриба.

Ядовитый гриб

Ядовитость как свойство сделаем присущей всем грибам. У обычных грибов значение poison сделаем 0, а у ядовитых например 20. При съедании ядовитого гриба будем вычитать из здоровья персонажа значение poison.

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

Зайдём в нашу папку префабов и создадим дубликат имеющегося префаба гриба с помощью CTRL+D. Два раза щелкнем на него, чтобы отредактировать. Назовём его PoisonedMushroom. Пропишем ему значение poison 20. А в настройках материала шляпки гриба сделаем цвет более красным.

Ура! Наконец-то снова немного творчества и можно поиграть с цветами!
Ура! Наконец-то снова немного творчества и можно поиграть с цветами!

Только вот незадача - обычные грибы тоже поменяли цвет. Это потому, что они используют один и тот же материал, а мы просто меняем его настройки. Давайте найдем на грибе компонент MeshRenderer, в нём найдём список материалов Materials и тыкнем на первый из них. Это материал шляпки и он отобразился в окне проекта. Давайте сделаем два его дубликата - один для ядовитого гриба, другой для МегаГриба.

В данном случае это простейшие материалы в которых нет текстур, а есть только цвет
В данном случае это простейшие материалы в которых нет текстур, а есть только цвет

Теперь давайте выберем цвета для разных грибов какие захотим. Я пожалуй обычные грибы оставлю коричневыми, ядовитые сделаю красными, а МегаГриб синим. Для удобства я создам в нашей папке Textures папку для материалов грибов под названием Mushrooms и перетащу их туда.

Все используемые нами материалы лучше держать в отдельной папке Materials или Textures
Все используемые нами материалы лучше держать в отдельной папке Materials или Textures

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

В общем-то ядовитый гриб готов - перетаскиваем его на сцену и проверяем.

Ядовитый гриб коварно маскируется под обычный!
Ядовитый гриб коварно маскируется под обычный!

Работает. Съедаем грибы и здоровье уменьшается на 20. Но если сразу выключить режим Play, то мы получаем ошибку.

MissingReference - это одна из самых частых ошибок. Либо ссылки не проставили, либо объекты к которым хоти обратится уже удалены
MissingReference - это одна из самых частых ошибок. Либо ссылки не проставили, либо объекты к которым хоти обратится уже удалены

Она возникает потому-что асинхронный метод работает даже после выключения игры и пытается заспавнить гриб даже когда игра уже выключена. Чтобы этого не было давайте добавим проверку, что объект не равен null (т.е. существует и не был уничтожен).

Проверка на null вообще очень важная вещь и встретится нам еще не раз
Проверка на null вообще очень важная вещь и встретится нам еще не раз

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

МегаГриб и наследование логики

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

Добавим в скрипте Game новую переменную maxHealth и подправим метод ChangeHealth() с учётом переменного значения максимального здоровья.

Теперь здоровье сможет изменятся от 0 и до значения maxHealth
Теперь здоровье сможет изменятся от 0 и до значения maxHealth

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

Добавим условия на перспективу - пусть максимальное значение здоровья может и уменьшаться, но максимум до 50.
Добавим условия на перспективу - пусть максимальное значение здоровья может и уменьшаться, но максимум до 50.

Теперь самое интересно! Создадим новый скрипт MegaMushroom и сделаем ему наследование от Mushroom.

Наследование выглядит вот так
Наследование выглядит вот так

Новый скрипт MegaMushroom хоть и выглядит пустым, но содержит все поля и методы скрипта Mushroom. А вот скрипт Mushroom немного подредактируем - вынесем реакцию на взаимодействие с игроком в отдельный метод, который сделаем виртуальным (virtual).

Передаём в новый метод ссылку на игрока
Передаём в новый метод ссылку на игрока

Теперь этот виртуальный метод наш МегаГриб сможет выполнять по своему, либо с какими-то своими дополнениями - нам нужно только переписать в нём этот метод с приставкой override.

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

В методе с приставкой override мы можем обратится к родительскому классу Mushroom с помощью обращения base и выполнить всю его логику. Но в дополнение к этому пропишем увеличение максимального здоровья и восполнения здоровья до максимума. После этого остаётся только в параметрах гриба выставить его сытость равно 100, т.к. пока это максимальный предел, а poison равной 0. Ну и давайте сделаем его побольше это же МегаГриб!

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

Главное не забыть убрать с префаба МегаГриба скрипт Mushroom, а вместо него добавить MegaMushroom. Проверим! Всё работает. МегаГриб восполняет здоровье до 101, потом респавнится, можно еще раз его собрать и будет уже 102. Остаётся добавить новые грибы в список грибов и тогда в начале игры они тоже появятся в случайном месте. Вот такие пока у нас приключения в грибном царстве! :)

Нашествие грибов во главе с боссом Синешляпом! :)
Нашествие грибов во главе с боссом Синешляпом! :)

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

Предыдущая статья (6 урок):

Первая статья из этой серии:

А вот тут вся подборка статей-уроков этой серии:

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