Skip to content

Instantly share code, notes, and snippets.

@tk009999
Forked from Vonflaken/VirtualJoystick.cs
Created June 1, 2021 02:36
Show Gist options
  • Save tk009999/e541e8da394ca0cf60195b1adf2c8de6 to your computer and use it in GitHub Desktop.
Save tk009999/e541e8da394ca0cf60195b1adf2c8de6 to your computer and use it in GitHub Desktop.
Virtual joystick component for Unity mobile projects.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_EDITOR && !UNITY_CLOUD_BUILD
using UnityEditor;
#endif
namespace Vonflaken
{
#if UNITY_EDITOR && !UNITY_CLOUD_BUILD
[CustomPropertyDrawer(typeof(EnumFlagsAttribute))]
public class EnumFlagsAttributeDrawer : PropertyDrawer
{
public override void OnGUI(Rect _position, SerializedProperty _property, GUIContent _label)
{
_property.intValue = EditorGUI.MaskField(_position, _label, _property.intValue, _property.enumNames);
}
}
#endif
public class EnumFlagsAttribute : PropertyAttribute
{
public EnumFlagsAttribute() { }
}
public class VirtualJoystick : MonoBehaviour
{
[Header("Main")]
[SerializeField, Tooltip("Pivot at (0.5, 0.5) required.")]
private RectTransform _joystickKnob;
[SerializeField, Tooltip("Pivot at (0.5, 0.5) required.")]
private RectTransform _backgroundStencil;
[SerializeField]
private float _radiusBackground;
[SerializeField, Tooltip("Joystick will return zero for axis values while the stick is inside this area.")]
private float _radiusDeadZone;
[System.Flags]
enum JoystickSettings
{
SpawnAtPointer = 1 << 0,
FollowPointer = 1 << 1,
}
[Header("Behaviour")]
[SerializeField, EnumFlags]
private JoystickSettings _settings;
[SerializeField, Range(0f, 1f), Tooltip("Strongness of the smooth finger following of the joystick.")]
public float _easeSharpness = 0.1f;
[SerializeField, Tooltip("Whereas or not the axis values are normalized from dead zone or center.")]
private bool _normalizeHappensAfterDeadZone = true;
[Header("Visual")]
[SerializeField, Tooltip("Alpha value of sprites when joystick is not being held.")]
private float _alphaInactive = 0.8f;
[SerializeField]
private float _scaleActive = 1.2f;
private Image _joystickKnobSprite;
private Image _backgroundStencilSprite;
private bool _pointerHeld = false;
private Vector2 _currPointerPos;
private Vector2 _initialPosition;
private float _initialScale;
/// <summary>
/// Normalized value of axis position at the moment.
/// </summary>
public Vector2 AxisValues { private set; get; }
private void Awake()
{
_backgroundStencilSprite = _backgroundStencil.GetComponent<Image>();
_joystickKnobSprite = _joystickKnob.GetComponent<Image>();
_initialPosition = _backgroundStencil.anchoredPosition;
_initialScale = _backgroundStencil.localScale.x;
SetJoystickAlpha(_alphaInactive);
}
private void OnDrawGizmosSelected()
{
// Draw hint for center of joystick knob
Gizmos.color = Color.green;
if (_joystickKnob != null)
Gizmos.DrawSphere(_joystickKnob.position, 1f);
if (_backgroundStencil != null)
{
// Draw hint for checking if radius fits inside the circular sprite
Gizmos.color = Color.blue;
Vector3 worldFrom = _backgroundStencil.position;
Vector3 worldTo = _backgroundStencil.TransformPoint(Vector3.right * _radiusBackground);
Gizmos.DrawLine(worldFrom, worldTo);
#if UNITY_EDITOR && !UNITY_CLOUD_BUILD
// Add description text for being more understandable from editor
GUIStyle newStyle = new GUIStyle();
newStyle.normal.textColor = Gizmos.color;
UnityEditor.Handles.Label(worldFrom, "Radius Background Stencil", newStyle);
#endif
// Draw hint for setting dead zone
Gizmos.color = Color.red;
worldTo = _backgroundStencil.TransformPoint(Vector3.left * _radiusDeadZone);
Gizmos.DrawLine(worldFrom, worldTo);
#if UNITY_EDITOR && !UNITY_CLOUD_BUILD
// Add description text for being more understandable from editor
newStyle.normal.textColor = Gizmos.color;
Camera guiCamera = Camera.current;
Vector3 screenFrom = guiCamera.WorldToScreenPoint(worldFrom);
Vector3 deadZoneWorldFrom = guiCamera.ScreenToWorldPoint(screenFrom + new Vector3(0f, -16f, 0f));
UnityEditor.Handles.Label(deadZoneWorldFrom, "Radius Dead Zone", newStyle);
#endif
}
}
/// <summary>
/// Call for the joystick starts working.
/// </summary>
/// <param name="pointerPos"></param>
public void PointerDown(Vector2 pointerPos)
{
if (_pointerHeld == false)
{
_pointerHeld = true;
_currPointerPos = pointerPos;
if (IsSettingActive(JoystickSettings.SpawnAtPointer))
_backgroundStencil.position = pointerPos; // Move joystick to pointer start pos
SetJoystickAlpha(1f);
SetJoystickScale(_scaleActive);
StartCoroutine(UpdateAxis());
}
}
/// <summary>
/// Call for the joystick ends working.
/// </summary>
public void PointerUp()
{
_pointerHeld = false;
// Reset values
AxisValues = Vector2.zero;
SetKnobPosition(Vector2.zero);
SetJoystickAlpha(_alphaInactive);
SetJoystickPosition(_initialPosition);
SetJoystickScale(_initialScale);
}
/// <summary>
/// Call for feeding the joystick with pointer position so it can update its state.
/// </summary>
/// <param name="newPointerPos"></param>
public void PointerMove(Vector2 newPointerPos)
{
_currPointerPos = newPointerPos;
}
/// <summary>
/// Update axis value while pointer is on.
/// </summary>
/// <returns></returns>
private IEnumerator UpdateAxis()
{
while (_pointerHeld)
{
// Update axis value
Vector2 center = Vector2.zero;
Vector2 relativePointerPos = _backgroundStencil.InverseTransformPoint(_currPointerPos); // Make pointer position relative to background sprite
Vector2 heading = relativePointerPos;
float angle = Mathf.Atan2(heading.y, heading.x); // Angle between center and pointer
float cos = Mathf.Cos(angle);
float sin = Mathf.Sin(angle);
Vector2 maxExtent = new Vector2(cos * _radiusBackground, sin * _radiusBackground); // Outtest point in circular shape of background sprite at the direction of pointer
Vector2 clampedPointerPos = ClampPos(relativePointerPos, maxExtent);
if (IsPosInsideDeadZone(relativePointerPos, cos, sin))
AxisValues = Vector2.zero;
else // Pointer is outside dead zone
{
AxisValues = new Vector2(clampedPointerPos.x / _radiusBackground, clampedPointerPos.y / _radiusBackground);
if (_normalizeHappensAfterDeadZone)
{
float normRadiusDeadZone = _radiusDeadZone / _radiusBackground;
float axisValuesLength = AxisValues.magnitude;
AxisValues = (AxisValues / axisValuesLength) * ((axisValuesLength - normRadiusDeadZone) / (1f - normRadiusDeadZone));
}
}
if (IsSettingActive(JoystickSettings.FollowPointer))
{
if (!IsPointInsideJoystick(relativePointerPos))
{
Vector3 vectorToPointer = relativePointerPos - maxExtent;
float blend = 1f - Mathf.Pow(1f - _easeSharpness, Time.deltaTime * 60f); // Exponential ease-out
_backgroundStencil.position = Vector3.Lerp(_backgroundStencil.position, _backgroundStencil.position + vectorToPointer, blend);
}
}
// Update joystick knob's sprite position
SetKnobPosition(clampedPointerPos);
yield return null;
}
}
private void SetKnobPosition(Vector2 newPos)
{
_joystickKnob.anchoredPosition = newPos;
}
private void SetJoystickPosition(Vector2 newPos)
{
_backgroundStencil.anchoredPosition = newPos;
}
private void SetJoystickScale(float newScale)
{
_backgroundStencil.localScale = new Vector3(newScale, newScale, newScale);
}
private Vector2 ClampPos(Vector2 pos, Vector2 extent)
{
if ((pos.x > 0f && pos.x > extent.x) || (pos.x < 0f && pos.x < extent.x))
pos.x = extent.x; // Clamp horizontal boundary
if ((pos.y > 0f && pos.y > extent.y) || (pos.y < 0f && pos.y < extent.y))
pos.y = extent.y; // Clamp vertical boundary
return pos;
}
private bool IsSettingActive(JoystickSettings setting)
{
return (_settings & setting) != 0;
}
private void SetJoystickAlpha(float newAlphaValue)
{
Color newBackgroundSpriteColor = _backgroundStencilSprite.color;
Color newKnobSpriteColor = _joystickKnobSprite.color;
newBackgroundSpriteColor.a = newKnobSpriteColor.a = newAlphaValue;
_backgroundStencilSprite.color = newBackgroundSpriteColor;
_joystickKnobSprite.color = newKnobSpriteColor;
}
private bool IsPosInsideDeadZone(Vector2 pointerPos, float pointerCos, float pointerSin)
{
Vector2 extent = new Vector2(pointerCos * _radiusDeadZone, pointerSin * _radiusDeadZone);
if ((pointerPos.x >= 0f && pointerPos.x <= extent.x) || (pointerPos.x <= 0f && pointerPos.x >= extent.x)) // Pointer is inside horizontal boundary?
if ((pointerPos.y >= 0f && pointerPos.y <= extent.y) || (pointerPos.y <= 0f && pointerPos.y >= extent.y)) // Pointer is inside vertical boundary?
return true;
return false;
}
private bool IsPointInsideJoystick(Vector2 relativePos)
{
return relativePos.sqrMagnitude <= (_radiusBackground * _radiusBackground);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment