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);
});
现在就变成了这样:
每次跳的时候,在随机方向生成一个新方块。
现在是 click 的时候就跳,实际上应该是 mousedown 的时候蓄力,mouseup 的时候跳。
我们先把 click 事件注释掉:
move 玩家和摄像机的逻辑也注释掉:
然后添加 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);
}
然后按下的时候每帧都增加速度。
按下一段时间再松开,这时会打印现在的速度,这就是蓄力。
为什么有两个速度呢?
因为蓄力之后有两个方向的移动,一个是 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,也就是平台高度,这个时候就要判断是否跳到了下一个平台。
试一下:
没啥问题,蓄力不同的时间,跳的远近不同。
然后加上移动 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 轴同步移动。
这样玩家就始终在屏幕中央了。
然后每跳一次生成下一个方块:
当玩家的 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 的随机值。
我们只保留一个方块,把之前创建的第二个方块去掉:
这样从第一个方块开始就是随机方向和距离的:
然后判断下是否跳成功了:
判断逻辑也很简单,就是 x 或者 z 是否是在下个平台的范围内。
判断下 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;
}
跳完之后,如果跳到了下一个方块,就累加分数,否则就提示失败,并打印当前分数:
if(playerInCube()) {
score++;
console.log(score);
} else {
console.log('失败, 当前分数' + score);
}
分数有点问题,没跳的时候就已经加一了。
这是以为最开始也会触发一次判断,在方块上,所以分数加一了。
我们把初始分数设置为 -1 就好了。
这样就对了:
然后我们加个 html 标签来展示当前分数:
<div id="score">0</div>
添加样式:
#score {
font-size: 50px;
color: #fff;
position: fixed;
top: 10%;
left: 10%;
}
然后在分数更新时更新这个标签的内容:
document.getElementById('score').innerHTML = score;
测试下:
没啥问题。
然后加上失败时的提示的 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;*/
}
最开始 display 为 none,失败的时候把它显示出来:
document.getElementById('fail').style.display = 'block';
document.getElementById('score2').innerHTML = score;
不过现在有个问题,失败了还是创建下一个方块了。
我们改一下:
把创建方块的逻辑移动到这里:
测试下:
然后我们处理下细节:
把 axesHelper 也就是坐标轴去掉:
然后蓄力的时候加个缩短的效果:
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
再就是黑色和背景颜色太接近了,我们换个颜色:
这样,我们的跳一跳小游戏就完成了。
全部代码如下,一共 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 实现了跳一跳小游戏。
转载自:https://juejin.cn/post/7270648629378203688