Найти тему
488 подписчиков

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

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

Решаем проблему с анимациями персонажа

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

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

Что ж, всё становится понятно стоит только заглянуть в Animator. Анимация ходьбы и анимация смерти не имеют никаких переходов. Для исправления проблемы нам нужно сделать такие же переходы между Walk и Death, как и между Idle и Death.

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

Но вот переход из Walk в Death нужен обязательно. Хотя если предположить, что в какой бы анимации герой не находился, в случае смерти он должен упасть, то получается, что все анимации должны иметь переход в Death. Есть универсальное решение - воспользуемся блоком Any State и тогда, сколько бы анимаций мы не добавили, переход в Death будет происходить всегда.

Теперь из любой анимации персонаж всегда сможет перейти в состояние Death, но важно не забыть в Settings убрать галочку с Can Transition To Self.
Теперь из любой анимации персонаж всегда сможет перейти в состояние Death, но важно не забыть в Settings убрать галочку с Can Transition To Self.

Главное не забыть убрать галочку с Can Transition To Self, иначе анимация будет постоянно переходить сама в себя, пока isDeath имеет значение true.

Теперь всё работает и персонаж артистично падает на ходу как подкошенный.

Добавляем в игру других персонажей

Чтобы нашему герою жизнь малиной не казалась, добавим в игру волков! Пускай побегает! :) Я нашёл в UnityAssetStore какого-то бесплатного волка с анимациями, добавил в проект и перетащил его префаб на сцену.

Судя по размеру грибов и животных этот лес находиться где-то в Чернобыле.
Судя по размеру грибов и животных этот лес находиться где-то в Чернобыле.

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

Как бы теперь заставить его двигаться? Мы могли бы повесить на него CharacterController и также управлять им из кода, посылая его в рандомные или задуманные нами точки леса. Но по дороге наш волк будет долбиться головой о все ёлки и камни, двигаясь по прямой, причём скорее всего упрется во что-то.

Поэтому сразу предлагаю познакомиться со стандартным компонентом для поиска пути от Unity под названием AI Navigation. Выберем нашего волка и добавим ему компоненты NavMeshAgent.

Компонент NavMeshAgent отвечает за передвижение с учётом препятствий.
Компонент NavMeshAgent отвечает за передвижение с учётом препятствий.

Само собой нам понадобится и скрипт, в котором мы напишем логику движения для персонажей, которыми мы не управляем. Пусть это будет скрипт для движения животных AnimalMove.

Распакуем префаб волка в панели Иерархии, повесим на него скрипт AnimalMove. В скрипте первым делом сделаем ссылку на компонент NavMeshAgent, для чего с помощью "возможных решений" подключим библиотеку UnityEngine.AI.

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

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

А также создадим метод Move(), который на вход будет получать координаты точки маршрута в пространстве.

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

Чтобы задать точку маршрута удобно будет создать пустой объект на сцене, передвинуть его в желаемое место и добавить этот пустой объект в список waypoints.

Давайте создадим пустой объект и назовём его Waypoints. Его координаты лучше обнулить. Он будет родительским объектом для всех точек маршрута. А внутри него создадим еще один пустой объект и назовём Waypoint1. Разместим его где-нибудь за ёлкой или другим объектом относительно волка.

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

Для волка в инспекторе проставим ссылку на компонент NavMeshAgent и в его список waypoints добавим Waypoint1 из панели иерархии.

Метод Move() предлагаю сделать асинхронным, т.к. в нём мы будем вычислять расстояние между волком и точкой маршрута с помощью функции Vector3.Distance(). Эта функция довольно "дорогая" с точки зрения производительности, поэтому вызывать её каждый кадр это слишком "дорого-богато". С помощью асинхронности сделаем вычисление расстояния, например, каждые пол секунды.

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

Будем вычислять расстояние каждые пол секунды до тех пор, пока оно не станет меньше единицы, т.е. меньше одного метра. После этого давайте сразу отправим волка к следующей точке маршрута. Для этого напишем метод NextWaypoint(), который будет выбирать случайную точку маршрута из списка и возвращать её нам для дальнейшего использования.

Метод, который возвращает значение

Метод, который выбирает случайную точку и возвращает ее в виде значения типа Vector3
Метод, который выбирает случайную точку и возвращает ее в виде значения типа Vector3

Наконец-то мы увидели метод без префикса void (что означает "пустой"). Метод с void просто выполняет прописанные нами команды, а метод, перед которым написан тип данных, должен кроме этого еще и непременно возвращать данные этого типа с помощью команды return.

Что значит возвращать? А это, говоря простым языком, значит, что мы можем использовать его и как метод, и как переменную одновременно. То есть мы можем и вызвать его, и использовать возвращаемое им значение для дальнейших вычислений.

Например можно проверить не является ли следующая точка маршрута началом координат:

if (NextWaypoint() == Vector.Zero)
{
print("Следующая остановка - начало координат");
}

Сразу должен сказать, что символ "=" в C# - это оператор присваивания значения, а символом равенства будет "==". Готовьтесь поначалу постоянно в этом ошибаться! :)

Однако, нам мало вычислить точку и посчитать расстояние до неё - нужно запустить само движение с помощью функции агента SetDestination().

Вызвав функцию SetDestination() и задав ей в качестве аргумента точку в пространстве, мы запустим движение объекта
Вызвав функцию SetDestination() и задав ей в качестве аргумента точку в пространстве, мы запустим движение объекта

В начале метода Move() вызываем функцию движения, а потом проверяем, как легендарный осёл из Шрека: "Мы уже пришли?.. А теперь?". В конце метода, мы посылаем волка куда подальше по новому случайному адресу. Вот только адресов то маловато - всего один. Давайте добавим еще пару -тройку точек маршрута. А еще пропишем в методе Start() первый вызов метода Move(), но обязательно проверим есть ли в списке хоть один "вейпоинт", иначе получим ошибку, что мы посылаем волка туда неведомо куда! :)

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

Переполнение памяти в цикле и как её избежать

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

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

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

Поскольку скрипт animalMove висит на объекте Wolf, то мы можем обратится к его имени и вывести его в сообщении
Поскольку скрипт animalMove висит на объекте Wolf, то мы можем обратится к его имени и вывести его в сообщении

Если мы нигде не накосячили с условием внутри скобок оператора while, то всё должно заработать. Хоть мы и предусмотрели проблему переполнения памяти циклом, на всякий случай всё равно, лучше сохранить проект в Unity перед запуском. Когда работаем с циклом while надо быть настороже! :)

Поскольку мы используем асинхронный метод, то чтобы не получать ошибок после выключения игры, давайте добавим проверку: "а скрипт то наш вообще еще существует?" :)

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

Все ссылки проверяем, всё сохраняем и жмём Play.

Запекание поверхности NavMesh

Итак, игра запустилась, но выдала в консоли ошибку. Говорит, нельзя вызывать метод движения, если объект не находится на NavMesh'е.

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

NavMesh - это специальная поверхность, которую компонент должен вычислить. Чтобы он её вычислил, мы должны во-первых обязательно пометить в инспекторе объект нашей земли (Ground) галочкой Static, а во вторых открыть окно навигации в меню Window - AI - Navigation, где во вкладке Bake нажать кнопку Bake.

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

Благодаря этой заливке мы сразу видим где сможет передвигаться наш волк
Благодаря этой заливке мы сразу видим где сможет передвигаться наш волк

Кроме того, нам нужно, чтобы алгоритм движения учёл объекты, стоящие на этой поверхности (ёлки, деревья, кусты и камни). Для этого их тоже нужно в инспекторе пометить как статические (static) и еще раз сделать запекание поверхности (Bake).

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

Теперь зона вокруг объектов "не запеклась" и движения в этой зоне происходить не будет. Мы можем изменить радиус запекания, чтобы отступы от объектов были не такие большие. Точнее это Agent Radius (радиус объектов, которые будут двигаться).

Движущиеся объекты в системе навигации называются агенты
Движущиеся объекты в системе навигации называются агенты

Когда мы запекли поверхность, запустим игру и если всё настроено (у волка есть Animator и в поле Controller выбран его контроллер), то волк по умолчанию имеет анимацию бега, в которой он находится постоянно. При этом он активно бегает со вполне естественной скоростью от точки к точке. Самое главное, что он изящно маневрирует между деревьями и всегда смотрит в сторону движения. Вот она магия стандартного компонента навигации!

Мы можем даже пока побродить вместе с ним, как будто он наш верный пёс! :)
Мы можем даже пока побродить вместе с ним, как будто он наш верный пёс! :)

Правда пока он нас абсолютно игнорирует и надменно проходит сквозь персонажа. Этим мы займёмся в дальнейшем. Как и нашим прохождением сквозь ёлки, оружием и боевой системой. :)

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

Предыдущая статья:

Вся серия статей по созданию игры тут: