123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- using System;
- using Unity.Collections;
- using UnityEngine.InputSystem.Layouts;
- using UnityEngine.InputSystem.LowLevel;
- 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.
- /// </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>"<Gamepad>/buttonSouth"</c>
- /// and another uses <c>"<Gamepad>/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);
- }
-
- protected virtual void OnEnable()
- {
- SetupInputControl();
- }
-
- protected virtual void OnDisable()
- {
- 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;
- }
- }
|