Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions Assets/Tests/InputSystem/CoreTests_Devices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2385,6 +2385,46 @@ public void Devices_JoysticksHaveDeadzonesOnStick()
Assert.That(joystick.stick.ReadValue(), Is.EqualTo(Vector2.zero));
}

[Test]
[Category("Devices")]
public void Devices_CanCreateGenericRacingWheel()
{
var wheel = InputSystem.AddDevice<RacingWheel>();

Assert.That(RacingWheel.current, Is.SameAs(wheel));

Assert.That(wheel.allControls.OfType<AxisControl>().Select(c => c.ReadValue()), Is.All.Zero);
Assert.That(wheel.gear.ReadValue(), Is.Zero);

AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.DpadUp), wheel.dpad.up);
AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.DpadDown), wheel.dpad.down);
AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.DpadLeft), wheel.dpad.left);
AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.DpadRight), wheel.dpad.right);
AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.GearUp), wheel.gearUp);
AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.GearDown), wheel.gearDown);
AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.Menu), wheel.menu);
AssertButtonPress(wheel, new RacingWheelState().WithButton(RacingWheelButton.View), wheel.view);

InputSystem.QueueStateEvent(wheel,
new RacingWheelState
{
gear = 123,
throttle = 0.234f,
brake = 0.345f,
clutch = 0.456f,
handbrake = 0.567f,
wheel = 0.678f,
});
InputSystem.Update();

Assert.That(wheel.gear.ReadValue(), Is.EqualTo(123));
Assert.That(wheel.throttle.ReadValue(), Is.EqualTo(0.234f));
Assert.That(wheel.brake.ReadValue(), Is.EqualTo(0.345f));
Assert.That(wheel.clutch.ReadValue(), Is.EqualTo(0.456f));
Assert.That(wheel.handbrake.ReadValue(), Is.EqualTo(0.567f));
Assert.That(wheel.wheel.ReadValue(), Is.EqualTo(0.678f));
}

[Test]
[Category("Devices")]
public void Devices_PointerDeltasDoNotAccumulateFromPreviousFrame()
Expand Down
2 changes: 2 additions & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ however, it has to be formatted properly to pass verification tests.

- Added support for PS5 DualSense controllers on Mac and Windows.
- Improved the user experience when creating single vs multi-touch touchscreen bindings in the Input Action Asset editor by making both options visible in the input action dropdown menu. Now it's not neccessary to be aware of the touch\*/press path binding syntax ([case 1357664](https://issuetracker.unity3d.com/issues/inputsystem-touchscreens-multi-touch-doesnt-work-when-using-a-custom-inputactionasset)).
- Added a new `RacingWheel` device class that represents a generic racing wheel.
* Support for specific wheels will come later.

## [1.1.1] - 2021-09-03

Expand Down
248 changes: 248 additions & 0 deletions Packages/com.unity.inputsystem/InputSystem/Devices/RacingWheel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;

namespace UnityEngine.InputSystem.LowLevel
{
/// <summary>
/// Input state for a <see cref="RacingWheel"/>.
/// </summary>
/// <remarks>
/// <example>
/// <code>
/// var wheel = InputSystem.AddDevice&lt;RacingWheel&gt;();
///
/// InputSystem.QueueStateEvent(wheel,
/// new RacingWheelState
/// {
/// gear = 2,
/// wheel = 0.5f, // Halfway right.
/// throttle = 0.25f, // Quarter pressure on gas pedal.
/// });
/// </code>
/// </example>
/// </remarks>
public struct RacingWheelState : IInputStateTypeInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We likely don't need state struct, rather only RacingWheel device class, so users can write code against that, as device structs will come with platform specific devices. And HID already has it's own thing anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My experience has been that every time we tried to hide the state struct or do away with this, it just ended up being painful because you had no way to send input to the device without using the tedious APIs that write values through individual controls.

It's like GamepadState. Gamepad has many different variations and possible states but just being able to spawn a gamepad and directly feed it input is quite useful. IMO better to treat all devices the same way there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So let's say PS4/PS5 code will need more state, e.g. it will be like DualShock gamepad vs Gamepad, how would that work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for the current DualShock support. I.e. it'd just come up with its own state format.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok but then it's questionable, e.g. you will not able to queue RacingWheelState into device that is racing wheel but implements it's own state format.

{
public static FourCC Format => new FourCC('W', 'E', 'E', 'L');

[InputControl(name = "dpad", layout = "Dpad", usage = "Hatswitch", displayName = "D-Pad", format = "BIT", sizeInBits = 4, bit = 0)]
[InputControl(name = "dpad/up", format = "BIT", bit = (uint)RacingWheelButton.DpadUp, sizeInBits = 1)]
[InputControl(name = "dpad/right", format = "BIT", bit = (uint)RacingWheelButton.DpadRight, sizeInBits = 1)]
[InputControl(name = "dpad/down", format = "BIT", bit = (uint)RacingWheelButton.DpadDown, sizeInBits = 1)]
[InputControl(name = "dpad/left", format = "BIT", bit = (uint)RacingWheelButton.DpadLeft, sizeInBits = 1)]
[InputControl(name = "gearUp", layout = "Button", bit = (uint)RacingWheelButton.GearUp, displayName = "Next Gear")]
[InputControl(name = "gearDown", layout = "Button", bit = (uint)RacingWheelButton.GearDown, displayName = "Previous Gear")]
[InputControl(name = "menu", alias = "start", layout = "Button", bit = (uint)RacingWheelButton.Menu, displayName = "Menu")]
[InputControl(name = "view", alias = "select", layout = "Button", bit = (uint)RacingWheelButton.View, displayName = "View")]
public uint buttons;

/// <summary>
/// Current gear selected in gear shift.
/// </summary>
[InputControl(displayName = "Gear", layout = "Integer")]
public int gear;

/// <summary>
/// Position of wheel in [-1,1] range. -1 is all the way left, 1 is all the way right.
/// </summary>
[InputControl(displayName = "Wheel", layout = "Axis")]
public float wheel;

/// <summary>
/// Position of throttle/gas pedal in [0,1] range. 0 is fully depressed, 1 is pressed all the way down.
/// </summary>
[InputControl(displayName = "Throttle", alias = "gas", layout = "Axis")]
public float throttle;

/// <summary>
/// Position of brake pedal in [0,1] range. 0 is fully depressed, 1 is pressed all the way down.
/// </summary>
[InputControl(displayName = "Break", layout = "Axis")]
public float brake;

/// <summary>
/// Position of clutch pedal in [0,1] range. 0 is fully depressed, 1 is pressed all the way down.
/// </summary>
[InputControl(displayName = "Clutch", layout = "Axis")]
public float clutch;

/// <summary>
/// Position of handbrake in [0,1] range. 0 is fully released, 1 is engaged at maximum.
/// </summary>
[InputControl(displayName = "Handbrake", layout = "Axis")]
public float handbrake;

public FourCC format => Format;

/// <summary>
/// Set the specific buttons to be pressed or unpressed.
/// </summary>
/// <param name="button">A racing wheel button.</param>
/// <param name="down">Whether to set <paramref name="button"/> to be pressed or not pressed in
/// <see cref="buttons"/>.</param>
/// <returns>RacingWheelState with a modified <see cref="buttons"/> mask.</returns>
public RacingWheelState WithButton(RacingWheelButton button, bool down = true)
{
Debug.Assert((int)button < 32, $"Expected button < 32, so we fit into the 32 bit wide bitmask");
var bit = 1U << (int)button;
if (down)
buttons |= bit;
else
buttons &= ~bit;
return this;
}
}

/// <summary>
/// Buttons on a <see cref="RacingWheel"/>.
/// </summary>
public enum RacingWheelButton
{
/// <summary>
/// The up button on a wheel's dpad.
/// </summary>
DpadUp = 0,

/// <summary>
/// The down button on a wheel's dpad.
/// </summary>
DpadDown = 1,

/// <summary>
/// The left button on a wheel's dpad.
/// </summary>
DpadLeft = 2,

/// <summary>
/// The right button on a wheel's dpad.
/// </summary>
DpadRight = 3,

/// <summary>
/// Button to shift up.
/// </summary>
GearUp = 4,

/// <summary>
/// Button to shift down.
/// </summary>
GearDown = 5,

/// <summary>
/// The "menu" button.
/// </summary>
Menu = 6,

/// <summary>
/// The "view" button.
/// </summary>
View = 7,
}
}

namespace UnityEngine.InputSystem
{
/// <summary>
/// Base class for racing wheels.
/// </summary>
[InputControlLayout(stateType = typeof(RacingWheelState), displayName = "Racing Wheel", isGenericTypeOfDevice = true)]
public class RacingWheel : InputDevice
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Biggest question for me is how we want this to relate to Gamepad. I think pretty much any wheel out there has controls (such as the face and shoulder buttons) to allow it to function mostly like a gamepad. But not every API (same GameInput) defines wheels that way.

So, guess the question is how much we want to put in here in the abstract definition.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely shouldn't be a gamepad, but rather dpad from racing wheel should participate in navigation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering about additional controls, though. Seems most sensible to add them here, too. Don't think the wheel should separately be represented as a gamepad but it should probably have ABXY face buttons and shoulder buttons instead of just the narrow set that's currently on the device.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree any additional face buttons should exist with the race wheel device.

{
/// <summary>
/// The D-Pad control on the wheel.
/// </summary>
public DpadControl dpad { get; protected set; }

/// <summary>
/// Button to shift gears up.
/// </summary>
public ButtonControl gearUp { get; protected set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea how https://www.logitechg.com/en-us/products/driving/driving-force-shifter.941-000119.html is exposed on HID layer? Not sure if gear lever needs more work here or not

Copy link
Contributor Author

@Rene-Damm Rene-Damm Oct 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My assumption is this would use the gear control and not the gearUp/gearDown button controls corresponding to the commonly found levers/buttons on the wheel. But not sure.

Also not sure how it's exposed at the HID layer. My very limited experience so far with Logitech wheels has been that they masquerade as plain XInput devices and have no interesting properties whatsoever as HIDs and need interfacing through the Logitech SDK to get to the advanced stuff. But no clue about the gear shifters.


/// <summary>
/// Button to shift gears down.
/// </summary>
public ButtonControl gearDown { get; protected set; }

/// <summary>
/// The menu/start button on the wheel.
/// </summary>
public ButtonControl menu { get; protected set; }

/// <summary>
/// The view/select button on the wheel.
/// </summary>
public ButtonControl view { get; protected set; }

/// <summary>
/// The control that represents the currently selected gear.
/// </summary>
public IntegerControl gear { get; protected set; }

/// <summary>
/// The position of the wheel in [-1,1] range. -1 is all the way left, 1 is all the way right, 0 is in neutral position.
/// </summary>
public AxisControl wheel { get; protected set; }

/// <summary>
/// The position of the gas/throttle pedal or control. In [0,1] range. 0 is fully released and 1 is pressed all the way down.
/// </summary>
public AxisControl throttle { get; protected set; }

/// <summary>
/// The position of the brake pedal or control. In [0,1] range. 0 is fully released and 1 is pressed all the way down.
/// </summary>
public AxisControl brake { get; protected set; }

/// <summary>
/// The position of the clutch pedal or control. In [0,1] range. 0 is fully released and 1 is pressed all the way down.
/// </summary>
public AxisControl clutch { get; protected set; }

/// <summary>
/// The position of the handbrake control. In [0,1] range. 0 is fully released and 1 is engaged at maximum.
/// </summary>
public AxisControl handbrake { get; protected set; }

/// <summary>
/// The currently active racing wheel or <c>null</c> if no racing wheel is present.
/// </summary>
public static RacingWheel current { get; private set; }

/// <summary>
/// Make this the <see cref="current"/> racing wheel. This happens automatically for a racing wheel
/// that receives input.
/// </summary>
public override void MakeCurrent()
{
base.MakeCurrent();
current = this;
}

protected override void OnRemoved()
{
base.OnRemoved();

if (this == current)
current = null;
}

protected override void FinishSetup()
{
base.FinishSetup();

dpad = GetChildControl<DpadControl>("dpad");
gearUp = GetChildControl<ButtonControl>("gearUp");
gearDown = GetChildControl<ButtonControl>("gearDown");
menu = GetChildControl<ButtonControl>("menu");
view = GetChildControl<ButtonControl>("view");
gear = GetChildControl<IntegerControl>("gear");
wheel = GetChildControl<AxisControl>("wheel");
throttle = GetChildControl<AxisControl>("throttle");
brake = GetChildControl<AxisControl>("brake");
clutch = GetChildControl<AxisControl>("clutch");
handbrake = GetChildControl<AxisControl>("handbrake");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Packages/com.unity.inputsystem/InputSystem/InputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,7 @@ internal void InitializeData()

RegisterControlLayout("Gamepad", typeof(Gamepad)); // Devices.
RegisterControlLayout("Joystick", typeof(Joystick));
RegisterControlLayout("RacingWheel", typeof(RacingWheel));
RegisterControlLayout("Keyboard", typeof(Keyboard));
RegisterControlLayout("Pointer", typeof(Pointer));
RegisterControlLayout("Mouse", typeof(Mouse));
Expand Down