我正在参加「掘金·启航计划」
线上地址:mygame.codeape.site/
源码地址(欢迎star)
- Gitee: gitee.com/wooden-join…
- GitHub: github.com/ApeWhoLoves…
项目介绍
- 主要开发技术: vue2,js,canvas
- 描述: 当前开发出来的游戏有:扫雷、贪吃蛇、俄罗斯方块、飞机大战、飞翔的小鸟、2048和五子棋。每个游戏都会弹窗存在于一个页面内,多个游戏可以在一个页面内进行,游戏结束显示得分排行榜并可以分享得分到评论区。游戏的开发技术都采用Vue和JS来进行开发,其中飞机大战和俄罗斯方块采用的是JS直接操作DOM的方式进行游戏的开发,而其他五个小游戏采用Canvas作为游戏的开发技术。
注:由于篇幅太长了,就简单讲解一下扫雷游戏的开发过程,其他的小游戏可以直接去看源码,里面也是有代码注释的
一、扫雷
游戏描述
鼠标左键点击打开雷区,右键点击标记当前位置(便于你游戏过程中记录雷的位置),打开雷区后,会告诉你四周八个格子一共有多少个雷。游戏等级分为:初级10雷,99=81格;中级40雷,1616=256格;高级100雷,2222=484格;地狱200雷,2525=625格。
开发流程
变量需求:游戏状态(开始、结束),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)
}
},
以上就是开发的扫雷中涉及到的主要功能函数,
二.五子棋
三.2048
四.飞翔的小鸟
五.贪吃蛇
六.俄罗斯方块
七.飞机大战
总结与感慨
刚开始只想做个小游戏
整个项目可以说开始于一年半之前吧,然后这里面的游戏有部分是借鉴网上的,然后自己进行改写,最早开发的是俄罗斯方块,大概一年多之前了。看着b站的js版本的视频,然后自己写的uniapp的vue版本,本来想做成一个单独的小程序的。后来放弃了,没做了。
俄罗斯方块参考视频:www.bilibili.com/video/BV11E…
然后写的飞机大战,鼠标移动控制飞机射击敌人,本来是设计到我的仿b站项目中的,想在用户刷视频空闲之余,还能玩玩小游戏的。
开始制作小游戏管理平台
大概在去年七月份,就萌生出做一个小游戏管理平台的想法了,然后就将这些小游戏改写一下,并集合到里面。但是由于canvas技术不太行然后就去看了下别人写的canvas版贪吃蛇,然后改写成自己需要的vue版本。
贪吃蛇参考视频:www.bilibili.com/video/BV1NC…
当有一定技术积累后,其他的小游戏就开始自主开发了,虽然中途也有些有参考网上的,比如2048也参考了一些网上的写法,但后面就开始自主开发了。
做成毕设
一开始是只有几个游戏在里面的,最后在这个小游戏平台中自己增加了个登录模块,排行榜,评论区,后台管理系统等,最后也作为我的毕设了,这也算是自己原创的一个小项目吧。
项目完整演示的视频:www.bilibili.com/video/BV1J3…
技术进步
当有一定的技术能力后就开发了个相对大型点的游戏了,一个塔防小游戏:保卫大司马