受力分析
直线行驶时的车轮受力如下:
水平方向上,所受合力为:
其中,为牵引力,
为空气阻力,
为滚动阻力,下面我们将逐个介绍。
驱动力
先来说扭矩,扭矩是使物体发生旋转的一个特殊力矩,等于力和力臂的乘积,单位为:
设驱动轴的扭矩为,车轮半径为
,那么牵引力:
如何求得驱动轴扭矩呢?设发动机扭矩为
,变速箱(Gear Ratio)和差速器(Differential Ratio)传送比分别为
和
,传输效率为
,那么发动机传送到驱动轴上的扭矩为:
发动机扭矩与发动机转速(RPM)有关:
那么发动机RPM如何取值呢?当司机踩下油门,气门角度变大,进气量增加,发动机输出扭矩增加,如果此时行驶阻力比发动机的输出扭矩小,则RPM会上升;如果行驶阻力比发动机的输出扭矩大,则RPM会下降。
在游戏中,我们可以设置一个变量SteerInput(0~1)代表油门的输入,让其乘以(用一个非零值作为发动机最小RPM求得),用根据牛顿第二定律计算出的车轮速度计算车轮RPM,再
和
反计算发动机RPM,从而使发动机RPM曲线发挥作用。
另外,说到变速箱,就不得不提换挡了。自动换挡的规则一般如下:
即油门与车速同时满足一定条件,才能触发换挡。之所以升档曲线与降档曲线不重合,是为了避免处于临界区时频繁换挡。
空气阻力
空气阻力的计算公式如下:
其中,为空气阻力系数(参考值0.3~0.5),A为车前面积(参考值2.2
),
为空气密度(参考值1.29
),
为车辆的运动速度。
滚动阻力
轮胎滚阻的计算公式如下:为整车重力,
为滚动阻力系数(参考值0.012~0.018)
在Unity中的实现
Unity给我们提供了WheelCollider组件,可以基于该组件进行动力学脚本编写:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;
// 车轴的封装类
[System.Serializable]
public class AxleInfo
{
// 车轮碰撞器
public WheelCollider Left;
public WheelCollider Right;
// 车轮模型GameObject
public GameObject LeftVisual;
public GameObject RightVisual;
public bool Motor;
public bool Steering;
public float MaxBrakeTorque = 1500.0f;
// 车轮的碰撞信息
[System.NonSerialized]
public WheelHit HitLeft;
[System.NonSerialized]
public WheelHit HitRight;
[System.NonSerialized]
public bool GroundedLeft = false;
[System.NonSerialized]
public bool GroundedRight = false;
}
public class VehicleDynamics : MonoBehaviour
{
// 车身
[Header("车身")]
[SerializeField] Rigidbody RB;
[SerializeField] Vector3 CenterOfMass = new Vector3(0f, 0.35f, 0f);// 质心
[SerializeField] float AirDragCoeff = 0.4f;// 空气阻力系数
[SerializeField] float MaxMotorTorque = 450f;// 最大牵引扭矩
// 发动机
[Header("发动机RPM")]
[SerializeField] AnimationCurve RPMCurve;
[SerializeField] float MinRPM = 800f;
[SerializeField] float MaxRPM = 8299f;
public float CurrentRPM { get; set; } = 0f;
[SerializeField] float RPMSmoothness = 20f;
float WheelsRPM = 0f;// 车轮RPM
// 挡位
[Header("挡位")]
[SerializeField] AnimationCurve ShiftUpCurve;// 这里以RPM代替车速
[SerializeField] AnimationCurve ShiftDownCurve;
[SerializeField] float[] GearRatios = new float[] { 4.17f, 3.14f, 2.11f, 1.67f, 1.28f, 1f, 0.84f, 0.67f };
public float CurrentGear { get; set; } = 1f;
float GearRatio = 0f;
[SerializeField] float FinalDriveRatio = 2.56f;// 最终传送比
[SerializeField] float ShiftDelay = 0.7f;// 两次换挡最小时间差
float LastShift = 0.0f;// 记录上一次换挡时刻
[SerializeField] float ShiftTime = 0.4f; // 换挡所需时间
private bool Shifting = false;
private int TargetGear = 1;
private int LastGear = 1;
public bool Reverse { get; set; } = false;// 是否为倒挡
// 车轮
[Header("车轮")]
[SerializeField] List<AxleInfo> Axles;
[SerializeField] float MaxSteeringAngle = 39.4f;// 车轮最大转向角
int NumberOfDrivingWheels;// 驱动轮的数量
[SerializeField] float WheelDamping = 1f;// 车轮碰撞器的阻尼率
// 输入
public float AccellInput { get; set; } = 0f;
public float SteerInput { get; set; } = 0f;
public bool HandBrake { get; set; } = false;
public void Awake()
{
RB = GetComponent<Rigidbody>();
RB.centerOfMass = CenterOfMass;
NumberOfDrivingWheels = Axles.Where(a => a.Motor).Count() * 2;
foreach (var axle in Axles)
{
axle.Left.wheelDampingRate = WheelDamping;
axle.Right.wheelDampingRate = WheelDamping;
}
}
public void FixedUpdate()
{
GetInput();// 获取油门及转向输入
SetGearRatio();// 调整变速箱传送比
SetRPM();// 计算发动机RPM
ApplySteer();// 设置转向
ApplyTorque();// 根据发动机RPM结合RPM曲线给轮子施加扭矩
RB.AddForce(-AirDragCoeff * 2.2f * 1.29f * RB.velocity * RB.velocity.magnitude / 2); // 施加空气阻力
}
private void Update()
{
UpdateWheelVisuals();
Debug.Log($"RPM:{CurrentRPM}, Gear:{CurrentGear}, Velocity: {RB.velocity.magnitude * 3.6}");
}
// 获取油门及转向输入
void GetInput()
{
SteerInput = CrossPlatformInputManager.GetAxis("Horizontal");
AccellInput = CrossPlatformInputManager.GetAxis("Vertical");
if (HandBrake)
{
AccellInput = -1.0f;
}
}
// 调整变速箱传送比
void SetGearRatio()
{
// 根据CurrentGear获取齿轮比
GearRatio = Mathf.Lerp(GearRatios[Mathf.FloorToInt(CurrentGear) - 1], GearRatios[Mathf.CeilToInt(CurrentGear) - 1], CurrentGear - Mathf.Floor(CurrentGear));// (因为数组的下标是从0开始的,所以要-1)
if (Reverse)
{
GearRatio = -1.0f * GearRatios[0];
}
AutoGearBox();
}
// 根据发动机RPM自动换挡
void AutoGearBox()
{
if (Time.time - LastShift > ShiftDelay)
{
// 根据升挡曲线升挡
if (CurrentRPM / MaxRPM > ShiftUpCurve.Evaluate(AccellInput) && Mathf.RoundToInt(CurrentGear) < GearRatios.Length)
{
if (Mathf.RoundToInt(CurrentGear) > 1 || RB.velocity.magnitude > 15f)// 如果正处于1挡,当速度高于15时再升挡
{
GearboxShiftUp();
}
}
// 根据降挡曲线降档
if (CurrentRPM / MaxRPM < ShiftDownCurve.Evaluate(AccellInput) && Mathf.RoundToInt(CurrentGear) > 1)
{
GearboxShiftDown();
}
}
// 完成换挡
if (Shifting)
{
float lerpVal = (Time.time - LastShift) / ShiftTime;
CurrentGear = Mathf.Lerp(LastGear, TargetGear, lerpVal);
if (lerpVal >= 1f)
Shifting = false;
}
// 限制挡位范围
if (CurrentGear >= GearRatios.Length)
{
CurrentGear = GearRatios.Length - 1;
}
else if (CurrentGear < 1)
{
CurrentGear = 1;
}
}
public bool GearboxShiftUp()
{
if (Reverse)
{
Reverse = false;
}
else
{
LastGear = Mathf.RoundToInt(CurrentGear);
TargetGear = LastGear + 1;
LastShift = Time.time;
Shifting = true;
}
return true;
}
public bool GearboxShiftDown()
{
if (Mathf.RoundToInt(CurrentGear) == 1)
{
Reverse = true;
}
else
{
LastGear = Mathf.RoundToInt(CurrentGear);
TargetGear = LastGear - 1;
LastShift = Time.time;
Shifting = true;
}
return true;
}
private void ApplyLocalPositionToVisuals(WheelCollider collider, GameObject visual)
{
if (visual == null || collider == null)
{
return;
}
Vector3 position;
Quaternion rotation;
collider.GetWorldPose(out position, out rotation);
visual.transform.position = position;
visual.transform.rotation = rotation;
}
private void SetRPM()
{
// 获取车轮的RPM
WheelsRPM = (Axles[1].Right.rpm + Axles[1].Left.rpm) / 2f;
if (WheelsRPM < 0)
{
WheelsRPM = 0;
}
// 根据车轮的RPM增加发动机的RPM
CurrentRPM = Mathf.Lerp(CurrentRPM, MinRPM + (WheelsRPM / GearRatio / FinalDriveRatio), Time.fixedDeltaTime * RPMSmoothness);
if (CurrentRPM < 0.02f)
{
CurrentRPM = 0.0f;
}
}
void ApplySteer()
{
float steer = MaxSteeringAngle * SteerInput;
foreach (var axle in Axles)
{
if (axle.Steering)
{
axle.Left.steerAngle = steer;
axle.Right.steerAngle = steer;
}
}
}
void ApplyTorque()
{
// 根据发动机RPM获取轮子的扭矩
var currentTorque = (float.IsNaN(CurrentRPM / MaxRPM)) ? 0.0f : RPMCurve.Evaluate(CurrentRPM / MaxRPM) * MaxMotorTorque * GearRatio * FinalDriveRatio;
// 给轮子施加扭矩
if (AccellInput >= 0)// 加速
{
float torquePerWheel = AccellInput * (currentTorque / NumberOfDrivingWheels);
foreach (var axle in Axles)
{
if (axle.Motor)
{
axle.Left.motorTorque = torquePerWheel;
axle.Right.motorTorque = torquePerWheel;
}
axle.Left.brakeTorque = 0f;
axle.Right.brakeTorque = 0f;
}
}
else// 制动
{
foreach (var axle in Axles)
{
var brakeTorque = AccellInput * -1 * axle.MaxBrakeTorque;
axle.Left.brakeTorque = brakeTorque;
axle.Right.brakeTorque = brakeTorque;
axle.Left.motorTorque = 0f;
axle.Right.motorTorque = 0f;
}
}
}
// 更新车轮模型的Pose
private void UpdateWheelVisuals()
{
foreach (var axle in Axles)
{
ApplyLocalPositionToVisuals(axle.Left, axle.LeftVisual);
ApplyLocalPositionToVisuals(axle.Right, axle.RightVisual);
}
}
}
对于WheelCollider的sidewaysFriction与forwardFriction,官方给的图表如下:
在急速起步、急速刹车或者急速转向时,轮胎可能会与地面产生滑动。Slip为车轮运动中滑动速度与车轮中心速度的比值,代表滑动成分所占的比例,取值范围为(0~1)。Force为制动力系数。
在低打滑条件下,轮胎可能会施加很大的力,因为橡胶会通过拉伸来补偿打滑。随后,当打滑变得非常高时,随着轮胎开始滑动或旋转,力会减小。
在UE4中的实现
UE4中的“轮子”比较完整,在WheeledVehicleMovementComponent4W组件的机械设置中,可以直接设置车子的机械属性。