Game Name: Cool Summer Xiaoxiaole Technology Stack: Vue3 + TypeScript + Vite + Element-Plus
Game experience address (PC/mobile phone): https://wmuhua.com/games/xxl
Open source address: https://github.com/wmuhua/vue3-xxl
Like and stay incense, and honored, thank you
game introduction
first look
Well, I know the interface is kind of ugly →_→
core idea
The main steps of the game are: elimination, whereabouts, replenishment, and movement. Three states are used to distinguish the ones that need to be deleted ( remove
), the newly added ones ( add
), and the normal blocks ( normal
)
- The main thing is to save the information of each block, the top, bottom, left and right blocks immediately after generating the list of small blocks.
- Then judge that each block is of the same type as up and down or left and right to be eliminated, and change the state of the block to
remove
- Then by changing
top
andleft
to control the whereabouts, and at the same time move the position of elimination, so that it can be displayed on the corresponding vacancy when it is added. Here, a matrix is specially used to save All corresponding grid information, distinguish which grids need to be eliminated/supplemented - The movement is relatively simple, because each block saves its own up, down, left and right information, so it only needs to be exchanged.
There is a pit, which is the key. Due to the diff algorithm, it is necessary to ensure that the key is unique without re-rendering. For example, it will be strange to re-render the visual effect if it falls.
core code
html
The following is all the html in the matrix area, which is made of a div, giving different class names according to the type, and then the ice cream is all background images
<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
The js part mainly encapsulates a class, which is convenient for unified management and operation
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(){}
}
game start/loop
// 要等动画执行完,所以用 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
}
delete
Note that the status is remove
is not actually deleted, but it is not visible on the page, and the status is remove
will be deleted when it is added.
// 清除
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)
})
}
whereabouts
There is a hole here. In addition to dropping the deleted grid, it is also necessary to place the deleted grid (the status is deleted and cannot be seen on the page) to the upper vacancy, otherwise, the newly added grid will emerge from the bottom. Come
// 下落
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)
})
}
Add to
It is conceivable that after the drop is executed, the matrix in the page is available in all the grids, but the grids that appear to be empty are actually deleted, and then the matrix is regenerated according to the order, and each grid is kept. A non remove
lattice state, is remove
is regenerated to achieve the effect of replacement and supplementation
// 添加
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)
})
}
The following logic is relatively simple, there is nothing to say, it is written in the comments
Generate matrix/data
// 生成全部为空的矩阵
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
// 点击小方块
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
}
}
}
change positions
The main logic here is to exchange the position information of the two blocks, and then regenerate the top, bottom, left, and right, and that's 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)
})
}
Epilogue
Game Name: Cool Summer Xiaoxiaole Technology Stack: Vue3 + TypeScript + Vite + Element-Plus
Game experience address (PC/mobile phone): https://wmuhua.com/games/xxl
Open source address: https://github.com/wmuhua/vue3-xxl
Like and stay incense, and honored, thank you
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。