游戏名称:清凉一夏消消乐
技术栈:Vue3 + TypeScript + Vite + Element-Plus
游戏体验地址 (pc/ 手机皆可):https://wmuhua.com/games/xxl
开源地址:https://github.com/wmuhua/vue…
点赞留香,与有荣焉,感激感激
游戏介绍
先看一下
好吧,我晓得界面有点丑 →_→
外围思路
游戏步骤次要就是:打消、着落、补充、挪动,采纳三种状态来辨别须要删除的(remove
)、新增加的(add
)、和失常的方块(normal
)
- 次要就是生成小方块列表后,马上保留每一个方块上下左右方块的信息
- 而后判断每一个方块和高低或和左右类型雷同即为须要打消,并把该方块状态改为
remove
- 而后通过扭转
top
和left
来管制着落,同时要把打消的地位上移,这样补充的时候能力在对应空位上显示,这里专门用了一个矩阵来保留所有对应格子信息,辨别出哪些格子是须要打消 / 补充的 - 挪动就比较简单了,因为每个方块上都保留了本人的上下左右信息,所以只须要替换就行了
有一个坑,就是 key,因为 diff 算法的起因,不须要从新渲染就要保障 key 是惟一的,比方着落的也从新渲染视觉效果会很奇怪
外围代码
html
以下是矩阵区域所有 html,就是用一个 div 来做的,依据类型给不同类名,而后雪糕全是背景图片
<div class="stage">
<div
v-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: number
size: 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 = x
this.y = y
this.size = size
this.getMatrix() // 生成矩阵
this.init(true) // 生成 data 渲染用
}
getMatrix(){}
init(){}
// 循环执行
gameLoop(){}
// 点击
click(){}
// 换位
swap(){}
// 删除
remove(){}
// 着落
down(){}
// 补充
add(){}
}
游戏开始 / 循环
// 要等动画执行完,所以用 await
async gameLoop(bool: boolean = false) {
// 完结游戏后从新开始时分数清 0
if (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 = false
return status
}
删除
留神 状态为 remove
的理论没有删除,只是页面上看不到了,到补充的时候才会删除掉状态为 remove
的
// 革除
remove() {return new Promise((resolve, reject) => {const { data} = this
data.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 = 0
this.score += 1
}
})
// 这里提早 100 毫秒是首次进页面的时候,先看到格子有货色,不然会是空的
}, 100)
// 动画时长 500 毫秒 css 那边定义了,所以提早 500 毫秒
setTimeout(() => {resolve(true)
}, 500)
})
}
着落
这里有个坑。除了要把删除格子下面的着落下来之外,还须要把曾经删除 (状态为删除,页面上看不到了的) 的格子上位到,下面的空位上,否则,新增的格子会从上面冒出来
// 着落
down() {return new Promise((resolve, reject) => {const { data, size, x, y} = this
data.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 -= distance
item.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 += distance
item.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 = 1
item.status = "normal"
}
})
}, 100)
// 动画完结
setTimeout(() => {resolve(true)
}, 500)
})
}
接下来前面的逻辑都比较简单了,没啥说的,都写在正文里了
生成矩阵 / 数据
// 生成全副为空的矩阵
getMatrix() {const { x, y} = this
const 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} = this
const data: Array<any> = []
// 这里用两个指针,没有用嵌套循环,缩小复杂度
let _x = 0
let _y = 0
for (let i = 0, len = Math.pow(x, 2); i < len; i++) {
let item
try {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 = true
this.target1 = target
this.isSelect = true
} else {
// 抉择第二个
if (this.target1 === target) return
this.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 = true
this.target1 = target
this.isSelect = true
}
}
}
换地位
这里的逻辑次要就是替换两个方块的地位信息,而后从新生成上下左右,就 ok 了
// 换地位
swap() {return new Promise((resolve, reject) => {const { target1, target2, data} = this
const {positionLeft: pl1, positionTop: pt1, x: x1, y: y1} = target1
const {positionLeft: pl2, positionTop: pt2, x: x2, y: y2} = target2
setTimeout(() => {
target1.positionLeft = pl2
target1.positionTop = pt2
target1.x = x2
target1.y = y2
target2.positionLeft = pl1
target2.positionTop = pt1
target2.x = x1
target2.y = y1
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
)
})
}, 0)
setTimeout(() => {resolve(true)
}, 500)
})
}
结语
游戏名称:清凉一夏消消乐
技术栈:Vue3 + TypeScript + Vite + Element-Plus
游戏体验地址 (pc/ 手机皆可):https://wmuhua.com/games/xxl
开源地址:https://github.com/wmuhua/vue…
点赞留香,与有荣焉,感激感激