《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解

时间:2022-10-19 21:53:23

《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以及其使用方式;
  • 敌机位移的实现;

具体内容可以参考以下的思维导图:
《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解

三. 项目地址以及最终效果

文本代码已上传CSDN上的gitCode,有兴趣的小伙伴可以直接clone,项目地址:https://gitcode.net/zy21131437/planegameuni
如果有小伙伴愿意点个星,那就非常感谢了~最终效果图如下:
《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解

四. requestAnimationFrame

4.1 介绍

首先来说说什么是 requestAnimationFrame?
Mozilla官方解释:window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行;这段话怎么理解呢?
首先,我们可以明确:requestAnimationFrame是JavaScript上的原生方法,虽然用的时候可以这么用,如下

requestAnimationFrame();

// 实际上的全写
window.requestAnimationFrame()

它是挂载在window对象上的,因此,在没有window对象的环境下,该方法不可使用;

其次,原文说到:你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画,这就是说requestAnimationFrame用于执行动画的,它有点类似于使用 setTimeoutsetInterval,通过无限循环执行实现动画;

最后,原文说到:该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
意思是它接受一个函数作为参数,这个函数用于执行动画的,比如在函数中修改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 ---');

结果如下:
《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解

结果很明显,requestAnimationFrame它是一个异步函数

4.3 JS实现动画

在具体聊 requestAnimationFrame 之前,我们先思考一下,假如现在有一个面试题,或者是有一个需求,要求是是使用JS实现一个动画,该怎么做?
正常情况下,我们第一时间可能会想到 setTimeoutsetInterval,假设动画的帧率是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>

其效果如下:
《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解

既然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>

对应效果图如下:
《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解

从效果图上看两者感觉差不多,从用法上看,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坐标大于屏幕时,就应该将超出屏幕的飞机移除,毕竟我们不可能让飞机永远存在,当其超出屏幕的情况下,我方飞机依然不可能击毁它,因此我们需要及时销毁;
故以上代码最终能实现的效果图就是如下:
《uni-app》一个非canvas的飞机对战小游戏实现-requestAnimationFrame详解

六. 小结

本文主要概述了requestAnimationFrame,通过本文我们知道:

  • requestAnimationFrame这事一个 原生的,帧动画函数,它可以让我们通过它来实现JS动画;
  • 相比 setTimeout 和 setInterval,不管从性能上,还是动画的流程度上requestAnimationFrame都更优,因此,如果是需要使用JS实现动画,可以选择requestAnimationFrame;
  • requestAnimationFrame是 异步函数,它执行的时机在于每次系统帧刷新前;

最后,我们重新回看了一下敌机的位移实现,发现其实并不复杂,就是利用requestAnimationFrame不断改变敌机在y轴上的坐标从而实现了敌机的位移动画效果,当然,至少这个阶段的敌机实现还并不复杂~