A game about forced loneliness, made by TACStudios
1using System; 2using Unity.Collections; 3using UnityEngine.InputSystem.Layouts; 4using UnityEngine.InputSystem.LowLevel; 5using UnityEngine.InputSystem.Utilities; 6 7////REVIEW: should we make this ExecuteInEditMode? 8 9////TODO: handle display strings for this in some form; shouldn't display generic gamepad binding strings, for example, for OSCs 10 11////TODO: give more control over when an OSC creates a new devices; going simply by name of layout only is inflexible 12 13////TODO: make this survive domain reloads 14 15////TODO: allow feeding into more than one control 16 17namespace UnityEngine.InputSystem.OnScreen 18{ 19 /// <summary> 20 /// Base class for on-screen controls. 21 /// </summary> 22 /// <remarks> 23 /// The set of on-screen controls together forms a device. A control layout 24 /// is automatically generated from the set and a device using the layout is 25 /// added to the system when the on-screen controls are enabled. 26 /// 27 /// The layout that the generated layout is based on is determined by the 28 /// control paths chosen for each on-screen control. If, for example, an 29 /// on-screen control chooses the 'a' key from the "Keyboard" layout as its 30 /// path, a device layout is generated that is based on the "Keyboard" layout 31 /// and the on-screen control becomes the 'a' key in that layout. 32 /// 33 /// If a <see cref="GameObject"/> has multiple on-screen controls that reference different 34 /// types of device layouts (e.g. one control references 'buttonWest' on 35 /// a gamepad and another references 'leftButton' on a mouse), then a device 36 /// is created for each type referenced by the setup. 37 /// </remarks> 38 public abstract class OnScreenControl : MonoBehaviour 39 { 40 /// <summary> 41 /// The control path (see <see cref="InputControlPath"/>) for the control that the on-screen 42 /// control will feed input into. 43 /// </summary> 44 /// <remarks> 45 /// A device will be created from the device layout referenced by the control path (see 46 /// <see cref="InputControlPath.TryGetDeviceLayout"/>). The path is then used to look up 47 /// <see cref="control"/> on the device. The resulting control will be fed values from 48 /// the on-screen control. 49 /// 50 /// Multiple on-screen controls sharing the same device layout will together create a single 51 /// virtual device. If, for example, one component uses <c>"&lt;Gamepad&gt;/buttonSouth"</c> 52 /// and another uses <c>"&lt;Gamepad&gt;/leftStick"</c> as the control path, a single 53 /// <see cref="Gamepad"/> will be created and the first component will feed data to 54 /// <see cref="Gamepad.buttonSouth"/> and the second component will feed data to 55 /// <see cref="Gamepad.leftStick"/>. 56 /// </remarks> 57 /// <seealso cref="InputControlPath"/> 58 public string controlPath 59 { 60 get => controlPathInternal; 61 set 62 { 63 controlPathInternal = value; 64 if (isActiveAndEnabled) 65 SetupInputControl(); 66 } 67 } 68 69 /// <summary> 70 /// The actual control that is fed input from the on-screen control. 71 /// </summary> 72 /// <remarks> 73 /// This is only valid while the on-screen control is enabled. Otherwise, it is <c>null</c>. Also, 74 /// if no <see cref="controlPath"/> has been set, this will remain <c>null</c> even if the component is enabled. 75 /// </remarks> 76 public InputControl control => m_Control; 77 78 private InputControl m_Control; 79 private OnScreenControl m_NextControlOnDevice; 80 private InputEventPtr m_InputEventPtr; 81 82 /// <summary> 83 /// Accessor for the <see cref="controlPath"/> of the component. Must be implemented by subclasses. 84 /// </summary> 85 /// <remarks> 86 /// Moving the definition of how the control path is stored into subclasses allows them to 87 /// apply their own <see cref="InputControlAttribute"/> attributes to them and thus set their 88 /// own layout filters. 89 /// </remarks> 90 protected abstract string controlPathInternal { get; set; } 91 92 private void SetupInputControl() 93 { 94 Debug.Assert(m_Control == null, "InputControl already initialized"); 95 Debug.Assert(m_NextControlOnDevice == null, "Previous InputControl has not been properly uninitialized (m_NextControlOnDevice still set)"); 96 Debug.Assert(!m_InputEventPtr.valid, "Previous InputControl has not been properly uninitialized (m_InputEventPtr still set)"); 97 98 // Nothing to do if we don't have a control path. 99 var path = controlPathInternal; 100 if (string.IsNullOrEmpty(path)) 101 return; 102 103 // Determine what type of device to work with. 104 var layoutName = InputControlPath.TryGetDeviceLayout(path); 105 if (layoutName == null) 106 { 107 Debug.LogError( 108 $"Cannot determine device layout to use based on control path '{path}' used in {GetType().Name} component", 109 this); 110 return; 111 } 112 113 // Try to find existing on-screen device that matches. 114 var internedLayoutName = new InternedString(layoutName); 115 var deviceInfoIndex = -1; 116 for (var i = 0; i < s_OnScreenDevices.length; ++i) 117 { 118 ////FIXME: this does not take things such as different device usages into account 119 if (s_OnScreenDevices[i].device.m_Layout == internedLayoutName) 120 { 121 deviceInfoIndex = i; 122 break; 123 } 124 } 125 126 // If we don't have a matching one, create a new one. 127 InputDevice device; 128 if (deviceInfoIndex == -1) 129 { 130 // Try to create device. 131 try 132 { 133 device = InputSystem.AddDevice(layoutName); 134 } 135 catch (Exception exception) 136 { 137 Debug.LogError( 138 $"Could not create device with layout '{layoutName}' used in '{GetType().Name}' component"); 139 Debug.LogException(exception); 140 return; 141 } 142 InputSystem.AddDeviceUsage(device, "OnScreen"); 143 144 // Create event buffer. 145 var buffer = StateEvent.From(device, out var eventPtr, Allocator.Persistent); 146 147 // Add to list. 148 deviceInfoIndex = s_OnScreenDevices.Append(new OnScreenDeviceInfo 149 { 150 eventPtr = eventPtr, 151 buffer = buffer, 152 device = device, 153 }); 154 } 155 else 156 { 157 device = s_OnScreenDevices[deviceInfoIndex].device; 158 } 159 160 // Try to find control on device. 161 m_Control = InputControlPath.TryFindControl(device, path); 162 if (m_Control == null) 163 { 164 Debug.LogError( 165 $"Cannot find control with path '{path}' on device of type '{layoutName}' referenced by component '{GetType().Name}'", 166 this); 167 168 // Remove the device, if we just created one. 169 if (s_OnScreenDevices[deviceInfoIndex].firstControl == null) 170 { 171 s_OnScreenDevices[deviceInfoIndex].Destroy(); 172 s_OnScreenDevices.RemoveAt(deviceInfoIndex); 173 } 174 175 return; 176 } 177 m_InputEventPtr = s_OnScreenDevices[deviceInfoIndex].eventPtr; 178 179 // We have all we need. Permanently add us. 180 s_OnScreenDevices[deviceInfoIndex] = 181 s_OnScreenDevices[deviceInfoIndex].AddControl(this); 182 } 183 184 protected void SendValueToControl<TValue>(TValue value) 185 where TValue : struct 186 { 187 if (m_Control == null) 188 return; 189 190 if (!(m_Control is InputControl<TValue> control)) 191 throw new ArgumentException( 192 $"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)); 193 194 ////FIXME: this gives us a one-frame lag (use InputState.Change instead?) 195 m_InputEventPtr.internalTime = InputRuntime.s_Instance.currentTime; 196 control.WriteValueIntoEvent(value, m_InputEventPtr); 197 InputSystem.QueueEvent(m_InputEventPtr); 198 } 199 200 protected void SentDefaultValueToControl() 201 { 202 if (m_Control == null) 203 return; 204 205 ////FIXME: this gives us a one-frame lag (use InputState.Change instead?) 206 m_InputEventPtr.internalTime = InputRuntime.s_Instance.currentTime; 207 m_Control.ResetToDefaultStateInEvent(m_InputEventPtr); 208 InputSystem.QueueEvent(m_InputEventPtr); 209 } 210 211 protected virtual void OnEnable() 212 { 213 SetupInputControl(); 214 } 215 216 protected virtual void OnDisable() 217 { 218 if (m_Control == null) 219 return; 220 221 var device = m_Control.device; 222 for (var i = 0; i < s_OnScreenDevices.length; ++i) 223 { 224 if (s_OnScreenDevices[i].device != device) 225 continue; 226 227 var deviceInfo = s_OnScreenDevices[i].RemoveControl(this); 228 if (deviceInfo.firstControl == null) 229 { 230 // We're the last on-screen control on this device. Remove the device. 231 s_OnScreenDevices[i].Destroy(); 232 s_OnScreenDevices.RemoveAt(i); 233 } 234 else 235 { 236 s_OnScreenDevices[i] = deviceInfo; 237 238 // We're keeping the device but we're disabling the on-screen representation 239 // for one of its controls. If the control isn't in default state, reset it 240 // to that now. This is what ensures that if, for example, OnScreenButton is 241 // disabled after OnPointerDown, we reset its button control to zero even 242 // though we will not see an OnPointerUp. 243 if (!m_Control.CheckStateIsAtDefault()) 244 SentDefaultValueToControl(); 245 } 246 247 m_Control = null; 248 m_InputEventPtr = new InputEventPtr(); 249 Debug.Assert(m_NextControlOnDevice == null); 250 251 break; 252 } 253 } 254 255 private struct OnScreenDeviceInfo 256 { 257 public InputEventPtr eventPtr; 258 public NativeArray<byte> buffer; 259 public InputDevice device; 260 public OnScreenControl firstControl; 261 262 public OnScreenDeviceInfo AddControl(OnScreenControl control) 263 { 264 control.m_NextControlOnDevice = firstControl; 265 firstControl = control; 266 return this; 267 } 268 269 public OnScreenDeviceInfo RemoveControl(OnScreenControl control) 270 { 271 if (firstControl == control) 272 firstControl = control.m_NextControlOnDevice; 273 else 274 { 275 for (OnScreenControl current = firstControl.m_NextControlOnDevice, previous = firstControl; 276 current != null; previous = current, current = current.m_NextControlOnDevice) 277 { 278 if (current != control) 279 continue; 280 281 previous.m_NextControlOnDevice = current.m_NextControlOnDevice; 282 break; 283 } 284 } 285 286 control.m_NextControlOnDevice = null; 287 return this; 288 } 289 290 public void Destroy() 291 { 292 if (buffer.IsCreated) 293 buffer.Dispose(); 294 if (device != null) 295 InputSystem.RemoveDevice(device); 296 device = null; 297 buffer = new NativeArray<byte>(); 298 } 299 } 300 301 private static InlinedArray<OnScreenDeviceInfo> s_OnScreenDevices; 302 303 internal string GetWarningMessage() 304 { 305 return $"{GetType()} needs to be attached as a child to a UI Canvas and have a RectTransform component to function properly."; 306 } 307 } 308 309 internal static class UGUIOnScreenControlUtils 310 { 311 public static RectTransform GetCanvasRectTransform(Transform transform) 312 { 313 var parentTransform = transform.parent; 314 return parentTransform != null ? transform.parent.GetComponentInParent<RectTransform>() : null; 315 } 316 } 317 318#if UNITY_EDITOR 319 internal static class UGUIOnScreenControlEditorUtils 320 { 321 public static void ShowWarningIfNotPartOfCanvasHierarchy(OnScreenControl target) 322 { 323 if (UGUIOnScreenControlUtils.GetCanvasRectTransform(target.transform) == null) 324 UnityEditor.EditorGUILayout.HelpBox(target.GetWarningMessage(), UnityEditor.MessageType.Warning); 325 } 326 } 327#endif 328}