Найти тему
ElandGames

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

Оглавление

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

Компонент управления врагами

Забавно, что компонент по управлению чем-либо, частенько именуют с приставкой Handler, что переводится как "обработчик", но в то же время это слово можно перевести как "дрессировщик". :) Давайте создадим нашего "дрессировщика волков" и назовём его EnemyHandler.

Создадим в объекте Game дочерний объект EnemyHandler, на который и повесим одноименный скрипт.

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

В скрипте EnemyHandler создадим список enemies типа Enemy, в котором будем хранить всех наших врагов. Если нам потребуется, например, увеличить им всем здоровье, то мы сможем легко пробежаться циклом по этому списку, а не вылавливать каждого волка в лесу по-отдельности. :)

А также создадим метод RespawnEnemy, который в качестве входящего параметра будет принимать объект типа Enemy и совершать с ним "воскресительные" манипуляции. Для этих манипуляций нам потребуется как минимум время "респауна", т.е. время через которое волк снова будет готов главного героя съесть.

-3

Этот метод будет вызывать сам волк при своей смерти и запускать тем самым таймер времени респауна. Для этого в скрипте Enemy давайте сделаем метод Respawn(), который будет возвращать параметры волка в более живое состояние. Этот метод будет работать как метод Death(), только наоборот.

По сути мы получили дублирование кода. Одни и те же вызовы, только с разными параметрами.
По сути мы получили дублирование кода. Одни и те же вызовы, только с разными параметрами.

Мы можем избежать дублирования кода, если метод Death сделаем с параметром bool. Тогда в зависимости от параметра мы будем или вырубать волка, или его воскрешать. Но мы не сможем просто так добавить входной параметр в скрипте Enemy - получим ошибку.

Переопределяемый метод должен быть с тем же набором входных параметров
Переопределяемый метод должен быть с тем же набором входных параметров

Этот метод у нас наследуется из класса Unit, так что нужно параметр добавить и там. А учитывая, что и игрок его наследует, то и в классе игрока метод надо модифицировать.

Для вызова метода Death() теперь обязательно нужно задавать входной параметр
Для вызова метода Death() теперь обязательно нужно задавать входной параметр

У игрока входной параметр можно сразу передавать в метод анимации, а метод GameOver() вызывать только, если isDeath = true.

Если isDeath равно false, то персонаж уже не мёртв, а значит метод GameOver() вызывать не нужно.
Если isDeath равно false, то персонаж уже не мёртв, а значит метод GameOver() вызывать не нужно.

У врага же входной параметр в зависимости от логики придётся использовать где-то в его значении, а где-то со знаком ! (не).

Если isDeath равно true, то isAlive равно "не true", т.е. false - логично!
Если isDeath равно true, то isAlive равно "не true", т.е. false - логично!

Теперь остаётся только врагу как-то получить ссылку на EnemyHandler и оттуда запустить таймер своего воскрешения. Какая-то лапша получается. :) Точнее спагетти-код! Ещё одна частая ошибка новичков! Это такой термин в программировании означающий сложную последовательность ссылок одних скриптов на другие.

Enemy запустит метод Death(), который в классе EnemyHandler вызовет метод Respawn(), который в свою очередь вызовет в классе Enemy другой метод Respawn(). Мудрёно получается. Давайте подумаем как упростить.

По сути первый метод Respawn() это по своему функционалу асинхронный таймер. Значит на его работу активность компонента не повлияет и мы сможем даже отключить врага, а этот асинхронный метод все равно продолжит работать. Тогда попробуем перенести логику таймера в Enemy - пусть волк сам разбирается когда ему воскреснуть. Переменную respawnTime тоже отдадим волку в личное пользование.

Теперь все взаимосвязанные части кода находятся в одном скрипте
Теперь все взаимосвязанные части кода находятся в одном скрипте

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

Запускаем и проверяем. Отлично! Волк через 5 секунд воспрял духом и полон здоровья. Вот только если мы достаточно далеко от него, то на прогулку идти он что-то не спешит. Он забыл как гулять! А все потому-что ранее мы сделали выход из цикла Move(). Стало быть при воскрешении надо запустить его заново!

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

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

После воскрешения волк отказывается куда-либо идти пока рядом игрок.
После воскрешения волк отказывается куда-либо идти пока рядом игрок.

А проблема в том, что переменная isPlayerDetected осталась в состоянии true. А в нашей логике, когда она true, то движение не должно происходить. Всё правильно. Значит надо при смерти волка не забыть эту переменную сделать false.

Оставим пока isPlayerDetected в значении false и в случае воскрешения тоже. Возможно не помешает :)
Оставим пока isPlayerDetected в значении 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.

Говорят нельзя в асинхронном методе ничего такого возвращать.
Говорят нельзя в асинхронном методе ничего такого возвращать.

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

-19

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

Если подходящую точку цикл так и не найдёт, то метод вернёт начало координат и волк появится там.
Если подходящую точку цикл так и не найдёт, то метод вернёт начало координат и волк появится там.

Теперь нужно расставить на карте пустые объекты - они будут точками воскрешения. И добавить эти пустые объекты в список spawnpoints скрипта EnemyHandler, что висит на объекте EnemyHandler. А самого волка сразу добавим в список enemies.

Все точки возрождения лучше поместить в один родительский объект.
Все точки возрождения лучше поместить в один родительский объект.

Кстати, куб и функцию расчёта расстояния при старте, думаю можно уже и удалить. :)

В классе Enemy нам все таки потребуется ссылка на EnemyHandler. Сделаем для неё поле, но будем задавать ему значение не в инспекторе.

-22

Пускай EnemyHandler сам расскажет о себе всем объектам класса Enemy, которые у него в списке enemies. Познакомиться так сказать! :)

Цикл foreach порой очень нагляден и удобен!
Цикл foreach порой очень нагляден и удобен!

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

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

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

Когда время респауна подходит к концу, то мгновенно перемещаем врага на новую точку.
Когда время респауна подходит к концу, то мгновенно перемещаем врага на новую точку.

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

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

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

А еще можно почитать про основы C# вот в этих статьях:

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