Unity Як створити мобільне сенсорне керування

Елементи керування є однією з найважливіших частин відеоігри, і не дивно, що саме вони дозволяють гравцям взаємодіяти з ігровим світом.

Елементи керування грою – це сигнали, які надсилаються через взаємодію з обладнанням (миша/клавіатура, контролер, сенсорний екран тощо), які потім обробляються кодом гри, застосовуючи певні дії.

Комп’ютери та Ігрові консолі мають фізичні кнопки, які можна натискати, однак сучасні мобільні пристрої мають лише кілька фізичних кнопок, решта взаємодії здійснюється за допомогою сенсорних жестів, це означає, що кнопки гри повинні відображатися на екрані. Ось чому під час створення мобільної гри важливо знайти баланс між наявністю всіх кнопок на екрані, зберігаючи її зручною для користувача та вільною від безладу.

Мобільні елементи керування Unity

У цьому підручнику я покажу, як створити повнофункціональні (джойстики та кнопки) мобільні елементи керування в Unity за допомогою UI Canvas.

Крок 1: Створіть усі необхідні сценарії

Цей посібник містить 2 сценарії, 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 Object
  • Клацніть правою кнопкою миші Canvas Object -> UI -> Image
  • Перейменуйте новостворене зображення на "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 Object -> UI -> Button
  • Перейменувати об’єкт кнопки на "SprintButton"
  • Змініть спрайт "SprintButton" на коло з ефектом скосу:

  • Встановіть значення "SprintButton" Rect Transform так само, як на знімку екрана нижче:

  • Змініть альфа кольору "SprintButton" зображення на 0,5
  • Приєднайте сценарій SC_ClickTracker до об’єкта "SprintButton"
  • У SC_ClickTracker змініть назву кнопки на 'Sprinting'
  • Виберіть текстовий об’єкт у "SprintButton" та змініть його текст на 'Sprint', а також змініть розмір шрифту на 'Bold'

Мобільна кнопка Unity

Кнопка готова.

Ми збираємося створити ще одну кнопку під назвою "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 Controller з цього підручника. Спочатку дотримуйтеся цього посібника, він досить простий.

Якби ви дотримувалися цього підручника, ви мали б об’єкт "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 для компіляції для конкретної платформи, щоб відокремити мобільну логіку від решти платформ.

Контролер Mobile FPS тепер готовий, давайте перевіримо його:

Sharp Coder Відеоплеєр

Як бачите, усі джойстики та кнопки функціональні (за винятком кнопки "Action", яка не була реалізована через відсутність відповідної функції для неї).

Джерело
📁MobileControls.unitypackage272.33 KB
Рекомендовані статті
Мобільний джойстик сенсорного введення в Unity
Створення руху гравця в Unity
Як створити Crane Control в Unity
Контролер літака для Unity
Підручник з ліхтариком для Unity
Контролер вертольота для Unity
Додавання присідання до FPS Player в Unity