我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
前言
最近在lol火爆了一款提莫的走位小游戏,利用贱萌贱萌的提莫来不断走位躲避弹幕,并且可以保留时间作为分数,今天我们就用js来实现一个类似的小游戏。
实现
在线体验地址: 提莫快跑
可以在线游玩或者使用编辑器游玩,编辑器可能需要打开掘金编辑器调大屏幕体验会比较友好。
实现讲解
整个游戏的实现依赖于一个基础的方法 requestAnimationFrame
方法
1、requestAnimationFrame
会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
依赖这个方法,我们就能够实现整个页面的动效。
写一个提莫
关于整个提莫部分,我是使用了PS去进行抠图抠出来的gif,
这个并不是重点,重点的是我们要完成提莫移动的逻辑,逻辑完成了,哪怕是一个正正方方的盒子在那里都是可以的。
首先我们既然要实现点击的时候提莫朝着我们的鼠标移动,那么就要获取到鼠标点击的时候的坐标,以及提莫当前所在的坐标,通过在window上绑定一个点击事件,我们就能够拿到每次点击的坐标,提莫的坐标就更好获取了,只需要获得它对应的这个div元素即可。
在得到了两个坐标之后,就需要涉及到一点点小小的数学,我们都知道,两个点之间的运动,需要存在一个速度,并且这个速度对于 x 轴 和 y 轴的速度又会因为角度的不同而发生改变。
这个时候我们就需要根据鼠标点下的位置以及提莫当前的位置的夹角来计算 x 轴 的速度以及 y 轴 的速度。
通过两个关系式子我们就能够简单的计算出来,鼠标点击坐标以及提莫的夹角,就是速度的比例关系,能列出第一个式子,x 和 y 的速度的平方和,就是我们设定好的移动速度的平方,能列出第二个式子,这样我们就能够根据鼠标的点击计算出 x 轴和 y轴 一帧的移动。
function windowClick(e) {
cancelAnimationFrame(rafId);
ex = e.x - timo.clientWidth / 2;
ey = e.y - timo.clientHeight / 2;
let transFormX = ex - getPosition(timo.style.left);
let transFormY = ey - getPosition(timo.style.top);
let squareTransFormX = Math.pow(transFormX, 2);
let squareTransFormY = Math.pow(transFormY, 2);
let squareSpeed = Math.pow(speed, 2);
speedX = Math.sqrt(
(squareTransFormX / (squareTransFormX + squareTransFormY)) *
squareSpeed
);
speedY = Math.sqrt(
(squareTransFormY / (squareTransFormY + squareTransFormX)) *
squareSpeed
);
animloop(
ex - getPosition(timo.style.left) > 0,
ey - getPosition(timo.style.top) > 0
);
}
这样我们就能够完成一个会移动的提莫,但是这个时候,就会存在一些问题,提莫的移动似乎并不能随心所欲,那是因为我们没有对负值的情况作出判断,当我们的鼠标点击和提莫的位置关系发生上下改变的时候,这个时候提莫的偏移应该是要递减而不是递增的。
像上面这张图,点击1的位置和2的位置,提莫做出的移动是完全相反的,它的定位值改变也是完全相反的,我们需要对此做出判断。
function animloop(pOrNX, pOrNY) {
render(pOrNX, pOrNY);
rafId = requestAnimationFrame(() => {
animloop(pOrNX, pOrNY);
});
let elmX = getPosition(timo.style.left);
let elmY = getPosition(timo.style.top);
if (
elmX <= ex + 10 &&
elmX >= ex - 10 &&
elmY <= ey + 10 &&
elmY >= ey - 10
) {
cancelAnimationFrame(rafId);
}
}
function render(pOrNX, pOrNY) {
let newLeft = getPosition(timo.style.left) + (pOrNX ? speedX : -speedX);
let newTop = getPosition(timo.style.top) + (pOrNY ? speedY : -speedY);
timo.style.left = ` ${newLeft}px`;
timo.style.top = ` ${newTop}px`;
}
我们这里是通过设定了两个布尔值来判断,x 和 y 上是增加还是减少,这样子来完成提莫的移动,
并且在提莫移动到鼠标的位置的时候,做出一个范围判断,这里是因为js对于小数的计算是有限的,可能会产生误差,所以我们可以取一个范围,比如只要满足进入到鼠标点击的上下10px就停止移动,到这里就可以得到一个移动非常丝滑的提莫了。
障碍物
对于障碍物的编写,本文暂时就只写了一个简单的圆形,之后会继续添加别的障碍,对于圆形来说,就是一个从浏览器页面的一边移动到另一边的原型,我们需要在一开始的时候得到两个随机的坐标点,分别代表了浏览器的左侧的一个点以及右侧的一个点,这样我们才能够让这个元素从浏览器的一侧移动到另外一侧。
function missileRender(missile, missileEnd, pOrNX, pOrNY) {
let newLeft =
getPosition(missile.style.left) +
+(pOrNX ? difficultSpeedX : -difficultSpeedX);
let newBottom =
getPosition(missile.style.bottom) +
(pOrNY ? difficultSpeedY : -difficultSpeedY);
missile.style.left = setPosition(newLeft);
missile.style.bottom = setPosition(newBottom);
missileId = requestAnimationFrame(() => {
let timer = document.querySelector(".time");
timer.innerHTML = getScore();
missileRender(missile, missileEnd, pOrNX, pOrNY);
});
collisionDetection(missile);
if (
getPosition(missile.style.left) +
getPosition(missile.clientWidth) +
20 >
missileEnd[0]
) {
console.log("end");
body.removeChild(missile);
cancelAnimationFrame(missileId);
missileInit();
}
}
然后在这个圆形移动出浏览器的时候将它移除,并且创建下一个圆形,这里的这段逻辑应该是比提莫的移动要来的简单一些的,毕竟要考虑到的移动没有那么的多,在得到了障碍物以后,我们就需要来对提莫和障碍物做出碰撞检测了。
碰撞检测
那碰撞检测这边的逻辑就更加的简单了,因为只是要去判断题目和小球有没有存在重合的部分,我们可以通过小球以及提莫的坐标,加上两个人的大小,也就是宽高,来大概的定义出两个人的碰撞范围,并且当小球的左上,左下,右上,右下,四个点中的某一个进入到了小球的范围当中,就是判定当时两者出现了重合,这个时候就可以调用游戏结束的方法来结束游戏
function collisionDetection(missile) {
let missileTL = [
getPosition(missile.style.left) + 10,
getPosition(missile.style.bottom) - 10,
];
let missileTR = [
getPosition(missile.style.left) + 50,
getPosition(missile.style.bottom) - 10,
];
let missileBL = [
getPosition(missile.style.left) + 10,
getPosition(missile.style.bottom) - 50,
];
let missileBR = [
getPosition(missile.style.left) + 50,
getPosition(missile.style.bottom) - 50,
];
let timoBottom = bodyY - getPosition(timo.style.top);
let timoTL = [getPosition(timo.style.left) + 20, timoBottom - 90];
let timoTR = [getPosition(timo.style.left) + 80, timoBottom - 90];
let timoBL = [getPosition(timo.style.left) + 20, timoBottom - 180];
let timoBR = [getPosition(timo.style.left) + 80, timoBottom - 180];
function TR() {
return (
missileTL[0] <= timoTR[0] &&
missileBR[0] >= timoTR[0] &&
missileTL[1] >= timoTR[1] &&
missileBR[1] <= timoTR[1]
);
}
function TL() {
return (
missileTL[0] <= timoTL[0] &&
missileBR[0] >= timoTL[0] &&
missileTL[1] >= timoTL[1] &&
missileBR[1] <= timoTL[1]
);
}
function BR() {
return (
missileTL[0] <= timoBR[0] &&
missileBR[0] >= timoBR[0] &&
missileTL[1] >= timoBR[1] &&
missileBR[1] <= timoBR[1]
);
}
function BL() {
return (
missileTL[0] <= timoBL[0] &&
missileBR[0] >= timoBL[0] &&
missileTL[1] >= timoBL[1] &&
missileBR[1] <= timoBL[1]
);
}
if (TR() || TL() || BR() || BL()) {
gameEnd();
}
}
小补充
游戏的时间我们可以通过 Date
对象的 getTime
方法来获取,这样就可以进行成绩的统计,还有就是游戏结束后的重置部分,我们需要移除对象上的监听,以及复原所有的参数,重新显示点击按钮,重置提莫的位置,具体的代码在这里就不在多说了,可以直接上代码看一下。
游戏还有大部分的东西没有完成,类似于现在的碰撞物只有一个,并且移动轨迹也还不够随机,在之后会找时间继续完成这个小游戏,尽量做到和steam上的游戏的还原。
总结
本文介绍了一款走位小游戏的开发,虽然游戏还未完成,但是这个游戏的制作中存在需要可以学习的东西,相信通过一款这种比较综合的游戏的制作,也能够对js有更加深入的了解,并且还可以锻炼一下p图的技巧,早日成为全干工程师。