Найти тему
ElandGames

Делаем игру на Unity и изучаем C# (Часть 10)

Оглавление

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

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

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

Не забудьте подписать на канал и поехали!

Автоматическое переключение и смешивание анимаций

Есть в аниматоре Unity такая замечательная вещь как Blend Tree, которая в зависимости от значения анимационных параметров переключается между входящими в неё анимациями. Да не просто переключаться, а плавно переходит между ними, смешивая их между собой.

Суть задумки в том, чтобы переключаться между состоянием покоя, ходьбы и бега в зависимости от скорости движения персонажа. Анимационный параметр скорости (Velocity) у нас уже имеется, так что остаётся только создать этот самый Blend Tree, кликнув в окне аниматора на пустое место правой клавишей мыши и во всплывающем меню выбрать Create State -> From New Blend Tree.

Несколько анимаций движения можно объединить в один BlendTree
Несколько анимаций движения можно объединить в один BlendTree

Создав Blend Tree нужно его открыть кликнув двойным щелчком мыши по нему. В открывшейся вкладке, кликнув на его блок, мы увидим в инспекторе поле Motion, в которое нужно добавить с помощью иконки "+" и пункта Add Motion Field, три поля движения - для анимации покоя, ходьбы и бега.

По умолчанию параметром Blend Tree стал наш параметр скорости Velocity, потому что никаких других числовых параметров в нашем аниматоре нет. Если же их будет много, то можно выбрать нужный в инспекторе, в поле Parameter.
По умолчанию параметром Blend Tree стал наш параметр скорости Velocity, потому что никаких других числовых параметров в нашем аниматоре нет. Если же их будет много, то можно выбрать нужный в инспекторе, в поле Parameter.

Добавляем три поля.

Threshold это значения входного параметра, которое будет соответствовать конкретной анимации из списка Motion
Threshold это значения входного параметра, которое будет соответствовать конкретной анимации из списка Motion

Галочку с Automate Threshholds убираем, чтобы самим выставить значения скорости, которые будут соответствовать нашим анимациям.

В пустые поля Motion перетащим нужные нам анимации Idle, Walk и Run, найдя их в папках нашего проекта (т.к. анимации есть и у волка, то важно не перепутать). Удобно будет переключится в базовый слой аниматора (Base Layer), чтобы кликнув на имеющиеся анимации, быстро найти их в папках проекта.

Значения Thresholds будет удобно сделать равными скоростям ходьбы и бега
Значения Thresholds будет удобно сделать равными скоростям ходьбы и бега

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

Теперь переключимся в базовый слой аниматора (Base Layer) и удалим блоки анимации Idle и Walk.

Blend Tree сделаем блоком по умолчанию, кликнув на нём правой кнопкой мыши и выбрав Set as Layer Default State.

Из блока Death сделаем переход в него при значении isDeath равным false.

Запустим и проверим. Если нигде ничего не упустили, то персонаж двигается во время ходьбы и переходит в анимацию покоя при остановке. Но происходит это совсем не плавно. А всё потому, что значение скорости мы меняем в нашем коде, просто задавая 0, либо 2, а анимационный параметр мгновенно его принимает. Для плавности перехода анимации нам нужно анимационный параметр менять постепенно.

Плавное изменение параметров анимации

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

В методе Update() будем её изменять с помощью математического метода интерполяции в зависимости от времени Mathf.MoveTowards().

  • В качестве первого аргумента впишем туда currentSpeed, который будем плавно менять.
  • В качестве второго аргумента текущую скорость нашего контроллера персонажа controller.velocity.magnitude
  • В качестве третьего аргумента будет Time.deltaTime умноженный на коэффициент, который будет определять скорость изменения анимации. Выберем пока его равным 4.

В аниматор будем передавать теперь currentSpeed, вместо speed.

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

Mathf - это стандартная библиотека, содержащая множество полезных математических функций, как, например, MoveTowards(), которая меняет первый аргумент в сторону второго аргумента шагом равным третьему аргументу. Таким образом можно плавно за несколько шагов привести первое значение ко второму. Чтобы привязать шаги ко времени используем Time.deltaTime.

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

Добавляем персонажу возможность бегать

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

Сделаем так, чтоб при нажатой клавиши Shift, персонаж бегал, в при отпускании снова переходил на шаг.

Пока сделаем переключение с помощью старой системы ввода - она для начинающих будет нагляднее и проще, но в последствии для оптимизации лучше перейти на New Input System
Пока сделаем переключение с помощью старой системы ввода - она для начинающих будет нагляднее и проще, но в последствии для оптимизации лучше перейти на New Input System

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

Анимация бега может не соответствовать скорости движения и её надо будет поднастроить
Анимация бега может не соответствовать скорости движения и её надо будет поднастроить

Blend Tree еще и удобен тем, что мы можем подобрать подходящие скорости проигрывания анимаций (в третьем столбце, там где сейчас единицы), чтобы движение персонажа выглядело более естественным.

Третий параметр это фактически множитель скорости анимации - если ввести 0,5, то анимация будет в 2 раза медленнее, а если 2, то в 2 раза быстрее.
Третий параметр это фактически множитель скорости анимации - если ввести 0,5, то анимация будет в 2 раза медленнее, а если 2, то в 2 раза быстрее.

Смена скорости врагов

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

Создадим в его аниматоре анимационный параметр Velocity и Blend Tree, в котором выберем подходящие анимации для покоя, ходьбы и бега.

У волка скорость движения в NavMeshAgent была равной 3.5 - это значение пока и возьмём в качестве скорости бега
У волка скорость движения в NavMeshAgent была равной 3.5 - это значение пока и возьмём в качестве скорости бега

В основном слое аниматора (Base Layer) заменим блок анимации бега на Belnd Tree и сделаем переходы между ним и блоком анимации атаки. Переход к атаке надо сделать без галочки Has Exit Time, да и обратно лучше тоже без нее, но при этом в настройках смешивания лучше сделать пересечение анимаций как можно больше, чтобы момент с анимационным событием точно отработал.

Наложение анимаций и длину перехода можно настроить для достижения плавности и естественности перехода
Наложение анимаций и длину перехода можно настроить для достижения плавности и естественности перехода

В скрипте AnimalMove добавим два поля - walkSpeed и runSpeed. Будем переключать скорость компонента движения при сработке DetectTrigger. В методе Start() зададим первоначальную скорость равную скорости ходьбы.

Будем просто передавать нужные значения скорости в NavMeshAgent игрока
Будем просто передавать нужные значения скорости в NavMeshAgent игрока

А чтобы управлять анимационным параметром Velocity добавим метод Update() и переменную currentSpeed. Будем плавно её изменять с помощью Mathf.MoveTowards().

А вот скорость анимации будем считать каждый кадр.
А вот скорость анимации будем считать каждый кадр.

Еще нужно внести изменения в скрипт Enemy, потому-что там в методе FollowPlayer() мы тоже изменяем скорость. Во время преследования волк должен переключаться между нулевой скоростью и runSpeed, а при выходе из метода должен перейти в walkSpeed.

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

Лучше скорость бега игрока (runSpeed) сделать 4.5 или 5, чтоб было проще убежать от волка.

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

Выносим повторяющийся код в отдельный метод

Переход к блужданию у нас всегда сопровождается проверкой, достаточно ли у волка точек маршрута. И эта строчка кода встречается в скрипте AnimalMove трижды. Поэтому будет правильно вынести её в отдельный метод. Назовём его например Walk(). Сделаем себе даже вывод сообщения в консоль на случай, если вдруг забудем добавить точки маршрута.

Небольшой рефакторинг - избавляемся от повторяющегося кода и делаем код более читаемым.
Небольшой рефакторинг - избавляемся от повторяющегося кода и делаем код более читаемым.

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

Для этого давайте в методе PlayerDetect() будем сохранять ссылку на скрипт Player, чтобы волк всегда через него мог узнать жив игрок или нет - надо ли продолжать его кусать. Добавим в AnimalMove поле player.

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

В методе PlayerDetect() сделаем сохранение ссылки на игрока. Вот только появилась проблема - внутренняя переменная метода называется точно также как и наша ссылка на игрока - player. Об этом нам VisualStudio и сообщает. Ошибка не критическая и скрипт не сломается, но логика нарушится.

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

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

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

Теперь имея ссылку на игрока мы можем в скрипте Enemy в методе DamageEnable каждый раз при выключении триггера проверять, а жив ли игрок. Для проверки сделаем отдельный метод CheckPlayer(), а при его запуске, на всякий случай, будем проверять, что ссылка на игрока не равна null.

Двойное логическое "и" && позволяет не проверять второе условие, если первое ложно.
Двойное логическое "и" && позволяет не проверять второе условие, если первое ложно.

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

Теперь волк почуяв, что здоровье персонажа иссякло, пойдёт по своим делам. Правда если наткнётся на лежащего игрока, то снова куснёт и снова, убедившись, что персонаж мертв, отправится гулять. Оставим пока так. :)

Столкновение игрока с объектами

Неплохо бы еще сделать, чтоб игрок не проходил сквозь деревья, кусты и камни. Проходит он сквозь них потому-что у них нет коллайлера и нам надо его добавить. Например, SphereCollider или CapsuleCollider, подогнав их под размеры объекта. Но точнее будет воспользоваться MeshCollider, который примет форму объекта.

Причём поскольку наши объекты являются экземплярами префаба, то мы можем добавленный компонент сохранить в префабе, нажав правую клавишу мыши на компоненте и выбрав Added Component -> Apply to Prefab.

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

Сохраняем компонент, добавленный на объект на сцене, в его префабе
Сохраняем компонент, добавленный на объект на сцене, в его префабе

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

Опять левитация вне Хогвартса!
Опять левитация вне Хогвартса!

Это потому-что в игре у нас пока нет гравитации. Но сначала в настройках объекта Player в его компоненте CharacterController выставим значение поля Step Offset на 0. Благодаря этому персонаж не сможет перешагивать на возвышенности выше уровня земли. Нечего ему лазать по кустам!

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

Простейшая гравитация

Давайте добавим простейшую гравитацию в скрипте Player в виде постоянного движения вниз.

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

Теперь даже если игрок куда-то заберется, то потом упадёт оттуда на землю.

Надо все объекты на сцене проверить на наличие на них коллайдеров и добавить в случае отсутствия.

Игрок будет биться об все ёлки, камни и кусты, а убегание от волка станет интереснее.

Границы игровой карты

Ну и напоследок сделаем еще одну простую, но важную штуку. Запретим игроку выбегать за край карты. Для этого просто создадим пустые объекты, добавим на них BoxCollider'ы и разместим их по границам нашей карты.

Типичная невидимая стена, которую можно встретить во многих играх :)
Типичная невидимая стена, которую можно встретить во многих играх :)

Эти объекты можно разместить внутри объекта Ground и назвать например Border. Жмём Edit Collider и с помощью квадратиков на гранях коллайдера вручную выставляем его длину и расположение. Объект можно продублировать с помощью CTRL+D и два из них развернуть на 90 градусов по Y. Теперь область перемещения игрока будет ограничена этими границами. Scale по Y лучше увеличить, чтобы игрок эти границы точно не смог перешагнуть.

Получилась своеобразная песочница для выживания :)
Получилась своеобразная песочница для выживания :)

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

Подборка всех уроков по разработке игры:

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

А вот тут мой дипломный проект, на котором я сам практиковался: