Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Knob Layout

Discussion in 'Input System' started by tgaldi, Oct 16, 2019.

  1. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    Hello,

    I have a custom HID device and am creating a DeviceState for it.

    The device has a knob where each position corresponds to an exclusive state.

    The input is a bit-field and comes in on a byte with other input, where the last four bits signify which state/position the knob is in.

    Looking at the docs and playing around with different [InputControl] attributes, I'm unsure how to implement this use case. It seems "Dpad" will work, but the knob state does not correspond to up, down, left, right. I also have another knob that has more than 4 states.

    Thanks.
     
  2. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Recommend taking a look at how DualShock4HIDInputReport sets up the dpad. From what you describe, you're looking at a similar setup, i.e. basically an enum. The key ingredient is the DiscreteButton layout with the minValue, maxValue, nullValue, and wrapAtValue parameters. If there's no range of values (i.e. no diagonals), it's simpler. Each value is just one state hardcoded on the DiscreteButton.
     
  3. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    I'm seeing errors:
    "Could not re-recreate input device '{deviceState.description}' with layout '{deviceState.layout}' and variants '{deviceState.variants}' after domain reload"
    "Cannot find layout matching device description '{description}'", nameof(description)"


    When trying to do the following:

    [InputControl( name = "WINDOW_COVER_STATE", layout = "Button", bit = 0, displayName = "Cover State" )]
    [InputControl( name = "RESET_HEADING_BUTTON", layout = "Button", bit = 1, displayName = "Reset Heading")]
    // bits 2 & 3 are unused
    [InputControl(name = "dpad", format = "BIT", layout = "Dpad", sizeInBits = 4, defaultState = 0)]
    [InputControl(name = "dpad/up", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 4, sizeInBits = 1)]
    [InputControl(name = "dpad/right", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 5, sizeInBits = 1)]
    [InputControl(name = "dpad/down", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 6, sizeInBits = 1)]
    [InputControl(name = "dpad/left", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0, maxValue=1", bit = 7, sizeInBits = 1)]
    public byte inputByte0;


    What I'd really like to do is:

    [InputControl( name = "KNOB", format = "BIT", layout = "DiscreteButton", sizeInBits = 4, displayName = "Knob" )]
    [InputControl( name = "KNOB/off", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 4, sizeInBits = 1, displayName = "Knob off" )]
    [InputControl( name = "KNOB/stdby", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 5, sizeInBits = 1, displayName = "Knob stdby" )]
    [InputControl( name = "KNOB/rdy", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 6, sizeInBits = 1, displayName = "Knob rdy" )]
    [InputControl( name = "KNOB/ovrd", format = "BIT", layout = "DiscreteButton", parameters = "minValue=0,maxValue=1", bit = 7, sizeInBits = 1, displayName = "Knob ovrd" )]
     
  4. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    That's usually a sign of the layout not becoming available in time (usually before the first input update). What's the values of the fields in the error message here? Is it from your layout?

    If you have discrete bits corresponding to each control, you do not need DiscreteButton.

    Code (CSharp):
    1.  
    2. [InputControl( name = "KnobOff", layout = "Button", bit = 4, sizeInBits = 1, displayName = "Knob off" )]
    3.  
    4. [InputControl( name = "KnobStdby", layout = "Button", bit = 5, sizeInBits = 1, displayName = "Knob stdby" )]
    5.  
    6. [InputControl( name = "KnobRdy", layout = "Button", bit = 6, sizeInBits = 1, displayName = "Knob rdy" )]
    7.  
    8. [InputControl( name = "KnobOvrd", layout = "Button", bit = 7, sizeInBits = 1, displayName = "Knob ovrd" )]
    9.  
    To introduce a "KNOB" control at the top with "off", "stdby", "rdy", and "ovrd" as children, you need a custom control layout. However, doing so really only makes sense if indeed it makes sense to treat the entire knob as a single control with a single value for the entire knob. If that is indeed the case, you can create a custom InputControl-derived control in C# (same way that Dpad is set up). Something like (just sketched out; haven't tried to compile or run this so I may have made mistakes):

    Code (CSharp):
    1. [Flags]
    2. public enum KnobState
    3. {
    4.     Off = 1 << 0,
    5.     Stdby = 1 << 1,
    6.     Rdy = 1 << 2,
    7.     Ovrd = 1 << 3,
    8. }
    9.  
    10. public class KnobControl : InputControl<KnobState>
    11. {
    12.     public ButtonControl off { get; private set; }
    13.     public ButtonControl stdby { get; private set; }
    14.     public ButtonControl rdy { get; private set; }
    15.     public ButtonControl ovrd { get; private set; }
    16.  
    17.     protected void override void FinishSetup()
    18.     {
    19.         base.FinishSetup();
    20.  
    21.         off = GetChildControl<ButtonControl>("off");
    22.         stdby = GetChildControl<ButtonControl>("stdby");
    23.         rdy = GetChildControl<ButtonControl>("rdy");
    24.         ovrd = GetChildControl<ButtonControl>("ovrd");
    25.     }
    26.  
    27.     public override unsafe KnobState ReadUnprocessedValueFromState(void* statePtr)
    28.     {
    29.         return (KnobState) stateBlock.ReadInt(statePtr);
    30.     }
    31. }
    With that registered, you can add a "Knob" control with something like this:

    Code (CSharp):
    1. [InputControl(name = "KNOB", layout = "Knob", bit = 4, sizeInBits = 4)]
    2. [InputControl(name = "KNOB/off", bit = 4)]
    3. [InputControl(name = "KNOB/stdby", bit = 5)]
    4. [InputControl(name = "KNOB/rdy", bit = 6)]
    5. [InputControl(name = "KNOB/ovrd", bit = 7)]
    Note that using slashes in paths has special meaning. It signifies a control setting that does not introduce a new control but rather modifies the settings of a child control introduced by another layout.
     
    tgaldi likes this.
  5. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    Yes, the values are from my layout.

    "Could not re-recreate input device 'myDeviceName' with 'MyInputDevice' and variants 'Default' after domain reload"
    "Cannot find layout matching device description 'myDeviceName'"
    "parameter name: description"



    The custom Knob control is exactly what I'm looking for, but after trying to implement it I'm seeing the following errors:

    Layout 'MyInputDevice' matches existing device 'MyDeviceName' but failed to instantiate: UnityEngine.InputSystem.Layouts.InputControlLayout+LayoutNotFoundException: Cannot find layout 'Knob' used in control 'KNOB' of layout 'MyInputDevice' --->
    LayoutNotFoundException: Cannot find control layout 'Knob'

    Code (CSharp):
    1. using System;
    2.  
    3. using UnityEngine.Scripting;
    4.  
    5. [Flags]
    6. public enum KnobState
    7. {
    8.     Off = 1 << 0,
    9.     Stdby = 1 << 1,
    10.     Rdy = 1 << 2,
    11.     Ovrd = 1 << 3,
    12. }
    13.  
    14. namespace UnityEngine.InputSystem.Controls
    15. {
    16.     [Preserve]
    17.     public class KnobControl : InputControl<KnobState>
    18.     {
    19.         public ButtonControl off { get; private set; }
    20.         public ButtonControl stdby { get; private set; }
    21.         public ButtonControl rdy { get; private set; }
    22.         public ButtonControl ovrd { get; private set; }
    23.  
    24.         public KnobControl() { }
    25.  
    26.         protected override void FinishSetup()
    27.         {
    28.             base.FinishSetup();
    29.  
    30.             off = GetChildControl<ButtonControl>( "off" );
    31.             stdby = GetChildControl<ButtonControl>( "stdby" );
    32.             rdy = GetChildControl<ButtonControl>( "rdy" );
    33.             ovrd = GetChildControl<ButtonControl>( "ovrd" );
    34.         }
    35.  
    36.         public override unsafe KnobState ReadUnprocessedValueFromState( void* statePtr )
    37.         {
    38.             return (KnobState)stateBlock.ReadInt( statePtr );
    39.         }
    40.     }
    41. }
     
  6. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    You need to register the control layout. Same procedure as registering your device layout, i.e. calling InputSystem.RegisterLayout<...>. You can do it in the same place where you currently register the device layout.

    Code (CSharp):
    1. InputSystem.RegisterLayout<KnobControl>("Knob");
     
  7. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    "Layout 'MyInputDevice' matches existing device 'MyDeviceName' but failed to instantiate: System.InvalidOperationException: Control '/MyInputDevice/KNOB' with layout 'Knob' has no size set and has no children to compute size from"
     
  8. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Can you show me the snippet where you add the knob to your device layout?
     
  9. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    Code (CSharp):
    1. #if UNITY_EDITOR
    2. [InitializeOnLoad] // Call static class constructor in editor.
    3. #endif
    4. [InputControlLayout( stateType = typeof( MyInputDeviceState ) )]
    5. public class MyInputDevice : InputDevice
    6. {
    7. #if UNITY_EDITOR
    8.     static MyInputDevice() { Initialize(); }
    9. #endif
    10.  
    11.     [RuntimeInitializeOnLoadMethod]
    12.     private static void Initialize()
    13.     {
    14.         InputSystem.RegisterLayout<KnobControl>( "Knob" );
    15.  
    16.         InputSystem.RegisterLayout<MyInputDevice>(
    17.             matches: new InputDeviceMatcher()
    18.                 .WithInterface( "HID" )
    19.                 .WithCapability( "vendorId", 8263 )
    20.                 .WithCapability( "productId", 991 )
    21.                 );
    22.     }
    23.  
    24.     public ButtonControl coverOpen { get; private set; }
    25.     public ButtonControl resetHeading { get; private set; }
    26.     public KnobControl knob { get; private set; }
    27.  
    28.     protected override void FinishSetup()
    29.     {
    30.         base.FinishSetup();
    31.  
    32.         coverOpen = GetChildControl<ButtonControl>( "WINDOW_COVER_STATE" );
    33.         resetHeading = GetChildControl<ButtonControl>( "RESET_HEADING_BUTTON" );
    34.         knob = GetChildControl<KnobControl>( "KNOB" );
    35.     }
    36.  
     
  10. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    You're missing the configuration of the knob control from above on the device.

    Code (CSharp):
    1. [InputControl(name = "KNOB", bit = 4, sizeInBits = 4)]
    2. [InputControl(name = "KNOB/off", bit = 4)]
    3. [InputControl(name = "KNOB/stdby", bit = 5)]
    4. [InputControl(name = "KNOB/rdy", bit = 6)]
    5. [InputControl(name = "KNOB/ovrd", bit = 7)]
    6. public KnobControl knob { get; private set; }
    That should shut the layouter up and get rid of the exceptions.
     
  11. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Actually, sorry, in your case this needs to go on the knob in your MyInputDeviceState struct. And the "KNOB" control will need an explicit layout="Knob" setting.
     
  12. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    I have that in the device state. Will that not work?

    Code (CSharp):
    1. public struct MyInputDeviceState : IInputStateTypeInfo
    2. {
    3.     public FourCC format => new FourCC( "HID" );
    4.  
    5.     public byte reportId;
    6.     public byte reportLength;
    7.  
    8.     [InputControl( name = "WINDOW_COVER_STATE", layout = "Button", bit = 0, displayName = "Cover State" )]
    9.     [InputControl( name = "RESET_HEADING_BUTTON", layout = "Button", bit = 1, displayName = "Reset Heading" )]
    10.  
    11.     [InputControl( name = "KNOB", layout = "Knob", bit = 4, sizeInBits = 4 )]
    12.     [InputControl( name = "KNOB/off", bit = 4)]
    13.     [InputControl( name = "KNOB/stdby", bit = 5)]
    14.     [InputControl( name = "KNOB/rdy", bit = 6)]
    15.     [InputControl( name = "KNOB/ovrd", bit = 7)]
    16.     public byte inputByte0;
    17. }
    18.  
     
  13. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Ah doh, sorry, forgot a vital bit. We changed it some time ago so that properties need an explicit [InputControl] attribute instead of anything that is InputControl-derived getting picked up automatically. Adding [InputControl] to each KnobControl child control should do the trick.

    Code (CSharp):
    1.         [InputControl]
    2.         public ButtonControl off { get; private set; }
    3.         [InputControl]
    4.         public ButtonControl stdby { get; private set; }
    5.         [InputControl]
    6.         public ButtonControl rdy { get; private set; }
    7.         [InputControl]
    8.         public ButtonControl ovrd { get; private set; }
     
    tgaldi likes this.
  14. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    Ah perfect now everything works. Thank you for the assistance!

    For any future readers:

    Code (CSharp):
    1. using System;
    2.  
    3. using UnityEngine.InputSystem.Layouts;
    4.  
    5. [Flags]
    6. public enum KnobState
    7. {
    8.     Off = 1 << 0,
    9.     Stdby = 1 << 1,
    10.     Rdy = 1 << 2,
    11.     Ovrd = 1 << 3,
    12. }
    13.  
    14. namespace UnityEngine.InputSystem.Controls
    15. {
    16.     public class KnobControl : InputControl<KnobState>
    17.     {
    18.         [InputControl]
    19.         public ButtonControl off { get; private set; }
    20.         [InputControl]
    21.         public ButtonControl stdby { get; private set; }
    22.         [InputControl]
    23.         public ButtonControl rdy { get; private set; }
    24.         [InputControl]
    25.         public ButtonControl ovrd { get; private set; }
    26.  
    27.         protected override void FinishSetup()
    28.         {
    29.             base.FinishSetup();
    30.  
    31.             off = GetChildControl<ButtonControl>( "off" );
    32.             stdby = GetChildControl<ButtonControl>( "stdby" );
    33.             rdy = GetChildControl<ButtonControl>( "rdy" );
    34.             ovrd = GetChildControl<ButtonControl>( "ovrd" );
    35.         }
    36.  
    37.         public override unsafe KnobState ReadUnprocessedValueFromState( void* statePtr )
    38.         {
    39.             return (KnobState)stateBlock.ReadInt( statePtr );
    40.         }
    41.     }
    42. }
    43.  
     
  15. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    I've fully implemented my device and everything looks great in the InputDebug tool, but when running/compiling I see:

    Could not re-recreate input device 'MyDevice' with layout 'MyInputDevice' and variants 'Default' after domain reload
    ArgumentException: Cannot find layout matching device description 'MyDevice'
    Parameter name: description
     
  16. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Looking at it, this is a problem in the core system that we need to fix. I've filed a bug.

    To explain, what the system does when it initializes is to run one initial update that only re-creates devices. It does so to make sure that MonoBehaviour.Start methods have devices available. However, this means that if system initialization is triggered from somewhere else (e.g. someone else running InitializeOnLoad/RuntimeInitializeOnLoadMethod code that accesses InputSystem) before your RegisterLayout code, your device won't get recreated properly.

    I'll try to have it fixed for the next package (1.0.0-preview.2) which shouldn't be too far out.
     
    tgaldi likes this.
  17. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Fix pending. Expected to be in 1.0.0-preview.2.
     
    tgaldi likes this.
  18. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    Hey Rene,

    I've updated to preview 2 and the exceptions have been fixed, but when subscribing to
    InputSystem.onDeviceChange
    I receive 3 device added callbacks for my device - one for each layout I'm registering (2 knobs and the device itself).
     
  19. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Hmm, on domain reload or also first time around?

    Is it still this code?

    Code (CSharp):
    1.     [RuntimeInitializeOnLoadMethod]
    2.     private static void Initialize()
    3.     {
    4.         InputSystem.RegisterLayout<KnobControl>( "Knob" );
    5.         InputSystem.RegisterLayout<MyInputDevice>(
    6.             matches: new InputDeviceMatcher()
    7.                 .WithInterface( "HID" )
    8.                 .WithCapability( "vendorId", 8263 )
    9.                 .WithCapability( "productId", 991 )
    10.                 );
    11.     }
    And within that, you're seeing three InputDeviceChange.Added messages?

    Device recreation due to layout changes can be a bit surprising and depending on the code, it *may* do the right thing here. But it sure sounds fishy.
     
  20. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    Yep its still that code. I'm listening for devices in an OnEnable(). Happens when I hit play.

    Code (CSharp):
    1.         private void OnEnable()
    2.         {
    3.             InputSystem.onDeviceChange +=
    4.                 ( device, change ) =>
    5.                     {
    6.                         switch( change )
    7.                         {
    8.                             case InputDeviceChange.Reconnected:
    9.                                 Debug.Log( device + " Connected" );
    10.                                 break;
    11.  
    12.                             case InputDeviceChange.Disconnected:
    13.                                 Debug.Log( device + " Disconnected" );
    14.                                 break;
    15.  
    16.                             case InputDeviceChange.Added:
    17.                             {
    18.                                 Debug.Log( device + " Added" );
    19.                                 break;
    20.                             }
    21.                         }
    22.                     };
    23.         }
    If I disconnect my device while running and connect it again I only see added once. I need the added when the app first runs though because I have a simulated (UI) version of the device and I want to choose which one to use.
     
    Last edited: Nov 5, 2019
  21. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Ah, I have a hunch what's going on here. If that's indeed the case, it's not a bug but certainly another gotcha with the hole registration mechanism (for after 1.0, I'd like to reconsider just switching back to using reflection instead of requiring explicit RegisterXXX calls).

    So, there's both InitializeOnLoad *and* RuntimeInitializeOnLoadMethod attributes on that initialization code. Means that the editor probably invokes the static class constructor first and then later RuntimeInitializeOnLoadMethods run and trigger another Initialize() invocation.

    So, first time around, everything works as expected. Second time around, the Knob layout is re-registered first (leading to the existing device being affected and thus recreated) and then the device layout itself is re-registered (leading to the device getting recreated another time).

    Are you also seeing *two* notifications for InputDeviceChange.Removed or *only* for Added?

    To work around the initialization problem, one possible setup is:

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. [InitializeOnLoad]
    3. #endif
    4. public class MyClass
    5. {
    6.     static MyClass()
    7.     {
    8.         // Put initialization code here.
    9.     }
    10.  
    11.     [RuntimeInitializeOnLoadMethod]
    12.     static void InitializeInPlayer()
    13.     {
    14.         // Empty method. Execute static class constructor as a side effect.
    15.     }
    16. }
    17.  
    Note that due to a bug in IL2CPP, this does not work in players built with that .NET backend. It won't invoke the static class constructor (if that hasn't been fixed since; haven't checked).
     
  22. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    I'm seeing:

    Device added
    Device removed
    Device added
    Device removed
    Device added
     
  23. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Ah yes, if my theory indeed matches what's going on there, that'd be the expected sequence.
    1. [InitializeOnLoad]
    2. Device gets created from RegisterLayout<MyInputDevice>
    3. InputDeviceChange.Added
    4. [RuntimeInitializeOnLoadMethod]
    5. RegisterLayout<KnobControl>
    6. InputDeviceChange.Removed
    7. Device is recreated
    8. InputDeviceChange.Added
    9. RegiterLayout<MyInputDevice>
    10. InputDeviceChange.Removed
    11. Device is recreated
    12. InputDeviceChange.Added
    ATM the system can't tell whether a layout has changed or not. So it blindly recreates everything that is currently using the layout -- which goes for both control and device layouts.
     
  24. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    Hmm ok, switching to the work around initialization does fix the issue, and now I only get the added callback when I launch the editor (and I assume when I launch player executable). This will work, though it does make working/testing code that changes the code path based on the device being present at startup a little harder. I guess i could change it to actively looking for the device, but that won't make my implementation as clean.
     
  25. Rene-Damm

    Rene-Damm

    Joined:
    Sep 15, 2012
    Posts:
    1,779
    Hmm wait, with the change, the InitializeOnLoad code should still get run on every domain reload, i.e. when you go into play mode and every time you change scripts.

    Initialization order there with devices can indeed be tricky. The safest thing is to not assume any specific devices to be present while InitializeOnLoad/RuntimeInitializeOnLoadMethod code is running. The idea here is that these are basically reserved for system startup and no guarantees are made by the input system as to the order in which things become available. There is a guarantee, however, that by the time MonoBehaviour.Start and OnEnable are called, devices are fully initialized.

    If you want to force a dependency, the best thing is to directly make your code that uses the device dependent the registration code for the layout. E.g. by calling the registration code with MyDevice.Initialize() directly from the code depending on the presence of the device.
     
  26. tgaldi

    tgaldi

    Joined:
    Oct 28, 2015
    Posts:
    102
    I only see it when I launch the editor, not when I enter playmode.
    Code (CSharp):
    1.  
    2. #if UNITY_EDITOR
    3. [InitializeOnLoad] // Call static class constructor in editor.
    4. #endif
    5.     [InputControlLayout( stateType = typeof( MyDeviceState ) )]
    6.     public class MyDevice : InputDevice
    7.     {
    8.         static MyDevice()
    9.         {
    10.             InputSystem.RegisterLayout<Knob1Control>( "Knob 1" );
    11.             InputSystem.RegisterLayout<Knob2Control>( "Knob 2" );
    12.  
    13.             InputSystem.RegisterLayout<MyDevice>(
    14.                 matches: new InputDeviceMatcher()
    15.                     .WithInterface( "HID" )
    16.                     .WithCapability( "vendorId", 8263 )
    17.                     .WithCapability( "productId", 991 )
    18.                     );
    19.  
    20.             //Initialize();
    21.         }
    22.  
    23.         [RuntimeInitializeOnLoadMethod]
    24.         static void InitializeInPlayer()
    25.         {
    26.         }
    I have an inventory scriptable object to define different inventories, and it looks for the "added" cb in OnEnable so it will replace the simulated version of the device with the physical version's implementation. This way the device itself doesn't need to know about the inventory.