예제 #1
0
        void UpdateTireMarks(WheelData wheelData, TireFxData fxData)
        {
            // If we are already drawing marks to this wheel, wait before updating.

            if (fxData.lastMarksIndex != -1 && wheelData.grounded && fxData.marksDelta < updateInterval)
            {
            fxData.marksDelta += Time.deltaTime;
            return;
            }

            // deltaT = time since last mark for this wheel

            float deltaT = fxData.marksDelta;
            if (deltaT == 0.0f)
            deltaT = Time.deltaTime;
            fxData.marksDelta = 0.0f;

            // Verify: Should we put marks?
            // - Grounded
            // - Contacted object has not a rigidbody (assumed to be static)

            if (!wheelData.grounded || wheelData.hit.collider.attachedRigidbody != null)
            {
            fxData.lastMarksIndex = -1;
            return;
            }

            // Have we changed renderer? If so, start a new tread

            TireMarksRenderer marksRenderer =
            wheelData.groundMaterial != null? wheelData.groundMaterial.marksRenderer : null;

            if (marksRenderer != fxData.lastRenderer)
            {
            fxData.lastRenderer = marksRenderer;
            fxData.lastMarksIndex = -1;
            }

            if (marksRenderer != null)
            {
            float pressureRatio = Mathf.Clamp01(intensity * wheelData.downforceRatio * 0.5f);
            float skidRatio = Mathf.InverseLerp(minSlip, maxSlip, wheelData.combinedTireSlip);

            fxData.lastMarksIndex = marksRenderer.AddMark(
                wheelData.rayHit.point - wheelData.transform.right * wheelData.collider.center.x + wheelData.velocity * deltaT,
                wheelData.rayHit.normal,
                pressureRatio,
                skidRatio,
                tireWidth,
                fxData.lastMarksIndex
                );
            }
        }
예제 #2
0
        void OnEnable()
        {
            // Cache/find components and configure rigidbody

            m_transform = GetComponent<Transform>();
            m_rigidbody = GetComponent<Rigidbody>();
            m_groundMaterialManager = FindObjectOfType<GroundMaterialManager>();
            FindColliders();

            m_rigidbody.maxAngularVelocity = 14.0f;
            m_rigidbody.maxDepenetrationVelocity = 8.0f;

            if (centerOfMass)
            m_rigidbody.centerOfMass = m_transform.InverseTransformPoint(centerOfMass.position);

            if (wheels.Length == 0)
            {
            Debug.LogWarning("The wheels property is empty. You must configure wheels and WheelColliders first. Component is disabled.");
            enabled = false;
            return;
            }

            // Initialize wheel data

            m_usesHandbrake = false;

            m_wheelData = new WheelData[wheels.Length];
            for (int i = 0; i < m_wheelData.Length; i++)
            {
            m_wheelData[i] = new WheelData();
            m_wheelData[i].wheel = wheels[i];

            if (wheels[i].wheelCollider == null)
                Debug.LogError("A WheelCollider is missing in the list of wheels for this vehicle: " + gameObject.name);

            m_wheelData[i].collider = wheels[i].wheelCollider;
            m_wheelData[i].transform = wheels[i].wheelCollider.transform;
            if (wheels[i].handbrake) m_usesHandbrake = true;

            // Calculate the distance of the force app point wrt the center of mass

            UpdateWheelCollider(m_wheelData[i].collider);
            m_wheelData[i].forceDistance = GetWheelForceDistance(m_wheelData[i].collider);
            }

            // Configure WheelColliders

            WheelCollider someWheel = GetComponentInChildren<WheelCollider>();
            someWheel.ConfigureVehicleSubsteps(1000.0f, 1, 1);

            foreach (Wheel wheel in wheels)
            {
            SetupWheelCollider(wheel.wheelCollider);
            UpdateWheelCollider(wheel.wheelCollider);
            }

            // Initialize other data

            m_lastImpactedMaterial = new PhysicMaterial();	// A new reference to ensure cache missmatch at the first query
        }
예제 #3
0
        float FixSteerAngle(WheelData wd, float inputSteerAngle)
        {
            // World-space forward vector for the wheel in the desired steer angle

            Quaternion steerRot = Quaternion.AngleAxis(inputSteerAngle, wd.transform.up);
            Vector3 wheelForward = steerRot * wd.transform.forward;

            // Stupid PhysX Vehicle SDK assumes all wheels point in the same direction as the rigidbody.
            //
            // Step 1:	Project the forward direction into the rigidbody's XZ plane.
            // 			This is the vector we want our wheel to point to as seen from the rigidbody.

            Vector3 rbWheelForward = wheelForward - Vector3.Project(wheelForward, m_transform.up);

            // Step 2:	Calculate the final steer angle to feed PhysX with.

            return Vector3.Angle(m_transform.forward, rbWheelForward) * Mathf.Sign(Vector3.Dot(m_transform.right, rbWheelForward));
        }
예제 #4
0
        void ComputeTireForces(WheelData wd)
        {
            // Throttle for this wheel

            float wheelThrottleInput = wd.wheel.drive? throttleInput : 0.0f;
            float wheelMaxDriveSlip = maxDriveSlip;

            if (Mathf.Sign(wheelThrottleInput) != Mathf.Sign(wd.localVelocity.y))
            wheelMaxDriveSlip -= wd.localVelocity.y * Mathf.Sign(wheelThrottleInput);

            // Calculate the combined brake out of brake and handbrake for this wheel

            float wheelBrakeInput = 0.0f;
            float wheelBrakeRatio = 0.0f;
            float wheelBrakeSlip = 0.0f;

            if (wd.wheel.brake && wd.wheel.handbrake)
            {
            wheelBrakeInput = Mathf.Max(brakeInput, handbrakeInput);

            if (handbrakeInput >= brakeInput)
                ComputeBrakeValues(wd, handbrakeMode, maxHandbrakeSlip, maxHandbrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio);
            else
                ComputeBrakeValues(wd, brakeMode, maxBrakeSlip, maxBrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio);
            }
            else
            if (wd.wheel.brake)
            {
            wheelBrakeInput = brakeInput;
            ComputeBrakeValues(wd, brakeMode, maxBrakeSlip, maxBrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio);
            }
            else
            if (wd.wheel.handbrake)
            {
            wheelBrakeInput = handbrakeInput;
            ComputeBrakeValues(wd, handbrakeMode, maxHandbrakeSlip, maxHandbrakeRatio, out wheelBrakeSlip, out wheelBrakeRatio);
            }

            // Combine throttle and brake inputs. There can be only one.
            // (Not really - EVP uses this simplication. VPP combines throttle and brake in the
            // physically correct way)

            float absThrottleInput = Mathf.Abs(wheelThrottleInput);

            if (absThrottleInput >= wheelBrakeInput)
            {
            wd.finalInput = (absThrottleInput - wheelBrakeInput) * Mathf.Sign(wheelThrottleInput);
            wd.isBraking = false;
            }
            else
            {
            wd.finalInput = wheelBrakeInput - absThrottleInput;
            wd.isBraking = true;
            }

            // Calculate demanded force coming from the wheel's axle

            float demandedForce;

            if (wd.isBraking)
            demandedForce = wd.finalInput * maxBrakeForce;
            else
            demandedForce = ComputeDriveForce(wd.finalInput * maxDriveForce, maxDriveForce, wd.grounded);

            // ABS and TC limits

            if (wd.grounded)
            {
            if (tcEnabled)
                wheelMaxDriveSlip = Mathf.Lerp(wheelMaxDriveSlip, 0.1f, tcRatio);

            if (absEnabled && brakeInput > handbrakeInput)
                {
                wheelBrakeSlip = Mathf.Lerp(wheelBrakeSlip, 0.1f, absRatio);
                wheelBrakeRatio = Mathf.Lerp(wheelBrakeRatio, wheelBrakeRatio * 0.1f, absRatio);
                }
            }

            // Calculate tire forces

            if (wd.grounded)
            {
            wd.tireSlip.x = wd.localVelocity.x;
            wd.tireSlip.y = wd.localVelocity.y - wd.angularVelocity * wd.collider.radius;

            // Get the ground properties

            float groundGrip;
            float groundDrag;

            if (wd.groundMaterial != null)
                {
                groundGrip = wd.groundMaterial.grip;
                groundDrag = wd.groundMaterial.drag;
                }
            else
                {
                groundGrip = defaultGroundGrip;
                groundDrag = defaultGroundDrag;
                }

            // Ensure there's longitudinal slip enough for the demanded longitudinal force

            float forceMagnitude = tireFriction * wd.downforce * groundGrip;
            float minSlipY;

            if (wd.isBraking)
                {
                float wheelMaxBrakeSlip = Mathf.Max(Mathf.Abs(wd.localVelocity.y * wheelBrakeRatio),  wheelBrakeSlip);
                minSlipY = Mathf.Clamp(Mathf.Abs(demandedForce * wd.tireSlip.x) / forceMagnitude, 0.0f, wheelMaxBrakeSlip);
                }
            else
                {
                minSlipY = Mathf.Min(Mathf.Abs(demandedForce * wd.tireSlip.x) / forceMagnitude, wheelMaxDriveSlip);
                if (demandedForce != 0.0f && minSlipY < 0.1f) minSlipY = 0.1f;
                }

            if (Mathf.Abs(wd.tireSlip.y) < minSlipY) wd.tireSlip.y = minSlipY * Mathf.Sign(wd.tireSlip.y);

            // Compute combined tire forces

            Vector2 rawTireForce = -forceMagnitude * wd.tireSlip.normalized;
            rawTireForce.x = Mathf.Abs(rawTireForce.x);
            rawTireForce.y = Mathf.Abs(rawTireForce.y);

            // Sideways force

            wd.tireForce.x = Mathf.Clamp(wd.localRigForce.x, -rawTireForce.x, +rawTireForce.x);

            // Forward force

            if (wd.isBraking)
                {
                float maxFy = Mathf.Min(rawTireForce.y, demandedForce);
                wd.tireForce.y = Mathf.Clamp(wd.localRigForce.y, -maxFy, +maxFy);
                }
            else
                {
                wd.tireForce.y = Mathf.Clamp(demandedForce, -rawTireForce.y, +rawTireForce.y);
                }

            // Drag force as for the surface resistance

            wd.dragForce = -(forceMagnitude * wd.localVelocity.magnitude * groundDrag * 0.001f) * wd.localVelocity;
            }
            else
            {
            wd.tireSlip = Vector2.zero;
            wd.tireForce = Vector2.zero;
            wd.dragForce = Vector2.zero;
            }

            // Compute angular velocity for the next step

            float slipToForce = wd.isBraking? brakeForceToMaxSlip : driveForceToMaxSlip;
            float slipRatio = Mathf.Clamp01((Mathf.Abs(demandedForce) - Mathf.Abs(wd.tireForce.y)) / slipToForce);

            float slip;

            if (wd.isBraking)
            slip = Mathf.Clamp(-slipRatio * wd.localVelocity.y * wheelBrakeRatio, -wheelBrakeSlip, wheelBrakeSlip);
            else
            slip = slipRatio * wheelMaxDriveSlip * Mathf.Sign(demandedForce);

            wd.angularVelocity = (wd.localVelocity.y + slip) / wd.collider.radius;
        }
예제 #5
0
 void ComputeExtendedTireData(WheelData wd, float referenceDownforce)
 {
     wd.combinedTireSlip = ComputeCombinedSlip(wd.localVelocity, wd.tireSlip);
     wd.downforceRatio = wd.hit.force / referenceDownforce;
 }
예제 #6
0
 // Calculate brake ratio and slip based on the current brake method
 void ComputeBrakeValues(WheelData wd, BrakeMode mode, float maxSlip, float maxRatio, out float brakeSlip, out float brakeRatio)
 {
     if (mode == BrakeMode.Slip)
     {
     brakeSlip = maxSlip;
     brakeRatio = 1.0f;
     }
     else
     {
     brakeSlip = Mathf.Abs(wd.localVelocity.y);
     brakeRatio = maxRatio;
     }
 }
예제 #7
0
        void ApplyTireForces(WheelData wd)
        {
            if (wd.grounded)
            {
            if (!disallowRuntimeChanges)
                wd.forceDistance = GetWheelForceDistance(wd.collider);

            Vector3 sidewaysForcePoint = wd.hit.point + wd.transform.up * antiRoll * wd.forceDistance;
            Vector3 forwardForce = wd.hit.forwardDir * (wd.tireForce.y + wd.dragForce.y);
            Vector3 sidewaysForce = wd.hit.sidewaysDir * (wd.tireForce.x + wd.dragForce.x);

            if (wd.wheel.steer)
                {
                if (wd.steerAngle != 0.0f && Mathf.Sign(wd.steerAngle) != Mathf.Sign(wd.tireSlip.x))
                    {
                    sidewaysForcePoint += wd.hit.forwardDir * steeringOverdrive;
                    }
                }

            m_rigidbody.AddForceAtPosition(forwardForce, wd.hit.point);
            m_rigidbody.AddForceAtPosition(sidewaysForce, sidewaysForcePoint);

            Rigidbody otherRb = wd.hit.collider.attachedRigidbody;
            if (otherRb != null && !otherRb.isKinematic)
                {
                otherRb.AddForceAtPosition(-forwardForce, wd.hit.point);
                otherRb.AddForceAtPosition(-sidewaysForce, sidewaysForcePoint);
                }
            }
        }
예제 #8
0
 void UpdateWheelSleep(WheelData wd)
 {
     if (wd.localVelocity.magnitude < sleepVelocity
     && Time.time-m_lastStrongImpactTime > 0.2f
     && (wd.isBraking && wd.finalInput > 0.01f || m_usesHandbrake && handbrakeInput > 0.1f)
     )
     {
     wd.collider.motorTorque = 0.0f;
     }
     else
     {
     wd.collider.motorTorque = 0.00001f;
     }
 }
예제 #9
0
        // Set the visual transform for the wheel
        void UpdateTransform(WheelData wd)
        {
            if (wd.wheel.wheelTransform != null)
            {
            wd.angularPosition = (wd.angularPosition + wd.angularVelocity * Time.deltaTime) % (Mathf.PI*2.0f);

            // Wheel position

            float elongation;

            if (wheelPositionMode == PositionMode.Fast)
                {
                elongation = wd.collider.suspensionDistance * (1.0f - wd.suspensionCompression) + wd.collider.radius * 0.05f;
                wd.rayHit.point = wd.hit.point;
                wd.rayHit.normal = wd.hit.normal;
                }
            else
                {
                if (Physics.Raycast(wd.origin, -wd.transform.up, out wd.rayHit, (wd.collider.suspensionDistance + wd.collider.radius)))
                    elongation = wd.rayHit.distance - wd.collider.radius * 0.95f;
                else
                    elongation = wd.collider.suspensionDistance + wd.collider.radius * 0.05f;
                }

            Vector3 wheelPosition = wd.transform.position - wd.transform.up * elongation;
            wd.wheel.wheelTransform.position = wheelPosition;

            // Wheel rotation

            wd.wheel.wheelTransform.rotation = wd.transform.rotation * Quaternion.Euler(wd.angularPosition * Mathf.Rad2Deg, wd.steerAngle, 0.0f);
            }
            else
            {
            wd.rayHit.point = wd.hit.point;
            wd.rayHit.normal = wd.hit.normal;
            }
        }
예제 #10
0
        void UpdateSuspension(WheelData wd)
        {
            // Retrieve the wheel's contact point

            wd.grounded = wd.collider.GetGroundHit(out wd.hit);
            wd.origin = wd.transform.TransformPoint(wd.collider.center);

            if (wd.grounded && !disableWheelHitCorrection)
            {
            RaycastHit rayHit;

            if (Physics.Raycast(wd.origin, -wd.transform.up, out rayHit, wd.collider.suspensionDistance + wd.collider.radius))
                {
                wd.hit.point = rayHit.point;
                wd.hit.normal = rayHit.normal;
                }
            }

            // Suspension compression and downforce

            if (wd.grounded)
            {
            wd.suspensionCompression = 1.0f - (-wd.transform.InverseTransformPoint(wd.hit.point).y - wd.collider.radius) / wd.collider.suspensionDistance;
            if (wd.hit.force < 0.0f) wd.hit.force = 0.0f;
            wd.downforce = wd.hit.force;
            }
            else
            {
            wd.suspensionCompression = 0.0f;
            wd.downforce = 0.0f;
            }
        }
예제 #11
0
        //----------------------------------------------------------------------------------------------
        void UpdateSteering(WheelData wd)
        {
            if (wd.wheel.steer)
            {
            if (espEnabled && m_speed > 0.0f)
                {
                float forwardSpeed = m_speed * espRatio;
                float maxEspAngle = Mathf.Asin(Mathf.Clamp01(3.0f / forwardSpeed)) * Mathf.Rad2Deg;
                maxEspAngle = Mathf.Max(maxEspAngle, m_speedAngle);

                wd.steerAngle = Mathf.Clamp(maxSteerAngle * steerInput, -maxEspAngle, +maxEspAngle);
                }
            else
                {
                wd.steerAngle = maxSteerAngle * steerInput;
                }
            }
            else
            {
            wd.steerAngle = 0.0f;
            }

            wd.collider.steerAngle = disableSteerAngleCorrection? wd.steerAngle : FixSteerAngle(wd, wd.steerAngle);
        }
예제 #12
0
        void UpdateLocalFrame(WheelData wd)
        {
            // Speed of the wheel rig

            if (!wd.grounded)
            {
            // Ensure continuity even when the wheel is lifted

            wd.hit.point = wd.origin - wd.transform.up * (wd.collider.suspensionDistance + wd.collider.radius);
            wd.hit.normal = wd.transform.up;
            wd.hit.collider = null;
            }

            Vector3 wheelV = m_rigidbody.GetPointVelocity(wd.hit.point);

            if (wd.hit.collider != null)
            {
            Rigidbody rb = wd.hit.collider.attachedRigidbody;
            if (rb != null) wheelV -= rb.GetPointVelocity(wd.hit.point);
            }

            wd.velocity = wheelV - Vector3.Project(wheelV, wd.hit.normal);
            wd.localVelocity.y = Vector3.Dot(wd.hit.forwardDir, wd.velocity);
            wd.localVelocity.x = Vector3.Dot(wd.hit.sidewaysDir, wd.velocity);

            // Forces related to the wheel rig

            if (!wd.grounded)
            {
            wd.localRigForce = Vector2.zero;
            return;
            }

            Vector2 localSurfaceForce;

            float surfaceForceRatio = Mathf.InverseLerp(1.0f, 0.25f, wd.velocity.sqrMagnitude);
            if (surfaceForceRatio > 0.0f)
            {
            Vector3 surfaceForce;

            float upNormal = Vector3.Dot(Vector3.up, wd.hit.normal);
            if (upNormal > 0.000001f)
                {
                Vector3 downForceUp = Vector3.up * wd.hit.force / upNormal;
                surfaceForce = downForceUp - Vector3.Project(downForceUp, wd.hit.normal);
                }
            else
                {
                surfaceForce = Vector3.up * 100000.0f;
                }

            localSurfaceForce.y = Vector3.Dot(wd.hit.forwardDir, surfaceForce);
            localSurfaceForce.x = Vector3.Dot(wd.hit.sidewaysDir, surfaceForce);
            localSurfaceForce *= surfaceForceRatio;
            }
            else
            {
            localSurfaceForce = Vector2.zero;
            }

            float estimatedSprungMass = Mathf.Clamp(wd.hit.force / -Physics.gravity.y, 0.0f, wd.collider.sprungMass) * 0.5f;
            Vector2 localVelocityForce = -estimatedSprungMass * wd.localVelocity / Time.deltaTime;

            wd.localRigForce = localVelocityForce + localSurfaceForce;
        }
예제 #13
0
 void UpdateGroundMaterial(WheelData wd)
 {
     if (wd.grounded)
     UpdateGroundMaterialCached(wd.hit.collider.sharedMaterial, ref wd.lastPhysicMaterial, ref wd.groundMaterial);
 }
예제 #14
0
        void UpdateTireParticles(WheelData wheelData, TireFxData fxData)
        {
            if (!wheelData.grounded)
            {
            // Not grounded: clear particle state and decrement the particle slip time

            fxData.lastParticleTime = -1.0f;

            fxData.slipTime -= Time.deltaTime;
            if (fxData.slipTime < 0.0f) fxData.slipTime = 0.0f;
            return;
            }

            TireParticleEmitter particleEmitter =
            wheelData.groundMaterial != null? wheelData.groundMaterial.particleEmitter : null;

            if (particleEmitter != fxData.lastEmitter)
            {
            fxData.lastEmitter = particleEmitter;
            fxData.lastParticleTime = -1.0f;
            }

            if (particleEmitter != null)
            {
            Vector3 position = wheelData.rayHit.point + wheelData.transform.up * tireWidth * 0.5f;
            Vector3 positionRandom = Random.insideUnitSphere * tireWidth;

            float pressureRatio = Mathf.Clamp01(wheelData.downforceRatio);
            float skidRatio = Mathf.InverseLerp(minSlip, maxSlip, wheelData.combinedTireSlip);

            // Emulate tire "heating" as for the time it has been skidding over the minSlip value.
            // Tire will "heat" at full rate when completely skidding at full pressure.

            if (skidRatio > 0.0f && particleEmitter.mode == TireParticleEmitter.Mode.PressureAndSkid)
                fxData.slipTime += Time.deltaTime * skidRatio * pressureRatio;
            else
                fxData.slipTime -= Time.deltaTime;
            fxData.slipTime = Mathf.Clamp(fxData.slipTime, minIntensityTime, limitIntensityTime);

            float slipTimeRatio = Mathf.InverseLerp(minIntensityTime, maxIntensityTime, fxData.slipTime);

            fxData.lastParticleTime = particleEmitter.EmitParticle(
                position + positionRandom,
                wheelData.velocity,
                wheelData.tireSlip.y * wheelData.transform.forward,
                pressureRatio,
                skidRatio * slipTimeRatio,
                fxData.lastParticleTime
                );
            }
            else
            {
            // No particles set up for this material. Assume is not a "heating material"
            // and "cold down" the tire surface.

            fxData.slipTime -= Time.deltaTime;
            if (fxData.slipTime < 0.0f) fxData.slipTime = 0.0f;
            }
        }
예제 #15
0
        void DrawWheelGizmos(WheelData wd)
        {
            RaycastHit rayHit;
            if (wd.grounded && Physics.Raycast(wd.transform.TransformPoint(wd.collider.center), -wd.transform.up, out rayHit, (wd.collider.suspensionDistance + wd.collider.radius)))
            {
            Debug.DrawLine(rayHit.point, rayHit.point + wd.transform.up * (wd.downforce / 10000.0f), wd.suspensionCompression > 0.99f? Color.magenta : Color.white);

            CommonTools.DrawCrossMark(wd.transform.position, wd.transform, Color.Lerp(Color.green, Color.gray, 0.5f));

            Vector3 forcePoint = rayHit.point + wd.transform.up * target.antiRoll * wd.forceDistance;

            if (wd.wheel.steer)
                {
                if (wd.steerAngle != 0.0f && Mathf.Sign(wd.steerAngle) != Mathf.Sign(wd.tireSlip.x))
                    forcePoint += wd.hit.forwardDir * target.steeringOverdrive;
                }

            CommonTools.DrawCrossMark(forcePoint, wd.transform, Color.Lerp(Color.yellow, Color.gray, 0.5f));

            Vector3 tireForce = wd.hit.forwardDir * wd.tireForce.y + wd.hit.sidewaysDir * wd.tireForce.x;
            Debug.DrawLine(forcePoint, forcePoint + CommonTools.Lin2Log(tireForce) * 0.1f, Color.green);

            Vector3 tireSlip = wd.hit.forwardDir * wd.tireSlip.y + wd.hit.sidewaysDir * wd.tireSlip.x;
            Debug.DrawLine(rayHit.point, rayHit.point + CommonTools.Lin2Log(tireSlip) * 0.5f, Color.cyan);

            // Vector3 wheelVelocity = wd.hit.forwardDir * wd.localVelocity.y + wd.hit.sidewaysDir * wd.localVelocity.x;
            // Debug.DrawLine(rayHit.point, rayHit.point + CommonTools.Lin2Log(wheelVelocity) * 0.5f, Color.Lerp(Color.blue, Color.white, 0.5f));

            // Vector3 rigForce = wd.hit.sidewaysDir * wd.localRigForce.x + wd.hit.forwardDir * wd.localRigForce.y;
            // Debug.DrawLine(rayHit.point, rayHit.point + rigForce / 10000.0f, Color.Lerp(Color.red, Color.gray, 0.5f));
            }
        }
예제 #16
0
        string GetWheelTelemetry(WheelData wd, ref float suspensionForce)
        {
            bool sleeping = !(wd.collider.motorTorque > 0.0f);

            string text = string.Format("{0,-10}{1}{2,5:0.} rpm  ", wd.collider.gameObject.name, sleeping? "×" : ":", wd.angularVelocity * VehicleController.WToRpm);

            if (wd.grounded)
            {
            text += string.Format("C:{0,5:0.00}  ", wd.suspensionCompression);
            // text += string.Format("Vx:{0,6:0.00} Vy:{1,6:0.00}  ", wd.localVelocity.x, wd.localVelocity.y);

            switch (dataMode)
                {
                case DataMode.TireSlipAndForce:
                    text += string.Format("F:{0,5:0.}  ", wd.downforce);
                    text += string.Format("Sx:{0,6:0.00} Sy:{1,6:0.00} ", wd.tireSlip.x, wd.tireSlip.y);
                    text += string.Format("Fx:{0,5:0.} Fy:{1,5:0.}  ", wd.tireForce.x, wd.tireForce.y);
                    break;

                case DataMode.GroundMaterial:
                    // text += string.Format("Sa:{0,5:0.0}  ", wd.GetSlipAngle() * Mathf.Rad2Deg);
                    // text += string.Format("Slip:{0,4:0.0}  ", wd.GetCombinedSlip());
                    text += string.Format("F:{0,4:0.0} %  ", wd.downforceRatio);
                    text += string.Format("Slip:{0,4:0.0}  ", wd.combinedTireSlip);

                    if (wd.groundMaterial != null)
                        {
                        text += string.Format("Grip:{0,4:0.0} Drag:{1,4:0.0}  [{2}]",
                            wd.groundMaterial.grip, wd.groundMaterial.drag,
                            wd.groundMaterial.physicMaterial != null? wd.groundMaterial.physicMaterial.name : "no mat");
                        }
                    break;
                }

            suspensionForce += wd.hit.force;
            }
            else
            {
            text += string.Format("C: 0.--  ");
            }

            return text + "\n";
        }