В этой статье расскажу как сделать из волка полноценного врага, научив его обнаруживать игрока, преследовать его и атаковать. Если интересна тема создания игр, то не забудьте подписаться на канал, чтобы изучить предыдущие уроки и давайте начнём!
Кто такой враг и как его создать
Враги в играх - это наверное одна из древнейших игровых механик и поэтому куда уж без них! Тем более, что созданный нами волк, парень от природы суровый и его "хлебом не корми дай, кого-нибудь покусать". Логично, что он должен нашего главного героя кусать, отнимая его здоровье. Надо только решить каким образом он с нашим главным героем будет пересекаться. Есть несколько способов:
- Аркадный: волк ходит по своим делам своими маршрутами, но если герой по неосторожности становится у него на пути, то оказывается незамедлительно покусан.
- Хардкорный: волк всегда преследует главного героя и как только его догоняет, то неспешно пожёвывает
- Более-менее реалистичный: волк шастает по своим делам, но когда замечает игрока, то начинает его преследовать. Если догоняет, то кусает, если же теряет из виду, то со словами "Не больно то и хотелось" идёт дальше по своим волчьим делам.
Первый вариант, в самом простом виде, можно вообще просто реализовать в виде триггера, попадая в который герой теряет здоровье. В аркадных играх так и устроено. Но раз уж мы начали работать с анимациями персонажей, а у нашего волка где-то в закромах завалялась анимация атаки, то почему бы не сделать красиво. А если сделать, то будет глупо, если получение урона не будет происходить во время анимации укуса. :)
Хардкорный вариант с постоянным преследованием боюсь сделает игру очень напряженной и у игрока не будет возможности перевести дух. Мне бы это не понравилось! Поэтому надо, чтобы на определенной дистанции волк терял к персонажу интерес. Ну и на этой же дистанции, соответственно, этот интерес приобретал, даже если раньше был к герою безразличен. :)
Поэтому будем делать третий вариант - сложный, но реалистичный!
Механика атаки для врага с анимацией
Давайте сделаем для начала анимацию атаки и метод для её включения/выключения. Мы уже работали с анимациями, так что давайте вспоминать как это делается:
1. Выбрать нужный объект (Wolf) в окне иерархии и открыть окно Аниматора.
2. Создать анимационный параметр isAttack типа bool.
3. В окне аниматора правой клавишей мыши на пустом месте вызвать всплывающее меню и выбрать Create State, чтобы создать новый блок анимации.
4. В окне проекта найти папку с моделью волка, найти FBX-файл, который содержит анимацию атаки (Wolf_attack1) и стрелочкой возле иконки файла раскрыть его содержимое.
5. Выбрать новый блок анимации, назвать его Attack и в его поле Motion перетащить файл анимации attack1, который находится внутри FBX-файла Wolf_attack1.
6. С помощью клика правой клавишей мыши на блоке анимации выбрать Make Transition и связать блок Wolf_run и Attack переходами.
Кликнув на сами стрелочки переходов, нужно добавить условия (Conditions), в которых выбрать наш параметр isAttack, в одном случае в значении true, в другом false.
Теперь немного премудростей программирования. :)
Сделаем в скрипте AnimalMove ссылку на Animator и перетащим его туда в инспекторе.
Создадим скрипт Enemy, cделаем ему наследование от AnimalMove и создадим в нём метод Attack(), который будет включат/выключать анимацию атаки.
Как видите мы легко обращаемся к аниматору, который объявлен в скрипте AnimalMove, т.к. скрипт Enemy его наследует, а поле аниматора публичное.
Поскольку скриптов уже много, то давайте создадим для них отдельную папку Scripts и перетащим туда все скрипты. А заодно наведём и порядок в окне иерархии, создав пустой объект Mushrooms и переместив в него все грибы. Кроме того, предлагаю перетащить объекты EventSystem, Main Camera и Directional Light в объект Game, чтоб не мозолили глаза - на сцене итак уже много всего.
Поскольку часть логики поведения для нашего волка теперь будет еще и в скрипте Enemy, то с объекта волка скрипт AnimalMove нужно убрать заменив его на Enemy. Все унаследованные поля также отобразятся в инспекторе. Только ссылки на компоненты Animator и NavMeshAgent придётся протащить заново.
Точки маршрута пока перетаскивать не будем - пусть волк стоит на месте. Так будет проще его тестировать. :)
Теперь каким-то образом мы должны вызывать метод Attack(), когда игрок находится достаточно близко к волку. Мы могли бы вычислять расстояние с помощью Vector3.Distance(), но эта функция довольно ресурсоёмкая и если волк каждый кадр будет считать расстояние до игрока, то ничего криминального конечно не произойдёт, но в последствии, при большом количестве врагов, это ощутимо скажется на производительности.
Гораздо эффективнее будет создать триггер, который, при попадании игрока в него, даст волку понять, что пора кусать. Создадим внутри объекта волка пустой объект AttackTrigger и добавим ему компонент SphereCollider. Увеличим радиус сферы до 1 и расположим её в районе морды-лица хищника. Ну и конечно не забудем сделать этот коллайдер триггером поставив галочку isTrigger. Поскольку триггер является дочерним объектом для волка, то он будет перемещаться вместе с ним.
Создадим специально обученный скрипт AttackTrigger и повесим его на одноимённый объект. В скрипте сделаем ссылку на Enemy и пропишем реакцию на попадание игрока в триггер с помощью метода OnTriggerEnter(), а также метод выхода из триггера OnTriggerExit(), чтобы дать волку понять, что кусать уже некого.
Не забыв проставить в инспекторе в скрипте AttackTrigger ссылку на волка, запускаем игру и если нигде ничего не забыли, то при подходе к волку он начинает неистово кусать персонажа за ляжку, т.к. тот нагло вторгся в его триггер!
Теперь надо сделать так, чтобы в момент анимации укуса у главного героя каким-то образом отнималось здоровье. Можно сделать по тому же принципу. Создадим абсолютно такую же конструкцию и назовём DamageTrigger. На нем будет скрипт DamageTrigger и SphereCollider радиусом поменьше в районе пасти. При пересечении игрока с этим триггером, у игрока будет вычитаться здоровье равное величине урона, наносимое врагом. Для урона сделаем переменную damage в скрипте Enemy.
Вот только наклёвывается проблема. Даже если волк не будет в анимации атаки, то урон всё равно нанесётся при пересечении этого триггера. А если игрок окажется в таком положении, что не будет выходить из триггера, то урон наносится больше не будет. Поэтому триггер неплохо бы включать в момент укуса и после него выключать.
Анимационные события
Самый простой8 (но возможно не самый понятный) способ сделать так, чтобы урон наносился строго в момент удара - это привязать активность триггера к моменту анимации, когда урон по логике должен наноситься. В настройках анимации можно создать анимационные события, привязанные к определенным кадрам анимации, которые будут вызывать нужный нам метод.
Создадим в скрипте Enemy ссылку на DamageTrigger и метод DamageEnable(), который будет получать числовое значение типа int, потому как анимационные события почему-то не в курсе про тип bool. :) Метод будет включать и выключать объект DamageTrigger в нужный момент в зависимости от анимации.
Вот сейчас будет сложновато для понимания. :)
Находим FBX-файл анимации атаки, из которого мы её брали. В его настройках в инспекторе во вкладке Animation находим поле Events и разворачиваем. Снизу в окне предпросмотра анимации есть белая полоска и её мы можем перетаскивать, перебирая кадры и находя нужный. Находим кадр визуального начала укуса. На временной шкале поля Events это место тоже отобразится и правой клавишей мышки тыкнув туда, выберем Add Animation Event, чтобы его создать.
- В его поле Function вписываем название нашего метода DamageEnable.
- В поле int вписываем 1.
Такое же анимационное событие создаём чуть дальше на временной шкале анимации - например когда волк отводит голову. Настраиваем его точно также, только int оставляем равным 0.
Как это всё работает? Во время проигрывания анимации, на кадрах, где мы разместили события, оно будет срабатывать и каким-то неведомым мне образом, находить на объекте, на котором расположен именно этот аниматор, внутри скриптов, которые висят на этом же объекте метод под названием DamageEnable() и, в зависимости от выбранного типа входного значения метода, передаст туда значение одного из своих полей - в нашем случае поля int. Больше вопросов, чем ответов, но оно работает. :)
Объект DamageTrigger теперь нужно отключить, чтоб по умолчанию касание пасти волка не наносило персонажу урон. Если все ссылки проставили, то можно запускать и подойдя к волку увидеть, как объект DamageTrigger в момент атаки ненадолго включается, а урон наносится именно в этот момент.
Всё! Теперь наша серая собака опасна и свирепа - лучше к ней не подходить и на пути не вставать! Но вот научить её саму к нам подходить и осознанно начинать преследовать было бы неплохо.
Обнаружение игрока и преследование
Для обнаружения игрока тоже можно было бы использовать измерение расстояния. Но проблема даже не столько в неоптимизированности такого подхода, сколько в том, что в этом случае не важно, повёрнут ли волк в нашу сторону. Мы сможем подкрасться сзади и волк почует нас своей пятой точкой на внушительном расстоянии. Это не особо реалистично.
Мы снова можем воспользоваться триггером, но на этот раз сместить его так, чтоб зона обнаружения совпадала примерно с зоной видимости волка и ограничивалась выбранным нами расстоянием.
Создадим внутри волка пустой объект DetectTrigger, повесим на него SphereCollider с радиусом например 7 и повесим новый скрипт DetectTrigger.
В скрипте DetectTrigger нам нужно получить ссылку на компонент движения AnimalMove, так как механика обнаружения игрока может быть свойственна и врагам, и безобидным существам. А в нём добавим булевую переменную isPlayerDetected, которая будет своеобразным переключателем между режимами блуждания и преследования.
Но isPlayerDetected не будем изменять напрямую, так как кроме его изменений потребуется и запуск движения к новой цели, поэтому сделаем метод PlayerDetect(). Причём в качестве входного параметра сделаем ссылку на игрока - если ссылка есть, то игрок обнаружен, если вместо ссылки пришёл null, значит можно спокойно гулять дальше.
Скрипт DetectTrigger будет просто регистрировать момент входа и момент выхода игрока из триггера и запускать одну из веток этого метода.
Поскольку метод движения у нас асинхронный и живёт своей жизнью, то во избежание ошибок его нужно остановить. Для этого добавим проверку на isPlayerDetected и в случае если игрок обнаружен, то с помощью оператора return прервём выполнение метода. Return в данном случае ничего не возвращает, а только прерывает дальнейшее выполнение метода.
Следование за целью сделаем в виде отдельного метода в скрипте Enemy, а в скрипте AnimalMove сделаем виртуальный метод PlayerDetectReaction(), который в зависимости от типа существа, сможем переопределять при наследовании (например, волки бегут за нами, а зайцы от нас).
Добавляем вызов этого метода в PlayerDetect(), передавая в него ссылку на игрока, а также прописываем продолжение свободного движения в случае если игрок потерян.
В скрипте Enemy переопределим метод PlayerDetectReaction(), не забыв для этого дописать override, чтобы он вызывал новый асинхронный метод FollowPlayer(), который пропишем ниже.
Этот метод будет направлять волка к игроку, до тех пор пока волк его не потеряет. А еще он будет уменьшать скорость движения волка в компоненте NavMeshAgent до нуля при достижении подходящей дистанции для атаки. Её подбираем опытным путём, так чтобы волк не забегал внутрь игрока и при этом останавливался достаточно близко, чтобы наносить урон.
Если дистанция больше подобранной, то возвращаем волку его скорость движения, чтобы он подошел поближе. И при выходе из цикла, скорость тоже надо вернуть к изначальному значению. Надо также не забыть вернуть волку точки маршрута.
В целом, дело сделано - волк бегает за персонажем, кусает его, а если теряет, то идёт гулять дальше. Вот только единственный способ от него убежать это зайти ему за спину, т.к. скорость игрока меньше скорости волка. Нужно добавить игроку бег. Кроме того, поведение волка и его параметры можно настроить получше. Но эта статья и без того сложная и объёмная, так что не будем её перегружать. К тому же, писать статьи более 4 часов уже морально сложновато. :)
Подписывайтесь на канал, чтобы не пропустить продолжение, ставьте лайки и пишите комментарии, чтобы помочь моему каналу развиваться и смотивировать меня поскорее выпускать продолжение. :) Изучайте предыдущие статьи, чтобы понять, что и откуда взялось и предлагайте варианты того, над чем стоит поработать в дальнейших уроках!
Подборка всех статей по разработке игры:
А вот мой дипломный проект на котором я сам учился:
А вот тут я со своей группой играю весёлую музыку, как уж не поделиться :)