自制载具系统:WheelCollider2

本博客实现的代码以 Unity Asset Store 中 USHISOFT 的资源代码 WheelCollider2 为参考

本博客内容较多,代码量较大,可以直接点击右侧目录跳转到你想看的地方

整体代码


变量与属性查询

变量

变量名称 变量解释
SweepType 碰撞检测的形状。
射线(如 WheelCollider),使用单束光检测与地面的碰撞。可能是最快的,但在有小孔的地面上可能会卡住。
球体,检测球体和地面之间的碰撞。
圆柱体,使用最接近轮胎形状的圆柱体来确定与地面的碰撞。推荐使用。
Radius 轮胎半径 (m)。
Inertia 轮胎惯性矩。
惯性矩是衡量物体转动或停止难度的指标。
Width 轮胎宽度 (m)。用于 SweepType/Cylinder
Cylinder Resolution 用于 SweepType/Cylinder 的圆柱体网格平滑度。
Suspension Spring 悬挂弹簧常数 (Nm)。
Suspension Bump 悬挂压缩侧的阻尼力 (Nm)。
Suspension Rebound 悬挂回弹侧的阻尼力 (Nm)。
Suspension Distance 悬挂长度 (m)。
Camber Angle 倾角 (度)。
对于左侧轮胎,设置负值会导致负倾角,设置正值会导致正倾角。
对于右侧轮胎,正负值需要反转。如果你想让右侧轮胎有负倾角,设置正值。
Toe Angle 转向角度 (度)。
对于左侧轮胎,负值会导致内束角,正值会导致外束角。
对于右侧轮胎,正负值需要反转。如果你想让右侧轮胎有内束角,设置正值。
Magic Formula B, C, D, E 魔术公式(简化版)的系数,用于计算摩擦力。
通过调整这些系数,可以模拟各种路面。
D 是摩擦系数。
Rolling Resistance Coef 滚动阻力系数。
Bump Amplitude 地面不平整度 (m)。用于模拟砾石路和赛道旁草地等不平整路面。
Bump Period 地面不平整周期。周期越大,不平整度越短。建议增加细颗粒路面如砾石路的周期大小。
Model 轮胎的 3D 模型。配置的 3D 模型将根据此组件的状态进行动画处理。即使不设置也没有问题。

属性

属性名称 属性解释
SteerAngle 转向角度 (度)
DriveTorque 驱动扭矩 (Nm)。等同于 WheelColliderMotorTorque
BrakeTorque 制动扭矩 (Nm)
DrivetrainInertia 传动系统惯性矩。驱动轮具有较大的惯性矩,因为它们与传动系统相连。设置这个值对于实现逼真的行为至关重要
DrivetrainFrictionTorque 传动系统摩擦扭矩 (Nm)
AntiRollBarForce 防倾杆力 (Nm)
Grounded 是否接地
HitInfo 接地点信息
CurrentSuspensionDistance 当前悬挂长度 (m)。
SuspensionCompressionRate 悬挂压缩率。
AngularVelocity 轮胎角速度 (rad/s)。
RPM 轮胎转速 (rpm)。
ForwardDirection 纵向摩擦力作用方向。对应 WheelColliderGetGroundHit 获取的 forwardDir
ForwardSlip 纵向滑移率。对应 WheelColliderGetGroundHit 获取的 forwardSlip
ForwardForce 垂直方向上的摩擦力 (Nm)。
SidewaysDirection 横向摩擦力作用方向。对应 WheelColliderGetGroundHit 获取的 sidewaysDir
SidewaysSlip 横向滑移率。对应 WheelColliderGetGroundHit 获取的 sidewaysSlip
SidewaysForce 横向摩擦力 (Nm)。
Load 轮胎负载 (Nm)。对应 WheelColliderGetGroundHit 获取的力。
Center 轮胎中心坐标 (世界空间)。对应 WheelColliderGetWorldPose 获取的 pos
Rotation 轮胎旋转 (世界空间)。等同于 WheelColliderGetWorldPose 获取的 quat
PeakSlip 摩擦力最大时的滑移率。

变量解释

在开始主要程序的编写之前,我们需要先对 WheelCollider2 所需的变量有一个基本的了解

SweepType 部分

1
2
3
4
5
6
7
8
public enum SweepType
{
Ray,
Sphere,
Cylinder,
}

[SerializeField] private SweepType _sweepType = SweepType.Ray;

SweepType 指的是车轮与地面碰撞检测的三种类型,这里通过枚举让玩家可以自由选择使用哪种方式来配置 WheelCollider2

  1. 射线检测(Ray)是指通过单根射线与地面碰撞来实现检测,是性能最优的方法(也是传统 WheelCollider 提供的方法),但很容易卡在地面的小洞中,此时射线无法检测到地面而判断轮胎在空中,从而无法提供动力(哪怕车轮的其他部位都在地面上)。

  2. 球体检测(Sphere)是将车轮模拟为一个球体来与地面进行碰撞,是许多游戏模型常用的方法(将车轮的碰撞体用球体代替)。

  3. 圆柱体检测(Cylinder)是将车轮模拟为一个侧面着地圆柱体(最符合实际的情况),也是在该类中最推荐的选择。

常量部分

1
2
3
private const float MinDenominator = 10f;          // 用于防止除以零错误或避免在分母非常小的情况下进行除法运算
private const float MinDenominator2 = 0.00001f; // 同样用于防止除以零错误或避免在分母非常小的情况下进行除法运算
private const int GizmosSmoothness = 16; // 用于控制 Unity 中 Gizmos 的平滑度或细节级别

此处的两个 MinDenominator 常量均用于防止在后面的变量中出现除以零错误,举个例子:

1
2
3
4
5
6
7
8
float force = CalculateForce();
float mass = GetMass();

// 需要一定精度的计算
float acceleration = force / Mathf.Max(mass, MinDenominator);

// 需要更高精度的计算
float preciseAcceleration = force / Mathf.Max(mass, MinDenominator2);

通过 Mathf.Max() 方法将可能很小的变量用 MinDenominator 替代,在一定程度上保证精确度的同时避免除以零错误的产生。

车轮几何部分

1
2
3
4
5
[SerializeField, Min(0.001f)] private float _radius = 0.3f;         // 车轮 半径
[SerializeField, Min(0.001f)] private float _inertia = 1f; // 车轮 质量惯性矩

[SerializeField, Min(0.001f)] private float _width = 0.2f; // 车轮 宽度
[SerializeField, Min(8f)] private int _cylinderResolution = 16; // 车轮 圆柱分辨率
  1. _radius 车轮半径,影响车辆的高度和行驶性能,需要进行初始设定以适配轮胎模型的大小和位置,通过 Min(0.001f) 确定最小值为 0.001。

  2. _inertia 车轮质量惯性矩,影响车轮旋转惯性和响应速度。同样最小值为 0.001。

  3. _width 车轮宽度,当选择 SweepType 为圆柱体检测时设置圆柱体的侧边高度(即轮胎宽度),影响抓地力和视觉效果。

  4. _cylinderResolution 车轮圆柱分辨率,当选择 SweepType 为圆柱体检测时设定轮胎形状的细分程度,影响后续的绘制渲染质量和碰撞检测精度,更高的车轮分辨率意味着车轮更“圆”,更贴合实际轮胎情况,在模拟时可以提供更好的碰撞检测精度,但更高分辨率也会加大性能消耗,建议直接采用默认的 16 即可。

悬挂系统部分

1
2
3
4
[SerializeField, Min(0f)] private float _suspensionSpring = 40000f;     // 悬挂系统 弹簧刚度 / 弹簧力度
[SerializeField, Min(0f)] private float _suspensionBump = 2000f; // 悬挂系统 压缩阻尼系数
[SerializeField, Min(0f)] private float _suspensionRebound = 2000f; // 悬挂系统 回弹阻尼系数
[SerializeField, Min(0.001f)] private float _suspensionLength = 0.1f; // 悬挂系统 最大行程长度
  1. _suspensionSpring 悬挂系统的弹簧力度,值越大,悬挂到达目标位置的速度越快,车辆在不平路面上的反应更快,但也会传递更多的路面振动到车身;值越小,悬挂更软,可以更好地吸收路面不平带来的冲击,但在快速转弯时可能会导致车身过度侧倾。

  2. _suspensionBump 悬挂系统的压缩阻尼系数,即悬挂压缩时的能量消耗速度。值越大,悬挂压缩速度越慢,减少车身在撞击障碍物时的上下跳动,有助于保持轮胎与地面的良好接触;值越小,悬挂压缩越快,允许车身更快地响应路面变化,但可能导致车辆在颠簸路面上的跳跃感增加。

  3. _suspensionRebound 悬挂系统的回弹阻尼系数,即悬挂从压缩状态恢复到原始位置时的能量消耗速度。值越大,悬挂回弹越慢,减少车身在压缩后迅速反弹引起的震荡,有助于稳定车身姿态;值越小,悬挂回弹越快,允许车身迅速回复到正常高度,但可能导致车辆在颠簸路面上的频繁跳跃。

  4. _suspensionLength 悬挂系统的最大行程长度,即悬挂能够压缩和拉伸的最大距离。值越大,悬挂就能够在更大范围内运动,适合越野环境,能够更好地应对较大的路面起伏;值越小,悬挂运动范围越有限,适合平坦道路,提供更精确的操控反馈。

车轮倾角部分

1
2
[SerializeField, Range(-45f, 45f)] private float _camberAngle = 0f;     // 车轮对齐角度 内倾角
[SerializeField, Range(-45f, 45f)] private float _toeAngle = 0f; // 车轮对齐角度 前束角

内倾角 (Camber Angle, _camberAngle)

定义:车轮垂直中心线相对于车辆纵向垂直平面的倾斜角度。负内倾角表示车轮顶部向内倾斜,正内倾角表示车轮顶部向外倾斜。

  1. 负内倾角:增加外侧轮胎在转弯时的接地面积,提高抓地力,适合高性能驾驶和赛道使用。

  2. 正内倾角:减少外侧轮胎在转弯时的接地面积,降低抓地力,但可能改善直线行驶的稳定性。

  3. 零内倾角:车轮完全垂直于地面,适合日常驾驶,提供均衡的操控和轮胎寿命。

前束角 (Toe Angle, _toeAngle)

定义:车轮前端相对于车辆横向轴线的偏移角度。前束(Toe-in)表示车轮前端向内靠拢,外倾(Toe-out)表示车轮前端向外分开。

  1. 前束(Toe-in):使车辆在高速行驶时更加稳定,减少转向不足,适合长途驾驶。

  2. 外倾(Toe-out):使车辆更容易转向,增加转向灵敏度,适合赛车和需要快速转向响应的场景。

  3. 零前束角:车轮完全平行于车辆横向轴线,适合大多数日常驾驶情况,提供均衡的操控性能和轮胎寿命。

如果是制作城市等公路使用的普通车辆,此处推荐直接使用默认的 0 内倾角、0 前束角。

魔术公式部分

魔术公式是由荷兰 Delft 理工大学 H.B.Pacejke 教授等人提出并发展起来的,它是用三角函数的组合公式建立的轮胎的纵向力、侧向力和回正力矩的数学模型,因只用一套公式就完整地表达了纯工况下轮胎的力特性,故称为“魔术公式”。

1
2
3
4
[SerializeField] private float _magicFormulaB = 10f;                    // 形状因子(Shape Factor),影响 力-滑移关系曲线 的宽度
[SerializeField] private float _magicFormulaC = 1.65f; // 另一个形状因子,主要控制 力-滑移关系曲线 的峰值位置
[SerializeField, Min(0f)] private float _magicFormulaD = 1.5f; // 峰值因子(Peak Factor),代表了轮胎所能提供的最大侧向力或纵向力的比例
[SerializeField] private float _magicFormulaE = 0.97f; // 曲率因子(Curvature Factor),影响了 力-滑移关系曲线 的斜率

Magic Formula B (_magicFormulaB)

Magic Formula 中的一个形状因子(Shape Factor),它影响了该模型中力-滑移关系曲线的宽度。

  1. 较大的 _magicFormulaB 值会使力-滑移曲线更宽,意味着在较大滑移率下仍然能保持较高的抓地力。

  2. 较小的 _magicFormulaB 值会使曲线更窄,表示轮胎在较小的滑移率范围内就能达到最大抓地力,但超过这个范围后抓地力迅速下降。

Magic Formula C (_magicFormulaC)

Magic Formula 中的另一个形状因子,主要控制力-滑移曲线的峰值位置。

  1. 它决定了轮胎在什么滑移率时能达到最大抓地力。

  2. 对于不同的轮胎和驾驶条件,优化 _magicFormulaC 可以使轮胎在最需要的时候提供最佳抓地力。

Magic Formula D (_magicFormulaD)

Magic Formula 中的峰值因子(Peak Factor),代表了轮胎所能提供的最大侧向力或纵向力的比例。

  1. 较高的 _magicFormulaD 值意味着轮胎能够产生更大的力,即更高的抓地力。

  2. 该值应根据实际轮胎性能进行调整,过高或过低都会导致模拟不准确。

  3. 限制:最小值为 0f,确保非负值。

Magic Formula E (_magicFormulaE)

Magic Formula 中的曲率因子(Curvature Factor),影响了力-滑移曲线的斜率。

  1. 它决定了曲线在接近峰值时的变化速度,从而影响轮胎从线性区域过渡到非线性区域的行为。

  2. 较小的 _magicFormulaE 值可以使曲线更加平缓,表示轮胎在接近极限时仍有较好的渐进性;较大的值则会使曲线更陡峭,表示轮胎一旦超过一定滑移率就迅速失去抓地力。

名词解释:滑移率 (Slip Ratio)

滑移率是指车轮的旋转速度与其线速度之间的差异比率,主要用于描述车轮的纵向滑动情况。它分为两种类型:

  1. 纵向滑移率(Longitudinal Slip Ratio):用于描述刹车或加速时车轮的滑动情况,指车轮在旋转时,其实际速度与理论线速度之间的差异比例。

  2. 侧向滑移率(Lateral Slip Ratio) 或 侧偏角(Slip Angle):用于描述转向时车轮的侧向滑动情况,指车轮沿侧向的速度分量与车辆行驶速度的比例。它反映了车辆转弯时轮胎是否能够跟随预期路径。

名词解释:力-滑移关系曲线

力-滑移关系曲线展示了轮胎产生的力(如侧向力、纵向力)随着滑移率或侧偏角变化的关系。这条曲线是非线性的,并且具有以下特点:

  1. 线性区域:当滑移率或侧偏角较小时,轮胎产生的力与滑移率成正比增加。此时,轮胎处于稳定工作状态,抓地力良好。

  2. 峰值点:随着滑移率或侧偏角的增加,轮胎产生的力达到最大值。这个最大值对应于最佳抓地力点,通常发生在一定的滑移率范围内。

  3. 非线性区域:超过峰值点后,轮胎产生的力迅速下降,即使滑移率继续增加。这表明轮胎已经失去抓地力,进入了打滑或漂移状态。

魔术公式的详细数学原理:

  1. H.B.Pacejke 轮胎模型(魔术公式)

  2. Tire-Road Interaction (Magic Formula)

在此不详细叙述其数学原理部分,Magic Formula B、C、D、E 在公式中的应用会在后续的函数中提及。

轮胎滚动阻力和路面影响

1
2
3
4
5
[SerializeField, Min(0f)] private float _rollingResistanceCoef = 0.015f;    // 轮胎的滚动阻力系数

// 模拟路面颠簸
[SerializeField, Min(0f)] private float _bumpAmplitude = 0f; // 路面上颠簸的最大高度(振幅)
[SerializeField, Min(0f)] private float _bumpPeriod = 0f; // 路面上颠簸之间的距离(波长)

滚动阻力系数 (_rollingResistanceCoef)

定义轮胎的滚动阻力系数,这是一个无量纲的比例因子,用于计算滚动阻力。

  1. 滚动阻力是轮胎在滚动时由于变形、摩擦等因素而产生的阻力。它直接影响车辆的燃油效率和加速性能。

  2. 较高的 _rollingResistanceCoef 值意味着更大的滚动阻力,导致车辆需要更多的能量来维持速度,影响加速性和油耗。

  3. 较低的值则表示较小的滚动阻力,有助于提高燃油经济性和加速性能,但可能降低抓地力。

颠簸幅度 (_bumpAmplitude)

定义路面上颠簸的最大高度(振幅),用于模拟不平整路面的效果。

  1. 这个参数用于模拟道路上的起伏,如减速带、坑洼或其他障碍物。

  2. 较大的 _bumpAmplitude 值表示更剧烈的路面变化,可能导致车辆更频繁和剧烈的上下运动。

  3. 设置为 0f 表示没有明显的路面不平度,适用于模拟平坦的道路条件。

颠簸周期 (_bumpPeriod)

定义路面上颠簸之间的距离(波长),用于控制颠簸的频率。

  1. 这个参数决定了颠簸出现的频率。较大的 _bumpPeriod 值表示颠簸之间的距离较长,车辆经过两次颠簸的时间间隔较大;较小的值则表示颠簸更加密集。

  2. 设置为 0f 表示没有明确的周期性颠簸,适用于模拟随机或无规律的路面状况。

交互部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 用来绑定车轮模型,运行期间将物理模拟的结果传递给车轮模型使其转动
[SerializeField] private Transform _model;

// 调试模式开关,方便进行 Debug
[SerializeField] private bool _debug = true;

// 车轮的刚体
private Rigidbody _rigidbody;

// 自定义 CylinderCaster 类
private CylinderCaster _cylinderCaster;

// 摩擦力最大时的滑移率
private float _peakSlip;

// 判断是否处于地面上
private bool _grounded;
// 接地点信息
private RaycastHit _hitInfo;

// 存储轮胎与地面接触点的位置
private Vector3 _contactPosition;

其中,CylinderCaster 为自定义类,详细信息可以在 自制载具系统:CylinderCaster 中找到

车辆行为内部变量

此部分变量为 WheelCollider2 运行时发生变化的内部变量,用来记录车辆运行时的部分参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private float _suspensionCompression;       // 悬挂压缩量,表示当前悬挂被压缩的程度
private float _suspensionPrevCompression; // 上一帧的悬挂压缩量,用于计算悬挂的变化率

private float _antiRollBarForce; // 防倾杆产生的力,用于抵抗车身侧倾

private Vector3 _forwardDirection; // 车轮前方的方向向量
private Vector3 _sidewaysDirection; // 车轮侧面的方向向量
private float _currentCamberAngle; // 当前车轮的倾角(以弧度为单位)

private float _forwardVelocity; // 车轮沿前进方向的速度分量
private float _sidewaysVelocity; // 车轮沿侧向的速度分量

private float _forwardSlip; // 车轮沿前进方向的滑移率
private float _sidewaysSlip; // 车轮沿侧向的滑移率
private float _slipMagnitude; // 滑移的总大小,即前向和侧向滑移的组合

private float _load; // 作用在车轮上的载荷,影响抓地力
private float _forwardForce; // 作用在车轮上的前向力,如驱动力或阻力
private float _sidewaysForce; // 作用在车轮上的侧向力,如转弯时的离心力

private float _angleInRadian; // 车轮角度(以弧度为单位),可用于计算转向角度
private float _angularVelocity; // 车轮的角速度,用于模拟旋转

private float _steerAngle; // 车轮的转向角度
private float _driveTorque; // 应用于驱动轴的扭矩
private float _brakeTorque; // 应用于制动的扭矩

private float _drivetrainInertia; // 动力传动系统的惯性,影响加速和减速特性
private float _drivetrainFrictionTorque; // 动力传动系统的摩擦扭矩,模拟内部摩擦损失

名词解释:防倾杆

防倾杆(Anti-roll bar),也被称为稳定杆或抗侧倾杆,是一种用于减少车辆在转弯时车身侧倾的悬挂系统组件。它通过连接左右两侧的悬挂来平衡车轮的运动,从而改善车辆的操控性和稳定性。

防倾杆通常由一根扭力杆构成,两端分别连接到车辆两侧的悬挂臂上,并且中间固定在车架或副车架上。当车辆转弯时,外侧悬挂会受到向上的力而拉伸,内侧悬挂则受到向下的力而压缩。防倾杆的作用就是在这两个悬挂之间传递扭转力,以抵抗这种差异,减小车身侧倾的程度。

变量读写权限

可读可写部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// 车轮模型的Transform,用于关联3D模型的位置和旋转。
public Transform Model
{
get => _model;
set => _model = value;
}

// 轮胎半径,最小值被限制为0.001以避免除零错误或不合理的小值。
public float Radius
{
get => _radius;
set => _radius = Mathf.Max(value, 0.001f);
}

// 轮胎惯性矩,最小值被限制为0.001以确保物理计算的稳定性。
public float Inertia
{
get => _inertia;
set => _inertia = Mathf.Max(value, 0.001f);
}

// 轮胎宽度,最小值被限制为0.001以确保合理的尺寸。
public float Width
{
get => _width;
set => _width = Mathf.Max(value, 0.001f);
}

// 圆柱体解析度,决定了轮胎形状的精细程度,最小值被限制为8以保证足够的细节。
public int CylinderResolution
{
get => _cylinderResolution;
set => _cylinderResolution = Mathf.Max(value, 8);
}

// 悬挂弹簧刚度,不允许为负值。
public float SuspensionSpring
{
get => _suspensionSpring;
set => _suspensionSpring = Mathf.Max(value, 0f);
}

// 悬挂压缩阻尼,不允许为负值。
public float SuspensionBump
{
get => _suspensionBump;
set => _suspensionBump = Mathf.Max(value, 0f);
}

// 悬挂回弹阻尼,不允许为负值。
public float SuspensionRebound
{
get => _suspensionRebound;
set => _suspensionRebound = Mathf.Max(value, 0f);
}

// 悬挂行程长度,最小值被限制为0.001以确保悬挂系统有实际作用。
public float SuspensionLength
{
get => _suspensionLength;
set => _suspensionLength = Mathf.Max(value, 0.001f);
}

// 车轮倾角(Camber Angle),范围限制在-45到45度之间,以防止极端角度影响操控。
public float CamberAngle
{
get => _camberAngle;
set => _camberAngle = Mathf.Clamp(value, -45f, 45f);
}

// 车轮外倾角(Toe Angle),同样范围限制在-45到45度之间。
public float ToeAngle
{
get => _toeAngle;
set => _toeAngle = Mathf.Clamp(value, -45f, 45f);
}

// Magic Formula B 参数,直接赋值,没有特别的边界检查。
public float MagicFormulaB
{
get => _magicFormulaB;
set => _magicFormulaB = value;
}

// Magic Formula C 参数,直接赋值,没有特别的边界检查。
public float MagicFormulaC
{
get => _magicFormulaC;
set => _magicFormulaC = value;
}

// Magic Formula D 参数,不允许为负值。
public float MagicFormulaD
{
get => _magicFormulaD;
set => _magicFormulaD = Mathf.Max(value, 0f);
}

// Magic Formula E 参数,直接赋值,没有特别的边界检查。
public float MagicFormulaE
{
get => _magicFormulaE;
set => _magicFormulaE = value;
}

// 滚动阻力系数,不允许为负值。
public float RollingResistanceCoef
{
get => _rollingResistanceCoef;
set => _rollingResistanceCoef = Mathf.Max(value, 0f);
}

// 减震器振幅,不允许为负值。
public float BumpAmplitude
{
get => _bumpAmplitude;
set => _bumpAmplitude = Mathf.Max(value, 0f);
}

// 减震器周期,不允许为负值。
public float BumpPeriod
{
get => _bumpPeriod;
set => _bumpPeriod = Mathf.Max(value, 0f);
}

// 转向角度,直接赋值,没有特别的边界检查。
public float SteerAngle
{
get => _steerAngle;
set => _steerAngle = value;
}

// 驱动扭矩,直接赋值,可以是正值或负值,取决于驱动方向。
public float DriveTorque
{
get => _driveTorque;
set => _driveTorque = value;
}

// 制动扭矩,不允许为负值。
public float BrakeTorque
{
get => _brakeTorque;
set => _brakeTorque = Mathf.Max(value, 0f);
}

// 动力传动系统的惯性,不允许为负值。
public float DrivetrainInertia
{
get => _drivetrainInertia;
set => _drivetrainInertia = Mathf.Max(value, 0f);
}

// 动力传动系统的摩擦扭矩,不允许为负值。
public float DrivetrainFrictionTorque
{
get => _drivetrainFrictionTorque;
set => _drivetrainFrictionTorque = Mathf.Max(value, 0f);
}

// 防倾杆产生的力,直接赋值,可以是任意值。
public float AntiRollBarForce
{
get => _antiRollBarForce;
set => _antiRollBarForce = value;
}

只读部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// 是否与地面接触,用于判断车轮是否处于悬空状态。
public bool Grounded
{
get => _grounded;
}

// 雷达碰撞检测的信息,包含与地面或其他物体碰撞的具体细节。
public RaycastHit HitInfo
{
get => _hitInfo;
}

// 车轮与地面接触点的位置。
public Vector3 ContactPosition
{
get => _contactPosition;
}

// 当前悬挂的实际长度,考虑了悬挂的压缩量。
public float CurrentSuspensionLength
{
get => _suspensionLength - _suspensionCompression;
}

// 悬挂压缩的比例,表示悬挂被压缩的程度相对于其总长度。
public float SuspensionCompressionRate
{
get => _suspensionCompression / _suspensionLength;
}

// 车轮的角速度,用于描述车轮旋转的速度。
public float AngularVelocity
{
get => _angularVelocity;
}

// 车轮的转速(RPM),通过角速度转换而来,单位是每分钟转数。
public float RPM
{
get => _angularVelocity * EngineMath.RPSToRPM; // 假设EngineMath类中有一个RPSToRPM常量或方法
}

// 车轮前方的方向向量,用于计算摩擦力和其他力的方向。
public Vector3 ForwardDirection
{
get => _forwardDirection;
}

// 作用在车轮上的前向力,如驱动力或阻力。
public float ForwardForce
{
get => _forwardForce;
}

// 车轮沿前进方向的滑移率,影响抓地力和操控性。
public float ForwardSlip
{
get => _forwardSlip;
}

// 滑移的总大小,可能是前向和侧向滑移的组合,用于评估轮胎的整体滑移情况。
public float SlipMagnitude
{
get => _slipMagnitude;
}

// 车轮侧面的方向向量,用于计算横向摩擦力和其他力的方向。
public Vector3 SidewaysDirection
{
get => _sidewaysDirection;
}

// 作用在车轮上的侧向力,如转弯时的离心力。
public float SidewaysForce
{
get => _sidewaysForce;
}

// 车轮沿侧向的滑移率,影响抓地力和稳定性。
public float SidewaysSlip
{
get => _sidewaysSlip;
}

// 作用在车轮上的载荷,影响轮胎的抓地力。
public float Load
{
get => _load;
}

// 车轮中心位置,基于悬挂压缩调整后的实际位置。
public Vector3 Center
{
get => transform.position - transform.up * CurrentSuspensionLength;
}

// 车轮的完整旋转状态,包括转向角度、外倾角、倾角以及车轮自身的旋转角度。
public Quaternion Rotation
{
get => transform.rotation // 基础的旋转状态,通常由车辆的整体旋转决定。
* Quaternion.Euler(0f, _steerAngle + _toeAngle, 0f) // 应用了转向角度和外倾角。转向角度 _steerAngle 控制车轮相对于车辆前进方向的偏转,而外倾角 _toeAngle 则是轮胎安装时相对于车辆中心线的小角度调整。
* Quaternion.Euler(0f, 0f, _camberAngle) // 应用了倾角 _camberAngle,它指的是轮胎安装时相对于垂直方向的倾斜角度。正的倾角意味着轮胎顶部向内倾斜。
* Quaternion.Euler(_angleInRadian * Mathf.Rad2Deg, 0f, 0f); // 将弧度表示的车轮旋转角度转换为欧拉角,并应用于车轮旋转。这确保了车轮能够正确地模拟其自旋。
} // Mathf.Rad2Deg 是一个用于将弧度转换为度数的常量。这个常量的值等于 "360 / (PI * 2)",它提供了一个简单的方法来执行弧度到度的转换。

// 最大滑移率,指在特定条件下,轮胎能够提供的最大抓地力对应的滑移率。
public float PeakSlip
{
get => _peakSlip;
}

方法部分

初始化方法 Awake

固定每帧更新方法 FixedUpdate

绘制辅助图形方法 OnDrawGizmos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private void OnDrawGizmos()
{
Gizmos.color = Color.green;

Gizmos.DrawLine(transform.position, Center);

Gizmos.matrix = Matrix4x4.TRS(Center, Rotation, transform.lossyScale);
switch (_sweepType)
{
case SweepType.Ray:
DrawCircle(0f);
Gizmos.DrawLine(Vector3.zero, GetLocalPointOnCircumference(0f, 0f));
break;

case SweepType.Sphere:
Gizmos.DrawWireSphere(Vector3.zero, _radius);
break;

case SweepType.Cylinder:
DrawCircle(-_width / 2f);
DrawCircle(_width / 2f);
Gizmos.DrawLine(Vector3.zero, GetLocalPointOnCircumference(0f, 0f));
break;
}
Gizmos.matrix = Matrix4x4.identity;
}

private void DrawCircle(float x)
{
for (var i = 0; i < GizmosSmoothness; i++)
{
var angleInRad1 = 2f * Mathf.PI * (float)i / (float)GizmosSmoothness;
var angleInRad2 = 2f * Mathf.PI * (float)(i + 1) / (float)GizmosSmoothness;
Gizmos.DrawLine(
GetLocalPointOnCircumference(angleInRad1, x),
GetLocalPointOnCircumference(angleInRad2, x));
}
}

private Vector3 GetLocalPointOnCircumference(float angleInRadian, float x)
{
return new Vector3(
x,
Mathf.Sin(angleInRadian) * _radius,
-Mathf.Cos(angleInRadian) * _radius);
}

绘制悬挂

1
Gizmos.DrawLine(transform.position, Center);

从车轮的当前位置(transform.position)到计算出的车轮中心位置(Center)之间绘制一条直线。

注意:由于 WheelCollider2 需要绑定在一个相对车辆固定的 Empty 物体上,所以此处的 transform.position 实际上获取的是车轮固定在车辆上的位置(相当于初始悬挂位置,不受悬挂影响),而 Center 则代表车轮计算出的当前位置(受到悬挂影响),所以二者连线可以表示悬挂对轮胎的位置影响

设置矩阵以应用 transform 变换

1
Gizmos.matrix = Matrix4x4.TRS(Center, Rotation, transform.lossyScale);

设置当前Gizmos的变换矩阵(Matrix),以应用位置(Translation)、旋转(Rotation)和缩放(Scale)。

  1. TRS 是 “Translation, Rotation, Scale” 的缩写,表示依次应用平移、旋转和缩放。

  2. Center 是之前计算得到的车轮实际中心位置。

  3. Rotation 是车轮的完整旋转状态。

  4. transform.lossyScale 获取了对象的累积缩放值,确保Gizmos正确反映物体的实际大小。

绘制 Gizmos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch (_sweepType)
{
case SweepType.Ray:
DrawCircle(0f);
Gizmos.DrawLine(Vector3.zero, GetLocalPointOnCircumference(0f, 0f));
break;

case SweepType.Sphere:
Gizmos.DrawWireSphere(Vector3.zero, _radius);
break;

case SweepType.Cylinder:
DrawCircle(-_width / 2f);
DrawCircle(_width / 2f);
Gizmos.DrawLine(Vector3.zero, GetLocalPointOnCircumference(0f, 0f));
break;
}

此处根据不同的扫掠类型(SweepType),选择绘制不同形状的Gizmos。

  1. SweepType.Ray:绘制一个圆形,并从原点到轮胎边缘上弧度为 0 处的点绘制一条线,这条线会随着车轮旋转而旋转,可以用来可视化车轮旋转。

  2. SweepType.Sphere:绘制一个空心球体(Wire Sphere),其半径为 _radius。这可以用来可视化轮胎的碰撞体积。

  3. SweepType.Cylinder:绘制两个圆形,分别位于宽度的两侧(-_width / 2f_width / 2f),并从原点到轮胎边缘上弧度为 0 处的点绘制一条线,这条线会随着车轮旋转而旋转,可以用来可视化车轮转动和转过的角度。

重置变换矩阵

1
Gizmos.matrix = Matrix4x4.identity;

可以确保后续的 Gizmos 绘制不会受到之前设置的变换矩阵的影响,让 transform 的变换回到默认的世界坐标系。

DrawCircle(float) 方法

1
2
3
4
5
6
7
8
9
10
11
private void DrawCircle(float x)
{
for (var i = 0; i < GizmosSmoothness; i++)
{
var angleInRad1 = 2f * Mathf.PI * (float)i / (float)GizmosSmoothness;
var angleInRad2 = 2f * Mathf.PI * (float)(i + 1) / (float)GizmosSmoothness;
Gizmos.DrawLine(
GetLocalPointOnCircumference(angleInRad1, x),
GetLocalPointOnCircumference(angleInRad2, x));
}
}

作用:绘制一个圆,其中心位于 (x, 0, 0),半径为 _radius

  1. 循环遍历角度:for 循环迭代 GizmosSmoothness 次,而这决定了圆的平滑度(即使用多少个线段来近似表示圆)。值越大,绘制的圆越平滑。

  2. angleInRad1angleInRad2 分别是当前点和下一个点的角度(以弧度为单位),通过公式 2 * PI * i / GizmosSmoothness 计算得出。这确保了角度均匀分布在一个完整的圆周上(360 度或 2π 弧度)。

  3. 绘制线段:Gizmos.DrawLine 方法用于在两个连续的角度位置之间绘制一条直线。它调用了 GetLocalPointOnCircumference 来获取这些位置的坐标。

GetLocalPointOnCircumference(float angleInRadian, float x) 方法

1
2
3
4
5
6
7
private Vector3 GetLocalPointOnCircumference(float angleInRadian, float x)
{
return new Vector3(
x,
Mathf.Sin(angleInRadian) * _radius,
-Mathf.Cos(angleInRadian) * _radius);
}

作用:根据给定的角度(弧度)和偏移量 x,返回圆周上的一个点的位置(Vector3)。

  1. 构造 Vector3:这个方法使用正弦和余弦函数来计算圆周上的点的 y 和 z 坐标,而 x 坐标则直接使用传入的参数 x。

  2. Mathf.Sin(angleInRadian) * _radius 计算 y 坐标,即从圆心到该点的垂直距离。

  3. -Mathf.Cos(angleInRadian) * _radius 计算 z 坐标,负号确保圆周按预期方向绘制(逆时针)。

  4. 传入的 x 直接作为 x 坐标,使得圆可以沿 x 轴偏移,从而绘制不同位置的圆周。

  5. 返回这个计算出的 Vector3 值。

扫描检测方法 Sweep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private void Sweep()
{
switch (_sweepType)
{
case SweepType.Ray:
_grounded = Physics.Raycast(
transform.position,
-transform.up,
out _hitInfo,
_suspensionLength + _radius);
break;

case SweepType.Sphere:
_grounded = Physics.SphereCast(
transform.position + transform.up * _radius,
_radius,
-transform.up,
out _hitInfo,
_suspensionLength + _radius);
break;

case SweepType.Cylinder:
UpdateCylinderCaster();

_grounded = _cylinderCaster.Cast(
-transform.up,
out _hitInfo,
_suspensionLength + _radius);
break;
}
}

private void UpdateCylinderCaster()
{
if (_sweepType != SweepType.Cylinder) // 如果 SweepType 不为 Cylinder 则直接跳过
{
return;
}

if (_cylinderCaster == null) // 如果 CylinderCaster 对象为空则初始化一个该对象
{
var go = new GameObject("CylinderCast"); // 新建 GameObject 用来挂载 CylinderCaster 的 Monobehaviour
go.transform.parent = transform; // 将新 GameObject 设为挂有 WheelCollider2 的空物体的子物体
go.transform.localPosition = Vector3.up * _radius; // 调整子物体局部坐标,确保生成的圆柱体中心位于轮胎中心
_cylinderCaster = go.AddComponent<CylinderCaster>(); // 挂载 CylinderCaster,此时触发 CylinderCaster 内的 Awake 函数
_cylinderCaster.Init(_radius, _width, _cylinderResolution); // 使用 CylinderCaster 的 Init 方法初始化该新类
}

if (_cylinderCaster.Radius != _radius
|| _cylinderCaster.Width != _width
|| _cylinderCaster.Resolution != _cylinderResolution) // 同时判断三个条件是否符合,确保 CylinderCaster 的参数与实际始终保持一致
{
_cylinderCaster.transform.localPosition = Vector3.up * _radius;
_cylinderCaster.Init(_radius, _width, _cylinderResolution);
}

_cylinderCaster.transform.localEulerAngles = new Vector3( // 更新 CylinderCaster 的本地旋转,使其与车轮的姿态匹配
0f,
_steerAngle + _toeAngle, // 使用车轮传出的 steerAngle 和 toeAngle 更新车轮的转向角和束角
_camberAngle); // 在 Z 向上更新车轮给出的 camberAngle 倾角
}

该方法根据选择的 SweepType 的不同,分别选择不同的方式进行对地面的扫描。

SweepType.Ray 射线式扫描

1
2
3
4
5
6
7
case SweepType.Ray:
_grounded = Physics.Raycast(
transform.position,
-transform.up,
out _hitInfo,
_suspensionLength + _radius);
break;
  1. Physics.Raycast 方法接收到给出的变量,发出一条从 transform.position 开始沿 -transform.up 方向的射线。

  2. 射线的最大长度是 _suspensionLength + _radius,这意味着射线会延伸到悬挂系统的最大伸展长度加上轮子半径的位置。

  3. 只有当物体在空中时,即使悬挂伸长到最大长度轮胎也无法触碰地面。所以如果射线击中了任何碰撞体,则表示物体不在空中, _grounded 变量会被设置为 true,表示物体接地;否则,它保持 false。

  4. out _hitInfo 参数允许获取关于撞击点的额外信息(如位置、法线等),但此处并未获取。

SweepType.Sphere 球体式扫描

1
2
3
4
5
6
7
8
case SweepType.Sphere:
_grounded = Physics.SphereCast(
transform.position + transform.up * _radius,
_radius,
-transform.up,
out _hitInfo,
_suspensionLength + _radius);
break;
  1. Physics.SphereCast 沿着 -transform.up 方向投射一个球体,起始于 transform.position + transform.up * _radius,也就是从轮胎的上边缘点出发,沿向下方向投射一个半径为 _radius 的球体,此时球体中心会正好处于轮胎的几何中心。

  2. 射线的最大长度同样是 _suspensionLength + _radius,这意味着射线会延伸到悬挂系统的最大伸展长度加上轮子半径的位置。

  3. 类似于 Raycast,SphereCast 也会更新 _grounded_hitInfo,基于是否有一个碰撞体在球体路径上。

SweepType.Cylinder 圆柱体式扫描

1
2
3
4
5
6
7
8
case SweepType.Cylinder:
UpdateCylinderCaster();

_grounded = _cylinderCaster.Cast(
-transform.up,
out _hitInfo,
_suspensionLength + _radius);
break;
  1. 在进行圆柱体检测之前,UpdateCylinderCaster() 方法被调用,可能是为了更新圆柱体检测器的状态或参数,使其适应当前物体的姿势或其他属性。

  2. _cylinderCaster.Cast 执行一次圆柱体检测,沿 -transform.up 方向进行,最大检测距离为 _suspensionLength + _radius

  3. 结果同样更新 _grounded_hitInfo

UpdateCylinderCaster() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void UpdateCylinderCaster()
{
if (_sweepType != SweepType.Cylinder) // 如果 SweepType 不为 Cylinder 则直接跳过
{
return;
}

if (_cylinderCaster == null) // 如果 CylinderCaster 对象为空则初始化一个该对象
{
var go = new GameObject("CylinderCast"); // 新建 GameObject 用来挂载 CylinderCaster 的 Monobehaviour
go.transform.parent = transform; // 将新 GameObject 设为挂有 WheelCollider2 的空物体的子物体
go.transform.localPosition = Vector3.up * _radius; // 调整子物体局部坐标,确保生成的圆柱体中心位于轮胎中心
_cylinderCaster = go.AddComponent<CylinderCaster>(); // 挂载 CylinderCaster,此时触发 CylinderCaster 内的 Awake 函数
_cylinderCaster.Init(_radius, _width, _cylinderResolution); // 使用 CylinderCaster 的 Init 方法初始化该新类
}

if (_cylinderCaster.Radius != _radius
|| _cylinderCaster.Width != _width
|| _cylinderCaster.Resolution != _cylinderResolution) // 同时判断三个条件是否符合,确保 CylinderCaster 的参数与实际始终保持一致
{
_cylinderCaster.transform.localPosition = Vector3.up * _radius;
_cylinderCaster.Init(_radius, _width, _cylinderResolution);
}

_cylinderCaster.transform.localEulerAngles = new Vector3( // 更新 CylinderCaster 的本地旋转,使其与车轮的姿态匹配
0f,
_steerAngle + _toeAngle, // 使用车轮传出的 steerAngle 和 toeAngle 更新车轮的转向角和束角
_camberAngle); // 在 Z 向上更新车轮给出的 camberAngle 倾角
}

作用:初始化和更新用于圆柱体检测的 CylinderCaster 组件

  1. 先根据 SweepType 的值来确定是否需要使用该函数,如果不为圆柱体类型则直接跳过该函数。

  2. 初始化 CylinderCaster 类,如果检测不到该 WheelCollider2 对象含有 CylinderCaster 则新建一个并传入设定好的值进行初始化。

  3. 持续判断半径、宽度、分辨率三个条件是否与实际情况符合,如果不符合则重新确定位置并初始化。

  4. 更新 CylinderCaster 的本地旋转,应用车轮的转向、倾角和束角,其中倾角 CamberAngle 为车轮与地面之间的夹角与 90 度的差值,由车轮绕 Z 轴旋转得到;转向角为车辆转向时车轮的偏角,束角 ToeAngle 是指的我们从车辆正上方看车子的时候,轮胎与车辆中轴线形成的夹角,这两个角度由车轮绕 Y 轴(垂直地面轴)旋转得到。

更新轮胎状态方法 UpdateState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private void UpdateStates()
{
_suspensionPrevCompression = _suspensionCompression;

if (_grounded)
{
var localAddForcePos = transform.InverseTransformPoint(_hitInfo.point);
localAddForcePos.x = 0f;
localAddForcePos.z = 0f;
_contactPosition = transform.TransformPoint(localAddForcePos);

var localAngleY = _steerAngle + _toeAngle;
var forward = Quaternion.AngleAxis(localAngleY, transform.up) * transform.forward;
var right = Quaternion.AngleAxis(localAngleY, transform.up) * transform.right;
_forwardDirection = Vector3.ProjectOnPlane(forward, _hitInfo.normal).normalized;
_sidewaysDirection = Vector3.ProjectOnPlane(right, _hitInfo.normal).normalized;

_currentCamberAngle = Vector3.SignedAngle(_hitInfo.normal, transform.up, forward) + _camberAngle;

var vel = _rigidbody.GetPointVelocity(_hitInfo.point);

if (_hitInfo.collider.attachedRigidbody != null)
{
var contactVel = _hitInfo.collider.attachedRigidbody.GetPointVelocity(_hitInfo.point);
vel -= contactVel;
}

_forwardVelocity = Vector3.Dot(vel, _forwardDirection);
_sidewaysVelocity = Vector3.Dot(vel, _sidewaysDirection);

var noiseX = _hitInfo.point.x * _bumpPeriod;
var noiseY = _hitInfo.point.z * _bumpPeriod;
var bump = (Mathf.PerlinNoise(noiseX, noiseY) - 0.5f) * _bumpAmplitude;

_suspensionCompression = _suspensionLength - (_hitInfo.distance + bump - _radius);
}
else
{
_contactPosition = Vector3.zero;

_forwardDirection = Vector3.zero;
_sidewaysDirection = Vector3.zero;

_forwardVelocity = 0f;
_sidewaysVelocity = 0f;

_suspensionCompression = 0f;
}
}

该方法负责更新车轮的各种状态变量,包括悬挂压缩、接触位置、方向向量(前向和侧向)、速度分量以及悬挂压缩的当前值。

保存当前悬挂压缩值

1
_suspensionPrevCompression = _suspensionCompression;

保存当前的悬挂压缩值 _suspensionCompression_suspensionPrevCompression。这使得在后续计算中可以比较当前帧和上一帧的悬挂压缩变化,有助于模拟悬挂系统的动态行为。

更新车轮触地时的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
if (_grounded)                                                                                      
{
// 更新接触位置
var localAddForcePos = transform.InverseTransformPoint(_hitInfo.point);
localAddForcePos.x = 0f;
localAddForcePos.z = 0f;
_contactPosition = transform.TransformPoint(localAddForcePos);

// 计算前向和侧向方向向量
var localAngleY = _steerAngle + _toeAngle;
var forward = Quaternion.AngleAxis(localAngleY, transform.up) * transform.forward;
var right = Quaternion.AngleAxis(localAngleY, transform.up) * transform.right;
_forwardDirection = Vector3.ProjectOnPlane(forward, _hitInfo.normal).normalized;
_sidewaysDirection = Vector3.ProjectOnPlane(right, _hitInfo.normal).normalized;

// 更新当前倾角
_currentCamberAngle = Vector3.SignedAngle(_hitInfo.normal, transform.up, forward) + _camberAngle;

// 计算速度分量
var vel = _rigidbody.GetPointVelocity(_hitInfo.point);

if (_hitInfo.collider.attachedRigidbody != null)
{
var contactVel = _hitInfo.collider.attachedRigidbody.GetPointVelocity(_hitInfo.point);
vel -= contactVel;
}

_forwardVelocity = Vector3.Dot(vel, _forwardDirection);
_sidewaysVelocity = Vector3.Dot(vel, _sidewaysDirection);

// 添加地形起伏效果
var noiseX = _hitInfo.point.x * _bumpPeriod;
var noiseY = _hitInfo.point.z * _bumpPeriod;
var bump = (Mathf.PerlinNoise(noiseX, noiseY) - 0.5f) * _bumpAmplitude;

_suspensionCompression = _suspensionLength - (_hitInfo.distance + bump - _radius);
}

更新接触位置

1
2
3
4
var localAddForcePos = transform.InverseTransformPoint(_hitInfo.point);                         
localAddForcePos.x = 0f;
localAddForcePos.z = 0f;
_contactPosition = transform.TransformPoint(localAddForcePos);

通过获取并修改碰撞点信息更新得到接触位置 _ContactPosition

  1. 使用 transform.InverseTransformPoint(_hitInfo.point) 将碰撞点从世界坐标转换为本地坐标。

  2. 设置 localAddForcePos.xlocalAddForcePos.z 为 0f,确保只保留垂直方向上的偏移。

  3. 使用 transform.TransformPoint(localAddForcePos) 将修改后的本地坐标再转换回世界坐标,得到最终的接触位置。

计算前向和侧向方向向量

1
2
3
4
5
var localAngleY = _steerAngle + _toeAngle;
var forward = Quaternion.AngleAxis(localAngleY, transform.up) * transform.forward;
var right = Quaternion.AngleAxis(localAngleY, transform.up) * transform.right;
_forwardDirection = Vector3.ProjectOnPlane(forward, _hitInfo.normal).normalized;
_sidewaysDirection = Vector3.ProjectOnPlane(right, _hitInfo.normal).normalized;

计算并更新前向方向向量 _forwardDirection 和侧向方向向量 _sidewaysDirection

  1. 定义变量 localAngleY 并设为转向角度和外倾角的总和,在这里称为总转向角度。

  2. 通过 Quaternion.AngleAxis(localAngleY, transform.up) 创建一个绕 Y 轴 (transform.up) 旋转的四元数,表示进行总转向后的方向,将其与原始的前向 transform.forward 相乘,即四元数相乘运算,也就是将总转向角度加在正前方向上,表示从前向转向后的角度,将其定义为新的前向角度。

  3. 同样,将进行总转向后的方向与原始的右向 transform.right 相乘,将总转向角度加在正右侧方向上,表示从原始正右侧转向后的角度,将其定义为新的右侧角度。

  4. 使用 Vector3.ProjectOnPlane 方法将前向和侧向向量分别投影到与碰撞法线垂直的平面上,并进行归一化,以确保它们正确地反映相对于地面的方向,同时避免因为向量长度不一致导致的计算误差,然后分别赋给_forwardDirection_sidewaysDirection

更新当前倾角

1
_currentCamberAngle = Vector3.SignedAngle(_hitInfo.normal, transform.up, forward) + _camberAngle;

更新当前的倾角 _currentCamberAngle

  1. Vector3.SignedAngle(0A, OB, forward) 方法可以理解为得到“两个向量之间的夹角(有符号的)”,或者理解为“将两个向量都放在坐标原点,一个向量要向哪个方向旋转多少度 才能与另一个向量重合。”,通过输入向量 OA 和 OB 以及旋转轴 forward 也就是 Z 轴正向,可以得到向量之间的差角,并且该差角永远是两个差角之间较小的那一个,其值永远在 -180° ~ 180° 之间。

  2. 这里通过 Vector3.SignedAngle(_hitInfo.normal, transform.up, forward) 得到触地点法向方向(也就是该处地面的法线方向)到 WheelCollider2 所处物体的正上方向(也就是载具视角的正上方向)之间绕 Z 轴正向之间的夹角,将计算出来的夹角加上初始设定的 CamberAngle 即得到实际的倾角。

计算速度分量

1
2
3
4
5
6
7
8
9
10
var vel = _rigidbody.GetPointVelocity(_hitInfo.point);

if (_hitInfo.collider.attachedRigidbody != null)
{
var contactVel = _hitInfo.collider.attachedRigidbody.GetPointVelocity(_hitInfo.point);
vel -= contactVel;
}

_forwardVelocity = Vector3.Dot(vel, _forwardDirection);
_sidewaysVelocity = Vector3.Dot(vel, _sidewaysDirection);

计算并更新前向速度 _forwardVelocity 和侧向速度 _sidewaysVelocity

  1. 通过 GetPointVelocity 方法获取到车轮刚体组件与地面碰撞点的速度并赋给变量 vel

  2. 如果车轮碰撞到的物体也存在刚体组件,则通过碰撞到的物体的 GetPointVelocity 方法获取被碰撞物体在同一点的速度,两者相减得到车轮的相对速度。

  3. 将得到的相对速度与前向和侧向两个单位向量进行 Vector3.Dot 点乘方法来分解速度,并分别得到前向分速度和侧向分速度。