PAYATHON 2026

Unity WheelCollider 油门松开后仍行驶、起步和制动延迟怎么办?

数据小许

结论

这段代码至少有三处需要修正:距离相等时,motorInput 被连续赋值覆盖;该分支没有重置 brakeInput;松开所有按键时,代码会主动给后轮施加 200f 的制动扭矩。

建议在每个物理步重新计算驱动和制动扭矩,用车辆纵向速度判断前进或倒车状态,不要通过几个参考点之间的距离切换控制模式。

现有代码不足以证明“按住油门多久,松开后就继续加速多久”的原因。松开油门后继续滑行,以及松开后速度仍持续上升,需要分开观察。

代码中可以直接确认的问题

距离相等时,W 的输入被下一行覆盖

原代码的 else 分支是:

motorInput = Input.GetKey(KeyCode.W) ? _maxMotorFroce : 0;
motorInput = Input.GetKey(KeyCode.S) ? _maxMotorFroce * -1 : 0;

只按 W 时,第一行赋予驱动扭矩,第二行又把它改成 0。如果车辆静止时两个距离恰好相等,就可能无法用 W 正常起步。

这个分支也没有给 brakeInput 赋值,会保留上一帧的制动状态。如果之前 brakeInput 非零,进入这个分支后仍可能持续制动。

可以把输入合并成一个方向值,避免连续覆盖:

float driveInput =
    (Input.GetKey(KeyCode.W) ? 1f : 0f) -
    (Input.GetKey(KeyCode.S) ? 1f : 0f);

这样,W 为 1,S 为 -1,同时按下或全部松开时为 0。

距离比较依赖参考点布局,不适合判断行驶方向

原代码把位置和速度直接相加:

Vector3 glawa = points[0].transform.position + rg.velocity;

随后比较两个点到这个位置的距离。结果会受 points 布局影响,也没有明确表达低速或静止状态。位置与速度直接相加,还隐含了一个没有写明的时间尺度。

如果车头方向沿当前对象的局部 Z 正方向,可以用点积获取纵向速度:

float longitudinalSpeed =
    Vector3.Dot(rg.velocity, transform.forward);

结果为正表示沿车头方向运动,为负表示向后运动。接近零时,可以设置一个小范围作为切换阈值,避免微小的速度变化导致状态反复切换。

松开按键后,代码会自动制动

以下逻辑会在无驱动、无制动输入时,对后轮施加制动扭矩:

if (motorInput == 0 && brakeInput == 0)
{
    _rearWheels[i].brakeTorque = 200f;
}

因此,松开按键后后轮出现制动,不一定是 WheelCollider 自行卡住。排查时可以先删除这段逻辑;如果需要松油门减速,再把制动力做成可调参数。

200f 是否合适,需要结合车身质量、轮胎半径、摩擦和其他配置判断。

一个简化的修改示例

下面的版本保留前轮转向、后轮驱动和四轮制动,控制方式如下:

  • W 请求前进,S 请求倒车。
  • 行驶时按相反方向,先制动;纵向速度降到阈值内后,再施加对应方向的驱动扭矩。
  • 松开 W、S 后,驱动和制动扭矩都归零,允许车辆滑行。

示例移除了 points 判断和 Space 修改摩擦的逻辑,方便先验证基础驾驶行为。_brakeForce 和 _directionChangeSpeed 的初始值只是示例,需要根据车辆配置调整。

using TMPro;
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class Car : MonoBehaviour
{
    [SerializeField] private WheelCollider[] _frontWheels;
    [SerializeField] private WheelCollider[] _rearWheels;
    [SerializeField] private TextMeshProUGUI textt;

    [SerializeField] private float _maxMotorFroce = 1000f;
    [SerializeField] private float _maxSteeringAngle = 30f;
    [SerializeField] private float _brakeForce = 2000f;
    [SerializeField] private float _directionChangeSpeed = 0.5f;

    private Rigidbody rg;
    private float driveInput;
    private float steeringInput;

    private void Awake()
    {
        rg = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        driveInput =
            (Input.GetKey(KeyCode.W) ? 1f : 0f) -
            (Input.GetKey(KeyCode.S) ? 1f : 0f);

        steeringInput =
            Input.GetAxisRaw("Horizontal") * _maxSteeringAngle;

        if (textt != null)
        {
            textt.text = (rg.velocity.magnitude * 3.6f).ToString("F1");
        }
    }

    private void FixedUpdate()
    {
        float longitudinalSpeed =
            Vector3.Dot(rg.velocity, transform.forward);

        float motorTorque = 0f;
        float brakeTorque = 0f;

        bool wantsOppositeDirection =
            (driveInput > 0f && longitudinalSpeed < -_directionChangeSpeed) ||
            (driveInput < 0f && longitudinalSpeed > _directionChangeSpeed);

        if (wantsOppositeDirection)
        {
            brakeTorque = _brakeForce;
        }
        else
        {
            motorTorque = driveInput * _maxMotorFroce;
        }

        foreach (WheelCollider wheel in _frontWheels)
        {
            wheel.steerAngle = steeringInput;
            wheel.motorTorque = 0f;
            wheel.brakeTorque = brakeTorque;
            UpdateWheelPose(wheel);
        }

        foreach (WheelCollider wheel in _rearWheels)
        {
            wheel.motorTorque = motorTorque;
            wheel.brakeTorque = brakeTorque;
            UpdateWheelPose(wheel);
        }
    }

    private void UpdateWheelPose(WheelCollider wheel)
    {
        if (wheel.transform.childCount == 0)
        {
            return;
        }

        Transform child = wheel.transform.GetChild(0);
        wheel.GetWorldPose(out Vector3 position, out Quaternion rotation);
        child.position = position;
        child.rotation = rotation;
    }
}

使用前,需要在 Inspector 中正确设置前后轮引用,并确认 _brakeForce 大于零。还要检查已有组件上的序列化值,不能只看代码中的初始值。

示例沿用原代码的旧版输入 API 和 rg.velocity 写法,适用于项目仍支持这些 API 的情况。它还假定车辆局部 Z 正方向就是车头方向,且前后轮数组互不重复。

怎样确认松开油门后是否还在驱动

在 FixedUpdate() 中观察实际写入的 motorTorque、brakeTorque 和纵向速度。可以临时在示例计算扭矩之后添加:

Debug.Log(
    $"input={driveInput:F1}, " +
    $"motor={motorTorque:F1}, " +
    $"brake={brakeTorque:F1}, " +
    $"speed={longitudinalSpeed:F2}");

松开 W、S 后,示例中的 driveInput、motorTorque 和 brakeTorque 都应归零。检查后移除日志,避免持续输出影响运行表现。

如果驱动扭矩已归零,车辆继续前进可能只是惯性滑行。若在平坦地面上速度仍持续增加,再检查是否有其他脚本写入 motorTorque、修改刚体速度或施加力,同时检查车轮接地、摩擦和物理运行情况。

原代码的油门使用 Input.GetKey,没有轴平滑处理;Input.GetAxis("Horizontal") 影响的是转向。因此,不能仅凭松开油门后的现象,把原因归结为输入轴平滑。

Space 的摩擦修改需要单独处理

原代码按下 Space 时降低后轮的 forwardFriction.stiffness 和 sidewaysFriction.stiffness,松开时再恢复。这会改变轮胎抓地表现,但没有施加手刹制动扭矩。

如果 Space 的用途是手刹,应另外定义后轮 brakeTorque,并在同一处计算最终制动值,避免多个逻辑互相覆盖。需要配合降低摩擦时,建议保存每个车轮的原始摩擦参数,松开后直接恢复,减少对按下、松开事件严格配对的依赖。

先在固定摩擦参数下验证起步、滑行、制动和换向,再加入 Space 的效果,更容易判断异常来自哪里。

备注:内容仅供参考。