Three.js 手写跳一跳小游戏(上)

前几年,跳一跳小游戏火过一段时间。

玩家从一个方块跳到下一个方块,如果没跳过去就算失败,跳过去了就会再出现下一个方块。

游戏逻辑和这个 3D 场景都挺简单的。

那我们能不能用 Three.js 自己实现一个呢?

我们来写写看。

这篇文章我们要实现这种效果:

新建一个 html,引入 threejs:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>跳一跳</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
    <script src="https://www.unpkg.com/three@0.154.0/build/three.js"></script>
</head>
<body>
<script>
    console.log(THREE);
</script>
</body>
</html>

跑个静态服务器:

npx http-server .

浏览器访问下:

three.js 引入成功了。

three.js 涉及到这些概念:

Mesh 是物体,它要指定是什么几何体 Geometry,什么材质 Material。

Light 是光源,有了光源才能看到东西,并且有的材质还会反光。

Scene 是场景,把上面所有的东西管理起来,然后让渲染器 Renderer 渲染出来。

Camera 是摄像机,也就是从什么角度去观察场景,我们能看到的就是摄像机的位置看到的东西。

了解了这些概念,我们在 script 部分写下 three.js 的初始化代码:

const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);

const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer();

renderer.setSize(width, height);
camera.position.set(0, 0, 500);
camera.lookAt(scene.position);

const pointLight = new THREE.PointLight( 0xffffff );
pointLight.position.set(0, 0, 500);
scene.add(pointLight);

document.body.appendChild(renderer.domElement)

function create() {
    const geometry = new THREE.BoxGeometry( 100, 100, 100 ); 
    const material = new THREE.MeshPhongMaterial( {color: 0x00ff00} ); 
    const cube = new THREE.Mesh( geometry, material ); 
    cube.rotation.y = 0.5;
    cube.rotation.x = 0.5;
    scene.add( cube );
}

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

create();
render();

先看效果:

回过头来再解释:

这段代码是创建摄像机的:

PerspectiveCamera 是透视相机,也就是近大远小的效果。

它指定了 4 个参数,都是什么意思呢?

就是这个:

从一个位置去看,是不是得指定你看的角度是多大,也就是图里的 fov,上面指定的 45 度。

然后就是你看的这个范围的宽高比是多少,我们用的是窗口的宽高比。

再就是你要看从哪里到哪里的范围,我们是看从 0.1 到距离 1000 的范围。

这就创建好了透视相机。

然后是光源:

创建个白色的点光源,放在 0,0,500 的位置,添加到场景中。

摄像机也在 0,0, 500 的位置来看场景 scene 的位置:

然后我们创建个立方体,旋转一下:

默认是在 0,0,0 的位置,我们从 0,0,500 的位置去观察看到的就是个平面,所以要旋转下。

我们加个 AxesHelper 把坐标轴显示出来,长度指定 1000

const axesHelper = new THREE.AxesHelper( 1000 );
axesHelper.position.set(0,0,0);
scene.add( axesHelper );

向右为 x,向上为 y,向前为 z。

因为摄像机在 0,0,500 的位置,所以看不到 z 轴。

我们改下摄像机位置:

把摄像机移动到 500,500,500 的位置,物体就不用旋转了。

这样看到的是这样的:

为什么 2 个面是黑的呢?

因为点光源在 0,0,500 的位置啊,另外两个面照不到。

调整下光源位置到 0,500, 500 呢?

这样就能看到 2 个面了:

当然,这里能反光,因为我们创建立方体用的是 MeshPhongMaterial,它是反光材质:

如果你把它换成 MeshBasicMaterial,其他代码不变:

那就是均匀的颜色,不受光照影响:

最后用 renderer 把 scene 渲染出来,当然,是从 camera 角度能看到的 scene:

所以 render 的时候要传 scene 和 camera 两个参数:

用 requestAnimationFrame 一帧帧的渲染。

基础过了一遍 three.js 基础,接下来正式来写跳一跳小游戏。

我们先创建底下这些平台:

很显然,也是 BoxGeometry。

我们把之前的立方体去掉,给 renderer 设置个背景颜色,并把摄像机移动到 100,100,100 的位置:

然后添加两个立方体:

function create() {
    const geometry = new THREE.BoxGeometry( 30, 20, 30 );
    const material = new THREE.MeshPhongMaterial( {color: 0xffffff} );
    const cube = new THREE.Mesh( geometry, material ); 
    scene.add( cube );


    const geometry2 = new THREE.BoxGeometry( 30, 20, 30 );
    const material2 = new THREE.MeshPhongMaterial( {color: 0xffffff} );
    const cube2 = new THREE.Mesh( geometry, material ); 
    cube2.position.z = -50;
    scene.add( cube2 );    
}

x、z 轴的尺寸为 30,y 轴的尺寸为 20.

渲染出来是这样的:

我们调整下点光源位置:

const pointLight = new THREE.PointLight( 0xffffff );
pointLight.position.set(40, 100, 60);

scene.add( pointLight );

调整到 40,100,60 的位置。

光照射到的部分越多,颜色越浅,照射到的越少,颜色越深。

我们希望上面的面(y 轴)照射到的多一些,前面那个面(z 轴)其次,右边那个面(x 轴)最深。

所以要按照 y > z > x 的关系来设置点光源位置。

确实,渲染出来的效果是我们想要的。

只不过每个立方体的反光不同,我们想让每个立方体都一样,怎么办呢?

那就不能用点光源 PointLight 了,要换成平行光 DirectionalLight。

const directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set(40, 100, 60);

scene.add( directionalLight );

参数不变,还是在同样的位置。

换成平行光光源之后,每个立方体的反光就都一样了。

不过现在背景颜色太浅了,对比不明显,我们调深一点:

好多了:

但不知道大家有没有发现,现在是有锯齿的:

这个的解决很简单,给 WebGLRenderer 传个参数就好了:

const renderer = new THREE.WebGLRenderer({ antialias: true });

平滑多了。

然后我们把创建立方体的逻辑封装成函数。

function createCube(x, z) {
    const geometry = new THREE.BoxGeometry( 30, 20, 30 );
    const material = new THREE.MeshPhongMaterial( {color: 0xffffff} );
    const cube = new THREE.Mesh( geometry, material ); 
    cube.position.x = x;
    cube.position.z = z;
    scene.add( cube );
}

调用几次:

createCube(0, 0);
createCube(0, -100);
createCube(0, -200);
createCube(0, -300);
createCube(-100, 0);
createCube(-200, 0);
createCube(-300, 0);

创建了 7 个立方体:

玩家就是在这些立方体上跳来跳去。

那么问题来了:现在同一方向只能显示 4 个立方体,那如果玩家跳到第 5 个、第 6 个立方体,不就看不到了?

怎么办呢?

移动摄像机!

大家见过这种摄像方式没有:

想拍一个运动的人,可以踩在平衡车上,手拿着摄像机跟着拍,这样能保证人物一直在镜头中央。

在 threejs 世界里也是一样,玩家跳过去之后,摄像机跟着移动过去。

玩家移动多少,摄像机移动多少,这样是不是就相对不变了?也就是玩家一直在镜头中央了?

我们放一个黑色的立方体在上面,代表玩家:

function createPlayer() {
    const geometry = new THREE.BoxGeometry( 5, 20, 5 );
    const material = new THREE.MeshPhongMaterial( {color: 0x000000} );
    const player = new THREE.Mesh( geometry, material ); 
    player.position.x = 0;
    player.position.y = 17.5;
    player.position.z = 0;
    scene.add( player )
    return player;
}

const player = createPlayer();

为什么 y 是 17.5 呢?

因为两个立方体都是 0、0、0 的位置,一个高度是 20,一个高度是 15:

黑色立方体往上移动 7.5 的时候,刚好底部到了原点。

再往上移动 10,就到了白色立方体的上面了:

我们调整下摄像机位置到 100,20,100

这样,刚好可以看到两者的接触面,确实严丝合缝的:

把 y 设置为 20,就有缝隙了:

所以计算出的 17.5 刚刚好。

然后我们做下玩家的移动,先做的简单点,点击的时候就移动到下一个位置:

document.body.addEventListener('click', () => {
    player.position.z -= 100;
});

效果是这样的:

 

不移动摄像机的情况下,玩家跳几次就看不到了。

我们同步移动下摄像机试试:

let focusPos = { x: 0, y: 0, z: 0 };
document.body.addEventListener('click', () => {
    player.position.z -= 100;

    camera.position.z -= 100;

    focusPos.z -= 100;
    camera.lookAt(focusPos.x, focusPos.y, focusPos.z);
});

玩家的 position.z 减 100,那摄像机的 position.z 就减 100,这样就是跟着拍。

当然 lookAt 的焦点位置得移动到下一个方块。

相机位置和聚焦的位置都得变,不能相机跟着移动了,但焦点还是在第一个方块那。

效果是这样的:

能感觉到玩家一直在镜头中央么?

这就是摄像机跟拍的效果。

当然,现在的位置是直接变到下一个方块,太突兀了,得有个动画的过程。

我们新建这几个全局变量:

const targetCameraPos = { x: 100, y: 100, z: 100 };

const cameraFocus = { x: 0, y: 0, z: 0 };
const targetCameraFocus = { x: 0, y: 0, z: 0 };


从一个位置到另一个位置,显然需要起点和终点坐标。

摄像机的当前位置可以从 camera.position 来取,而目标位置我们通过 targetCameraPos 变量保存。

焦点的起始位置是 cameraFocus,结束位置是 targetCameraFocus。

知道了从什么位置到什么位置,就可以开始移动了:

function moveCamera() {

    const { x, z } = camera.position;
    if(x > targetCameraPos.x) {
        camera.position.x -= 3;
    }
    if(z > targetCameraPos.z) {
        camera.position.z -= 3;
    }

    if(cameraFocus.x > targetCameraFocus.x) {
        cameraFocus.x -= 3;
    }
    if(cameraFocus.z > targetCameraFocus.z) {
        cameraFocus.z -= 3;
    }

    camera.lookAt(cameraFocus.x, cameraFocus.y, cameraFocus.z);  
}

function render() {
    moveCamera();

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

如果摄像机没有达到目标位置,就每次渲染移动 3.

焦点位置也是同步移动。

每次 render 的时候调用下,这样每帧都会移动摄像机。

然后当点击的时候,玩家移动,并且设置摄像机的位置和焦点的目标位置:

document.body.addEventListener('click', () => {
    player.position.z -= 100;

    targetCameraPos.z = camera.position.z - 100

    targetCameraFocus.z -= 100
});

效果是这样的:

这就是我们想要的效果,每次玩家跳到下一个方块,就同步移动摄像机并调整焦点位置,这样玩家就是始终在屏幕中央了。

只不过现在玩家是直接移动过去的,没有一个跳的过程。

我们补充上跳的过程:

同样是要把起始位置和结束位置记录下来:

const playerPos = { x: 0, y: 17.5, z: 0};
const targetPlayerPos = { x: 0, y: 17.5, z: 0};

let player;
let speed = 0;

不过这里还需要个 spped,因为有个向上跳的速度。

同时把 player 提取成全局变量。

同样的方式写个 movePlayer 方法:

function movePlayer() {
    if(player.position.x > targetPlayerPos.x) {
        player.position.x -= 3;
    }
    if(player.position.z > targetPlayerPos.z) {
        player.position.z -= 3;
    }
    player.position.y += speed;
    speed -= 0.3;
    if(player.position.y < 17.5) {
        player.position.y = 17.5;
    }
}

function render() {
    moveCamera();
    movePlayer();

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

如果 player 的位置没有到目标位置就移动,并且这里在 y 方向还有个 speed,只不过每次渲染 speed 减 0.3。

然后在点击的时候不再直接改变 player 位置,而是设置 targetPlayerPos 并且设置一个 speed:

这样每帧渲染的时候都会调用 movePlayer 改变玩家位置。

这样就有了跳的感觉。

只不过现在方块数量是有限的,并且跳的速度也是固定的,这个我们后面再继续完善。

现阶段全部代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>跳一跳</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
    <script src="https://www.unpkg.com/three@0.154.0/build/three.js"></script>
</head>
<body>
<script>
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);

const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer({ antialias: true });

renderer.setSize(width, height);
renderer.setClearColor(0x333333);

camera.position.set(100, 100, 100);
camera.lookAt(scene.position);

const directionalLight = new THREE.DirectionalLight( 0xffffff );
directionalLight.position.set(40, 100, 60);

scene.add( directionalLight );

document.body.appendChild(renderer.domElement)

const axesHelper = new THREE.AxesHelper( 1000 );
axesHelper.position.set(0,0,0);
scene.add( axesHelper );

const targetCameraPos = { x: 100, y: 100, z: 100 };

const cameraFocus = { x: 0, y: 0, z: 0 };
const targetCameraFocus = { x: 0, y: 0, z: 0 };

const playerPos = { x: 0, y: 17.5, z: 0};
const targetPlayerPos = { x: 0, y: 17.5, z: 0};

let player;
let speed = 0;

function create() {

    function createCube(x, z) {
        const geometry = new THREE.BoxGeometry( 30, 20, 30 );
        const material = new THREE.MeshPhongMaterial( {color: 0xffffff} );
        const cube = new THREE.Mesh( geometry, material ); 
        cube.position.x = x;
        cube.position.z = z;
        scene.add( cube );
    }

    function createPlayer() {
        const geometry = new THREE.BoxGeometry( 5, 15, 5 );
        const material = new THREE.MeshPhongMaterial( {color: 0x000000} );
        const player = new THREE.Mesh( geometry, material ); 
        player.position.x = 0;
        player.position.y = 17.5;
        player.position.z = 0;
        scene.add( player )
        return player;
    }

    player = createPlayer();

    createCube(0, 0);
    createCube(0, -100);
    createCube(0, -200);
    createCube(0, -300);
    createCube(-100, 0);
    createCube(-200, 0);
    createCube(-300, 0);

    document.body.addEventListener('click', () => {
        targetCameraPos.z = camera.position.z - 100

        targetCameraFocus.z -= 100
        
        targetPlayerPos.z -=100;
        speed = 5;
    });
}

function moveCamera() {
    const { x, z } = camera.position;
    if(x > targetCameraPos.x) {
        camera.position.x -= 3;
    }
    if(z > targetCameraPos.z) {
        camera.position.z -= 3;
    }

    if(cameraFocus.x > targetCameraFocus.x) {
        cameraFocus.x -= 3;
    }
    if(cameraFocus.z > targetCameraFocus.z) {
        cameraFocus.z -= 3;
    }

    camera.lookAt(cameraFocus.x, cameraFocus.y, cameraFocus.z);  
}

function movePlayer() {
    if(player.position.x > targetPlayerPos.x) {
        player.position.x -= 3;
    }
    if(player.position.z > targetPlayerPos.z) {
        player.position.z -= 3;
    }
    player.position.y += speed;
    speed -= 0.3;
    if(player.position.y < 17.5) {
        player.position.y = 17.5;
    }
}

function render() {
    moveCamera();
    movePlayer();

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

create();
render();
</script>
</body>
</html>


总结

我们想用 Three.js 写一个跳一跳小游戏。

先过了一下 Three.js 的基础,也就是场景 Scene、物体 Mesh、几何体 Geometry、材质 Material、摄像机 Camera、灯光 Light、渲染器 Renderer 这些概念。

这些概念的关系看这张图就好了:

在 three.js 里,向右为 x 轴,向上为 y 轴,向前为 z 轴,可以用 AxesHelper 来画出坐标系。

我们用 BoxGeometry 创建了一些方块,并且添加了平行光 DirectionalLight,这样每个方块的明暗度都是一样的。

然后又添加了一个 BoxGeometry 作为玩家,跳一跳就是移动玩家的位置。

但是摄像机要跟随玩家的移动而同步移动,就像现实中拍运动的人要跟着拍,这样才能保证它始终在屏幕中央。

我们通过动画的方式改变玩家位置和相机位置,并且玩家还有一个向上的速度,只不过逐步递减,这样就实现了跳的效果。

现在还有很多地方没做完,但已经有雏形了。

下篇文章我们继续搞。

作者-神说要有光 ,欢迎大家关注!

  • 37
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Sure! Three.js is a JavaScript library that allows you to create and display 3D graphics in a web browser. To model a sphere using Three.js, you can follow these steps: 1. Include the Three.js library in your HTML file by adding the following script tag: ```html <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> ``` 2. Create a scene, camera, and renderer: ```javascript const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); ``` 3. Create a sphere geometry and material: ```javascript const geometry = new THREE.SphereGeometry(1, 32, 32); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); ``` 4. Create a mesh by combining the geometry and material: ```javascript const sphere = new THREE.Mesh(geometry, material); scene.add(sphere); ``` 5. Position the camera and sphere: ```javascript camera.position.z = 5; sphere.position.set(0, 0, 0); ``` 6. Create an animation loop to render the scene: ```javascript function animate() { requestAnimationFrame(animate); sphere.rotation.x += 0.01; sphere.rotation.y += 0.01; renderer.render(scene, camera); } animate(); ``` This code will create a green sphere in the center of the screen that rotates slowly. You can customize the sphere's size, position, color, and other properties according to your needs.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值