百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-07-03 01:58 16 浏览 0 评论

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

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

游戏逻辑和这个 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) 提供标准化、数字化的服务商入驻流程,可以实现对入驻小游戏商户的动态管理。


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

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码