上节创建了公路的场景:

这节让汽车动起来。
一共 4 条车道。

我们用 4 个数组来保存车道里的车。

创建 cars 数组,有 4 个子数组存放 4 个车道的车。
用定时器每 3 秒放一辆车进去。
改造之前的 createCars 方法,随机决定创建橙色或者蓝色的车,这里每次创建都要 clone 一下。
export const cars = [[],[],[],[]];
let blueCarGltf;
let orangeCarGltf;
async function createCar() {
const isBlueCar = Math.random() < 0.5;
if(isBlueCar) {
if(!blueCarGltf) {
blueCarGltf = await gltfLoader.loadAsync('./blue-car.glb');
}
blueCarGltf.scene.scale.setScalar(150);
return blueCarGltf.scene.clone();
} else {
if(!orangeCarGltf) {
orangeCarGltf = await gltfLoader.loadAsync('./orange-car.glb');
}
orangeCarGltf.scene.scale.setScalar(130);
return orangeCarGltf.scene.clone();
}
}
setInterval(async () => {
const car = await createCar();
car.visible = false;
group.add(car);
const index = Math.floor(Math.random() * 4);
cars[index].push(car);
console.log(cars);
}, 3000);

可以看到,每三秒都会创建一辆车。
不过现在车都是隐藏的。
我们在渲染循环里把它渲染出来:

cars.forEach((arr, index) => {
arr.forEach(item => {
item.visible = true;
item.position.x = -400 + index * 250;
item.position.z = -1300;
});
});
把 visible 设置为 true,然后设置 x、z。
看下效果:

看到最远处那一排车了么?
这样位置就设置对了。
当然,这里的位置应该是在创建的时候就设置下。

setInterval(async () => {
const car = await createCar();
// car.visible = false;
group.add(car);
const index = Math.floor(Math.random() * 4);
cars[index].push(car);
car.position.x = -400 + index * 250;
car.position.z = -1300;
car.speed = 10 + Math.random() * 5;
console.log(cars);
}, 1000);
设置好位置,并且计算一个 10 到 15 的速度。
车的位置设置好也就不用 visible 设置 false 了。
并且创建车的间隔设置为 1s
渲染循环里让车跑起来:

item.position.z += item.speed;

然后加一下人物的左右移动:
首先让人物转身:

manGltf.scene.rotateY(Math.PI);
manGltf.scene.name = 'man';
给它一个名字方便查找。

然后加一下键盘控制:

按下左键 x 减小,否则增加。

这样是可以移动,但看起来太假了,应该有个走路的骨骼动画。
我们换个模型,用 threejs 官方仓库的这个:
https://github.com/mrdoob/three.js/blob/dev/examples/models/gltf/Soldier.glb


下载下来放 public 目录下。
然后换成这个:


它带了 4 个骨骼动画。
我们播放下:

const mixer = new THREE.AnimationMixer(manGltf.scene);
const clipAction = mixer.clipAction(manGltf.animations[3]);
clipAction.play();
const clock = new THREE.Clock();
function render() {
requestAnimationFrame(render);
const delta = clock.getDelta();
mixer.update(delta);
}
render();
这个不用转身,播放下走路的骨骼动画。

好多了,但走路的时候应该转身。

if(e.code === 'ArrowLeft') {
delta = -20;
man.rotation.y = Math.PI / 2;
} else if(e.code === 'ArrowRight') {
delta = 20;
man.rotation.y = - Math.PI / 2;
}
window.addEventListener('keyup', (e) => {
const man = scene.getObjectByName('man');
man.rotation.y = 0;
});

好多了,但走起来还是一顿一顿的。
因为每次移动 20 还是太突兀了,应该用缓动动画运动过去。
安装下 gsap 或者 tween.js:
npm install --save gsap
并且动画过程中不能再触发移动,需要做下节流:
npm install --save lodash-es

function moveMan(man, x) {
gsap.to(man.position, {
x,
duration: 0.3,
ease: 'none'
});
}
const moveManFn = throttle(moveMan, 300);
window.addEventListener('keydown', (e) => {
const man = scene.getObjectByName('man');
if(man) {
let delta = 0;
if(e.code === 'ArrowLeft') {
delta = -50;
man.rotation.y = Math.PI / 2;
} else if(e.code === 'ArrowRight') {
delta = 50;
man.rotation.y = - Math.PI / 2;
}
moveManFn(man, man.position.x + delta);
}
});
// window.addEventListener('keyup', (e) => {
// const man = scene.getObjectByName('man');
// man.rotation.y = 0;
// });
用 gsap 来移动人物,x 是一点点变化的,就不会卡顿了。
动画持续 300ms 这 300ms 内要节流,不再触发动画。
现在就不是键盘抬起就转身了,把它注释掉。
试下效果:

此外,车辆现在会一直增加,我们让它驶出视野后就销毁:

arr = arr.filter(item => {
if(item.position.z > 500) {
item.parent?.remove(item);
return false;
};
return true;
})
驶出视野就删除对象,并且从数组中删除。

视野拉高就可以看到车行驶到一定的距离就会消失。
案例代码上传了小册仓库
总结
这节我们实现了人物走路和车辆行驶。
用 4 个数组来存储 4 条车道的车,设置初始位置,渲染循环循环里改变位置,让车跑起来。
人物播放走路的骨骼动画,并且用 gsap 来做补间动画,这样人物走路就不会卡顿,走路动画过程中做下节流。
这样,人走路和车辆行驶就都实现了,下节我们来做碰撞的处理。