Skip to content

201. 综合实战:开放世界(二)

Published:

这节加一下阴影,还有人物行走的控制。

首先加一下阴影,增加真实感:

开一下渲染器的阴影:

image.png

renderer.shadowMap.enabled = true;

阳光的阴影以及投影矩阵:

image.png

sun.castShadow = true;
sun.shadow.camera.left = -30;
sun.shadow.camera.right = 30;
sun.shadow.camera.top = 30;
sun.shadow.camera.bottom = -30;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;

然后给地面加上接收阴影:

image.png

groundMesh.receiveShadow = true;

方块和人加上投射阴影:

image.png

mesh.castShadow = true;
mesh.receiveShadow = true;

image.png

characterModel.traverse((child) => {
    if (child.isMesh) {
      child.castShadow = true;
      child.receiveShadow = true;
    }
});

看下效果:

2025-12-07 16.34.29.gif

有阴影后真实多了。

然后加上人物行走的控制:

把 OrbitControls 禁用掉,导出 camera:

image.png

export { camera }

把相机加到人物的 group 里,跟随人一起移动:

image.png

let currentAction = null;
currentAction = idleAction;

characterModel.add(camera);

// 设置相机相对人物的位置(局部坐标)
camera.position.set(0, 1.5, 2.5); // 在人物后上方
camera.lookAt(0, 1, 0);

idleAction.play();

记录下当前的动画,后面切换的时候用。

把相机加到人物的 group 里,放到人后面,这样镜头就可以跟随人一起移动了。

试下效果:

2025-12-07 17.29.50.gif

然后加一下人物移动和方向改变的逻辑:

image.png

下面的渲染循环做了 mixer.update,上面的就可以去掉了。

代码比较多,我们一部分一部分看:

const keyPressed = {
  w: false,
  a: false,
  s: false,
  d: false
};

document.addEventListener('keydown', e => {
  switch(e.code) {
    case 'KeyA':
      keyPressed.a = true;
      break;
    case 'KeyW':
      keyPressed.w = true;
      break;
    case 'KeyS':
      keyPressed.s = true;
      break;
    case 'KeyD':
      keyPressed.d = true;
      break;
  }
});

document.addEventListener('keyup', e => {
  switch(e.code) {
    case 'KeyA':
      keyPressed.a = false;
      break;
    case 'KeyW':
      keyPressed.w = false;
      break;
    case 'KeyS':
      keyPressed.s = false;
      break;
    case 'KeyD':
      keyPressed.d = false;
      break;
  }
});

const moveSpeed = 3;

function updatePlayerMovement(deltaTime) {
  if (!characterModel) return;

  const forward = new THREE.Vector3();
  camera.getWorldDirection(forward);
  forward.y = 0;
  forward.normalize();

  const right = new THREE.Vector3();
  right.crossVectors(new THREE.Vector3(0, 1, 0), forward).normalize();

  let isMoving = false;
  const moveDirection = new THREE.Vector3(0, 0, 0);

  if (keyPressed.w) {
    moveDirection.add(forward);
    isMoving = true;
  }
  if (keyPressed.s) {
    moveDirection.add(forward.clone().negate());
    isMoving = true;
  }
  if (keyPressed.a) {
    const leftDir = right;
    moveDirection.add(leftDir);
    isMoving = true;
  }
  if (keyPressed.d) {
    const rightDir = right.clone().negate();
    moveDirection.add(rightDir);
    isMoving = true;
  }

  if (isMoving) {
    moveDirection.normalize();
    const moveOffset = moveDirection.multiplyScalar(moveSpeed * deltaTime);
    characterModel.position.add(moveOffset);
  }

  if (mixer) {
    if (isMoving && currentAction !== walkAction) {
      if (currentAction) currentAction.stop();
      walkAction.play();
      currentAction = walkAction;
    } else if (!isMoving && currentAction !== idleAction) {
      if (currentAction) currentAction.stop();
      idleAction.play();
      currentAction = idleAction;
    }
  }
}

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);

  const dt = Math.min(clock.getDelta(), 0.1);

  updatePlayerMovement(dt);

  if (mixer) {
    mixer.update(dt);
  }
}

animate();

首先,记录 WASD 每个键的按下状态:

image.png

然后基于按下的键来调整方向:

image.png

camera.getWorldDirection 拿到相机方向,也就拿到了前后方向。

用叉乘计算出向右的方向,也就拿到了左右方向。

根据按下的 WSAD 来设置方向,速度 * 时间,在这个方向上运动。

此外,还要切换动画:

image.png

根据是否在运动,切换走路和静止动画。

image.png

渲染循环里调用,传入每一帧的间隔时间,用于计算移动距离。

试下效果:

2025-12-07 17.51.05.gif

这样,前后左右移动就实现了。

案例代码上传了小册仓库

总结

这节我们实现了相机跟随人物移动,以及键盘控制前后左右运动。

相机跟随就是把 camera 加到玩家的 group,这样只要改变人物位置、方向就可以了,相机会跟着动。

键盘控制前后左右运动,就是拿到相机方向,通过叉乘计算左右方向,然后每一帧计算移动距离改变位置就好了。

下节我们继续添加其他功能。

评论