IndieGame/client/Packages/com.unity.inputsystem@1.7.0/InputSystem/Plugins/OnScreen/OnScreenStick.cs

497 lines
22 KiB
C#
Raw Normal View History

2024-10-11 10:12:15 +08:00
#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
using System;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.AnimatedValues;
#endif
////TODO: custom icon for OnScreenStick component
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// A stick control displayed on screen and moved around by touch or other pointer
/// input.
/// </summary>
/// <remarks>
/// The <see cref="OnScreenStick"/> works by simulating events from the device specified in the <see cref="OnScreenControl.controlPath"/>
/// property. Some parts of the Input System, such as the <see cref="PlayerInput"/> component, can be set up to
/// auto-switch to a new device when input from them is detected. When a device is switched, any currently running
/// inputs from the previously active device are cancelled. In the case of <see cref="OnScreenStick"/>, this can mean that the
/// <see cref="IPointerUpHandler.OnPointerUp"/> method will be called and the stick will jump back to center, even though
/// the pointer input has not physically been released.
///
/// To avoid this situation, set the <see cref="useIsolatedInputActions"/> property to true. This will create a set of local
/// Input Actions to drive the stick that are not cancelled when device switching occurs.
/// </remarks>
[AddComponentMenu("Input/On-Screen Stick")]
[HelpURL(InputSystem.kDocUrl + "/manual/OnScreen.html#on-screen-sticks")]
public class OnScreenStick : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler
{
private const string kDynamicOriginClickable = "DynamicOriginClickable";
/// <summary>
/// Callback to handle OnPointerDown UI events.
/// </summary>
public void OnPointerDown(PointerEventData eventData)
{
if (m_UseIsolatedInputActions)
return;
if (eventData == null)
throw new System.ArgumentNullException(nameof(eventData));
BeginInteraction(eventData.position, eventData.pressEventCamera);
}
/// <summary>
/// Callback to handle OnDrag UI events.
/// </summary>
public void OnDrag(PointerEventData eventData)
{
if (m_UseIsolatedInputActions)
return;
if (eventData == null)
throw new System.ArgumentNullException(nameof(eventData));
MoveStick(eventData.position, eventData.pressEventCamera);
}
/// <summary>
/// Callback to handle OnPointerUp UI events.
/// </summary>
public void OnPointerUp(PointerEventData eventData)
{
if (m_UseIsolatedInputActions)
return;
EndInteraction();
}
private void Start()
{
if (m_UseIsolatedInputActions)
{
// avoid allocations every time the pointer down event fires by allocating these here
// and re-using them
m_RaycastResults = new List<RaycastResult>();
m_PointerEventData = new PointerEventData(EventSystem.current);
// if the pointer actions have no bindings (the default), add some
if (m_PointerDownAction == null || m_PointerDownAction.bindings.Count == 0)
{
if (m_PointerDownAction == null)
m_PointerDownAction = new InputAction();
m_PointerDownAction.AddBinding("<Mouse>/leftButton");
m_PointerDownAction.AddBinding("<Pen>/tip");
m_PointerDownAction.AddBinding("<Touchscreen>/touch*/press");
m_PointerDownAction.AddBinding("<XRController>/trigger");
}
if (m_PointerMoveAction == null || m_PointerMoveAction.bindings.Count == 0)
{
if (m_PointerMoveAction == null)
m_PointerMoveAction = new InputAction();
m_PointerMoveAction.AddBinding("<Mouse>/position");
m_PointerMoveAction.AddBinding("<Pen>/position");
m_PointerMoveAction.AddBinding("<Touchscreen>/touch*/position");
}
m_PointerDownAction.started += OnPointerDown;
m_PointerDownAction.canceled += OnPointerUp;
m_PointerDownAction.Enable();
m_PointerMoveAction.Enable();
}
m_StartPos = ((RectTransform)transform).anchoredPosition;
if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return;
m_PointerDownPos = m_StartPos;
var dynamicOrigin = new GameObject(kDynamicOriginClickable, typeof(Image));
dynamicOrigin.transform.SetParent(transform);
var image = dynamicOrigin.GetComponent<Image>();
image.color = new Color(1, 1, 1, 0);
var rectTransform = (RectTransform)dynamicOrigin.transform;
rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2);
rectTransform.localScale = new Vector3(1, 1, 0);
rectTransform.anchoredPosition3D = Vector3.zero;
image.sprite = SpriteUtilities.CreateCircleSprite(16, new Color32(255, 255, 255, 255));
image.alphaHitTestMinimumThreshold = 0.5f;
}
private void BeginInteraction(Vector2 pointerPosition, Camera uiCamera)
{
var canvasRect = transform.parent?.GetComponentInParent<RectTransform>();
if (canvasRect == null)
{
Debug.LogError("OnScreenStick needs to be attached as a child to a UI Canvas to function properly.");
return;
}
switch (m_Behaviour)
{
case Behaviour.RelativePositionWithStaticOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out m_PointerDownPos);
break;
case Behaviour.ExactPositionWithStaticOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out m_PointerDownPos);
MoveStick(pointerPosition, uiCamera);
break;
case Behaviour.ExactPositionWithDynamicOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out var pointerDown);
m_PointerDownPos = ((RectTransform)transform).anchoredPosition = pointerDown;
break;
}
}
private void MoveStick(Vector2 pointerPosition, Camera uiCamera)
{
var canvasRect = transform.parent?.GetComponentInParent<RectTransform>();
if (canvasRect == null)
{
Debug.LogError("OnScreenStick needs to be attached as a child to a UI Canvas to function properly.");
return;
}
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pointerPosition, uiCamera, out var position);
var delta = position - m_PointerDownPos;
switch (m_Behaviour)
{
case Behaviour.RelativePositionWithStaticOrigin:
delta = Vector2.ClampMagnitude(delta, movementRange);
((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta;
break;
case Behaviour.ExactPositionWithStaticOrigin:
delta = position - (Vector2)m_StartPos;
delta = Vector2.ClampMagnitude(delta, movementRange);
((RectTransform)transform).anchoredPosition = (Vector2)m_StartPos + delta;
break;
case Behaviour.ExactPositionWithDynamicOrigin:
delta = Vector2.ClampMagnitude(delta, movementRange);
((RectTransform)transform).anchoredPosition = m_PointerDownPos + delta;
break;
}
var newPos = new Vector2(delta.x / movementRange, delta.y / movementRange);
SendValueToControl(newPos);
}
private void EndInteraction()
{
((RectTransform)transform).anchoredPosition = m_PointerDownPos = m_StartPos;
SendValueToControl(Vector2.zero);
}
private void OnPointerDown(InputAction.CallbackContext ctx)
{
Debug.Assert(EventSystem.current != null);
var screenPosition = Vector2.zero;
if (ctx.control?.device is Pointer pointer)
screenPosition = pointer.position.ReadValue();
m_PointerEventData.position = screenPosition;
EventSystem.current.RaycastAll(m_PointerEventData, m_RaycastResults);
if (m_RaycastResults.Count == 0)
return;
var stickSelected = false;
foreach (var result in m_RaycastResults)
{
if (result.gameObject != gameObject) continue;
stickSelected = true;
break;
}
if (!stickSelected)
return;
BeginInteraction(screenPosition, GetCameraFromCanvas());
m_PointerMoveAction.performed += OnPointerMove;
}
private void OnPointerMove(InputAction.CallbackContext ctx)
{
// only pointer devices are allowed
Debug.Assert(ctx.control?.device is Pointer);
var screenPosition = ((Pointer)ctx.control.device).position.ReadValue();
MoveStick(screenPosition, GetCameraFromCanvas());
}
private void OnPointerUp(InputAction.CallbackContext ctx)
{
EndInteraction();
m_PointerMoveAction.performed -= OnPointerMove;
}
private Camera GetCameraFromCanvas()
{
var canvas = GetComponentInParent<Canvas>();
var renderMode = canvas?.renderMode;
if (renderMode == RenderMode.ScreenSpaceOverlay
|| (renderMode == RenderMode.ScreenSpaceCamera && canvas?.worldCamera == null))
return null;
return canvas?.worldCamera ?? Camera.main;
}
private void OnDrawGizmosSelected()
{
Gizmos.matrix = ((RectTransform)transform.parent).localToWorldMatrix;
var startPos = ((RectTransform)transform).anchoredPosition;
if (Application.isPlaying)
startPos = m_StartPos;
Gizmos.color = new Color32(84, 173, 219, 255);
var center = startPos;
if (Application.isPlaying && m_Behaviour == Behaviour.ExactPositionWithDynamicOrigin)
center = m_PointerDownPos;
DrawGizmoCircle(center, m_MovementRange);
if (m_Behaviour != Behaviour.ExactPositionWithDynamicOrigin) return;
Gizmos.color = new Color32(158, 84, 219, 255);
DrawGizmoCircle(startPos, m_DynamicOriginRange);
}
private void DrawGizmoCircle(Vector2 center, float radius)
{
for (var i = 0; i < 32; i++)
{
var radians = i / 32f * Mathf.PI * 2;
var nextRadian = (i + 1) / 32f * Mathf.PI * 2;
Gizmos.DrawLine(
new Vector3(center.x + Mathf.Cos(radians) * radius, center.y + Mathf.Sin(radians) * radius, 0),
new Vector3(center.x + Mathf.Cos(nextRadian) * radius, center.y + Mathf.Sin(nextRadian) * radius, 0));
}
}
private void UpdateDynamicOriginClickableArea()
{
var dynamicOriginTransform = transform.Find(kDynamicOriginClickable);
if (dynamicOriginTransform)
{
var rectTransform = (RectTransform)dynamicOriginTransform;
rectTransform.sizeDelta = new Vector2(m_DynamicOriginRange * 2, m_DynamicOriginRange * 2);
}
}
/// <summary>
/// The distance from the onscreen control's center of origin, around which the control can move.
/// </summary>
public float movementRange
{
get => m_MovementRange;
set => m_MovementRange = value;
}
/// <summary>
/// Defines the circular region where the onscreen control may have it's origin placed.
/// </summary>
/// <remarks>
/// This only applies if <see cref="behaviour"/> is set to <see cref="Behaviour.ExactPositionWithDynamicOrigin"/>.
/// When the first press is within this region, then the control will appear at that position and have it's origin of motion placed there.
/// Otherwise, if pressed outside of this region the control will ignore it.
/// This property defines the radius of the circular region. The center point being defined by the component position in the scene.
/// </remarks>
public float dynamicOriginRange
{
get => m_DynamicOriginRange;
set
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (m_DynamicOriginRange != value)
{
m_DynamicOriginRange = value;
UpdateDynamicOriginClickableArea();
}
}
}
/// <summary>
/// Prevents stick interactions from getting cancelled due to device switching.
/// </summary>
/// <remarks>
/// This property is useful for scenarios where the active device switches automatically
/// based on the most recently actuated device. A common situation where this happens is
/// when using a <see cref="PlayerInput"/> component with Auto-switch set to true. Imagine
/// a mobile game where an on-screen stick simulates the left stick of a gamepad device.
/// When the on-screen stick is moved, the Input System will see an input event from a gamepad
/// and switch the active device to it. This causes any active actions to be cancelled, including
/// the pointer action driving the on screen stick, which results in the stick jumping back to
/// the center as though it had been released.
///
/// In isolated mode, the actions driving the stick are not cancelled because they are
/// unique Input Action instances that don't share state with any others.
/// </remarks>
public bool useIsolatedInputActions
{
get => m_UseIsolatedInputActions;
set => m_UseIsolatedInputActions = value;
}
[FormerlySerializedAs("movementRange")]
[SerializeField]
[Min(0)]
private float m_MovementRange = 50;
[SerializeField]
[Tooltip("Defines the circular region where the onscreen control may have it's origin placed.")]
[Min(0)]
private float m_DynamicOriginRange = 100;
[InputControl(layout = "Vector2")]
[SerializeField]
private string m_ControlPath;
[SerializeField]
[Tooltip("Choose how the onscreen stick will move relative to it's origin and the press position.\n\n" +
"RelativePositionWithStaticOrigin: The control's center of origin is fixed. " +
"The control will begin un-actuated at it's centered position and then move relative to the pointer or finger motion.\n\n" +
"ExactPositionWithStaticOrigin: The control's center of origin is fixed. The stick will immediately jump to the " +
"exact position of the click or touch and begin tracking motion from there.\n\n" +
"ExactPositionWithDynamicOrigin: The control's center of origin is determined by the initial press position. " +
"The stick will begin un-actuated at this center position and then track the current pointer or finger position.")]
private Behaviour m_Behaviour;
[SerializeField]
[Tooltip("Set this to true to prevent cancellation of pointer events due to device switching. Cancellation " +
"will appear as the stick jumping back and forth between the pointer position and the stick center.")]
private bool m_UseIsolatedInputActions;
[SerializeField]
[Tooltip("The action that will be used to detect pointer down events on the stick control. Note that if no bindings " +
"are set, default ones will be provided.")]
private InputAction m_PointerDownAction;
[SerializeField]
[Tooltip("The action that will be used to detect pointer movement on the stick control. Note that if no bindings " +
"are set, default ones will be provided.")]
private InputAction m_PointerMoveAction;
private Vector3 m_StartPos;
private Vector2 m_PointerDownPos;
[NonSerialized]
private List<RaycastResult> m_RaycastResults;
[NonSerialized]
private PointerEventData m_PointerEventData;
protected override string controlPathInternal
{
get => m_ControlPath;
set => m_ControlPath = value;
}
/// <summary>Defines how the onscreen stick will move relative to it's origin and the press position.</summary>
public Behaviour behaviour
{
get => m_Behaviour;
set => m_Behaviour = value;
}
/// <summary>Defines how the onscreen stick will move relative to it's center of origin and the press position.</summary>
public enum Behaviour
{
/// <summary>The control's center of origin is fixed in the scene.
/// The control will begin un-actuated at it's centered position and then move relative to the press motion.</summary>
RelativePositionWithStaticOrigin,
/// <summary>The control's center of origin is fixed in the scene.
/// The control may begin from an actuated position to ensure it is always tracking the current press position.</summary>
ExactPositionWithStaticOrigin,
/// <summary>The control's center of origin is determined by the initial press position.
/// The control will begin unactuated at this center position and then track the current press position.</summary>
ExactPositionWithDynamicOrigin
}
#if UNITY_EDITOR
[CustomEditor(typeof(OnScreenStick))]
internal class OnScreenStickEditor : UnityEditor.Editor
{
private AnimBool m_ShowDynamicOriginOptions;
private AnimBool m_ShowIsolatedInputActions;
private SerializedProperty m_UseIsolatedInputActions;
private SerializedProperty m_Behaviour;
private SerializedProperty m_ControlPathInternal;
private SerializedProperty m_MovementRange;
private SerializedProperty m_DynamicOriginRange;
private SerializedProperty m_PointerDownAction;
private SerializedProperty m_PointerMoveAction;
public void OnEnable()
{
m_ShowDynamicOriginOptions = new AnimBool(false);
m_ShowIsolatedInputActions = new AnimBool(false);
m_UseIsolatedInputActions = serializedObject.FindProperty(nameof(OnScreenStick.m_UseIsolatedInputActions));
m_Behaviour = serializedObject.FindProperty(nameof(OnScreenStick.m_Behaviour));
m_ControlPathInternal = serializedObject.FindProperty(nameof(OnScreenStick.m_ControlPath));
m_MovementRange = serializedObject.FindProperty(nameof(OnScreenStick.m_MovementRange));
m_DynamicOriginRange = serializedObject.FindProperty(nameof(OnScreenStick.m_DynamicOriginRange));
m_PointerDownAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerDownAction));
m_PointerMoveAction = serializedObject.FindProperty(nameof(OnScreenStick.m_PointerMoveAction));
}
public override void OnInspectorGUI()
{
EditorGUILayout.PropertyField(m_MovementRange);
EditorGUILayout.PropertyField(m_ControlPathInternal);
EditorGUILayout.PropertyField(m_Behaviour);
m_ShowDynamicOriginOptions.target = ((OnScreenStick)target).behaviour ==
Behaviour.ExactPositionWithDynamicOrigin;
if (EditorGUILayout.BeginFadeGroup(m_ShowDynamicOriginOptions.faded))
{
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_DynamicOriginRange);
if (EditorGUI.EndChangeCheck())
{
((OnScreenStick)target).UpdateDynamicOriginClickableArea();
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFadeGroup();
EditorGUILayout.PropertyField(m_UseIsolatedInputActions);
m_ShowIsolatedInputActions.target = m_UseIsolatedInputActions.boolValue;
if (EditorGUILayout.BeginFadeGroup(m_ShowIsolatedInputActions.faded))
{
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(m_PointerDownAction);
EditorGUILayout.PropertyField(m_PointerMoveAction);
EditorGUI.indentLevel--;
}
EditorGUILayout.EndFadeGroup();
serializedObject.ApplyModifiedProperties();
}
}
#endif
}
}
#endif