Vue网页小音游(malody青春版)
书接上回,当写完了登录注册的功能的时候,我就在想登录之后进入的页面能写一些什么有意思的东西,一开始还没开始写登录的时候实际上是想做一个网页版的malady,但是因为一直找不到游戏内的素材文件,这个想法就实现不了了。恰巧在找素材的时候碰巧在github上找到了一个用纯js写的网页小音游,虽然不是vue写的,但是也值得我参考一番。
页面整体构建
游戏部分的实现思路主要是将游戏块,也就是玩家游玩的部分在js部分动态添加和管理(但在vue实现这个部分有点问题,这里先按下不表,稍后再讲),在函数将需要的元素节点添加到页面上时,会自动根据默认的数值添加该元素节点的大小以及样式,而非游戏块部分,也就是设置和分数显示部分,是在前端页面上写好的,在js部分的操作只是控制该元素的显示和隐藏而已。下面我将把页面分几个地方讲解一下:
模版
首先在vue模版的设计中,共有两个部分,在下层的gameBody部分和在上层welcome部分,gameBody是由函数动态添加的游戏层,用途为提供给玩家游玩,游玩成绩的显示等功能。welcome部分是已经写好的设置页面,玩家可以在该页面根据自己的游玩需要,更改游戏的默认数据,比如说游戏块的背景图片,需要的note排布,或者更改自己熟悉的键盘键位,以及一些其他设置。
<template>
<div v-if="isDesktop">
<div id="gameBody" ref="gameBody">
//<!-- 详情略 -->
</div>
<div id="welcome" ref="welcome">
//<!-- 详情略 -->
</div>
</div>
</template>
具体实现过程
虽然有前人的代码参考,但是在将代码转vue的过程中遇到的坑可是真的不少,关于动态添加的部分我几乎把代码改了个遍。只能说js和vue的区别还是很大的。
在gameBodyBG的动态创建上,一开始先入为主的使用了js原生的动态添加写法,使用了v-html 和 v-if 来让该元素节点显示在页面上,但是这么做有个问题,就是虽然在查看控制台时能够发现该元素已经被创建在了页面中,但是在js后台不管怎么样获取该元素节点始终会有问题(为undefined,为null,为空数组等)。
经过排查后发现,原因是因为vue动态响应系统自动将新创建的Node包装了起来,访问属性变为了只读,无法在内部进行样式的修改。所以只能改变写法,使用 v-for 将需要的元素节点添加进dom中,这样就可以在vue内部使用 ref属性 进行访问,获取到该元素节点。
<div id="GameLayerBG" ref="GameLayerBG" v-if="showGameLayer">
<div v-for="i in 2" :key="i" :id="'GameLayer' + i" :ref="'GameLayer' + i" class="GameLayer">
<div v-for="j in (k * 2 >= 10 ? k * 2 : k * 3)" :key="j" :id="'GameLayerChildern' + i " :ref="'GameLayerChildern' + i">
<div v-for="l in k"
:key="l"
:id="'GameLayer' + i + '-' + (l + (j - 1) * k)"
:num="l + (j - 1) * k"
:class="'block' + (l ? ' bl' : '')"
></div>
</div>
</div>
</div>
这样建好了两个游戏层,gameLayer1和gameLayer2,在创建的过程中需要判断玩家k(按键数量)的取值来判断游戏块的创建数量,所以第二层循环决定了最内层游戏块的创建数量,根据计算可以发现,假设游玩方式为4key,最终在gameLayer1中游戏块的数量就为 一共48个,但是这么做就导致了一个问题,在不创建新的变量时,获取到这些游戏块的方式就显得异常复杂。
至此,遇到的第一个难题解决了。但是迎面而来的就是刚才说的第二个问题,当我按照第一种思路在写获取游戏块的代码时,我希望将 gameLayer1 、2 元素添加到一个数组内部,以便后续会这两个元素节点及其子节点进行操作,但是在实际操作中会发现,实际操作的子节点已经变成了那12个子节点,而非最内层的游戏块,在思考下,我在循环中为第二层的div添加了 ref属性 ,将 i 与之相绑定,这样在添加时就可以根据对应关系来进行添加,获取时也能根据对应关系来获取,现在当得到这 12 个元素节点时,只需要将其对应的 children div 使用循环添加到相应的数组内,这样就和第一种的思路的结果相吻合了。
// 节选部分关键代码片段
mounted(){
this.GameLayerChildern1 = this.$refs.GameLayerChildern1;
this.GameLayerChildern2 = this.$refs.GameLayerChildern2;
for (let i = 0; i < this.GameLayerChildern1.length; i++) {
this.ForeachGameLayerChildern1.push(this.GameLayerChildern1[i]); //把第二层循环添加的 div元素节点 添加进该数组
this.ForeachGameLayerChildern2.push(this.GameLayerChildern2[i]);
}
for (let j = 0; j < this.ForeachGameLayerChildern1.length; j++) { // 通过循环和 children属性 把第三层的div元素节点添加进数组
this.threeChildren1.push(...Array.from(this.ForeachGameLayerChildern1[j].querySelectorAll('div')));
this.threeChildren2.push(...Array.from(this.ForeachGameLayerChildern2[j].querySelectorAll('div')));
}
this.threeChildren1 = this.threeChildren1.flat();
this.threeChildren2 = this.threeChildren2.flat();
}
当能够正确的获取元素,操控元素的样式自然也是不在话下,但是问题显然并没有就此结束。当处理好点击对应游戏块后游戏块消失的逻辑之后(居然这个地方神奇的没错)。问题来了,想象中的效果为,当点击一个游戏块之后,调用函数计算出这个游戏块的长度,用整个游戏层减去该游戏块的长度,并返回且动态赋值给整个游戏层的style translateY
就可以实现点击后移动的效果。
这里得先讲讲游戏能一直移动下去的原理。在该设计中,共有两个游戏层gameLayer1和gameLayer2,gameLayer1游戏层后紧跟着gameLayer2游戏层,玩家从gameLayer1开始游玩,点击一个游戏块,gameLayer1向下平移一个游戏块的单位长度,而gameLayer2则是从 (gameLayer1 + 一个游戏块)的长度平移到 一个gameLayer1 的长度,当gameLayer1中的48个元素结束时紧接着点击第一个gameLayer2中的游戏块时,gameLayer1则又排到gameLayer2的后面,如此往复就可以一直游玩下去。
回到刚才的问题,如何才能将移动效果实现?
这里有两个重要的点,一是能够同时得到gameLayer1和gameLayer2的元素节点(或者说几乎同时),二是能够准确的刷新游戏层的位置。然而在具体的代码实现中,仍然有很多的坑要踩(现在的代码硬是熬出来的)。需要注意游戏层node的获取和游戏块node的获取,需要用这两个元素节点的高度来调控 translateY 的值,同时,在逻辑上也要注意每个元素节点的含义,不要被绕晕了(又是一把心酸泪)。
methods(){
gameLayerMoveNextRow() { // 移动游戏层 并 刷新
for (let i = 1; i <= 2; i++) {
let elementGameLayer = `gameLayerElement${i}`;// 通过插值语法得到gameLayer的元素节点名称
let g = this[elementGameLayer]; // 通过 this 指向gameLayer的元素节点
let elementChildren = `threeChildren${i}`; // 通过插值语法拿到 游戏块 的元素节点名称
let l = this[elementChildren]; // 通过 this 指向 游戏块 的元素节点
g.y += this.blockSize; // 同步元素节点的长度
if (g.y > this.blockSize * (Math.floor(l.length / this.k))) { // 什么时候移动什么时候刷新
this.refreshGameLayer(l, 1, -1); // 调用刷新方法
} else {
g.style[this.transform] = `translateY(${g.y}px)`; // 移动
}
}
},
}
函数调用:
下面是几个比较重要的函数的实现逻辑:
- 挂载完成之后,执行mounte钩子函数初始化变量,同时创建各种监听函数,调用gameInit、initSetting创建页面node和更新初始值,初始化时需要用
$nextTick
保证页面上已经将这些元素加载了出来
mounted() {
this.$nextTick(() => {
if (this.isDesktop) { // 如果为pc端就监听按键
// 创建按键事件监听函数
const handleKeyDown = (e) => {
const key = e.key.toLowerCase();
// 检查按下的键是否存在于map中且游戏正在进行中
if (Object.keys(this.map).includes(key) && this.isplaying()) {
this.click(this.map[key]);
}
// 检查按下的键是否为'R'且分数显示层可见
if (key === 'r' && this.GameScoreLayer.style.display !== 'none') {
this.gameRestart();
this.GameScoreLayer.style.display = 'none';
}
}
// 添加按键事件监听
document.addEventListener('keydown', handleKeyDown);
// 销毁组件时移除按键事件监听
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keydown', handleKeyDown);
});
}
this.len = this.key.length;//
this.isDesktop = navigator.userAgent.match(/(ipad|iphone|ipod|android|windows phone)/i) ? false : true;
this.fontunit = this.isDesktop ? 20 : ((window.innerWidth > window.innerHeight ? window.innerHeight : window.innerWidth) / 320) * 10;
this.showWelcomeLayer();
this.body = this.$refs.gameBody;
this.body.style.height = window.innerHeight + 'px';
this.transform = typeof (this.body.style) !==
'undefined' ? 'webkitTransform' : typeof (this.body.style.msTransform) !== 'undefined' ? 'msTransform' : 'transform';
this.transitionDuration = this.transform.replace(/ransform/g, 'ransitionDuration');
this.GameTimeLayer = document.getElementById('GameTimeLayer');
// 初始化游戏层的值
this.GameLayer1 = this.$refs.GameLayer1; // 获取到 GameLayer1 元素节点
this.GameLayer2 = this.$refs.GameLayer2; // 获取到 GameLayer2 元素节点
this.gameLayerElement1 = this.GameLayer1[0];
this.gameLayerElement2 = this.GameLayer2[0];
this.GameLayerChildern1 = this.$refs.GameLayerChildern1;
this.GameLayerChildern2 = this.$refs.GameLayerChildern2;
for (let i = 0; i < this.GameLayerChildern1.length; i++) {
this.ForeachGameLayerChildern1.push(this.GameLayerChildern1[i]); //把第二层循环添加的 div元素节点 添加进该数组
this.ForeachGameLayerChildern2.push(this.GameLayerChildern2[i]);
}
for (let j = 0; j < this.ForeachGameLayerChildern1.length; j++) { // 通过循环和 children属性 把第三层的div元素节点添加进数组
this.threeChildren1.push(...Array.from(this.ForeachGameLayerChildern1[j].querySelectorAll('div')));
this.threeChildren2.push(...Array.from(this.ForeachGameLayerChildern2[j].querySelectorAll('div')));
}
this.threeChildren1 = this.threeChildren1.flat(); // 将二维数组拆分为数组
this.threeChildren2 = this.threeChildren2.flat();
const gameLayerChildrenNew1 = this.GameLayerChildern1[0];
this.gameLayer1new.children= Array.from(gameLayerChildrenNew1.querySelectorAll('div'));
this.GameLayerBG = this.$refs.GameLayerBG;
// 绑定触摸事件和鼠标点击事件
if (this.GameLayerBG.ontouchstart === null) {
this.GameLayerBG.ontouchstart = this.gameTapEvent;
} else {
this.GameLayerBG.onmousedown = this.gameTapEvent;
}
this.gameInit();
this.initSetting();// 从cookie中获取值来初始化
// 在mounted钩子中绑定事件监听器
window.addEventListener('resize', this.refreshSize);
window.addEventListener('click', this.gameTapEvent);
window.addEventListener('touchstart', this.gameTapEvent, { passive: false });
});
},
-
refreshGameLayer
需要注意的是在何处使用游戏层node,什么地方使用游戏块node。在该函数中box代表游戏块,gamelayerBox代表游戏层
refreshGameLayer(box,loop,offset){ // 刷新游戏层,对 note 设置样式
let gamelayerBox = box[0].parentNode.parentNode // 将 最小游戏块 的爷爷元素节点 Gamelayer1/2 赋值给 gamelayerBox
let i = this.randomPos() + (loop ? 0 : this.k); //
for (let j = 0; j < box.length; j++) { // 设置循环次数为 最小游戏块 的数量
let r = box[j], // 将最小游戏块的元素节点赋给 r
rstyle = r.style; // 将 r 的 style 值赋值给 rstyle
// 设置 最小游戏块 的基本样式属性
rstyle.left = (j % this.k) * this.blockSize + 'px';
rstyle.bottom = Math.floor(j / this.k) * this.blockSize + 'px';
rstyle.width = this.blockSize + 'px';
rstyle.height = this.blockSize + 'px';
rstyle.backgroundImage = "none";
r.className = r.className.replace(this.clearttClsReg, '');
if (i == j) {
this.gameBBList.push({
cell: i % this.k,
id: r.id
});
rstyle.backgroundImage = "url(" + this.url + ")";
rstyle.backgroundSize = 'cover';
r.className += ' t' + (Math.floor(Math.random() * 1000) % (this.k + 1) + 1);
r.notEmpty = true;
if (j < box.length - this.k) {
i = this.randomPos() + (Math.floor(j / this.k) + 1) * this.k;
}
} else {
r.notEmpty = false;
}
}
if(loop){
gamelayerBox.style.webkitTransitionDuration = '0ms';
gamelayerBox.style.display = 'none';
gamelayerBox.y = -(this.blockSize) * (Math.floor(box.length / this.k) + (offset || 0)) * loop;
setTimeout(() => {
gamelayerBox.style[this.transform] = 'translate3D(0,' + gamelayerBox.y + 'px,0)';
setTimeout(() => {
gamelayerBox.style.display = 'block';
}, 0);
}, 0);
}else {
gamelayerBox.y = 0;
gamelayerBox.style[this.transform] = 'translate3D(0,' + gamelayerBox.y + 'px,0)';
}
gamelayerBox.style[this.transitionDuration] = '180ms';
},
结语
在这个组件中耗的时间太长了,遇到的问题处理的问题都很多,有些不知道该从何说起,就把在写的过程中遇到的最难解决的几个问题拉出来讲了一下。历时一个月将这个小网页写完,回头再来看,遇到的问题很多都是有些知识未曾了解过或者说不是很熟悉,导致很多问题的来源检查都显得极为困难,只能说路漫漫其修远兮了。当然在这个过程中也是收获颇丰,对vue这个框架其中很多的用法也更加的熟悉,总结了很多小的知识点。