В пятой версии Unreal Engine появилась новая система управления - Enhanced Input.
Разберёмся с подключением системы в некий абстрактный проект (пусть будет шутер - без разницы, FPS или TPS).
Что нам потребуется?
Этап 1 - Создаём блюпринты
Нам понадобятся: режим игры (GameMode), контроллер игрока (Player Controller), персонаж (Character).
Чайнико-справка:
Game Mode - это "во что играем". Обычно нём задаются и хранятся данные, определяющие состояние игрового процесса, в частности - и следующие два пункта.
Player Controller - это "как играем". Unreal - очень гибкая система, и любой персонаж гипотетически может управляться игроком. Как это будет делаться - отчасти задаётся в Player Controller, отчасти - в персонаже, отчасти (в зависимости от конкретной игры) - в других местах.
Character - это "кем играем". Здесь собиратся всё, связанное с персонажем. Отображаемая модель, анимации, физическая модель и прочее, прочее, прочее.
У каждого проекта в Unreal (в том числе - пустого) уже есть предсозданные блюпринты. При желании можно поменять их, при желании - создать новые.
Пойдём от варианта, что у нас ничего нет, и всё создаем руками.
- Открываем Content Browser (по умолчанию - Ctrl + Space).
- Щелкаем правой кнопкой мыши (далее - ПКМ) по любому свободному месту.
- В открывшемся списке выбираем пункт CREATE BASIC ASSET -> Blueprint Class.
- откроется меню создания нового блюпринта. Все необходимые нам вещи относятся к часто создаваемым и вынесены на панель слева:
- Щелкаем по кнопке Character. Окно закроется, в контент браузере появится иконка нового блюпринта. Предлагается вбить в неё какое-нибудь название.
Чайнико-справка:
Для себя имя можно вбивать любое - лишь бы самому было понятно. В индустрии же есть определенные стандарты:
1. Название должно быть уникальным (то есть, не совпадать с другими ассетами того же класса в проекте);
2. Название начинается с короткого префикса заглавными буквами, из которого можно понять тип ассета (BP_ для блюпринтов, LS_ для левел сиквенсов, и так далее);
3. Название должно быть читаемым и понятным (тот же GameMode намного понятнее, чем HVF*Yh874hfiu, правда?);
4. Название должно быть насколько можно коротким и информативным (представьте, что у вас в проекте лежим несколько десятков гигабайт различных ассетов, и всё это разложено по очень ветвистой структуре папок и подпапок. Чем длиннее адрес до финального ассета - тем дольше движок будет до него добираться, поэтому хорошей практикой считается сокращать всё, что можно без потери смысла);
Для нового Game Mode, например, подойдет название BP_GameMode_FPS.
Из него видно, что это: блюпринт; конкретная разновидность блюпринта - Game Mode; FPS - расхожее сокращение для First Person Shooter - стрелялки с видом от первого лица.
- Повторяем шаги выше для создания всех нужных сущностей (повторюсь, нужны Character, Player Controller, Game Mode).
- Открываем созданный GameMode. В разделе Classes выбираем нужно установить в Player Controller Class - созданный нами Player Controller, в Default Pawn Class - созданный нами Character.
- Нажимаем кнопки Compile и Save. Если всё сделано правильно, то на кнопке Compile будет значок с зеленой галочкой.
Этап 2 - Создаём Input Action'ы
Input Action - действие, которое мы хотим выполнять с помощью контроллера и модификация его ввода (по необходимости). "Я хочу нажать W на клавиатуре и идти вперёд", или "я хочу зажать ПКМ, чтобы прицелиться, и вернуться в обычный режим, отпустив ПКМ" - примеры таких действий.
Input Action как игровой ассет хранит в себе абстрактные данные, необходимые для выполнения такого действия.
Для создания нового Input Action:
- Открываем Content Browser (по умолчанию - Ctrl + Space).
- Щелкаем ПКМ по любому свободному месту.
- В открывшемся списке выбираем пункт CREATE ADVANCED ASSET -> Input Action.
- Появится новый ассет. Зададим ему название и откроем двойным щелчком по иконке. Увидим вот такую картину:
Спрашивается, и где же здесь задавать кнопку, "чтобы я нажал и оно пошло?".
Это будет дальше, а пока - нужно представить нужное действие и задать для него необходимые параметры. Так что разберём часть настроек, которые могут пригодиться для создания управления:
Value Type
Указывает, в каких состояниях может находиться данный параметр. Для простоты разберем каждый вариант на примерах:
- Digital (bool) - булеана, то есть у этой переменной могут быть только два значения - или 0, или 1. Никаких промежуточных значений вроде 0,3. Где используется: для действия, которое требует от игрока одиночного нажатия на клавишу (например, выстрел или прыжок).
- Axis1D (float) - число с плавающей точкой, которым можно описать движение по одной оси. На флоаты часто подвязывают анимацию персонажа, когда требуется плавное и красивое изменение состояния, а не резкий переход из одного состояние в другое (что дало бы использование для этой операции булеаны). Пример: в шутере - нажал на кнопку - выглянул из-за угла, отпустил - вернулся в прежнее положение.
- Axis2D (Vector 2D) - вектор, построенный в двух измерениях (в плоскости). Плоскость может быть любой - XY, YZ, XZ. Это - вечная основа для управления игрока во всевозможных action, rpg, shooter - в играх с высокой динамикой и постоянным инпутом. Примеры: ходить вправо-влево и вперед-назад, смотреть по сторонам мышкой.
- Axis3D (Vector) - полноценный вектор, существующий в трёх измерениях. Довольно экзотическая вещь в управлении персонажем, и встречается не часто. Примеры: управление космическим или подводным кораблём, самолётами-вертолётами.
Trigger
В этом разделе заранее можно указать какой тип инпута ожидается и указать дополнительные параметры для данного типа ввода (например, время между двумя вводами данного типа). Поскольку UE - многоплатформенный движок, предполагающий создание приложений и для ПК, и для консолей, и для смарт-устройств - типы инпута на любой вкус.
Для ПК и консолей наиболее актуальны Pressed (зарегистрировано нажатие кнопки), Released (зарегистрировано отпускание кнопки), Hold (зарегистрировано нажатие кнопки на заданное время), Hold And Release (зарегистрировано нажатие кнопки на заданное время, после чего кнопка отпущена).
Возможно, на досуге разберем этот раздел в деталях и с примерами, как было с прошлым разделом.
Modifiers
Модификаторы - разнообразные функции, позволяющие или изменить входящий инпут, или завязать на него какие-то дополнительные функции.
Чайнико-справка:
Нажатие кнопки на клавиатуре с точки зрения пользователя бинарно - кнопка или нажата, или нет; или 0 , или 1.
С точки зрения движка - так бывает не всегда. Например, перемещение в игровом пространстве, как уже говорилось выше - это вектор. Длина вектора - это скорость персонажа, то есть, сколько условных "клеточек" поля персонаж игрока проходит за единицу времени.
Но игрок не может (ладно, может - при определенных настройках) сразу набрать максимальную скорость - это бы смотрелось неестественно. Поэтому в современных игровых движках симуляция движения игрока подражает действительности.
Простой пример для понимания - забудьте, что ваш аватар в игре выглядит как человечек. Представьте на его месте обычный куб из набора примитивов движка. Куб имеет определенные размеры и ряд других параметров, например, массу. Чтобы переместить куб определенной массы на определенное расстояние за определенное время, к кубу надо приложить определенную физическую силу. Сила сообщает кубу ускорение, и куб постепенно набирает скорость.
Пока вы нажимаете кнопку на контроллере - к вашему персонажу применяется такая сила. Для плавности движения она тоже изменяется со временем - как плавающая переменная, от 0 до 1 (или от 0 до 100%, если так проще). Эти изменения привязаны к машинному времени - к тикам, и поэтому невооруженным взглядом практически незаметны.
Это максимально упрощенное представление перемещения персонажа в движке, и оно далеко от куда более сложной действительности.
Однако мы отвлеклись.
Математическую часть модификаторов можно представить в формульном виде как y = f(x), где x - значение сигнала инпута игрока, представленное в одном из видов (что задаётся выше, в Value Type), а y - результирующее значение после обработки, которое и будет использоваться в дальнейших расчётах.
Самый простой пример для этого - модификатор Negate, возвращающий отрицательное значение при положительном вводе и положительное - при отрицательном.
То есть, это y = -x.
Один этот раздел и его математическое представление - уже тема для отдельной статьи. Возможно, позже появится и она. Но вы же здесь не за этим?
Так что перейдём к рубрике "готовые рецепты".
Готовые рецепты - управление
Раздел составлялся на разборе примеров контента от Epic Games, за что разработчикам большое спасибо.
Перемещение в двух измерениях (вперед-назад и вправо-влево) - создаём Input Action c Value Type = Axis2D (Vector 2D). Больше ничего не требуется.
Круговой обзор с помощью мыши - создаём Input Action c Value Type = Axis2D (Vector 2D). Больше ничего не требуется.
Простое действие (выстрел, прыжок, приседание) - создаём Input Action c Digital (bool). Опционально можно добавить триггеры Pressed и Released.
Этап 3 - Создаём Input Mapping Context
Input Action'ы были абстракцией - теперь переходим к конкретике.
Input Mapping Context (IMC) связывает ранее созданные нами Input Action'ы с заданными клавишами на устройстве ввода и позволяет добавить дополнительные триггеры и модификаторы (те же, что у Input Action - но здесь их можно отдельно настроить на отдельные случаи ввода, а не на всё разом).
Для создания нового Input Mapping Context:
- Открываем Content Browser (по умолчанию - Ctrl + Space).
- Щелкаем ПКМ по любому свободному месту.
- В открывшемся списке выбираем пункт CREATE ADVANCED ASSET -> Input Mapping Context.
Появится новый ассет. Зададим ему название и откроем двойным щелчком по иконке. Увидим вот такую картину:
В разделе Mappings нажимаем на иконку с плюсом, чтобы добавить новый маппинг. Нельзя добавить более одной пустой строчки, так что действуем по циклу - нажали плюсик, добавили в появившуюся запись один из ранее созданных маппингов. Повторять до тех пор, пока все нужные действия не будут на месте.
Чайнико-справка:
Не обязательно набивать все Input Action'ы в один IMC. Можно сделать по отдельному Input Mapping Context для различных ситуаций - например, в один убрать всё Input Action'ы, относящиеся к движению, в другой - к стрельбе, третий - к ремесленной системе, и т.д.
Опять пример из шутера:
У вас есть несколько видов оружия. Револьвер стреляет одиночными патронами и не имеет альтернативных режимов огня. Штурмовая винтовка умеет стрелять одиночными выстрелами, очередями - и ещё на ней установлен прицел, через который игрок может - вот сюрприз! - целиться. И есть лазерная пушка, которая какое-то время разогревается перед выстрелом, и затем стреляет лучом, пока игрок удерживает кнопку огня.
Все перечисленные варианты можно реализовать, не изменяя глобальной раскладки управления - если у каждого вида оружия будет собственный IMC, который применяется к игроку при экипировке этого оружия и удаляется при его снятии.
Теперь разворачиваем созданные записи, и начинаем их заполнять.
Чтобы добавить новый инпут, нужно нажать на иконку с плюсом рядом с названием Input Action'а.
Задать нужное действие можно или выбрав его из раскрывающегося списка, или щелкнув по иконке клавиатуры и затем нажав на устройстве управления нужную клавишу. Последний вариант удобнее, но некоторые методы ввода так, к сожалению, не задать (например, перемещение мыши по плоскости).
К одному Input Action'у можно указать произвольное количество клавиш, оказывающих на него влияние - хоть все доступные. То есть, можно задать дублирующую раскладку (например, перемещение будет не только по стандартной для игр с видом от первого и третьего лица WASD, но и по стрелочкам в случае клавиатуры и по отклонению стика на геймпаде).
Готовые рецепты - управление
WASD или стрелочки (перемещение по X и Y)
Для W указываем модификатор Swizzle Input Axis Values и выбираем Order = YXZ.
Для S - модификатор Swizzle Input Axis Values и выбираем Order = YXZ, а так же модификатор Negate (проставляем чекбоксы X, Y, Z).
Для A - модификатор Negate (проставляем чекбоксы X, Y, Z).
Для D - ничего не указываем.
Чайнико-справка:
Почему так? Почему для A и D не указывается порядок осей? Что тут делает Negate?
Всё довольно просто.
Чтобы задать направление движения традиционно используются два вектора: форвард-вектор (который указывает вперед) и райт-вектор (указывающий вправо). Чтобы получить "назад" - мы умножаем форвард-вектор на -1, что даёт тот же вектор, но в обратном направлении. Чтобы получить "влево" - ту же операцию проделываем с правым вектором.
Что касается порядка осей: в движке по умолчанию установлен порядок XYZ, который и применяется к A и D - поэтому для них ничего не приходится изменять. Для W и S мы задаем движение по другой оси, только и всего.
Mouse Look
В качестве "кнопки" задаем Mouse XY 2D-Axis.
Такой сценарий в основном используется для игр с видом от первого или третьего лица. При желании можно добавить модификатор Negate и проставить там чекбокс Y (инвертирование ввода по Y заставит камеру правильно наклоняться вверх и вниз).
Простые действия
То есть, прыжок, выстрел, приседание и прочее-прочее-прочее.
Для них только назначаем нужные кнопки, никаких модификаторов не требуется (на ранних стадиях разработки - точно).
Этап 4 - Привязываем Input Mapping Context
В начале статьи мы создавали блюпринты - теперь пришло время к ним вернуться. Открываем созданный нами PlayerController, и открываем вкладку Event Graph.
И - начинаем заниматься программированием.
Чайнико-справка:
Блюпринт создаётся из нодов - небольших блоков кода, написанного на C++, которые связываются воедино "проводками". С помощью "визуального программирования" (как это сейчас называют) можно довольно быстро собрать фрагмент работающей логики, а при должном энтузиазме - и целую игру. Работа с блюпринтами требует некоторых знаний в программировании, но на порядок меньше, чем при работе с тем же C++, не говоря уже о каком-нибудь Assembler.
Какие минусы?
Почти никаких. Кроме того, что код, написанный профессиональным программистом в "плюсах" не будет содержать ничего лишнего - значит, будет исполняться быстрее и неожиданно не поломается неизвестно почему. Тем не менее, с простыми задачами блюпринты справляются великолепно, и позволяют делать быстрые прототипы игровых механик. Если прототип оказался рабочим - его передают в отдел разработки со словами "сделайте тоже самое, только нормально".
Изначально в обычном блюпринте находится две базовые ноды - Event BeginPlay и Event Tick.
Event BeginPlay отрабатывает один раз при запуске игровой сессии (запустились, выполнили всё нужное - и всё, до следующего запуска с этим блоком игровой логики больше ничего не произойдет).
Event Tick постоянно обновляется (подключенная к этой ноде игровая логика исполняется каждый тик машинного времени).
В данном случае нам нужен Begin Play, поскольку мы один раз присваиваем игроку раскладку управлению, и далее она никак не меняется.
А теперь по пунктам:
- Щелкаем правой кнопкой мыши (далее - ПКМ) по любому свободному месту внутри окна Event Graph.
- Появляется список нодов, которые можно добавить. В поиск вбиваем Enhanced Input, затем находим раздел Local Player Subsystems и выбираем Get EnhancedInputLocalPlayerSubsystem. На экране появится соответствующая нода.
- На появившейся ноде нажимаем на голубой точке и при зажатой ЛКМ ведем курсор в сторону. Отрисуется "проводок", который можно подсоединить к другой ноде. Если вы отпустите ЛКМ на пустом поле, опять откроется список добавляемых нодов - но уже значительно сокращенный, поскольку из него будут исключены все ноды, которые нельзя подключить к базовой. Вытягиваем проводок на пустое место, в открывшемся списке выбираем Utilities -> Is Valid. На экране появится новая нода, связанная с ранее вызванной нами подсистемой.
- Вытягиваем проводок из пятиугольной стрелочки на Event BeginPlay и отпускаем его на такой же стрелочке на Is Valid (вход помечен как Exec).
Чайнико-справка:
Пятиугольные стрелочки Exec - это последовательность исполняемых команд. По ним удобно прослеживать что происходит в блюпринте. Она всегда идёт первой (то есть верхней в списке вводов-выводов). Если нода может вернуть несколько вариантов развития событий (как в случае с Is Valid - валидация может пройти успешно или нет - на выходе будет несколько Exec-контактов, к каждому из которых можно подвести свою логику).
- Вытягиваем проводок из Enhanced Input Local Player Subsystem и добавляем ноду Input -> Add Mapping Context. Затем протягиваем проводок из выхода Is Valid одноименной ноды и подключаем его к Exec-входу Add Mapping Context. В ноде Add Mapping Context вызываем список из Mapping Context и указываем в нём созданный ранее IMC.
Чайнико-справка:
А почему нельзя было протянуть проводок из Exec ноды Is Valid?
Если в меню добавления новых нод у вас стоит галочка Context Sensitive - поиск ничего не даст. За Enhanced Input отвечает отдельная подсистема - нода, которую мы создали первой. Эта подсистема "знает" о существовании нужной ноды, поэтому она и будет там в поиске.
Если вы только начинаете работу с блюпринтами, то отключать галочку Context Sensitive строго противопоказано - она нарочно стоит для упрощения жизни начинающих блюпринтистов, не позволяя подключать друг к другу несовместимые объекты.
- Нажимаем кнопки Compile и Save. Если всё сделано правильно, то на кнопке Compile будет значок с зеленой галочкой (вводите в привычку при работе с блюпринтами периодически компилировать собранный код, и сохранять промежуточные результаты работы. Пятая версия UE менее стабильна, чем четвертая, от неожиданных крашей движка никто не застрахован).
Все действия в этом блюпринте выполнены, его можно закрывать.
Этап 5 - Привязываем Input Action'ы
Открываем созданный ранее блюпринт персонажа, открываем вкладку Event Graph. Input Actions, подключенные к IMC (которую в прошлом шаге мы вшили в Player Controller) теперь можно будет вызывать здесь через добавление нодов.
Управление движением (WASD)
- Щелкаем ПКМ по любому свободному месту внутри окна Event Graph, в разделе Enhanced Action Events находим созданный ранее Input Action (проще всего его искать по имени).
- У созданной ноды щелкаем на синей точке с подписью Action Value правой кнопкой мыши и выбираем Split Struct Pin. Один синий выход заменится на два зеленых - Action Value X и Action Value Y.
- Из ноды Input Action'а вытягиваем Exec-проводок и находим в списке ноду Add Movement Input.
- Выбираем свежесозданную ноду, нажимаем Ctrl + C, затем Ctrl + V. Да, нужные ноды (и даже группы соединенных между собой нодов) можно копировать и вставлять внутри блюпринта, как текст в "Блокноте". Подключаем первую ноду Add Movement Input ко второй через Exec.
Одна нода Add Movement Input будет отвечать за движение влево-вправо. Другая - за вперед-назад. Какая будет отвечать за что - выбирайте сами.
- Для движения вправо и влево нужно добавить в блюпринт ноду Get Actor Right Vector, и подключить её к входу World Direction у Add Movement Input;
- К входу Scale Value у Add Movement Input нужно подключить Action Value X от ноды Input Action.
- Для движения вперед и назад нужно добавить в блюпринт ноду Get Actor Forward Vector, и подключить её к входу World Direction у второго Add Movement Input;
- К входу Scale Value у второго Add Movement Input нужно подключить Action Value Y от ноды Input Action.
Чайнико-справка:
Что это было? Что мы сейчас сделали такое непонятное?
В IMC для кнопок WASD мы назначили перемещение по осям X и Y для соответствующих клавиш (при перемещении в плоскости двух координат вполне достаточно).
Нода Input Action относится к категории ивентов. Этот ивент присылается, когда вы нажимаете заданную кнопку на клавиатуре во время игры. Базово нода присылает 2D-вектор, который мы через Split Struct Pin разделили на два отдельных вектора - для оси X (вправо-влево) и для оси Y (вверх-вниз).
К World Direction подключены ноды, выдающие райт- и форвард-векторы для некоторого объекта на сцене ("актёра"). Поскольку к этим нодам не подключён какой-то конкретный актёр, они сейчас возвращают базовое значение self - то есть, векторы берутся от блюпринта, с которым мы сейчас работаем.
Управление камерой (mouse-look)
- Щелкаем ПКМ по любому свободному месту внутри окна Event Graph, в разделе Enhanced Action Events находим созданный ранее Input Action;
- У созданной ноды щелкаем на синей точке с подписью Action Value правой кнопкой мыши и выбираем Split Struct Pin. Один синий выход заменится на два зеленых - Action Value X и Action Value Y.
- Из ноды Input Action'а вытягиваем Exec-проводок и находим в списке ноду Add Controller Yaw Input. К входу Val подключаем проводок от Action Value X.
- Из ноды Add Controller Yaw Input'а вытягиваем Exec-проводок и находим в списке ноду Add Controller Pitch Input. К входу Val подключаем проводок от Action Value Y.
Чайнико-справка:
Ладно, а здесь мы что сотворили?
Для мышки (или для стика контроллера) перемещение тоже происходит в двух осях - опять X и Y. Как и в прошлом случае, мы разбиваем выходной вектор на два отдельных значения.
Одно из них используется для модификации Yaw (поворота по оси Y), другое - для Pitch (поворота по оси X).
Обе ноды работают непосредственно с Player Controller и крутят его в пространстве вместе с камерой. Если у вас вид от первого лица и отсутствует честная модель персонажа - то можно обойтись и таким подходом. А вот при виде от третьего лица начнутся сюрпризы - вместе с камерой начнёт крутиться и персонаж целиком.
Но у нас сейчас не стоит задачи сделать рабочую игровую камеру, так что в этой статье углубляться в тему не станем.
Простые действия ("из коробки")
- Щелкаем ПКМ по любому свободному месту внутри окна Event Graph, в разделе Enhanced Action Events находим созданный ранее Input Action;
- Щелкнем по белой стрелочке в нижней части ноды Input Action. Нода развернется, показывая дополнительные выходы;
- Для реализации прыжка достаточно к нужному Input Action подключить ноду Jump к выходу Triggered и ноду Stop Jumping к выходу Completed;
- Для реализации приседания достаточно к нужному Input Action подключить ноду Crouch к выходу Triggered и ноду Un Crouch к выходу Completed;
Чайнико-справка:
Почему-то с подключением этих функций ожидают, что и модель персонажа сама собой как-то начнёт красиво прыгать и приседать - с анимациями и прочими радостями жизни. Нет. Так, увы, не работает.
Эта логика отвечает именно за смену состояний (и за приложение к актёру игрока вертикальной направленной силы, за счёт чего и получается прыжок).
Если к вашему актёру не подключён настроенный анимационный блюпринт (совершенно отдельная сущность, не путайте!) - чуда не будет.
Этап 6 - Тестируем, что получилось
В качестве тестового пространства для передвижения можно использовать что угодно. Можно создать отдельную карту с полосой препятствий, можно воспользоваться готовой картой из Starter Pack'a нового проекта.
Для карты в World Settings надо установить созданный Game Mode в поле GameMode Override. Действие оверрайда (то есть, форсированной замены) будет распространяться только на эту карту.
Если вы хотите, чтобы созданный вами GameMode действовал на всех картах по умолчанию, то нужно его прописать в настройках проекта (Меню Edit -> Project Settings -> Maps & Modes -> Default Modes -> Default GameMode).
После установки нужного игрового режима открываем тестовую карту и запускаем игру.
Готово!
Теперь можно побегать по уровню, покрутить камерой и попрыгать.