基于vue编写一个月饼连连看游戏
作者:冰镇白干
"月圆花好夜,花好月圆人更圆。"
前言
中秋节快要到啦,我们一起用Vue创建一个简单的连连看游戏。
连连看大家一定都玩过吧,通过消除相同的图案来清理棋盘。上面就是我们今天要做的效果图,看上去也挺像那么回事,不过实现起来也是非常简单。
我将一步步引导大家完成整个游戏的制作过程,让我们开始吧,一起为中秋节增添一些互动和娱乐!
设计思路
在构建连连看游戏之前,首先需要考虑游戏的设计思路。
- 游戏界面:我们将创建一个网格状的游戏界面,每个格子上都有一个图标或者数字。
- 游戏逻辑:玩家可以点击两个相同的格子来连接它们,但是连接的路径上下左右不能有障碍物
- 游戏结束:当所有的格子都被连接后,游戏结束。
初始棋盘
<template> <div class="game-board"> <!-- 棋盘网格 --> <div v-for="(row,rowIndex) in grid" :key="rowIndex" class="row"> <div v-for="(cell,colIndex) in row" class="cell" :key="colIndex" > {{grid[rowIndex][colIndex]}} </div> </div> </div> </template> <script setup lang="ts"> import { onMounted, ref } from 'vue'; const grid = ref<number[][]>([]) /** * 随机生成grid */ const randomizeGrid = () => { const rows = 10 const cols = 10 for(let i = 0 ; i < rows ; i ++) { const row = [] for(let j = 0 ; j < cols ; j ++) { row.push(Math.floor(Math.random() * 3 + 1)) } grid.value.push(row) } } onMounted(() => { randomizeGrid() }) </script> <style scoped lang="less"> .game-board { background-color: palegoldenrod; background-size: contain; .row { display: flex; .cell { width: 60px; height: 60px; margin: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all .3s; } } } </style>
这样就生成了一个随机棋盘,后续我们将数组换成对应的图片就可以达到效果。
但是上面的写法是我前面写的,后续发现一个问题,我们不能简单的随机1、2、3
,我们要确保每种值都是成对存在的。为了实现这一点,可以采用以下步骤:
- 初始化一个存储可用格子坐标的数组
availableCells
。 - 遍历棋盘格子,并将每个格子的坐标添加到
availableCells
。 - 随机选择一个可用格子,为其分配一个随机图案值,然后从
availableCells
中移除该格子。 - 再次随机选择一个可用格子,并为其分配相同的图案值。
- 重复步骤 3 和 4,直到生成了足够的图案对。
- 将这些图案值填充到游戏棋盘中。
/** * 随机生成grid,确保每一个值都是成对存在的 */ const randomizeGrid = () => { const rows = 10; const cols = 10; const numPairs = (rows * cols) / 2; // 总共需要的值对数 const igrid:number[][] = Array(rows).fill(0).map(() => Array(cols).fill(0)) const availableCells = []; // 存储可用格子的坐标 // 初始化可用格子的坐标数组 for (let i = 0; i < rows; i++) { for (let j = 0; j < cols; j++) { availableCells.push([i, j]); } } // 随机生成成对的图案,并填充到grid中 for (let pair = 1; pair <= numPairs; pair++) { // 随机选择一个可用格子 const cellIndex = Math.floor(Math.random() * availableCells.length); const [row, col] = availableCells[cellIndex]; // 随机生成一个图案 const value = Math.floor(Math.random() * 4+ 1); igrid[row][col] = value; availableCells.splice(cellIndex, 1); // 随机选择一个可用格子,将相同图案填充到第二个位置 const secondCellIndex = Math.floor(Math.random() * availableCells.length); const [secondRow, secondCol] = availableCells[secondCellIndex]; igrid[secondRow][secondCol] = value; // 从可用格子数组中移除已经填充的格子 availableCells.splice(secondCellIndex, 1); } grid.value = igrid };
上面注释已经很清楚啦,如果大家有更好的方法欢迎在评论区交流 !
点击动作
上一个步骤中,我们已经获取到了每个格子的值。现在我们写一下选中逻辑,由于每次点击都需要和上一次结果进行对比,就需要将上一个点击位置存起来
现在我们要实现一下功能
const lastSelectedCell = ref<number[]>([]) const selectCell = (rowIndex: number, colIndex: number) => { // 上次选中了 if(lastSelectedCell.value?.length) { if(canConnect(lastSelectedCell.value,[rowIndex,colIndex])) { console.log('可以连接~~'); grid.value[lastSelectedCell.value[0]][lastSelectedCell.value[1]] = 0 grid.value[rowIndex][colIndex] = 0 } lastSelectedCell.value = [] } else { lastSelectedCell.value = [rowIndex,colIndex] } } /** * 格子是否被选中 */ const isCellSelected = (rowIndex: number, colIndex: number): boolean => { return lastSelectedCell.value[0] === rowIndex && lastSelectedCell.value[1] === colIndex; };
isCellSelected
是我们选中要配对的那个方格,为了是对它添加动画方便。
点击时有两种情况:
- 正在选第一个,我们需要记录次方格,为了下一次连接配对
- 选了第一个了,此时点击选中第二个,判断是否配对成功 (配对成功的逻辑下一步介绍)
连接判定
连接判断使用bfs来做
这里需要注意,判断逻辑需要写在for循环里面,因为需要配对的那个方格值一定不为0,也就是说目标方格只要有值就到达不了
/** * 使用 BFS 检查路径是否通 */ const directions = [ [-1, 0], // 上 [1, 0], // 下 [0, -1], // 左 [0, 1], // 右 ]; const isPathConnected = (startCell: number[], endCell: number[]): boolean => { const visited = new Set<string>(); const queue: number[][] = [startCell]; while (queue.length) { const [row, col] = queue.shift()!; // 检查四个方向的相邻格子 for (const [dx, dy] of directions) { const newRow = row + dx; const newCol = col + dy; const key = `${newRow}-${newCol}`; if(endCell[0] === newRow && endCell[1] === newCol) return true if ( newRow >= 0 && newRow < grid.value.length && newCol >= 0 && newCol < grid.value[0].length && !visited.has(key) && grid.value[newRow][newCol] === 0 ) { queue.push([newRow, newCol]); visited.add(key); } } } return false; // 没找到路径 };
此时我们就可以点击配对了,最简单的版本也就完成了,贴一下完整代码:
<template> <div class="game-board"> <!-- 棋盘网格 --> <div v-for="(row,rowIndex) in grid" :key="rowIndex" class="row"> <div v-for="(cell,colIndex) in row" class="cell" :class="{isSelected : isCellSelected(rowIndex,colIndex)}" :key="colIndex" @click="selectCell(rowIndex,colIndex)" > {{grid[rowIndex][colIndex]}} </div> </div> </div> </template> <script setup lang="ts"> import { onMounted, ref } from 'vue'; const grid = ref<number[][]>([]) const lastSelectedCell = ref<number[]>([]) const selectCell = (rowIndex: number, colIndex: number) => { // 上次选中了 if(lastSelectedCell.value?.length) { if(canConnect(lastSelectedCell.value,[rowIndex,colIndex])) { console.log('可以连接~~'); grid.value[lastSelectedCell.value[0]][lastSelectedCell.value[1]] = 0 grid.value[rowIndex][colIndex] = 0 } lastSelectedCell.value = [] } else { lastSelectedCell.value = [rowIndex,colIndex] } } /** * 格子是否被选中 */ const isCellSelected = (rowIndex: number, colIndex: number): boolean => { return lastSelectedCell.value[0] === rowIndex && lastSelectedCell.value[1] === colIndex; }; /** * 检查两个格子是否可以连接 */ const canConnect = (cell1: number[],cell2: number[]): boolean => { // 点击同一个格子 if(cell1[0] === cell2[0] && cell1[1] === cell2[1]) { return false } // 是否值相同 if(grid.value[cell1[0]][cell1[1]] !== grid.value[cell2[0]][cell2[1]]) { return false } // 路径是否通 return isPathConnected(cell1,cell2) } /** * 使用 BFS 检查路径是否通 */ const directions = [ [-1, 0], // 上 [1, 0], // 下 [0, -1], // 左 [0, 1], // 右 ]; const isPathConnected = (startCell: number[], endCell: number[]): boolean => { const visited = new Set<string>(); const queue: number[][] = [startCell]; while (queue.length) { const [row, col] = queue.shift()!; // 检查四个方向的相邻格子 for (const [dx, dy] of directions) { const newRow = row + dx; const newCol = col + dy; const key = `${newRow}-${newCol}`; if(endCell[0] === newRow && endCell[1] === newCol) return true if ( newRow >= 0 && newRow < grid.value.length && newCol >= 0 && newCol < grid.value[0].length && !visited.has(key) && grid.value[newRow][newCol] === 0 ) { queue.push([newRow, newCol]); visited.add(key); } } } return false; // 没找到路径 }; /** * 随机生成grid */ const randomizeGrid = () => { const rows = 10 const cols = 10 for(let i = 0 ; i < rows ; i ++) { const row = [] for(let j = 0 ; j < cols ; j ++) { row.push(Math.floor(Math.random() * 3 + 1)) } grid.value.push(row) } } onMounted(() => { randomizeGrid() }) </script> <style scoped lang="less"> .game-board { // width: 80vw; // height: 80vh; background-color: palegoldenrod; background-size: contain; .row { display: flex; .cell { width: 60px; height: 60px; margin: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all .3s; background-color: aquamarine; } } } </style>
现在效果:
引入月饼图片
现在简单功能算是完成了,但是太简单了。我们得做一个看起来高大上的练练看。
此时,直接请班里妹子画几个好看的月饼图片:
真的太好看了,项目直接升个档次。
拿ps切出来:
然后就可以根据当前格子渲染出对应的小月饼啦!
动态加载图片需要用到vite
自己的方法
加了点击的动画,选中的图片放大并且添加了光圈。大家自己看一看具体实现吧,也是很简单。
小结
最后贴一下项目地址
项目地址:https://github.com/Bbbtt04/mooncake-match
主要有两个难点:
- bfs判断连接
- 随机生成成对的值
- 最难的一点:找一个会手绘的妹子
大家也可以试一试自己实现一下,做出来成就感满满!
以上就是基于vue编写一个月饼连连看游戏的详细内容,更多关于vue月饼连连看的资料请关注脚本之家其它相关文章!