Restrict joysticks movement inside circle while finger is outside circle

Hi

I’m currently trialing a sprite as the control for a virtual joystick. At the moment I can:

  • Successfully latch onto the sprite and move it around the imaginary circle with full 360 degree rotation, and
  • Successfully restrict the sprite so it wont go outside the circle doing a Vector2.distance calculation measured from the centre of the joystick. Note: I’m still latched to the joystick even though my finger is no longer ontop of the joystick.

however I would very much like the joystick to:

  • Follow my finger around the inner edge of the circle even if my finger is outside the circle, because at the moment when my finger goes outside the set distance it wont work and I’m really not sure how to approach this problem.

Note:

  • See attached pictures for clarification
  • I’m using SM2 for the sprite, it has a box collider component attached, and I’d like to be able to plonk the joystick anywhere on the screen (I.E. bottom left, bottom right, even in the middle if I wanted to).
  • I’ve played with the joystick scripts and GUITexture joystick with the penelope tutorial however I’d really like to use a sprite and understand how to get this to work.

Any tips, suggestions would very much be appreciated.
Dean

476064--16712--$1.png
476064--16713--$2.png
476064--16714--$3.png
476064--16715--$4.png
476064--16716--$5.png

Here’s what I wrote for my 3d joystick (I even commented it for you :)):

var joystick : GameObject; //Self explanitory
var bg : GameObject; //Background object for the joystick boundaries
var deadzone : Vector2; //Deadzone for the joystick

private var cam : Camera; //Durr :P
private var joyCol : Collider; //Herpaderp :d
private var lastfingerid : int = -1; //Used for latching onto the finger
private var position : Vector2; //Explained later
private var xClampMin : float; //Clamps to boundaries
private var xClampMax : float;
private var yClampMin : float;
private var yClampMax : float;

function Awake ()
{
	cam = GameObject.FindWithTag ("MainCamera").camera;
	joyCol = joystick.GetComponent (Collider);
	xClampMin = -bg.transform.localScale.x / 2; //Joystick boundary is determined by the size of the
	xClampMax = bg.transform.localScale.x / 2; //background. The bigger the background, the more room
	yClampMin = -bg.transform.localScale.y / 2; //your joystick can move
	yClampMax = bg.transform.localScale.y / 2;
}

function Disable ()
{
	gameObject.active = false; //Turns off the joystick when called
}

function ResetJoystick () //Called when the joystick should be moved back to it's original position (relative to parent object)
{
	lastfingerid = -1;
	joystick.transform.localPosition = Vector3.zero;
}

function Update ()
{
	if (Input.touchCount == 0)
	{
		ResetJoystick (); //I would have figured this would be obvious? :P
	}
	else
	{
		for (var i : int = 0; i < Input.touchCount; i++)
		{
			var touch : Touch = Input.GetTouch(i);
			var latchFinger : boolean = false;
			
			if (touch.phase == TouchPhase.Began) //When you begin the touch, the code here gets called once
			{
				var ray : Ray = cam.ScreenPointToRay (touch.position); //Creates a ray at the touch spot
				var hit : RaycastHit; //Used for determining if something was hit
				Debug.DrawRay (ray.origin, ray.direction * 10, Color.yellow); //Delete this line. It's not important.
				
				if (Physics.Raycast (ray, hit, 2)) //The ray hit something...
				{
					if (hit.collider == joyCol) //Apparently, that ray hit your joystick
					{
						latchFinger = true; //This turns on and sets off a chain reaction
					}
				}
			}
			
			if (latchFinger  (lastfingerid == -1 || lastfingerid != touch.fingerId)) //Latch finger being true turns this code on
			{
				lastfingerid = touch.fingerId;
			}
			
			if (lastfingerid == touch.fingerId) //The real meat of the code
			{
				var sTW : Vector3 = cam.ScreenToWorldPoint (new Vector3 (touch.position.x, touch.position.y, 1)); //Transforms the screen touch coordinates into world coordinates to move our joystick
				joystick.transform.position = sTW; //Hurr :O
				
				var xClamp : float = Mathf.Clamp (joystick.transform.localPosition.x, xClampMin, xClampMax); //Limit movement to the boundaries
				var yClamp : float = Mathf.Clamp (joystick.transform.localPosition.y, yClampMin, yClampMax);
				joystick.transform.localPosition = Vector3 (xClamp, yClamp, joystick.transform.localPosition.z); //Finally, the joystick moves like it should with everything in place
				
				if (touch.phase == (TouchPhase.Ended || TouchPhase.Canceled))
				{
					ResetJoystick (); //PISHDUDTSBFJDPKDLSZMDPLFF!!!!!!111111one
				}
			}
		}
	}
	
	position.x = (joystick.transform.localPosition.x / bg.transform.localScale.x) * 2; //This converts the final position into a value ranging from -1 to +1 on both axis (like a real joystick)
	position.y = (joystick.transform.localPosition.y / bg.transform.localScale.y) *2;
	
	var absX : float = Mathf.Abs (position.x); //Used for figuring out the deadzone (ABS returns a value of zero or higher)
	var absY : float = Mathf.Abs (position.y);
	
	if (absX < deadzone.x)
	{
		position.x = 0; //If the joystick is less than the the deadzone length, the joystick graphic will still move, but the value for it will be zero until it's out of the deadzone
	}
	
	if (absY < deadzone.y)
	{
		position.y = 0;
	}
}

It’s a variant of the joystick script that comes with the mobile assets, except instead of GUI Textures, it uses actual 3d objects. This code should do what you want.

Use the center of the circle as the origin.

dir = org - pos;
Then you would do something like arctan2(dir.y,dir.x) * RAD_2PI;

This would give you the angle.

@theinfomercial, Thanks! and thankyou for providing the code and the much appreciated comments. FYI I moved the:
var latchFinger : boolean = false;
decleration to the top as latchFinger will always be set to false even after a successful latch. FYI I really do appreciate the help do you know how to clamp the joystick to work in a CIRCULAR motion instead of a rectangle or square boundary?

@aiursrage2K, do you mean:

  • arctan2 = Mathf.Atan2 function?
  • RAD_2PI = (2* Mathf.PI), … what is RAD?

I’m guessing RAD for radians, as in 2pi expressed in radians (as opposed to degrees).

Why put the latchFinger variable to be false all the time? The code won’t work if you do that. Besides, latchFinger only gets set to true only for one frame when you touch the joystick.

As for getting the joystick to move circularly, I honestly don’t know. Math aint my strong point, but radians is obviously the way to go. If someone wants to write up an extension for this, that would be awesome.:slight_smile:

@theinfomercial, … Sorry my previous post wasn’t saying the full story, I wasn’t in front of my main computer before so I couldn’t post the code, … I moved the decleration up to the top however I set the boolean to be either TRUE or FALSE in the TouchPhase.Began section based on if there was a hit or not.

if (touch.phase == TouchPhase.Began) //When you begin the touch, the code here gets called once
{
	var ray : Ray = cam.ScreenPointToRay (touch.position); //Creates a ray at the touch spot
	var hit : RaycastHit; //Used for determining if something was hit
				
	if (Physics.Raycast (ray, hit)) //The ray hit something...
	{	
	        if (hit.collider == joyCol) //Apparently, that ray hit your joystick
		{
			latchFinger = true; //This turns on and sets off a chain reaction
		}
	}
	else
	{
		latchFinger = false;	
	}
}
using UnityEngine;
using System.Collections;

public class Joystick3D : MonoBehaviour
{

	public GameObject joystick; //Self explanitory
	public GameObject bg; //Background object for the joystick boundaries
	public float deadzone = 0f; //Deadzone for the joystick
	public float radius = 1f; //The radius that the joystick thumb is allowed to move from its origin
	public Camera cam;
	
	protected Collider joyCol;
	protected int lastfingerid = -1; //Used for latching onto the finger
	
	public Vector2 position = Vector2.zero;
	

	void Awake ()
	{
		joyCol = bg.GetComponent<Collider>();
		
	}

	public void Disable ()
	{
		gameObject.active = false; //Turns off the joystick when called
	}

	public void ResetJoystick () //Called when the joystick should be moved back to it's original position (relative to parent object)
	{
		lastfingerid = -1;
		joystick.transform.localPosition = Vector3.zero;
	}

	void Update ()
	{
		if (Input.touchCount == 0)
		{
			ResetJoystick (); //I would have figured this would be obvious? :P
		}
		else
		{
			for (int i=0; i < Input.touchCount; i++)
			{
				Touch touch = Input.GetTouch(i);
				bool latchFinger = false;
				if (touch.phase == TouchPhase.Began) //When you begin the touch, the code here gets called once
				{
					Ray ray = cam.ScreenPointToRay (touch.position); //Creates a ray at the touch spot
					RaycastHit hit; //Used for determining if something was hit
					
					if (joyCol.Raycast(ray,out hit,Mathf.Infinity)) //The ray hit something...
					{
						if (hit.collider == joyCol) //Apparently, that ray hit your joystick
						{
							latchFinger = true; //This turns on and sets off a chain reaction
						}
					}
				}
				
				if (latchFinger  (lastfingerid == -1 || lastfingerid != touch.fingerId)) //Latch finger being true turns this code on
				{
					lastfingerid = touch.fingerId;
				}
				
				if (lastfingerid == touch.fingerId) //The real meat of the code
				{
					Vector3 sTW = cam.ScreenToWorldPoint (new Vector3 (touch.position.x, touch.position.y, 1)); //Transforms the screen touch coordinates into world coordinates to move our joystick
					joystick.transform.position = sTW; //Hurr :O
					joystick.transform.localPosition = new Vector3(joystick.transform.localPosition.x,joystick.transform.localPosition.y,0);
					//float xClamp = Mathf.Clamp (joystick.transform.localPosition.x, xClampMin, xClampMax); //Limit movement to the boundaries
					//float yClamp = Mathf.Clamp (joystick.transform.localPosition.y, yClampMin, yClampMax);
					
					
					/*float distFromCenter = Mathf.Sqrt(joystick.transform.localPosition.x*joystick.transform.localPosition.x + joystick.transform.localPosition.y*joystick.transform.transform.localPosition.y);
	
					float actualPercent = Mathf.Clamp01(distFromCenter/radius);
					
					float percent = Mathf.Clamp01((distFromCenter - deadzone)/(radius - deadzone));
					*/
					
					joystick.transform.localPosition = Vector3.ClampMagnitude(joystick.transform.localPosition,radius);
					
					
					
					
					
					
					//joystick.transform.localPosition = new Vector3 (xClamp, yClamp, joystick.transform.localPosition.z); //Finally, the joystick moves like it should with everything in place
					
					if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
					{
						ResetJoystick ();
					}
				}
				
				Ray touchray = cam.ScreenPointToRay (touch.position); //Creates a ray at the touch spot
					//RaycastHit hit; //Used for determining if something was hit
				Debug.DrawRay (touchray.origin, touchray.direction * 100, Color.yellow); //Delete this line. It's not important.
					
				
			}
		}
		
		//set our position variables x and y values to the joysticks values but clamped to a percent value instead of world coords
		position.x = joystick.transform.localPosition.x/radius;
		position.y = joystick.transform.localPosition.y/radius;
		
		if(position.magnitude < deadzone)
		{
			position.x = 0;
			position.y = 0;
		}
		
		
	}
}

Improved the 3d joystick script with some changes, added limit with a radius, and made it so that you dont have to touch the joystick thumb to start it, you can touch anywhere on the joystick base.

You can also check this thread : http://forum.unity3d.com/threads/138973-Joystick-For-Mobile-(Third-Person-Controller)-Released-V1.1

Hey guys. I took the Penelope Joystick script and modified it to do what the OP was asking.

Read the comments at the top for tips on how to use the script, it’s very simple.

The joystick will do exactly what the original posters image description requested.

using UnityEngine;
using System.Collections;

/*  READ BEFORE USING... THIS WILL HELP YOU
 * 
 * ---------------BE SURE TO SET SCALE.X and SCALE.Y to ZERO and SCALE.Z to ONE or the texture will not appear--------------
 * 
 * Use the transform position in the editor to set the position of the Joystic Image
 * position.x = Screen.width * transform.position.x;
 * position.y = Screen.height * transform.position.y;
 * OR
 * Use the pixel inset on the GUITexture Component.
 * 
 * Using the first method will insure proper ratio for all screen sizes.
 * 
 * Pixel inset width and height values are to set the size of the guiTexture.
 * 
 * I don't use the left, right, top, and bottom border values so.... yeah.
 * 
 * 
 * In the BPJoystick script in the editor the position value is public for debugging.
 * The Radius value is used to adjust the max radius of the joystick.
 * Unfortunately the radius is in pixels but it is no biggie, works fine.
 * 
 * Do the same as the other script to get values of joystick position.
 * 
 * position.x and position.y are scaled from -1 to 1.
 * 
 * Enjoy! 
 * 
 * */


[RequireComponent(typeof(GUITexture))]
public class BPJoystick : MonoBehaviour {

 
	private static BPJoystick[] joysticks;					// A static collection of all joysticks
	private static bool enumeratedJoysticks = false;
	private static float tapTimeDelta = 0.3f;				// Time allowed between taps
 
	public Vector2 position = Vector2.zero;
	private Vector2 tempPosVec;
	private int tapCount;
 
	private int lastFingerId = -1;								// Finger last used for this joystick
	private float tapTimeWindow;							// How much time there is left for a tap to occur

 
	private GUITexture gui;
	private Rect defaultRect;								// Default position / extents of the joystick graphic
	private Vector2 guiTouchOffset;						// Offset to apply to touch input
	private Vector2 guiCenter;							// Center of joystick
 
	public float radius = 10f;	
	
	
	void Start() {
		// get GUITexture component, assign to gui variable
		gui = (GUITexture)GetComponent(typeof(GUITexture));
 
		// get manually added pixel inset and add it to defaultRect
		defaultRect = gui.pixelInset;
		// get transform position in pixels and add it to defaultRect
		defaultRect.x += transform.position.x * Screen.width;
		defaultRect.y += transform.position.y * Screen.height;
 
		// set transform position to zero now that pixels are gotten
		transform.position = Vector3.zero; 
		
		// get offset pixels for GUITexture size to help find center of texture
		guiTouchOffset.x = defaultRect.width * 0.5f;
		guiTouchOffset.y = defaultRect.height * 0.5f;
		
 
		// Cache the center of the GUI, since it doesn't change
		guiCenter.x = defaultRect.x + guiTouchOffset.x;
		guiCenter.y = defaultRect.y + guiTouchOffset.y;
		
	}
 
	public Vector2 getGUICenter() {
		return guiCenter;
	}
 
	void Disable() {
		gameObject.active = false;
		//enumeratedJoysticks = false;
	}
 
	private void ResetJoystick() {
		gui.pixelInset = defaultRect;
		lastFingerId = -1;
		position = Vector2.zero;
	}
 
	public bool IsFingerDown() {
		return (lastFingerId != -1);
	}
 
	public void LatchedFinger(int fingerId) {
		// If another joystick has latched this finger, then we must release it
		if ( lastFingerId == fingerId )
			ResetJoystick();
	}
 
	void Update() {
		if (!enumeratedJoysticks) {
			// Collect all joysticks in the game, so we can relay finger latching messages
			joysticks = (BPJoystick[])FindObjectsOfType(typeof(BPJoystick));
			enumeratedJoysticks = true;
		}
 
		int count = Input.touchCount;
 
		if ( tapTimeWindow > 0 )
			tapTimeWindow -= Time.deltaTime;
		else
			tapCount = 0;
 
		if ( count == 0 )
			ResetJoystick();
		else
		{
			for(int i = 0; i < count; i++) {
				// get touches as they occur
				Touch touch = Input.GetTouch(i);			
				
				// set current touch position to vector
				Vector2 guiTouchPos = touch.position;
 
				bool shouldLatchFinger = false;
				
				// if touch was on GUItexture, latch
				if (gui.HitTest(touch.position)) {
					shouldLatchFinger = true;
				}
 
				// Latch the finger if this is a new touch
				if (shouldLatchFinger  (lastFingerId == -1 || lastFingerId != touch.fingerId )) { 
 
					lastFingerId = touch.fingerId;
 
					// Accumulate taps if it is within the time window
					if ( tapTimeWindow > 0 )
						tapCount++;
					else {
						tapCount = 1;
						tapTimeWindow = tapTimeDelta;
					}
 
					// Tell other joysticks we've latched this finger
					foreach (BPJoystick j in joysticks) {
						if (j != this)
							j.LatchedFinger( touch.fingerId );
					}
				}
 
				if ( lastFingerId == touch.fingerId ) {
					// Override the tap count with what the iPhone SDK reports if it is greater
					// This is a workaround, since the iPhone SDK does not currently track taps
					// for multiple touches
					if ( touch.tapCount > tapCount )
						tapCount = touch.tapCount;
 
					
					// set tempPosVec to equal the vector from center of gui to curret touch
					tempPosVec = guiTouchPos - guiCenter;					
					
					// clamp the vector to your selected radius
					tempPosVec = Vector2.ClampMagnitude(tempPosVec,radius);
					
					// apply that vector to the GUITexture
					guiTouchPos = tempPosVec + guiCenter;											
					
					// set the rectangle values using the texture offset guiTouchOffset to correct for the texture offset
					Rect r = gui.pixelInset;
					r.x = guiTouchPos.x - guiTouchOffset.x;
					r.y = guiTouchPos.y - guiTouchOffset.y;
					
					// set position of texture
					gui.pixelInset = r;
 
					// reset if finger released
					if (touch.phase == TouchPhase.Ended)
						ResetJoystick();
					
				}
			}
		}
 

		// Get a value between -1 and 1 based on the joystick graphic location
		position.x = tempPosVec.x/radius;
		position.y = tempPosVec.y/radius;
 
	}
	
}