UE5.8 载具物理与 Chaos Vehicle

拉力赛 Demo 里的那辆越野车

阿杰最近在做一个开放世界拉力赛 Demo。赛道穿过泥地、碎石坡和一段废弃矿场,玩家要驾驶一辆改装越野车冲过终点。他用 Skeletal Mesh 搭了车身,给四个轮子绑了骨骼,挂上 UWheeledVehicleMovementComponent,又在蓝图中把油门、转向、刹车连到 Enhanced Input。第一次跑测试时,车子直线加速还算不错,可一到弯道就侧滑冲出赛道,遇到小坡直接腾空翻滚,着地时底盘卡在石头缝里再也动不了。

阿杰一开始以为是美术给的碰撞体太粗糙,把底盘的 Convex Decomposition 调到最高,结果车变得更颠,低速转弯时还会原地打转。后来他意识到,问题出在车辆物理参数的理解上:轮胎摩擦曲线、悬挂刚度、差速器分配、质量分布,每一项都会改变整辆车的性格。这篇文章就从这辆越野车的调试过程出发,把 UE5.8 Chaos Vehicle 的核心机制串一遍。

Chaos Vehicle 与 Wheeled Vehicle Movement Component

UE5 早期版本的载具系统还保留了一部分 PhysX Vehicle 的封装,到了 UE5.8,Chaos Vehicle 已经成为默认且推荐的方案。Chaos Vehicle 把整车拆成几个核心对象:代表车身的 Physics Body、代表每个车轮的 Wheel 实例、把车身与车轮联系起来的 Suspension 约束,以及负责汇总计算并输出速度的 UWheeledVehicleMovementComponent

flowchart TD
    subgraph vehicle ["Chaos Vehicle Actor"]
        mesh["Skeletal Mesh Component"]
        movement["Wheeled Vehicle Movement Component"]
        body["Vehicle Physics Body"]
        wheel1["Wheel FL"]
        wheel2["Wheel FR"]
        wheel3["Wheel RL"]
        wheel4["Wheel RR"]
    end
    subgraph chaos ["Chaos Solver"]
        solver["Vehicle Solver"]
        broad["Broadphase"]
        narrow["Narrowphase"]
    end
    mesh --> movement
    movement --> body
    body --> wheel1
    body --> wheel2
    body --> wheel3
    body --> wheel4
    body --> solver
    solver --> broad
    broad --> narrow

UWheeledVehicleMovementComponent 并不是直接驱动骨骼旋转,而是每帧向 Chaos 提交一组目标值:油门请求 TreqT_{req}、转向角度请求 δreq\delta_{req}、刹车请求 BreqB_{req}、手刹请求 HreqH_{req}。Chaos 在物理线程里根据这些请求、轮胎与地面的接触情况、悬挂压缩量,反推出车身应该受到的力和力矩,再积分得到下一帧的位置与速度。渲染线程拿到新的车身位姿后,由动画蓝图根据车轮半径和打滑程度计算骨骼旋转,玩家才看到轮胎在转。

这种分层把 Gameplay 意图和物理响应拆开:Gameplay 代码描述玩家希望车辆做什么,物理代码计算世界实际会如何反馈。阿杰最早犯的错误之一,是在 Tick 里直接设置车轮骨骼的角度,结果和 Movement Component 的输出打架,车子一抖一抖地前进。正确的做法是把所有控制都交给 Movement Component,只在动画蓝图中读取它的输出状态。

车辆蓝图搭建

搭一辆可驾驶的车,通常从 Skeletal Mesh 开始。车身是根骨骼,四个轮子各自有独立骨骼,命名可以自定义,但要在 UWheeledVehicleMovementComponent 的 Wheel Setups 数组里正确对应。数组长度决定了车辆是四轮、六轮还是八轮,引擎内部会为每一项创建一个 UVehicleWheel 子类实例。

阿杰的越野车用了四轮,Wheel Setups 配置如下:

索引Bone NameOffsetWheel Class
0Wheel_FL(70, -80, -40)UDemoFrontWheel
1Wheel_FR(70, 80, -40)UDemoFrontWheel
2Wheel_RL(-70, -80, -40)UDemoRearWheel
3Wheel_RR(-70, 80, -40)UDemoRearWheel

Offset 是车轮相对于车身坐标原点的悬挂静止位置。这个值不决定美术骨骼的位置,只告诉物理系统"如果悬挂没有压缩,轮子应该在哪里"。如果填错,车子会整体浮空或陷进地面。

flowchart TD
    A["导入带骨骼的车辆 Skeletal Mesh"] --> B["创建 UWheeledVehicleMovementComponent"]
    B --> C["配置 Wheel Setups 数组"]
    C --> D["创建 UVehicleWheel 子类并指定前后参数"]
    D --> E["设置 Collision Mesh 与 Mass"]
    E --> F["创建动画蓝图读取车轮状态"]
    F --> G["连接 Enhanced Input 到 Movement Component"]
    G --> H["运行并调参"]

质量分布是下一步关键。阿杰把车身总质量设为 1600kg1600\,\text{kg},质心高度 zz 方向调低约 15cm15\,\text{cm}。质心太高会导致转弯侧翻,太低会让车像贴地飞行器一样缺乏侧倾感。他反复试了几组值,最后把质心放在前后轴中间偏前 5cm5\,\text{cm}、离地约 55cm55\,\text{cm} 的位置,直线加速和入弯姿态都比较自然。

碰撞体方面,Chaos Vehicle 需要 Simple Collision 参与物理,Complex Collision 只用于查询。阿杰给车身做了一个包裹形状的 Convex Hull,底盘部分额外加了一个略低的碰撞盒,用来检测是否会卡在地形凸起上。轮拱附近要留出足够空间,否则悬挂压缩时轮胎会穿透车身。

轮胎摩擦与悬挂

轮胎是车辆与地面的唯一接触点,也是手感的主要来源。Chaos Vehicle 用摩擦椭圆或简化的 Pacejka 式曲线描述轮胎侧向力与纵向力的关系。UVehicleWheel 里的 Friction Force Multiplier 和 Tire Config 里的曲线共同决定抓地力。

阿杰的拉力赛 Demo 里有三种路面:柏油路、泥地和碎石。他创建了三种 Physical Material,分别设置不同的摩擦系数 μ\mu。柏油路 μ0.9\mu \approx 0.9,泥地 μ0.45\mu \approx 0.45,碎石 μ0.55\mu \approx 0.55。Movement Component 在物理 tick 中通过轮下射线检测拿到地面的 Physical Material,再查表得到当前摩擦系数。

轮胎能提供的最大合成力可以用一个简单的摩擦圆近似:

(FxμFz)2+(FyμFz)21\left(\frac{F_x}{\mu F_z}\right)^2 + \left(\frac{F_y}{\mu F_z}\right)^2 \le 1

其中 FxF_x 是纵向力,FyF_y 是侧向力,FzF_z 是轮胎垂直载荷。这意味着同时大脚油门和猛打方向会突破摩擦极限,车子开始侧滑或甩尾。阿杰把转向灵敏度曲线在低速时调得陡峭,高速时调得平缓,避免高速下轻微拨动手柄就让车辆失控。

悬挂系统负责吸收路面冲击并保持轮胎贴地。每个车轮通过一条射线或一个形状与车身相连,悬挂静止长度 L0L_0、压缩刚度 kk、阻尼系数 cc 共同决定悬挂行为。Chaos 里常用的弹簧阻尼力公式是:

Fsuspension=k(L0L)cL˙F_{suspension} = k\,(L_0 - L) - c\,\dot{L}

LL 是当前悬挂长度,L˙\dot{L} 是压缩速度。阿杰最初把 kk 设得很高,车子在碎石路上不停地弹跳,像一块铁板。降低刚度、提高阻尼后,车身起伏变得柔和,轮胎能更长时间贴地。前后悬挂参数也可以不同:前轴稍硬能减少刹车点头,后轴稍软能在加速时让车尾下沉、增加驱动轮抓地力。

Anti-Roll Bar 用于抑制侧倾。阿杰给前后轴都加了横向稳定杆,数值不大,刚好让高速过弯时车身有可见但可控的侧倾。没有稳定杆的车像船一样左右摇晃,数值太大则内侧轮胎会离地,反而损失抓地力。

graph LR
    A["路面 Physical Material"] --> B["摩擦系数查询"]
    B --> C["轮胎最大合成力"]
    C --> D{当前力是否超限}
    D -->|"是"| E["进入滑移状态"]
    D -->|"否"| F["保持抓地"]
    E --> G["悬挂参数影响载荷转移"]
    F --> G
    G --> H["车身加速度与姿态"]

引擎、变速箱、差速器

UWheeledVehicleMovementComponent 把动力源抽象成 Engine Config。关键参数包括最大功率 PmaxP_{max}、最大扭矩曲线、最大转速 ωmax\omega_{max} 和惯性。阿杰的越野车用了 220kW220\,\text{kW} 峰值功率,转速红线 6500rpm6500\,\text{rpm}。扭矩曲线在低转速区饱满,起步有力;高转速区逐渐下降,逼迫玩家升档。

变速箱参数决定档位数量、齿比和换档时机。Chaos Vehicle 支持自动档和手排档。阿杰给 Demo 做了六前速自动档,齿比逐级收紧,最高档偏向极速。Gear Switch Time 设得较短,换挡冲击明显; civilian 驾驶游戏通常会把换挡时间拉长、冲击调小。

差速器决定左右车轮之间的扭矩分配。前驱车常用 Open Differential,左右轮扭矩相等,内侧轮打滑时动力流失严重;性能车常用 Limited Slip Differential,可以限制打滑轮转速差,把更多扭矩传给抓地轮;越野车则常用 Locking Differential,完全锁死左右轮。阿杰给后桥设了 LSD,前桥保持 Open,这样在泥地漂移时车尾容易甩起来,车头仍保留一定指向性。

差速器锁定率 LL 与左右轮转速差 Δω\Delta\omega 的关系可以粗略写成:

Tlock=LklsdΔωT_{lock} = L \cdot k_{lsd} \cdot \Delta\omega

TlockT_{lock} 是差速器额外传递给慢轮的扭矩,klsdk_{lsd} 是 LSD 刚度系数。L=0L=0 就是 Open Differential,L=1L=1 接近完全锁止。阿杰把后桥 LSD 锁定率设在 0.450.45 左右,既能做出可控的甩尾,又不会让低速转弯变得像坦克一样费劲。

四轮驱动还需要分配前后轴扭矩。默认 50/50 适合稳定巡航,70/30 偏向后驱更有驾驶乐趣,30/70 偏向前驱更稳但容易推头。阿杰根据赛道不同路段动态修改这个比例,直道偏后驱提高加速效率,弯道偏四驱增加稳定性。

载具输入映射

UE5.8 推荐使用 Enhanced Input 处理车辆控制。阿杰把操作拆成几个独立 Action:IA_ThrottleIA_SteerIA_BrakeIA_HandbrakeIA_Look。每个 Action 绑定到游戏手柄或键盘的不同输入源,Scale 和 Curve 在 Input Mapping Context 里配置。

输入值经过处理后喂给 Movement Component:

void ADemoVehicle::Throttle(const FInputActionValue& Value)
{
    float Input = Value.Get<float>();
    GetVehicleMovementComponent()->SetThrottleInput(Input);
}

void ADemoVehicle::Steer(const FInputActionValue& Value)
{
    float Input = Value.Get<float>();
    GetVehicleMovementComponent()->SetSteeringInput(Input);
}

void ADemoVehicle::Brake(const FInputActionValue& Value)
{
    float Input = Value.Get<float>();
    GetVehicleMovementComponent()->SetBrakeInput(Input);
}

油门和刹车通常不需要 Smoothing,玩家希望输入立刻反映;转向则需要根据车速做曲线映射。阿杰在蓝图中加了一个 FMath::InterpTo,把原始输入平滑成目标转向角,时间常量约 0.08s0.08\,\text{s}。转向回正也做了处理:当玩家松开方向键,前轮会自动回到中立位置,模拟方向盘回正力矩。

sequenceDiagram
    participant P as Player
    participant E as "Enhanced Input"
    participant V as "Vehicle Pawn"
    participant M as "Movement Component"
    participant C as "Chaos Solver"
    P->>E: 推动左摇杆 / 按键
    E->>V: IA_Throttle / IA_Steer
    V->>M: SetThrottleInput / SetSteeringInput
    M->>C: 提交目标油门、转向角
    C->>C: 物理 Tick 求解轮胎力
    C-->>M: 返回车身速度、车轮转速
    M-->>V: 更新动画状态

手刹在拉力赛里特别重要。阿杰把后轮的 Handbrake Torque 调得比较高,拉动手刹时后轮抱死,车尾向外甩出,配合反打方向可以做出惯性漂移。如果手刹只作用于前轮,那基本等同于普通刹车,不会有甩尾效果。

载具碰撞与翻车恢复

车辆高速撞上障碍物或从空中砸向地面时,Chaos 会计算车身与环境的碰撞冲量。阿杰发现,如果碰撞体质量太轻,车辆会被小石头弹飞;如果太重,撞树时车纹丝不动,树反而被撞倒。他最终把车质量定在 1600kg1600\,\text{kg},障碍物根据真实材质设置质量,碰撞结果看起来可信。

翻车是越野 Demo 的常见问题。阿杰在几个地方做了处理:

第一,质心高度控制。质心越低越不容易翻,但也会减少越野感。他把质心高度、轮距、悬挂行程综合考虑,让车辆在正常驾驶下有合理侧倾,但在极端情况下仍可能翻车。

第二,碰撞恢复。车辆翻滚时,Chaos 的约束求解器会持续工作,车身会尽量按物理规律停下。阿杰给车身加了 Angular Damping,翻滚速度逐渐衰减,避免车子像陀螺一样转个不停。

第三,自动扶正。如果车辆翻倒后卡住,玩家会很难受。阿杰加了一个简单的翻车检测:当车身 Up 向量与世界 Up 向量的点积持续小于 0.30.3 超过 2s2\,\text{s},且车辆几乎静止时,触发扶正逻辑。扶正不是瞬间传送,而是在短时间里给车身施加一个逐渐增大的上推力矩和向上的线速度,让它自然翻回四轮着地。

void ADemoVehicle::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    FVector Up = GetActorUpVector();
    float Dot = FVector::DotProduct(Up, FVector::UpVector);
    float Speed = GetVelocity().Size();

    if (Dot < 0.3f && Speed < 50.0f)
    {
        StuckTime += DeltaTime;
        if (StuckTime > 2.0f)
        {
            FVector Torque = FVector::CrossProduct(Up, FVector::UpVector) * RollTorqueStrength;
            Mesh->AddTorqueInRadians(Torque);
            Mesh->AddImpulse(FVector::UpVector * RollImpulseStrength);
            StuckTime = 0.0f;
        }
    }
    else
    {
        StuckTime = 0.0f;
    }
}

这段代码只是示例,实际项目里可以用更平滑的插值,或者让玩家按某个按键主动扶正车身。阿杰的 Demo 选择了自动扶正,因为赛道里有很多陡坡和岩石,玩家翻车频率较高。

性能与网络同步

物理载具对性能的影响主要来自几个方面:车身刚体、四个车轮、悬挂射线检测、轮胎摩擦计算、传动系统求解。Chaos 把这些都放进了物理线程,游戏线程只负责输入和读取结果。阿杰在测试时发现,同屏二十辆车时帧时间还能接受,超过四十辆后物理线程开始吃紧。

优化手段包括:降低远处车辆的物理 tick 频率、关闭非玩家车辆的 Detailed Suspension、用 LOD 切换简化碰撞体、减少同时开启 Simulate Physics 的车辆数量。对于背景 traffic,可以用简化的运动学车辆或样条线移动,只在靠近玩家时才启用真实物理。

网络同步方面,车辆是最难同步的物理对象之一。车身位置、旋转、速度、角速度、车轮转速、档位、刹车灯状态都需要在不同客户端之间达成一致。UE5.8 提供了 UCharacterMovementComponent 式的网络预测给角色,但车辆通常用更传统的 Server-Client 模型。

阿杰的实现方式是:客户端本地模拟车辆,同时把输入样本发送给服务器;服务器权威模拟,定期把车身 Transform 和车辆状态回传给客户端;客户端用插值和平滑误差修正把服务器状态融合进本地预测。关键状态如速度、档位、手刹是否拉起,用 RepNotify 同步,保证所有玩家看到一致的漂移烟雾和轮胎痕迹。

graph TD
    A["Client 本地输入"] --> B["发送 Input Sample 到 Server"]
    B --> C["Server 权威物理模拟"]
    C --> D["同步 Transform 与状态"]
    D --> E["Client 接收服务器状态"]
    E --> F["与本地预测对比"]
    F -->|"误差小"| G["平滑插值修正"]
    F -->|"误差大"| H["直接拉回并重置预测"]
    G --> I["渲染最终姿态"]
    H --> I

对于高速竞速,建议开启 Network Prediction for Vehicles 相关实验功能,让客户端预测更激进,服务器只做校验。不过 UE5.8 中这部分仍在改进,正式上线前需要做大量测试。

UE5.8 载具改进

UE5.8 在 Chaos Vehicle 上做了不少打磨。首先是默认模板和示例工程的更新,新建 Vehicle 项目时会直接生成 Chaos Vehicle 的蓝图结构,不再混杂 PhysX Vehicle 的旧资产。文档和工具提示也更加统一,阿杰在调参时少走了很多弯路。

物理线程与游戏线程的同步延迟进一步降低。Chaos 的 Determinism 模式在车辆相关场景下更稳定,录制下来的物理回放与实时运行的偏差明显减小。这对需要回放系统或反作弊校验的竞速游戏很有帮助。

轮胎模型新增了更多可调曲线,包括 Camber Sensitivity、Load Sensitivity 和 Rolling Resistance。阿杰用 Load Sensitivity 模拟了重载车辆在碎石路上的抓地力衰减:垂直载荷越大,轮胎单位载荷能提供的侧向力反而略有下降,这让满载货物的货运车辆在弯道里明显比空车笨重。

悬挂可视化工具也得到了加强。编辑器里可以直接看到每根悬挂的压缩量、轮胎与地面接触点、当前摩擦系数。阿杰打开 Debug Draw 后,终于明白为什么他的车前轮经常在急刹时离地:前悬挂刚度太高,刹车重心前移时前悬挂几乎没有压缩行程,内侧前轮被顶离地面。

最后,Chaos Vehicle 与 Destruction、Fluid、Niagara 的集成更顺畅。车辆碾过可破坏围栏时,碎片会根据车轮位置和速度被正确推开;溅起的泥水可以用 Niagara 读取轮胎滑移率来触发,泥地漂移时会甩出大量泥浆。

结语

阿杰的拉力赛 Demo 跑了三个月,车辆手感终于从僵硬死板变得有几分真实。回头看,最关键的几步反而是最朴素的:把质心高度降下来、把悬挂阻尼调合适、把前后摩擦差做清楚、把差速器锁定率设合理。Chaos Vehicle 提供了一整套参数,但参数之间互相耦合,调参没有银弹,只能在具体赛道上反复试。

这篇文章覆盖了 UE5.8 Chaos Vehicle 的搭建流程、轮胎与悬挂、动力传动、输入处理、碰撞翻车、性能网络和版本改进。希望这些来自实际 Demo 的经验,能帮你在自己的载具项目里少翻几次车。调车这件事,本质上是把一堆互相牵制的参数拨到平衡位置,没什么捷径,只能在赛道上反复跑、反复改。每一组悬挂刚度、每一个差速器比例、每一条摩擦曲线,都会在不同车速和路面组合下暴露出新问题。阿杰的越野车现在能稳稳碾过碎石坡,也能在泥地里甩出漂亮的弧线,正是这些琐碎调整堆出来的结果。