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

Учимся на ошибках и прокачиваем навыки! Создание игры на Unity и изучение C# (Часть 15)

Каждая решённая проблема и каждая исправленная ошибка - это прокачка нашего навыка разработчика! Ошибки и баги возникают всегда. Борьба с ними занимает большую часть нашего времени, но делает нас опытнее. В этой статье мы исправим некоторые внезапно возникшие баги и доработаем движение волка. Напомню, что у нас периодически стала возникать проблема, что волк оказывается на дистанции, когда уже останавливается, но при этом ничего не делает. Его AttackTrigger всё еще не достаёт до игрока. Как вариант, мы могли бы попытаться подобрать другие параметры компонента движения NavMesh, чтобы волк подходил не слишком близко, но и не останавливался слишком далеко. Либо, как вариант, подобрать другие радиусы триггеров, но тогда получится, что волк будет кусать персонажа находясь за 2 метра от него. Эдакий волк-чародей кусающий телекинезом. :) Гораздо интереснее будет сделать так, чтобы волк сам решал вопрос с дистанцией. :) Надо всё автоматизировать! Идея в том, чтобы оставить решение с авто-ост
Оглавление

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

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

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

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

Гораздо интереснее будет сделать так, чтобы волк сам решал вопрос с дистанцией. :) Надо всё автоматизировать!

Видит вкусного человека, а что с ним делать не понимает и подойти не может!
Видит вкусного человека, а что с ним делать не понимает и подойти не может!

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

"Мужик, у тебя всё нормально?"
"Мужик, у тебя всё нормально?"

Метод подстройки дистанции атаки

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

При движении дистанция это всегда дробное число, а значит тип float.
При движении дистанция это всегда дробное число, а значит тип float.
Во время преследования игрока дистанция до него будет рассчитываться каждые 200 миллисекунд и сохранятся в переменную, чтобы мы могли увидеть её в инспекторе.
Во время преследования игрока дистанция до него будет рассчитываться каждые 200 миллисекунд и сохранятся в переменную, чтобы мы могли увидеть её в инспекторе.

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

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

Всегда хороший вопрос - как назвать новый метод. :) В программировании хорошей практикой является называние метода в соответствии с тем, что он делает. Это как правило глагол. Есть такой глагол fix, который переводится как "исправлять/чинить". Все же помнят фиксиков? :) Главное уточнить, что именно мы будем фиксить!

Создадим новый метод FixDistance() и добавим новую булевую переменную isFixDistance. Метод будет проверять не находится ли волк дальше, чем 1.5 метра и если да, то заставлять его подойти поближе. Для этого придётся включать ему скорость и менять значение stoppingDistance компонента НавМеша, чтобы он не тормозил волка.

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

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

В методе FollowPlayer() сделаем проверку на isFixDistance, чтобы в момент подстройки дистанции скорость не обнулялась, как положено при дистанции меньше дистанции остановки.

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

Сам метод FixDistance() будет запускаться из асинхронного метода RotateToPlayer(). А он в свою очередь выполняется у нас каждые 20 миллисекунд, так как мы прописали ожидание Task.Delay(20). Однако, чтобы это выглядело более плавым и соответствовало 60 кадрам в секунду, предлагаю сделать ожидание в 16 миллисекунд.

1 секунда разделенная на 16 миллисекунд это 62,5 кадра в секунду. При большей задержке, визуально повороты будут немного рванные - с небольшими рывками.
1 секунда разделенная на 16 миллисекунд это 62,5 кадра в секунду. При большей задержке, визуально повороты будут немного рванные - с небольшими рывками.

Проверим наш метод в режиме Play. Всё более-менее работает. Из-за тормозного пути волк останавливается компонентом движения где-то за 1,1 метр, не смотря на stoppingDistance в 2 метра. Но это выглядит вполне неплохо. Постоянства тут нет - разные скорости движения и плавное замедление движения НавМеша даёт разный "тормозной путь".

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

Более явно проблема себя проявляла во время ходьбы, когда пытаешься от атакующего волка уйти пешком. :) Вот тогда то волк и останавливался за 2 метра не зная что делать. Теперь дистанция всегда подстраивается до расстояния меньше, чем 1.5 метра, за счёт того, что волк подходит чуть ближе благодаря нашему новому методу FixDistance(). Дело сделано! Точнее половина дела!

Такая дистанция атаки работает с нашими триггерами и выглядит вполне реалистично.
Такая дистанция атаки работает с нашими триггерами и выглядит вполне реалистично.

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

Давайте компенсируем разницу тормозного пути в зависимости от скорости. Предлагаю просто отодвигать волка назад, до расстояния например в 1,3 метра.

Сделаем это не НавМешем, а простым заданием новой позиции объекта c помощью transform.position. Из позиции волка будем вычитать вектор направления к игроку. Но так как это смещение будет происходить каждые 16 миллисекунд, т.е. фактически каждый кадр, то оно должно быть небольшое. Поэтому умножим его например на 0.01.

Вектор направления от волка к игроку мы высчитываем в методе RotateToPlayer() и можем просто его передать в FixDistance() в качестве входного параметра.

От координат волка отнимаем вектор направления к игроку.
От координат волка отнимаем вектор направления к игроку.

В методе RotateToPlayer() добавим параметр при вызове метода FixDistance().

Вычислить направление довольно просто и можно было бы, как вариант, его не передавать, а вычислять прям в методе FixDistance(), но так у нас минус одна вычислительная операция :)
Вычислить направление довольно просто и можно было бы, как вариант, его не передавать, а вычислять прям в методе FixDistance(), но так у нас минус одна вычислительная операция :)

Я ещё заметил, что зачем-то умножаю в асинхронном методе на Time.deltaTime. В данной ситуации это не имеет смысла, т.к. время ожидания у нас итак в реальных миллисекундах, а Time.deltaTime это время между кадрами. Поэтому его можно убрать и подобрать скорость поворота поменьше.

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

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

Баг: бесконечный респаун волка

Я заметил, что волк после возрождения теперь проходит несколько метров и телепортируется обратно на место возрождения. Очень короткий такой день сурка, длинною примерно в 5 секунд. Причина довольно банальна.

Поскольку мы сделали метод Death() двойного назначение - он у нас и убивает волка, и его возрождает, то некоторые значения внутри него должны зависеть от входного параметра Death(true) или Death(false).

Догадываетесь в чём ошибка? :)
Догадываетесь в чём ошибка? :)

Метод Respawn() вызывается вне зависимости от входного параметра и мы при возрождении получаем еще один вызов метода возрождения. Проблема решается очень просто.

Если isDeath будет true, то вызываем метод возрождения.
Если isDeath будет true, то вызываем метод возрождения.

Добавим проверку имеет ли входной параметр isDeath значение true и тогда только в этом случае будет вызываться метод Respawn().

Теперь волк возрождается на случайной точке возрождения и спокойно идёт до точки маршрута. Вот только дойдя до неё, начинает топтаться на месте.

Баг: волк не хочет гулять

Это вторая проблема. Её причина оказалась в том, что мы добавили stoppingDistance равный 2, но при этом в методе Move() проверяем не приблизился ли волк к точке маршрута ближе, чем на метр. Волк, то очень хочет подойти поближе, но компонент НавМеш ему не даёт. Меняем метр на два и всё работает.

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

Баг: волк забыл как гулять :)

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

Причина этой проблемы кроется в прописанном нами в скрипте Enemy методе Start().

Нашёл я источник проблемы только методом "тыка", закомментировав этот метод.
Нашёл я источник проблемы только методом "тыка", закомментировав этот метод.

Оказалось, что метод Start() в скрипте Enemy, заменил собой одноименный метод Start() в скрипте AnimalMove, который Enemy наследует. В итоге метод Start() из AnimalMove просто не выполнялся и движение волка не запускалось.

Решение довольно простое для тех, кто знаком с наследованием. Делаем метод Start() в скрипте AnimalMove публичным и виртуальным, а в Enemy переопределяем его с помощью модификатора override и делаем вызов базового метода с помощью обращения base.

Скрипт AnimalMove. Переопределяемый метод делаем виртуальным и публичным
Скрипт AnimalMove. Переопределяемый метод делаем виртуальным и публичным
Скрипт Enemy. Ко второму методу дописываем override и вызов первого через base
Скрипт Enemy. Ко второму методу дописываем override и вызов первого через base

Почему просто не вынести задание дистанции остановки в AnimalMove? Тоже вариант, но с ним надо туда вынести и переменную stoppingDistance, так как она объявляется только в классе Enemy.

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

Вот такие, возможно очевидные для профессионалов и совсем неочевидные для начинающих программистов, чудеса! :)

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

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

Вся подборка статей:

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

А вот тут про C#:

Основы языка С# для создания игр на Unity | Сергей Эланд | Дзен