Skip to content

166. 实战:躲避汽车(二)

Published:

上节创建了公路的场景:

2025-06-03 08.43.05.gif

这节让汽车动起来。

一共 4 条车道。

image.png

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

image.png

创建 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);

2025-06-03 16.31.55.gif

可以看到,每三秒都会创建一辆车。

不过现在车都是隐藏的。

我们在渲染循环里把它渲染出来:

image.png

cars.forEach((arr, index) => {
  arr.forEach(item => {
    item.visible = true;
    item.position.x = -400 + index * 250;
    item.position.z  = -1300;
  });
});

把 visible 设置为 true,然后设置 x、z。

看下效果:

image.png

看到最远处那一排车了么?

这样位置就设置对了。

当然,这里的位置应该是在创建的时候就设置下。

image.png

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

渲染循环里让车跑起来:

image.png

item.position.z += item.speed;

2025-06-03 16.53.07.gif

然后加一下人物的左右移动:

首先让人物转身:

image.png

manGltf.scene.rotateY(Math.PI);
manGltf.scene.name = 'man';

给它一个名字方便查找。

image.png

然后加一下键盘控制:

image.png

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

2025-06-03 17.03.22.gif

这样是可以移动,但看起来太假了,应该有个走路的骨骼动画。

我们换个模型,用 threejs 官方仓库的这个:

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

image.png

image.png

下载下来放 public 目录下。

然后换成这个:

image.png

image.png

它带了 4 个骨骼动画。

我们播放下:

image.png

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

这个不用转身,播放下走路的骨骼动画。

2025-06-03 17.10.48.gif

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

image.png

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

2025-06-03 17.17.48.gif

好多了,但走起来还是一顿一顿的。

因为每次移动 20 还是太突兀了,应该用缓动动画运动过去。

安装下 gsap 或者 tween.js:

npm install --save gsap

并且动画过程中不能再触发移动,需要做下节流:

npm install --save lodash-es

image.png

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 内要节流,不再触发动画。

现在就不是键盘抬起就转身了,把它注释掉。

试下效果:

2025-06-03 17.40.29.gif

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

image.png

arr = arr.filter(item => {
    if(item.position.z > 500) {
      item.parent?.remove(item);
      return false;
    };
    return true;
})

驶出视野就删除对象,并且从数组中删除。

2025-06-03 17.50.45.gif

视野拉高就可以看到车行驶到一定的距离就会消失。

案例代码上传了小册仓库

总结

这节我们实现了人物走路和车辆行驶。

用 4 个数组来存储 4 条车道的车,设置初始位置,渲染循环循环里改变位置,让车跑起来。

人物播放走路的骨骼动画,并且用 gsap 来做补间动画,这样人物走路就不会卡顿,走路动画过程中做下节流。

这样,人走路和车辆行驶就都实现了,下节我们来做碰撞的处理。

评论