-
Notifications
You must be signed in to change notification settings - Fork 318
/
Copy pathCustomDevice.cs
352 lines (329 loc) · 17.9 KB
/
CustomDevice.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif
// The input system stores a chunk of memory for each device. What that
// memory looks like we can determine ourselves. The easiest way is to just describe
// it as a struct.
//
// Each chunk of memory is tagged with a "format" identifier in the form
// of a "FourCC" (a 32-bit code comprised of four characters). Using
// IInputStateTypeInfo we allow the system to get to the FourCC specific
// to our struct.
public struct CustomDeviceState : IInputStateTypeInfo
{
// We use "CUST" here as our custom format code. It can be anything really.
// Should be sufficiently unique to identify our memory format, though.
public FourCC format => new FourCC('C', 'U', 'S', 'T');
// Next we just define fields that store the state for our input device.
// The only thing really interesting here is the [InputControl] attributes.
// These automatically attach InputControls to the various memory bits that
// we define.
//
// To get started, let's say that our device has a bitfield of buttons. Each
// bit indicates whether a certain button is pressed or not. For the sake of
// demonstration, let's say our device has 16 possible buttons. So, we define
// a ushort field that contains the state of each possible button on the
// device.
//
// On top of that, we need to tell the input system about each button. Both
// what to call it and where to find it. The "name" property tells the input system
// what to call the control; the "layout" property tells it what type of control
// to create ("Button" in our case); and the "bit" property tells it which bit
// in the bitfield corresponds to the button.
//
// We also tell the input system about "display names" here. These are names
// that get displayed in the UI and such.
[InputControl(name = "firstButton", layout = "Button", bit = 0, displayName = "First Button")]
[InputControl(name = "secondButton", layout = "Button", bit = 1, displayName = "Second Button")]
[InputControl(name = "thirdButton", layout = "Button", bit = 2, displayName = "Third Button")]
public ushort buttons;
// Let's say our device also has a stick. However, the stick isn't stored
// simply as two floats but as two unsigned bytes with the midpoint of each
// axis located at value 127. We can simply define two consecutive byte
// fields to represent the stick and annotate them like so.
//
// First, let's introduce stick control itself. This one is simple. We don't
// yet worry about X and Y individually as the stick as whole will itself read the
// component values from those controls.
//
// We need to set "format" here too as InputControlLayout will otherwise try to
// infer the memory format from the field. As we put this attribute on "X", that
// would come out as "BYTE" -- which we don't want. So we set it to "VC2B" (a Vector2
// of bytes).
[InputControl(name = "stick", format = "VC2B", layout = "Stick", displayName = "Main Stick")]
// So that's what we need next. By default, both X and Y on "Stick" are floating-point
// controls so here we need to individually configure them the way they work for our
// stick.
//
// NOTE: We don't mention things as "layout" and such here. The reason is that we are
// modifying a control already defined by "Stick". This means that we only need
// to set the values that are different from what "Stick" stick itself already
// configures. And since "Stick" configures both "X" and "Y" to be "Axis" controls,
// we don't need to worry about that here.
//
// Using "format", we tell the controls how their data is stored. As bytes in our case
// so we use "BYTE" (check the documentation for InputStateBlock for details on that).
//
// NOTE: We don't use "SBYT" (signed byte) here. Our values are not signed. They are
// unsigned. It's just that our "resting" (i.e. mid) point is at 127 and not at 0.
//
// Also, we use "defaultState" to tell the system that in our case, setting the
// memory to all zeroes will *NOT* result in a default value. Instead, if both x and y
// are set to zero, the result will be Vector2(-1,-1).
//
// And then, using the various "normalize" parameters, we tell the input system how to
// deal with the fact that our midpoint is located smack in the middle of our value range.
// Using "normalize" (which is equivalent to "normalize=true") we instruct the control
// to normalize values. Using "normalizeZero=0.5", we tell it that our midpoint is located
// at 0.5 (AxisControl will convert the BYTE value to a [0..1] floating-point value with
// 0=0 and 255=1) and that our lower limit is "normalizeMin=0" and our upper limit is
// "normalizeMax=1". Put another way, it will map [0..1] to [-1..1].
//
// Finally, we also set "offset" here as this is already set by StickControl.X and
// StickControl.Y -- which we inherit. Note that because we're looking at child controls
// of the stick, the offset is relative to the stick, not relative to the beginning
// of the state struct.
[InputControl(name = "stick/x", defaultState = 127, format = "BYTE",
offset = 0,
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
public byte x;
[InputControl(name = "stick/y", defaultState = 127, format = "BYTE",
offset = 1,
parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
// The stick up/down/left/right buttons automatically use the state set up for X
// and Y but they have their own parameters. Thus we need to also sync them to
// the parameter settings we need for our BYTE setup.
// NOTE: This is a shortcoming in the current layout system that cannot yet correctly
// merge parameters. Will be fixed in a future version.
[InputControl(name = "stick/up", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=0,clampMax=1")]
[InputControl(name = "stick/down", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "stick/left", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=-1,clampMax=0,invert")]
[InputControl(name = "stick/right", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=2,clampMin=0,clampMax=1")]
public byte y;
}
// Now that we have the state struct all sorted out, we have a way to lay out the memory
// for our device and we have a way to map InputControls to pieces of that memory. What
// we're still missing, however, is a way to represent our device as a whole within the
// input system.
//
// For that, we start with a class derived from InputDevice. We could also base this
// on something like Mouse or Gamepad in case our device is an instance of one of those
// specific types but for this demonstration, let's assume our device is nothing like
// those devices (if we base our devices on those layouts, we have to correctly map the
// controls we inherit from those devices).
//
// Other than deriving from InputDevice, there are two other noteworthy things here.
//
// For one, we want to ensure that the call to InputSystem.RegisterLayout happens as
// part of startup. Doing so ensures that the layout is known to the input system and
// thus appears in the control picker. So we use [InitializeOnLoad] and [RuntimeInitializeOnLoadMethod]
// here to ensure initialization in both the editor and the player.
//
// Also, we use the [InputControlLayout] attribute here. This attribute is optional on
// types that are used as layouts in the input system. In our case, we have to use it
// to tell the input system about the state struct we are using to define the memory
// layout we are using and the controls tied to it.
#if UNITY_EDITOR
[InitializeOnLoad] // Call static class constructor in editor.
#endif
[InputControlLayout(stateType = typeof(CustomDeviceState))]
public class CustomDevice : InputDevice, IInputUpdateCallbackReceiver
{
// [InitializeOnLoad] will ensure this gets called on every domain (re)load
// in the editor.
#if UNITY_EDITOR
static CustomDevice()
{
// Trigger our RegisterLayout code in the editor.
Initialize();
}
#endif
// In the player, [RuntimeInitializeOnLoadMethod] will make sure our
// initialization code gets called during startup.
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
// Register our device with the input system. We also register
// a "device matcher" here. These are used when a device is discovered
// by the input system. Each device is described by an InputDeviceDescription
// and an InputDeviceMatcher can be used to match specific properties of such
// a description. See the documentation of InputDeviceMatcher for more
// details.
//
// NOTE: In case your device is more dynamic in nature and cannot have a single
// static layout, there is also the possibility to build layouts on the fly.
// Check out the API documentation for InputSystem.onFindLayoutForDevice and
// for InputSystem.RegisterLayoutBuilder.
InputSystem.RegisterLayout<CustomDevice>(
matches: new InputDeviceMatcher()
.WithInterface("Custom"));
}
// While our device is fully functional at this point, we can refine the API
// for it a little bit. One thing we can do is expose the controls for our
// device directly. While anyone can look up our controls using strings, exposing
// the controls as properties makes it simpler to work with the device in script.
public ButtonControl firstButton { get; protected set; }
public ButtonControl secondButton { get; protected set; }
public ButtonControl thirdButton { get; protected set; }
public StickControl stick { get; protected set; }
// FinishSetup is where our device setup is finalized. Here we can look up
// the controls that have been created.
protected override void FinishSetup()
{
base.FinishSetup();
firstButton = GetChildControl<ButtonControl>("firstButton");
secondButton = GetChildControl<ButtonControl>("secondButton");
thirdButton = GetChildControl<ButtonControl>("thirdButton");
stick = GetChildControl<StickControl>("stick");
}
// We can also expose a '.current' getter equivalent to 'Gamepad.current'.
// Whenever our device receives input, MakeCurrent() is called. So we can
// simply update a '.current' getter based on that.
public static CustomDevice current { get; private set; }
public override void MakeCurrent()
{
base.MakeCurrent();
current = this;
}
// When one of our custom devices is removed, we want to make sure that if
// it is the '.current' device, we null out '.current'.
protected override void OnRemoved()
{
base.OnRemoved();
if (current == this)
current = null;
}
// So, this is all great and nice. But we have one problem. No one is actually
// creating an instance of our device yet. Which means that while we can bind
// to controls on the device from actions all we want, at runtime we will never
// actually receive input from our custom device. For that to happen, we need
// to make sure that an instance of the device is created at some point.
//
// This one's a bit tricky. Because it really depends on how the device is
// actually discovered in practice. In most real-world scenarios, there will be
// some external API that notifies us when a device under its domain is added or
// removed. In response, we would report a device being added (using
// InputSystem.AddDevice(new InputDeviceDescription { ... }) or removed
// (using DeviceRemoveEvent).
//
// In this demonstration, we don't have an external API to query. And we don't
// really have another criteria by which to determine when a device of our custom
// type should be added.
//
// So, let's fake it here. First, to create the device, we simply add a menu entry
// in the editor. Means that in the player, this device will never be functional
// but this serves as a demonstration only anyway.
//
// NOTE: Nothing of the following is necessary if you have a device that is
// detected and sent input for by the Unity runtime itself, i.e. that is
// picked up from the underlying platform APIs by Unity itself. In this
// case, when your device is connected, Unity will automatically report an
// InputDeviceDescription and all you have to do is make sure that the
// InputDeviceMatcher you supply to RegisterLayout matches that description.
//
// Also, IInputUpdateCallbackReceiver and any other manual queuing of input
// is unnecessary in that case as Unity will queue input for the device.
#if UNITY_EDITOR
[MenuItem("Tools/Custom Device Sample/Create Device")]
private static void CreateDevice()
{
// This is the code that you would normally run at the point where
// you discover devices of your custom type.
InputSystem.AddDevice(new InputDeviceDescription
{
interfaceName = "Custom",
product = "Sample Product"
});
}
// For completeness sake, let's also add code to remove one instance of our
// custom device. Note that you can also manually remove the device from
// the input debugger by right-clicking in and selecting "Remove Device".
[MenuItem("Tools/Custom Device Sample/Remove Device")]
private static void RemoveDevice()
{
var customDevice = InputSystem.devices.FirstOrDefault(x => x is CustomDevice);
if (customDevice != null)
InputSystem.RemoveDevice(customDevice);
}
#endif
// So the other part we need is to actually feed input for the device. Notice
// that we already have the IInputUpdateCallbackReceiver interface on our class.
// What this does is to add an OnUpdate method that will automatically be called
// by the input system whenever it updates (actually, it will be called *before*
// it updates, i.e. from the same point that InputSystem.onBeforeUpdate triggers).
//
// Here, we can feed input to our devices.
//
// NOTE: We don't have to do this here. InputSystem.QueueEvent can be called from
// anywhere, including from threads. So if, for example, you have a background
// thread polling input from your device, that's where you can also queue
// its input events.
//
// Again, we don't have actual input to read here. So we just make up some stuff
// here for the sake of demonstration. We just poll the keyboard
//
// NOTE: We poll the keyboard here as part of our OnUpdate. Remember, however,
// that we run our OnUpdate from onBeforeUpdate, i.e. from where keyboard
// input has not yet been processed. This means that our input will always
// be one frame late. Plus, because we are polling the keyboard state here
// on a frame-to-frame basis, we may miss inputs on the keyboard.
//
// NOTE: One thing we could instead is to actually use OnScreenControls that
// represent the controls of our device and then use that to generate
// input from actual human interaction.
public void OnUpdate()
{
var keyboard = Keyboard.current;
if (keyboard == null)
return;
var state = new CustomDeviceState();
state.x = 127;
state.y = 127;
// WARNING: It may be tempting to simply store some state related to updates
// directly on the device. For example, let's say we want scale the
// vector from WASD to a certain length which can be adjusted with
// the scroll wheel of the mouse. It seems natural to just store the
// current strength as a private field on CustomDevice.
//
// This will *NOT* work correctly. *All* input state must be stored
// under the domain of the input system. InputDevices themselves
// cannot private store their own separate state.
//
// What you *can* do however, is simply add fields your state struct
// (CustomDeviceState in our case) that contain the state you want
// to keep. It is not necessary to expose these as InputControls if
// you don't want to.
// Map WASD to stick.
var wPressed = keyboard.wKey.isPressed;
var aPressed = keyboard.aKey.isPressed;
var sPressed = keyboard.sKey.isPressed;
var dPressed = keyboard.dKey.isPressed;
if (aPressed)
state.x -= 127;
if (dPressed)
state.x += 127;
if (wPressed)
state.y += 127;
if (sPressed)
state.y -= 127;
// Map buttons to 1, 2, and 3.
if (keyboard.digit1Key.isPressed)
state.buttons |= 1 << 0;
if (keyboard.digit2Key.isPressed)
state.buttons |= 1 << 1;
if (keyboard.digit3Key.isPressed)
state.buttons |= 1 << 2;
// Finally, queue the event.
// NOTE: We are replacing the current device state wholesale here. An alternative
// would be to use QueueDeltaStateEvent to replace only select memory contents.
InputSystem.QueueStateEvent(this, state);
}
}