Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Plugins /OnScreen /OnScreenControl.cs
| 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; | |
| } | |
| } | |