UI 자동화를 위해 바인딩 기능 구현

- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
2026-01-25 01:31:34 +09:00
parent 2ceb28f55d
commit ce83f21c93
1861 changed files with 377882 additions and 211 deletions

View File

@@ -0,0 +1,81 @@
#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.Layouts;
#if UNITY_EDITOR
using UnityEngine.InputSystem.Editor;
#endif
////TODO: custom icon for OnScreenButton component
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// A button that is visually represented on-screen and triggered by touch or other pointer
/// input.
/// </summary>
[AddComponentMenu("Input/On-Screen Button")]
[HelpURL(InputSystem.kDocUrl + "/manual/OnScreen.html#on-screen-buttons")]
public class OnScreenButton : OnScreenControl, IPointerDownHandler, IPointerUpHandler
{
public void OnPointerUp(PointerEventData eventData)
{
SendValueToControl(0.0f);
}
public void OnPointerDown(PointerEventData eventData)
{
SendValueToControl(1.0f);
}
////TODO: pressure support
/*
/// <summary>
/// If true, the button's value is driven from the pressure value of touch or pen input.
/// </summary>
/// <remarks>
/// This essentially allows having trigger-like buttons as on-screen controls.
/// </remarks>
[SerializeField] private bool m_UsePressure;
*/
[InputControl(layout = "Button")]
[SerializeField]
private string m_ControlPath;
protected override string controlPathInternal
{
get => m_ControlPath;
set => m_ControlPath = value;
}
#if UNITY_EDITOR
[UnityEditor.CustomEditor(typeof(OnScreenButton))]
internal class OnScreenButtonEditor : UnityEditor.Editor
{
private UnityEditor.SerializedProperty m_ControlPathInternal;
public void OnEnable()
{
m_ControlPathInternal = serializedObject.FindProperty(nameof(OnScreenButton.m_ControlPath));
}
public void OnDisable()
{
new InputComponentEditorAnalytic(InputSystemComponent.OnScreenButton).Send();
}
public override void OnInspectorGUI()
{
// Current implementation has UGUI dependencies (ISXB-915, ISXB-916)
UGUIOnScreenControlEditorUtils.ShowWarningIfNotPartOfCanvasHierarchy((OnScreenButton)target);
UnityEditor.EditorGUILayout.PropertyField(m_ControlPathInternal);
serializedObject.ApplyModifiedProperties();
}
}
#endif
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d54531977ecb194c95e2d3aa7a5d72a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,369 @@
using System;
using Unity.Collections;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
////REVIEW: should we make this ExecuteInEditMode?
////TODO: handle display strings for this in some form; shouldn't display generic gamepad binding strings, for example, for OSCs
////TODO: give more control over when an OSC creates a new devices; going simply by name of layout only is inflexible
////TODO: make this survive domain reloads
////TODO: allow feeding into more than one control
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// Base class for on-screen controls.
/// </summary>
/// <remarks>
/// The set of on-screen controls together forms a device. A control layout
/// is automatically generated from the set and a device using the layout is
/// added to the system when the on-screen controls are enabled.
///
/// The layout that the generated layout is based on is determined by the
/// control paths chosen for each on-screen control. If, for example, an
/// on-screen control chooses the 'a' key from the "Keyboard" layout as its
/// path, a device layout is generated that is based on the "Keyboard" layout
/// and the on-screen control becomes the 'a' key in that layout.
///
/// If a <see cref="GameObject"/> has multiple on-screen controls that reference different
/// types of device layouts (e.g. one control references 'buttonWest' on
/// a gamepad and another references 'leftButton' on a mouse), then a device
/// is created for each type referenced by the setup.
///
/// The <see cref="OnScreenControl"/> 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 <see cref="PlayerInput.neverAutoSwitchControlSchemes"/> 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.
///
/// To avoid this situation, you need to ensure, depending on your case, that the Mouse, Pen, Touchsceen and/or XRController devices are not used in a concurent
/// control schemes of the simulated device.
/// </remarks>
public abstract class OnScreenControl : MonoBehaviour
{
/// <summary>
/// The control path (see <see cref="InputControlPath"/>) for the control that the on-screen
/// control will feed input into.
/// </summary>
/// <remarks>
/// A device will be created from the device layout referenced by the control path (see
/// <see cref="InputControlPath.TryGetDeviceLayout"/>). The path is then used to look up
/// <see cref="control"/> on the device. The resulting control will be fed values from
/// the on-screen control.
///
/// Multiple on-screen controls sharing the same device layout will together create a single
/// virtual device. If, for example, one component uses <c>"&lt;Gamepad&gt;/buttonSouth"</c>
/// and another uses <c>"&lt;Gamepad&gt;/leftStick"</c> as the control path, a single
/// <see cref="Gamepad"/> will be created and the first component will feed data to
/// <see cref="Gamepad.buttonSouth"/> and the second component will feed data to
/// <see cref="Gamepad.leftStick"/>.
/// </remarks>
/// <seealso cref="InputControlPath"/>
public string controlPath
{
get => controlPathInternal;
set
{
controlPathInternal = value;
if (isActiveAndEnabled)
SetupInputControl();
}
}
/// <summary>
/// The actual control that is fed input from the on-screen control.
/// </summary>
/// <remarks>
/// This is only valid while the on-screen control is enabled. Otherwise, it is <c>null</c>. Also,
/// if no <see cref="controlPath"/> has been set, this will remain <c>null</c> even if the component is enabled.
/// </remarks>
public InputControl control => m_Control;
private InputControl m_Control;
private OnScreenControl m_NextControlOnDevice;
private InputEventPtr m_InputEventPtr;
/// <summary>
/// Accessor for the <see cref="controlPath"/> of the component. Must be implemented by subclasses.
/// </summary>
/// <remarks>
/// Moving the definition of how the control path is stored into subclasses allows them to
/// apply their own <see cref="InputControlAttribute"/> attributes to them and thus set their
/// own layout filters.
/// </remarks>
protected abstract string controlPathInternal { get; set; }
private void SetupInputControl()
{
Debug.Assert(m_Control == null, "InputControl already initialized");
Debug.Assert(m_NextControlOnDevice == null, "Previous InputControl has not been properly uninitialized (m_NextControlOnDevice still set)");
Debug.Assert(!m_InputEventPtr.valid, "Previous InputControl has not been properly uninitialized (m_InputEventPtr still set)");
// Nothing to do if we don't have a control path.
var path = controlPathInternal;
if (string.IsNullOrEmpty(path))
return;
// Determine what type of device to work with.
var layoutName = InputControlPath.TryGetDeviceLayout(path);
if (layoutName == null)
{
Debug.LogError(
$"Cannot determine device layout to use based on control path '{path}' used in {GetType().Name} component",
this);
return;
}
// Try to find existing on-screen device that matches.
var internedLayoutName = new InternedString(layoutName);
var deviceInfoIndex = -1;
for (var i = 0; i < s_OnScreenDevices.length; ++i)
{
////FIXME: this does not take things such as different device usages into account
if (s_OnScreenDevices[i].device.m_Layout == internedLayoutName)
{
deviceInfoIndex = i;
break;
}
}
// If we don't have a matching one, create a new one.
InputDevice device;
if (deviceInfoIndex == -1)
{
// Try to create device.
try
{
device = InputSystem.AddDevice(layoutName);
}
catch (Exception exception)
{
Debug.LogError(
$"Could not create device with layout '{layoutName}' used in '{GetType().Name}' component");
Debug.LogException(exception);
return;
}
InputSystem.AddDeviceUsage(device, "OnScreen");
// Create event buffer.
var buffer = StateEvent.From(device, out var eventPtr, Allocator.Persistent);
// Add to list.
deviceInfoIndex = s_OnScreenDevices.Append(new OnScreenDeviceInfo
{
eventPtr = eventPtr,
buffer = buffer,
device = device,
});
}
else
{
device = s_OnScreenDevices[deviceInfoIndex].device;
}
// Try to find control on device.
m_Control = InputControlPath.TryFindControl(device, path);
if (m_Control == null)
{
Debug.LogError(
$"Cannot find control with path '{path}' on device of type '{layoutName}' referenced by component '{GetType().Name}'",
this);
// Remove the device, if we just created one.
if (s_OnScreenDevices[deviceInfoIndex].firstControl == null)
{
s_OnScreenDevices[deviceInfoIndex].Destroy();
s_OnScreenDevices.RemoveAt(deviceInfoIndex);
}
return;
}
m_InputEventPtr = s_OnScreenDevices[deviceInfoIndex].eventPtr;
// We have all we need. Permanently add us.
s_OnScreenDevices[deviceInfoIndex] =
s_OnScreenDevices[deviceInfoIndex].AddControl(this);
}
protected void SendValueToControl<TValue>(TValue value)
where TValue : struct
{
if (m_Control == null)
return;
if (!(m_Control is InputControl<TValue> control))
throw new ArgumentException(
$"The control path {controlPath} yields a control of type {m_Control.GetType().Name} which is not an InputControl with value type {typeof(TValue).Name}", nameof(value));
////FIXME: this gives us a one-frame lag (use InputState.Change instead?)
m_InputEventPtr.internalTime = InputRuntime.s_Instance.currentTime;
control.WriteValueIntoEvent(value, m_InputEventPtr);
InputSystem.QueueEvent(m_InputEventPtr);
}
protected void SentDefaultValueToControl()
{
if (m_Control == null)
return;
////FIXME: this gives us a one-frame lag (use InputState.Change instead?)
m_InputEventPtr.internalTime = InputRuntime.s_Instance.currentTime;
m_Control.ResetToDefaultStateInEvent(m_InputEventPtr);
InputSystem.QueueEvent(m_InputEventPtr);
}
// Used by PlayerInput auto switch for scheme to prevent using Pointer device.
internal static bool HasAnyActive => s_nbActiveInstances != 0;
private static int s_nbActiveInstances = 0;
protected virtual void OnEnable()
{
++s_nbActiveInstances;
SetupInputControl();
if (m_Control == null)
return;
// if we are in single player and if it the first active switch to the target device.
if (s_nbActiveInstances == 1 &&
PlayerInput.isSinglePlayer)
{
var firstPlayer = PlayerInput.GetPlayerByIndex(0);
if (firstPlayer?.neverAutoSwitchControlSchemes == false)
{
var devices = firstPlayer.devices;
bool deviceFound = false;
// skip is the device is already part of the current scheme
foreach (var device in devices)
{
if (m_Control.device.deviceId == device.deviceId)
{
deviceFound = true;
break;
}
}
if (!deviceFound)
{
firstPlayer.SwitchCurrentControlScheme(m_Control.device);
}
}
}
}
protected virtual void OnDisable()
{
--s_nbActiveInstances;
if (m_Control == null)
return;
var device = m_Control.device;
for (var i = 0; i < s_OnScreenDevices.length; ++i)
{
if (s_OnScreenDevices[i].device != device)
continue;
var deviceInfo = s_OnScreenDevices[i].RemoveControl(this);
if (deviceInfo.firstControl == null)
{
// We're the last on-screen control on this device. Remove the device.
s_OnScreenDevices[i].Destroy();
s_OnScreenDevices.RemoveAt(i);
}
else
{
s_OnScreenDevices[i] = deviceInfo;
// We're keeping the device but we're disabling the on-screen representation
// for one of its controls. If the control isn't in default state, reset it
// to that now. This is what ensures that if, for example, OnScreenButton is
// disabled after OnPointerDown, we reset its button control to zero even
// though we will not see an OnPointerUp.
if (!m_Control.CheckStateIsAtDefault())
SentDefaultValueToControl();
}
m_Control = null;
m_InputEventPtr = new InputEventPtr();
Debug.Assert(m_NextControlOnDevice == null);
break;
}
}
private struct OnScreenDeviceInfo
{
public InputEventPtr eventPtr;
public NativeArray<byte> buffer;
public InputDevice device;
public OnScreenControl firstControl;
public OnScreenDeviceInfo AddControl(OnScreenControl control)
{
control.m_NextControlOnDevice = firstControl;
firstControl = control;
return this;
}
public OnScreenDeviceInfo RemoveControl(OnScreenControl control)
{
if (firstControl == control)
firstControl = control.m_NextControlOnDevice;
else
{
for (OnScreenControl current = firstControl.m_NextControlOnDevice, previous = firstControl;
current != null; previous = current, current = current.m_NextControlOnDevice)
{
if (current != control)
continue;
previous.m_NextControlOnDevice = current.m_NextControlOnDevice;
break;
}
}
control.m_NextControlOnDevice = null;
return this;
}
public void Destroy()
{
if (buffer.IsCreated)
buffer.Dispose();
if (device != null)
InputSystem.RemoveDevice(device);
device = null;
buffer = new NativeArray<byte>();
}
}
private static InlinedArray<OnScreenDeviceInfo> s_OnScreenDevices;
internal string GetWarningMessage()
{
return $"{GetType()} needs to be attached as a child to a UI Canvas and have a RectTransform component to function properly.";
}
}
internal static class UGUIOnScreenControlUtils
{
public static RectTransform GetCanvasRectTransform(Transform transform)
{
var parentTransform = transform.parent;
return parentTransform != null ? transform.parent.GetComponentInParent<RectTransform>() : null;
}
}
#if UNITY_EDITOR
internal static class UGUIOnScreenControlEditorUtils
{
public static void ShowWarningIfNotPartOfCanvasHierarchy(OnScreenControl target)
{
if (UGUIOnScreenControlUtils.GetCanvasRectTransform(target.transform) == null)
UnityEditor.EditorGUILayout.HelpBox(target.GetWarningMessage(), UnityEditor.MessageType.Warning);
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92ae37f8da4c25141a0c43e136275e46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,596 @@
#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;
using UnityEngine.InputSystem.Controls;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.AnimatedValues;
using UnityEngine.InputSystem.Editor;
#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 <see cref="PlayerInput.neverAutoSwitchControlSchemes"/> 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.
/// You might also need to ensure, depending on your case, that the Mouse, Pen, Touchsceen and/or XRController devices are not used in a concurent
/// control schemes of the simulated device.
/// </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(type: InputActionType.PassThrough);
// ensure PassThrough mode
else if (m_PointerDownAction.m_Type != InputActionType.PassThrough)
m_PointerDownAction.m_Type = InputActionType.PassThrough;
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = true;
#endif
m_PointerDownAction.AddBinding("<Mouse>/leftButton");
m_PointerDownAction.AddBinding("<Pen>/tip");
m_PointerDownAction.AddBinding("<Touchscreen>/touch*/press");
m_PointerDownAction.AddBinding("<XRController>/trigger");
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = false;
#endif
}
if (m_PointerMoveAction == null || m_PointerMoveAction.bindings.Count == 0)
{
if (m_PointerMoveAction == null)
m_PointerMoveAction = new InputAction();
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = true;
#endif
m_PointerMoveAction.AddBinding("<Mouse>/position");
m_PointerMoveAction.AddBinding("<Pen>/position");
m_PointerMoveAction.AddBinding("<Touchscreen>/touch*/position");
#if UNITY_EDITOR
InputExitPlayModeAnalytic.suppress = false;
#endif
}
m_PointerDownAction.performed += OnPointerChanged;
m_PointerDownAction.Enable();
m_PointerMoveAction.Enable();
}
// Unable to setup elements according to settings if a RectTransform is not available (ISXB-915, ISXB-916).
if (!(transform is RectTransform))
return;
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 OnDestroy()
{
if (m_UseIsolatedInputActions)
{
m_PointerDownAction.performed -= OnPointerChanged;
}
}
private void BeginInteraction(Vector2 pointerPosition, Camera uiCamera)
{
var canvasRectTransform = UGUIOnScreenControlUtils.GetCanvasRectTransform(transform);
if (canvasRectTransform == null)
{
Debug.LogError(GetWarningMessage());
return;
}
switch (m_Behaviour)
{
case Behaviour.RelativePositionWithStaticOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, pointerPosition, uiCamera, out m_PointerDownPos);
break;
case Behaviour.ExactPositionWithStaticOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, pointerPosition, uiCamera, out m_PointerDownPos);
MoveStick(pointerPosition, uiCamera);
break;
case Behaviour.ExactPositionWithDynamicOrigin:
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, pointerPosition, uiCamera, out var pointerDown);
m_PointerDownPos = ((RectTransform)transform).anchoredPosition = pointerDown;
break;
}
}
private void MoveStick(Vector2 pointerPosition, Camera uiCamera)
{
var canvasRectTransform = UGUIOnScreenControlUtils.GetCanvasRectTransform(transform);
if (canvasRectTransform == null)
{
Debug.LogError(GetWarningMessage());
return;
}
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, 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)
{
if (m_IsIsolationActive) { return; }
Debug.Assert(EventSystem.current != null);
var screenPosition = Vector2.zero;
TouchControl touchControl = null;
if (ctx.control?.parent is TouchControl touch)
{
touchControl = touch;
screenPosition = touch.position.ReadValue();
}
else 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());
if (touchControl != null)
{
m_TouchControl = touchControl;
m_PointerMoveAction.ApplyBindingOverride($"{touchControl.path}/position", path: "<Touchscreen>/touch*/position");
}
m_PointerMoveAction.performed += OnPointerMove;
m_IsIsolationActive = true;
}
private void OnPointerChanged(InputAction.CallbackContext ctx)
{
if (ctx.control.IsPressed())
OnPointerDown(ctx);
else
OnPointerUp(ctx);
}
private void OnPointerMove(InputAction.CallbackContext ctx)
{
// only pointer devices are allowed
Debug.Assert(ctx.control?.device is Pointer);
Vector2 screenPosition;
// If it's a finger take the value from the finger that initiated the change
if (m_TouchControl != null)
{
// if the finger is up ignore the move
if (m_TouchControl.isInProgress == false)
{
return;
}
screenPosition = m_TouchControl.position.ReadValue();
}
else
{
screenPosition = ((Pointer)ctx.control.device).position.ReadValue();
}
MoveStick(screenPosition, GetCameraFromCanvas());
}
private void OnPointerUp(InputAction.CallbackContext ctx)
{
if (!m_IsIsolationActive) return;
// if it's a finger ensure that is the one that get released
if (m_TouchControl != null)
{
if (m_TouchControl.isInProgress) return;
m_PointerMoveAction.ApplyBindingOverride(null, path: "<Touchscreen>/touch*/position");
m_TouchControl = null;
}
EndInteraction();
m_PointerMoveAction.performed -= OnPointerMove;
m_IsIsolationActive = false;
}
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()
{
// This will not produce meaningful results unless we have a rect transform (ISXB-915, ISXB-916).
var parentRectTransform = transform.parent as RectTransform;
if (parentRectTransform == null)
return;
Gizmos.matrix = parentRectTransform.localToWorldMatrix;
var startPos = parentRectTransform.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;
[NonSerialized]
private TouchControl m_TouchControl;
[NonSerialized]
private bool m_IsIsolationActive;
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 void OnDisable()
{
// Report analytics
new InputComponentEditorAnalytic(InputSystemComponent.OnScreenStick).Send();
new OnScreenStickEditorAnalytic(this).Send();
}
public override void OnInspectorGUI()
{
// Current implementation has UGUI dependencies (ISXB-915, ISXB-916)
UGUIOnScreenControlEditorUtils.ShowWarningIfNotPartOfCanvasHierarchy((OnScreenStick)target);
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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9d677f1681015749b15c436eec6d880
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
#if UNITY_EDITOR || UNITY_ANDROID || UNITY_IOS || UNITY_TVOS || UNITY_WSA || UNITY_VISIONOS
namespace UnityEngine.InputSystem.OnScreen
{
/// <summary>
/// Support for various forms of on-screen controls.
/// </summary>
/// <remarks>
/// On-screen input visually represents control elements either through (potentially) built-in
/// mechanisms like <see cref="OnScreenKeyboard"/> or through manually arranged control setups
/// in the form of <see cref="OnScreenControl">OnScreenControls</see>.
/// </remarks>
#if UNITY_DISABLE_DEFAULT_INPUT_PLUGIN_INITIALIZATION
public
#else
internal
#endif
static class OnScreenSupport
{
public static void Initialize()
{
////TODO: OnScreenKeyboard support
//InputSystem.RegisterLayout<OnScreenKeyboard>();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 74405005f3013c2498103a79211ba93b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: