DriftingPlugin

纯逻辑

给动态车物理加漂移响应:检测打滑并改变车控行为。

类别角色 / 载具运动
依赖档位纯逻辑
相关模块
演示场race · 将随该演示场展示(即将上线)
只取这个模块
modules/actor-motion/ground-vehicle/DriftingPlugin.js

连同其内部依赖一并复制,保留相对目录结构。

源码

import { clamp, lerp, smoothToward } from '../../math/ScalarUtils.js';
import { DEFAULT_WORLD_BASIS } from '../../math/WorldBasis.js';

const EPS = 1e-6;
const DRIFT_EFFECT_KEY = 'vehicleDrifting';

function localVelocity(velocity, bodyFrame) {
  const tangent = velocity.clone().projectOnPlane(bodyFrame.up);

  return {
    forward: tangent.dot(bodyFrame.forward),
    right: tangent.dot(bodyFrame.right),
    speed: tangent.length(),
  };
}

function driftWheelMatch(wheel, drift, index) {
  if (Array.isArray(drift.rearWheelIndices)) return drift.rearWheelIndices.includes(index);
  if (Array.isArray(drift.rearWheelNames)) return drift.rearWheelNames.includes(wheel.name);
  return Boolean(wheel.handbrake || (wheel.drive && !wheel.steerable) || /rear/i.test(wheel.name ?? ''));
}

function stateFromDrift(drift = null) {
  const amount = drift ? clamp(drift.amount, 0, 1) : 0;
  const side = drift ? Math.sign(drift.side) : 0;
  return {
    [DRIFT_EFFECT_KEY]: {
      driftAmount: amount,
      driftSide: side,
      drift,
    },
  };
}

export class DriftingPlugin {
  constructor({
    speedForFullDrift = 8,
    steerForFullDrift = 0.18,
    slipAngleFull = 0.58,
    steeringDrift = 0.35,
    handbrakeDrift = 1,
    enterLag = 0.08,
    exitLag = 0.20,
    steeringAssist = 0.18,
    yawAssist = 5.2,
    maxYawRate = 2.8,
    speedForFullYaw = 24,
    rearSideFrictionScale = 0.3,
    frontSideFrictionScale = 1.0,
    rearFrictionSlipScale = 1.0,
    frontFrictionSlipScale = 1.0,
    handbrakeBrakeScale = 0.0,
  }) {
    this.id = 'vehicle-drifting';
    this.cfg = {
      speedForFullDrift,
      steerForFullDrift,
      slipAngleFull,
      steeringDrift,
      handbrakeDrift,
      enterLag,
      exitLag,
      steeringAssist,
      yawAssist,
      maxYawRate,
      speedForFullYaw,
      wheels: {
        rearSideFrictionScale,
        frontSideFrictionScale,
        rearFrictionSlipScale,
        frontFrictionSlipScale,
        handbrakeBrakeScale,
      },
    };
    this.reset();
  }

  reset() {
    this.driftAmount = 0;
    this.driftSide = 0;
    this.forwardSpeed = 0;
    this.rightSpeed = 0;
    return {
      state: {
        driftAmount: this.driftAmount,
        driftSide: this.driftSide,
        drift: null,
      },
    };
  }

  planMovement({ controller, deltaSeconds }) {
    const velocity = localVelocity(controller.velocity, controller.bodyFrame);
    const speedAbs = velocity.speed;
    const steerAbs = Math.abs(controller.steer);
    const slipAngle = Math.atan2(Math.abs(velocity.right), Math.max(1, Math.abs(velocity.forward)));
    const slipDrift = clamp(slipAngle / Math.max(EPS, this.cfg.slipAngleFull), 0, 1);
    const speedDrift = clamp(speedAbs / Math.max(EPS, this.cfg.speedForFullDrift), 0, 1);
    const steerDrift = clamp(steerAbs / Math.max(EPS, this.cfg.steerForFullDrift), 0, 1);
    const steeringDemand = speedDrift * steerDrift * this.cfg.steeringDrift;
    const handbrakeDemand = controller.handbrake
      ? speedDrift * this.cfg.handbrakeDrift
      : 0;
    const driftTarget = clamp(Math.max(slipDrift, steeringDemand, handbrakeDemand), 0, 1);
    const driftAmount = smoothToward(
      this.driftAmount,
      driftTarget,
      driftTarget > this.driftAmount ? this.cfg.enterLag : this.cfg.exitLag,
      deltaSeconds
    );
    const sideSource = Math.abs(controller.inputSteer) > EPS
      ? controller.inputSteer
      : Math.abs(controller.steer) > EPS
        ? controller.steer
        : velocity.right;
    const driftSide = driftAmount > EPS ? Math.sign(sideSource || this.driftSide) : 0;
    const speedScale = clamp(speedAbs / Math.max(EPS, this.cfg.speedForFullYaw), 0, 1.35);
    const drift = {
      amount: driftAmount,
      side: driftSide,
      yawAssist: driftSide * this.cfg.yawAssist * speedScale,
      maxYawRate: this.cfg.maxYawRate,
      steeringAssist: this.cfg.steeringAssist,
      ...this.cfg.wheels,
    };

    this.driftAmount = driftAmount;
    this.driftSide = driftSide;
    this.forwardSpeed = velocity.forward;
    this.rightSpeed = velocity.right;

    return {
      intent: {
        effects: {
          [DRIFT_EFFECT_KEY]: drift,
        },
      },
      state: {
        driftAmount,
        driftSide,
        drift,
      },
    };
  }

  commitMovement({ resolved }) {
    const state = resolved.extensionState[DRIFT_EFFECT_KEY];
    const drift = state.drift;
    this.driftAmount = state.driftAmount;
    this.driftSide = state.driftSide;
    return {
      state: {
        driftAmount: this.driftAmount,
        driftSide: this.driftSide,
        drift,
      },
    };
  }

  applyDynamicCarControls({
    actor,
    controls,
    wheelControls,
    deltaSeconds,
    state,
    basis = DEFAULT_WORLD_BASIS
  }) {
    const drift = controls.effects?.[DRIFT_EFFECT_KEY];
    if (!drift) {
      const driftState = stateFromDrift(null);
      Object.assign(state, driftState);
      return;
    }

    const amount = drift.amount;

    for (const control of wheelControls) {
      const { wheel, index } = control;
      const isRear = driftWheelMatch(wheel, drift, index);
      const sideScale = isRear ? drift.rearSideFrictionScale : drift.frontSideFrictionScale;
      const slipScale = isRear ? drift.rearFrictionSlipScale : drift.frontFrictionSlipScale;
      const sideTarget = wheel.sideFrictionStiffness * sideScale;
      const slipTarget = wheel.frictionSlip * slipScale;

      control.sideFrictionStiffness = lerp(wheel.sideFrictionStiffness, sideTarget, amount);
      control.frictionSlip = lerp(wheel.frictionSlip, slipTarget, amount);
      control.steering *= 1 + amount * drift.steeringAssist;

      if (isRear) {
        control.handbrakeForce *= lerp(1, drift.handbrakeBrakeScale, amount);
        control.brake = control.brakeForce + control.handbrakeForce;
      }
    }

    this.applyYawAssist(actor, drift, deltaSeconds, basis);

    const driftState = stateFromDrift(drift);
    Object.assign(state, driftState);
  }

  applyYawAssist(actor, drift, deltaSeconds, basis = DEFAULT_WORLD_BASIS) {
    const amount = clamp(drift.amount, 0, 1);
    const yawAccel = drift.yawAssist;
    if (deltaSeconds <= 0 || amount <= EPS || Math.abs(yawAccel) <= EPS) return;

    const av = actor.rigidBody.angvel();
    const up = basis.upVector();
    const currentYaw = av.x * up.x + av.y * up.y + av.z * up.z;
    const maxYawRate = Math.max(0, drift.maxYawRate);
    const requestedYawDelta = yawAccel * amount * deltaSeconds;
    let yawDelta = requestedYawDelta;
    if (maxYawRate > 0) {
      const currentAbs = Math.abs(currentYaw);
      const deltaSign = Math.sign(requestedYawDelta);
      if (currentAbs >= maxYawRate && Math.sign(currentYaw) === deltaSign) return;
      const targetYaw = currentYaw + requestedYawDelta;
      if (Math.abs(targetYaw) > maxYawRate && Math.sign(targetYaw) === deltaSign) {
        yawDelta = deltaSign * Math.max(0, maxYawRate - currentAbs);
      }
    }
    if (Math.abs(yawDelta) <= EPS) return;

    actor.rigidBody.setAngvel({
      x: av.x + up.x * yawDelta,
      y: av.y + up.y * yawDelta,
      z: av.z + up.z * yawDelta,
    }, true);
  }

  snapshot() {
    return {
      id: this.id,
      driftAmount: this.driftAmount,
      driftSide: this.driftSide,
      forwardSpeed: this.forwardSpeed,
      rightSpeed: this.rightSpeed,
    };
  }
}

export { DRIFT_EFFECT_KEY as VEHICLE_DRIFTING_EFFECT_KEY };