我的前端小项目:小游戏管理平台(vue+js)

1,882 阅读5分钟

我正在参加「掘金·启航计划」

线上地址:mygame.codeape.site/

源码地址(欢迎star)

项目介绍

  • 主要开发技术: vue2jscanvas
  • 描述: 当前开发出来的游戏有:扫雷、贪吃蛇、俄罗斯方块、飞机大战、飞翔的小鸟、2048和五子棋。每个游戏都会弹窗存在于一个页面内,多个游戏可以在一个页面内进行,游戏结束显示得分排行榜并可以分享得分到评论区。游戏的开发技术都采用Vue和JS来进行开发,其中飞机大战和俄罗斯方块采用的是JS直接操作DOM的方式进行游戏的开发,而其他五个小游戏采用Canvas作为游戏的开发技术。

注:由于篇幅太长了,就简单讲解一下扫雷游戏的开发过程,其他的小游戏可以直接去看源码,里面也是有代码注释的

一、扫雷

游戏描述

鼠标左键点击打开雷区,右键点击标记当前位置(便于你游戏过程中记录雷的位置),打开雷区后,会告诉你四周八个格子一共有多少个雷。游戏等级分为:初级10雷,99=81格;中级40雷,1616=256格;高级100雷,2222=484格;地狱200雷,2525=625格。

开发流程

源码: gitee.com/wooden-join…

变量需求:游戏状态(开始、结束),Canvas画布的对象、2d上下文、宽高和Canvas画板距离浏览器左/上的距离,格子宽度,线的粗细,每个级别对应的线总数和雷总数,当前游戏中雷的数量,储存所有雷的对象数组(得分(0:空 1-8:数字 9:雷)、是否已经打开、标记(1:旗子 2:问号)),当前用时变量和对应的计时器。

mounted 时初始化游戏,获取canvas2d上下文,根据等级设置格子,然后画棋盘,保存旗子的坐标,并开始计时。

init() {
  this.setLevelData()
  let width = this.canvasWidth + 60
  // 刚开始获取浏览器的宽高
  this.popWidth = parseInt(
    (width * 100) / document.documentElement.clientWidth
  );
  this.popHeight = parseInt(
    (width * 100) / document.documentElement.clientHeight
  );
  // 监听浏览器窗口大小变化
  window.removeEventListener('resize', this.getPopInfo)
  window.addEventListener("resize", this.getPopInfo);
  setTimeout(() => {
    this.canvas = this.$refs.canvas
    this.ctx = this.canvas.getContext("2d")
    // 画棋盘
    this.drawCheckerboard();
    // 保存棋子的坐标
    this.saveLocation();
    // 开始计时
    this.openTiming()
  }, 100);
},

根据等级设置棋盘的格子数量和雷的数量

// 设置对应等级棋盘和雷的数量
setLevelData() {
  this.gameOver = false
  this.usedTime = 0
  clearInterval(this.timer)
  const {lineNum, rayNum} = this.LevelData[this.selectLevel]
  // 30(格子宽度) * 9(格子数量) + 3(线粗细) * 10(线的数量) = 300
  this.canvasWidth = this.gridWidth * (lineNum - 1) + this.lineWidth * lineNum
  this.lineNum = lineNum
  this.rayNum = rayNum
  // this.rayNum = 1  // 快速结束游戏
  // 清空格子和雷数组
  this.allArr.length = 0
  this.rayArr.length = 0
},

根据上面的数据来画棋盘

// 画棋盘
drawCheckerboard() {
  let begin = 1.5; // 开始的位置
  let lineLength = this.canvasWidth; // 每条线的总长度
  let gridWidth = this.gridWidth + 3; // 每个格子加右边/下边线的宽度
  for (let i = 0; i < this.lineNum; i++) {
    // 横线
    this.ctx.beginPath();
    this.ctx.moveTo(begin, begin + gridWidth * i);
    this.ctx.lineTo(begin + lineLength, begin + gridWidth * i);
    this.ctx.lineWidth = 3;
    this.ctx.strokeStyle = "#c0cce4";
    this.ctx.stroke();
    // 竖线
    this.ctx.beginPath();
    this.ctx.moveTo(begin + gridWidth * i, begin);
    this.ctx.lineTo(begin + gridWidth * i, begin + lineLength);
    this.ctx.lineWidth = 3;
    this.ctx.strokeStyle = "#c0cce4";
    this.ctx.stroke();
  }
},

初始化格子信息,并在根据登记随机生成对应数量的雷,然后每生成一个雷,都要将该雷周边一圈的格子的得分都加一。

// 初始化每个位置的值
saveLocation() {
  let gridWidth = this.gridWidth + 3;
  // 总格子数
  let gridNum = this.lineNum - 1
  for (let i = 0; i < gridNum; i++) {
    this.allArr.push([]);
    for (let j = 0; j < gridNum; j++) {
      // 保存所有的棋子位置信息
      this.$set(this.allArr[i], j, {
        x: 15 + gridWidth * j,
        y: 15 + gridWidth * i,
        score: 0,
        isPlay: false
      })
      // tag 不用监听
      this.allArr[i][j].tag = 0
      this.drawGrid(gridWidth * j + 3, gridWidth * i + 3)
    }
  }
  // 随机生成雷
  for(let i = 0; i < this.rayNum; i++) {
    let {x, y} = this.randomRay()
    this.rayArr.push([x, y])
    this.allArr[x][y].score = 9
    this.besidRayGrid(x, y)
  }
  const rayArrRes = JSON.parse(JSON.stringify(this.rayArr))
  console.log('作弊专用,雷的位置是:', rayArrRes.sort((a,b)=> a[0] - b[0]))
},
// 画格子
drawGrid(x, y) {
  let img = new Image()
  img.src = require('@/assets/img/grid.png')
  img.onload = () => {
    this.ctx.drawImage(img, x, y, this.gridWidth, this.gridWidth)
  }
},

大致初始化完成后,就可以开始进行游戏了,当点击了棋盘的格子后,需要获取鼠标的位置,然后进行下棋操作,同时这里需要区分是左键还是右键

// 点击获取鼠标位置,下棋
getMouse(isRight, e) {
  if (this.gameOver) return;
  // canvas 中棋子的位置
  let chessX = window.event.pageX - this.canvasLeft;
  let chessY = window.event.pageY - this.canvasTop;
  // 获取鼠标点击 下棋的位置
  for (let i = 0; i < this.lineNum - 1; i++) {
    for (let j = 0; j < this.lineNum - 1; j++) {
      if (!this.allArr[i][j].isPlay) {
        let {x, y, score, tag} = this.allArr[i][j]
        if (
          chessX >= x - 15 &&
          chessX <= x + 15 &&
          chessY >= y - 15 &&
          chessY <= y + 15
        ) {
          if(isRight) {
            let newTag = tag === 0 ? 1 : ( tag === 1 ? 2 : 0)
            this.allArr[i][j].tag = newTag
            this.drawTagRay(x, y, newTag)
          } else {
            if(this.allArr[i][j].tag !== 0) return
            if(score === 0) this.openNearGrid(i, j)
            else this.drawChess(x, y, score);
            this.allArr[i][j].isPlay = true
          }
        }
      }
    }
  }
},

左键则需要打开该格子,并通过一开始初始化的值来判断是否需要进行绘画对应的数字。

// 点击后画每一格
drawChess(x, y, score) {
  // 画背面正方形
  this.ctx.beginPath()
  this.ctx.fillStyle = '#dbf1fb'
  this.ctx.fillRect(x - 15 + 3, y - 15 + 3, this.gridWidth, this.gridWidth)
  this.ctx.closePath()
  this.ctx.stroke();
  // 画雷
  if(score === 9) {
    let img = new Image()
    img.src = require('@/assets/img/ray.png')
    img.onload = () => {
      this.ctx.drawImage(img, x - 15 + 7, y - 15 + 7, 22, 22)
    }
    this.gameOver = true
    return
  }
  let fontColor = ''
  switch(score) {
    case 0: return;
    case 1: fontColor = '#689eeb'; break;
    case 2: fontColor = '#207210'; break;
    case 3: fontColor = '#b00e0b'; break;
    default: fontColor = '#34016e';
  }
  // 设置字体
  this.ctx.font = "20px bold 黑体";
  this.ctx.fillStyle = fontColor;
  // 设置水平对齐方式
  this.ctx.textAlign = "center";
  // 设置垂直对齐方式
  this.ctx.textBaseline = "middle";
  // 绘制文字(参数:要写的字,x坐标,y坐标)
  this.ctx.fillText(score, x + 3, y + 5);
},

右击则只需要绘画一个旗子或问号就好。

// 右键 画旗子或问号
drawTagRay(x, y, tag) {
  this.drawGrid(x-15+3, y-15+3)
  if(tag === 0) return
  let imgName = tag === 1 ? 'flag' : 'uncertain'
  let img = new Image()
  img.src = require(`@/assets/img/${imgName}.png`)
  img.onload = () => {
    this.ctx.drawImage(img, x - 15 + 7, y - 15 + 7, 22, 22)
  }
},

左键打开格子的时候需要判断当前位置是否为雷,如果是9则代表雷游戏结束了,如果是0,则需要打开附件相邻的其他的为0的格子。

// 点击了附近没有雷的格子 打开旁边所有这样的格子 
openNearGrid(i, j, isEnd) {
  let max = this.lineNum - 1 - 1
  this.allArr[i][j].isPlay = true
  let {x, y, score} = this.allArr[i][j]
  this.drawChess(x, y, score)
  // 已经打开数字了结束这个递归
  if(isEnd) return
  if(j > 0 && !this.allArr[i][j - 1].isPlay) {  // 上
    if(this.allArr[i][j - 1].score === 0) this.openNearGrid(i, j - 1)
    else this.openNearGrid(i, j - 1, true)
  } 
  if(i < max && !this.allArr[i + 1][j].isPlay) {  // 右
    if(this.allArr[i + 1][j].score === 0) this.openNearGrid(i + 1, j)
    else this.openNearGrid(i + 1, j, true)
  } 
  if(j < max && !this.allArr[i][j + 1].isPlay) {  // 下
    if(this.allArr[i][j + 1].score === 0) this.openNearGrid(i, j + 1)
    else this.openNearGrid(i, j + 1, true)
  }
  if(i > 0 && !this.allArr[i - 1][j].isPlay) {   // 左
    if(this.allArr[i - 1][j].score === 0) this.openNearGrid(i - 1, j)
    else this.openNearGrid(i - 1, j, true)
  }
},

以上就是开发的扫雷中涉及到的主要功能函数,

二.五子棋

源码:gitee.com/wooden-join…

三.2048

源码:gitee.com/wooden-join…

四.飞翔的小鸟

源码:gitee.com/wooden-join…

五.贪吃蛇

源码:gitee.com/wooden-join…

六.俄罗斯方块

源码:gitee.com/wooden-join…

七.飞机大战

源码:gitee.com/wooden-join…

总结与感慨

刚开始只想做个小游戏

整个项目可以说开始于一年半之前吧,然后这里面的游戏有部分是借鉴网上的,然后自己进行改写,最早开发的是俄罗斯方块,大概一年多之前了。看着b站的js版本的视频,然后自己写的uniapp的vue版本,本来想做成一个单独的小程序的。后来放弃了,没做了。

俄罗斯方块参考视频:www.bilibili.com/video/BV11E…

然后写的飞机大战,鼠标移动控制飞机射击敌人,本来是设计到我的仿b站项目中的,想在用户刷视频空闲之余,还能玩玩小游戏的。

仿b站:codeape.site:8181/

开始制作小游戏管理平台

大概在去年七月份,就萌生出做一个小游戏管理平台的想法了,然后就将这些小游戏改写一下,并集合到里面。但是由于canvas技术不太行然后就去看了下别人写的canvas版贪吃蛇,然后改写成自己需要的vue版本。

贪吃蛇参考视频:www.bilibili.com/video/BV1NC…

当有一定技术积累后,其他的小游戏就开始自主开发了,虽然中途也有些有参考网上的,比如2048也参考了一些网上的写法,但后面就开始自主开发了。

做成毕设

一开始是只有几个游戏在里面的,最后在这个小游戏平台中自己增加了个登录模块,排行榜,评论区,后台管理系统等,最后也作为我的毕设了,这也算是自己原创的一个小项目吧。

项目完整演示的视频:www.bilibili.com/video/BV1J3…

技术进步

当有一定的技术能力后就开发了个相对大型点的游戏了,一个塔防小游戏:保卫大司马

项目地址:link.juejin.cn/?target=htt…

文章链接:juejin.cn/post/708776…