В этом уроке выдадим нашему главному герою топор и научим отбиваться от волка, а ещё разберём одну важную проблему, которая может привести нашу игру к ошибке. Не забудьте подписаться, если интересна тем создания игр, чтобы изучить предыдущие статьи и не пропустить продолжение. И давайте начнём творить! :)
Переполнение стека вызовов
Благодаря комментарию подписчика, который обратил внимание на проблему переполнения стека вызовов, я изучил тему (сам был не в курсе) и думаю стоит о ней рассказать простыми словами. И обязательно учесть эту проблему при написании нашего кода.
Ошибку StackOverflowException мы можем получить по разным причинам, но одна из них это переполнения стека вызовов. Каждый раз когда мы запускаем какой-либо метод, то наша программа сохраняет его в стек вызовов и держит там пока метод не отработает до конца. Если наш метод вызовет другой метод, то он тоже сохранится в стеке и будут они там храниться целым списком пока последний из них не отработает до конца.
Почему так происходит? Сделано это чтобы программа знала куда возвращать значение метода, но почему-то даже в случае методов void, которые не возвращают значение они всё равно сохраняются в стек. Возможно это нужно для отслеживания цепочки вызовов в случае ошибки. И вроде что такого? Пускай сохраняет!
Проблема кроется в том, что под стек выделен маленький участок памяти в один мегабайт и поэтому бесконечно вызывать один метод другим, либо вызывать методом самого себя, не получится. Стек рано или поздно забьётся цепочкой вызванных методов и нам выйдет ошибка из-за которой наша игра перестанет работать.
Вот простой эксперимент: в методе Start() вызываем Method(), который будет вызывать сам себя и выводить сообщение в консоль.
Запускаем игру и сразу получаем ошибку. За считанные миллисекунды метод забивает собой стек, отработав 13648 раз.
Вроде бы огромное число, но в случае нашего метода AnimalMove(), который каждые полсекунды теоретически может вызывать сам себя, если точки маршрута будут очень близко, то где-то через полтора часа получим ошибку. Но даже если точки маршрута далеко, то один волк набегает нам на ошибку например за 10 часов, а вот 10 таких волков за час и это уже серьёзная проблема.
Поэтому решением будет воспользоваться циклом while, который по сути будет делать тоже самое, но внутри одного запущенного метода. Главное теперь проконтролировать, чтобы метод Move() вызывался только один раз, так как он будет работать бесконечно, а несколько запущенных методов будут работать параллельно и заставят волка метаться из стороны в сторону.
Добавляем в игру оружие
В UnityAssetStore найдём подходящую модель оружия - например, я для нашего низкополигонального персонажа нашел вот такой низкополигональный топор.
Добавляем в проект, перетаскиваем префаб понравившегося топора на сцену и порой так случается, что префаб совсем не префаб, т.е. он вообще не настроен.
Надо подобрать масштаб и найти в папке ассета нужные материалы. Масштаб топора увеличим в 7 раз и вроде нормально. Теперь надо на него повесить материалы, а то он совсем белый. В папке ассета нужно найти папку с материалами, которая спряталась в папке Axes. В инспекторе на объекте топора найти компонент MeshRenderer и вместо дефолтных белых материалов вставить какие-нибудь подходящие из имеющихся.
Теперь нам нужно сделать этот топор дочерним объектом к кисти персонажа. В окне иерархии найдем игрока в бесконечновложенных друг в друга объектах костей найдем кисть правой руки. Там даже специальный пустой объект есть под предмет - в него и перетащим топор.
Теперь если обнулить координаты топора, то он окажется там где надо.
Если запустить игру, то видно, что топор двигается вместе с кистью персонажа как влитой.
Механика атаки персонажа
Топор в руках есть и самое очевидное, что мы можем с ним сделать в текущих обстоятельствах - это использовать его для защиты от волка! Тогда, также как в случае с атакой волка, нам понадобятся:
- Анимация удара
- Триггер урона (DamageTrigger)
- Анимационное событие включение и выключения триггера
- Скрипт, в котором будет величина урона и метод для анимационного события
Нам также нужно придумать логику срабатывания атаки, т.е. при каких обстоятельствах наш персонаж вдруг должен начать размахивать топором. Предлагаю для начала сделать это просто по нажатию клавиши. Например, по нажатию левой кнопки мыши.
Анимация атаки персонажа
Для начала добавим анимацию атаки:
- Откроем аниматор персонажа
- Создадим новый блок анимации, назовём его Attack и свяжем переходами с BlendTree
- Найдём подходящую анимацию в папке персонажа и перетащим её в поле Motion нового блока анимации
- Добавим новый анимационный параметр isAttack и поставим его в качестве условия перехода к атаке в значении true и в качестве условия выхода из неё в значении false
Вызывать эту анимацию пока временно и для наглядности будем просто из метода Update() в скрипте Player. Когда левая кнопка мыши зажата, то анимация атаки проигрывается по кругу, когда отпускаем она прекращается.
Чтобы анимация проигрывалась зациклено, нужно в ее настройках поставить галочку возле Loop Time. В списке Clips нужно выбрать анимацию атаки, которую мы настраиваем, т.к. анимаций в одном FBX-файле может быть несколько.
При определенной настройке смешивания анимаций наш персонаж начинает вполне неплохо махать топором, плавно переходя к ней даже из анимации бега.
Триггер урона и скрипт оружия
Давайте создадим скрипт Weapon, в котором объявим переменную damage, ссылку на DamageTrigger и метод DamageEnable() с входным параметром типа int. Мы можем воспользоваться имеющимся скриптом DamageTrigger и проверять в нём, кто им пользуется - враг или игрок, но лучше все таки во избежание лишних проверок сделать PlayerDamageTrigger. Создадим его тоже и сделаем ссылку на него в скрипе Weapon.
Но напомню, что анимационное событие сможет увидеть метод, только в скриптах того же объекта, на котором висит аниматор. Поэтому метод DamageEnable() придётся вызывать не напрямую анимационным событием, а подобным методом в скрипте Player, в котором надо сделать ссылку скрипт Weapon.
Внутри объекта топора создадим пустой объект, добавим на него компонент BoxCollider и повесим скрипт PlayerDamageTrigger. BoxCollider надо настроить по размерам топора.
Анимационные события для управления триггером
Добавляем нашей анимации атаки, которую мы выбрали для нашего персонажа, анимационные события. В них вписываем название метода DamageEnable и значения параметра int 1 для включения и 0 для выключения. Напомню, что события создаются правой клавишей на шкале в поле Events и выбором Add Animation Event, а кадры нужные кадры подбираются на шкале предпросмотра. Также выбрав FBX-файл убедитесь, что в инспекторе в списке анимаций выбрана нужная нам.
Скрипт Weapon вешаем на объект топора, протаскиваем в нём ссылку на PlayerDamageTrigger. Также в скрипте Player протаскиваем ссылку на объект топора, на котором висит скрипт Weapon. Объект PlayerDamageTrigger выключаем.
Регистрация попадания оружия по врагу
Чтобы PlayerDamageTrigger смог сработать при пересечении с врагом, нам нужно добавить врагу коллайдер.
В скрипте PlayerDamageTrigger нам понадобится ссылка на Weapon. Ссылку в данном случае можно получить из кода. Поскольку триггер является дочерним объектом для топора, то мы сможем обратится из PlayerDamageTrigger к объекту топора, через ссылку transform.parent.gameObject. И если на этом объекте найдётся скрипт Weapon, то и присвоим ссылку на него нашей ссылке.
Можно запустить и проверить - в режиме Play в поле Weapon появляется ссылка на объект топора.
Вот только теперь возникает проблема - волк с коллайдером толкает нашего игрока, что и не особо реалистично и нами не запланировано. Поэтому давайте лучше коллайдер волка сделаем триггером поставив галочку isTrigger, а PlayerDamageTrigger коллайдером галочку убрав. Если топор будет толкаться при ударе, то это вполне реалистично.
В скрипте Enemy пропишем метод OnTriggerEnter() и метод TakeDamage(), который пока будет просто сообщать в консоли, что волк получил урон.
Всё сохраняем, проверяем все ссылки и запускаем игру. Пытаемся бить волка, но ничего не происходит. А всё потому-что на коллайдере должен присутствовать компонент Rigidbody, чтобы он мог регистрироваться триггером. В случае с игроком компонент CharacterController его уже содержит, а вот "триггер" нашего топора нет.
Нужно обязательно поставить галочку isKinematic, чтобы наш "триггер" не пытался подчиняться гравитации и реагировать на столкновения. Если isKinematic включен, то этот объект будет воспроизводить физику твердого тела, но двигаться только самостоятельно - в нашем случае с помощью анимации.
Проверяем и видим, что в консоли выводится сообщение при попадании топором по волку со значением урона, который мы получили из скрипта Weapon.
Ну, что ж, дело сделано! Теперь нам предстоит заняться реакцией волка на попадание топора, добавить ему параметр здоровья, анимации получения урона и механику смерти или побега. Но этим мы займёмся в следующий раз, а пока подписывайтесь на канал, чтобы не пропустить продолжение и изучить предыдущие статьи из этой серии!
Подборка статей по разработке игры:
А вот тут можно почитать про основы C#: