前几年,跳一跳小游戏火过一段时间。
玩家从一个方块跳到下一个方块,如果没跳过去就算失败,跳过去了就会再出现下一个方块。
游戏逻辑和这个 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