Создание виртуального джойстика на юнити (основная статья)

Всем привет! В этой статье мы будем делать простой виртуальный джойстик на юнити.

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

Подготовка

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

ВАЖНО! Смещение должно быть нормализованным. То есть максимальное значение по любой оси не должно быть больше единицы, а минимальное - минус единицы. Иначе будет трудно настроить ту же скорость движения персонажа. Например, нам нужно чтобы персонаж двигался с максимальной скоростью 3 метра в секунду. Если смещение будет нормализованным, то максимальная скорость персонажа будет равна заданной, то есть 3 м/с, если же оно не будет нормализованным, то скорость может быть равна, например 8 или 0,2 или 114 м/с.

Написание скрипта

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

На сцене создаем канвас, создаем в нем объект с компонентом Image, кидаем в свойтво source image наш спрайт зоны и ставим в нужное место на экране. Готово! Это наш джойстик. Затем создаём скрипт управления джойстиком и вешаем на него. Далее создаем джойстику дочерний объект с тем же имаджем (не бейте пж за мой английский) и кидаем на него спрайт ручки. Это наша ручка (как ни странно).

Самая скучная работа закончена и можно начинать кодить.

Итак, прежде всего нам нужно отслеживать взаимодействие пользователя с джойстиком. Для этого будем использовать интерфейсы IDragHandler (для отслеживания "перетаскивания" джойстика пальцем) и IEndDragHandler (для того, чтобы знать когда пользователь убрал палец с джойстика). Объявляем класс наследником этих интерфейсов и реализуем их. Наш код должен выглядеть примерно так:

using UnityEngine; using UnityEngine.EventSystems; public class Joystick : MonoBehaviour, IDragHandler, IEndDragHandler { public void OnDrag(PointerEventData eventData) { //Движение пальцем по экрану } public void OnEndDrag(PointerEventData eventData) { //Конец движения пальца. } }

ВАЖНО! Обязательно добавьте CircleCollider2D на джойстик для регистрации нажатий по нему и не забудьте сделать его триггером. На основную камеру добавьте Physics 2D Raycaster для того чтобы узнавать о клике пальцем на джойстик.

Далее, нам нужно двигать ручку за пальцем. Для этого модифицируем наш скрипт:

using UnityEngine; using UnityEngine.EventSystems; #pragma warning disable 0649 //Строчка сверху отключает сообщения о том, что "переменной не присвоено значение" public class Joystick : MonoBehaviour, IDragHandler, IEndDragHandler { [SerializeField] GameObject Handle; //Ручка public void OnDrag(PointerEventData eventData) { Vector2 inputPosition = Camera.main.ScreenToWorldPoint(eventData.position); //Позиция пальца на экране в текущий момент Handle.transform.position = new Vector3(inputPosition.x, inputPosition.y, 0); } public void OnEndDrag(PointerEventData eventData) { Handle.transform.localPosition = Vector3.zero; //По окончании перетаскивания ручки, ставим её в начальную позицию. } }

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

Это Vector3.ClampMagnitude. Этот метод возвращает обрезанную до определенной длины копию вектора. То что нам и нужно. Модифицируем код.

using UnityEngine; using UnityEngine.EventSystems; #pragma warning disable 0649 public class Joystick : MonoBehaviour, IDragHandler, IEndDragHandler { [SerializeField] GameObject Handle; [SerializeField] float MoveRadius; //Максимальная длина, на которую может удаляться ручка от центра джойстика public void OnDrag(PointerEventData eventData) { Vector3 inputPosition = Camera.main.ScreenToWorldPoint(eventData.position); Vector3 offset = inputPosition - gameObject.transform.position; //Расстояние между ручкой и зоной. offset = new Vector3(offset.x, offset.y, 0); Handle.gameObject.transform.position = gameObject.transform.position + Vector3.ClampMagnitude(offset, MoveRadius); //Ограничиваем позицию ручки "зоной" } public void OnEndDrag(PointerEventData eventData) { Handle.transform.localPosition = Vector3.zero; } }

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

ВАЖНО! В коде ниже будут использоваться свойства. Почитайте, что это такое (если вы не знаете) чтобы понимать, что там происходит.

using UnityEngine; using UnityEngine.EventSystems; #pragma warning disable 0649 //Строчка сверху отключает сообщения о том, что "переменной не присвоено значение" public class Joystick : MonoBehaviour, IDragHandler, IEndDragHandler { [SerializeField] GameObject Handle; [SerializeField] float MoveRadius; static Joystick instance; //Ссылка на джойстик public static Vector2 Position { get //Это называется геттер. Код ниже будет выполняться, когда мы считываем значение свойства { return (instance.Handle.transform.position - instance.gameObject.transform.position).normalized; } //При считывании значения позиции ручки, возвращаем нормализованное направление от ручки до центра } private void Start() { instance = this; //В ссылку закидываем этот джойстик } public void OnDrag(PointerEventData eventData) { Vector3 inputPosition = Camera.main.ScreenToWorldPoint(eventData.position); Vector3 offset = inputPosition - gameObject.transform.position; offset = new Vector3(offset.x, offset.y, 0); Handle.gameObject.transform.position = gameObject.transform.position + Vector3.ClampMagnitude(offset, MoveRadius); } public void OnEndDrag(PointerEventData eventData) { Handle.gameObject.transform.localPosition = Vector3.zero; } }

Это финальный скрипт. Больше мы не будем его дорабатывать.

P.S. Выше я использовал свойство, только затем, чтобы сэкономить пару операций и не изменять значение каждый кадр. А static я использовал, чтобы получать значение проще. Вот пример:

public class Player : MonoBehaviour { [SerializeField] float Speed; private void Update() { //Без static Joystick joystick = FindObjectOfType<Joystick>(); transform.Translate(joystick.Position * Speed); //Со static transform.Translate(Joystick.Position * Speed); } }

Заключение

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

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

Всем пока!

Начать дискуссию