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