WheelControllerManager
WheelControllerManager is a singleton that runs physics substepping for all WheelController components. It lets wheel physics run at higher frequencies than Unity's native FixedUpdate.
Overview
Purpose
Unity's physics typically runs at 50Hz (0.02s Fixed Timestep). For accurate wheel physics simulation, higher frequencies are beneficial:
- Better friction calculations at high speeds
- Smoother suspension response over rough terrain
- More accurate tire grip during rapid weight transfers
- Stable vehicle dynamics during aggressive driving
WheelControllerManager solves this by running multiple physics substeps within each FixedUpdate.
Key Classes
| Class | Purpose |
|---|---|
WheelControllerManager |
Singleton manager that runs the substep loop |
WheelControllerGroup |
Groups wheels per Rigidbody, handles force accumulation |
WheelControllerState |
Caches Rigidbody state for substep integration |
Architecture
WheelControllerManager (singleton, ExecutionOrder 110)
├── Dictionary<int, WheelControllerGroup> - Groups indexed by Rigidbody ID
│
└── FixedUpdate()
│
├── READ: group.BeginFrame()
│ └── state.ReadFromRigidbody()
│
├── SUBSTEP LOOP (N iterations)
│ ├── group.BeginSubstep()
│ │ └── Reset force accumulators
│ │
│ ├── ISubstepCallback.OnBeforeSubstep(dt)
│ │ └── Powertrain integration (if registered)
│ │
│ ├── wheel.SubStep(dt, state, group)
│ │ ├── Ground detection
│ │ ├── Calculate suspension forces
│ │ ├── Calculate friction forces
│ │ └── Accumulate to group
│ │
│ ├── group.CoordinateStaticFrictionBreak()
│ │
│ └── group.EndSubstep()
│ └── Apply forces to state, integrate velocity
│
└── WRITE: group.EndFrame()
└── rb.linearVelocity = state.velocity
Configuration
Target Effective Rate
The manager automatically calculates substep count based on Unity's fixedDeltaTime:
// Example: Target 200Hz with 50Hz physics
float fixedDeltaTime = 0.02f; // 50Hz
int targetRate = 200; // Hz
int substepCount = Mathf.RoundToInt(targetRate * fixedDeltaTime);
// Result: 4 substeps per FixedUpdate
Default Settings
| Setting | Default | Description |
|---|---|---|
| Target Effective Rate | 200 Hz | Target physics rate |
| Substep Count | 4 | At 50Hz base physics |
| Min Substeps | 1 | Minimum substep count |
| Max Substeps | 8 | Maximum substep count |
Performance Impact
| Substeps | Effective Rate | Relative Cost |
|---|---|---|
| 1 | 50 Hz | 1.0x (baseline) |
| 2 | 100 Hz | ~1.8x |
| 4 | 200 Hz | ~3.5x |
| 8 | 400 Hz | ~7.0x |
Per-Vehicle Configuration
Each vehicle can override the global substep count through WheelControllerGroup:
// Get the wheel group for a vehicle
WheelControllerGroup group = WheelControllerManager.Instance.GetGroup(vehicleRigidbody);
// Override substep count for this vehicle
group.targetEffectiveRateOverride = 400; // Higher rate for player vehicle
// Or use lower rate for AI vehicles
aiGroup.targetEffectiveRateOverride = 100;
Use Cases
- Player vehicles: Higher substep rate for best handling feel
- AI vehicles: Lower rate to save performance
- Distant vehicles: Minimal rate or sleep entirely
- Physics-heavy scenarios: Temporarily increase rate
Powertrain Integration
VehicleController registers with WheelControllerGroup to run powertrain calculations at each substep.
ISubstepCallback Interface
public interface ISubstepCallback
{
void OnBeforeSubstep(float substepDt);
}
Registration
// In VehicleController.Start() after initialization:
_rigidbodyGroup = WheelControllerManager.Instance?.GetGroup(vehicleRigidbody);
if (_rigidbodyGroup != null)
{
_rigidbodyGroup.substepCallback = this;
}
Callback Implementation
// In VehicleController:
public void OnBeforeSubstep(float substepDt)
{
// Powertrain calculates torques at substep frequency
powertrain.engine.IntegrateDownwards(substepDt);
}
Benefits
- Accurate torque calculation: Powertrain responds to wheel changes at each substep
- No double updates: Single code path for wheel physics
- Proper counter-torque: Available immediately in same substep
- Clean separation: Manager orchestrates, VehicleController provides callbacks
Static Properties
Access global physics state:
// Global physics tick counter (increments each FixedUpdate)
int frameCount = WheelControllerManager.PhysicsFrameCount;
// Current substep index (0 to substepCount-1)
int substep = WheelControllerManager.CurrentSubstep;
// Delta time for current substep
float substepDt = WheelControllerManager.SubstepDeltaTime;
API Reference
Singleton Access
// Get manager instance (auto-creates if needed)
WheelControllerManager manager = WheelControllerManager.Instance;
Wheel Registration
Wheels register automatically on enable:
// Called automatically by WheelController.OnEnable()
WheelControllerManager.Instance.Register(wheelController);
// Called automatically by WheelController.OnDisable()
WheelControllerManager.Instance.Deregister(wheelController);
Group Access
// Get the wheel group for a Rigidbody
WheelControllerGroup group = WheelControllerManager.Instance.GetGroup(rigidbody);
// Check if group exists
if (group != null)
{
// Access group properties
float effectiveRate = group.effectiveRate;
int wheelCount = group.wheelCount;
}
Static Friction Control
// Break static friction on all wheels of a vehicle
WheelControllerManager.Instance.BreakAllStaticFriction(rigidbody);
Events
WheelControllerGroup Events
// Fired when any wheel in the group breaks static friction
WheelControllerGroup group = WheelControllerManager.Instance.GetGroup(rb);
group.OnStaticFrictionBroke += OnStaticFrictionBroke;
private void OnStaticFrictionBroke()
{
// Handle static friction break (e.g., propagate to trailer)
}
Ground Detection in Substeps
Why Sweeps Run Every Substep
Ground detection must run at each substep, not just once per frame:
- Ground is static: Cannot extrapolate hit points with vehicle movement
- Position changes: Wheel position changes during integration
- Accuracy required: Suspension calculation needs real intersection data
Substep Optimization
For performance, multicast ground detection uses adaptive resolution:
Substep 0: Full multicast (3-7 sphere casts) - accurate detection
Substeps 1-N: Single biased cast at cached angle - uses substep 0 result
Performance impact:
- Before optimization: 4 wheels x 4 substeps x 5 casts = 80 casts/frame
- After optimization: (4 x 5) + (4 x 1 x 3) = 32 casts/frame (~60% reduction)
Integration with VehicleController
Execution Order
| Order | Component | Action |
|---|---|---|
| 90 | VehicleController.FixedUpdate | Input, steering, brakes, wheel groups |
| 100 | WheelController.OnEnable | Register with manager |
| 110 | WheelControllerManager.FixedUpdate | Substep loop with callbacks |
Data Flow
VehicleController.FixedUpdate (Order 90)
├── Process input
├── Calculate steering angles
├── Calculate brake torques
└── Update wheel groups (NOT powertrain)
WheelControllerManager.FixedUpdate (Order 110)
├── For each substep:
│ ├── Call VehicleController.OnBeforeSubstep()
│ │ └── Powertrain calculates torques
│ └── WheelController.SubStep()
│ └── Uses torques from powertrain
└── Apply final forces to Rigidbody
Standalone Usage
WheelController can be used without VehicleController (standalone mode):
// Wheels still register with WheelControllerManager
// Substepping works normally
// No powertrain callback (wheels use external motor/brake torque)
// Apply torque directly
wheelController.motorTorque = 500f;
wheelController.brakeTorque = 1000f;
The CarController demo script shows standalone usage:
// Simple car controller without full VehiclePhysics
foreach (var wheel in wheels)
{
if (wheel.powered)
wheel.wheelController.motorTorque = throttle * maxTorque;
if (wheel.handbrake)
wheel.wheelController.brakeTorque = handbrake * maxBrake;
}
Performance Considerations
Substep Count Guidelines
| Platform | Recommended | Notes |
|---|---|---|
| Desktop (Player) | 4-8 substeps | Best handling feel |
| Desktop (AI) | 2-4 substeps | Good balance |
| Mobile (Player) | 2-4 substeps | Performance priority |
| Mobile (AI) | 1-2 substeps | Minimal overhead |
Optimization Strategies
- LOD-based substeps: Reduce substep count for distant vehicles
- Sleep inactive vehicles: Disable WheelController when not needed
- Ground detection mode: Use simpler cast for flat terrain
- Pool vehicles: Reuse wheel groups when spawning/despawning
Profiling
Key markers to watch in Unity Profiler:
WheelControllerManager.FixedUpdateWheelController.SubStepStandardGroundDetection.WheelCastStandardFriction.UpdateFriction
Common Issues
Wheels Not Substepping
Symptom: Wheels update once per frame instead of multiple substeps
Causes:
- WheelController not enabled
- No Rigidbody on parent
- WheelControllerManager destroyed
Solution: Verify wheel is enabled and has valid Rigidbody reference
Bouncy Suspension
Symptom: Vehicle bounces excessively on terrain
Causes:
- Matrix caching not updated per substep
- Incorrect suspension settings
Solution: Ensure ground detection updates cached matrix each substep
Performance Issues
Symptom: High CPU usage in wheel physics
Solutions:
- Reduce substep count
- Use simpler ground detection for AI
- Enable LOD-based substep reduction
- Profile to identify bottlenecks
Technical Notes
Singleton Lifecycle
- Auto-creates on first access
- Uses
DontDestroyOnLoadto persist across scenes - Returns null during application quit to prevent teardown issues
Thread Safety
- All wheel physics runs on main thread
- Manager coordinates wheel groups synchronously
- No multi-threading in substep loop
Compatibility
- Works with Unity 6000+
- Compatible with all render pipelines
- Supports both Input System and Input Manager
Related Documentation
- WheelController - Individual wheel physics
- FrictionPreset - Tire friction configuration
- NWH Vehicle Physics 2 Documentation - Architecture and Powertrain integration