29 · 角色移动、追踪与平滑转向

返回系列目录

需求与运动模型

角色沿地面以每秒 2 个世界单位走向目标,到达后停止;身体最多每秒旋转 180°。位置更新和旋转更新是两个问题,不能用同一个固定插值系数代替速度。

令当前位置为 p,目标为 q,距离 d = |q − p|,单位方向 u = (q − p) / d。本帧移动量为:

Δp=umin⁡(vΔt,d). \Delta p=u\min(v\Delta t,d).

min 防止跨过目标。dt 使用秒,而不是毫秒。接近零距离时没有可靠方向,应停止归一化和朝向更新。示例约定角色本地 +Z 是前方,+Y 是上方。

完整核心代码

import { createDemo } from './runtime.js';

const { THREE, scene, onFrame } = createDemo();
const actor = new THREE.Group();
const body = new THREE.Mesh(
  new THREE.BoxGeometry(0.6, 1, 0.8).translate(0, 0.5, 0),
  new THREE.MeshNormalMaterial()
);
actor.add(body);
actor.add(new THREE.ArrowHelper(new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 1, 0), 1));
scene.add(actor, new THREE.GridHelper(20, 20));
const target = new THREE.Vector3(4, 0, -3);
const marker = new THREE.Mesh(new THREE.SphereGeometry(0.15), new THREE.MeshBasicMaterial({ color: 0xff6644 }));
marker.position.copy(target);
scene.add(marker);
const direction = new THREE.Vector3();
const up = new THREE.Vector3(0, 1, 0);
const targetRotation = new THREE.Quaternion();
const speed = 2;
const angularSpeed = Math.PI;

onFrame((dt) => {
  direction.subVectors(target, actor.position);
  direction.y = 0;
  const distance = direction.length();
  if (distance < 1e-5) return;
  direction.divideScalar(distance);
  actor.position.addScaledVector(direction, Math.min(speed * dt, distance));
  const yaw = Math.atan2(direction.x, direction.z);
  targetRotation.setFromAxisAngle(up, yaw);
  actor.quaternion.rotateTowards(targetRotation, angularSpeed * dt);
});

这里角色直接挂在 scene 下,位置采用世界坐标。带变换父节点时,应先在世界空间计算新位置,再 parent.worldToLocal();目标世界四元数也应转换为父空间。存在非均匀缩放和剪切时,简单四元数转换不足以完整描述变换,建议用不带非均匀缩放的独立运动根节点承载角色。

跟随与插值不能混为一谈

匀速追踪适合有明确移动速度的角色。摄像机缓慢追上目标更适合指数平滑:

α=1−e−λΔt,p←lerp⁡(p,q,α). \alpha=1-e^{-\lambda\Delta t},\qquad p\leftarrow\operatorname{lerp}(p,q,\alpha).

例如 actor.position.lerp(target, 1 - Math.exp(-5 * dt))。在目标静止时,这种形式对时间步划分一致;它渐近到达目标,通常需要距离阈值,不保证恒定速度。每帧固定 0.1 则会随刷新率改变结果。

当前代码允许角色侧移一小段,同时身体慢慢转向。车辆不能侧移时,应按身体当前前向移动,而不是直接按目标方向移动,并加入转弯半径或转向模型。

验证与练习(暂不提供答案)

验证不同刷新率下的到达时间;目标正好等于当前位置时不应出现 NaN;将目标放到角色身后,观察最短方向转向。

  1. 用键盘输入控制角色,防止斜向移动比单轴更快。
  2. 把模型前向改成 −Z,保持控制行为正确。
  3. 让目标持续移动,对比匀速追踪和指数平滑。
  4. 加入带旋转父组,仍以世界速度每秒 2 单位移动。