Skip to content

199. 实战:小镇旅游(二)

Published:

上节把小镇做了出来:

image.png

这节我们加入旅游的玩家。

还是用之前那个模型,因为它有走路的骨骼动画:

https://github.com/mrdoob/three.js/blob/dev/examples/models/gltf/Soldier.glb

image.png

下载下来,放 public 目录:

image.png

创建 person.js

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();

const mesh = new THREE.Group();

loader.load("./Soldier.glb", function (gltf) {
    console.log(gltf);
    gltf.scene.scale.setScalar(20);
    mesh.add(gltf.scene);
});

export default mesh;

引入下:

image.png

import person from './person.js';
scene.add(person);

image.png

看起来比较小,但其实人物大小就是这么大。

播放下走路动画:

image.png

const mixer = new THREE.AnimationMixer(mesh);

const walkAction = mixer.clipAction(gltf.animations[3]);
walkAction.play();

const clock = new THREE.Clock();
function render() {
    const delta = clock.getDelta();
    mixer.update(delta);

    requestAnimationFrame(render);
}

render();

2025-09-22 18.46.03.gif

然后把相机放到人物后面,把 OrbitControls 去掉:

image.png

image.png

camera.position.y = 50;
camera.position.z = 100;
camera.lookAt(0, 50, 0);

person.add(camera);

2025-09-22 18.49.05.gif

然后实现下前后左右移动。

在 main.js 后面加一下这段代码:

let 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 clock = new THREE.Clock();

const v = new THREE.Vector3(0, 0, 0);
const a = 100;
const resistance = -0.01;

function render() {
  const deltaTime = clock.getDelta();
  if(keyPressed.w) {
    const dir = camera.getWorldDirection(new THREE.Vector3());
    dir.y = 0;

    if(v.length() > -400) {
      v.add(dir.multiplyScalar(a * deltaTime));
    }
  } else if(keyPressed.s) {
    const dir = camera.getWorldDirection(new THREE.Vector3());
    dir.y = 0;
    if(v.length() < 400) {
      v.add(dir.multiplyScalar(-a * deltaTime));
    }
  } else if(keyPressed.a) {
    const frontDir = camera.getWorldDirection(new THREE.Vector3());
    const topDir = new THREE.Vector3(0, 1, 0);
    const dir = topDir.cross(frontDir);

    if(v.length() < 400) {
      v.add(dir.multiplyScalar(a * deltaTime));
    }
  } else if(keyPressed.d) {
    const frontDir = camera.getWorldDirection(new THREE.Vector3());
    const topDir = new THREE.Vector3(0, 1, 0);
    const dir = frontDir.cross(topDir);

    if(v.length() < 400) {
      v.add(dir.multiplyScalar(a * deltaTime));
    }
  }

  v.addScaledVector(v, resistance);

  const movePos = v.clone().multiplyScalar(deltaTime);
  person.position.add(movePos);

  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

render();

我们之前写过,就是记录 WASD 键的按下状态。

渲染循环里分别在相机的方向上做运动。

速度是变化的,有一个加速度,并且还有一个阻力。

试下效果:

2025-09-22 18.55.09.gif

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

然后加一下方向的转换。

image.png


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

const min = THREE.MathUtils.degToRad(-20);
const max = THREE.MathUtils.degToRad(20);

document.addEventListener('mousemove', (e) => {
  if(document.pointerLockElement === document.body) {
    person.rotation.y -= e.movementX / 500;
    camera.rotation.x -= e.movementY / 500;

    if(camera.rotation.x > max) {
      camera.rotation.x = max
    } 
    if(camera.rotation.x < min) {
      camera.rotation.x = min
    }
  }
});

这也是前面讲过的,用鼠标锁定模式,鼠标移动控制方向改变。

左右无限转动,上下限制在一定范围。

试一下:

2025-09-22 19.01.50.gif

这样,人物在小镇内的自由移动,镜头跟随就完成了。

案例代码上传了小册仓库

总结

这节我们加入了玩家,可以在场景内随意行走,并实现了镜头跟随。

首先我们把相机放到和玩家一个 group,这样玩家移动,镜头会跟着移动。

然后实现了 WASD 控制前后左右,鼠标移动控制方向转动。

这样,玩家就可以在小镇内随意浏览了。

评论