《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解
这是一个没有套路的前端博主,热衷各种前端向的骚操作,经常想到哪就写到哪,如果有感兴趣的技术和前端效果可以留言~博主看到后会去代替大家踩坑的~接下来的几篇都是uni-app的小实战,有助于我们更好的去学习uni-app~
主页: oliver尹的主页
格言: 跌倒了爬起来就好~
准备篇:https://oliver.blog.csdn.net/article/details/127185461
启动页实现:https://oliver.blog.csdn.net/article/details/127217681
敌机模型的实现:https://oliver.blog.csdn.net/article/details/127332264
一. 前言
上一篇中主要实现的是敌机模型的实现,如果我们可以将敌机模型视作为一个JavaScript的类,它这个类上包含了,创建,坐标生成,位移,碰撞检测等等方法,这些方法完整的构成了一个敌机模型,上一篇别的我们都介绍了,唯独位移这个没有细说,那么本篇主要介绍的就是位移的实现以及其实现方法:requestAnimationFrame;
耐心看完,或许你会所有收获~
二. 阅读对象与难度
本文难度属于:中级,主要分享的内容为 模型位移的实现,其实不仅仅是敌机模型,我方控制的飞机,子弹,都是需要位移,通过文本你可以大致了解到一下内容
- requestAnimationFrame以及其使用方式;
- 敌机位移的实现;
具体内容可以参考以下的思维导图:
三. 项目地址以及最终效果
文本代码已上传CSDN上的gitCode,有兴趣的小伙伴可以直接clone,项目地址:https://gitcode.net/zy21131437/planegameuni
如果有小伙伴愿意点个星,那就非常感谢了~最终效果图如下:
四. requestAnimationFrame
4.1 介绍
首先来说说什么是 requestAnimationFrame?
Mozilla官方解释:window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行;这段话怎么理解呢?
首先,我们可以明确:requestAnimationFrame是JavaScript上的原生方法,虽然用的时候可以这么用,如下
requestAnimationFrame();
// 实际上的全写
window.requestAnimationFrame()
它是挂载在window对象上的,因此,在没有window对象的环境下,该方法不可使用;
其次,原文说到:你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画,这就是说requestAnimationFrame用于执行动画的,它有点类似于使用 setTimeout,setInterval,通过无限循环执行实现动画;
最后,原文说到:该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,
意思是它接受一个函数作为参数,这个函数用于执行动画的,比如在函数中修改y轴坐标,那么无限循环下DOM在Y轴上的坐标将不断变化,从而实现了一个动画~
到这里,我们大致知道了 requestAnimationFrame 这是一个啥,简单的说,就是 渲染动画的函数,它接收一个具体执行DOM变化的函数作为参数,通过无限循环执行这个函数达到了动画的效果~
4.2 异步还是同步?
看一个函数首先就是 异步还是同步的问题,requestAnimationFrame 是异步还是同步?做一个实验
setTimeout(() => {
console.log('--- setTimeout 1 ---');
});
window.requestAnimationFrame(() => {
console.log('--- requestAnimationFrame 2 ---');
});
async function testAsync() {
console.log('--- testAsync 3 ---');
await testAsync2();
console.log('--- testAsync 4 ---');
}
async function testAsync2() {
console.log('--- testAsync 5 ---');
}
testAsync();
new Promise((resolve) => {
console.log('--- promise 6 ---');
resolve();
console.log('--- promise 7 ---');
}).then(() => {
console.log('--- promise 8 ---');
});
console.log('--- promise 9 ---');
结果如下:
结果很明显,requestAnimationFrame它是一个异步函数;
4.3 JS实现动画
在具体聊 requestAnimationFrame 之前,我们先思考一下,假如现在有一个面试题,或者是有一个需求,要求是是使用JS实现一个动画,该怎么做?
正常情况下,我们第一时间可能会想到 setTimeout,setInterval,假设动画的帧率是60帧,那么我只需要这么写就行
setInterval(()=>{
// 具体执行的代码
}, 1000 / 60)
通过setInterval实现了一个循环动画,为什么参数是 1000 / 60,那就说到另外一个原理了,动画的本质其实就是将一张张切片连接起来,当这个连接或者说切换的速度够快时,人眼将无法区分是单独的每一张,从而形成一个不卡顿的连续动画,看起来像是活了一样,动起来了;拿这边举例,当1000毫秒也就是1秒内,连续移动超过60次,那么自然而然人眼看起来就是一副完整的动画;
因此,如果使用setInterval实现了一个简易动画,我们可以这么写
<template>
<div class="demo" :style="{ left: x + 'px', top: y + 'px' }"> </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const x = ref(0);
const y = ref(40);
setInterval(() => {
x.value = x.value + 1;
}, 1000 / 60);
</script>
<style scoped lang="less">
.demo {
position: absolute;
width: 100px;
height: 100px;
background-color: red;
}
</style>
其效果如下:
既然setInterval已经可以实现动画,那么requestAnimationFrame 的意义是什么呢?不着急,继续往下试验;
4.4 requestAnimationFrame 实现动画
依然是上方那个动画,如果使用requestAnimationFrame怎么实现呢,大致如下:
<template>
<div class="demo" :style="{ left: x + 'px', top: y + 'px' }"> </div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
// setInterval(() => {
// x.value = x.value + 1;
// console.log(x.value);
// // y.value = y.value + 1;
// }, 1000 / 60);
const move = () => {
x.value = x.value + 1;
window.requestAnimationFrame(move);
};
move();
</script>
<style scoped lang="less">
.demo {
position: absolute;
width: 100px;
height: 100px;
background-color: red;
}
</style>
对应效果图如下:
从效果图上看两者感觉差不多,从用法上看,requestAnimationFrame 的用法貌似更接近setTimeout,而不是setInterval,当然,用法还是既然已经有了setTimeout 和 setInterval了,为什么还要requestAnimationFrame?那当然是为了解决一些瓶颈的问题;
4.5 requestAnimationFrame优点
第一个优点:执行函数的时间更加精准
我们知道 setTimeout 和 setInterval 都是借助于浏览器的定时器线程执行的,因此,当界面上存在大量异步的时候,会堵塞异步代码的执行,在这种情况下setTimeout 和 setInterval 将变得不准时,反映到界面上的效果就是动画执行的过程中会有卡顿,动画整体不再流畅;这是一个硬伤,如果出现的频繁,那么用户体验将变得极差,不可接受;
那requestAnimationFrame 呢?上面我们测试过了,requestAnimationFrame 也是一个异步函数,它会不会也有这种问题,实际上不会,最大的原因是,它执行的时机造成的,requestAnimationFrame 的执行时机是由系统决定的,每一次页面进行刷新时都会执行一次requestAnimationFrame 函数,举个例子吧,可能容易明白点;
比如:我们的电脑刷新率是60Hz,那么这就代表1秒内刷新了60次,刷新的间隔为 1000 / 60 毫秒,每一次进行刷新的时候,浏览器会主动去执行一次 requestAnimationFrame 函数,这将大大的提升精准度,那如果电脑的刷新率是75Hz呢,那刷新间隔是 1000 / 75;
所以,在精准度上,requestAnimationFrame 的效果远好于 setTimeout 和 setInterval,因为它的执行间隔不由代码决定,而由系统决定,适用性更强;
第二个优点:性能更好
这点体现在执行时页面的重绘与回流,我们知道重绘与回流是影响DOM性能最大的因素之一,当使用 setTimeout 和setInterval 等时,就是正常的重绘与回流,但requestAnimationFrame不同,它会把每一帧中需要执行的操作或者说动画步骤都给收集起来,在一次重绘或回流中就完成全部的操作;
除此之外,当网页处于hidden状态时,requestAnimationFrame会被冻结,不再执行,这也会节省电脑的CPU和GPU资源;
五. 敌机模型位移的实现
到这里,我们再回看一下上一篇中关于敌机位移的实现,代码如下:
<script>
export default {
props: {
data: {
type: Object,
default: () => {
return {};
}
},
},
data() {
return {
moveTimer: null
};
},
methods: {
move() {
if (this.data.y < 300) {
//敌机的加速度
let speed = this.data.type === 1 ? 0 : 0.5;
this.data.y += this.enemyY + speed;
} else {
this.remove();
}
},
init() {
this.moveTimer = () => {
//敌机移动
this.move();
// 重绘,无限循环
requestAnimationFrame(this.moveTimer);
};
this.moveTimer();
},
},
};
</script>
一共两个方法:init 和 move ,我们分别看一下
init函数
代码不复杂,通过requestAnimationFrame实现了一个无限循环动画,该动画主要实现的位移逻辑是由this.move()实现的;
move函数
首先是一个if判断,判断当前在y轴上的坐标值是否小于300,如果小于300修改DOM在y轴上的坐标,修改值也可以称作与加速度,其值取决于敌机的类型,敌机的类型如果是小飞机,那么每次加速度是 默认速度+0.5,大飞机则是 默认速度,这也就实现了不同的敌机类型飞行速度不一致的效果;
那么这个300是什么意思?这个300在这里只是一个演示值,正常情况下应该是 屏幕的高度,作用是:当飞机在屏幕的y坐标大于屏幕时,就应该将超出屏幕的飞机移除,毕竟我们不可能让飞机永远存在,当其超出屏幕的情况下,我方飞机依然不可能击毁它,因此我们需要及时销毁;
故以上代码最终能实现的效果图就是如下:
六. 小结
本文主要概述了requestAnimationFrame,通过本文我们知道:
- requestAnimationFrame这事一个 原生的,帧动画函数,它可以让我们通过它来实现JS动画;
- 相比 setTimeout 和 setInterval,不管从性能上,还是动画的流程度上requestAnimationFrame都更优,因此,如果是需要使用JS实现动画,可以选择requestAnimationFrame;
- requestAnimationFrame是 异步函数,它执行的时机在于每次系统帧刷新前;
最后,我们重新回看了一下敌机的位移实现,发现其实并不复杂,就是利用requestAnimationFrame不断改变敌机在y轴上的坐标从而实现了敌机的位移动画效果,当然,至少这个阶段的敌机实现还并不复杂~