Как сделать шутер на юнити 3д
Перейти к содержимому

Как сделать шутер на юнити 3д

  • автор:

Создание шутера на Unity3D

3D шутер c мультиплеером в Unity / Урок #1 - Начало

3D шутер c мультиплеером в Unity / Урок #1 — Начало

Приступаем к видеокурсу по созданию шутера на Unity. Это будет полноценная 3D игра с возможностью мультиплеера. За курс мы узнаем как сделать шутер в Unity (Unity3D) с возможностью игры по серверу.

Видеоурок

Полезные ссылки:

  1. Официальный сайт Unity ;
Информация про Unity

Unity – это игровой движок, на котором строится большая часть современных игр на разных платформах. Посредством движка реально создавать игры, ориентированные на запуск на компьютерах (Windows, Linux, MacOS), мобильных телефонах, планшетных компьютерах (Android, iOS, Windows Phone) и даже игровых приставках PlayStation, Xbox, Nintendo.

Создание игр на движке Unity невозможно представить без написания дополнительного кода. В Unity поддерживается два языка программирования — JavaScript и C# . Раньше также поддерживался язык Boo, но позже от него отказались.

Вы можете самостоятельно выбрать на каком языке программировать в Unity, так как особого значения это не имеет. Большинство разработчиков дают предпочтение языку C# , так как он более мощный и найти различную документацию намного проще, нежели для языка JavaScript .

В Unity используется встроенные функции, которых нет ни в C#, ни в JavaScript. За счёт этого особо не имеет значения знает вы язык программирования или нет. Тем не менее, мы все же рекомендуем сперва пройти курс по изучению языка C#. Курс представлен на нашем сайте по этой ссылке .

Как сделать шутер на Unity?

Разработка игр на Unity ведется в два этапа:

  • построения дизайна через Unity графический дизайнер;
  • написание скриптов через язык C#.

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

Шутер на Юнити создается столь же просто, как и другие проекты.

План курса

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В верхнем левом углу на скриншоте находится окно Иерархии (“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. Ссылки на них вы сможете найти в конце этой статьи.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zombie : MonoBehaviour < // Start is called before the first frame update void Start() < >// Update is called once per frame void Update() < >>

Это стандартная заготовка для компонентов. Как мы видим, 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). У нас должен получиться вот такой скрипт:

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour < NavMeshAgent navMeshAgent; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); navMeshAgent.SetDestination(Vector3.zero); > // Update is called once per frame void Update() < >>

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

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour < NavMeshAgent navMeshAgent; Player player; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); player = FindObjectOfType(); > // Update is called once per frame void Update() < navMeshAgent.SetDestination(player.transform.position); >>

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

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

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Player : MonoBehaviour < NavMeshAgent navMeshAgent; public float moveSpeed; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); > // Update is called once per frame void Update() < Vector3 dir = Vector3.zero; if (Input.GetKey(KeyCode.LeftArrow)) dir.z = -1.0f; if (Input.GetKey(KeyCode.RightArrow)) dir.z = 1.0f; if (Input.GetKey(KeyCode.UpArrow)) dir.x = -1.0f; if (Input.GetKey(KeyCode.DownArrow)) dir.x = 1.0f; navMeshAgent.velocity = dir.normalized * moveSpeed; >>

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

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

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerCamera : MonoBehaviour < Player player; Vector3 offset; // Start is called before the first frame update void Start() < player = FindObjectOfType(); offset = transform.position - player.transform.position; > // Update is called once per frame void LateUpdate() < transform.position = player.transform.position + offset; >>

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

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

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

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

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

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

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

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

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

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

Теперь надо сказать 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.

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class MovementAnimator : MonoBehaviour < NavMeshAgent navMeshAgent; Animator animator; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); animator = GetComponentInChildren(); > // Update is called once per frame void Update() < animator.SetFloat("speed", navMeshAgent.velocity.magnitude); >>

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

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

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

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

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

navMeshAgent.updateRotation = false;
transform.rotation = Quaternion.LookRotation(navMeshAgent.velocity.normalized);

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

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

Я вижу цель

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

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

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

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

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

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

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

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Cursor : MonoBehaviour < SpriteRenderer spriteRenderer; int layerMask; // Start is called before the first frame update void Start() < spriteRenderer = GetComponent(); layerMask = LayerMask.GetMask("Ground"); > // Update is called once per frame void Update() < Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (!Physics.Raycast(ray, out hit, 1000, layerMask)) spriteRenderer.enabled = false; else < transform.position = new Vector3(hit.point.x, transform.position.y, hit.point.z); spriteRenderer.enabled = true; >> >

Поскольку курсор — это спрайт (двумерный рисунок), то для его отрисовки 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 добавим следующие строчки:

cursor = FindObjectOfType(); navMeshAgent.updateRotation = false;

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

Vector3 forward = cursor.transform.position - transform.position; transform.rotation = Quaternion.LookRotation(new Vector3(forward.x, 0, forward.z)); 

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

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

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

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

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Shot : MonoBehaviour < LineRenderer lineRenderer; bool visible; // Start is called before the first frame update void Start() < lineRenderer = GetComponent(); > // Update is called once per frame void FixedUpdate() < if (visible) visible = false; else gameObject.SetActive(false); >public void Show(Vector3 from, Vector3 to) < lineRenderer.SetPositions(new Vector3[]< from, to >); visible = true; gameObject.SetActive(true); > >

Этот скрипт, как вы, наверное, уже догадались, нужно добавить к игровому объекту 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. Разместим его так, чтобы он находился прямо возле дула пистолета:

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

Shot shot; public Transform gunBarrel;

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

shot = FindObjectOfType();

И в метод Update:

if (Input.GetMouseButtonDown(0))

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

Если теперь запустить игру, то мы наконец-то сможем стрелять по зомби!

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

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

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

var direction = (to - from).normalized; RaycastHit hit; if (Physics.Raycast(from, to - from, out hit, 100)) to = new Vector3(hit.point.x, from.y, hit.point.z); else to = from + direction * 100;

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

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

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

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

Зомби — зомбячья смерть!

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

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

CapsuleCollider capsuleCollider; Animator animator; MovementAnimator movementAnimator; bool dead;

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

capsuleCollider = GetComponent(); animator = GetComponentInChildren(); movementAnimator = GetComponent();

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

if (dead) return;

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

public void Kill() < if (!dead) < dead = true; Destroy(capsuleCollider); Destroy(movementAnimator); Destroy(navMeshAgent); animator.SetTrigger("died"); >>

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

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

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

if (hit.transform != null) < var zombie = hit.transform.GetComponent(); if (zombie != null) zombie.Kill(); >

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

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

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

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

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

  • Position: Y 0.5
  • Rotation: X -90
  • Duration: 0.2
  • Looping: false
  • Start Lifetime: 0.8
  • Start Size: 0.5
  • Start color: зеленый
  • Gravity Modifier: 1
  • Play on Awake: false
  • Emission:
  • Rate over Time: 100
  • Shape:
  • Radius: 0.25

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

GetComponentInChildren().Play();

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

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

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

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

using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemySpawner : MonoBehaviour < public float Period; public GameObject Enemy; float TimeUntilNextSpawn; // Start is called before the first frame update void Start() < TimeUntilNextSpawn = Random.Range(0, Period); >// Update is called once per frame void Update() < TimeUntilNextSpawn -= Time.deltaTime; if (TimeUntilNextSpawn > >

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

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

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

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

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

Заключение

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

В общем, друзья, надеюсь вам понравилась моя статья. Пишите ваши вопросы в комментариях, постараюсь ответить. Исходный код проекта можно скачать на гитхабе: https://github.com/zapolnov/otus_zombies. Вам потребуется Unity 2019.3.0f3 или выше, его можно скачать совершенно бесплатно и без СМС с официального сайта: https://store.unity.com/download.

Ссылки на ассеты, использованные в статье:

  • https://assetstore.unity.com/packages/3d/environments/industrial/rpg-fps-game-assets-for-pc-mobile-industrial-set-v2-0-86679
  • https://assetstore.unity.com/packages/3d/characters/toony-tiny-people-demo-113188
  • https://assetstore.unity.com/packages/2d/gui/icons/crosshairs-plus-139902

Создаём простой зомби-шутер на Unity

Аватарка пользователя Дмитрий Головин

Преподаватель курса по геймдеву Николай Запольнов пошагово описывает процесс создания простейшего шутера на Unity. Статья подойдёт новичкам.

Обложка поста Создаём простой зомби-шутер на Unity

В преддверии старта нового потока курса «Unity Game Developer. Professional», подготовили статью по мотивам урока от преподавателя курса — Николая Запольнова, где он пошагово разъяснил процесс создания простейшего шутера на Unity.

Статья нацелена в первую очередь на новичков с базовыми знаниями терминологии программирования. Главной её целью является желание показать, что при некотором багаже знаний вы сможете довольно скоро освоить работу с движком и создадите свои первые проекты.

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

Введение в Unity

Можете пропустить раздел, если вы уже знакомы с этим редактором. Переходите к разделу «Игровой мир: начало».

Базовым блоком при создании шутера на Unity является «сцена», представляет, обычно, 1 уровень игры. Однако, бывает так, что одна сцена включает несколько уровней, или один объёмный уровень дробится на ряд сцен. Сцена, как матрёшка, состоит из объектов, наполненных компонентами.

Именно компоненты делают интерфейс живым и привлекательным, обеспечивают такие функции как, например, визуализация, анимация, реалистичность. С помощью C# разработчик может сам написать их, сформировать нужную ему логику будущей игры. Перейдём непосредственно к движку.

После запуска редактора и создания нового файла, перед вами появится окно, состоящее из 4-х основных областей:

Создаём простой зомби-шутер на Unity 1

Слева вверху расположено окно Hierarchy, в котором отображается иерархия игровых объектов сцены: камеру (Main Camera), отображающую виртуальный мир глазами играющего, и источник света (Directional Light), определяющий направление освещённости сцены, без которого она выглядела бы как чёрное поле.

По центру расположено окно Scene, где, собственно, можно визуально редактировать уровень — вращать и передвигать с помощью мыши, получая наглядный результат. Справа от него находится вкладка Game (на скриншоте она неактивна). При переключении на неё кнопкой со значком воспроизведения сцена отображается через Main Camera.

В правой части экрана можно увидеть фрэйм Inspector — представляет собой набор полей с параметрами отмеченного объекта с возможностью их редактирования. На картинке видны 2 компонента: Transform, отвечающий за положение камеры, и, непосредственно, Camera — воплощает её функционал. Компонент Transform свойственен всем игровым объектам в Unity.

Внизу окна находится вкладка Project, содержащая цифровые объекты проекта (ассеты). По сути, это файл с данными, которые можно применить для создания сцен и интерфейса: текстуры, двумерные и трёхмерные элементы, звуковое сопровождение, анимация и конфигурации. Разработчики, не обладающие выдающимися навыками графического дизайна, могут загружать ассеты из Unity Asset Store в том числе и бесплатно. Также в Unity поддерживается опция загрузки файлов в стандартных форматах, например, png, jpg, fbx.

Справа от Project можете наблюдать неактивную вкладку Console. Здесь отображаются баги (ошибки), которые стоит периодически отслеживать, и сюда можно вводить части кода с целью отладки.

Игровой мир: начало

Неумение рисовать для разработчика игр — не приговор. В представленном примере взята графика из бесплатного раздела Unity Asset Store (ссылки внизу статьи), с помощью которой был собран элементарный уровень:

Создаём простой зомби-шутер на Unity 2

Выбранные элементы можно просто перетащить в окно проекта при помощи мыши и расположить по желанию:

Помимо этого, в Unity можно в один клик наполнять сцену типовыми объектами: сфера, куб, плоскость. Это делается нажатием правой кнопки мыши в области Hierarchy, из предложенного списка можно выбрать нужный элемент. В качестве примера — 3DObject⇨Plane. В представленной сцене из этих плоскостей с наложением текстуры собран асфальт. Текстура выбрана также в Unity Asset Store.

Важно!

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

И всё-таки оно движется!

Игровой мир уже вырисовывается, но на уровне пока отсутствует динамика. Согласно концепции, игрок будет противостоять атакам зомби, для этого они должны не только «видеть» персонажа игрока, двигаться в его сторону, но и огибать преграды на пути.

Для реализации этих функций используется инструмент «навигационная сетка» (Navigation Mesh). По конфигурации уровня он вычисляет пространства, где можно двигаться, группирует их и даёт возможность поиска оптимального расстояния между двумя точками сцены. Информация сохраняется в ассет с возможностью последующего редактирования, это называется «запекание» (baking). Для более сложных сцен с динамичными преградами используется компонент NavMeshObstacle.

Обратите внимание, чтобы Unity мог распознать объекты верно, следует в области “Inspector” напротив объектов отметить Navigation Static и кликнуть стрелку вниз для параметра Static:

Создаём простой зомби-шутер на Unity 4

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

В меню нужно выбрать Window⇨AI⇨Navigation⇨Bake. В открывшейся вкладке Unity попросить обозначить физические параметры объектов: радиус персонажа, высоту ступеней и так далее. Оставляем предустановленные значения, нажимаем кнопку Bake.

После проведения нужных расчётов Unity выдаст результат:

Создаём простой зомби-шутер на Unity 6

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

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

Перейдём к персонажу зомби. Для его создания нужно добавить объёмную модель из ассетов и компонент NavMeshAgent:

На языке C# разработаем компонент, чтобы персонаж начал движение, а NavMeshAgent знал направление.

В Unity нужно выбрать корневую папку Assets, где нажатием правой кнопки мыши будет создана папка Scripts. Именно в ней будут находится все скрипты для облегчения поиска. В созданной папке создаём файл C# с именем Zombie и добавим в соответствующий игровой объект:

Скрипт открывается двойным щелчком левой кнопки мыши. Далее представлен результат в Unity:

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zombie : MonoBehaviour < // Start is called before the first frame update void Start() < >// Update is called once per frame void Update() < >> 

Unity подключил библиотеки для типовой заготовки System.Collections и System.Collections.Generic, которые часто необходимы в коде игр, а также UnityEngine с интерфейсом прикладного программирования движка.

Автоматически был создан класс под названием Zombie, которое совпадает с именем файла. Это важно, поскольку так Unity соотносит компонент и относящийся к нему скрипт.

Класс является производным от MonoBehaviour (базового класса для пользовательских компонентов). Он содержит 2 метода, которые будут вызываться автоматически: Start — после загрузки сцены, Update — покадрово. Это одни из множества аналогичных функций, которые вызывает движок. С полным перечнем можно ознакомиться здесь: https://docs.unity3d.com/Manual/ExecutionOrder.html.

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

Доступ к нужному классу можно получить, применив метод GetComponent (метод вызывается из компонента и возвращает значение в виде ссылки на любой другой компонент того же объекта). Создадим в классе поле NavMeshAgent navMeshAgent, с помощью Start получим ссылку на NavMeshAgent, и зададим движение в точку с координатами (0;0;0). Итоговый код должен иметь вид:

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour < NavMeshAgent navMeshAgent; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); navMeshAgent.SetDestination(Vector3.zero); > // Update is called once per frame void Update() < >> 

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

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

Игровой объект Player создаётся так же, как и для зомби — на основе 3D-модели. Выберем теперь полицейского, дополним компонентом NavMeshAgent и скриптом с соответствующим названием.

Файл с кодом Player пока остаётся без изменений, при этом потребуется внести исправления в скрипт Zombie. Игрока лучше наделить более высоким приоритетом, указав для свойства Priority в компоненте NavMeshAgent число меньше 50. Таким образом, при встрече на карте игрок будет иметь более крепкую позицию и сможет оттолкнуть зомби.

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

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour < NavMeshAgent navMeshAgent; Player player; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); player = FindObjectOfType(); > // Update is called once per frame void Update() < navMeshAgent.SetDestination(player.transform.position); >> 

Проверяем, находит ли зомби цель для нападения:

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

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

Важно!

Такое решение задачи принято для упрощения кода. Однако, общепринятым считается использование Input.GetAxis и биндинг через Project Settings⇨Input Manager.

Настало время внести корректировки в скрипт Player:

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Player : MonoBehaviour < NavMeshAgent navMeshAgent; public float moveSpeed; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); > // Update is called once per frame void Update() < Vector3 dir = Vector3.zero; if (Input.GetKey(KeyCode.LeftArrow)) dir.z = -1.0f; if (Input.GetKey(KeyCode.RightArrow)) dir.z = 1.0f; if (Input.GetKey(KeyCode.UpArrow)) dir.x = -1.0f; if (Input.GetKey(KeyCode.DownArrow)) dir.x = 1.0f; navMeshAgent.velocity = dir.normalized * moveSpeed; >> 

По примеру персонажа зомби при помощи Start получена ссылка на NavMeshAgent игрока, зафиксированная в поле класса. Также её следует добавить в поле moveSpeed. Его значение доступно для редактирования сразу в области Inspector в Unity, поскольку это общедоступное поле. Такая опция будет по достоинству оценена гейм-дизайнером, если он задействован в работе над проектом.

Устанавливаем значение скорости равное 10:

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

Вектор движения dir сформирован, теперь его нужно нормализовать и умножить на ранее заданную скорость. Результат итерации отправляется в navMeshAgent.velocity, нужное значение будет добавлено автоматически. Нормализация нужна, чтобы вектор не был длиннее единичного, а объект двигался с равной скоростью как по диагонали, так и по прямой.

Теперь персонажи имеют одинаковую подвижность, что можно проверить при запуске игры:

Важно, чтобы камера двигалась вместе с персонажем. В этом поможет несложный скрипт, назовём PlayerCamera:

using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerCamera : MonoBehaviour < Player player; Vector3 offset; // Start is called before the first frame update void Start() < player = FindObjectOfType(); offset = transform.position - player.transform.position; > // Update is called once per frame void LateUpdate() < transform.position = player.transform.position + offset; >> 

Главное, чтобы он был понятен. В качестве особенностей стоит отметить замену Update на LateUpdate — схожий по логике, но выполняется только после всех скриптов уровня. Эта замена используется, поскольку NavMeshAgent должен рассчитать новое расположение персонажа до перемещения камеры. В противном случае глаз будет резать эффект «подёргивания». После закрепления этого компонента за объектом Main Camera, при запуске игры, камера будет следовать за главным персонажем.

Во-первых, анимация — это красиво

После того, как персонажи игры стали подвижные, настал момент сделать их реалистичнее и привлекательнее. В этом поможет компонент Animator и инструмент Animator Controller.

Последний задаёт разные автоматически сменяемые состояния объекта, к которым привязывается определённая анимация. Чтобы применить этот инструмент к объекту, надо создать вложенную папку Animator Controller в директории Animations. Переименуем её в Zombie. Редактор примет вид:

Создаём простой зомби-шутер на Unity 13

На данном этапе состояния отсутствуют. Есть точки входа и выхода: Entry и Any State, Exit. Можно добавить несколько анимацией из ассетов:

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

Нужно выбрать кнопку Parameters в левом верхнем углу, далее нажать на «+», выбрав параметр типа float. Новый параметр назовём speed:

Следует создать 2 перехода: Z_idle_A в Z_run и в обратную сторону, чтобы воспроизводилась анимация при значении speed больше 0.

Чтобы перейти из idle в run нужен двойной щелчок мыши по прямоугольнику Z_idle_A, выбираем Make Transition. Далее настраиваем параметры появившейся стрелки. Нужно отжать чекбокс Has Exit Time, в противном случае анимация будет воспроизводиться не по нашему условию. Также в списке Conditions необходимо выбрать «+», чтобы значение speed было больше 0.

Аналогичный алгоритм при переходе в обратную сторону. Теперь условие speed принимает значение меньше 0.0001. Параметр float пока не проверен на равенство:

Контроллер переместим мышью в соответствующее поле компонента Animator к объекту Zombie:

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

Далее необходимо отправить запрос о векторе скорости у NavMeshAgent в Update, рассчитать длину и передать результат в параметр скорости speed.

Чтобы анимировать зомби, надо добавить MovementAnimator в объект Zombie:

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class MovementAnimator : MonoBehaviour < NavMeshAgent navMeshAgent; Animator animator; // Start is called before the first frame update void Start() < navMeshAgent = GetComponent(); animator = GetComponentInChildren(); > // Update is called once per frame void Update() < animator.SetFloat("speed", navMeshAgent.velocity.magnitude); >> 

Код управления анимацией, помещённый в отдельный компонент MovementAnimation, даёт возможность не создавать контроллер игрока заново, а скопировать уже созданный для зомби. Чтобы скопировать, следует выбрать файл Zombie и нажать горячие клавиши Ctrl+D. Нужно внести изменить анимации на m_idle_А и m_run.

Потребуется ещё ряд дополнений

В класс Zombie добавим несколько строк.

navMeshAgent.updateRotation = false; 
transform.rotation = Quaternion.LookRotation(navMeshAgent.velocity.normalized); 

В NavMeshAgent задан такой угол поворота, что персонаж с задержкой во времени реагирует на изменение траектории движения. Добавив эти 2 строчки, можно избавиться от такого недочёта: первая строка передаёт NavMeshAgent сигнал о снятии управления поворотом (сделаем это сами); вторая — назначает поворот в сторону по вектору движения.

Важно!

Чтобы назначить поворот объекта, в 3D-графике используются эйлеровы углы, матрицы поворота или кватернионы. Первые 2 подвержены эффекту шарнирного замка (Gimbal Lock) и часто не удобны в работе, поэтому в данном примере использован кватернион. Даже при использовании матриц и углов Эйлера в Unity, они всё равно хранятся в кватернионах. В остальном, инструментарий движка настолько удобен, что позволяет разработчику не вникать в математические тонкости.

Вижу цель, вижу препятствия

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

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

Создаём простой зомби-шутер на Unity 19

Человеческое зрение улавливает только элементы, попавшие в эту пирамиду. Система автоматически усекает пирамиду ближней и дальней плоскостями. Ближняя плоскость (окрашена жёлтым) находится со стороны экрана, который физически не может отображать элементы, расположенные к наблюдателю ближе него. Дальняя плоскость отсекается, поскольку ресурсы компьютера ограничены — невозможно продлить луч в бесконечность.

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

Создаём простой зомби-шутер на Unity 20

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

Выделив игровой объект, можно увидеть вверху инспектора выпадающий список Layer, начально значение которого Default. В списке нам интересен пункт Add layer…, предназначенный для редактирования слоёв. Нужно добавить слой с
названием Ground:

Всем плоскостям земли в сцене нужно назначить слой Ground, что позволит указать в скрипте метод Physics.Raycast для реализации проверок пересечений.

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

Курсору добавлен поворот вокруг оси X на 90° (пусть лежит на земле), масштаб: 0.25, Y = 0.01. Значение Y задано с целью избежать эффект Z-fighting. Для определения близости объектов к камере, видеокартой производятся расчёты с плавающей точкой, и если задать значение равное 0 для курсора и для земли, то они начнут «спорить». В итоге курсор будет частично просвечиваться и мерцать при перемещении. Значение 0.01 оптимально, чтобы сократить оплошности в расчетах видеокарты, при этом визуально не даёт ощущение, что курсор парит над землёй.

Далее назовём объект Cursor, добавим скрипт с аналогичным именем и следующим кодом:

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Cursor : MonoBehaviour < SpriteRenderer spriteRenderer; int layerMask; // Start is called before the first frame update void Start() < spriteRenderer = GetComponent(); layerMask = LayerMask.GetMask("Ground"); > // Update is called once per frame void Update() < Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (!Physics.Raycast(ray, out hit, 1000, layerMask)) spriteRenderer.enabled = false; else < transform.position = new Vector3(hit.point.x, transform.position.y, hit.point.z); spriteRenderer.enabled = true; >> > 

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

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

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

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

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

Самое время дополнить скрипт Player. Сперва нужно добавить поле Cursor cursor. После допишем несколько строк в Start:

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

Vector3 forward = cursor.transform.position - transform.position; transform.rotation = Quaternion.LookRotation(new Vector3(forward.x, 0, forward.z)); 

Координата Y по-прежнему 0.01.

Стрельба по зомби как призвание

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

Нужно создать новый объект Shot и добавить к нему LineRenderer. В поле Width обозначается ширина — 0.04 будет достаточно. На изображении видно, что объект имеет ярко-фиолетовый цвет — так в Unity обозначаются объекты без материала.

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

В папку Materials положим материал под названием Yellow. Для него выберем стандартный шейдер Unlit/Color. Он не учитывает освещённость, поэтому пулю будет хорошо видно.

Применяем жёлтый цвет к объекту:

Добавим скрипт Shot:

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Shot : MonoBehaviour < LineRenderer lineRenderer; bool visible; // Start is called before the first frame update void Start() < lineRenderer = GetComponent(); > // Update is called once per frame void FixedUpdate() < if (visible) visible = false; else gameObject.SetActive(false); >public void Show(Vector3 from, Vector3 to) < lineRenderer.SetPositions(new Vector3[]< from, to >); visible = true; gameObject.SetActive(true); > > 

Этот скрипт добавляем к объекту Shot.

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

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

В скрипте Player используем публичный метод Show для отображения пули при выстреле. Перед этим, чтобы выстрел производился из верного отверстия, нужно определить координаты дула оружия. После того, как в трёхмерной модели героя нашли объект Bip001⇨Bip001Pelvis⇨Bip001 Spine⇨Bip001 R Clavicle⇨Bip001 R UpperArm⇨Bip001 R Forearm⇨Bip001 R Hand⇨R_hand_container⇨w_handgun, нужно добавить в него «дочку» GunBarrel, расположив максимально близко к дулу.

Теперь скрипт Player дополним следующими полями:

Shot shot; public Transform gunBarrel; В Start скрипта Player впишем: shot = FindObjectOfType(); В Update тоже: if (Input.GetMouseButtonDown(0))

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

Теперь при запуске игры персонаж может отстреливаться от зомби.

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

Исправить это довольно просто­ — в классе Player между строками var to = … и shot.Show(…) допишем строки:

var direction = (to - from).normalized; RaycastHit hit; if (Physics.Raycast(from, to - from, out hit, 100)) to = new Vector3(hit.point.x, from.y, hit.point.z); else to = from + direction * 100; 

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

Однако, создатели ассета добавили объектам сцены коллайдер, кроме персонажей, поэтому пуля по-прежнему будет пролетать через зомби. Исправим это упущение.

Коллайдер является компонентом для определения столкновений объектов в сцене, обычно имеет простую геометрическую форму (куб, сфера, например). Точность столкновений при этом невысока, зато формулы пересечений простые и не требуют сложных вычислений. В случаях, когда точность важнее производительности, можно использовать MeshCollider. Нам подойдёт пока стандартный компонент CapsuleCollider:

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

Зомби — сдохни или умри!

Первым делом в Animation Controller зомби нужно добавить анимацию смерти. Перетаскиваем туда саму анимацию AssetPacks⇨ToonyTinyPeople⇨TT_demo⇨animation⇨zombie⇨Z_death_A. Параметр died с типом trigger создаётся для её активации. Триггер, в отличие от других параметров, выступают как вызов функции, их значение откатывается к начальному после выполнения перехода. Игрок одинаково успешно может убить как идущего зомби, так и застывшего на месте, поэтому переход следует добавить из состояния Any State:

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

CapsuleCollider capsuleCollider; Animator animator; MovementAnimator movementAnimator; bool dead; 

В Start класса Zombie внесём:

capsuleCollider = GetComponent(); animator = GetComponentInChildren(); movementAnimator = GetComponent(); 

Добавим проверку в начале метода Update:

if (dead) return; 

В классе Zombie укажем метод Kill:

public void Kill() < if (!dead) < dead = true; Destroy(capsuleCollider); Destroy(movementAnimator); Destroy(navMeshAgent); animator.SetTrigger("died"); >> 

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

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

В качестве финального аккорда в класс Player, метод Update, в месте вызова Physics.Raycast, к описанию случая обнаружения пересечения, необходимо добавить проверку:

if (hit.transform != null) < var zombie = hit.transform.GetComponent(); if (zombie != null) zombie.Kill(); > 

Вызов Physics.Raycast передаёт данные о пресечении в переменную hit. А именно, появится ссылка на компонент Transform объекта, с которым пересекся луч, в поле transform. Зомби будет убит, если в объекте обнаружен компонент Zombie.

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

Кликаем правой кнопкой мыши по объекту Zombie, чтобы добавить систему частиц (Effects⇨Particle System):

Как сделать Шутер на Unity за 30 минут

Привет, друзья! В этом уроке мы создадим 3д шутер на Юнити за кратчайший срок: нам понадобится всего 30 минут! Не верите? Долистайте эту статью до конца, чтобы посмотреть наш урок по Юнити. В нашем шутере можно будет играть от первого лица, давайте скорее начинать!

Для начала нам потребуются стандартные ассеты, которые мы можем скачать в Ассет Стор. Открываем Asset Store, вбиваем в поисковую строку Standard Assets. Создаем Plane. Растягиваем его площадь. В папке Character заходим в первую папку и в папке префаб перемещаем на сцену первый префаб. Так как у персонажа есть камера, основная камера нам не нужна, поэтому удаляем ее. Снова переходим в Ассет Стор, выбираем категории и жмем на Props.

шутер на юнити

Перемещаем префаб оружия на сцену и настраиваем его расположение.

шутер +на unity

Если оружие искажено, отражается не полностью, кликаем в иерархии на камеру и в инспекторе в Culling Mask — 0.1.

шутер 3д юнити

В иерархии перемещаем оружие в камеру.


Создаем скрипт, в нем объявляем переменные.

  • урон — damage
  • сила стрельбы — fireSpeed
  • объект камеры — cam
  • дальность стрельбы — range

простая игра на unity
Создаем функцию, в ней объявляем луч. RaycastHit hit.
Пишем if и в нем:

  • Physics.Raycast — это и есть луч. Далее указываем, откуда будет луч исходить. cam.transform.forward — направление луча.
  • out hit — результат выпуска луча.
  • range — ограничиваем выстрел

игры unity шутеры
И когда проверка будет пройдена, мы в консоль будем выводить слово enter.

unity создание шутера

Теперь нам нужно вызвать эту функцию, для этого в Update пишем: если нажали ЛКМ, то вызываем функцию выстрела.

unity создаем шутер

Запускаем и проверяем.

unity 3d шутер
Добавим теперь след от пули, саму пулю и звуки. Объявляем переменные:

  • префаб пули — bull;
  • след от пули — trace;
  • точка спавна — spawnBull;
  • звук — shot;
  • компонент воспроизведения — audioShot;
  • сила force = 310;

unity 3d создать игру
И в функции shootGun пишем воспроизведение звука.

2д шутер на юнити

Воспроизведение системы частиц (следа от пули).

2д игра на unity

И спавн пули.

2d шутер +на unity

Далее переходим в Юнити, создаем пустой объект в пистолете, это и будет точка спавна, добавляем ей иконку.

как сделать шутер на юнити

Настраиваем расположение, ставим ее в начало дула. Теперь в папке ParticleSystem выбираем префаб Flare и перемещаем так же к дулу. В иерархии систему частиц перемещаем на точку спавна. Снова выбираем систему частиц и настраиваем эффект, нам нужно убрать две галки Looping и Play Awake. Делаем так же и во вложенных объектах частиц. Это позволит проигрывать эффект один раз.
На оружие в инспекторе добавляем компонент AudioSource. Теперь заполняем скрипт:
Cam — добавляем камеру.
Bull — находим в проекте префаб пули.
Trace — добавляем созданый эффект из иерархии.
Spawn Bull — точка спавна.
Shot — нажимаем на кружок и выбираем звук.
AudioShot — добавляем оружие потому что, на нем есть этот компонент.

как сделать шутер +на unity

Создадим отдельный скрипт для пули и в нем объявлим скорость пули. Далее прикладываем силу и уничтожаем ее через 0.5 секунд. Перемещаем скрипт на префаб пули.

как делать игры на unity

Также на префаб добавляем Capsule collider и ставим галку isTrigger. Теперь создадим новый скрипт, который будет уничтожать объект, в который мы попали.

создание 3d игры на unity

Нажимаем в окне проекта Юнити на кнопку All prefab, находим любой объект врага. Перемещаем его на сцену. Если на нем есть скрипты, отключаем их, сняв галку. Теперь переместим наш скрипт на врага. Чтобы скрипт сработал, на пулю нужно добавить тег bull. Создаем тег.

2д игра на unity

И назначаем его префабу пули. Запускаем и проверяем. Далее добавим силу: когда пуля будет попадать в объект, на него будет воздействовать сила. Переходим в скрипт gunManager, где у нас основная функция выстрела. Переменная силы у нас уже объявлена, нам осталось только прописать воздействие. Проверяем, есть ли физика у объекта. Если да, то мы прикладываем силу.

игру unity 3д

Сохраняем и проверяем.

Вот и все! Если вы хотите поближе познакомиться с разработкой игр на Unity 3D и программированием на языке C#, приходите к нам на курс. Мы в школе «Пиксель» учим детей 10-14 лет разбираться во всех инструментах Юнити, писать скрипт на Си Шарп, создавать многоуровневые сложные игры. Присоединяйтесь!

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

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