CustomDevice.cs 18 KB

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