Skip to content

202. 综合实战:开放世界(三)

Published:

上节实现了镜头跟随和前后左右移动:

2025-12-07 17.51.05.gif

这节加一下方向的控制。

这个我们之前写过,就是鼠标左右移动,控制人物盒相机的方向:

image.png

用 requestPointerLock 来申请鼠标锁定模式,这样可以无限左右滑动。

并且加一个变量记录这个状态,在 pointerlockchange 的时候更新这个变量。

这样我们就可以只在进入鼠标锁定模式之后再响应移动改变鼠标方向。

然后 mousemove 的时候,根据移动的距离来计算相机旋转角度,并限制一个范围。

let isPointerLocked = false;

const minCameraAngle = THREE.MathUtils.degToRad(-20);
const maxCameraAngle = THREE.MathUtils.degToRad(20);

document.addEventListener('mousedown', () => {
  document.body.requestPointerLock();
});

document.addEventListener('pointerlockchange', () => {
  isPointerLocked = document.pointerLockElement === document.body;
});

document.addEventListener('mousemove', (e) => {
  if (!isPointerLocked || !characterModel) return;

  characterModel.rotation.y -= e.movementX / 500;

  camera.rotation.x -= e.movementY / 500;

  if (camera.rotation.x > maxCameraAngle) {
    camera.rotation.x = maxCameraAngle;
  }
  if (camera.rotation.x < minCameraAngle) {
    camera.rotation.x = minCameraAngle;
  }
});

试下效果:

2025-12-07 19.14.20.gif

这样,控制方向就完成了。

但是现在人物是会穿墙的:

2025-12-07 19.16.25.gif

我们希望能和真实世界一样,碰到墙无法穿过。

这种怎么实现呢?

很明显,要用物理引擎 cannon。

安装下:

pnpm install --save cannon-es

然后分别加一下地面和各种静止物体的刚体:

先创建物理世界:

image.png

import * as CANNON from 'cannon-es';

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);

先加一下地面的:

image.png

const groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(new CANNON.Plane());
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(groundBody);

地面的质量为 0,也就是不移动。

然后是 box 的:

image.png

这个也质量为 0,也就是不可移动

const body = new CANNON.Body({
    mass: 0,
    position: new CANNON.Vec3(x, y, z)
});
body.addShape(new CANNON.Box(new CANNON.Vec3(width / 2, height / 2, depth / 2)));
world.addBody(body);

最后再加一下人物的:

image.png

// 玩家物理体(圆柱体,模拟人物碰撞体积)
const playerRadius = 0.25;
const playerHeight = 1.8;
const playerBody = new CANNON.Body({
  mass: 80,
  position: new CANNON.Vec3(0, playerHeight / 2, 0),
  linearDamping: 0.9,
  angularDamping: 0.99,
  fixedRotation: true
});
playerBody.addShape(new CANNON.Cylinder(playerRadius, playerRadius, playerHeight, 16));
world.addBody(playerBody);

人物的碰撞体用圆柱就行。

这里 linearDamping 是线性阻尼,也就是前进、后退等的阻力,1 最大

angularDamping 是旋转的时候的阻尼

fixedRotation 是固定不改变角度,这样避免人碰到东西会被撞倒。固定角度就是角度不变。

之前我们是这样每帧更新下人物位置:

image.png

现在不需要了,设置 velocity 速度就行:

image.png

moving 的时候,设置下对应方向的速度。

停止的时候设置为 0

这里因为阻尼比较大,速度我们调为 10

const moveSpeed = 10;
if (isMoving) {
    moveDirection.normalize();
    playerBody.velocity.x = moveDirection.x * moveSpeed;
    playerBody.velocity.z = moveDirection.z * moveSpeed;
} else {
    playerBody.velocity.x = 0;
    playerBody.velocity.z = 0;
}

最后,渲染循环里更新下玩家的位置:

image.png

world.fixedStep();
if (characterModel) {
    characterModel.position.copy(playerBody.position);
    characterModel.position.y -= playerHeight / 2;
}

试下效果:

2025-12-07 21.20.16.gif

这样就不会穿墙了。

案例代码上传了小册仓库

总结

这节我们加了鼠标移动控制人物方向,并且加上了物理效果。

点击屏幕开始鼠标锁定模式,可以无限滚动,根据滚动距离计算相机旋转角度。

物理效果其余部分都比较简单,box、地面都是固定的,只有人用圆柱体模拟,可以移动,移动的时候更新位置,这样碰到其余物体会停下来,就可以实现真实的物理效果。

现在只是人碰到障碍物会停,下节我们加上跳跃,让人可以跳到盒子上。

评论