You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cannon.js动态体施加X/Z轴力时Y轴弹跳问题求助

彻底解决FPS玩家移动时Y轴弹跳问题

问题根源分析

移动时出现Y轴弹跳,核心原因包括:

  • 旋转90度的地面Box Body会导致碰撞检测的法线计算出现微小误差,引发玩家体的微小Y轴位移
  • 当前通过velocity.y < 0判断玩家是否在空中的逻辑不准确,会错误覆盖Y轴速度
  • 玩家体的碰撞形状和物理参数设置不够优化,导致地面接触时不稳定

具体解决方案

1. 重构地面Body,避免旋转带来的碰撞误差

不要通过旋转Box来制作地面,直接调整Box的尺寸参数匹配需求,让碰撞检测的法线计算更精准:

// 替换原旋转90度的地面创建代码
const groundShape = new CANNON.Box(new CANNON.Vec3(50, 0.5, 50)); // 直接设置Y轴为地面厚度,无需旋转
const groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
groundBody.position.y = -0.5; // 让地面刚好贴合原点下方
world.addBody(groundBody);

2. 优化玩家Body的物理参数与碰撞形状

使用CapsuleShape代替BoxShape,胶囊形在地面移动时的碰撞响应更平滑,同时调整参数增强稳定性:

// Player类中修改Body初始化代码
class Player extends CANNON.Body {
  public moveSpeed: number = 5;

  constructor() {
    super({
      mass: 1,
      shape: new CANNON.Capsule(0.5, 1), // 胶囊形:半径0.5,高度1
      angularDamping: 1,
      linearDamping: 0.1, // 添加线性阻尼,减少速度突变
      fixedRotation: true, // 锁定旋转,避免玩家倾倒
    });
  }

  // ... 保留原有其他方法
}

3. 替换错误的接地判断逻辑,添加可靠的射线检测

移除通过velocity.y < 0判断空中状态的逻辑,改用射线检测玩家是否真的接触地面:

// 在PlayerController类中添加接地检测方法
private isPlayerGrounded(): boolean {
  // 从玩家底部向下发射一条短射线,检测是否接触静态地面
  const rayStart = new CANNON.Vec3(this.player.position.x, this.player.position.y - 1, this.player.position.z);
  const rayEnd = new CANNON.Vec3(this.player.position.x, this.player.position.y - 1.1, this.player.position.z);
  const result = this.player.world.raycast(rayStart, rayEnd);
  
  return result && result.body.mass === 0;
}

// 修改keyboardControls中的速度处理逻辑
keyboardControls() {
  this.camera.position.x = this.player.position.x;
  this.camera.position.y = this.player.position.y + 1; // 适配胶囊形的高度
  this.camera.position.z = this.player.position.z;

  const velocity = new CANNON.Vec3(0, this.player.velocity.y, 0); // 保留原始Y轴速度
  const quaternion = this.cameraControls.getObject().quaternion;
  const forwardVector = new THREE.Vector3(0, 0, -1).applyQuaternion(quaternion);
  forwardVector.y = 0; // 移除Y轴分量,避免移动时带起Y轴速度
  forwardVector.normalize();

  // 移动逻辑
  if (this.keys["w"] || this.keys["W"]) {
    velocity.x += forwardVector.x * this.player.moveSpeed;
    velocity.z += forwardVector.z * this.player.moveSpeed;
  }
  if (this.keys["s"] || this.keys["S"]) {
    velocity.x -= forwardVector.x * this.player.moveSpeed;
    velocity.z -= forwardVector.z * this.player.moveSpeed;
  }
  if (this.keys["a"] || this.keys["A"]) {
    const rightVector = forwardVector.clone().applyAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
    velocity.x += rightVector.x * this.player.moveSpeed;
    velocity.z += rightVector.z * this.player.moveSpeed;
  }
  if (this.keys["d"] || this.keys["D"]) {
    const leftVector = forwardVector.clone().applyAxisAngle(new THREE.Vector3(0, 1, 0), -Math.PI / 2);
    velocity.x += leftVector.x * this.player.moveSpeed;
    velocity.z += leftVector.z * this.player.moveSpeed;
  }

  // 跳跃逻辑:仅接地时允许跳跃
  if (this.keys[" "] && this.isPlayerGrounded()) {
    this.player.applyImpulse(new CANNON.Vec3(0, 5, 0)); // 用冲量实现跳跃,更符合物理规则
  }

  // 速度赋值逻辑:接地时直接覆盖X/Z速度,空中保留惯性
  if (this.isPlayerGrounded()) {
    this.player.velocity.x = velocity.x;
    this.player.velocity.z = velocity.z;
  } else {
    this.player.velocity.x += velocity.x * 0.1;
    this.player.velocity.z += velocity.z * 0.1;
  }
}

4. 修复事件监听的重复绑定问题

当前代码中点击事件内重复绑定键盘事件,会导致多次触发输入异常,修改如下:

constructor(player: Player, camera: THREE.PerspectiveCamera) {
  this.player = player;
  this.camera = camera;
  this.cameraControls = new PointerLockControls(this.camera, document.body);

  document.addEventListener("click", () => {
    this.cameraControls.lock();
  });

  // 将键盘事件监听移到click事件外,避免重复绑定
  document.addEventListener("keydown", (event) => {
    this.keys[event.key] = true;
  });

  document.addEventListener("keyup", (event) => {
    this.keys[event.key] = false;
  });
}

内容的提问来源于stack exchange,提问作者CODERjack

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 07:28:20