• 《uni-app》一个非canvas的飞机对战小游戏实现-敌机模型实现


    在这里插入图片描述

    这是一个没有套路的前端博主,热衷各种前端向的骚操作,经常想到哪就写到哪,如果有感兴趣的技术和前端效果可以留言~博主看到后会去代替大家踩坑的~接下来的几篇都是uni-app的小实战,有助于我们更好的去学习uni-app~
    主页: oliver尹的主页
    格言: 跌倒了爬起来就好~
    准备篇:https://oliver.blog.csdn.net/article/details/127185461
    启动页实现:https://oliver.blog.csdn.net/article/details/127217681

    一. 前言

    上一篇中主要实现的是游戏的启动页,启动页作是整个游戏的第一个界面,界面本身并不复杂,一共才4个元素,相对复杂一点的也就是小飞机的穿梭动画,其核心实现也不过就是一个 animation,但是,这个界面确是相当重要,只有当点击“开始游戏”按钮之后,我们才会开始正式游戏~

    本文主要分享的内容为 敌机模型 的实现,耐心看完,或许你会所有收获~

    二. 阅读对象与难度

    本文难度属于:中级,本文中 主要实现的敌机模型相关的操作,包括坐标位置初始化,敌机类型初始化,敌机位移等等,通过文本你可以大致了解到一下内容

    • Vue中的基础知识,包括v-for等常规用法;
    • requestAnimationFrame以及其使用方式;
    • JavaScript相关的一些知识;

    具体内容可以参考以下的思维导图:
    在这里插入图片描述

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

    文本代码已上传CSDN上的gitCode,有兴趣的小伙伴可以直接clone,项目地址:https://gitcode.net/zy21131437/planegameuni
    如果有小伙伴愿意点个星,那就非常感谢了~最终效果图如下:
    在这里插入图片描述

    四. 敌机模型的实现

    4.1 分析分析

    根据上面的效果图,我们先分析一下要实现的功能:

    1. 通过效果图确认,敌机的类型一共有两种,小飞机大飞机,需要分别实现样式;
    2. 敌机的随机坐标生成,点击“开始游戏”的时候,敌机出现的x轴坐标是随机生成的,因此不会一成不变的在某个位置上出现~
    3. 敌机在Y轴上实现的位移;

    大致上这三个功能在本文这个阶段是最主要的,再来估计一下如果要实现这几个功能可能要用到什么实现逻辑
    第一个,敌机样式
    敌机样式,这一块其实包含两部分:

    • 第一部分,大小飞机的UI,不管是大敌机还是小敌机,其实就是DOM元素,加上背景图,这一点应该是毋庸置疑的;
    • 第二部分,爆炸动画,当敌机(包括大飞机和小飞机)被摧毁时,会有一个爆炸的效果动画,既然是动画那肯定是animation;

    第二个,随机坐标
    首先说一下坐标,坐标肯定是有x和y的,从效果上看y轴上的坐标是在UI之外,这个是固定的,x轴是随机的,既然是随机的,那估计是一个 随机数*x轴的长度,生成了一个值,然后敌机在这个坐标上创建一个敌机DOM;

    第三个,位移
    重头戏来了,如果不知道实现,可能会觉得这个非常复杂,实际上很简单的,但是这个方式可能比较冷门,叫requestAnimationFrame,具体我们后面再看;

    4.2 敌机样式

    先来看看敌机,我们的敌机一共有两种:小飞机大飞机,他们的素材图如下:
    小飞机
    在这里插入图片描述

    大飞机
    在这里插入图片描述

    可能看到这会有一点点疑惑,不对啊,怎么是这种图片,我们其实可以这么理解,先看一个示意图
    在这里插入图片描述
    正常情况下,默认显示最左边的敌机,也就是 正常状态下的敌机,当这个敌机模型接收到被摧毁的信号时,触发爆炸动画,此时的敌机模型只需要 通过animation将右侧隐藏的图片分步显示出来,形成一个动画,具体完整代码如下:

    <template>
    	<view :class="getEnemyClass" >view>
    template>
    
    <script>
    export default {
    	props: {
    		data: {
    			type: Object,
    			default: () => {
    				return {};
    			}
    		},
    	},
    	computed: {
    		getEnemyClass() {
    			const classStyle = [`enemy_${this.data.type}`];
    			const explosion = {};
    			explosion[`enemy_${this.data.type}_effect`] = this.data.isExplosion;
    			classStyle.push(explosion);
    			return classStyle;
    		}
    	},
    };
    script>
    <style scoped lang="scss">
    .enemy_1 {
    	width: 59px;
    	height: 36px;
    	position: fixed;
    	z-index: 1;
    	background: url(@/static/images/enemy1.png) no-repeat left top;
    }
    
    .enemy_1_effect {
    	animation: enemy_1_animate 0.5s steps(5) both infinite;
    	-webkit-animation: enemy_1_animate 0.5s steps(5) both infinite;
    }
    /* 敌机1-爆炸效果 */
    @keyframes enemy_1_animate {
    	0% {
    		background-position: 0 0;
    	}
    
    	100% {
    		background-position: -295px 0;
    	}
    }
    
    @-webkit-keyframes enemy_1_animate {
    	0% {
    		background-position: 0 0;
    	}
    
    	100% {
    		background-position: -295px 0;
    	}
    }
    /* 敌机2 */
    .enemy_2 {
    	width: 70px;
    	height: 92px;
    	position: fixed;
    	z-index: 1;
    	background: url(@/static/images/enemy2.png) no-repeat left top;
    }
    
    .enemy_2_effect {
    	animation: enemy_2_animate 0.5s steps(6) both infinite;
    	-webkit-animation: enemy_2_animate 0.5s steps(6) both infinite;
    }
    
    /* 敌机2-爆炸效果 */
    @keyframes enemy_2_animate {
    	0% {
    		background-position: 0 0;
    	}
    
    	100% {
    		background-position: -420px 0;
    	}
    }
    
    @-webkit-keyframes enemy_2_animate {
    	0% {
    		background-position: 0 0;
    	}
    
    	100% {
    		background-position: -420px 0;
    	}
    }
    style>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    完整代码差不多就是如上,我们分布看一下,首先是template

    <template>
    	<view :class="getEnemyClass" >view>
    template>
    
    • 1
    • 2
    • 3

    template部分非常简洁,通过一个名为 getEnemyClass的计算属性 获取完整的类名,接着这个计算属性

    getEnemyClass() {
      const classStyle = [`enemy_${this.data.type}`];
      const explosion = {};
      explosion[`enemy_${this.data.type}_effect`] = this.data.isExplosion;
      classStyle.push(explosion);
      return classStyle;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    通这个属性中可以看到,最终返回出去的是一个数组,数组一共有两项,第一项是一个字符串,第二项是一个对象,举个例子吧,假设this.data.type的值是1,那么这个计算属性最终返回的结果是这个

    ["enemy_1",{"enemy_1_effect":this.data.isExplosion}]
    
    • 1

    我们放到css中去看一下,这个两个对应的样式

    .enemy_1 {
    	width: 59px;
    	height: 36px;
    	position: fixed;
    	z-index: 1;
    	background: url(@/static/images/enemy1.png) no-repeat left top;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    标准DOM样式,宽,高,背景图等等,另外一个

    .enemy_1_effect {
    	animation: enemy_1_animate 0.5s steps(5) both infinite;
    	-webkit-animation: enemy_1_animate 0.5s steps(5) both infinite;
    }
    /* 敌机1-爆炸效果 */
    @keyframes enemy_1_animate {
    	0% {
    		background-position: 0 0;
    	}
    
    	100% {
    		background-position: -295px 0;
    	}
    }
    
    @-webkit-keyframes enemy_1_animate {
    	0% {
    		background-position: 0 0;
    	}
    
    	100% {
    		background-position: -295px 0;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这是一个动画,关于animation的用法具体可以参考上一篇,里面有详细的用法解释,简单的说,就是当 this.data.isExplosion 的值为true的时候,enemy_1_effect这个类名将会被添加到这个DOM上,当类名被添加的同时会立即执行名为enemy_1_animate的动画,该动画会在0.5秒内分成5步显示完,动画改变的背景图的坐标,将x轴从0变到了-295px,实现了动画效果;
    敌机类型一共有两种,其实原理一样,相同的代码实现后最终呈现的效果图如下:
    在这里插入图片描述

    4.3 敌机随机类型的实现

    既然游戏存在多种类型的敌机,那么游戏开始后,每一次敌机的生成其类型都应该是具有随机性的,因此需要有一个随机数的生成;
    以本游戏为例,我们一共有两种敌机, 那么随机数的生成函数

    /**
     * 敌机类型
     */
    enemyType() {
      let random = Math.round(Math.random() * 10);
      return random < 5 ? 1 : 2;
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    通过一个简单的随机数生成器或者一个 1 或者 2,目的是为了拼接字符串,比如当值为1的时候,最终拼接会拼接成

    ["enemy_1",{"enemy_1_effect":this.data.isExplosion}]
    
    • 1

    如果是2,最终会拼接成

    ["enemy_2",{"enemy_2_effect":this.data.isExplosion}]
    
    • 1

    那么到这里,我们基本可以这些写,上面的敌机模型是单独一个文件,接着通过 props 传入了敌机的类型,这个类型最终会通过计算属性getEnemyClass被拼接到dom上
    在这里插入图片描述

    4.4 敌机生成的实现

    通过上面两个小节,到这里其实我们已经可以有点眉目了,大致的流程是,敌机的文件是一个单独的.vue文件,里面有敌机模型的所有参数功能,当参数创建成功后存入敌机数组,通过v-for将敌机数组遍历,其它的类似于敌机类型等参数的则是通过props传入的敌机组件
    在这里插入图片描述

    因此,在父级,我们可以这么写

    
    <Enemy
      v-for="(enemy, index) in enemyData"
      :data="enemy"
      :key="enemy.id"
    />
    
      <script>
    import Enemy from '../view/enemy/enemy.vue';
    export default {
    	data() {
    		return {
    			enemyData: [],
    		};
    	},
    	components: { Enemy },
    	methods: {
    		/**
    		 * 敌机类型
    		 */
    		enemyType() {
    			let random = Math.round(Math.random() * 10);
    			return random < 5 ? 1 : 2;
    		},
    		initEnemy() {
    			// 创建飞机参数
    			const createEnemyParam = () => {
            // 随机生成敌机类型
    				const enemyType = this.enemyType();
    
    				return {
    					type: enemyType,
    					id: `enemy` + new Date().getTime(),
    					isExplosion: false
    				};
    			};
    
    			// 创建敌机
    			const createEnemy = () => {
    				const param = createEnemyParam();
    				this.enemyData.push(param);
    			};
    
    			createEnemy();
    			this.enemyTimer = setInterval(createEnemy, 1500);
    		},
    	}
    };
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    子组件,也就是敌机组件则是如下,通过props将父组件中的敌机配置参数传入子组件

    <script>
    export default {
    	props: {
    		data: {
    			type: Object,
    			default: () => {
    				return {};
    			}
    		},
    	},
    };
    script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4.5 敌机坐标的实现

    在分析中我们有说到,敌机每次生成的坐标在y轴上都是在在屏幕外,而在x轴上则是根据屏幕宽度随机生成,大致位置如下:
    在这里插入图片描述

    因此,在y轴上我们可以将坐标固定在-敌机尺寸,比如敌机的DOM元素高度为36px,那么设定敌机在y轴上距离顶部的距离为-36px,即可把敌机在y轴彻底隐藏;
    至于x轴,先看一段代码:

    const x = (this.config.winWdith - (enemyType === 1 ? 59 : 70)) * Math.random();
    
    • 1

    什么意思呢,简单的说,就是屏幕宽度 - 敌机宽度之后,乘以一个0~1之间的随机数,即可获得一个大于0,小于屏幕宽度的数值,这个数值即是在x轴上的坐标,因此父组件在创建敌机参数的时候完整代码如下:

    initEnemy() {
      // 创建飞机参数
      const createEnemyParam = () => {
        const enemyType = this.enemyType();
        const x = (this.config.winWdith - (enemyType === 1 ? 59 : 70)) * Math.random();
    
        return {
          type: enemyType,
          x,
          y: enemyType == 1 ? -36 : -90,
          id: `enemy` + new Date().getTime(),
          isExplosion: false
        };
      };
    
      // 创建敌机
      const createEnemy = () => {
        const param = createEnemyParam();
        this.enemyData.push(param);
      };
    
      createEnemy();
      this.enemyTimer = setInterval(createEnemy, 1500);
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    子组件接收

    <template>
    	<view :class="getEnemyClass" :style="{ left: data.x + 'px', top: data.y + 'px' }">view>
    template>
    
    • 1
    • 2
    • 3

    在子组件中直接设置left属性以及top属性来控制敌机的坐标方位;

    4.6 敌机位移的实现

    这里先简单说一下,位移的功能是通过requestAnimationFrame这个方法实现的,使用也非常简单,这是一个JavaScript原生的方法,具体是什么我们单独出一篇进行详细解释
    位移的实现代码还是简单的,具体如下:

    <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    五. 小结

    本文主要概述了敌机模型的实现,主要包含:

    • 敌机样式:其实就是设定好不同的DOM,加入背景图,预设好爆炸的CSS动画;
    • 敌机类型:当写好样式后,通过随机数生成一个样式的标志,拼接加入样式,生成对应的敌机;
    • 敌机生存:创建敌机配置参数,加入敌机的缓存数组,通过v-for指令循环生成敌机;
    • 敌机坐标:y坐标上在屏幕的上方,x坐标通过随机数生成;
    • 敌机位移:位移通过requestAnimationFrame实现;

    其实从代码量来看,它的实现在这个阶段其实并不复杂,剩下的一个核心功能就是碰撞检测以及触发碰撞信号时进行敌机的移除,当然,除此之外还有一种情况敌机没有被摧毁,只是运动超出了屏幕,这种也需要将敌机移除,关于移除的功能,我们放在碰撞检测中分析;

    已经看到这里了,请点个赞吧,谢谢~~~ 下一篇我们将来重点研究一下 requestAnimationFrame

  • 相关阅读:
    AR导览软件定制开发方案
    Python与数据分析--Matplotlib-2
    【Linux】JREE项目部署与发布
    Hugging Face 分词器新增聊天模板属性
    【牛客】SQL127 月总刷题数和日均刷题数
    Web——期末作业
    「MySQL高级篇」explain分析SQL,索引失效&&常见优化场景
    卡尔曼滤波器(目标跟踪一)(上)
    pycharm 远程运行报错 Failed to prepare environment
    Qt应用程序打包步骤(完美解决)
  • 原文地址:https://blog.csdn.net/zy21131437/article/details/127332264