鸟语天空
车轮碰撞器(WheelCollider)
post by:追风剑情 2019-10-19 12:10

https://www.taikr.com/article/536
[官方文档] WheelCollider Manual
[官方文档] WheelCollider Script

车轮碰撞器(WheelCollider)

22225.gif

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 侧滑摩擦力

碰撞几何体

因为赛车可以达到很高的速度,所以正确设置赛道碰撞几何体非常重要。具体而言,碰撞网格不应具有构成可见模型(例如栅栏杆)的微小凹凸痕迹。通常,用于赛道的碰撞网格与可见网格分开制作,使碰撞网格尽可能平滑。此外不应有薄型对象;如果有薄型轨道边界,请在碰撞网格中使其加宽(如果汽车绝不会到达该处,应完全移除另一侧)。

WheelGeometries.jpg

可见几何体(左侧)比碰撞几何体(右侧)复杂得多

车轮摩擦曲线

下文中显示的_车轮摩擦曲线_可以描述轮胎摩擦。车轮的前进(滚动)方向和侧向方向有单独的曲线。在这两个方向上,首先确定轮胎打滑的程度(基于轮胎橡胶和道路之间的速度差异)。然后,将该打滑值用于计算施加在接触点上的轮胎力。

曲线以轮胎打滑的度量值作为输入,并以力作为输出。曲线由包含两部分的样条图近似模拟。第一部分从 (0 , 0) 到 (ExtremumSlip , ExtremumValue),目标点处曲线的正切值为零。第二部分从 (ExtremumSlip , ExtremumValue) 到 (AsymptoteSlip , AsymptoteValue),目标点处曲线的正切值再次为零:

WheelFrictionCurve.png

车轮摩擦曲线的典型形状

根据真实轮胎的特性,在低打滑条件下,轮胎可能会施加很大的力,因为橡胶会通过拉伸来补偿打滑。随后,当打滑变得非常高时,随着轮胎开始滑动或旋转,力会减小。因此,轮胎摩擦曲线的形状与上图相似。

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();
        }
    }
}  

将上面的汽车控制器挂到汽车对象上
2222222.png

运行效果

11115.gif



评论:
发表评论:
昵称

邮件地址 (选填)

个人主页 (选填)

内容