Vue3 版消消乐小游戏(pc/手机皆可线上体验,附源码)

游戏名称:清凉一夏消消乐
技术栈:Vue3 + TypeScript + Vite + Element-Plus
游戏体验地址(pc/手机皆可):https://wmuhua.com/games/xxl
开源地址:https://github.com/wmuhua/vue3-xxl

点赞留香,与有荣焉,感谢感谢

核心思路

游戏步骤主要就是:消除、下落、补充、移动,采用三种状态来区分需要删除的(remove)、新添加的(add)、和正常的方块(normal)

  • 主要就是生成小方块列表后,马上保存每一个方块上下左右方块的信息
  • 然后判断每一个方块和上下或和左右类型相同即为需要消除,并把该方块状态改为 remove
  • 然后通过改变 topleft 来控制下落,同时要把消除的位置上移,这样补充的时候才能在对应空位上显示,这里专门用了一个矩阵来保存所有对应格子信息,区分出哪些格子是需要消除/补充的
  • 移动就比较简单了,由于每个方块上都保存了自己的上下左右信息,所以只需要交换就行了

有一个坑,就是 key,由于 diff 算法的原因,不需要重新渲染就要保证key是唯一的,比如下落的也重新渲染视觉效果会很奇怪

核心代码

html

以下是矩阵区域所有html,就是用一个div来做的,根据类型给不同类名,然后雪糕全是背景图片

<div class="stage"><divv-for="item in data":style="{left: `${item.positionLeft}px`,top: `${item.positionTop}px`,}":key="item.key":class="['square',`type${item.type}`,`scale${item.scale}`,{ active: item.active },]"@click="handleClick(item)">div>
div>

js

js 部分主要是封装了一个类,方便统一管理操作

export default class Stage implements IXXL {x: number // x和y 是游戏舞台行列方块个数y: numbersize: number // 方块大小typeCount = 7 // 方块类型个数matrix: Array<any> = [] // 方块矩阵,用于每次消除之后根据矩阵规则生成新的游戏棋盘data: Array<any> = [] // 用于渲染页面isHandle = false // 游戏是否正在消除/下落/添加处理中isSelect = false // 是否有选择score = 0 // 分数target1: any = { active: false } // 选中的方块target2: any = {}constructor(x: number, y: number, size: number) {this.x = xthis.y = ythis.size = sizethis.getMatrix() // 生成矩阵this.init(true) // 生成 data 渲染用}getMatrix(){}init(){}// 循环执行gameLoop(){}// 点击click(){}// 换位swap(){}// 删除remove(){}// 下落down(){}// 补充add(){}
}

游戏开始/循环

// 要等动画执行完,所以用 await
async gameLoop(bool: boolean = false) {// 结束游戏后重新开始时分数清0if (bool) this.score = 0// 游戏状态改为正在执行中,控制在动画执行过程中不能点击交换this.isHandle = true// 找出需要删除的await this.remove()// 用于检测点击交换后判断有没有需要删除的,没有就再换回来let status = this.data.some((item) => item.status === "remove")// 只要有删除了的,执行上面的下落、补充,补充后再循环找有没有可以删除的while (this.data.some((item) => item.status === "remove")) {await this.down()await this.add()await this.remove()}// 所有能删除的删除后,更改状态,然后就可以点击了this.isHandle = falsereturn status
}

删除

注意 状态为 remove 的实际没有删除,只是页面上看不到了,到补充的时候才会删除掉状态为 remove

// 清除
remove() {return new Promise((resolve, reject) => {const { data } = thisdata.forEach((item) => {const { left, right, top, bottom, type } = item// 如果自己 + 自己的左和右 类型都一样,状态变更为删除if (left?.type == type && right?.type == type) {left.status = "remove"item.status = "remove"right.status = "remove"}// 如果自己 + 自己的上和下 类型都一样,状态变更为删除if (top?.type == type && bottom?.type == type) {top.status = "remove"item.status = "remove"bottom.status = "remove"}})setTimeout(() => {// 执行删除动画,页面上看不到了,并统计分数,实际这时还没删除data.forEach((item, index) => {if (item.status === "remove") {item.scale = 0this.score += 1}})// 这里延迟100毫秒是首次进页面的时候,先看到格子有东西,不然会是空的}, 100)// 动画时长500毫秒 css 那边定义了,所以延迟500毫秒setTimeout(() => {resolve(true)}, 500)})
}

下落

这里有个坑。除了要把删除格子上面的下落下来之外,还需要把已经删除(状态为删除,页面上看不到了的)的格子上位到,上面的空位上,否则,新增的格子会从下面冒出来

// 下落
down() {return new Promise((resolve, reject) => {const { data, size, x, y } = thisdata.forEach((item, index) => {let distance = 0 // 移动格数if (item.status === "remove") {// 删除的位置上移,调整新增格子的位置let top = item.top// 统计需要上移多少步while (top) {if (top.status !== "remove") {distance += 1}top = top.top}// 上移if (distance) {item.y -= distanceitem.positionTop = item.positionTop - size * distance}} else {let bottom = item.bottom// 统计需要下落多少步while (bottom) {if (bottom.status === "remove") {distance += 1}bottom = bottom.bottom}// 下落if (distance) {item.y += distanceitem.positionTop = item.positionTop + size * distance}}})setTimeout(() => {resolve(true)}, 500)})
}

添加

可以想象到,在下落执行完之后,页面中的矩阵,是所有格子都有的,只是看起来空的格子,实际上是删除格子在那占位,然后只要根据顺序重新生成矩阵,并保留每个非remove格子的状态,是remove的就重新生成,达到替换补充的效果

// 添加
add() {return new Promise((resolve, reject) => {const { size, matrix } = this// 重置矩阵为空this.getMatrix()// 把当前所有格子信息保存为矩阵this.matrix = matrix.map((row, rowIndex) =>row.map((col: any, colIndex: number) => {return this.data.find((item) => {return colIndex == item.x && rowIndex == item.y})}))// 根据矩阵需要清除的位置替换新方块this.init()setTimeout(() => {// 新增的格子执行动画this.data.forEach((item) => {if (item.status === "add") {item.scale = 1item.status = "normal"}})}, 100)// 动画结束setTimeout(() => {resolve(true)}, 500)})
}

接下来后面的逻辑都比较简单了,没啥说的,都写在注释里了

生成矩阵/数据

// 生成全部为空的矩阵
getMatrix() {const { x, y } = thisconst row = new Array(x).fill(undefined)const matrix = new Array(y).fill(undefined).map((item) => row)this.matrix = matrix
}
// 生成小方块
init(bool: boolean = false) {const { x, y, typeCount, matrix, size } = thisconst data: Array<any> = []// 这里用两个指针,没有用嵌套循环,减少复杂度let _x = 0let _y = 0for (let i = 0, len = Math.pow(x, 2); i < len; i++) {let itemtry {item = matrix[_y][_x]} catch (e) {}// 根据矩阵信息来生成方块let flag: boolean = item && item.status !== "remove"// 每一个方块的信息let obj = {type: flag ? item.type : Math.floor(Math.random() * typeCount),x: _x,y: _y,status: bool ? "normal" : flag ? "normal" : "add",positionLeft: flag ? item.positionLeft : size * _x,positionTop: flag ? item.positionTop : size * _y,left: undefined,top: undefined,bottom: undefined,right: undefined,scale: bool ? 1 : flag ? 1 : 0,key: item ? item.key + i : `${_x}${_y}`,active: false,}data.push(obj)_x++if (_x == x) {_x = 0_y++}}// 保存每个格子上下左右的格子信息data.forEach((square) => {square.left = data.find((item) => item.x == square.x - 1 && item.y == square.y)square.right = data.find((item) => item.x == square.x + 1 && item.y == square.y)square.top = data.find((item) => item.x == square.x && item.y == square.y - 1)square.bottom = data.find((item) => item.x == square.x && item.y == square.y + 1)})this.data = data
}

点击

// 点击小方块
click(target: any) {// 游戏动画正在处理中的时候,不给点击if (this.isHandle) return// console.log(target)const { isSelect } = this// 如果没有选择过的if (!isSelect) {// 选择第一个target.active = truethis.target1 = targetthis.isSelect = true} else {// 选择第二个if (this.target1 === target) returnthis.target1.active = false// 如果是相邻的if (["left", "top", "bottom", "right"].some((item) => this.target1[item] == target)) {this.target2 = target;(async () => {// 调换位置await this.swap()// 会返回一个有没有可以删除的,的状态let res = await this.gameLoop()// 没有就再次调换位置,还原if (!res) {await this.swap()}})()this.isSelect = false} else {// 如果不是相邻的target.active = truethis.target1 = targetthis.isSelect = true}}
}

换位置

这里的逻辑主要就是交换两个方块的位置信息,然后重新生成上下左右,就ok 了

// 换位置
swap() {return new Promise((resolve, reject) => {const { target1, target2, data } = thisconst { positionLeft: pl1, positionTop: pt1, x: x1, y: y1 } = target1const { positionLeft: pl2, positionTop: pt2, x: x2, y: y2 } = target2setTimeout(() => {target1.positionLeft = pl2target1.positionTop = pt2target1.x = x2target1.y = y2target2.positionLeft = pl1target2.positionTop = pt1target2.x = x1target2.y = y1data.forEach((square) => {square.left = data.find((item) => item.x == square.x - 1 && item.y == square.y)square.right = data.find((item) => item.x == square.x + 1 && item.y == square.y)square.top = data.find((item) => item.x == square.x && item.y == square.y - 1)square.bottom = data.find((item) => item.x == square.x && item.y == square.y + 1)})}, 0)setTimeout(() => {resolve(true)}, 500)})
}

结语

游戏名称:清凉一夏消消乐
技术栈:Vue3 + TypeScript + Vite + Element-Plus
游戏体验地址(pc/手机皆可):https://wmuhua.com/games/xxl
开源地址:https://github.com/wmuhua/vue3-xxl

点赞留香,与有荣焉,感谢感谢


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部