File size: 18,276 Bytes
18a519f | 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 353 | 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; private set; }
public ButtonControl secondButton { get; private set; }
public ButtonControl thirdButton { get; private set; }
public StickControl stick { get; private 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);
}
}
|