上节把小镇做了出来:

这节我们加入旅游的玩家。
还是用之前那个模型,因为它有走路的骨骼动画:
https://github.com/mrdoob/three.js/blob/dev/examples/models/gltf/Soldier.glb

下载下来,放 public 目录:

创建 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;
引入下:

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

看起来比较小,但其实人物大小就是这么大。
播放下走路动画:

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();

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


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

然后实现下前后左右移动。
在 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 键的按下状态。
渲染循环里分别在相机的方向上做运动。
速度是变化的,有一个加速度,并且还有一个阻力。
试下效果:

这样,前后左右移动就实现了。
然后加一下方向的转换。

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
}
}
});
这也是前面讲过的,用鼠标锁定模式,鼠标移动控制方向改变。
左右无限转动,上下限制在一定范围。
试一下:

这样,人物在小镇内的自由移动,镜头跟随就完成了。
案例代码上传了小册仓库
总结
这节我们加入了玩家,可以在场景内随意行走,并实现了镜头跟随。
首先我们把相机放到和玩家一个 group,这样玩家移动,镜头会跟着移动。
然后实现了 WASD 控制前后左右,鼠标移动控制方向转动。
这样,玩家就可以在小镇内随意浏览了。