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>"<Gamepad>/buttonSouth"</c>
52 /// and another uses <c>"<Gamepad>/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}