Как сделать управление джойстиком в юнити
Традиционный игровой ввод
Unity поддерживает ввод с клавиатуры, джойстика и гейпада.
Вы можете настроить джойстики, геймпады, клавиатуру и мышь, затем обращаться к ним через один простой скриптовый интерфейс.
Virtual Axes
Из скриптов все виртуальные оси доступны по их именам.
Каждый проект при создании содержит следующие оси ввода по умолчанию:
Добавление новых осей ввода
Вы привязываете каждую ось к двум кнопкам на джойстике, мыши или клавиатуре.
Свойство: | Функция: |
---|---|
Name | Имя, используемое для проверки этой оси из скрипта. |
Descriptive Name | Имя положительного значения, отображаемое на вкладке Input диалогового окна Configuration в автономных сборках. |
Descriptive Negative Name | Имя отрицательного значения, отображаемое на вкладке Input диалогового окна Configuration в автономных сборках. |
Negative Button | Кнопка, используемая для смещения значения оси в отрицательном направлении. |
Positive Button | Кнопка, используемая для смещения значения оси в положительном направлении. |
Alt Negative Button | Альтернативная кнопка, используемая для смещения значения оси в отрицательном направлении. |
Alt Positive Button | Альтернативная кнопка, используемая для смещения значения оси в положительном направлении. |
Gravity | Скорость в единицах в секунду, с которой ось возвращается в нейтральное положения, когда кнопки не нажаты. |
Dead | Размер аналоговой мертвой зоны. Все значения аналоговых устройств, попадающие в этот диапазон, считаются нейтральными. |
Sensitivity | Скорость в единицах в секунду, с которой ось движется к заданному значению. Только для цифровых устройств. |
Snap | Если включено, значение оси будет сбрасываться в ноль при нажатии кнопки в противоположном направлении. |
Invert | Если включено, Negative Buttons будут выдавать положительные значения, и наоборот. |
Type | Тип ввода, который будет управлять осью. |
Axis | Ось подключенного устройства, которая будет управлять этой осью. |
Joy Num | Подключенный джойстик, который будет управлять этой осью. |
Используйте эти параметры для точной настройки внешнего вида ввода. Также, все они задокументированы во всплывающих подсказках в редакторе.
Использование осей ввода из скриптов
Вы можете запросить текущее состояние из скрипта так:
Это в случае ввода с джойстика и клавиатуры.
Однако изменения осей Mouse и Window Shake показывают, насколько мышь или окно сдвинулись по сравнению с последним кадром. Это значит, что они могут быть больше, чем 1 или меньше, чем –1, когда пользователь быстро двигает мышь.
Можно создавать несколько осей с одним именем. При получении ввода, будет возвращаться ось с наибольшим абсолютным значением. Это позволяет назначить больше одного устройства ввода на одно имя оси. Например, создайте одну ось для ввода с клавиатура и одну ось для ввода с джойстика с одинаковым именем. Если пользователь использует джойстик, ввод будет идти с джойстика, иначе ввод будет идти с клавиатуры. Таким образом, вам не нужно учитывать откуда приходит ввод при написании скриптов.
Названия кнопок
Keys (Клавиши)
Названия кнопок следуют этому соглашению:
Названия, используемые для определения кнопок одни и те же при написании скриптов и в окне Inspector.
Ось может иметь значение от –1 до 1. На нейтральное положение указывает 0. Note also that the keys are accessible using the KeyCode enum parameter.
Традиционный игровой ввод
Unity поддерживает ввод с клавиатуры, джойстика и гейпада.
Virtual axes and buttons can be created on the Input window, and end users can configure Keyboard input in a nice screen configuration dialog.
NOTE: This is a legacy image. This Input Selector image dates back to the very earliest versions of the Unity Editor in 2005. GooBall was a Unity Technologies game.
Вы можете настроить джойстики, геймпады, клавиатуру и мышь, затем обращаться к ним через один простой скриптовый интерфейс.
Virtual Axes
Из скриптов все виртуальные оси доступны по их именам.
Каждый проект при создании содержит следующие оси ввода по умолчанию:
Добавление новых осей ввода
If you want to add new virtual axes go to the Edit > Project Settings menu, then select the Input category. Here you can also change the settings of each axis.
Вы привязываете каждую ось к двум кнопкам на джойстике, мыши или клавиатуре.
Свойство: | Функция: |
---|---|
Name | Имя, используемое для проверки этой оси из скрипта. |
Descriptive Name | Имя положительного значения, отображаемое на вкладке Input диалогового окна Configuration в автономных сборках. |
Descriptive Negative Name | Имя отрицательного значения, отображаемое на вкладке Input диалогового окна Configuration в автономных сборках. |
Negative Button | Кнопка, используемая для смещения значения оси в отрицательном направлении. |
Positive Button | Кнопка, используемая для смещения значения оси в положительном направлении. |
Alt Negative Button | Альтернативная кнопка, используемая для смещения значения оси в отрицательном направлении. |
Alt Positive Button | Альтернативная кнопка, используемая для смещения значения оси в положительном направлении. |
Gravity | Скорость в единицах в секунду, с которой ось возвращается в нейтральное положения, когда кнопки не нажаты. |
Dead | Размер аналоговой мертвой зоны. Все значения аналоговых устройств, попадающие в этот диапазон, считаются нейтральными. |
Sensitivity | Speed in units per second that the axis will move toward the target value. This is for digital devices only. |
Snap | Если включено, значение оси будет сбрасываться в ноль при нажатии кнопки в противоположном направлении. |
Invert | Если включено, Negative Buttons будут выдавать положительные значения, и наоборот. |
Type | Тип ввода, который будет управлять осью. |
Axis | Ось подключенного устройства, которая будет управлять этой осью. |
Joy Num | Подключенный джойстик, который будет управлять этой осью. |
Используйте эти параметры для точной настройки внешнего вида ввода. Также, все они задокументированы во всплывающих подсказках в редакторе.
Использование осей ввода из скриптов
Вы можете запросить текущее состояние из скрипта так:
An axis has a value between –1 and 1. The neutral position is 0. This is the case for joystick input and keyboard input.
Однако изменения осей Mouse и Window Shake показывают, насколько мышь или окно сдвинулись по сравнению с последним кадром. Это значит, что они могут быть больше, чем 1 или меньше, чем –1, когда пользователь быстро двигает мышь.
Можно создавать несколько осей с одним именем. При получении ввода, будет возвращаться ось с наибольшим абсолютным значением. Это позволяет назначить больше одного устройства ввода на одно имя оси. Например, создайте одну ось для ввода с клавиатура и одну ось для ввода с джойстика с одинаковым именем. Если пользователь использует джойстик, ввод будет идти с джойстика, иначе ввод будет идти с клавиатуры. Таким образом, вам не нужно учитывать откуда приходит ввод при написании скриптов.
Названия кнопок
Чтобы назначить кнопку оси, вам необходимо ввести имя кнопки в свойстве Positive Button или Negative Button в окне Inspector.
Keys (Клавиши)
Названия кнопок следуют этому соглашению:
Названия, используемые для определения кнопок одни и те же при написании скриптов и в окне Inspector.
Note also that the keys are accessible using the KeyCode enum parameter.
Gamepad Support
A Gamepad is narrowly defined as a Device with two thumbsticks, a D-pad, and four face buttons. Additionally, gamepads usually have two shoulder and two trigger buttons. Most gamepads also have two buttons in the middle.
A gamepad can have additional Controls, such as a gyro, which the Device can expose. However, all gamepads are guaranteed to have at least the minimum set of Controls described above.
Gamepad support guarantees the correct location and functioning of Controls across platforms and hardware. For example, a PS4 DualShock controller layout should look identical regardless of which platform it is supported on. A gamepad’s south face button should always be the lowermost face button.
NOTE: In case you want to use the gamepad for driving mouse input, there is a sample called Gamepad Mouse Cursor you can install from the package manager UI when selecting the Input System package. The sample demonstrates how to set up gamepad input to drive a virtual mouse cursor.
Controls
Every gamepad has the following Controls:
Control | Type | Description |
---|---|---|
leftStick | StickControl | Thumbstick on the left side of the gamepad. Deadzoned. Provides a normalized 2D motion vector. X is [-1..1] from left to right, Y is [-1..1] from bottom to top. Has up/down/left/right buttons for use like a D-pad. |
rightStick | StickControl | Thumbstick on the right side of the gamepad. Deadzoned. Provides a normalized 2D motion vector. X is [-1..1] from left to right, Y is [-1..1] from bottom to top. Has up/down/left/right buttons for use like a D-pad. |
dpad | DpadControl | The D-pad on the gamepad. |
buttonNorth | ButtonControl | The upper button of the four action buttons, which are usually located on the right side of the gamepad. Labelled «Y» on Xbox controllers and «Triangle» on PlayStation controllers. |
buttonSouth | ButtonControl | The lower button of the four action buttons, which are usually located on the right side of the gamepad. Labelled «A» on Xbox controllers and «Cross» on PlayStation controllers. |
buttonWest | ButtonControl | The left button of the four action buttons, which are usually located on the right side of the gamepad. Labelled «X» on Xbox controllers and «Square» on PlayStation controllers. |
buttonEast | ButtonControl | The right button of the four action buttons, which are usually located on the right side of the gamepad. Labelled «B» on Xbox controllers and «Circle» on PlayStation controllers. |
leftShoulder | ButtonControl | The left shoulder button. |
rightShoulder | ButtonControl | The right shoulder button. |
leftTrigger | ButtonControl | The left trigger button. |
rightTrigger | ButtonControl | The right trigger button. |
startButton | ButtonControl | The start button. |
selectButton | ButtonControl | The select button. |
leftStickButton | ButtonControl | The button pressed when the user presses down the left stick. |
rightStickButton | ButtonControl | The button pressed when the user presses down the right stick. |
Note: Buttons are also full floating-point axes. For example, the left and right triggers can function as buttons as well as full floating-point axes.
You can also access gamepad buttons using the indexer property on Gamepad and the GamepadButton enumeration:
Gamepads have both both Xbox-style and PS4-style aliases on buttons. For example, the following four accessors all retrieve the same «north» face button:
Polling
On Windows (XInput controllers only), Universal Windows Platform (UWP), and Switch, Unity polls gamepads explicitly rather than deliver updates as events.
You can control polling frequency manually. The default polling frequency is 60 Hz. Use InputSystem.pollingFrequency to get or set the frequency.
Increased frequency should lead to an increased number of events on the respective Devices. The timestamps provided on the events should roughly follow the spacing dictated by the polling frequency. Note, however, that the asynchronous background polling depends on OS thread scheduling and can vary.
Rumble
The Gamepad class implements the IDualMotorRumble interface that allows you to control the left and right motor speeds. In most common gamepads, the left motor emits a low-frequency rumble, and the right motor emits a high-frequency rumble.
Note: Only the following combinations of Devices/OSes currently support rumble:
Pausing, resuming, and stopping haptics
In certain situations, you might want to globally pause or stop haptics for all Devices. For example, if the player enters an in-game menu, you can pause haptics while the player is in the menu, and then resume haptics once the player resumes the game. You can use the corresponding methods on InputSystem to achieve this result. These methods work the same way as Device-specific methods, but affect all Devices:
The difference between PauseHaptics and ResetHaptics is that the latter resets haptics playback state on each Device to its initial state, whereas PauseHaptics preserves playback state in memory and only stops playback on the hardware.
PlayStation controllers
PlayStation controllers are well supported on different Devices. The Input System implements these as different derived types of the DualShockGamepad base class, which derives from Gamepad ):
DualShock3GamepadHID : A DualShock 3 controller connected to a desktop computer using the HID interface. Currently only supported on macOS. Doesn’t support rumble.
DualShock4GamepadHID : A DualShock 4 controller connected to a desktop computer using the HID interface. Supported on macOS, Windows, UWP, and Linux.
DualShock4GampadiOS : A DualShock 4 controller connected to an iOS Device via Bluetooth. Requires iOS 13 or higher.
DualShock4GamepadHID implements additional, DualShock-specific functionality on top the general support in the Gamepad class.
On other platforms Unity, uses derived classes to represent Xbox controllers:
XboxGamepadMacOS : Any Xbox or compatible gamepad connected to a Mac via USB using the Xbox Controller Driver for macOS.
XboxOneGampadMacOSWireless : An Xbox One controller connected to a Mac via Bluetooth. Only the latest generation of Xbox One controllers supports Bluetooth. These controllers don’t require any additional drivers in this scenario.
XboxOneGampadiOS : An Xbox One controller connected to an iOS Device via Bluetooth. Requires iOS 13 or higher.
Switch
The Input System support Switch Pro controllers on desktop computers via the SwitchProControllerHID class, which implements basic gamepad functionality.
H Делаем свой джойстик для Unity3D с батчингом и спрайтами в черновиках Tutorial
Некоторое время назад мне понадобился мобильный джойстик для управления персонажем. Посмотрев на стандартный джойстик из включенного в дистрибутивную версию Unity3D пакета я понял, что это не совсем то, что мне нужно.
Во-первых, там очень сильно закрученная и мудреная система вложенных друг в друга объектов.
Во-вторых, джойстик не «подскакивает» к пальцу при нажатии.
В-третьих, почему-то он ограничивается квадратом, а не кругом.
И в конце концов, у него нет красивой подложки «из коробки».
Чтобы не изобретать велосипед, решил поискать бесплатный джойстик в местном Asset Store. Меня очень удивило, если не сказать поразило, отсутствие бесплатных джойстиков. Из 40 найденных позиций были джойстики по 5-100 долларов, при этом, судя по рейтингам и комментариям, большинство из них работали очень криво. (Единственный бесплатный джойстик я нашел намного позже, но об этом подробнее дальше)
Я решил помочь себе и другим, сделав бесплатный джойстик без использования платных GUI библиотек вроде NGUI. Тем более у меня давно лежал пак экранных контроллеров от Kenny (изображение ниже) и нужно было срочно найти ему применение.
Какие спрайты и батчинг?
Unity3D версии 4.3 наделала много шума добавлением нативной поддержки разработки 2D игр. Одним из новых компонент являлся SpriteRenderer, который позволил с легкостью делать 2D игры без дополнительных библиотек. Однако основной его особенностью является то, что разные спрайты из одного атласа батчатся в 1 Draw Call независимо от относительного изменения размеров через Scale. В мобильной разработке принято экономить на Draw Call в любом месте, где это возможно и SpriteRenderer дает нам эту возможность — если упаковать все используемые контроллеры на экране в один атлас, то отрисовку всего интерфейса можно вместить в один Draw Call.
Проблема стандартного джойстика была еще и в том, что используемая для отрисовки GUITexture не батчится, на каждую GUITexture на экране тратится ровно 1 Draw Call.
Когда я таки нашел бесплатный джойстик в магазине, оказалось что он использует аналогичный подход. В общем я здорово загорелся идеей бесплатного джойстика из спрайтов с батчингом и принялся за дело.
Основная идея
Если разместить наш джойстик перед камерой на расстоянии, скажем, 0.5 юнитов, спрайты не будут «врезаться» в другие объекты сцены и все время будут на переднем плане. Вместе со SpriteRenderer пришла система Z-сортировки внутри определенных заранее слоев, но она распространяется только на сами спрайты и системы частиц (насколько я смог разобраться, поправьте если не прав).
Этот же принцип, по-моему, используется в системах вроде NGUI (я не работал с ней, не могу сказать точно). В любом случае, картина получается следующая:
Нужно только найти размеры сечения этой пирамиды на заданном расстоянии от камеры.
Документация Unity3D в данном случае здорво помогла, формула оказалось простой:
Окей, с этим понятно. Теперь к самому джойстику. Если вы играли в игры типа Dungeon Hunter 4, то замечали, что джойстик подпрыгивает в точку нажатия, и управление идет относительно этой точки. Причем есть «подложка» под джойстик и собственно сам джойстик.
Я собрал простой джойстик с «рабочей зоной». При нажатии на любую точку внутри зеленого коллайдера (простой Box Collider), джойстик должен прыгнуть туда и управляться относительно этой точки.
Объект состоит из трех элементов, главный компонент с Box Collider и два вложенных объекта со SpriteRenderer
В итоге я решил использовать Physics.Raycast только для определения начального положения джойстика, а потом преобразовывать координаты касания на экране в координаты точки на сечении пирамиды и далее в локальные координаты джойстика. В целом логика ясна — проверяем, не нажали ли мы на BoxCollider с помощью Physics.Raycast, и если нажали, перемещаем весь джойстик в эту точку (подложку и сам джойстик, но не BoxCollider) и используя перевод экранных координат нажатия в координаты сечения пирамиды и локальные координаты джойстика находим конечное положение самого джойстика. Подложка при этом остается на своем месте. После того, как игрок отпускает палец от экрана, просто ставим джойстик в исходное положение. Вроде все просто.
Для превода экранных координат в координаты точки на сечении пирамиды я решил пойти простым путем — находим процентное отношение координат нажатия к размерам экрана, получаем два числа от 0 до 1, соответствующих координатам X и Y. Затем умножаем эти числа на ширину и высоту сечения пирамиды и получаем локальные координаты точки на этом сечении. После чего, если джойстик расположен не в центре, нужно вычесть из полученных координат относительный центр коллайдера. В итоге, без проверки выхода джойстика за рамки подложки, получается следующая картина. При этом мы уже можем найти относительное направление джойстика и нормировать его.
Используя разницу векторов центра подложки и центра джойстика можно найти длину вектора. Для проверки этого значения нам нужно знать радиус самой подложки. На помощь нам приходит свойство Pixels to Unit при импорте спрайтов. Фактически это свойство говорит о том, сколько пикселей исходного спрайта уместится в 1 юнит расстояния. Чем больше это значение, тем меньше выглядит спрайт. К сожалению, я не нашел адекватного способа определения этого свойства во время выполнения кода, поскольку требуется класс TextureImporter и его свойства, а он обычно доступен только для расширений редактора (скорее всего его физически можно использовать из рантайма, но по-моему это не совсем адекватный вариант). Пока решением остается ручное копирование значения Pixels to Unit в паблик свойство контроллера джойстика.
Посчитав фактическую величину подложки в юнитах, мы можем проверить, выходит ли джойстик за ее рамки или нет.
Для оптимизации проверки в данном случае нам здорово поможет свойство Vector3.sqrMagnitude, определяющее квадрат модуля вектора. Если сравнивать квадрат модуля с квадратом необходимого расстояния, можно избежать операции вычисления квадратного корня, что немного ускорит выполнения кода.
Общее условие выглядит следующим образом — если квадрат модуля вектора относительного направления меньше или равен квадрату радиуса подложки, джойстик находится под положением нажатия. В противном случае нормируем вектор относительного направления, умножаем на радиус подложки и ставим джойстик в точку с получившимися координатами.
Получается такая картина:
Можно сразу сделать так, чтобы относительный вектор был нормирован от 0 до 1 для использования в дальнейшем. Тут заморачиваться не надо, используются простые формулы нормировки.
Если прикрутить получившийся на данный момент джойстик к заготовке персонажа, получится нечто такое:
(На самом деле он ходит, просто неоднородная текстура, на которой видно передвижение, здорово увеличивает размер GIF файла)
Я уже начал думать что работа подходит к концу, но дальше все оказалось еще интересней.
Подводные камни
Все стало очень интересно, когда я начал делать снеппинг джойстика к углам экрана.
Поскольку локальные координаты точки нажатия на сечение не соответствуют локальным координатам самого джойстика, при перемещении самого джойстика (вместе с коллайдером) в точку, отличную от левого нижнего угла экрана, приходится пересчитывать дополнительное слагаемое локальных координат. Для привязки к верхнему левому углу выглядело это примерно так:
С этой проблемой я залип на очень длительное время, долго пытался понять мозгом, что и с чем нужно складывать и из чего вычитать. Оказалось, что все довольно сложно.
Прежде всего, как бы мы ни двигали BoxCollider, точка отсчета локальных координат в джойстике всегда будет в центре подложки (в обычном положении, без нажатия).
Оказалось что свойства collider.bounds считаются всегда в мировых координатах, поэтому для адекватного нахождения реальных размеров пришлось для начальных вычислений ставить объект джойстика в положение Reset, то есть в нулевую позицию с нулевым поворотом, а потом ставить обратно.
Общий вид систем отсчетов выглядел следующим образом:
Конечно, в расчет приходилось брать и размеры коллайдера. В общем я здорово заморочился, но все таки разобрался с этой системой.
Но это был не конец.
Еще одним подводным камнем оказалась поддержка мультитача.
Представим следующий сценарий:
У нас есть два джойстика и два пальца, пусть это будет Д1, Д2, П1 и П2.
Пользователь нажимает П1 на Д1, после чего нажимает П2 на Д2. Индекс П1 в массиве нажатий равен 0, индекс П2 в массиве нажатий равен 1. Если после этого пользователь отпускает П2, индекс П1 все еще остается равен 0, все хорошо. Но если вместо П2 пользователь отпустит П1, то индекс П2 станет равен 0, и Д1 будет думать, что его П1 переместился в точку П2, и получится так, что один П2 управляет двумя джойстиками.
Стоит отметить, что до этого я мало работал с мультитачами, и следующее предложение может показаться кому-то смешным и несуразным. Я сразу ринулся проверять дельту перемещений. Но это был слишком кривой костыль, он не спасал от случая, когда мы сводим два пальца вместе. Оба джойстика прилипают к одному пальцу и снова та же самая картина.
Потом у меня хватило ума посмотреть документацию и прочитать про волшебное свойство Touch.fingerId. Оно хранит индекс нажатого пальца.
Я поменял привязку джойстика к индексу в массиве касаний на индекс пальца и стал проверять, присутствует ли еще в списке присутствующих касаний нужный нам палец. Все стало работать просто отлично.
Итог:
Я проверил производительность джойстика по профайлеру, оказалось что он здорово превосходит стандартный (простой одновременный твикинг, без применения перемещения на цель, проверял через Unity Remote):
Пока писал статью, понял как сделать джойстик без коллайдера и Physics.Raycast(..)
Я определенно буду продолжать работать над этим продуктом, планируется сделать простой тачпад и ABC кнопки.
Если у вас есть какие-то замечания по содержанию, или советы по улучшению логики, пожалуйста, оставьте комментарий.
Оригинальный пак экранных контроллеров можно найти ТУТ