likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 11

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

这篇文章我们继续做。

现在是只有 7 个方块,而实际上方块应该是动态生成的。

比如最开始只有两个,跳到一个方块后,自动出现下一个,并且向左还是向右是随机的。

我们改一下:

const currentCubePos = {x: 0, y: 0, z: -100};
let direction = 'right';

记录下当前的位置和方向。

点击的时候判断下,如果是向右就改变 z 的位置,否则改变 x 的位置。

然后生成下一个方块,也是随机向左或者向右。

document.body.addEventListener('click', () => {
    if(direction === 'right') {
        targetCameraPos.z = camera.position.z - 100
        targetCameraFocus.z -= 100
        targetPlayerPos.z -=100;
    } else {
        targetCameraPos.x = camera.position.x - 100
        targetCameraFocus.x -= 100
        targetPlayerPos.x -=100;        
    }

    speed = 5;

    const num = Math.random();
    if(num > 0.5) {
        currentCubePos.z -= 100;
        direction = 'right';
    } else {
        currentCubePos.x -= 100;
        direction = 'left';
    }
    createCube(currentCubePos.x, currentCubePos.z);
});

现在就变成了这样:

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

每次跳的时候,在随机方向生成一个新方块。

现在是 click 的时候就跳,实际上应该是 mousedown 的时候蓄力,mouseup 的时候跳。

我们先把 click 事件注释掉:

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

move 玩家和摄像机的逻辑也注释掉:

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

然后添加 mousedown 和 mousedup 事件:

let pressed = false;
let speed = 0;
let speedY = 0;

document.body.addEventListener('mousedown', () => {
    speed = 0;
    speedY = 0;
    pressed = true;
});
document.body.addEventListener('mouseup', () => {
    pressed = false;
    console.log(speed);
})

mousedown 的时候把速度设为 0,然后标记 pressed 为 true。

在 mouseup 的时候标记 pressed 为 false,并且打印速度。


function speedUp() {
    if(pressed) {
        speed += 0.1;
        speedY += 0.1;
    }
}

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

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

然后按下的时候每帧都增加速度。

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

按下一段时间再松开,这时会打印现在的速度,这就是蓄力。

为什么有两个速度呢?

因为蓄力之后有两个方向的移动,一个是 x 轴或者 z 轴,一个是 y 轴。

然后在 movePlayer 的时候分别用 speed 和 speedY 计算现在的位置:

function movePlayer() {
    player.position.y += speedY;

    if(player.position.y < 17.5) {
        player.position.y = 17.5;
    } else {
        if(direction === 'right') {
            player.position.z -= speed;
        } else {
            player.position.x -= speed;
        }
    }
    speedY -= 0.3;
}

function speedUp() {
    if(pressed) {
        speed += 0.1;
        speedY += 0.1;
    }
}

function render() {
    if(!pressed) {
        // moveCamera();
        movePlayer();
    }
    speedUp();

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

现在只有非 pressed 状态才会移动,按下的时候不移动。

移动的截止条件就是 y 轴到了 17.5,也就是平台高度,这个时候就要判断是否跳到了下一个平台。

试一下:

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

没啥问题,蓄力不同的时间,跳的远近不同。

然后加上移动 camera 的逻辑:

function moveCamera() {
    if(player.position.y > 17.5) {
        if(direction === 'right') {
            camera.position.z -= speed;
            cameraFocus.z -= speed;
        } else {
            camera.position.x -= speed;
            cameraFocus.x -= speed;
        }
        
        camera.lookAt(cameraFocus.x, cameraFocus.y, cameraFocus.z);
    }
}

function render() {
    if(!pressed) {
        moveCamera();
        movePlayer();
    }
    speedUp();

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

结束条件同样是玩家跳到了平台高度。

然后相机的位置和焦点的 x 或者 z 轴同步移动。

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

这样玩家就始终在屏幕中央了。

然后每跳一次生成下一个方块:

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

当玩家的 y 到了 17.5 的时候,生成下一个方块。

if(player.position.y < 17.5) {
    player.position.y = 17.5;

    if(stopped === false){
        const distance = Math.floor(50 + Math.random() * 100);

        const num = Math.random();
        if(num > 0.5) {
            currentCubePos.z -= distance;
            direction = 'right';
        } else {
            currentCubePos.x -= distance;
            direction = 'left';
        }
        createCube(currentCubePos.x, currentCubePos.z);
    }

方向随机,距离是 50 到 150 的随机值。

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

我们只保留一个方块,把之前创建的第二个方块去掉:

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

这样从第一个方块开始就是随机方向和距离的:

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

然后判断下是否跳成功了:

判断逻辑也很简单,就是 x 或者 z 是否是在下个平台的范围内。

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

判断下 player.position.x 是否在方块中心点 currentCubePos.x 加减 15 的范围内。

function playerInCube() {
    if(direction === 'right') {
        if( player.position.z < currentCubePos.z + 15 && player.position.z > currentCubePos.z - 15) {
            return true;
        }
    } else if(direction === 'left') {
        if( player.position.x < currentCubePos.x + 15 && player.position.x > currentCubePos.x - 15) {
            return true;
        }
    }
    return false;
}

跳完之后,如果跳到了下一个方块,就累加分数,否则就提示失败,并打印当前分数:

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

if(playerInCube()) {
    score++;
    console.log(score);
} else {
    console.log('失败, 当前分数' + score);
}

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

分数有点问题,没跳的时候就已经加一了。

这是以为最开始也会触发一次判断,在方块上,所以分数加一了。

我们把初始分数设置为 -1 就好了。

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

这样就对了:

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

然后我们加个 html 标签来展示当前分数:

<div id="score">0</div>

添加样式:

#score {
    font-size: 50px;
    color: #fff;
    position: fixed;
    top: 10%;
    left: 10%;
}

然后在分数更新时更新这个标签的内容:

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

document.getElementById('score').innerHTML = score;

测试下:

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

没啥问题。

然后加上失败时的提示的 html:

<div id="fail">您的分数为<span id="score2">0</span></div>

加上样式:

#fail {
    position: fixed;
    background: rgba(0,0,0,0.5);
    left: 0;
    right: 0;
    width: 100%;
    height: 100%;
    font-size: 70px;
    color: #fff;
    padding-left: 40%;
    padding-top: 200px;
    /* display: none;*/
}

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

最开始 display 为 none,失败的时候把它显示出来:

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

document.getElementById('fail').style.display = 'block';
document.getElementById('score2').innerHTML = score;

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

不过现在有个问题,失败了还是创建下一个方块了。

我们改一下:

把创建方块的逻辑移动到这里:

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

测试下:

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

然后我们处理下细节:

把 axesHelper 也就是坐标轴去掉:

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

然后蓄力的时候加个缩短的效果:

function speedUp() {
    if(pressed) {
        speed += 0.1;
        speedY += 0.1;

        if(player.scale.y> 0) {
            player.scale.y -= 0.001;
        }
        player.position.y -= 15 - 15 * player.scale.y

        if(player.position.y < 10) {
            player.position.y = 10;
        }
    } else {
        player.scale.y = 1;
    }
}

改变高度直接修改 scale 就行,每次渲染,scale.y 减 0.001,但是不能小于 0。

并且还要改变 position.y,让它一直贴着方块,本来 player 高度是 15,减去缩小后的高度,少了多少, position.y 就减多少。

当然,如果 position.y 低于方块了,就设置到方块的上面,也就是 10。

如果不是在按下的状态,就恢复 scale.y 为 1

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

再就是黑色和背景颜色太接近了,我们换个颜色:

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

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

这样,我们的跳一跳小游戏就完成了。

全部代码如下,一共 200 多行代码:

<!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>
    <style>
        #score {
            font-size: 50px;
            color: #fff;
            position: fixed;
            top: 10%;
            left: 10%;
        }
        #fail {
            position: fixed;
            background: rgba(0,0,0,0.5);
            left: 0;
            right: 0;
            width: 100%;
            height: 100%;
            font-size: 70px;
            color: #fff;
            padding-left: 40%;
            padding-top: 200px;
            display: none;
        }
    </style>
</head>
<body>
    <div id="score">0</div>
    <div id="fail">您的分数为 <span id="score2">0</span></div>
<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;
let speedY = 0;

const currentCubePos = {x: 0, y: 0, z: 0};
let direction = 'right';

let pressed = false;

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 create() {


    function createPlayer() {
        const geometry = new THREE.BoxGeometry( 5, 15, 5 );
        const material = new THREE.MeshPhongMaterial( {color: 0xffff00} );
        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);

    document.body.addEventListener('mousedown', () => {
        speed = 0;
        speedY = 0;
        pressed = true;
    });

    document.body.addEventListener('mouseup', () => {
        pressed = false;
    })
}

function moveCamera() {
    if(player.position.y > 17.5) {
        if(direction === 'right') {
            camera.position.z -= speed;
            cameraFocus.z -= speed;
        } else {
            camera.position.x -= speed;
            cameraFocus.x -= speed;
        }
        
        camera.lookAt(cameraFocus.x, cameraFocus.y, cameraFocus.z);
    }
}

function playerInCube() {
    if(direction === 'right') {
        if( player.position.z < currentCubePos.z + 15 && player.position.z > currentCubePos.z - 15) {
            return true;
        }
    } else if(direction === 'left') {
        if( player.position.x < currentCubePos.x + 15 && player.position.x > currentCubePos.x - 15) {
            return true;
        }
    }
    return false;
}

let score = -1;
let stopped = true;
function movePlayer() {
    player.position.y += speedY;

    if(player.position.y < 17.5) {
        player.position.y = 17.5;

        if(stopped === false){

            if(playerInCube()) {
                score++;
                document.getElementById('score').innerHTML = score;

                const distance = Math.floor(50 + Math.random() * 100);

                const num = Math.random();
                if(num > 0.5) {
                    currentCubePos.z -= distance;
                    direction = 'right';
                } else {
                    currentCubePos.x -= distance;
                    direction = 'left';
                }
                createCube(currentCubePos.x, currentCubePos.z);
            } else {
                document.getElementById('fail').style.display = 'block';
                document.getElementById('score2').innerHTML = score;
            }

        }

        stopped = true;
    } else {
        stopped = false;
        if(direction === 'right') {
            player.position.z -= speed;
        } else {
            player.position.x -= speed;
        }
    }
    speedY -= 0.3;
}

function speedUp() {
    if(pressed) {
        speed += 0.1;
        speedY += 0.1;

        if(player.scale.y> 0) {
            player.scale.y -= 0.001;
        }
        player.position.y -= 15 - 15 * player.scale.y

        if(player.position.y < 10) {
            player.position.y = 10;
        }
    } else {
        player.scale.y = 1;
    }
}

function render() {
    if(!pressed) {
        moveCamera();
        movePlayer();
    }
    speedUp();

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

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

总结

上篇文章我们写了基础代码,完成了方块的创建,跳的过程以及摄像机跟拍。

这篇文章我们实现了蓄力,落点判断,以及分数计算。

首先,我们不再提前创建方块,改成随机方向,随机距离创建。

监听 mousedown 和 mouseup 来实现蓄力,mousedown 的时候不断增加速度,mouseup 的时候移动玩家和摄像机。

speedY 会逐渐变小,所以会有下落的过程,落到方块高度的时候,判断下是否在下个方块内,如果是,就累加分数,否则提示游戏结束,输出分数。

我们还做了蓄力的时候改变方块高度,通过调整 scale.y 和 position.y 来实现的。

这样,我们就通过 three.js 实现了跳一跳小游戏。