H_SafeExperienceDrivingSystem/U3D_DrivingSystem/Assets/EVP5/Scripts/VehicleController.cs

1967 lines
58 KiB
C#

//------------------------------------------------------------------------------------------------
// Edy's Vehicle Physics
// (c) Angel Garcia "Edy" - Oviedo, Spain
// http://www.edy.es
//------------------------------------------------------------------------------------------------
#if !UNITY_5_0 && !UNITY_5_1
#define UNITY_52_OR_GREATER
#endif
using UnityEngine;
using UnityEngine.Serialization;
using System;
using System.Collections.Generic;
namespace EVP
{
[Serializable]
public class Wheel
{
public WheelCollider wheelCollider;
public Transform wheelTransform;
public Transform caliperTransform;
public bool steer = false;
public bool drive = false;
public bool brake = true;
public bool handbrake = false;
}
public class WheelData
{
public Wheel wheel; // Wheel data from the inspector
public WheelCollider collider; // WheelCollider component for this wheel
public Transform transform; // Transform of the WheelCollider component
public Vector3 origin; // Origin point cosidering WheelCollider.center
public float forceDistance;
public float steerAngle = 0.0f;
public bool grounded = false;
public WheelHit hit;
public GroundMaterial groundMaterial = null;
public float suspensionCompression = 0.0f;
public float downforce = 0.0f;
public Vector3 velocity = Vector3.zero;
public Vector2 localVelocity = Vector2.zero;
public Vector2 localRigForce = Vector2.zero;
public bool isBraking = false;
public float finalInput = 0.0f;
public Vector2 tireSlip = Vector2.zero;
public Vector2 tireForce = Vector2.zero;
public Vector2 dragForce = Vector2.zero;
public Vector2 rawTireForce = Vector2.zero;
public float angularVelocity = 0.0f;
public float angularPosition = 0.0f;
public PhysicMaterial lastPhysicMaterial = new PhysicMaterial(); // A new physic material ensures the first iteration to match the changes.
public RaycastHit rayHit; // Result of raycast for visual wheels. Used for precise positioning (i.e. tire marks).
// Utility data
public float positionRatio = 0.0f;
public bool isWheelChildOfCaliper = false;
// Extended tire data.
// Calculated when extendedTireData is set to True by components.
public float combinedTireSlip = 0.0f; // Combined tire slip magnitude, in m/s
public float downforceRatio = 0.0f; // A relative measure of the amout of weight supported by each wheel. Will be 1.0 in a balanced car at rest.
}
[RequireComponent(typeof(Rigidbody))]
public class VehicleController : MonoBehaviour
{
public Wheel[] wheels = new Wheel[0];
public enum CenterOfMassMode { Transform, Parametric };
[Header("Center of Mass")]
public CenterOfMassMode centerOfMassMode = CenterOfMassMode.Parametric;
[Range(0.1f, 0.9f)]
public float centerOfMassPosition = 0.5f;
[Range(-1.0f, 1.0f)]
public float centerOfMassHeightOffset = 0.0f;
[FormerlySerializedAs("centerOfMass")]
public Transform centerOfMassTransform;
[Header("Vehicle Setup")]
[FormerlySerializedAs("maxSpeed")]
public float maxSpeedForward = 27.78f;
public float maxSpeedReverse = 12.0f;
[Range(0,3)]
public float tireFriction = 1.0f;
[Range(0,1)]
public float rollingResistance = 0.05f;
[Range(0,1)]
public float antiRoll = 0.2f;
[Range(0,89)]
public float maxSteerAngle = 35.0f;
[Range(0,4)]
public float aeroDrag = 0.0f;
[Range(0,2)]
public float aeroDownforce = 1.0f;
[Header("Vehicle Balance")]
[Range(0, 1)]
public float driveBalance = 0.5f;
[Range(0, 1)]
public float brakeBalance = 0.5f;
[Range(0.3f, 0.7f)]
public float tireFrictionBalance = 0.5f;
[Range(0,1)]
public float aeroBalance = 0.5f;
[Range(0,1)]
public float handlingBias = 0.5f;
[Header("Motor")]
// [Space(5)]
public float maxDriveForce = 2000.0f;
[Range(0.0001f, 0.9999f)]
public float forceCurveShape = 0.5f;
public float maxDriveSlip = 4.0f;
public float driveForceToMaxSlip = 1000.0f;
[Header("Brakes")]
public float maxBrakeForce = 3000.0f;
public float brakeForceToMaxSlip = 1000.0f;
public enum BrakeMode { Slip, Ratio };
public BrakeMode brakeMode = BrakeMode.Slip;
public float maxBrakeSlip = 2.0f;
[Range(0,1)]
public float maxBrakeRatio = 0.5f;
public BrakeMode handbrakeMode = BrakeMode.Slip;
public float maxHandbrakeSlip = 10.0f;
[Range(0,1)]
public float maxHandbrakeRatio = 1.0f;
[Header("Driving Aids")]
[FormerlySerializedAs("tcEnabled")]
public bool tractionControl = false;
[Range(0,1)]
[FormerlySerializedAs("tcRatio")]
public float tractionControlRatio = 1.0f;
[FormerlySerializedAs("absEnabled")]
public bool brakeAssist = false;
[Range(0,1)]
[FormerlySerializedAs("absRatio")]
public float brakeAssistRatio = 1.0f;
[FormerlySerializedAs("espEnabled")]
public bool steeringLimit = false;
[Range(0,1)]
[FormerlySerializedAs("espRatio")]
public float steeringLimitRatio = 0.5f;
public bool steeringAssist = true;
[Range(0,1)]
public float steeringAssistRatio = 0.5f;
// Vehicle controls
[Range(-1,1)]
public float steerInput = 0.0f;
[Range(-1,1)]
public float throttleInput = 0.0f;
[Range(0,1)]
public float brakeInput = 0.0f;
[Range(0,1)]
public float handbrakeInput = 0.0f;
public enum UpdateRate { OnUpdate, OnFixedUpdate, Disabled };
public enum PositionMode { Accurate, Fast };
[Header("Visual Wheels")]
[FormerlySerializedAs("wheelUpdateRate")]
public UpdateRate wheelUpdateRate = UpdateRate.OnUpdate;
public PositionMode wheelPositionMode = PositionMode.Accurate;
[Header("Wheel Contact")]
[Range(0,0.5f)]
public float sleepVelocity = 0.2f;
// Ground physic properties applied when no proper ground material is available.
public float defaultGroundGrip = 1.0f;
public float defaultGroundDrag = 0.0f;
[Header("Optimization & Debug")]
// Runtime updates can be disabled when the mass, the center of mass and the suspension
// are not expected to change in runtime. They may still change, but the vehicle should
// be disabled / enabled for the new values to have effect.
public bool disallowRuntimeChanges = false;
// In PhysX wheels are considered to point in the same direction as the vehicle's body.
// The steer angle correction allows the wheels to point in any direction as defined
// by their transform hierarchy.
public bool disableSteerAngleCorrection = false;
[FormerlySerializedAs("showContactGizmos")]
public bool showCollisionGizmos = false;
// Public non-exposed properties. To be configured from scripting if necessary.
[NonSerialized] public bool processContacts = false; // This is set to True by the components that use contacts (Audio, Damage)
[NonSerialized] public float impactThreeshold = 0.6f; // 0.0 - 1.0. The DotNormal of the impact is calculated. Less than this value means drag, more means impact.
[NonSerialized] public float impactInterval = 0.2f; // Time interval between processing impacts for visual or sound effects.
[NonSerialized] public float impactIntervalRandom = 0.4f; // Random percentaje for the impact interval, avoiding regularities.
[NonSerialized] public float impactMinSpeed = 2.0f; // Minimum relative velocity at which conctacts may be consideered impacts.
[NonSerialized] public bool computeExtendedTireData = false;// Components using extended tire data (tire marks, smoke, particles, audio) set this to True
// Add-on components subscribe to this delegate to get notified on impacts.
// Use VehicleController.current to access the values of the controller that sent the event.
public delegate void OnImpact();
public OnImpact onImpact;
public static VehicleController current = null;
// Impact properties. Use from an OnImpact event only.
// Private values get masaged properly just before invoking the event.
public Vector3 localImpactPosition { get { return m_sumImpactPosition; } }
public Vector3 localImpactVelocity { get { return m_sumImpactVelocity; } }
public bool isHardImpact { get { return m_sumImpactHardness >= 0; } }
// Body drag properties. Can be queued continuously.
public Vector3 localDragPosition { get { return m_localDragPosition; } }
public Vector3 localDragVelocity { get { return m_localDragVelocity; } }
public bool isHardDrag { get { return m_localDragHardness >= 0; } }
// Utility, also available for add-on components
public static float RpmToW = (2.0f * Mathf.PI) / 60.0f;
public static float WToRpm = 60.0f / (2.0f * Mathf.PI);
public WheelData[] wheelData { get { return m_wheelData; } }
public float speed { get { return m_speed; } }
public float speedAngle { get { return m_speedAngle; } }
public float steerAngle { get { return m_steerAngle; } }
public bool invertVisualWheelSpinDirection { get; set; }
// Cached and internal data, some accesible from scripting
Transform m_transform;
Rigidbody m_rigidbody;
GroundMaterialManager m_groundMaterialManager;
public Transform cachedTransform { get { return m_transform; } }
public Rigidbody cachedRigidbody { get { return m_rigidbody; } }
Rigidbody m_referenceBody = null;
Rigidbody m_referenceCandidate = null;
int m_referenceCandidateCount = 0;
// Debug
[NonSerialized] public string debugText = "";
// Internal data
WheelData[] m_wheelData = new WheelData[0];
float m_speed = 0.0f;
float m_speedAngle = 0.0f;
float m_steerAngle = 0.0f;
bool m_usesHandbrake = false;
CommonTools.BiasLerpContext m_forceBiasCtx = new CommonTools.BiasLerpContext();
VehicleFrame m_vehicleFrame;
void OnValidate ()
{
// Some parameters must be non-negative.
// This method is called from the Editor only.
maxDriveSlip = Mathf.Max(maxDriveSlip, 0.0f);
maxBrakeSlip = Mathf.Max(maxBrakeSlip, 0.0f);
maxHandbrakeSlip = Mathf.Max(maxHandbrakeSlip, 0.0f);
maxDriveForce = Mathf.Max(maxDriveForce, 0.0f);
maxBrakeForce = Mathf.Max(maxBrakeForce, 0.0f);
driveForceToMaxSlip = Mathf.Max(driveForceToMaxSlip, 1.0f);
brakeForceToMaxSlip = Mathf.Max(brakeForceToMaxSlip, 1.0f);
maxSpeedForward = Mathf.Max(maxSpeedForward, 0.0f);
maxSpeedReverse = Mathf.Max(maxSpeedReverse, 0.0f);
aeroDrag = Mathf.Max(aeroDrag, 0.0f);
}
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 (wheels.Length == 0)
{
Debug.LogWarning("The wheels property is empty. You must configure wheels and WheelColliders first. Component is disabled.");
enabled = false;
return;
}
// Compute the reference frame for balancing parameters in runtime
m_vehicleFrame = ComputeVehicleFrame();
ConfigureCenterOfMass();
// Initialize wheel data
m_usesHandbrake = false;
m_wheelData = new WheelData[wheels.Length];
for (int i = 0; i < m_wheelData.Length; i++)
{
Wheel w = wheels[i];
WheelData wd = new WheelData();
if (w.wheelCollider == null)
{
Debug.LogError("A WheelCollider is missing in the list of wheels for this vehicle: " + gameObject.name);
enabled = false;
return;
}
if (w.caliperTransform != null && w.wheelTransform != null && w.caliperTransform.IsChildOf(w.wheelTransform))
{
Debug.LogWarning(this.ToString() + ": caliper (" + w.caliperTransform.name + ") should not be child of wheel (" + w.wheelTransform.name + ").\n"
+ "Visual issues will surely appear. Either make wheel child of caliper, or put both at the same level (siblings).");
}
wd.isWheelChildOfCaliper = w.caliperTransform != null && w.wheelTransform != null && w.wheelTransform.IsChildOf(w.caliperTransform);
wd.collider = w.wheelCollider;
wd.transform = w.wheelCollider.transform;
if (w.handbrake) m_usesHandbrake = true;
// Calculate the force distance for center of mass and anti-roll
UpdateWheelCollider(wd.collider);
wd.forceDistance = GetWheelForceDistance(wd.collider);
// Determine whether this wheel is "front" or "rear"
float zPos = m_transform.InverseTransformPoint(wd.transform.TransformPoint(wd.collider.center)).z;
wd.positionRatio = zPos >= m_vehicleFrame.middlePoint? 1.0f : 0.0f;
// Store the data
wd.wheel = w;
m_wheelData[i] = wd;
}
// Configure WheelColliders
foreach (Wheel wheel in wheels)
{
SetupWheelCollider(wheel.wheelCollider);
UpdateWheelCollider(wheel.wheelCollider);
// Ensure all wheels for any rigidbody are properly set up
wheel.wheelCollider.ConfigureVehicleSubsteps(1000.0f, 1, 1);
}
// Initialize other data
m_lastImpactedMaterial = new PhysicMaterial(); // A new reference to ensure cache missmatch at the first query
}
void Update ()
{
// Single Update step.
// It takes place after the single fixed update step is completed.
//
// When paused, a single time step here should use Time.fixedDeltaTime instead of Time.deltaTime.
if (paused)
{
if ((m_singleFixedStep || !m_singleUpdateStep)) return;
m_singleUpdateStep = false;
}
// Update the visual wheels
if (wheelUpdateRate == UpdateRate.Disabled)
{
ComputeSteerAngle();
foreach (WheelData wd in m_wheelData)
{
UpdateSteering(wd);
}
}
else
if (wheelUpdateRate == UpdateRate.OnUpdate || wheelPositionMode == PositionMode.Accurate)
{
bool needDisableColliders = m_rigidbody.interpolation != RigidbodyInterpolation.None
&& wheelPositionMode == PositionMode.Accurate;
if (needDisableColliders)
DisableCollidersRaycast();
ComputeSteerAngle();
foreach (WheelData wd in m_wheelData)
{
UpdateSteering(wd);
UpdateTransform(wd, paused? Time.fixedDeltaTime : Time.deltaTime);
}
if (needDisableColliders)
EnableCollidersRaycast();
}
// Drag state is smoothly faded to zero. It gets raised/modified from the drag contacts.
if (processContacts)
{
UpdateDragState(Vector3.zero, Vector3.zero, m_localDragHardness);
// debugText = string.Format("Drag Pos: {0} Drag Velocity: {1,5:0.00} Drag Friction: {2,4:0.00}", localDragPosition, localDragVelocity.magnitude, localDragFriction);
}
}
void FixedUpdate ()
{
// If paused, allow to perform a single time step
if (paused)
{
if (!m_singleFixedStep) return;
m_singleFixedStep = false;
}
// Keep center of mass up to date
if (!disallowRuntimeChanges)
ConfigureCenterOfMass();
// Ensure input values within range
throttleInput = Mathf.Clamp (throttleInput, -1.0f, +1.0f);
brakeInput = Mathf.Clamp01(brakeInput);
handbrakeInput = Mathf.Clamp01(handbrakeInput);
// Calculate the velocity of the vehicle
if (m_referenceCandidateCount > m_wheelData.Length/2)
m_referenceBody = m_referenceCandidate;
Vector3 currentVelocity = m_rigidbody.velocity;
if (m_referenceBody != null) currentVelocity -= m_referenceBody.velocity;
m_speed = Vector3.Dot(currentVelocity, m_transform.forward);
m_speedAngle = Vector3.Angle(currentVelocity, m_transform.forward) * Mathf.Sign(Vector3.Dot(currentVelocity, m_transform.right));
// Prepare common data
float referenceDownforce =
computeExtendedTireData? (m_rigidbody.mass * Physics.gravity.magnitude) / m_wheelData.Length : 1.0f;
// Apply wheel physics
bool needUpdateVisuals =
wheelUpdateRate == UpdateRate.OnFixedUpdate && wheelPositionMode == PositionMode.Fast;
int groundedWheels = 0;
m_referenceCandidateCount = 0;
if (needUpdateVisuals)
ComputeSteerAngle();
foreach (WheelData wd in m_wheelData)
{
if (!disallowRuntimeChanges)
UpdateWheelCollider(wd.collider);
if (needUpdateVisuals)
UpdateSteering(wd);
UpdateSuspension(wd);
UpdateLocalFrame(wd);
UpdateGroundMaterial(wd);
ComputeTireForces(wd);
ApplyTireForces(wd);
UpdateWheelSleep(wd);
// Update visual wheel object
if (needUpdateVisuals)
UpdateTransform(wd, Time.deltaTime);
if (wd.grounded) groundedWheels++;
// Calculate extended tire data
if (computeExtendedTireData)
ComputeExtendedTireData(wd, referenceDownforce);
}
// Apply aerodynamic properties
float sqrVelocity = m_rigidbody.velocity.sqrMagnitude;
Vector3 normalizedVelocity = m_rigidbody.velocity.normalized;
float forwardVelocityFactor = Vector3.Dot(m_transform.forward, normalizedVelocity);
Vector3 dragForce = -aeroDrag * sqrVelocity * normalizedVelocity;
Vector3 loadForce = -aeroDownforce * sqrVelocity * forwardVelocityFactor * m_transform.up;
Vector3 aeroAppPoint = m_transform.TransformPoint(new Vector3(0.0f,
m_rigidbody.centerOfMass.y,
Mathf.Lerp(m_vehicleFrame.rearPosition, m_vehicleFrame.frontPosition, aeroBalance)));
m_rigidbody.AddForceAtPosition(dragForce, aeroAppPoint);
if (groundedWheels > 0) m_rigidbody.AddForceAtPosition(loadForce, aeroAppPoint);
// CommonTools.DrawCrossMark (aeroAppPoint, m_transform.forward, m_transform.right, m_transform.up, Color.magenta);
// debugText = string.Format("AeroDrag: {0,6:0.} AeroForce: {1,6:0.}", dragForce.magnitude, loadForce.magnitude);
// Handle impacts
if (processContacts)
HandleImpacts();
}
//----------------------------------------------------------------------------------------------
// Public Pause feature
//
// Equivalent to disabling the component but the internal state is preserved.
// Note that only pauses the internal updates. The Physics rigidbody and friction-less
// WheelColliders keep operating normally.
//
// Pause is currently designed to be used by other components (replay, pause vehicle...)
bool m_paused = false;
bool m_singleFixedStep = false;
bool m_singleUpdateStep = false;
public bool paused
{
get
{
return m_paused;
}
set
{
if (m_paused != value)
{
m_singleFixedStep = false;
m_singleUpdateStep = false;
m_paused = value;
}
}
}
public void SingleStep ()
{
// Both steps (fixed / update) must have been completed before initiating
// a new single step.
if (paused && m_singleFixedStep == false && m_singleUpdateStep == false)
{
m_singleFixedStep = true;
m_singleUpdateStep = true;
}
}
//----------------------------------------------------------------------------------------------
void ComputeSteerAngle ()
{
float inputSteerAngle = maxSteerAngle * steerInput;
// Debug.Log(maxSteerAngle+"---"+steerInput);
float speedFactor = Mathf.InverseLerp(0.1f, 3.0f, m_speed);
if (steeringLimit)
{
float forwardSpeed = m_speed * steeringLimitRatio * speedFactor;
float maxEspAngle = Mathf.Asin(Mathf.Clamp01(3.0f / forwardSpeed)) * Mathf.Rad2Deg;
float steerAngleLimit = Mathf.Min(maxSteerAngle, Mathf.Max(maxEspAngle, Mathf.Abs(m_speedAngle)));
inputSteerAngle = Mathf.Clamp(inputSteerAngle, -steerAngleLimit, +steerAngleLimit);
//Debug.Log(inputSteerAngle);
}
float assistedSteerAngle = 0.0f;
if (steeringAssist)
assistedSteerAngle = m_speedAngle * steeringAssistRatio * speedFactor * Mathf.InverseLerp(2.0f, 3.0f, Mathf.Abs(m_speedAngle));
m_steerAngle = Mathf.Clamp(inputSteerAngle + assistedSteerAngle, -maxSteerAngle, +maxSteerAngle);
// Debug.Log(m_steerAngle);
}
void UpdateSteering (WheelData wd)
{
if (wd.wheel.steer)
{
wd.steerAngle = m_steerAngle;
if (wd.positionRatio < 0.5f) wd.steerAngle = -wd.steerAngle;
}
else
{
wd.steerAngle = 0.0f;
}
wd.collider.steerAngle = disableSteerAngleCorrection? wd.steerAngle : FixSteerAngle(wd, wd.steerAngle);
}
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));
}
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);
wd.hit.point += m_rigidbody.velocity * Time.deltaTime;
// 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;
}
}
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);
}
// Contribute to change the reference body if the touching
// rigidbody is different to the actual reference.
if (rb != m_referenceBody)
{
m_referenceCandidate = rb;
m_referenceCandidateCount++;
}
}
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;
}
void UpdateGroundMaterial (WheelData wd)
{
if (wd.grounded)
UpdateGroundMaterialCached(wd.hit.collider.sharedMaterial, ref wd.lastPhysicMaterial, ref wd.groundMaterial);
}
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. Vehicle Physics Pro (VPP) combines
// throttle and brake in the physically correct way, which is WAY more complex)
float absThrottleInput = Mathf.Abs(wheelThrottleInput);
float combinedInput = -rollingResistance + absThrottleInput * (1.0f + rollingResistance) - wheelBrakeInput * (1.0f - rollingResistance);
if (combinedInput >= 0)
{
wd.finalInput = combinedInput * Mathf.Sign(wheelThrottleInput);
wd.isBraking = false;
}
else
{
wd.finalInput = -combinedInput;
wd.isBraking = true;
}
// Calculate demanded force coming from the wheel's axle
float demandedForce;
if (wd.isBraking)
{
demandedForce = wd.finalInput * GetRampBalancedValue(maxBrakeForce, brakeBalance, wd.positionRatio);
}
else
{
float balancedDriveForce = GetRampBalancedValue(maxDriveForce, driveBalance, wd.positionRatio);
demandedForce = ComputeDriveForce(wd.finalInput * balancedDriveForce, balancedDriveForce, wd.grounded);
}
// ABS and TC limits
if (wd.grounded)
{
if (tractionControl)
wheelMaxDriveSlip = Mathf.Lerp(wheelMaxDriveSlip, 0.1f, tractionControlRatio);
if (brakeAssist && brakeInput > handbrakeInput)
{
wheelBrakeSlip = Mathf.Lerp(wheelBrakeSlip, 0.1f, brakeAssistRatio);
wheelBrakeRatio = Mathf.Lerp(wheelBrakeRatio, wheelBrakeRatio * 0.1f, brakeAssistRatio);
}
}
// 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;
}
// Calculate the total tire force available
float balancedFriction = GetBalancedValue(tireFriction, tireFrictionBalance, wd.positionRatio);
float forceMagnitude = balancedFriction * wd.downforce * groundGrip;
// Ensure there's longitudinal slip enough for the demanded longitudinal force
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
wd.rawTireForce = -forceMagnitude * wd.tireSlip.normalized;
wd.rawTireForce.x = Mathf.Abs(wd.rawTireForce.x);
wd.rawTireForce.y = Mathf.Abs(wd.rawTireForce.y);
// Sideways force
wd.tireForce.x = Mathf.Clamp(wd.localRigForce.x, -wd.rawTireForce.x, +wd.rawTireForce.x);
// Forward force
if (wd.isBraking)
{
float maxFy = Mathf.Min(wd.rawTireForce.y, demandedForce);
wd.tireForce.y = Mathf.Clamp(wd.localRigForce.y, -maxFy, +maxFy);
}
else
{
wd.tireForce.y = Mathf.Clamp(demandedForce, -wd.rawTireForce.y, +wd.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;
}
void ApplyTireForces (WheelData wd)
{
if (wd.grounded)
{
if (!disallowRuntimeChanges)
wd.forceDistance = GetWheelForceDistance(wd.collider);
Vector3 forwardForce = wd.hit.forwardDir * (wd.tireForce.y + wd.dragForce.y);
Vector3 sidewaysForce = wd.hit.sidewaysDir * (wd.tireForce.x + wd.dragForce.x);
Vector3 sidewaysForcePoint = GetSidewaysForceAppPoint(wd, wd.hit.point);
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);
}
}
}
// Methods for calculating tire data
// Application point for the sideways force
public Vector3 GetSidewaysForceAppPoint (WheelData wd, Vector3 contactPoint)
{
Vector3 sidewaysForcePoint = contactPoint + wd.transform.up * antiRoll * wd.forceDistance;
if (wd.wheel.steer && wd.steerAngle != 0.0f && Mathf.Sign(wd.steerAngle) != Mathf.Sign(wd.tireSlip.x))
sidewaysForcePoint += wd.transform.forward * (m_vehicleFrame.frontPosition - m_vehicleFrame.rearPosition) * (handlingBias - 0.5f);
return sidewaysForcePoint;
}
// Slip angle based on velocity
static float ComputeSlipAngle (Vector2 localVelocity)
{
return localVelocity.magnitude > 0.01f
? Mathf.Atan2(localVelocity.x, Mathf.Abs(localVelocity.y))
: 0.0f;
}
// Combined slip value. It's the magnitude of tireSlip, but the x component being weighted with
// the velocity-based slip angle. The implementation is equivalent to:
//
// float combinedSlip = new Vector2(tireSlip.x * Mathf.Sin(GetSlipAngle()), tireSlip.y).magnitude
static float ComputeCombinedSlip (Vector2 localVelocity, Vector2 tireSlip)
{
float h = localVelocity.magnitude;
if (h > 0.01f)
{
float sx = tireSlip.x * localVelocity.x / h;
float sy = tireSlip.y;
return Mathf.Sqrt(sx*sx + sy*sy);
}
else
{
return tireSlip.magnitude;
}
}
void ComputeExtendedTireData (WheelData wd, float referenceDownforce)
{
wd.combinedTireSlip = ComputeCombinedSlip(wd.localVelocity, wd.tireSlip);
wd.downforceRatio = wd.hit.force / referenceDownforce;
}
// Calculate current maximum force according to speed
float ComputeDriveForce (float demandedForce, float maxForce, bool grounded)
{
float absSpeed = Mathf.Abs(m_speed);
float speedLimit = m_speed >= 0.0f? maxSpeedForward : maxSpeedReverse;
if (absSpeed < speedLimit)
{
if (m_speed < 0.0f && demandedForce > 0.0f || m_speed > 0.0f && demandedForce < 0.0f)
{
// Do not clamp the drive force that opposes the speed direction
}
else
{
maxForce *= CommonTools.BiasedLerp(1.0f - absSpeed/speedLimit, forceCurveShape, m_forceBiasCtx);
}
return Mathf.Clamp(demandedForce, -maxForce, +maxForce);
}
else
{
float opposingForce = maxForce * Mathf.Max(1.0f - absSpeed/speedLimit, -1.0f) * Mathf.Sign(m_speed);
if (m_speed < 0.0f && demandedForce > 0.0f || m_speed > 0.0f && demandedForce < 0.0f)
{
// Drive force that opposes the speed direction is added to the actual resistive speed
opposingForce = Mathf.Clamp(opposingForce + demandedForce, -maxForce, +maxForce);
}
return opposingForce;
}
}
// 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;
}
}
// Set the visual transform for the wheel
void UpdateTransform (WheelData wd, float deltaTime)
{
if (wd.wheel.wheelTransform != null || wd.wheel.caliperTransform != null)
{
// Disabled wheels get hidden
if (!wd.collider.enabled || !wd.collider.gameObject.activeInHierarchy)
{
if (wd.wheel.wheelTransform) wd.wheel.wheelTransform.gameObject.SetActive(false);
if (wd.wheel.caliperTransform) wd.wheel.caliperTransform.gameObject.SetActive(false);
return;
}
// Wheel spin
float deltaPos = wd.angularVelocity * deltaTime;
if (invertVisualWheelSpinDirection) deltaPos = -deltaPos;
wd.angularPosition = (wd.angularPosition + deltaPos) % (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 UNITY_52_OR_GREATER
bool collided = Physics.Raycast(wd.origin, -wd.transform.up, out wd.rayHit, (wd.collider.suspensionDistance + wd.collider.radius), Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore);
#else
bool collided = Physics.Raycast(wd.origin, -wd.transform.up, out wd.rayHit, (wd.collider.suspensionDistance + wd.collider.radius), Physics.DefaultRaycastLayers);
#endif
if (collided)
{
// If these layers are intended to ignore collisions then just use the actual WheelHit information
if (Physics.GetIgnoreLayerCollision(wd.collider.gameObject.layer, wd.rayHit.collider.gameObject.layer))
{
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
{
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;
// Caliper transform
if (wd.wheel.caliperTransform != null)
{
wd.wheel.caliperTransform.gameObject.SetActive(true);
wd.wheel.caliperTransform.position = wheelPosition;
// Rotation due to steering (Y)
wd.wheel.caliperTransform.rotation = wd.transform.rotation * Quaternion.Euler(0.0f, wd.steerAngle, 0.0f);
}
if (wd.wheel.wheelTransform != null)
{
wd.wheel.wheelTransform.gameObject.SetActive(true);
if (wd.isWheelChildOfCaliper)
{
// Wheel is child of caliper. Only local wheel spin is required.
wd.wheel.wheelTransform.localRotation = Quaternion.Euler(wd.angularPosition * Mathf.Rad2Deg, 0.0f, 0.0f);
}
else
{
// Wheel is not child of caliper. Apply full position & rotation
wd.wheel.wheelTransform.position = wheelPosition;
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;
}
}
// Updates a ground material reference based on the physics material assigned to a collider
// using a cached reference for the physics material. This way the ground material manager
// is queried only when the physic material changes.
void UpdateGroundMaterialCached (PhysicMaterial colliderMaterial, ref PhysicMaterial cachedMaterial, ref GroundMaterial groundMaterial)
{
if (m_groundMaterialManager != null)
{
// Query the ground material (slow, table look-up) only when the physic material changes.
// Otherwise keep the actual known material.
if (colliderMaterial != cachedMaterial)
{
cachedMaterial = colliderMaterial;
groundMaterial = m_groundMaterialManager.GetGroundMaterial(colliderMaterial);
}
}
// Don't do anything if no GroundMaterialManager is present.
// Ground materials may still be supplied externally via wheelData[].groundMaterial
}
//----------------------------------------------------------------------------------------------
public void ResetVehicle ()
{
Vector3 eulerAngles = transform.localEulerAngles;
m_rigidbody.MoveRotation(Quaternion.Euler(0, eulerAngles.y, 0));
m_rigidbody.MovePosition(m_rigidbody.position + Vector3.up * 1.6f);
m_rigidbody.velocity = Vector3.zero;
m_rigidbody.angularVelocity = Vector3.zero;
}
//----------------------------------------------------------------------------------------------
// Methods for dealing with colliders
Collider[] m_colliders = new Collider[0];
int[] m_colLayers = new int[0];
void FindColliders ()
{
Collider[] originalColliders = GetComponentsInChildren<Collider>(true);
List<Collider> filteredColliders = new List<Collider>();
// Keep non-trigger and non-wheel colliders only
foreach (Collider col in originalColliders)
{
if (!col.isTrigger && !(col is WheelCollider))
filteredColliders.Add(col);
}
m_colliders = filteredColliders.ToArray();
m_colLayers = new int[m_colliders.Length];
}
void DisableCollidersRaycast ()
{
for (int i=0, c=m_colliders.Length; i<c; i++)
{
GameObject go = m_colliders[i].gameObject;
m_colLayers[i] = go.layer;
go.layer = 2;
}
}
void EnableCollidersRaycast ()
{
for (int i=0, c=m_colliders.Length; i<c; i++)
m_colliders[i].gameObject.layer = m_colLayers[i];
}
public Vector3 RaycastOthers (Vector3 from, Vector3 to, int layerMask = Physics.DefaultRaycastLayers)
{
Vector3 path = to - from;
RaycastHit hit;
DisableCollidersRaycast();
#if UNITY_52_OR_GREATER
bool collided = Physics.Raycast(from, path, out hit, path.magnitude, layerMask, QueryTriggerInteraction.Ignore);
#else
bool collided = Physics.Raycast(from, path, out hit, path.magnitude, layerMask);
#endif
EnableCollidersRaycast();
return collided? hit.point : to;
}
public float SphereRaycastOthers (Vector3 origin, Vector3 direction, float radius, float maxDistance, int layerMask = Physics.DefaultRaycastLayers)
{
RaycastHit hit;
DisableCollidersRaycast();
#if UNITY_52_OR_GREATER
bool collided = Physics.SphereCast(origin, radius, direction, out hit, maxDistance, layerMask, QueryTriggerInteraction.Ignore);
#else
bool collided = Physics.SphereCast(origin, radius, direction, out hit, maxDistance, layerMask);
#endif
EnableCollidersRaycast();
return collided? hit.distance : maxDistance;
}
float GetWheelForceDistance (WheelCollider col)
{
return m_rigidbody.centerOfMass.y - m_transform.InverseTransformPoint(col.transform.position).y
+ col.radius + (1.0f - col.suspensionSpring.targetPosition) * col.suspensionDistance;
}
void UpdateWheelCollider (WheelCollider col)
{
if (!col.enabled) return;
JointSpring suspension = col.suspensionSpring;
float sprungForce = -col.sprungMass * Physics.gravity.y;
float pos = sprungForce / suspension.spring;
suspension.targetPosition = Mathf.Clamp01(pos / col.suspensionDistance);
float minSpringRate = sprungForce / col.suspensionDistance;
if (suspension.spring < minSpringRate) suspension.spring = minSpringRate;
col.suspensionSpring = suspension;
}
WheelFrictionCurve m_colliderFriction = new WheelFrictionCurve();
void SetupWheelCollider (WheelCollider col)
{
m_colliderFriction.stiffness = 0.0f;
col.sidewaysFriction = m_colliderFriction;
col.forwardFriction = m_colliderFriction;
col.motorTorque = 0.00001f;
}
void UpdateWheelSleep (WheelData wd)
{
if (wd.localVelocity.magnitude < sleepVelocity
&& Time.time-m_lastStrongImpactTime > 0.2f
&& (wd.isBraking && wd.rawTireForce.y >= Mathf.Abs(wd.localRigForce.y)
&& wd.rawTireForce.x >= Mathf.Abs(wd.localRigForce.x) || m_usesHandbrake && handbrakeInput > 0.1f)
)
{
wd.collider.motorTorque = 0.0f;
}
else
{
wd.collider.motorTorque = 0.00001f;
}
}
//----------------------------------------------------------------------------------------------
// Vehicle balancing
struct VehicleFrame
{
public float frontPosition; // Average forwards position of all "front" wheels
public float rearPosition; // Average forwards position of all "rear" wheels
public float baseHeight; // Average vertical position of all wheels (center.y)
public float frontWidth; // Average semi-axle distance for all "front" wheels
public float rearWidth; // Average semi-axle distance for all "rear" wheels
public float middlePoint; // Forwards position that separates "front" and "rear" wheels
}
VehicleFrame ComputeVehicleFrame ()
{
// Compute the middle position
float middlePoint = 0.0f;
int middleCount = 0;
foreach (Wheel w in wheels)
{
if (w.wheelCollider != null)
{
middlePoint += transform.InverseTransformPoint(w.wheelCollider.transform.TransformPoint(w.wheelCollider.center)).z;
middleCount++;
}
}
if (middleCount > 0) middlePoint /= middleCount;
// Compute the front / rear positions and the base height
float frontPos = 0.0f;
float frontWidth = 0.0f;
int frontCount = 0;
float rearPos = 0.0f;
float rearWidth = 0.0f;
int rearCount = 0;
float baseHeight = 0.0f;
int baseHeightCount = 0;
foreach (Wheel w in wheels)
{
if (w.wheelCollider != null)
{
Vector3 localPos = transform.InverseTransformPoint(w.wheelCollider.transform.TransformPoint(w.wheelCollider.center));
float wheelPos = localPos.z;
float axleWidth = Mathf.Abs(localPos.x);
if (wheelPos >= middlePoint)
{
frontPos += wheelPos;
frontWidth += axleWidth;
frontCount++;
}
else
{
rearPos += wheelPos;
rearWidth += axleWidth;
rearCount++;
}
baseHeight += localPos.y;
baseHeightCount++;
}
}
if (frontCount > 0)
{
frontPos = frontPos / frontCount;
frontWidth = frontWidth / frontCount;
}
if (rearCount > 0)
{
rearPos = rearPos / rearCount;
rearWidth = rearWidth / rearCount;
}
else
{
rearPos = frontPos;
rearWidth = frontWidth;
}
if (baseHeightCount > 0)
baseHeight = baseHeight / baseHeightCount;
// Return the results
VehicleFrame frame = new VehicleFrame();
frame.frontPosition = frontPos;
frame.rearPosition = rearPos;
frame.baseHeight = baseHeight;
frame.frontWidth = frontWidth;
frame.rearWidth = rearWidth;
frame.middlePoint = middlePoint;
return frame;
}
void ConfigureCenterOfMass ()
{
// Checking whether rigidbody.centerOfMass has changed really makes a difference.
// Otherwise all internal calculations are triggered (inertia tensor, sprung masses...)
if (centerOfMassMode == CenterOfMassMode.Parametric)
{
Vector3 CoM = new Vector3(0.0f,
m_vehicleFrame.baseHeight + centerOfMassHeightOffset,
Mathf.Lerp(m_vehicleFrame.rearPosition, m_vehicleFrame.frontPosition, centerOfMassPosition));
if (m_rigidbody.centerOfMass != CoM)
m_rigidbody.centerOfMass = CoM;
}
else
{
if (centerOfMassTransform != null)
{
Vector3 CoM = m_transform.InverseTransformPoint(centerOfMassTransform.position);
if (m_rigidbody.centerOfMass != CoM)
m_rigidbody.centerOfMass = CoM;
}
}
}
// Balanced value (linear cross-faded):
//
// bias rear front
// 0.0 0.0 2.0
// 0.5 1.0 1.0
// 1.0 2.0 0.0
public static float GetBalancedValue (float value, float bias, float positionRatio)
{
float frontRatio = bias;
float rearRatio = 1.0f - bias;
return value * (positionRatio * frontRatio + (1.0f-positionRatio) * rearRatio) * 2.0f;
}
// Ramp balanced value:
//
// bias rear front
// 0.0 0.0 1.0
// 0.5 1.0 1.0
// 1.0 1.0 0.0
public static float GetRampBalancedValue (float value, float bias, float positionRatio)
{
float frontRatio = Mathf.Clamp01(2.0f * bias);
float rearRatio = Mathf.Clamp01(2.0f * (1.0f - bias));
return value * (positionRatio * frontRatio + (1.0f-positionRatio) * rearRatio);
}
//----------------------------------------------------------------------------------------------
// Contact processing
// Private data for internal use
int m_sumImpactCount = 0;
Vector3 m_sumImpactPosition = Vector3.zero;
Vector3 m_sumImpactVelocity = Vector3.zero;
int m_sumImpactHardness = 0;
float m_lastImpactTime = 0.0f;
Vector3 m_localDragPosition = Vector3.zero;
Vector3 m_localDragVelocity = Vector3.zero;
int m_localDragHardness = 0;
float m_lastStrongImpactTime = 0.0f;
PhysicMaterial m_lastImpactedMaterial;
GroundMaterial m_impactedGroundMaterial = null;
void OnCollisionEnter (Collision collision)
{
// Prevent the wheels to sleep for some time if a strong impact occurs
if (collision.relativeVelocity.magnitude > 4.0f)
m_lastStrongImpactTime = Time.time;
if (processContacts)
ProcessContacts(collision, true);
}
void OnCollisionStay (Collision collision)
{
if (processContacts)
ProcessContacts(collision, false);
}
void ProcessContacts (Collision col, bool forceImpact)
{
int impactCount = 0; // All impacts
Vector3 impactPosition = Vector3.zero;
Vector3 impactVelocity = Vector3.zero;
int impactHardness = 0;
int dragCount = 0;
Vector3 dragPosition = Vector3.zero;
Vector3 dragVelocity = Vector3.zero;
int dragHardness = 0;
float sqrImpactSpeed = impactMinSpeed*impactMinSpeed;
// We process all contacts individually and get an impact and/or drag amount out of each one.
foreach (ContactPoint contact in col.contacts)
{
Collider collider = contact.otherCollider;
// Get the type of the impacted material: hard +1, soft -1
int hardness = 0;
UpdateGroundMaterialCached(collider.sharedMaterial, ref m_lastImpactedMaterial, ref m_impactedGroundMaterial);
if (m_impactedGroundMaterial != null)
hardness = m_impactedGroundMaterial.surfaceType == GroundMaterial.SurfaceType.Hard? +1 : -1;
// Calculate the velocity of the body in the contact point with respect to the colliding object
Vector3 v = m_rigidbody.GetPointVelocity(contact.point);
if (collider.attachedRigidbody != null)
v -= collider.attachedRigidbody.GetPointVelocity(contact.point);
float dragRatio = Vector3.Dot(v, contact.normal);
// Determine whether this contact is an impact or a drag
if (dragRatio < -impactThreeshold || forceImpact && col.relativeVelocity.sqrMagnitude > sqrImpactSpeed)
{
// Impact
impactCount++;
impactPosition += contact.point;
impactVelocity += col.relativeVelocity;
impactHardness += hardness;
if (showCollisionGizmos)
Debug.DrawLine(contact.point, contact.point + CommonTools.Lin2Log(v), Color.red);
}
else if (dragRatio < impactThreeshold)
{
// Drag
dragCount++;
dragPosition += contact.point;
dragVelocity += v;
dragHardness += hardness;
if (showCollisionGizmos)
Debug.DrawLine(contact.point, contact.point + CommonTools.Lin2Log(v), Color.cyan);
}
// Debug.DrawLine(contact.point, contact.point + CommonTools.Lin2Log(v), Color.Lerp(Color.cyan, Color.red, Mathf.Abs(dragRatio)));
if (showCollisionGizmos)
Debug.DrawLine(contact.point, contact.point + contact.normal*0.25f, Color.yellow);
}
// Accumulate impact values received.
if (impactCount > 0)
{
float invCount = 1.0f / impactCount;
impactPosition *= invCount;
impactVelocity *= invCount;
m_sumImpactCount++;
m_sumImpactPosition += m_transform.InverseTransformPoint(impactPosition);
m_sumImpactVelocity += m_transform.InverseTransformDirection(impactVelocity);
m_sumImpactHardness += impactHardness;
}
// Update the current drag value
if (dragCount > 0)
{
float invCount = 1.0f / dragCount;
dragPosition *= invCount;
dragVelocity *= invCount;
UpdateDragState(m_transform.InverseTransformPoint(dragPosition), m_transform.InverseTransformDirection(dragVelocity), dragHardness);
}
}
// Impact processing
void HandleImpacts ()
{
// Multiple impacts within an impact interval are accumulated and averaged later.
if (Time.time-m_lastImpactTime >= impactInterval && m_sumImpactCount > 0)
{
// Prepare the impact parameters
float invCount = 1.0f / m_sumImpactCount;
m_sumImpactPosition *= invCount;
m_sumImpactVelocity *= invCount;
// Notify the listeners on the impact
if (onImpact != null)
{
current = this;
onImpact();
current = null;
}
// debugText = string.Format("Count: {4} Impact Pos: {0} Impact Velocity: {1} ({2,5:0.00}) Impact Friction: {3,4:0.00}", localImpactPosition, localImpactVelocity, localImpactVelocity.magnitude, localImpactFriction, m_sumImpactCount);
if (showCollisionGizmos && localImpactVelocity.sqrMagnitude > 0.001f)
Debug.DrawLine(transform.TransformPoint(localImpactPosition), transform.TransformPoint(localImpactPosition) + CommonTools.Lin2Log(transform.TransformDirection(localImpactVelocity)), Color.red, 0.2f, false);
// Reset impact data
m_sumImpactCount = 0;
m_sumImpactPosition = Vector3.zero;
m_sumImpactVelocity = Vector3.zero;
m_sumImpactHardness = 0;
m_lastImpactTime = Time.time + impactInterval * UnityEngine.Random.Range(-impactIntervalRandom, impactIntervalRandom); // Add a random variation for avoiding regularities
}
}
// Drag processing
// The values come from OnCollisionEnter/Stay so the actual drag value is updated accordingly.
//
// This function is invoked from both OnCollision (increase the drag value) and Update
// (smoothly decrease the value to zero).
void UpdateDragState (Vector3 dragPosition, Vector3 dragVelocity, int dragHardness)
{
if (dragVelocity.sqrMagnitude > 0.001f)
{
m_localDragPosition = Vector3.Lerp(m_localDragPosition, dragPosition, 10.0f * Time.deltaTime);
m_localDragVelocity = Vector3.Lerp(m_localDragVelocity, dragVelocity, 20.0f * Time.deltaTime);
m_localDragHardness = dragHardness;
}
else
{
m_localDragVelocity = Vector3.Lerp(m_localDragVelocity, Vector3.zero, 10.0f * Time.deltaTime);
}
if (showCollisionGizmos && localDragVelocity.sqrMagnitude > 0.001f)
Debug.DrawLine(transform.TransformPoint(localDragPosition), transform.TransformPoint(localDragPosition) + CommonTools.Lin2Log(transform.TransformDirection(localDragVelocity)), Color.cyan, 0.05f, false);
}
//----------------------------------------------------------------------------------------------
[ContextMenu("Adjust WheelColliders to their meshes")]
void AdjustWheelColliders ()
{
foreach (Wheel wheel in wheels)
{
if (wheel.wheelCollider != null)
AdjustColliderToWheelMesh(wheel.wheelCollider, wheel.wheelTransform);
}
}
static void AdjustColliderToWheelMesh (WheelCollider wheelCollider, Transform wheelTransform)
{
// Adjust position and rotation
if (wheelTransform == null)
{
Debug.LogError(wheelCollider.gameObject.name + ": A Wheel transform is required");
return;
}
wheelCollider.transform.position = wheelTransform.position + wheelTransform.up * wheelCollider.suspensionDistance * 0.5f;
wheelCollider.transform.rotation = wheelTransform.rotation;
// Adjust radius
MeshFilter[] meshFilters = wheelTransform.GetComponentsInChildren<MeshFilter>();
if (meshFilters == null || meshFilters.Length == 0)
{
Debug.LogWarning(wheelTransform.gameObject.name + ": Couldn't calculate radius. There are no meshes in the Wheel transform or its children");
return;
}
// Calculate the bounds of the meshes contained in the Wheel transform
Bounds bounds = GetScaledBounds(meshFilters[0]);
for (int i=1, c=meshFilters.Length; i<c; i++)
{
Bounds meshBounds = GetScaledBounds(meshFilters[i]);
bounds.Encapsulate(meshBounds.min);
bounds.Encapsulate(meshBounds.max);
}
// If this is a correct round wheel then extents for y and z should be approximately the same.
if (Mathf.Abs(bounds.extents.y-bounds.extents.z) > 0.01f)
Debug.LogWarning(wheelTransform.gameObject.name + ": The Wheel mesh might not be a correct wheel. The calculated radius is different along forward and vertical axis.");
wheelCollider.radius = bounds.extents.y;
}
static Bounds GetScaledBounds (MeshFilter meshFilter)
{
Bounds bounds = meshFilter.sharedMesh.bounds;
Vector3 scale = meshFilter.transform.lossyScale;
bounds.max = Vector3.Scale(bounds.max, scale);
bounds.min = Vector3.Scale(bounds.min, scale);
return bounds;
}
[ContextMenu("Convert Center of Mass from Transform to Parametric")]
void FromTransformToParametricCoM()
{
if (centerOfMassTransform != null)
{
VehicleFrame frame = ComputeVehicleFrame();
Vector3 transformCom = transform.InverseTransformPoint(centerOfMassTransform.position);
centerOfMassPosition = Mathf.InverseLerp(frame.rearPosition, frame.frontPosition, transformCom.z);
centerOfMassHeightOffset = transformCom.y - frame.baseHeight;
centerOfMassMode = CenterOfMassMode.Parametric;
}
}
//----------------------------------------------------------------------------------------------
#if UNITY_EDITOR
public void OnDrawGizmos ()
{
if (!enabled) return;
VehicleFrame frame = ComputeVehicleFrame();
// Draw the wheel gizmos
Color originalColor = UnityEditor.Handles.color;
UnityEditor.Handles.color = AlphaColor(Color.green, 0.1f);
foreach (Wheel w in wheels)
{
if (w.wheelCollider != null)
{
Vector3 basePos = w.wheelCollider.transform.TransformPoint(w.wheelCollider.center);
UnityEditor.Handles.DrawSolidDisc(basePos, transform.right, w.wheelCollider.radius * 0.2f);
}
}
// Draw the vehicle frame
UnityEditor.Handles.color = AlphaColor(Color.green, 0.5f);
Vector3 front = transform.TransformPoint(new Vector3 (0.0f, frame.baseHeight, frame.frontPosition));
Vector3 middle = transform.TransformPoint(new Vector3 (0.0f, frame.baseHeight, (frame.frontPosition + frame.rearPosition)*0.5f));
Vector3 rear = transform.TransformPoint(new Vector3 (0.0f, frame.baseHeight, frame.rearPosition));
UnityEditor.Handles.DrawLine(front, rear);
UnityEditor.Handles.DrawLine(front - transform.right * frame.frontWidth, front + transform.right * frame.frontWidth);
UnityEditor.Handles.DrawLine(rear - transform.right * frame.rearWidth, rear + transform.right * frame.rearWidth);
// float middleWidth = (frame.frontWidth + frame.rearWidth) * 0.5f * 0.25f;
// UnityEditor.Handles.DrawLine(middle - transform.right * middleWidth, middle + transform.right * middleWidth);
UnityEditor.Handles.color = AlphaColor(Color.white, 0.05f);
UnityEditor.Handles.DrawSolidDisc(middle, transform.up, 0.1f);
UnityEditor.Handles.color = AlphaColor(Color.white, 0.5f);
UnityEditor.Handles.DrawLine(middle - transform.right * 0.1f, middle + transform.right * 0.1f);
// Draw tire friction
float featureWidth = (frame.frontWidth + frame.rearWidth) * 0.5f * 0.25f;
Vector3 featureDir = transform.right * featureWidth;
UnityEditor.Handles.color = AlphaColor(Color.Lerp(Color.yellow, Color.red, 0.5f), 0.5f);
Vector3 frictionPos = Vector3.Lerp(rear, front, tireFrictionBalance);
UnityEditor.Handles.DrawLine(frictionPos - featureDir, frictionPos + featureDir);
// Draw brake balance
UnityEditor.Handles.color = AlphaColor(Color.red, 0.5f);
Vector3 brakePos = Vector3.Lerp(rear, front, brakeBalance);
UnityEditor.Handles.DrawLine(brakePos - featureDir, brakePos + featureDir);
// Draw drive balance
UnityEditor.Handles.color = AlphaColor(Color.green, 0.5f);
Vector3 drivePos = Vector3.Lerp(rear, front, driveBalance);
UnityEditor.Handles.DrawLine(drivePos - featureDir, drivePos + featureDir);
Vector3 driveForwardDir = transform.forward * 0.05f;
UnityEditor.Handles.DrawLine(drivePos - featureDir - driveForwardDir, drivePos - featureDir + driveForwardDir);
UnityEditor.Handles.DrawLine(drivePos + featureDir - driveForwardDir, drivePos + featureDir + driveForwardDir);
// Draw handling bias
UnityEditor.Handles.color = AlphaColor(Color.green, 0.5f);
Vector3 semiLengthDir = (front-middle);
Vector3 handlingPos = Vector3.Lerp(front - semiLengthDir, front + semiLengthDir, handlingBias);
UnityEditor.Handles.DrawLine(handlingPos - featureDir, handlingPos + featureDir);
UnityEditor.Handles.DrawLine(handlingPos + featureDir, front + featureDir);
UnityEditor.Handles.DrawLine(handlingPos - featureDir, front - featureDir);
// Draw Center of Mass
float comPos = Mathf.Lerp(frame.rearPosition, frame.frontPosition, centerOfMassPosition);
Vector3 localCom = new Vector3(0.0f, frame.baseHeight + centerOfMassHeightOffset, comPos);
Vector3 CoM = transform.TransformPoint(localCom);
UnityEditor.Handles.color = AlphaColor(Color.white, 0.8f);
DrawCrossMarkHandle(CoM, transform.forward, transform.right, transform.up);
Vector3 comBase = new Vector3(0.0f, frame.baseHeight, comPos);
UnityEditor.Handles.color = AlphaColor(Color.white, 0.1f);
UnityEditor.Handles.DrawLine(transform.TransformPoint(comBase), CoM);
// Draw aerodynamics
float aeroForwardPos = Mathf.Lerp(frame.rearPosition, frame.frontPosition, aeroBalance);
Vector3 localAeroPos = new Vector3(0.0f, localCom.y, aeroForwardPos);
Vector3 aeroPos = transform.TransformPoint(localAeroPos);
float aeroDragLength = Mathf.Min(Mathf.Abs(aeroDrag), featureWidth);
float aeroDownforceLength = Mathf.Min(Mathf.Abs(aeroDownforce), 0.1f);
if (aeroDragLength > 0.000001f || aeroDownforceLength > 0.00001f)
{
UnityEditor.Handles.color = AlphaColor(Color.cyan, 0.1f);
UnityEditor.Handles.DrawLine(aeroPos, CoM);
}
UnityEditor.Handles.color = AlphaColor(Color.cyan, 0.8f);
DrawCrossMarkHandle(transform.TransformPoint(localAeroPos),
Vector3.zero, transform.right * aeroDragLength, transform.up * aeroDownforceLength, 1.0f);
// Restore handles color
UnityEditor.Handles.color = originalColor;
}
static Color AlphaColor (Color col, float alpha = 1.0f)
{
col.a = alpha;
return col;
}
static void DrawCrossMarkHandle (Vector3 pos, Vector3 forward, Vector3 right, Vector3 up, float length = 0.1f)
{
length *= 0.5f;
Vector3 F = forward * length;
Vector3 U = up * length;
Vector3 R = right * length;
UnityEditor.Handles.DrawLine(pos - F, pos + F);
UnityEditor.Handles.DrawLine(pos - U, pos + U);
UnityEditor.Handles.DrawLine(pos - R, pos + R);
}
#endif
}
}