код для зомби в юнити 3д

Зомби-шутер на DOTS в Unity

код для зомби в юнити 3д

Всем привет! Меня зовут Николай Запольнов, я преподаватель на курсах Unity Basic и Unity Pro в OTUS. И сегодня я хочу рассказать вам про, без преувеличения, будущее Unity — технологию DOTS.

В этой статье мы узнаем, что же такое этот DOTS, какие проблемы он решает и как применить его на практике.

Но начать, все же, стоит с теории.

DOTS (“Data Oriented Tech Stack” — технический стек, ориентированный на данные) — это новый подход к разработке игр на движке Unity. Впервые он был анонсирован в 2017 году, а некоторые его компоненты еще раньше — в 2016. В рамках этого технического стека Unity руководствуется слоганом “performance by default” (“производительный по умолчанию”): они создают оптимизированный и эффективный фреймворк в противоположность “классическому” Unity, где программист должен внимательно выбирать, какое API он использует, создавать пулы объектов и т.д.

Для обеспечения максимальной производительности, DOTS включает в себя множество компонентов. Так, например, Job System (Система задач) предоставляет удобный и простой API для многопоточного программирования. Но самым важным нововведением DOTS является использование паттерна Entity-Component-System (ECS).

Entity Component System

Одним из основных столпов классического объектно-ориентированного программирования является наследование. Дочерние классы, собственно, наследуют свойства и методы родительского класса, расширяя и дополняя их новыми возможностями. Такой подход позволяет переиспользовать код, но в больших проектах он также создает и проблему: построить подходящую иерархию объектов не всегда возможно. В результате, в проекте появляются так называемые божественные классы (god classes), реализующие огромный набор методов, часть которых используется одними дочерними классами, а часть — другими.

В качестве альтернативы наследованию была предложена композиция. В геймдеве такой подход часто называют Entity-Component (EC) и именно он применяется в “классическом” Unity. В паттерне EC сущности (entities, в Unity они реализованы классом GameObject) являются лишь контейнерами для компонентов. А компоненты, в свою очередь, реализуют конкретную функциональность и содержат в себе как код, так и данные. Это позволяет собирать каждую конкретную сущность из фрагментов функциональности, как из кубиков Лего.

Паттерн Entity-Component-System развивает эту парадигму еще дальше. Он предлагает оставить в компонентах только данные, а код вынести в системы. Это позволяет оперировать группой компонентов и выводит переиспользование кода на новый уровень. А еще, такой подход позволяет хранить данные компонентов в памяти последовательно. Это сильно увеличивает эффективность работы кеша на современных процессорах и, соответственно, повышает производительность.

Но довольно теории, давайте перейдем к практике!

Подготавливаем проект

Сразу хочется предупредить, что хотя DOTS и находится в разработке уже много лет, он все еще находится в состоянии preview и довольно сырой. Многие компоненты еще не завершены и поддерживают только часть функциональности, а иногда могут содержать и серьезные баги. По этой причине, в сегодняшней статье я буду использовать смешанный подход: часть кода будет написана на DOTS, а часть будет реализована на старых добрых GameObject’ах.

Эта статья написана и проверена на Unity 2020.1.16f1. Вы можете использовать и другую версию, но тогда существует вероятность, что что-то не заработает. DOTS очень активно развивается и программные интерфейсы иногда меняются.

Начнем с пустого проекта (я использовал шаблон “3D” в Unity Hub). Итак, прежде всего нам потребуется добавить DOTS в наш проект. Делается это через менеджер пакетов, но просто так их не найти. Некоторое время назад авторы Unity убрали все preview-пакеты из общего списка, и теперь, чтобы их установить, необходимо нажать в верхнем левом углу пакетного менеджера кнопку “+” и в появившемся меню выбрать “Add package from git URL…”:

Потребуется установить следующие пакеты:

Установка этих пакетов также приведет к установке и других компонентов DOTS. Давайте кратко рассмотрим, что еще добавится в наш проект:

Кроме компонентов DOTS нам также пригодится пакет Cinemachine. Это очень удобный инструмент для управления камерами в Unity. Подробно останавливаться на нем сегодня я не буду, но крайне рекомендую ознакомиться, если вы про него еще не слышали. Устанавливаем:

Ну и наконец стоит добавить в проект пару наборов ассетов. Я буду использовать следующие бесплатные паки:

Познаем сущности

Для начала, давайте создадим в сцене плоскость, которая будет играть роль Земли. Я делаю это как обычно: щелкаю правой кнопкой мыши в окне Hierarchy и выбираю 3D Object⇨Plane:

Назначу ей материал зеленого цвета, чтобы она была больше похожа на траву:

код для зомби в юнити 3д

Теперь у нас в сцене есть обычный GameObject, изображающий плоскость. Так причем же здесь DOTS? Пока что не причем. На текущем этапе развития технологии, сцену в Unity мы по-прежнему создаем на основе игровых объектов.

Но теперь у нас на вооружении есть новый компонент: ConvertToEntity. Добавив этот компонент в объект Plane, мы укажем Unity при запуске игры превратить его в сущность в системе ECS:

Компонент предлагает два режима преобразования: Convert and Destroy (выбран по умолчанию) и Convert And Inject Game Object. Выбор первого режима приводит к удалению игрового объекта после создания сущности, а при выборе второго игровой объект сохраняется. Мы пока оставим эту настройку как есть, т.е. будем уничтожать объект.

Давайте запустим игру и посмотрим, что получилось:

Итак, игровой объект Plane пропал из иерархии, но плоскость все же рисуется в игровом окне. Очевидно, что теперь она стала сущностью. Как можно убедиться в этом?

Unity предоставляет для этого удобный инструмент, который можно найти в меню Window⇨Analysis⇨Entity Debugger:

В левой части этого окна показаны все системы в порядке их исполнения, а также длительность работы каждой из них. Мы пока не создали ни одной своей системы, поэтому все, что мы видим здесь — встроенные системы Unity. Не будем подробно на них останавливаться.

Выбрав конкретную систему, в правом столбце можно увидеть, какими конкретно сущностями она оперирует. А если выбран пункт All Entities, как на скриншоте, то мы увидим список абсолютно всех сущностей. В моем примере их всего две: WorldTime (стандартная сущность Unity, отслеживающая текущее время) и Plane — наша плоскость!

Если выбрать сущность Plane, инспектор покажет нам, какие компоненты она в себя включает:

Как видите, вместо стандартных компонентов Unity здесь используются совершенно другие, новые компоненты. Так, компонент Transform был заменен на три компонента: LocalToWorld, Rotation и Translation. Вместо MeshFilter и MeshRenderer используется компонент RenderMesh. А для физики добавился компонент PhysicsCollider.

Я покажу как пользоваться этими компонентами чуть позже. Пока же для нас важно обратить внимание на следующие моменты:

Что за игра без игрока?

Давайте теперь создадим главного персонажа, которым будет управлять наш игрок. Я добавлю новый, пустой игровой объект и назову его Player. Сразу же добавлю туда компонент ConvertToEntity, чтобы не забыть про него.

Для удобства, я создам отдельный, вложенный игровой объект для внешнего вида игрока (т.е. содержащий 3d-модель персонажа). Давайте для начала используем простую капсулу (щелчок правой кнопкой в иерархии, 3D Object⇨Capsule). Чтобы она не проваливалась в землю, ей необходимо поставить Position.Y равным 1.

При создании капсулы, Unity сразу же создает и коллайдер. Но чтобы физика могла управлять нашим игровым объектом, потребуется добавить соответствующий компонент. Его я буду добавлять в родительский объект (Player) и вместо привычного RigidBody я добавлю компонент DOTS, который называется PhysicsBody:

В добавленном компоненте нужно поправить несколько параметров. Во-первых, массу (параметр Mass) стоит увеличить: я поставлю 70. Во-вторых, я сброшу параметр Angular Damping в 0. Поскольку у нас персонаж будет поворачиваться из кода, мы не хотим, чтобы физика замедляла это движение. Ну и наконец, я поставлю Gravity Factor равным 1.5. В моих экспериментах, если оставить этот параметр равным 1, физика воспринимается как на Луне.

Итак, у нас есть персонаж-капсула. Но как заставить его двигаться? Если помните, я говорил, что в ECS любая логика должна находиться в системах. Нам потребуется написать систему движения игрока. А чтобы система знала, какими сущностями она должна оперировать, мы создадим специальный компонент и назначим его объекту персонажа.

Код компонента ( Scripts/Components/PlayerComponent.cs ):

Очевидно, что он значительно отличается от привычных компонентов. Так, например, необходимо использовать библиотеку Unity.Entities вместо UnityEngine. Взамен класса создается структура. Родительский класс MonoBehaviour был заменен интерфейсом IComponentData. А еще в компоненте нет никаких методов!

Важно также обратить внимание на атрибут GenerateAuthoringComponent. Если его не добавлять, то в целом такой компонент тоже можно использовать. Но его нельзя будет создавать и редактировать в Unity. Именно благодаря этому атрибуту мы теперь сможем в инспекторе добавить наш новый компонент в игровой объект Player:

И даже параметры movementSpeed и rotationSpeed доступны для редактирования! Все, как мы привыкли. На скриншоте я уже проставил туда соответствующие значения (20 и 500).

Давайте теперь реализуем систему ( Scripts/Systems/PlayerMovementSystem.cs ):

Давайте запустим игру:

Система работает! Персонажем можно управлять! Но погодите, что же это? Почему капсула заваливается?

В стандартной физике Unity мы могли бы использовать параметр Freeze Rotation у Rigidbody, чтобы предотвратить падение персонажа. В физике DOTS такого параметра пока что, к сожалению, нет. Поэтому я создаем еще один компонент и систему, чтобы решить эту проблему.

Компонент ( Scripts/Components/FreezeVerticalRotationComponent.cs ) мне здесь нужен исключительно как маркер, чтобы обозначить системе, какие именно сущности она должна обрабатывать. Никаких дополнительных данных я в нем хранить не буду:

А вот код системы ( Scripts/Systems/FreezeVerticalRotationSystem.cs ):

Добавим компонент FreezeVerticalRotationComponent к объекту Player в редакторе и запустим игру:

И теперь наш персонаж не заваливается.

Иллюзия жизни

Персонаж-капсула — это, несомненно, весело. Но веселее было бы, если бы наш главный герой был больше похож на человека, а главное — был анимирован.

К сожалению, система анимации на DOTS все еще находится на очень ранних этапах разработки и в данном проекте я ее использовать не буду. Воспользуюсь старым добрым Animator Controller.

Для начала, перетащу в качестве дочернего объекта в Player префаб TT_demo/prefabs/TT_demo_police из пакета ToonyTinyPeopleDemo и назначу ему заранее заготовленный контроллер. Я не буду подробно останавливаться на устройстве Animator Controller, приведу здесь лишь скриншот:

И тут есть один нюанс. Если сейчас запустить игру, мы увидим, что при движении нашего персонажа игровой объект с 3d-моделью будет оставаться на месте:

Чтобы это поправить, я добавлю новый компонент и систему.

Компонент ( Scripts/Components/CopyTransformComponent.cs ) также выполняет роль простого маркера:

А система ( Scripts/Systems/CopyTransformSystem.cs ) просто осуществляет копирование положения сущности в компонент Transform игрового объекта:

И да, теперь наш персонаж двигается.

Осталось добавить 3d-модельке игрока анимацию. Как вы, наверное, уже догадались, потребуется создать еще один компонент и систему.

N.B. В начале работы над игрой, использующей паттерн ECS, требуется создавать довольно большое количество систем и компонентов, и может показаться, что это лишняя работа по сравнению с паттерном EC. На самом деле, это не так. Преимущества ECS полностью проявляются, когда накоплена некоторая “критическая масса” компонентов и систем. В некоторой мере мы это увидим и в сегодняшней статье, когда будем реализовывать врагов.

Запустив игру, убеждаемся, что теперь анимация работает:

А я все гляжу, глаз не отвожу

Я сейчас ненадолго отвлекусь и наведу немного красоты: добавлю контента в уровень и настрою камеру.

Теперь можно создать виртуальную камеру. Для этого выберу пункт меню Cinemachine⇨Create Virtual Camera (если этого меню у вас нет, проверьте, установили ли вы пакет Cinemachine ).

Стреляй, Глеб Егорыч!

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

Я создам внутри пистолета пустой объект (назову его GunHole) и задам ему положение (-0.1174, 0, 0.394) и поворот (0, 0, 90). Этот объект я буду использовать как референс для кода, создающего пули.

Поэтому, я создам новый объект Shooter внутри объекта Player и прикреплю к нему небольшой компонент на основе MonoBehaviour ( Scripts/Shooter.cs ):

И пропишу в него ссылку на GunHole в инспекторе.

Кроме положения дула, мне также потребуется и префаб пули. В префабах также можно использовать ConvertToEntity — такие префабы превратятся в сущности с компонентом Prefab при загрузке игры и будут исключены из обработки, но будут загружены и проинициализированы. Также, по аналогии с обычными префабами, из таких префабов-сущностей можно создавать обычные сущности и работает это гораздо быстрее, чем метод Instantiate!

Из важного: коллайдер у пули настроен как триггер, а в параметры PhysicsBody внесены небольшие изменения: Linear Damping выставлен в 0, чтобы пуля не теряла скорость со временем, и Gravity Factor также выставлен в 0.

В объект Shooter также добавлю компонент BulletPrefabComponent ( Scripts/Components/BulletPrefabComponent.cs ):

Этот компонент позволит нам получить доступ к префабу из ECS-кода, а также — задать скорость пули (сразу можно ее проставить в инспекторе, я использовал значение 30).

И еще один компонент я буду проставлять непосредственно на пули ( Scripts/Components/Bullet.cs ):

Здесь параметр speed будет определять вектор направления движения пули и ее скорость, а флаг destroyed будет использоваться для обозначения пуль, столкнувшихся с препятствием. Зачем нужен этот флаг и как он используется я объясню чуть дальше.

Теперь для выстрелов нам потребуется система ( Scripts/Systems/PlayerShootingSystem.cs ):

Если сейчас запустить игру, мы увидим, что при выстреле пули действительно появляются:

Но они не двигаются! Как исправить? Правильно — завести систему ( Scripts/Systems/BulletSystem.cs ):

Как видите, тут все очень просто: задаем пулям постоянную скорость и пусть себе летят.

Ожившие мертвецы

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

Прежде всего, нужно добавить врагу жизни ( Scripts/Components/HealthComponent.cs ):

И создадим систему ( Scripts/Components/AnimatedCharacterDeathSystem.cs ), которая будет отыгрывать анимацию смерти персонажа, когда счетчик жизней достигнет 0:

Для проверки я добавлю зомби компонент HealthComponent с количеством жизней 0 и запущу игру. Зомби должен сразу умереть:

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

Для этого мне потребуется самая сложная в этой статье система ( Scripts/Systems/BulletDamageSystem.cs ). Давайте сначала взглянем на нее, а потом будем разбираться:

В методе OnUpdate я сначала прошу физический движок сообщить информацию о столкновениях и передаю ее в CollisionEventSystemJob для обработки. Как только эта работа закончена, я проверяю были ли уничтожены какие-то пули и если были, то передаю их в в командный буфер для фактического уничтожения этих сущностей.

Беги, Лола, Беги!

Последняя важная составляющая логики врага, которая у нас все еще отсутствует — это движение. Давайте же реализуем охоту на игрока!

В дополнение к этому, мне потребуется новый компонент NavMeshAgentComponent ( Scripts/Components/NavMeshAgentComponent.cs ):

Еще один новый компонент ( Scripts/Components/FollowTargetComponent.cs ):

Этот компонент я буду использовать просто как маркер: только враги, имеющие этот компонент, будут преследовать игрока.

Последним штрихом будет написание соответствующей системы ( Scripts/Systems/FollowPlayerSystem.cs ):

Конец

Сегодня мы с вами познакомились с платформой DOTS и паттерном ECS. Надеюсь, мне удалось заинтересовать вас этими технологиями и показать, как их использовать в реальном игровом проекте.

Источник

Простой зомби-шутер на Unity

Всем привет! Скоро стартуют занятия в первой группе курса «Разработчик игр на Unity». В преддверии начала курса прошел открытый урок по созданию зомби-шутера на Unity. Вебинар провёл Николай Запольнов, Senior Game Developer из Rovio Entertainment Corporation. Он также написал подробную статью, которую мы и предлагаем вашему вниманию.

код для зомби в юнити 3д

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

Дисклеймер №1: данная статья рассчитана на новичков. Если вы собаку съели в Unity, то она может показаться вам скучной.

Дисклеймер №2: для прочтения этой статьи вам потребуется хотя-бы базовое знание программирования. Как минимум, слова «класс» и «метод» не должны вас пугать.

Осторожно, под катом трафик!

Введение в Юнити

Если вы уже знакомы с редактором Unity, можете пропустить введение и перейти сразу к разделу “Создаем игровой мир”.

Основной структурной единицей в Unity является “сцена”. Сцена — это обычно один уровень игры, хотя в некоторых случаях может быть и сразу нескольких уровней в одной сцене или, наоборот, один большой уровень может быть разбит на несколько сцен, подгружаемых динамически. Сцены наполняются игровыми объектами, а они, в свою очередь, наполняются компонентами. Именно компоненты реализуют различные игровые функции: рисование объектов, анимацию, физику и т.п. Такая модель позволяет собирать функциональность из простых блоков, как игрушку из конструктора Лего.

Компоненты можно писать и самому, для этого используется язык программирования C#. Именно таким образом пишется игровая логика. Чуть ниже мы посмотрим как это делается, а пока давайте взглянем на сам движок.

Когда вы запустите движок и создадите новый проект, вы увидите перед собой окно, в котором можно выделить четыре основных элемента:

код для зомби в юнити 3д

В верхнем левом углу на скриншоте находится окно Иерархии (“Hierarchy”). Здесь мы можем видеть иерархию игровых объектов в текущей открытой сцене. Unity создал для нас два игровых объекта: камеру (“Main Camera”), через которую игрок будет видеть наш игровой мир и источник света (“Directional Light”), который будет освещать нашу сцену. Без него мы бы видели только черный квадрат.

В центре находится окно редактирования сцены (“Scene”). Здесь мы видим наш уровень и можем его редактировать визуально — двигать и поворачивать объекты мышкой и смотреть, что из этого получается. Рядышком можно увидеть вкладку “Game”, которая сейчас неактивна; если переключиться на нее, то можно будет увидеть, как игра выглядит из камеры. А если запустить игру (кнопкой со значком воспроизведения на панели инструментов), то Unity переключится на эту вкладку, где мы и будем играть в запущенную игру.

В правой верхней части находится окно Инспектора (“Inspector”). В этом окне Unity показывает параметры выбранного объекта и мы можем их редактировать. В частности, мы можем видеть, что у выбранной камеры есть два компонента — “Transform”, который задает положение камеры в игровом мире и, собственно, “Camera”, который и реализует функциональность камеры.

Кстати, компонент Transform есть в том или ином виде у всех игровых объектов в Unity.

Ну и, наконец, в нижней части располагается вкладка “Project”, где мы можем видеть все так называемые ассеты, которые есть в нашем проекте. Ассеты — это файлы с данными, такие как текстуры, спрайты, 3d-модели, анимации, звуки и музыка, конфигурационные файлы. То есть, любые данные, которые мы можем использовать для создания уровней или пользовательского интерфейса. Unity понимает большое количество стандартных форматов (например, png и jpg для картинок, или fbx для 3d-моделей), так что проблем с загрузкой данных в проект не возникнет. А если вы, как и я, не умеете рисовать, то ассеты можно загружать из Unity Asset Store, где собрана огромная коллекция всевозможных ресурсов: как бесплатных, так и продаваемых за деньги.

Справа от вкладки “Project” виднеется неактивная вкладка “Console”. В консоль Unity пишет предупреждения и сообщения об ошибках, так что не забывайте туда периодически поглядывать. Особенно, если что-то не работает — скорее всего, в консоли будет намек на причину проблемы. Также, в консоль можно выводить сообщения и из игрового кода, для отладки.

Создаем игровой мир

Поскольку я программист и рисую хуже, чем курица лапой, для графики я взял несколько бесплатных ассетов из Unity Asset Store. Ссылки на них вы сможете найти в конце этой статьи.

Из этих ассетов я собрал простой уровень, с которым мы и будем работать:

код для зомби в юнити 3д

Никакой магии, я просто перетащил понравившиеся мне объекты из окна Проекта и с помощью мышки расставил их как мне нравится:

код для зомби в юнити 3д

Кстати, Unity позволяет в один клик добавлять в сцену стандартные объекты, такие как куб, сфера или плоскость. Для этого достаточно нажать правой кнопкой в окне Иерархии и выбрать, например, 3D Object⇨Plane. Так, асфальт в моем уровне как раз собран из набора таких плоскостей, на которые я “натянул” текстуру из набора ассетов.

N.B. Если вы задаетесь вопросом, почему я использовал множество плоскостей, а не одну с большими значениями scale, то ответ довольно прост: одна плоскость с большим scale будет иметь сильно увеличенную текстуру, что будет смотреться неестественно относительно других объектов в сцене (это можно исправить параметрами материала, но мы ведь пытаемся делать все максимально просто, не так ли?)

Зомби в поисках пути

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

Чтобы это реализовать, мы будем использовать инструмент “навигационная сетка” (Navigation Mesh). На основе данных сцены этот инструмент вычисляет области, где можно перемещаться, и формирует набор данных, по которым во время игры может быть произведен поиск оптимального маршрута из любой точки уровня в любую другую. Эти данные сохраняются в ассет и в дальнейшем не могут быть изменены — этот процесс называется “запеканием” (“baking”). Если вам нужны динамически изменяющиеся препятствия, то можно использовать компонент NavMeshObstacle, но для нашей игры это не нужно.

Важный момент: чтобы Unity знал, какие объекты нужно включить в расчет, необходимо в Инспекторе для каждого объекта (можно в окне Иерархии выделить сразу все) нажать на стрелочку вниз около опции “Static” и отметить пункт “Navigation Static”:

код для зомби в юнити 3д

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

Теперь воспользуемся пунктом меню Window⇨AI⇨Navigation и в открывшемся окне выберем вкладку “Bake”. Здесь Unity предложит нам задать такие параметры, как высота и радиус персонажа, максимальный угол наклона земли по которому еще можно ходить, максимальную высоту ступенек и так далее. Мы пока не будем ничего этого менять и просто нажмем кнопку «Bake”.

код для зомби в юнити 3д

Unity произведет необходимые расчеты и продемонстрирует нам результат:

код для зомби в юнити 3д

Здесь синим отмечена область, где можно ходить. Как видите, Unity оставил небольшой бортик вокруг препятствий — ширина этого бортика как раз зависит от радиуса персонажа. Таким образом, если центр персонажа находится в синей зоне, то он не будет “проваливаться” внутрь препятствий.

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

Давайте создадим игровой объект “Zombie”, добавим в него 3d-модель зомби из ассетов, а также — компонент NavMeshAgent:

код для зомби в юнити 3д

Если запустить игру сейчас, то ничего не произойдет. Мы должны сказать компоненту NavMeshAgent, куда двигаться. Для этого мы создадим свой первый компонент на языке C#.

В окне проекта выберите корневую директорию (она называется “Assets”) и в списке файлов нажмите правой кнопкой мыши, чтобы создать директорию “Scripts”. Мы будем хранить все наши скрипты в ней, чтобы в проекте был порядок. Теперь, внутри “Scripts” давайте создадим скрипт “Zombie” и добавим его в игровой объект зомби:

код для зомби в юнити 3д

Двойной щелчок на скрипте откроет его в редакторе. Давайте посмотрим, что Unity для нас создал

Это стандартная заготовка для компонентов. Как мы видим, Unity подключил нам библиотеки System.Collections и System.Collections.Generic (сейчас они не нужны, но часто бывают нужны в коде игр на Unity, поэтому их включили в стандартный шаблон), а также — библиотеку UnityEngine, где содержится весь основной API движка.

Также, Unity создал для нас класс Zombie (название совпадает с именем файла; это важно: если они не будут совпадать, Unity не сможет сопоставить скрипт с компонентом в сцене). Класс унаследован от MonoBehaviour — это базовый класс для компонентов, создаваемых пользователем.

Внутри класса Unity создал для нас два метода: Start и Update. Эти методы движок будет вызывать сам: Start — сразу после того, как сцена была загружена, а Update — каждый кадр. На самом деле, таких вызываемых движком функций очень много, но большинство из них сегодня нам не понадобятся. Полный список, а также последовательность их вызова всегда можно подсмотреть в документации: https://docs.unity3d.com/Manual/ExecutionOrder.html

Давайте заставим зомби двигаться по карте!

Для начала, нам нужно подключить библиотеку UnityEngine.AI. Именно в ней содержится класс NavMeshAgent и другие классы, связанные с навигационной сеткой. Для этого добавим в начало файла директиву using UnityEngine.AI.

Затем, нам нужно получить доступ к компоненту NavMeshAgent. Для этого мы можем использовать стандартный метод GetComponent. Он позволяет получить ссылку на любой компонент в том же игровом объекте, в котором находится компонент из которого мы вызываем этот метод (в нашем случае — это игровой объект “Zombie”). Заведем в классе поле NavMeshAgent navMeshAgent, в методе Start получим ссылку на NavMeshAgent и попросим его двигаться в точку (0, 0, 0). У нас должен получиться вот такой скрипт:

Запустив игру, мы увидим, как зомби двигается к центру карты:

код для зомби в юнити 3д

Зомби преследует жертву

Замечательно. Но нашему зомби скучно и одиноко, давайте добавим для него в игру жертву игрока.

По аналогии с зомби, создадим игровой объект “Player” (на этот раз выберем 3d-модель полицейского), также добавим в него компонент NavMeshAgent и свежесозданный скрипт Player. Содержимое скрипта Player пока трогать не будем, а вот в скрипт Zombie потребуется внести правки. Также, рекомендую поставить у игрока в компоненте NavMeshAgent значение свойства Priority в 10 (или любое другое значение меньше стандартных 50, то есть задать игроку более высокий приоритет). В таком случае, если игрок и зомби встретятся на карте, зомби не сможет сдвинуть игрока, тогда как игрок сможет отпихнуть зомби.

Чтобы преследовать игрока, зомби нужно знать его положение. А для этого нам нужно получить ссылку на него в нашем классе Zombie с помощью стандартного метода FindObjectOfType. Запомнив ссылку, мы сможем обратиться к компоненту transform игрока и попросить у него значение position. А чтобы зомби преследовал игрока всегда, а не только в начале игры, мы будем задавать цель для NavMeshAgent в методе Update. Получится вот такой скрипт:

Запустим игру и убедимся, что зомби нашел свою жертву:

код для зомби в юнити 3д

Спасение бегством

Наш игрок пока что стоит, как истукан. Это явно не поможет ему выжить в таком агрессивном мире, поэтому нужно научить его перемещаться по карте.

Для этого нам потребуется получать от Unity информацию о нажатых клавишах. Метод GetKey стандартного класса Input как раз предоставляет такую информацию!

N.B. Вообще, такой способ получения ввода не совсем каноничен. Лучше использовать Input.GetAxis и биндинг через Project Settings⇨Input Manager. А еще лучше — New Input System. Но эта статья и так получилась чересчур длинная, так что, сделаем как попроще.

Откроем скрипт Player и изменим его следующим образом:

Как и в случае с зомби, в методе Start мы получаем ссылку на компонент NavMeshAgent игрока и запоминаем ее в поле класса. Но теперь мы еще и добавили поле moveSpeed.
Благодаря тому, что это поле публичное, его значение можно редактировать прямо в Инспекторе в Unity! Если у вас в команде есть гейм-дизайнер, он будет очень рад, что ему не нужно лезть в код, чтобы подредактировать параметры игрока.

Поставим 10 в качестве скорости:

код для зомби в юнити 3д

В методе Update будем используем Input.GetKey, чтобы проверить нажата ли какая-то из стрелок на клавиатуре и сформировать вектор направления движения для игрока. Обратите внимание, что мы используем координаты X и Z. Это связано с тем, что в Unity ось Y смотрит вверх, в небо, а земля расположена в плоскости XZ.

После того, как мы сформировали вектор направления движения dir, мы его нормализуем (в противном случае, если игрок захочет двигаться по диагонали, вектор будет чуть длиннее единичного и такое движение будет быстрее, чем движение прямо) и умножаем на заданную скорость движения. Результат передаем в navMeshAgent.velocity и агент сделает всю остальную работу.

Запустив игру, мы сможем наконец-то попытаться убежать от зомби в безопасное место:

код для зомби в юнити 3д

Чтобы камера двигалась вместе с игроком, давайте напишем еще один простой скрипт. Назовем его “PlayerCamera”:

Смысл этого скрипта должен быть по большей части понятен. Из особенностей — здесь вместо Update мы используем LateUpdate. Этот метод аналогичен Update, но вызывается всегда строго после того, как отработают Update у всех скриптов в сцене. В данном случае мы используем LateUpdate, потому что нам важно, чтобы NavMeshAgent рассчитал новое положение игрока до того, как мы переместим камеру. Иначе может возникать неприятный эффект “подергивания”.

Если теперь прикрепить этот компонент к игровому объекту “Main Camera” и запустить игру, персонаж игрока будет всегда в центре внимания!

Минутка анимации

Отвлечемся на минутку от проблем выживания в условиях зомби-апокалипсиса и задумаемся о вечном — об искусстве. Наши персонажи сейчас похожи на ожившие статуи, приводимые в движение неведомой силой (возможно, магнитами под асфальтом). А хотелось бы, чтобы они выглядели как настоящие, живые (и уже не очень) люди — шевелили руками и ногами. В этом нам поможет компонент Animator и инструмент под названием Animator Controller.

Animator Controller — это конечный автомат (стейт-машина), где мы задаем определенные состояния (персонаж стоит, персонаж идет, персонаж умирает и т.п.), привязываем к ним анимации и задаем правила перехода из одного состояния в другое. Unity будет автоматически переключаться от одной анимации к другой, как только сработает соответствующее правило.

Давайте создадим Animator Controller для зомби. Для этого создадим в проекте директорию Animations (помните про порядок в проекте), а в ней — с помощью правой кнопки — Animator Controller. И назовем его “Zombie”. Двойной щелчок — и перед нами предстанет редактор:

код для зомби в юнити 3д

Пока что здесь нет никаких состояний, но есть две точки входа (“Entry” и “Any State”) и одна точка выхода (“Exit”). Перетащим пару анимаций из ассетов:

код для зомби в юнити 3д

Как видите, как только мы перетащили первую анимацию, Unity автоматически привязал ее к точке входа Entry. Это так называемая анимация по-умолчанию. Она будет проигрываться сразу после старта уровня.

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

В верхнем левом углу окна редактора есть две кнопки: “Layers” и “Parameters”. По умолчанию выбрана вкладка “Layers”, нам же нужно переключиться на “Parameters”. Теперь мы можем добавить новый параметр типа float, воспользовавшись кнопкой “+”. Назовем его “speed”:

код для зомби в юнити 3д

Теперь надо сказать Unity, что должна воспроизводиться анимация “Z_run”, когда speed больше 0 и “Z_idle_A”, когда speed равен нулю. Для этого мы должны создать два перехода: один из “Z_idle_A” в “Z_run”, а другой — в обратную сторону.

Начнем с перехода из idle в run. Щелкаем правой кнопкой по прямоугольнику “Z_idle_A” и выбираем “Make Transition”. Появится стрелочка, щелкнув по которой можно настроить ее параметры. Во-первых, необходимо убрать галочку “Has Exit Time”. Если этого не сделать, анимация будет переключаться не по нашему условию, а когда закончит воспроизводиться предыдущая. Нам это совершенно не нужно, поэтому галочку снимаем. Во-вторых, внизу, в списке условий (“Conditions”) нужно нажать на “+” и Unity добавит нам условие. Значения по умолчанию в данном случае — как раз те, что нам нужны: параметр “speed” должен быть больше нуля для перехода из idle в run.

код для зомби в юнити 3д

По аналогии создаем переход в обратную сторону, но в качестве условия теперь указываем “speed” меньше, чем 0.0001. Проверки на равенство для параметров типа float нет, их можно сравнивать только на больше/меньше:

код для зомби в юнити 3д

Теперь нужно привязать контроллер к игровому объекту. Выберем 3d-модель зомби в сцене (это дочерний объект у объекта “Zombie”) и перетащим контроллер мышкой в соответствующее поле в компоненте Animator:

код для зомби в юнити 3д

Осталось только написать скрипт, который будет управлять параметром speed!

Создадим скрипт MovementAnimator следующего содержания:

Здесь мы, как и в других скриптах, в методе Start получаем доступ к NavMeshAgent. Также мы получаем доступ к компоненту Animator, но, так как компонент MovementAnimator мы будем крепить к игровому объекту “Zombie”, а Animator находится в дочернем объекте, вместо GetComponent нужно использовать стандартный метод GetComponentInChildren.

В методе Update мы запрашиваем у NavMeshAgent его вектор скорости, рассчитываем его длину и передаем ее аниматору в качестве параметра speed. Никакой магии, все по науке!

Теперь добавим компонент MovementAnimator в игровой объект Zombie и, если запустить игру, мы увидим, что зомби теперь анимирован:

код для зомби в юнити 3д

Заметьте, что, поскольку мы поместили код управления аниматором в отдельный компонент MovementAnimation, его можно легко добавить и для игрока. Нам даже не потребуется создавать контроллер с нуля — можно скопировать контроллер зомби (это можно сделать, выбрав файл “Zombie” и нажав Ctrl+D) и заменить анимации в прямоугольниках-состояниях на “m_idle_А” и “m_run”. Все остальное — аналогично зомби. Я оставлю это вам в качестве упражнения (ну или скачайте код в конце статьи).

Одно маленькое дополнение, которое полезно сделать — добавить следующие строчки в класс Zombie:

Первая строчка говорит NavMeshAgent’у, что он не должен управлять поворотом персонажа, мы будем делать это сами. Вторая строчка задает поворот персонажа в ту же сторону, куда направлено его движение. NavMeshAgent по умолчанию интерполирует угол поворота персонажа и это выглядит не очень красиво (зомби поворачивается медленнее, чем меняет направление движения). Добавление этих строчек убирает этот эффект.

N.B. Для задания поворота мы используем кватернион. В трехмерной графике основными способами задания поворота объекта являются углы Эйлера, матрицы поворота и кватернионы. Первые два не всегда удобны в работе, а также подвержены такому неприятному эффекту, как “Gimbal Lock”. Кватернионы лишены этого недостатка и сейчас используются практически повсеместно. Unity предоставляет удобный инструментарий для работы с кватернионами (как, впрочем и с матрицами, и с углами Эйлера), позволяющий не вдаваться в подробности устройства этого математического аппарата.

Я вижу цель

На экране курсор мыши передвигается по двумерному пространству — поверхности монитора. При этом, наша игровая сцена — трехмерная. Наблюдатель видит сцену через свой глаз, где все лучи света сходятся в одной точке. Объединив все эти лучи, мы получим пирамиду видимости:

код для зомби в юнити 3д

Глаз наблюдателя видит только то, что попадает в эту пирамиду. Причем, движок специально усекает эту пирамиду с двух сторон: во-первых, со стороны наблюдателя располагается экран монитора, так называемая “ближняя плоскость” (на рисунке она окрашена в желтый цвет). Монитор не может физически отобразить объекты ближе, чем экран, поэтому движок их отсекает. Во-вторых, поскольку компьютер обладает конечным объемом ресурсов, движок не может продлить лучи в бесконечность (например, для буфера глубины должен быть задан некоторый диапазон возможных значений; причем, чем он шире, тем ниже будет точность), поэтому пирамида отсекается сзади так называемой “дальней плоскостью”.

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

код для зомби в юнити 3д

Построить такой луч и найти его пересечение с объектами в сцене можно с помощью стандартного метода Raycast из класса Physics. Но если мы используем этот метод, он будет находить пересечение со всеми объектами в сцене — землей, стенами, зомби… Мы же хотим, чтобы курсор перемещался только по земле, поэтому нам нужно каким-то образом объяснить Unity, что поиск пересечения должен ограничиваться только заданным набором объектов (в нашем случае — только плоскостями земли).

Если выделить любой игровой объект в сцене, то в верхней части инспектора можно увидеть выпадающий список “Layer”. По умолчанию там будет значение “Default”. Открыв выпадающий список, в нем можно найти пункт “Add layer…”, который откроет окно редактора слоев. В редакторе нужно добавить новый слой (назовем его “Ground”):

код для зомби в юнити 3д

Теперь можно выбрать все плоскости земли в сцене и с помощью этого выпадающего списка назначить им слой Ground. Это позволит далее в скрипте указать методу Physics.Raycast, что нужно проверять пересечения луча только с этими объектами.

Теперь давайте перетащим спрайт курсора из ассетов в сцену (я использую Spags Assets⇨Textures⇨Demo⇨white_hip⇨white_hip_14):

код для зомби в юнити 3д

Я добавил курсору поворот на 90 градусов вокруг оси Х, чтобы он лежал горизонтально на земле, задал масштаб 0.25, чтобы он не был таким большим и указал в качестве координаты Y значение 0.01. Последнее важно, чтобы не было эффекта, называемого “Z-fighting”. Видеокарта использует вычисления с плавающей запятой, чтобы определить, какие объекты находятся ближе к камере. Если задать курсору значение 0 (т.е. такое же, как и у плоскости земли), то из-за погрешностей в этих вычислениях, для некоторых пикселей видеокарта решит, что курсор ближе, а для других — что земля. Причем в разных кадрах наборы пикселей будут разные, что создаст неприятный эффект просвечивания кусков курсора сквозь землю и “мерцания” при его движении. Значение 0.01 достаточно велико, чтобы нивелировать погрешности в расчетах видеокарты, но при этом не настолько большое, чтобы глаз заметил, что курсор висит в воздухе.

Теперь переименуем игровой объект в Cursor и создадим скрипт с таким же названием и следующим содержанием:

Поскольку курсор — это спрайт (двумерный рисунок), то для его отрисовки Unity использует компонент SpriteRenderer. Мы получаем ссылку на этот компонент в методе Start, чтобы иметь возможность включать/выключать его по мере необходимости.

Также в методе Start мы преобразуем имя слоя “Ground”, который мы создали ранее, в битовую маску. Unity использует битовые операции для фильтрации объектов при поиске пересечений и метод LayerMask.GetMask возвращает битовую маску, соответствующую указанному слою.

В методе Update мы получаем доступ к главной камере сцены с помощью Camera.main и просим ее пересчитать двумерные координаты мыши (полученные с помощью Input.mousePosition) в трехмерный луч. Далее, мы передаем этот луч в метод Physics.Raycast и проверяем, пересекся ли он с каким-то объектом в сцене. Значение 1000 — это максимальное расстояние. В математике лучи бесконечны, а вычислительные ресурсы и память у компьютера — нет. Поэтому Unity просит нас определить какое-то разумное максимальное расстояние.

Если пересечения не было, то мы выключаем SpriteRenderer и изображение курсора пропадает с экрана. Если же пересечение было найдено, то мы перемещаем курсор в точку пересечения. Обратите внимание, что мы не меняем координату Y, потому что точка пересечения луча с землей будет иметь Y равный нулю и присвоив ее нашему курсору мы опять получим эффект Z-fighting, от которого мы пытались избавиться выше. Поэтому, мы берем от точки пересечения только координаты X и Z, а Y оставляем прежний.

Добавляем компонент Cursor к игровому объекту Cursor.

Теперь, доработаем скрипт Player: во-первых, добавим поле Cursor cursor. Затем в методе Start добавим следующие строчки:

И, наконец, чтобы игрок всегда поворачивался в сторону курсора, в методе Update добавим:

Здесь мы также не берем в расчет координату Y.

Стреляй, чтобы выжить

Создадим игровой объект Shot и добавим в него стандартный компонент LineRenderer. С помощью поля “Width” в редакторе зададим ему небольшую ширину, например 0.04. Как мы видим, Unity рисует его ярким фиолетовым цветом — таким образом подсвечиваются объекты, не имеющие материала.

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

Создадим в проекте директорию Materials и внутри нее — материал, назовем его Yellow. В качестве шейдера выберем Unlit/Color. Этот стандартный шейдер не учитывает освещение, поэтому нашу пулю будет видно даже в темноте. Выберем желтый цвет:

код для зомби в юнити 3д

Теперь, когда материал создан, можно назначить его LineRenderer’у:

код для зомби в юнити 3д

Создадим скрипт Shot:

Этот скрипт, как вы, наверное, уже догадались, нужно добавить к игровому объекту Shot.

Здесь я использовал небольшой трюк, чтобы с минимумом кода отображать выстрел на экране в течение строго одного кадра. Во-первых, я использую FixedUpdate вместо Update. Метод FixedUpdate вызывается с заданной периодичностью (по умолчанию — 60 кадров в секунду), даже если реальная частота кадров непостоянна. Во-вторых, я завел переменную visible, которую я ставлю в true, когда отображаю выстрел на экране. В следующем FixedUpdate я сбрасываю ее в false, и только в следующем кадре выключаю игровой объект выстрела. По сути, я использую логическую переменную как счетчик от 1 до 0.

Метод gameObject.SetActive включает или выключает весь игровой объект, на котором находится наш компонент. Выключенные игровые объекты не рисуются на экране и у их компонентов не вызываются методы Update, FixedUpdate и т.п. Использование этого метода позволяет делать выстрел невидимым, когда игрок не стреляет.

Также в скрипте есть публичный метод Show, который мы будем использовать в скрипте Player, чтобы фактически отображать пулю при выстреле.

Но сначала нужно иметь возможность получить координаты дула пистолета, чтобы выстрел происходил из правильного отверстия. Для этого найдем в 3d-модели игрока объект Bip001⇨Bip001 Pelvis⇨Bip001 Spine⇨Bip001 R Clavicle⇨Bip001 R UpperArm⇨Bip001 R Forearm⇨Bip001 R Hand⇨R_hand_container⇨w_handgun и добавим в него дочерний объект GunBarrel. Разместим его так, чтобы он находился прямо возле дула пистолета:

код для зомби в юнити 3д

Теперь в скрипте Player добавим поля:

В метод Start скрипта Player добавим:

Как вы можете догадаться, добавленное публичное поле gunBarrel также, как и moveSpeed ранее, будет доступно в Инспекторе. Давайте назначим ему реальный игровой объект, который мы создали:

код для зомби в юнити 3д

код для зомби в юнити 3д

Что-то тут не так! Кажется, выстрелы не убивают зомби, а просто пролетают сквозь него!

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

Это довольно легко исправить. В коде обработки клика мышкой в классе Player после строки var to = … и перед строкой shot.Show(. ) нужно добавить следующие строчки:

Здесь мы используем уже знакомый вам Physics.Raycast, чтобы выпустить луч из дула пистолета и определить, пересекся ли он с каким-либо игровым объектом.

Тут, правда, есть один нюанс: пуля все-равно будет пролетать сквозь зомби. Дело в том, что объектам уровня (зданиям, ящикам и т.п.) автор ассета добавил коллайдер. А автор ассета с персонажами этого не сделал. Давайте исправим это досадное недоразумение.

Коллайдер — это компонент, с помощью которого физический движок определяет столкновения между объектами. Обычно в качестве коллайдеров используются простые геометрические фигуры — кубы, сферы и т.п. Хотя такой подход дает меньшую точность столкновений, формулы пересечений между такими объектами довольно просты и не требуют больших вычислительных ресурсов. Конечно, если вам нужна максимальная точность, всегда можно пожертвовать производительностью и использовать MeshCollider. Но нам высокая точность не нужна, поэтому воспользуемся компонентом CapsuleCollider:

код для зомби в юнити 3д

Теперь пуля не будет пролетать сквозь зомби. Однако зомби все еще бессмертен.

Давайте сначала добавим в Animation Controller зомби анимацию смерти. Для этого перетащим в него анимацию AssetPacks⇨ToonyTinyPeople⇨TT_demo⇨animation⇨zombie⇨Z_death_A. Чтобы активировать ее, создадим новый параметр died с типом trigger. В отличие от других параметров (bool, float и т.п.), триггеры не запоминают свое состояние и больше похожи на вызов функции: активировали триггер — сработал переход, а триггер сбросился обратно. А поскольку умереть зомби может в любом состоянии — и если стоит на месте, и если бежит, то переход будем добавлять из состояния Any State:

код для зомби в юнити 3д

Добавим в скрипт Zombie следующие поля:

В метод Start класса Zombie вставляем:

В самое начало метода Update нужно добавить проверку:

Ну и, наконец, добавим классу Zombie публичный метод Kill:

Назначение новых полей, думаю, достаточно очевидно. А что касается метода Kill — в нем мы (если мы еще не мертвы) ставим флаг смерти зомби и удаляем из нашего игрового объекта компоненты CapsuleCollider, MovementAnimator и NavMeshAgent, после чего активируем воспроизведение анимации смерти у контроллера анимаций.

Зачем удалять компоненты? Чтобы как только зомби умирает, он переставал перемещаться по карте и более не был препятствием для пуль. По хорошему, надо еще каким-то красивым образом избавляться от тела после того, как анимация смерти отыграна. Иначе мертвые зомби продолжат отъедать ресурсы и, когда трупов станет слишком много, игра начнет заметно подтормаживать. Самый простой способ — добавить сюда же вызов Destroy(gameObject, 3). Это приведет к тому, что Unity удалит этот игровой объект через 3 секунды после этого вызова.

Чтобы все это наконец заработало, остался последний штрих. В класс Player, в метод Update, там где мы вызываем Physics.Raycast, в ветку для случая, когда было найдено пересечение, добавляем проверку:

В переменную hit вызов Physics.Raycast записывает информацию о пересечении. В частности, в поле transform будет ссылка на компонент Transform игрового объекта, с которым пересекся луч. Если у этого игрового объекта есть компонент Zombie, значит это зомби и мы его убиваем. Элементарно!

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

Добавим к игровому объекту Zombie систему частиц (щелкаем на нем правой кнопкой и выбираем Effects⇨Particle System):

Я предлагаю следующие параметры:
Transform:

код для зомби в юнити 3д

Осталось активировать ее в методе Kill класса Zombie:

И вот теперь совсем другое дело!

код для зомби в юнити 3д

Зомби нападают стаей

На самом деле, сражаться с единственным зомби скучно. Вы его убили и все. Где драма? Где страх умереть молодым? Чтобы создать настоящую атмосферу апокалипсиса и безнадежности, зомби должно быть много.

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

С помощью публичного поля Period гейм-дизайнер сможет в Инспекторе задать, как часто нужно создавать нового врага. В поле Enemy мы укажем, какого именно врага нужно создавать (пока у нас враг только один, но в дальнейшем мы можем добавить еще). Ну а дальше все просто — с помощью TimeUntilNextSpawn мы отсчитываем, сколько времени осталось до следующего появления врага и, как только время пришло, добавляем в сцену нового зомби с помощью стандартного метода Instantiate. Ах да, в методе Start мы назначаем полю TimeUntilNextSpawn случайное значение, чтобы если у нас в уровне есть несколько спавнеров с одинаковыми задержками, они не добавляли зомби одновременно.

Остался один вопрос — как задать врага в поле Enemy? Для этого мы воспользуемся таким инструментом Unity, как “префабы” (“Prefabs”). По сути, префаб — это кусочек сцены, сохраненный в отдельный файл. Потом мы можем этот файл вставлять в другие сцены (или в эту же) и нам не нужно собирать его из кусочков каждый раз заново. Собрали мы, допустим, из объектов стен, пола, потолка, окон и двери какой-нибудь красивый домик и сохранили его в префаб. Теперь можно легким движением руки вставлять этот домик в другие карты. При этом, если отредактировать файл префаба (например, добавить к домику заднюю дверь), то объект изменится во всех сценах. Иногда это бывает очень удобно. Также мы можем использовать префабы как шаблоны для Instantiate — и этой возможностью мы воспользуемся прямо сейчас.

Чтобы создать префаб, достаточно просто перетащить игровой объект из окна иерархии в окно проекта, остальное Unity сделает сам. Давайте создадим префаб из зомби, а затем добавим в сцену спавнер врагов:

код для зомби в юнити 3д

Я в проекте для разнообразия добавил еще три спавнера (так что, в итоге, у меня их 4). И вот, что получилось:

код для зомби в юнити 3д

Вот! Это уже похоже на зомби-апокалипсис!

Заключение

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *