Мы научили нашего главного героя сражаться с волком и даже его побеждать. Но как-то грустно, что волк после этого валяется и ничего не делает. Чтобы главному герою не было скучно, давайте волка будем воскрешать через какое-то время в случайной точке леса. А может и еще чего интересного по ходу дела придумаем. :)
Компонент управления врагами
Забавно, что компонент по управлению чем-либо, частенько именуют с приставкой Handler, что переводится как "обработчик", но в то же время это слово можно перевести как "дрессировщик". :) Давайте создадим нашего "дрессировщика волков" и назовём его EnemyHandler.
Создадим в объекте Game дочерний объект EnemyHandler, на который и повесим одноименный скрипт.
В скрипте EnemyHandler создадим список enemies типа Enemy, в котором будем хранить всех наших врагов. Если нам потребуется, например, увеличить им всем здоровье, то мы сможем легко пробежаться циклом по этому списку, а не вылавливать каждого волка в лесу по-отдельности. :)
А также создадим метод RespawnEnemy, который в качестве входящего параметра будет принимать объект типа Enemy и совершать с ним "воскресительные" манипуляции. Для этих манипуляций нам потребуется как минимум время "респауна", т.е. время через которое волк снова будет готов главного героя съесть.
Этот метод будет вызывать сам волк при своей смерти и запускать тем самым таймер времени респауна. Для этого в скрипте Enemy давайте сделаем метод Respawn(), который будет возвращать параметры волка в более живое состояние. Этот метод будет работать как метод Death(), только наоборот.
Мы можем избежать дублирования кода, если метод Death сделаем с параметром bool. Тогда в зависимости от параметра мы будем или вырубать волка, или его воскрешать. Но мы не сможем просто так добавить входной параметр в скрипте Enemy - получим ошибку.
Этот метод у нас наследуется из класса Unit, так что нужно параметр добавить и там. А учитывая, что и игрок его наследует, то и в классе игрока метод надо модифицировать.
У игрока входной параметр можно сразу передавать в метод анимации, а метод GameOver() вызывать только, если isDeath = true.
У врага же входной параметр в зависимости от логики придётся использовать где-то в его значении, а где-то со знаком ! (не).
Теперь остаётся только врагу как-то получить ссылку на EnemyHandler и оттуда запустить таймер своего воскрешения. Какая-то лапша получается. :) Точнее спагетти-код! Ещё одна частая ошибка новичков! Это такой термин в программировании означающий сложную последовательность ссылок одних скриптов на другие.
Enemy запустит метод Death(), который в классе EnemyHandler вызовет метод Respawn(), который в свою очередь вызовет в классе Enemy другой метод Respawn(). Мудрёно получается. Давайте подумаем как упростить.
По сути первый метод Respawn() это по своему функционалу асинхронный таймер. Значит на его работу активность компонента не повлияет и мы сможем даже отключить врага, а этот асинхронный метод все равно продолжит работать. Тогда попробуем перенести логику таймера в Enemy - пусть волк сам разбирается когда ему воскреснуть. Переменную respawnTime тоже отдадим волку в личное пользование.
Стало намного понятнее и нагляднее. Можно конечно еще порассуждать, а стоит ли волку доверять собственное воскрешение, но для нашей простой игры такое решение вполне приемлемо.
Запускаем и проверяем. Отлично! Волк через 5 секунд воспрял духом и полон здоровья. Вот только если мы достаточно далеко от него, то на прогулку идти он что-то не спешит. Он забыл как гулять! А все потому-что ранее мы сделали выход из цикла Move(). Стало быть при воскрешении надо запустить его заново!
Проверяем и замечаем, что всё работает, но только если персонаж достаточно далеко от волка. В противном случае, волк игрока при воскрешении сразу обнаруживает и никуда не идёт.
А проблема в том, что переменная isPlayerDetected осталась в состоянии true. А в нашей логике, когда она true, то движение не должно происходить. Всё правильно. Значит надо при смерти волка не забыть эту переменную сделать false.
Отлично, теперь волк сразу куда-то пошёл, но совсем не обратил внимание на стоящего перед ним игрока.
Это происходит потому-что DetectTrigger не выключался, а значит нашего нового попадания в него не произошло и логика обнаружения не запустилась. Очевидным решением будет при смерти волка DetectTrigger выключать, а при воскрешении включать. Вопрос в том, нужна ли нам такая механика в игре, чтоб мёртвый волк неожиданно оживал и сразу набрасывался на игрока. Волчий зомби-апокалипсис какой-то! :)
Предлагаю вместо перезапуска триггера перемещать волка в новую точку появления. Причём точку выбирать достаточно удаленную от игрока, чтобы волк не появился рядом. А то будет очень похоже на то, что волк отобрал у Гарри Поттера мантию невидимку и решил сделать нам сюрприз. :)
Система умного возрождения врагов
Чтобы враг не появлялся из воздуха его нужно возрождать вне видимости игрока. В нашем случае это за границами видимой части экрана. Т.е. место появления должно располагаться от игрока не меньше, чем длина диагонали экрана.
Немного математики. :) Если игрок у нас изначально стоит в начале координат и является центром экрана, то расстояние до объекта, находящегося в верхнем углу экрана, можно посчитать как гипотенузу треугольника, у которого один катет равен значению координаты X, а второй значению координаты Z.
Квадрат гипотенузы равен сумме квадратов катетов:
Distance^2 = X^2 + Z^2
Distance^2 = 25*25 + 23*23 = 1154
Distance = ~34
Мы можем это посчитать и в нашей программе:
Прокинем ссылку на куб в инспекторе и с помощью функции Vector3.Distance вычислим расстоянии между кубом и началом координат, запустив игру:
Для верности возьмём дистанцию с запасом, например 40. Сохраним ее в переменной minRespawnDistance в классе EnemyHandler. Также в классе EnemyHandler создадим список точек возрождения spawnpoints.
Сделаем метод для поиска точки возрождения, которая находится дальше, чем вычисленное нами минимальное видимое расстояние. До тех пор, пока не найдём нужную точку, будем случайным образом брать их из списка и сравнивать расстояние между текущей позицией игрока и этой точкой.
Вот только возникает вопрос - как вернуть координаты точки возрождения врагу. Асинхронный метод в отличии от обычного не сможет вернуть значение типа Vector3.
Хорошо, пусть метод будет обычный, но тогда надо предусмотреть выход из цикла, если ни одна точка возрождения не окажется достаточно далеко, иначе игра зависнет. Либо убедиться, что точки точно далеко расставлены.
Не факт, что у нас до каждой из точек возрождения будет больше 40 метров. Эту ситуацию лучше предусмотреть и сделать на всякий случай ограничение числа итераций.
Теперь нужно расставить на карте пустые объекты - они будут точками воскрешения. И добавить эти пустые объекты в список spawnpoints скрипта EnemyHandler, что висит на объекте EnemyHandler. А самого волка сразу добавим в список enemies.
Кстати, куб и функцию расчёта расстояния при старте, думаю можно уже и удалить. :)
В классе Enemy нам все таки потребуется ссылка на EnemyHandler. Сделаем для неё поле, но будем задавать ему значение не в инспекторе.
Пускай EnemyHandler сам расскажет о себе всем объектам класса Enemy, которые у него в списке enemies. Познакомиться так сказать! :)
Таким же образом лучше сделать знакомство всех врагов с игроком. В классе EnemyHandler объявим поле типа Player и всех перезнакомим.
Теперь, когда все знакомы, можно из класса Enemy обратиться в класс EnemyHandler за поиском подходящей точки возрождения. Поскольку EnemyHandler уже знаком с игроком, то из метода Spawnpoint() входной параметр в виде игрока можно убрать и просто использовать этот метод для получения координат подходящей точки возрождения.
Проверяем. Работает! Волк после смерти переместился в другую часть леса и немного побегав, я снова его нашел и он снова на меня напал! Вот только была замечена проблема с тем, что волк забегает внутрь персонажа и сражение превращается в какие-то нелепые попытки друг по другу попасть! :) С этим разберемся в следующий раз!
Если урок был для вас полезен или интересен, то не поскупитесь поставить лайк или оставить комментарий, так я буду знать, что он оказался для кого-то полезен. Чтобы не пропустить дальнейшие уроки подписывайтесь на канал, а предыдущие можно поизучать в моей подборке статей по разработке игры.
А еще можно почитать про основы C# вот в этих статьях: