Vue网页小音游(malody青春版)

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中游戏块的数量就为12 * 4 一共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)`; // 移动
        }
      }
    },
}

函数调用:

下面是几个比较重要的函数的实现逻辑:

  1. 挂载完成之后,执行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 });


    });
  },
  1. 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这个框架其中很多的用法也更加的熟悉,总结了很多小的知识点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,198评论 6 514
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,334评论 3 398
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,643评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,495评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,502评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,156评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,743评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,659评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,200评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,282评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,424评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,107评论 5 349
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,789评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,264评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,390评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,798评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,435评论 2 359

推荐阅读更多精彩内容