Unity Как создать мобильное сенсорное управление
Элементы управления — одна из самых важных частей видеоигры, и неудивительно, что именно они позволяют игрокам взаимодействовать с игровым миром.
Элементы управления игрой — это сигналы, которые передаются посредством взаимодействия с оборудованием (мышь/клавиатура, контроллер, сенсорный экран и т. д.), которые затем обрабатываются игровым кодом, применяя определенные действия.
ПК и Игровые консоли имеют физические кнопки, которые можно нажимать, однако современные мобильные устройства имеют лишь несколько физических кнопок, остальная часть взаимодействия осуществляется посредством сенсорных жестов, а это значит, что игровые кнопки должны отображаться на экране. Вот почему при создании мобильной игры важно найти баланс между размещением всех кнопок на экране, сохраняя при этом удобство использования и отсутствие беспорядка.
В этом уроке я покажу, как создать полнофункциональные мобильные элементы управления (джойстики и кнопки) в Unity с использованием UI Canvas.
Шаг 1. Создайте все необходимые сценарии
В этом руководстве представлены два сценария: SC_ClickTracker.cs и SC_MobileControls.cs. Первый скрипт будет прослушивать события щелчка, а второй скрипт будет считывать значения, сгенерированные из этих событий.
SC_ClickTracker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class SC_ClickTracker : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
public string buttonName = ""; //This should be an unique name of the button
public bool isJoystick = false;
public float movementLimit = 1; //How far the joystick can be moved (n x Joystick Width)
public float movementThreshold = 0.1f; //Minimum distance (n x Joystick Width) that the Joystick need to be moved to trigger inputAxis (Must be less than movementLimit)
//Reference variables
RectTransform rt;
Vector3 startPos;
Vector2 clickPos;
//Input variables
Vector2 inputAxis = Vector2.zero;
bool holding = false;
bool clicked = false;
void Start()
{
//Add this button to the list
SC_MobileControls.instance.AddButton(this);
rt = GetComponent<RectTransform>();
startPos = rt.anchoredPosition3D;
}
//Do this when the mouse is clicked over the selectable object this script is attached to.
public void OnPointerDown(PointerEventData eventData)
{
//Debug.Log(this.gameObject.name + " Was Clicked.");
holding = true;
if (!isJoystick)
{
clicked = true;
StartCoroutine(StopClickEvent());
}
else
{
//Initialize Joystick movement
clickPos = eventData.pressPosition;
}
}
WaitForEndOfFrame waitForEndOfFrame = new WaitForEndOfFrame();
//Wait for next update then release the click event
IEnumerator StopClickEvent()
{
yield return waitForEndOfFrame;
clicked = false;
}
//Joystick movement
public void OnDrag(PointerEventData eventData)
{
//Debug.Log(this.gameObject.name + " The element is being dragged");
if (isJoystick)
{
Vector3 movementVector = Vector3.ClampMagnitude((eventData.position - clickPos) / SC_MobileControls.instance.canvas.scaleFactor, (rt.sizeDelta.x * movementLimit) + (rt.sizeDelta.x * movementThreshold));
Vector3 movePos = startPos + movementVector;
rt.anchoredPosition = movePos;
//Update inputAxis
float inputX = 0;
float inputY = 0;
if (Mathf.Abs(movementVector.x) > rt.sizeDelta.x * movementThreshold)
{
inputX = (movementVector.x - (rt.sizeDelta.x * movementThreshold * (movementVector.x > 0 ? 1 : -1))) / (rt.sizeDelta.x * movementLimit);
}
if (Mathf.Abs(movementVector.y) > rt.sizeDelta.x * movementThreshold)
{
inputY = (movementVector.y - (rt.sizeDelta.x * movementThreshold * (movementVector.y > 0 ? 1 : -1))) / (rt.sizeDelta.x * movementLimit);
}
inputAxis = new Vector2(inputX, inputY);
}
}
//Do this when the mouse click on this selectable UI object is released.
public void OnPointerUp(PointerEventData eventData)
{
//Debug.Log(this.gameObject.name + " The mouse click was released");
holding = false;
if (isJoystick)
{
//Reset Joystick position
rt.anchoredPosition = startPos;
inputAxis = Vector2.zero;
}
}
public Vector2 GetInputAxis()
{
return inputAxis;
}
public bool GetClickedStatus()
{
return clicked;
}
public bool GetHoldStatus()
{
return holding;
}
}
#if UNITY_EDITOR
//Custom Editor
[CustomEditor(typeof(SC_ClickTracker))]
public class SC_ClickTracker_Editor : Editor
{
public override void OnInspectorGUI()
{
SC_ClickTracker script = (SC_ClickTracker)target;
script.buttonName = EditorGUILayout.TextField("Button Name", script.buttonName);
script.isJoystick = EditorGUILayout.Toggle("Is Joystick", script.isJoystick);
if (script.isJoystick)
{
script.movementLimit = EditorGUILayout.FloatField("Movement Limit", script.movementLimit);
script.movementThreshold = EditorGUILayout.FloatField("Movement Threshold", script.movementThreshold);
}
}
}
#endif
SC_MobileControls.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_MobileControls : MonoBehaviour
{
[HideInInspector]
public Canvas canvas;
List<SC_ClickTracker> buttons = new List<SC_ClickTracker>();
public static SC_MobileControls instance;
void Awake()
{
//Assign this script to static variable, so it can be accessed from other scripts. Make sure there is only one SC_MobileControls in the Scene.
instance = this;
canvas = GetComponent<Canvas>();
}
public int AddButton(SC_ClickTracker button)
{
buttons.Add(button);
return buttons.Count - 1;
}
public Vector2 GetJoystick(string joystickName)
{
for(int i = 0; i < buttons.Count; i++)
{
if(buttons[i].buttonName == joystickName)
{
return buttons[i].GetInputAxis();
}
}
Debug.LogError("Joystick with a name '" + joystickName + "' not found. Make sure SC_ClickTracker is assigned to the button and the name is matching.");
return Vector2.zero;
}
public bool GetMobileButton(string buttonName)
{
for (int i = 0; i < buttons.Count; i++)
{
if (buttons[i].buttonName == buttonName)
{
return buttons[i].GetHoldStatus();
}
}
Debug.LogError("Button with a name '" + buttonName + "' not found. Make sure SC_ClickTracker is assigned to the button and the name is matching.");
return false;
}
public bool GetMobileButtonDown(string buttonName)
{
for (int i = 0; i < buttons.Count; i++)
{
if (buttons[i].buttonName == buttonName)
{
return buttons[i].GetClickedStatus();
}
}
Debug.LogError("Button with a name '" + buttonName + "' not found. Make sure SC_ClickTracker is assigned to the button and the name is matching.");
return false;
}
}
Шаг 2. Настройте мобильные элементы управления
- Создайте новый холст (GameObject -> UI -> Canvas).
- Измените 'UI Scale Mode' в Canvas Scaler на 'Scale With Screen Size' и измените эталонное разрешение на то, с которым вы работаете (в моем случае это 1000 x 600).
- Прикрепите скрипт SC_MobileControls к объекту Canvas.
- Щелкните правой кнопкой мыши объект Canvas -> Пользовательский интерфейс -> Изображение.
- Переименуйте вновь созданное изображение в "JoystickLeft"
- Измените спрайт "JoystickLeft" на пустой круг (не забудьте изменить тип текстуры на 'Sprite (2D and UI)' после импорта в Unity)
- Установите "JoystickLeft" значения Rect Transform, такие же, как на скриншот ниже:
- В компоненте «Изображение» установите для параметра «Цвет альфа» значение 0,5, чтобы сделать спрайт слегка прозрачным:
- Дублируйте объект "JoystickLeft" и переименуйте его в "JoystickLeftButton"
- Переместите "JoystickLeftButton" внутрь объекта "JoystickLeft".
- Измените спрайт "JoystickLeftButton" на закрашенный круг:
- Установите "JoystickLeftButton" значения Rect Transform, такие же, как на скриншоте ниже:
- Добавьте компонент Button в "JoystickLeftButton"
- В компоненте «Кнопка» измените «Переход» на 'None'
- Прикрепите скрипт SC_ClickTracker к "JoystickLeftButton"
- В SC_ClickTracker задайте для имени кнопки любое уникальное имя (в моем случае я установил его 'JoystickLeft') и включите флажок 'Is Joystick'.
Кнопка-джойстик готова. Вы можете иметь любое количество джойстиков (в моем случае их будет 2: один слева для управления движением и один справа для управления вращением).
- Дублируйте "JoystickLeft" и переименуйте его в "JoystickRight"
- Разверните "JoystickRight" и переименуйте "JoystickLeftButton" в "JoystickRightButton"
- Установите "JoystickRight" значения Rect Transform, такие же, как на скриншоте ниже:
- Выберите объект "JoystickRightButton" и в SC_ClickTracker измените имя кнопки на 'JoystickRight'
Второй джойстик готов.
Теперь создадим обычную кнопку:
- Щелкните правой кнопкой мыши объект Canvas -> Пользовательский интерфейс -> Кнопка.
- Переименуйте объект кнопки в "SprintButton"
- Измените спрайт "SprintButton" на круг с эффектом скоса:
- Установите "SprintButton" значения Rect Transform, как на скриншоте ниже:
- Измените альфу цвета изображения "SprintButton" на 0,5.
- Прикрепите скрипт SC_ClickTracker к объекту "SprintButton".
- В SC_ClickTracker измените имя кнопки на 'Sprinting'
- Выберите текстовый объект внутри "SprintButton" и измените его текст на 'Sprint', а также измените размер шрифта на 'Bold'
Кнопка готова.
Мы собираемся создать еще одну кнопку под названием "Jump":
- Дублируйте объект "SprintButton" и переименуйте его в "JumpButton"
- Измените значение "JumpButton" Pos Y на 250.
- В SC_ClickTracker измените имя кнопки на 'Jumping'
- Измените текст внутри "JumpButton" на 'Jump'
И последняя кнопка — "Action":
- Дублируйте объект "JumpButton" и переименуйте его в "ActionButton"
- Измените значение "ActionButton" Pos X на -185.
- В SC_ClickTracker измените Имя кнопки на 'Action'
- Измените текст внутри "ActionButton" на 'Action'
Шаг 3. Внедрите мобильные элементы управления
Если вы выполнили описанные выше шаги, теперь вы можете использовать эти функции для реализации мобильных элементов управления в своем скрипте:
if(SC_MobileControls.instance.GetMobileButtonDown("BUTTON_NAME")){
//Mobile button has been pressed one time, equivalent to if(Input.GetKeyDown(KeyCode...))
}
if(SC_MobileControls.instance.GetMobileButton("BUTTON_NAME")){
//Mobile button is being held pressed, equivalent to if(Input.GetKey(KeyCode...))
}
//Get normalized direction of a on-screen Joystick
//Could be compared to: new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) or new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"))
Vector2 inputAxis = SC_MobileControls.instance.GetJoystick("JOYSTICK_NAME");
В качестве примера я буду реализовывать мобильные элементы управления с помощью контроллера FPS из этого руководства. Сначала следуйте этому руководству, оно довольно простое.
Если вы следовали этому руководству, теперь у вас будет объект "FPSPlayer" вместе с Canvas с мобильными элементами управления.
Мы сохраним элементы управления рабочего стола, а также реализуем мобильные элементы управления, что сделает их кроссплатформенными:
- Откройте скрипт SC_FPSController, прокрутите до строки 28 и удалите эту часть (удаление этой части предотвратит блокировку курсора и позволит нажимать на мобильные элементы управления в редакторе):
// Lock cursor
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
- Прокрутите до строки 39 и замените:
bool isRunning = Input.GetKey(KeyCode.LeftShift);
float curSpeedX = canMove ? (isRunning ? runningSpeed : walkingSpeed) * Input.GetAxis("Vertical") : 0;
float curSpeedY = canMove ? (isRunning ? runningSpeed : walkingSpeed) * Input.GetAxis("Horizontal") : 0;
- С:
bool isRunning = Input.GetKey(KeyCode.LeftShift) || SC_MobileControls.instance.GetMobileButton("Sprinting");
float curSpeedX = canMove ? (isRunning ? runningSpeed : walkingSpeed) * (Input.GetAxis("Vertical") + SC_MobileControls.instance.GetJoystick("JoystickLeft").y) : 0;
float curSpeedY = canMove ? (isRunning ? runningSpeed : walkingSpeed) * (Input.GetAxis("Horizontal") + SC_MobileControls.instance.GetJoystick("JoystickLeft").x) : 0;
- Прокрутите вниз до строки 45 и замените:
if (Input.GetButton("Jump") && canMove && characterController.isGrounded)
- С:
if ((Input.GetButton("Jump") || SC_MobileControls.instance.GetMobileButtonDown("Jumping")) && canMove && characterController.isGrounded)
- Прокрутите вниз до строки 68 и замените:
rotationX += -Input.GetAxis("Mouse Y") * lookSpeed;
rotationX = Mathf.Clamp(rotationX, -lookXLimit, lookXLimit);
playerCamera.transform.localRotation = Quaternion.Euler(rotationX, 0, 0);
transform.rotation *= Quaternion.Euler(0, Input.GetAxis("Mouse X") * lookSpeed, 0);
- С:
#if UNITY_IPHONE || UNITY_ANDROID || UNITY_EDITOR
rotationX += -(SC_MobileControls.instance.GetJoystick("JoystickRight").y) * lookSpeed;
#else
rotationX += -Input.GetAxis("Mouse Y") * lookSpeed;
#endif
rotationX = Mathf.Clamp(rotationX, -lookXLimit, lookXLimit);
playerCamera.transform.localRotation = Quaternion.Euler(rotationX, 0, 0);
#if UNITY_IPHONE || UNITY_ANDROID || UNITY_EDITOR
transform.rotation *= Quaternion.Euler(0, SC_MobileControls.instance.GetJoystick("JoystickRight").x * lookSpeed, 0);
#else
transform.rotation *= Quaternion.Euler(0, Input.GetAxis("Mouse X") * lookSpeed, 0);
#endif
Поскольку движение взгляда будет мешать тестированию джойстика в редакторе, мы используем #if для компиляции для конкретной платформы, чтобы отделить мобильную логику от остальных платформ.
Мобильный контроллер FPS готов, давайте его проверим:
Как видите, все джойстики и кнопки работоспособны (кроме кнопки "Action", которая не была реализована из-за отсутствия для нее подходящей функции).