FFB Input Provider (Steering Wheel Support)
FFBInputProvider adds steering wheel support with force feedback (FFB) to NWH Vehicle Physics 2 using the DirectInput API. It works with any DirectInput-compatible device including Logitech, Thrustmaster, Fanatec, Moza, Simagic, Simucube, and other manufacturers.
Warning: Requires two front wheels. Windows only - DirectInput does not exist on other platforms.
The force sent to the wheel is derived from the tire model, not from a set of canned effects. Self-aligning torque comes from the same aligning moment the tires produce in the physics step, so the wheel lightens as the front tires approach the grip limit and weights up again as they recover.
Compatible Devices
All DirectInput-compatible devices work, including:
| Device | Status |
|---|---|
| Simucube 2 Pro/Ultimate | Verified |
| Fanatec DD1/DD2/CSL DD | Verified |
| Moza R9 | Verified |
| Simagic Alpha | Verified |
| Logitech G29/G920/G923/PRO | Verified |
| Thrustmaster TX/T300 | Verified |
Setup
1. Install Unity-DirectInput Package
Add the Unity-DirectInput package via Unity Package Manager:
- Open Window > Package Manager
- Click the + button and select Add package from git URL
- Enter:
https://github.com/imDanoush/Unity-DirectInput.git - Click Install
- Restart Unity after installation
2. Import the DirectInput FFB Sample
- Open Window > Package Manager
- Find NWH Vehicle Physics 2 in the list
- Expand Samples
- Import DirectInput FFB Sample
3. Add FFBInputProvider and DIInputManager to Scene
- Add the
FFBInputProviderprefab from the sample to your scene, OR - Add both
DIInputManagerandFFBInputProvidercomponents to a GameObject
The input provider requires DIInputManager to function and will automatically detect the first FFB-capable device.
Note: only one
DIInputManagermay exist in the scene. Both theFFBInputProviderprefab and theDirectInputManagerprefab carry one, so adding both gives you two managers fighting over the device. Use one or the other.
4. Configure Input Mappings
Configure your device's axis and button mappings via the DIInputManager component. The axis and button names on FFBInputProvider are string keys that must match the mapping names configured there - a typo produces a silently dead axis, not an error.
Default Axis Mappings:
- Steering: X axis
- Throttle: Y axis
- Brake: Z rotation
- Clutch: Z axis
Default Button Mappings:
- Shift Up/Down: Paddles or sequential shifter
- H-Pattern: Buttons for each gear position
5. Set Wheelbase Torque and Check Polarity
Set wheelbaseTorque to your device's actual peak torque before anything else - every force in the system is scaled against it.
Then check the direction of the force. Output polarity is not the same on every device: DirectInput does not define which sign means "push left", so roughly half of all wheels need the output inverted. Drive slowly and turn:
- Correct: the wheel resists the turn and tries to return to centre.
- Wrong: the wheel pulls itself further into the turn and runs to full lock.
If it is wrong, enable invertFFBOutput. Do not try to correct it with negative coefficients elsewhere - that inverts the detail effects along with the main force.
Gotcha:
invertFFBOutput,softLockStrength,softLockRampDegandcollisionFullScaleDeltaVexist on the component but are not drawn by the custom inspector. To reach them, switch the Inspector to Debug mode (context menu at the top right of the Inspector window), or set them from a script.
FFBInputProvider Settings
The sections below match the inspector layout.
Core Settings
| Setting | Description |
|---|---|
vehicleController |
Vehicle to read forces from. Follows the active vehicle if a VehicleChanger is present. |
useDirectInput |
Bypasses the steering smoothing and speed-sensitivity in Steering, giving 1:1 wheel-to-road angle. Leave on for a wheel. |
hShifterUse34asDR |
Uses H-pattern positions 3 and 4 as Drive and Reverse, for an automatic gearbox driven with a shifter. |
Hardware Configuration
| Setting | Description |
|---|---|
wheelbaseTorque |
Peak torque of your wheel in Nm. G29/G923 = 2-3, T300 = 4-5, CSL DD = 5-8, Moza R9 = 9, DD1/DD2 = 15-20, Simucube 2 Pro = 25. Set this correctly first - it is the reference every other force is scaled against. |
deviceRotationRange |
Physical rotation range the wheel is configured for in its driver, in degrees (typically 900). Must match the driver setting or the steering angle will not line up. |
steeringSensitivity |
Multiplier on steering input. Leave at 1 for a 1:1 wheel. |
Steering Geometry
These two set how much of the steering weight comes from vertical load rather than from lateral force. Load-based weight survives a slide and lightens over a crest, which is what makes the wheel feel connected to the road rather than only to the tires.
| Setting | Description |
|---|---|
casterAngle |
Caster in degrees. 3-7 for road cars, 5-8 for rally. Overridden per vehicle by FFBVehicleSettings.casterAngle when that is above 0. |
kingpinOffset |
Lateral distance from the kingpin axis to the contact patch, in metres. Drives parking effort and the lightening felt over a crest. |
Force Feedback Settings
| Setting | Description |
|---|---|
overallEffectStrength |
Master multiplier. 0 disables force feedback. |
forceFeedbackEffect |
Curve scaling FFB strength against steering angle. |
springEffect |
Curve scaling the DirectInput spring effect against steering angle. |
springEffectStrength |
DirectInput spring (centring) effect. 0 by default - centring already comes from the tire model. |
lowSpeedFriction |
Steering resistance while stationary or nearly so. |
friction |
Steering resistance at speed. |
centeringForceStrength |
Legacy suspension-based centring. |
centeringForceOffset |
Offsets the centre by the drift angle, so the wheel centres on the direction of travel rather than straight ahead. |
collisionEffectMultiplier |
Strength of the impact jolt. |
High-Rate Output and Mixing
Force is sent to the device from a dedicated thread rather than once per rendered frame, so kerb and grip-limit detail is not tied to your framerate.
| Setting | Description |
|---|---|
ffbThreadHz |
Output rate of the FFB thread. The default 333 Hz covers the 40-150 Hz band where kerb, rut and grip-limit detail live. Lower it only if you are chasing CPU. |
textureBudget |
Ceiling on the combined road-texture effects as a fraction of full force, before mixing with the self-aligning torque. Keeps detail from drowning out the main force. |
softClipKnee |
Force is linear up to this fraction of full scale, then compresses smoothly instead of clipping flat. 1 = hard clip. |
maxSlewNmPerSec |
Limit on how fast output torque may change, in Nm/s. Bounds impact spikes a real steering rack could not produce. 0 = unlimited. |
Damping and Centering
| Setting | Description |
|---|---|
damperStrength |
Velocity damper strength. Stabilises the wheel without attenuating the tire signal. |
damperFadeSpeed |
Vehicle speed (m/s) at which the damper drops to its high-speed floor. |
damperHighSpeedFloor |
Damper multiplier at and above the fade speed. This is the steering "drag" felt at speed - too low and a direct-drive wheel feels like it is freewheeling, too high and road detail smears. |
frictionCoeffAmount |
Small constant DirectInput friction so the wheel never feels loose. Fades as the tires start to slide. |
suspensionCenteringStrength |
Legacy suspension-difference centring offset. 0 by default; useful only on low-torque belt or gear wheels. |
Texture Effects
| Setting | Description |
|---|---|
roadTextureStrength |
Strength of road texture. Derived by high-pass filtering the real rack force, so it is automatically correct for surface and speed. 0 = off. |
Note: earlier versions had separate kerb, scrub, engine-resonance and ABS oscillators. Those were fixed-frequency tones advanced once per frame, which aliases - a 55 Hz kerb tone folds down to about 5 Hz at 60 fps, and its pitch changes with framerate. They are replaced by this single term.
Suspension Feedback
| Setting | Description |
|---|---|
suspensionBumpStrength |
Kerb, pothole and road-surface feel through the steering. |
suspensionBumpThreshold |
Minimum suspension velocity (m/s) before bump feedback triggers. Lower is more sensitive. |
Input Inversion, Axes and Deadzones
flipSteeringInput, flipThrottleInput, flipBrakeInput, flipClutchInput and flipHandbrakeInput invert a pedal or axis that reads backwards. The pedal flips default to on, which suits most wheels - a pedal that reads full when released usually needs its flip turned off, not on.
steeringAxis0to1 tells the provider whether the steering axis reports 0-1 (most devices) or already reports -1 to 1.
Deadzones (throttleDeadzone, brakeDeadzone, clutchDeadzone, handbrakeDeadzone, steeringDeadzone) trim noise around the resting position of worn pedals. Steering deadzone defaults to 0 and should normally stay there.
Axis names (steeringAxis, throttleAxis, brakeAxis, clutchAxis, handbrakeAxis) and the shifter button names must match the mappings in DIInputManager.
Fields Not Shown in the Inspector
| Setting | Description |
|---|---|
invertFFBOutput |
Flips the sign of the force sent to the device. See step 5 of Setup. |
softLockStrength |
Strength of the end stop felt past full steering lock, as a fraction of full device torque. 0 disables it. |
softLockRampDeg |
Degrees of wheel travel past lock over which the end stop ramps up to full strength. |
collisionFullScaleDeltaV |
Impact speed change (m/s) that produces a full-scale collision jolt. |
Per-Vehicle Tuning
Add FFBVehicleSettings to your VehicleController GameObject to scale FFB per vehicle. A truck and a formula car want different steering weight from the same physical wheel.
| Setting | Description |
|---|---|
overallCoeff |
Overall FFB strength for this vehicle. |
satCoefficient |
Self-aligning torque multiplier. |
mechanicalTrailCoeff |
Multiplier on the caster/kingpin load term. 0 = tire trail only (very light), 0.2 = baseline, above 1 = exaggerated weight. |
centeringCoeff |
Centring force multiplier. |
casterAngle |
Caster for this vehicle. 0 = use the provider's global value. |
kingpinOffset |
Kingpin offset for this vehicle, in metres. Negative = use global. |
wheelRotationRange |
Steering wheel rotation range for this vehicle, in degrees. |
staticFrontLoadOverride |
Measured static front axle load in Newtons, used to normalise how load modulates the rack force. 0 = estimate as half the vehicle weight. |
frictionCoeff |
At-speed friction multiplier. |
lowSpeedFrictionCoeff |
Stationary friction multiplier. |
longitudinalCoeff |
How much braking and acceleration are felt through the steering. |
scrubRadius |
Scrub radius in metres. Typical 0.01-0.03 for road cars. |
suspensionBumpStrength |
Bump feedback for this vehicle. Negative = use global. |
suspensionBumpThreshold |
Bump sensitivity for this vehicle. Negative = use global. |
Gotcha: fields whose "inherit" value is negative (
kingpinOffset,suspensionBumpStrength,suspensionBumpThreshold) treat 0 as a real value meaning off. Leave them negative unless you actually want that effect disabled on this vehicle.
Gotcha:
wheelRotationRangehere also sets the steering ratio used to convert rack torque to torque at the rim. It is not the same field asSteering.steeringWheelTurnRatio, which only rotates the in-game steering wheel mesh and does nothing if no mesh is assigned.
Tuning by Wheel Type
Direct drive (CSL DD, R9, Alpha, Simucube). Defaults are aimed here. Set wheelbaseTorque correctly, leave springEffectStrength and suspensionCenteringStrength at 0, and tune feel with damperHighSpeedFloor - it is the single knob that decides whether the wheel feels planted or loose at speed.
Belt and gear wheels (T300, G29, G923). These have mechanical friction and less headroom. Raising springEffectStrength and suspensionCenteringStrength above 0 helps the wheel find centre, and lowering textureBudget keeps detail from eating the limited torque available for the main force.
Clipping. If _compressionPct sits high in the debug readout, the soft clip is working constantly and detail is being squashed. Lower overallEffectStrength or the per-vehicle overallCoeff rather than raising wheelbaseTorque above the device's real figure.
Diagnostics
In play mode the inspector shows a Debug Values section:
| Readout | Meaning |
|---|---|
_signedSatNm |
Signed steering-column torque in Nm. Use this to confirm polarity: turning right should give a torque pushing back toward centre. |
_primaryForcePct |
Share of output coming from the self-aligning torque. |
_effectsForcePct |
Share coming from texture and secondary effects. |
_totalForcePct |
Total output as a percentage of full device torque. |
_compressionPct |
How hard the soft clip is compressing. Persistently high means you are asking for more torque than the device has. |
Troubleshooting
Wheel Pulls Into the Turn Instead of Centering
Output polarity is inverted for your device. Enable invertFFBOutput (Debug inspector mode - see the gotcha in Setup).
Device Not Detected
- Ensure Unity-DirectInput package is installed
- Restart Unity after installation
- Check that your device appears in Windows Game Controllers
- Try disconnecting and reconnecting the device
- Check there is only one
DIInputManagerin the scene
Slow Enumeration
DirectInput device enumeration can occasionally take up to 60 seconds due to a Windows bug. USB Audio DACs and certain keyboards (e.g. Corsair) can cause this. Try disconnecting non-essential USB devices.
Force Feedback Not Working
- Verify
overallEffectStrengthis greater than 0 - Check
wheelbaseTorquematches your wheel's actual torque output - Ensure the vehicle has at least 2 front wheels with WheelControllers
- Check DIInputManager shows your device as FFB-capable
Forces Cut Out After a Pause or Script Recompile
Expected. Output fades to zero if the physics step stops publishing for 250 ms, which covers pausing, entering a domain reload, and main-thread stalls. It recovers on its own once the game resumes.
Wheel Feels Numb or Over-Damped
Lower damperHighSpeedFloor first. If the wheel then oscillates around centre, raise it back slightly rather than adding spring or centring force.
Extending the Provider
If you are modifying FFBInputProvider, the output loop runs on its own thread. No UnityEngine API may be called from it - not Time, not AnimationCurve.Evaluate, not any UnityEngine.Object access, not Debug.Log. Mathf is safe because it is pure static maths. Anything else must be sampled during FixedUpdate and passed across in the snapshot struct the thread reads. Breaking this rule hangs the editor rather than producing a warning.
Requirements
- Unity-DirectInput package (https://github.com/imDanoush/Unity-DirectInput.git)
- Windows 10/11 (DirectInput is Windows-only)
- DirectInput-compatible steering wheel with force feedback
- Vehicle must have at least 2 front wheels with WheelControllers