Skip to content

169. 实战:躲避汽车(五)

Published:

上节实现了碰撞的流血效果:

2025-06-03 23.16.28.gif

这节我们加一些文字,比如开始的 Ready Go,结束时的 Game Over,以及左右箭头。

这个用 SpriteText 来做。

安装下:

npm install --save three-spritetext

创建 text.js

import SpriteText from "three-spritetext";

export function createLeftArrow() {
    const arrowLeft = new SpriteText('←', 200);
    arrowLeft.color = 'white';
    arrowLeft.strokeColor = 'blue';
    arrowLeft.strokeWidth = 1;
    arrowLeft.position.x = -500;
    arrowLeft.position.y = 100;
    arrowLeft.position.z = 100;
    return arrowLeft;
}

我们先画一下左箭头。

字体是白色,有蓝色的描边。

在 main.js 里引入:

image.png

scene.add(createLeftArrow());

image.png

然后添加右箭头:

export function createRightArrow() {
    const arrowLeft = new SpriteText('→', 200);
    arrowLeft.color = 'white';
    arrowLeft.strokeColor = 'blue';
    arrowLeft.strokeWidth = 1;
    arrowLeft.position.x = 500;
    arrowLeft.position.y = 100;
    arrowLeft.position.z = 100;
    return arrowLeft;
}
scene.add(createRightArrow());

image.png

接下来是最开始的 Ready Go

export function createReady() {
    const ready = new SpriteText('Ready', 200);
    ready.color = 'white';
    ready.strokeColor = 'lightgreen';
    ready.strokeWidth = 1;

    ready.position.y = 200;
    ready.position.z = 100;
    return ready;
}
scene.add(createReady());

image.png

直接出现太突兀了,我们用 gsap 做一下从远到近的补间动画。

image.png

设置 z 和 textHeight 的初始值。

用 timeline 来做并行的 z 和 textHeight 变化的动画。

0.5s 后串行做 textHeight 变小的动画,这个速度要快一点。

export function createReady() {
    const ready = new SpriteText('Ready', 200);
    ready.color = 'white';
    ready.strokeColor = 'green';
    ready.strokeWidth = 1;

    ready.position.y = 200;
    
    ready.position.z = -500;
    ready.textHeight = 0;

    const timeline = gsap.timeline();
    timeline
        .to(ready.position, {
            duration: 0.5,
            z: 100,
        })
        .to(ready, {
            textHeight: 200,
            duration: 0.5
        }, '<')
        .to(ready, {
            textHeight: 0,
            duration: 0.05,
            onComplete: () => {
                ready.parent.remove(ready);
            }
        }, '+=0.5');
    return ready;
}

2025-06-04 08.35.55.gif

没啥问题,我们再同样的方式写下 Go 的

export function createGo() {
    const ready = new SpriteText('Go', 200);
    ready.color = 'white';
    ready.strokeColor = 'green';
    ready.strokeWidth = 1;

    ready.position.y = 200;
    
    ready.position.z = -500;
    ready.textHeight = 0;

    const timeline = gsap.timeline();
    timeline
        .to(ready.position, {
            duration: 0.5,
            z: 100,
        }, '+=1')
        .to(ready, {
            textHeight: 200,
            duration: 0.5
        }, '<')
        .to(ready, {
            textHeight: 0,
            duration: 0.05,
            onComplete: () => {
                ready.parent.remove(ready);
            }
        }, '+=0.5');
    return ready;
}
scene.add(createGo());

唯一的区别是最开始要延迟 1s

image.png

2025-06-04 08.38.33.gif

最后,我们加上 GameOver

export function createGameOver() {
    const ready = new SpriteText('Game Over', 200);
    ready.color = 'red';
    ready.strokeColor = 'white';
    ready.strokeWidth = 1;
    ready.padding = 80;

    ready.position.y = 200;
    
    ready.position.z = -500;
    ready.textHeight = 0;

    const timeline = gsap.timeline();
    timeline
        .to(ready.position, {
            duration: 0.3,
            z: 100,
        })
        .to(ready, {
            textHeight: 120,
            duration: 0.3
        }, '<');
    return ready;
}

在碰撞的时候再展示:

image.png

scene.add(createGameOver());

看下效果:

2025-06-04 08.43.55.gif

没啥问题。

最后我们来优化一下:

现在走路是 300ms 一次,改成 200ms,这样灵敏一些。

image.png

去掉 OrbitControls,玩家不需要改变相机位置。

image.png

然后最开始,我们做一个从远到近的相机动画:

image.png

camera.position.y = 2000;
camera.position.z = 2000;
gsap.to(camera.position, {
  y: 500,
  z: 500,
  duration: 2
});

2s 内镜头从远到近。

2025-06-04 08.50.52.gif

这样,入场效果更好了。

还有两个小问题要修复:

一个是现在车的速度是是随机的 10 到 15

image.png

这样同一车道的车是有追尾的可能的。

应该是中间两条车道速度快,两边速度慢,但同一车道的车速度一样。

image.png

car.speed = [1, 2].includes(index) ? 20 : 10

2025-06-04 10.56.54.gif

另一个问题是当你切换到其他页面或者其他软件一段时间,再切回来:

image.png

会一下子有很多的车。

因为定时器没有暂停。

我们要检测下页面可见性,不可见的时候就暂停创建车子:

image.png

let paused = false;
document.addEventListener('visibilitychange', (e) => {
    paused = document.visibilityState !== 'visible'
});
if(paused) {
    return;
}

这样,游戏体验就没啥问题了。

案例代码上传了小册仓库

总结

这节我们加上了文字,包括左右箭头、Ready、Go、Game Over

用 three-spritetext 实现的,不用自己用 canvas 画。

并且用 gasp 做了文字放大、消失的补间动画。

最后我们还加了一个镜头从远到近的入场相机动画。

至此,我们的躲避汽车游戏就完成了。

评论