受力分析
直线行驶时的车轮受力如下: 水平方向上,所受合力为:
F
=
F
t
+
F
w
+
F
f
F=F_t+F_w+F_f
F=Ft?+Fw?+Ff? 其中,
F
t
F_t
Ft?为牵引力,
F
w
F_w
Fw?为空气阻力,
F
f
F_f
Ff?为滚动阻力,下面我们将逐个介绍。
驱动力
先来说扭矩,扭矩是使物体发生旋转的一个特殊力矩,等于力和力臂的乘积,单位为
N
?
m
N?m
N?m: 设驱动轴的扭矩为
T
t
T_t
Tt?,车轮半径为
r
r
r,那么牵引力:
F
t
=
T
t
?
r
F_t=T_t?r
Ft?=Tt??r 如何求得驱动轴扭矩
T
t
T_t
Tt?呢?设发动机扭矩为
T
e
T_e
Te?,变速箱(Gear Ratio)和差速器(Differential Ratio)传送比分别为
i
g
i_g
ig?和
i
d
i_d
id?,传输效率为
η
η
η,那么发动机传送到驱动轴上的扭矩为:
T
t
=
T
e
i
g
i
d
η
T_t=T_e i_g i_d η
Tt?=Te?ig?id?η 发动机扭矩
T
e
T_e
Te?与发动机转速(RPM)有关: 那么发动机RPM如何取值呢?当司机踩下油门,气门角度变大,进气量增加,发动机输出扭矩增加,如果此时行驶阻力比发动机的输出扭矩小,则RPM会上升;如果行驶阻力比发动机的输出扭矩大,则RPM会下降。 在游戏中,我们可以设置一个变量SteerInput(0~1)代表油门的输入,让其乘以
F
t
F_t
Ft?(用一个非零值作为发动机最小RPM求得),用根据牛顿第二定律计算出的车轮速度计算车轮RPM,再
i
g
i_g
ig?和
i
d
i_d
id?反计算发动机RPM,从而使发动机RPM曲线发挥作用。 另外,说到变速箱,就不得不提换挡了。自动换挡的规则一般如下: 即油门与车速同时满足一定条件,才能触发换挡。之所以升档曲线与降档曲线不重合,是为了避免处于临界区时频繁换挡。
空气阻力
空气阻力的计算公式如下:
F
w
=
C
d
A
ρ
v
2
/
2
F_w=C_dAρv^2/2
Fw?=Cd?Aρv2/2 其中,
C
d
C_d
Cd?为空气阻力系数(参考值0.3~0.5),A为车前面积(参考值2.2
m
2
m^2
m2),
ρ
ρ
ρ为空气密度(参考值1.29
k
g
?
m
3
kg?m^3
kg?m3),
v
v
v为车辆的运动速度。
滚动阻力
轮胎滚阻的计算公式如下:
F
f
=
G
f
F_f=Gf
Ff?=Gf
G
G
G为整车重力,
f
f
f为滚动阻力系数(参考值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;
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;
[Header("挡位")]
[SerializeField] AnimationCurve ShiftUpCurve;
[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();
ApplySteer();
ApplyTorque();
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()
{
GearRatio = Mathf.Lerp(GearRatios[Mathf.FloorToInt(CurrentGear) - 1], GearRatios[Mathf.CeilToInt(CurrentGear) - 1], CurrentGear - Mathf.Floor(CurrentGear));
if (Reverse)
{
GearRatio = -1.0f * GearRatios[0];
}
AutoGearBox();
}
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)
{
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()
{
WheelsRPM = (Axles[1].Right.rpm + Axles[1].Left.rpm) / 2f;
if (WheelsRPM < 0)
{
WheelsRPM = 0;
}
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()
{
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;
}
}
}
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组件的机械设置中,可以直接设置车子的机械属性。
|