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
相关产品推荐
相关产品推荐

