Sunday 28 February 2016

Virtual Joypad

This is a tutorial on how to create a virtual joypad for mobile controls. In this example, I will use it to move around a spaceship in the scene.

I used a nice skybox from here and the spaceship model is from the Unity project "Space Shooter" (found here).

This is my own version of virtual joypad, chances are there are more efficient ways to do it, however, this is what I came up with.

Let's start by dragging in the ship model in the scene and place it at origin and rotate it on its X axis by -90. We don't have to worry about it now as we are going to use only later on once the joypad is ready.

To make the visual joypad, create a Canvas and place 2 Images in it, one child of he other one. I called one Joypad (parent image)and the other one FrontKnob (child image). In the Source Image box, select the Knob sprite for both UI elements, which should be available with every Unity project (Fig 1). I rescaled the 2 images so they look like a joypad, like in Fig 2.

Fig 1

Fig 2

The Joypad image is anchored and positioned to the bottom left corner of the canvas. Its child image is anchored and positioned to the center.

Now, the Joypad script. Create a new C# script and assign it to the FrontKnob image.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class Joypad : MonoBehaviour, IDragHandler,IEndDragHandler {

 public Image parentImage;
 public float returningSpeed;

 float parentWidth;
 float maxDistance;

 Vector3 startingPosition;
 Vector3 vectorGenerated;

 bool isDraging = false;

 void Start () {

  parentWidth = parentImage.rectTransform.rect.width;
  startingPosition = transform.position;
  maxDistance = parentWidth / 2;  
 }

 void Update () {  
 
  vectorGenerated = transform.position - startingPosition;

  if (!isDraging)
   transform.position = Vector3.MoveTowards (transform.position, startingPosition, returningSpeed * Time.deltaTime); 
 }

 public void OnEndDrag (PointerEventData eventData)
 {
  isDraging = false;
 }

 public void OnDrag (PointerEventData eventData)
 {
  isDraging = true;
  Vector3 vectorToMouse = Input.mousePosition - startingPosition;

  Ray r = new Ray (startingPosition, vectorToMouse.normalized);

  Physics.Raycast (r);

  if (vectorToMouse.magnitude < maxDistance)
   transform.position = r.GetPoint (vectorToMouse.magnitude);
  else
   transform.position = r.GetPoint (maxDistance);
   
 }

 public float GetRelativeMagnitude()
 {
  float d = vectorGenerated.magnitude / maxDistance;
  return Mathf.Abs (d);
 }

 public Vector3 GetDirection()
 {
  return vectorGenerated.normalized;
 }
}

First thing we notice, is the I included the UnityEngine.EventSystem on line 4. This is necessary as we are going to implement 2 interfaces needed for the dragging mechanics. We can see these at the beginning of the class, after the Monobehaviour keyword we have IDragHandler and IEndDragHandler . This will force us to implement the methods OnEndDrag(...) and OnDrag(...).

In the Start() method, I beging by getting the width of the parent image, which is a public parameter, so I can quickly assign it in the inspector. I use this to determine the maximum distance when dragging our knob (the variable maxDistance). I could have very well just use a predefined float variable. Also, I store the initial position of the FrontKnob image.

In the Update(), I continuously calculate the vector the is originated from the starting position of the knob to its current position. This is use to calculate the distance of the knob from the center and the direction. We can see right away on line 61 the to get the direction I simply return the normalized version of this vector.

On line 31 I check whether the isDraging variable is set. If not, I will move the knob back to its starting position.

The first interface implementation we see is OnEndDrag(...). This is a listener that is called whenever we stop dragging our object (in our case, the FrontKnob image). When his method is called, we simply set the isDraging variable to false.

On line 40 is where everything happens. With the method OnDrag(...), an interface implementation, we can detect  when the object is being dragged around. First thing we do, is to set the isDraging to true. After that, we create a vector with origins at the starting position of the knob and points towards the mouse position on the screen.

Then, I create a ray with the same origins and direction of the just created vector and I cast it.

To position the knob while it's being dragged, I used raycasting. When I raycast, I checked the lengthof the vectorToMouse vector (line 49). If this distance is within the range, I detect the point on the ray at that particular distance and place the knob. Otherwise, I place it at its maximum distance allowed.

With this technique, we can keep dragging the knob around even when the mouse is way beyond the maximum distance range, however, the knob will only travel as far as the maxDistance float variable dictates.

The method on line 56 returns the so called relative distance. Basically, this is a value between 0 and 1: 0 when the knob is at the starting position, 1 when is at its maximum distance allowed. This value can now be used just like the float value we obtain when using Input.GetAxis(...).

This is pretty much all we need for the joypad.

Let's see now the PlayerMovement script:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PlayerMovement : MonoBehaviour {

 public float speed = 10;

 float acc;
 Vector3 dir;

 GameObject joypad;
 Joypad pad;
 Rigidbody rb;

 // Use this for initialization
 void Start () {

  joypad = GameObject.FindGameObjectWithTag ("Joypad");
  pad = joypad.GetComponentInChildren<Joypad> ();
  rb = GetComponent<Rigidbody> (); 
 }
 
 // Update is called once per frame
 void Update () {

  acc = pad.GetRelativeMagnitude ();
  dir = pad.GetDirection ();

  Vector3 newPos = transform.position + (dir * acc * speed * Time.deltaTime);

  rb.MovePosition (newPos); 
 }
}

Pretty simple!

The pad parameter is the Joypad script we just analysed. Calling the 2 methods of this script GetRelativeMagnitude() and GetDirection(), we can have the acceleration and direction for our ship.

All we have to do is to move its rigibody accordingly (line 26 - 28).

Bear in mind that the direction obtained by the Joypad script works for X and Y axis, which is why we rotated our ship on the X axis at the beginning. By doing so, the ship follows the X and Y direction given by the joypad. If, for example, you were using a top view camera, your ship would be moving along the X an Z axis, and all you have to do is to create a new vector for movement.

Also, this example is done with mouse input, so you can try it out in the example scene. It would work exactly the same using touch input for mobile devices.

Example scene here.

No comments:

Post a Comment