手把手教学 | 用 Three.js 手写跳一跳小游戏

手把手教学 | 用 Three.js 手写跳一跳小游戏

首页休闲益智方块跳动的冒险更新时间:2024-04-15

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

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

游戏逻辑和这个 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); let p1 = scene.position; camera.lookAt(p1); 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 作为玩家,跳一跳就是移动玩家的位置。

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

我们通过动画的方式改变玩家位置和相机位置,并且玩家还有一个向上的速度,只不过逐步递减,这样就实现了跳的效果。后面还会继续分享其他功能哦~

小游戏也能在自有App上运行

那小游戏开发好了,你们有没有想过上架到自己的 App 上呢,让自己的 App 也具备小游戏的运行能力

这里可以推荐一款工具帮助大家实现,就不管是企业还是个人开发者都可以通过 FinClip 在自有App上运行小游戏,通过轻量的技术形态有利于实现小游戏的流量分发,实现多平台布局。

目前 FinClip 支持主流游戏引擎(Cocos、egret、pixi.js、Laya等) ,可满足各种进阶开发的需求。

其次,FinClip 还支持微信小程序语法、微信登陆、支付,当企业主 App 需要引入第三方微信小程序游戏时,可低成本快速引入变现。


另外 FinClip (finclip.com) 提供标准化、数字化的服务商入驻流程,可以实现对入驻小游戏商户的动态管理。

技术分享由「神光的编程秘籍」提供

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved