Unity Jak tworzyć mobilne elementy sterujące dotykiem
Sterowanie to jedna z najważniejszych części gier wideo i nic dziwnego, że to ona pozwala graczom na interakcję ze światem gry.
Sterowanie grą to sygnały wysyłane poprzez interakcję sprzętową (mysz/klawiatura, kontroler, ekran dotykowy itp.), które są następnie przetwarzane przez kod gry, wykonując określone działania.
Komputery PC i Konsole do gier mają fizyczne przyciski, które można nacisnąć, jednak nowoczesne urządzenia mobilne mają tylko kilka fizycznych przycisków, reszta interakcji odbywa się za pomocą gestów dotykowych, co oznacza, że przyciski gry muszą być wyświetlane na ekranie. Dlatego tworząc grę mobilną, ważne jest, aby znaleźć równowagę pomiędzy umieszczeniem wszystkich przycisków na ekranie, a jednocześnie dbaniem o to, aby był on przyjazny dla użytkownika i uporządkowany.
W tym samouczku pokażę, jak utworzyć w pełni funkcjonalne mobilne elementy sterujące (joysticki i przyciski) w Unity przy użyciu UI Canvas.
Krok 1: Utwórz wszystkie niezbędne skrypty
Ten samouczek zawiera 2 skrypty, SC_ClickTracker.cs i SC_MobileControls.cs. Pierwszy skrypt będzie nasłuchiwał zdarzeń kliknięcia, a drugi odczytał wartości wygenerowane na podstawie tych zdarzeń.
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;
}
}
Krok 2: Skonfiguruj sterowanie mobilne
- Utwórz nowe płótno (GameObject -> UI -> Canvas)
- Zmień 'UI Scale Mode' w Canvas Scaler na 'Scale With Screen Size' i zmień rozdzielczość odniesienia na tę, z którą pracujesz (w moim przypadku jest to 1000 x 600)
- Dołącz skrypt SC_MobileControls do obiektu Canvas
- Kliknij prawym przyciskiem myszy obiekt Canvas -> Interfejs użytkownika -> Obraz
- Zmień nazwę nowo utworzonego obrazu na "JoystickLeft"
- Zmień "JoystickLeft" Sprite'a na pusty okrąg (nie zapomnij zmienić typu tekstury na 'Sprite (2D and UI)' po zaimportowaniu go do Unity)
- Ustaw wartości "JoystickLeft" Rect Transform tak samo, jak na zrzucie ekranu poniżej:
- W komponencie Obraz ustaw wartość Kolor alfa na 0,5, aby duszek był lekko przezroczysty:
- Zduplikuj obiekt "JoystickLeft" i zmień jego nazwę na "JoystickLeftButton"
- Przesuń "JoystickLeftButton" wewnątrz obiektu "JoystickLeft"
- Zmień duszka "JoystickLeftButton" na wypełnione koło:
- Ustaw wartości "JoystickLeftButton" Rect Transform tak samo, jak na zrzucie ekranu poniżej:
- Dodaj komponent Button do "JoystickLeftButton"
- W komponencie Przycisk zmień Przejście na 'None'
- Dołącz skrypt SC_ClickTracker do "JoystickLeftButton"
- W SC_ClickTracker ustaw nazwę przycisku na dowolną unikalną nazwę (w moim przypadku ustawiłem ją na 'JoystickLeft') i zaznacz pole wyboru 'Is Joystick'.
Przycisk joysticka jest gotowy. Możesz mieć dowolną liczbę joysticków (w moim przypadku będę miał 2, jeden po lewej stronie do sterowania ruchem i jeden po prawej stronie do sterowania obrotem).
- Zduplikuj "JoystickLeft" i zmień jego nazwę na "JoystickRight"
- Rozwiń "JoystickRight" i zmień nazwę "JoystickLeftButton" na "JoystickRightButton"
- Ustaw wartości "JoystickRight" Rect Transform tak samo, jak na zrzucie ekranu poniżej:
- Wybierz obiekt "JoystickRightButton" i w SC_ClickTracker zmień nazwę przycisku na 'JoystickRight'
Drugi joystick jest gotowy.
Teraz utwórzmy zwykły przycisk:
- Kliknij prawym przyciskiem myszy obiekt Canvas -> Interfejs użytkownika -> Przycisk
- Zmień nazwę obiektu przycisku na "SprintButton"
- Zmień duszka "SprintButton" na okrąg z efektem skosu:
- Ustaw wartości "SprintButton" Rect Transform tak samo, jak na zrzucie ekranu poniżej:
- Zmień "SprintButton" Kolor obrazu alfa na 0,5
- Dołącz skrypt SC_ClickTracker do obiektu "SprintButton"
- W SC_ClickTracker zmień nazwę przycisku na 'Sprinting'
- Wybierz obiekt tekstowy wewnątrz "SprintButton" i zmień jego tekst na 'Sprint', a także zmień Rozmiar czcionki na 'Bold'
Przycisk jest gotowy.
Zamierzamy utworzyć kolejny przycisk o nazwie "Jump":
- Zduplikuj obiekt "SprintButton" i zmień jego nazwę na "JumpButton"
- Zmień wartość "JumpButton" Pos Y na 250
- W SC_ClickTracker zmień nazwę przycisku na 'Jumping'
- Zmień tekst wewnątrz "JumpButton" na 'Jump'
A ostatni przycisk to "Action":
- Zduplikuj obiekt "JumpButton" i zmień jego nazwę na "ActionButton"
- Zmień wartość "ActionButton" Poz X na -185
- W SC_ClickTracker zmień nazwę przycisku na 'Action'
- Zmień tekst wewnątrz "ActionButton" na 'Action'
Krok 3: Wdróż kontrolę mobilną
Jeśli wykonałeś powyższe kroki, możesz teraz użyć tych funkcji, aby zaimplementować kontrolki mobilne w swoim skrypcie:
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");
Jako przykład zaimplementuję sterowanie mobilne za pomocą kontrolera FPS z tego samouczka. Najpierw postępuj zgodnie z tym samouczkiem, to dość proste.
Jeśli postępowałeś zgodnie z tym samouczkiem, będziesz mieć teraz obiekt "FPSPlayer" wraz z kanwą z mobilnymi elementami sterującymi.
Zachowamy elementy sterujące przeznaczone dla komputerów stacjonarnych, jednocześnie wdrażając elementy sterujące mobilne, dzięki czemu będzie to rozwiązanie wieloplatformowe:
- Otwórz skrypt SC_FPSController, przewiń do linii 28 i usuń tę część (usunięcie tej części zapobiegnie zablokowaniu kursora i umożliwi klikanie elementów sterujących urządzeń mobilnych w Edytorze.):
// Lock cursor
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
- Przewiń do linii 39 i zamień:
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;
- Z:
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;
- Przewiń w dół do linii 45 i zamień:
if (Input.GetButton("Jump") && canMove && characterController.isGrounded)
- Z:
if ((Input.GetButton("Jump") || SC_MobileControls.instance.GetMobileButtonDown("Jumping")) && canMove && characterController.isGrounded)
- Przewiń w dół do linii 68 i zamień:
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);
- Z:
#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
Ponieważ ruch wyglądu będzie kolidował z testowaniem joysticka w Edytorze, używamy #if do kompilacji specyficznej dla platformy, aby oddzielić logikę mobilną od reszty platform.
Mobilny kontroler FPS jest już gotowy, przetestujmy go:
Jak widać, wszystkie joysticki i przyciski działają (z wyjątkiem przycisku "Action", który nie został zaimplementowany ze względu na brak odpowiedniej funkcji).