https://www.taikr.com/article/536
[官方文档] WheelCollider Manual
[官方文档] WheelCollider Script
车轮碰撞器(WheelCollider)
WheelCollider属性 | |
属性 | 功能 |
Mass | 车轮的质量 |
Radius | 车轮的半径 |
Wheel Damping Rate | 应用于车轮的阴尼值 |
Suspension Distance | 车轮悬架的最大延伸距离(在局部空间中测量)。悬架始终延Y轴向下延伸。 |
Force App Point Distance | 此参数定义车轮上的受力点。此距离应该是距车轮底部静止位置的距离(沿悬架行程方向),以米为单位。当 forceAppPointDistance=0 时,受力点位于静止的车轮底部。较好的车辆会使受力点略低于车辆质心。 |
Center | WheelCollider的中心位置。 |
Suspension Spring |
Spring: 弹簧的弹力,值越大,悬架恢复到目标位置(Target Position)就越快; Damper: 弹簧受到的阻尼力,值越大,悬架恢复到目标位置(Target Position)就越慢; Target Position: 悬架沿悬架距离 (Suspension Distance) 的静止距离。1 对应于完全展开的悬架,0 对应于完全压缩的悬架。默认值为 0.5,与常规汽车的悬架行为匹配。 |
Forward Friction | 前向滚动摩擦力 |
Sideways Friction | 侧滑摩擦力 |
因为赛车可以达到很高的速度,所以正确设置赛道碰撞几何体非常重要。具体而言,碰撞网格不应具有构成可见模型(例如栅栏杆)的微小凹凸痕迹。通常,用于赛道的碰撞网格与可见网格分开制作,使碰撞网格尽可能平滑。此外不应有薄型对象;如果有薄型轨道边界,请在碰撞网格中使其加宽(如果汽车绝不会到达该处,应完全移除另一侧)。
可见几何体(左侧)比碰撞几何体(右侧)复杂得多
下文中显示的_车轮摩擦曲线_可以描述轮胎摩擦。车轮的前进(滚动)方向和侧向方向有单独的曲线。在这两个方向上,首先确定轮胎打滑的程度(基于轮胎橡胶和道路之间的速度差异)。然后,将该打滑值用于计算施加在接触点上的轮胎力。
曲线以轮胎打滑的度量值作为输入,并以力作为输出。曲线由包含两部分的样条图近似模拟。第一部分从 (0 , 0) 到 (ExtremumSlip , ExtremumValue),目标点处曲线的正切值为零。第二部分从 (ExtremumSlip , ExtremumValue) 到 (AsymptoteSlip , AsymptoteValue),目标点处曲线的正切值再次为零:
车轮摩擦曲线的典型形状
根据真实轮胎的特性,在低打滑条件下,轮胎可能会施加很大的力,因为橡胶会通过拉伸来补偿打滑。随后,当打滑变得非常高时,随着轮胎开始滑动或旋转,力会减小。因此,轮胎摩擦曲线的形状与上图相似。
Forward/Sideways Friction | |
属性 | 功能 |
Extremum Slip/Value | 曲线的极值点(Slip, Value)。 |
Asymptote Slip/Value | 曲线的渐近点(Slip, Value)。 |
Stiffness |
摩擦力的刚度。 Extremum Value 和 Asymptote Value 的乘数(默认值为 1)。改变摩擦力的刚度。将此值设置为零将完全禁用车轮的所有摩擦力。通常在运行时修改刚度以使用脚本来模拟各种地面材质。 |
● 可能希望在 Time 窗口中降低物理时间步长长度以获得更稳定的汽车物理特性,特别是在高速赛车的情况下。
● 为防止汽车太容易翻转,可以通过脚本稍微降低其刚体质心(rigidbody.centerOfMass),并施加取决于汽车速度的“下压”力。
● 刹车过程中,需要模拟ABS,防止车轮抱死失去转向能力。ABS仿真实验
注意事项
1、车轮模型与WheelCollider必须分开为不同的GameObject
2、车轮模型不要挂MeshCollider(会影响WheelCollider与地面的接触)。
3、利用WheelCollider.GetWorldPose()方法实现WheelCollider与车轮模型的状态同步。
4、车轮碰撞器对象上不要有Rigidbody
5、WheelCollider的半径(Radius)调整为车轮模型的半径
6、车辆的根GameObject上必须挂Collider和Rigidbody,质量(Mass)按照真实车辆设置(如桥车1800kg左右)
7、如果运行时车轮偏高或偏低,可以微调悬挂距离(Suspension Distance)参数。
8、WheelCollider会忽略标准的PhysicMaterial设置
9、通过改变WheelCollider的正向摩擦力(forwardFriction)和横向摩擦力(sidewaysFriction)来模拟不同的道路材质
10、一个普通的轿车轮胎的重量在25~40kg左右,轮胎质量(Mass)越大,与地面之间的摩擦力就越大,如果容易原地打滑,可以适当增加轮胎质量。
11、0.8L排量的发动机最大扭矩(Motor Torque)为60~70牛·米左右,6.0L排量的发动机最大扭矩为600牛·米左右,跑车的扭矩500以上。
12、轿车制动力矩(Brake Torque)通常在1500-3000牛
13、如果高速转弯时车辆倾斜厉害甚至容易侧翻时,可以通过降低刚体质心(rigidbody.centerOfMass),来使车辆高速行驶时更稳重。
14、不要设置刚体(RigidBody)的角阻力(AngularDrag), 不要给车子设定其他作用力。
15、汽车挂上刚体(RigidBody)后,WheelCollider在Scene中的辅助线框才会显示出来。
Vector3 pos;
Quaternion rotation;
wheelCollider.GetWorldPose(out pos, out rotation);
wheelModel.position = pos;
wheelModel.rotation = rotation;
//轮子转速 (转/分)
float rpm = wheelCollider.rpm;
//每帧绕X轴旋转的度数
float xAngle = rpm / 60 * 360 * Time.deltaTime;
//magnitude是米/秒,乘以3.6转成千米/小时
float speed = carRigidbody.velocity.magnitude * 3.6f;
//发动机扭矩 (控制驱动力)
wheelCollider.motorTorque = motorTorque;
//转向角 (控制转弯)
wheelCollider.steerAngle = steerAngle;
//制动力 (控制刹车)
wheelCollider.brakeTorque = brakeTorque;
变速箱的传动比又叫齿轮比(gear ratio)。
例如五档变速箱的齿比: 3.182、 1.895、 1.25、 0.909、 0.703;倒档 3.133;主减速比 4.308。
无级变速器(CVT),没有固定传动比,会自动调整以适应当前的驾驶条件。
引擎转速=车轮转速×档位齿轮比
车轮滑动率用来恒量打滑程度,滑动率为0时,表示车轮自由滚动,滑动率为1时,表示车轮完全抱死。
滑动率=(车速-轮速)/车速
ABS(防抱死系统)通过动态调整制动力,将滑动率控制在20~30%。
参考
手动变速器各档位齿比
How to Find Your Car’s Gearbox and Final Drive Ratios and Efficiencies?
Finding the Gearbox and Final Drive Ratios and Efficiencies
汽车控制器脚本
using System;
using UnityEngine;
/// <summary>
/// 汽车控制器
/// </summary>
public partial class CarController : MonoBehaviour
{
#region 声明变量
[Tooltip("车辆驱动类型")]
[SerializeField]
private CarDriveType carDriveType;
[Tooltip("车辆重心")]
[SerializeField]
private Vector3 centerOfMass;
[Tooltip("发动机扭矩(输出动力,单位: 牛顿米)")]
[SerializeField]
private float motorTorque = 310f;
[Tooltip("制动力")]
[SerializeField]
private float brakeTorque = 3000;
[Tooltip("发动机最大转速(rpm)")]
[SerializeField]
private float MAX_RPM = 6600;
[Tooltip("最大转向角")]
public float MAX_STEER_ANGLE = 40;
//左前轮
[SerializeField]
private WheelColliderParam wheelColliderParamFrontL;
//右前轮
[SerializeField]
private WheelColliderParam wheelColliderParamFrontR;
//左后轮
[SerializeField]
private WheelColliderParam wheelColliderParamBackL;
//右后轮
[SerializeField]
private WheelColliderParam wheelColliderParamBackR;
private Rigidbody carRigidbody;
#endregion
//是否允许键盘操控
[SerializeField]
private bool keyboard = true;
//五座车位,用来保存驾乘人员的ID
[NonSerialized]
public int seat0, seat1, seat2, seat3, seat4;
private Transform cacheTransform;
public Vector3 Position {
get { return cacheTransform.position; }
set { cacheTransform.position = value; }
}
public Quaternion Rotation {
get { return cacheTransform.rotation; }
set { cacheTransform.rotation = value; }
}
public Vector3 Velocity {
get { return carRigidbody.velocity; }
set { carRigidbody.velocity = value; }
}
// 判断当前汽车是否停止
public bool IsStop()
{
var v = Velocity;
bool stop =
Mathf.Abs(v.x) < 0.001f &&
Mathf.Abs(v.y) < 0.001f &&
Mathf.Abs(v.z) < 0.001f;
return stop;
}
private void Awake()
{
cacheTransform = transform;
carRigidbody = this.GetComponent<Rigidbody>();
if (carRigidbody != null)
carRigidbody.centerOfMass = centerOfMass;
wheelColliderParamFrontL.MAX_RPM = MAX_RPM;
wheelColliderParamFrontR.MAX_RPM = MAX_RPM;
wheelColliderParamBackL.MAX_RPM = MAX_RPM;
wheelColliderParamBackR.MAX_RPM = MAX_RPM;
wheelColliderParamFrontL.MAX_STEER_ANGLE = MAX_STEER_ANGLE;
wheelColliderParamFrontR.MAX_STEER_ANGLE = MAX_STEER_ANGLE;
wheelColliderParamBackL.MAX_STEER_ANGLE = MAX_STEER_ANGLE;
wheelColliderParamBackR.MAX_STEER_ANGLE = MAX_STEER_ANGLE;
}
private void Update()
{
#region 键盘操控
if (keyboard)
{
//加速(前进)
if (Input.GetKeyDown(KeyCode.W))
{
PressForward(true);
}
if (Input.GetKeyUp(KeyCode.W))
PressForward(false);
//加速(后退)
if (Input.GetKeyDown(KeyCode.S))
{
PressBack(true);
}
if (Input.GetKeyUp(KeyCode.S))
PressBack(false);
//制动
if (Input.GetKeyDown(KeyCode.Space))
{
PressBrake(true);
}
if (Input.GetKeyUp(KeyCode.Space))
PressBrake(false);
//向左转向
if (Input.GetKeyDown(KeyCode.A))
PressSteerLeft(true);
if (Input.GetKeyUp(KeyCode.A))
PressSteerLeft(false);
//向右转向
if (Input.GetKeyDown(KeyCode.D))
PressSteerRight(true);
if (Input.GetKeyUp(KeyCode.D))
PressSteerRight(false);
}
#endregion
}
private void FixedUpdate()
{
wheelColliderParamFrontL.velocity = carRigidbody.velocity;
wheelColliderParamFrontR.velocity = carRigidbody.velocity;
wheelColliderParamBackL.velocity = carRigidbody.velocity;
wheelColliderParamBackR.velocity = carRigidbody.velocity;
wheelColliderParamFrontL.Update();
wheelColliderParamFrontR.Update();
wheelColliderParamBackL.Update();
wheelColliderParamBackR.Update();
//根据当前车速自动换档
float kmph = GetKmph();
if (kmph > 50)
ChangeGearRatio(4);//换五档
else if (kmph > 40)
ChangeGearRatio(3);//换四档
else if (kmph > 30)
ChangeGearRatio(2);//换三档
else if (kmph > 20)
ChangeGearRatio(1);//换二档
else
ChangeGearRatio(0);//换一档
}
// 切换档位
private void ChangeGearRatio(int index)
{
//五档变速箱
float[] gearRatios = { 3.182f, 1.895f, 1.25f, 0.909f, 0.703f };
if (index < 0 || index >= gearRatios.Length)
return;
float gearRatio = gearRatios[index];
wheelColliderParamFrontL.gearRatio = gearRatio;
wheelColliderParamFrontR.gearRatio = gearRatio;
wheelColliderParamBackL.gearRatio = gearRatio;
wheelColliderParamBackR.gearRatio = gearRatio;
}
// 设置发动机扭矩
public void SetMotorTorque(float motorTorque)
{
switch(carDriveType)
{
case CarDriveType.Front:
wheelColliderParamFrontL.motorTorque = motorTorque;
wheelColliderParamFrontR.motorTorque = motorTorque;
break;
case CarDriveType.Back:
wheelColliderParamBackL.motorTorque = motorTorque;
wheelColliderParamBackR.motorTorque = motorTorque;
break;
case CarDriveType.Four:
wheelColliderParamFrontL.motorTorque = motorTorque;
wheelColliderParamFrontR.motorTorque = motorTorque;
wheelColliderParamBackL.motorTorque = motorTorque;
wheelColliderParamBackR.motorTorque = motorTorque;
break;
}
}
// 设置制动力矩
public void SetBrakeTorque(float brakeTorque)
{
wheelColliderParamFrontL.brakeTorque = brakeTorque;
wheelColliderParamFrontR.brakeTorque = brakeTorque;
wheelColliderParamBackL.brakeTorque = brakeTorque;
wheelColliderParamBackR.brakeTorque = brakeTorque;
}
// 设置转向角
public void SetSteerAngle(float steerAngle)
{
wheelColliderParamFrontL.steerAngle = steerAngle;
wheelColliderParamFrontR.steerAngle = steerAngle;
}
// 获取车辆时速 (km/h)
public int GetKmph()
{
float speed = carRigidbody.velocity.magnitude * 3.6f;
return Mathf.RoundToInt(speed);
}
// 获取车辆速度 (m/s)
public float GetSpeed()
{
float speed = carRigidbody.velocity.magnitude;
return speed;
}
// 引擎转速
public int GetEngineRPM()
{
int rpm = 0;
//如果要考虑仿真的话,需要乘以当前档位的齿轮比
//engineRPM=wheelRPM*gearRatio*finalDriveRatio;
switch (carDriveType)
{
case CarDriveType.Front:
rpm = (int)(wheelColliderParamFrontL.rpm + wheelColliderParamFrontR.rpm) / 2;
break;
case CarDriveType.Back:
rpm = (int)(wheelColliderParamBackL.rpm + wheelColliderParamBackR.rpm) / 2;
break;
case CarDriveType.Four:
rpm = (int)(wheelColliderParamFrontL.rpm + wheelColliderParamFrontR.rpm +
wheelColliderParamBackL.rpm + wheelColliderParamBackR.rpm) / 4;
break;
}
return rpm;
}
#region 按键操控接口
/// <summary>
/// 前进
/// </summary>
/// <param name="press">true:施加前向驱动力; false:撤消前向驱动力</param>
public void PressForward(bool press)
{
if (press)
{
//SetBrakeTorque(0);
SetMotorTorque(motorTorque);
}
else
{
SetMotorTorque(0);
}
}
/// <summary>
/// 后退
/// </summary>
/// <param name="press">true:施加后向驱动力; false:撤消后向驱动力</param>
public void PressBack(bool press)
{
if (press)
{
SetBrakeTorque(0);
SetMotorTorque(-motorTorque);
}
else
{
SetMotorTorque(0);
}
}
/// <summary>
/// 制动
/// </summary>
/// <param name="press">true:施加制动力; false:撤消制动力</param>
public void PressBrake(bool press)
{
if (press)
{
//SetMotorTorque(0);
SetBrakeTorque(brakeTorque);
}
else
{
SetBrakeTorque(0);
}
}
/// <summary>
/// 转向
/// </summary>
/// <param name="steerRate">取值范围[-1, 1], -1为向左最大转向,1为向右最大转向</param>
public void PressSteer(float steerRate)
{
SetSteerAngle(MAX_STEER_ANGLE * steerRate);
}
/// <summary>
/// 左转
/// </summary>
/// <param name="press">true:向左打方向盘; false:方向盘回正</param>
public void PressSteerLeft(bool press)
{
if (press)
{
SetSteerAngle(-MAX_STEER_ANGLE);
}
else
{
SetSteerAngle(0);
}
}
/// <summary>
/// 右转
/// </summary>
/// <param name="press">true:向右打方向盘; false:方向盘回正</param>
public void PressSteerRight(bool press)
{
if (press)
{
SetSteerAngle(MAX_STEER_ANGLE);
}
else
{
SetSteerAngle(0);
}
}
#endregion
// 实时音效
private void UpdateAudio()
{
}
// 获取汽车当前信息
public string GetDriveInfo()
{
float rpm_fl = wheelColliderParamFrontL.rpm;
float rpm_fr = wheelColliderParamFrontR.rpm;
float rpm_bl = wheelColliderParamBackL.rpm;
float rpm_br = wheelColliderParamBackR.rpm;
float sidewaysSlip_fl = wheelColliderParamFrontL.wheelHit.sidewaysSlip;
float sidewaysSlip_fr = wheelColliderParamFrontR.wheelHit.sidewaysSlip;
float sidewaysSlip_bl = wheelColliderParamBackL.wheelHit.sidewaysSlip;
float sidewaysSlip_br = wheelColliderParamBackR.wheelHit.sidewaysSlip;
float kmph = wheelColliderParamFrontL.kmph;
string drive_info = string.Format(
"front left rpm={0}\n" +
"front right rpm={1}\n"+
"back left rpm={2}\n"+
"back right rpm={3}\n"+
"front left sidewaysSlip={4}\n"+
"front right sidewaysSlip={5}\n"+
"back left sidewaysSlip={6}\n"+
"back right sidewaysSlip={7}\n",
rpm_fl, rpm_fr, rpm_bl, rpm_br,
sidewaysSlip_fl, sidewaysSlip_fr, sidewaysSlip_bl, sidewaysSlip_br);
return drive_info;
}
}
//车轮碰撞器参数
[Serializable]
public class WheelColliderParam
{
//车轮碰撞器
[SerializeField]
private WheelCollider wheelCollider;
//车轮模型
[SerializeField]
private Transform wheelModel;
//车轮驱动力 (单位: 牛顿米)
[NonSerialized]
public float motorTorque;
//车轮转向 (欧拉角)
[NonSerialized]
public float steerAngle;
//车轮制动力 (单位: 牛顿米)
//碟刹约3000N,鼓刹约1500N
[NonSerialized]
public float brakeTorque;
//最大rpm
[NonSerialized]
public float MAX_RPM = 6600;
//最大转角
[NonSerialized]
public float MAX_STEER_ANGLE = 30;
//传动比(模拟档位)
public float gearRatio = 3.182f;
//当前车速
[NonSerialized]
public Vector3 velocity;
//滑移率 (0:自由滚动; 1:车轮完全抱死)
//最佳滑移率为 20%-30%
public float slipRate
{
get
{
float v = velocity.magnitude;
float s = (v - mps) / v;
return s;
}
}
//车轮转速
public float rpm
{
get { return wheelCollider.rpm; }
}
//速度 (m/s)
public float mps
{
get
{
float rpm = wheelCollider.rpm;//(转/分钟)
float rps = rpm / 60f;//(转/秒)
float r = wheelCollider.radius;
float c = 2 * Mathf.PI * r;//车轮周长
float _mps = rps * c; //(米/秒)
return _mps;
}
}
//时速 (m/h)
public float mph
{
get
{
float rpm = wheelCollider.rpm;//(转/分钟)
float rph = rpm * 60; //(转/小时)
float r = wheelCollider.radius;
float c = 2 * Mathf.PI * r;//车轮周长
float _mph = rph * c; //(米/小时)
return _mph;
}
}
//时速 (km/h)
public float kmph
{
get { return Mathf.Round(mph / 1000); }
}
//是否与地面接触
public bool isGrounded
{
get { return wheelCollider.isGrounded; }
}
//车轮与地面的碰撞信息
public WheelHit wheelHit
{
get {
WheelHit hit;
wheelCollider.GetGroundHit(out hit);
return hit;
}
}
public void Update()
{
//以加速到最大速度,撤消驱动力
if (wheelCollider.rpm >= MAX_RPM)
motorTorque = 0;
//wheelCollider.suspensionExpansionLimited = true;
wheelCollider.motorTorque = motorTorque * gearRatio;
wheelCollider.steerAngle = steerAngle;
//模拟ABS防抱死系统
if (brakeTorque > 0) //踩下刹车
{
if (wheelCollider.brakeTorque > 0)
{
//如果车轮抱得太死,就减少制动力
if (slipRate > 0.3f)
{
wheelCollider.brakeTorque /= 2;
}
}
else
{
wheelCollider.brakeTorque = brakeTorque;
}
}
else
{
wheelCollider.brakeTorque = 0;
}
//更新车轮模型转角
Vector3 pos;
Quaternion rotation;
wheelCollider.GetWorldPose(out pos, out rotation);
wheelModel.position = pos;
wheelModel.rotation = rotation;
}
}
//车辆驱动类型
public enum CarDriveType
{
Front,//前驱
Back, //后驱
Four //四驱
}
CarControllerInspector.cs
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(CarController))]
[CanEditMultipleObjects]
public class CarControllerInspector : Editor
{
protected CarController controller;
protected virtual void OnEnable()
{
controller = target as CarController;
}
private bool DrawHeader(string text)
{
string key = text;
bool state = EditorPrefs.GetBool(key, true);
if (!state) GUI.backgroundColor = new Color(0.8f, 0.8f, 0.8f);
GUILayout.BeginHorizontal();
GUI.changed = false;
text = "<b><size=11>" + text + "</size></b>";
if (state) text = "\u25BC " + text;
else text = "\u25BA " + text;
if (!GUILayout.Toggle(true, text, "dragtab", GUILayout.MinWidth(20f))) state = !state;
if (GUI.changed) EditorPrefs.SetBool(key, state);
GUILayout.EndHorizontal();
GUI.backgroundColor = Color.white;
if (!state) GUILayout.Space(3f);
return state;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();//保留Unity自动生成的Inspector
if (DrawHeader("Drive Info"))
{
EditorGUILayout.BeginVertical("AS TextArea", GUILayout.MinHeight(10f));
EditorGUI.BeginDisabledGroup(true);//true:禁用以下操作
GUILayout.Label(controller.GetDriveInfo());
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndVertical();
SceneView.RepaintAll();
}
}
}
运行效果