基于Konva的六边形棋盘游戏瓦片地图高效实现技术问询
嘿,手动一个个定义六边形瓦片确实会让代码很快变得臃肿不堪——我之前做类似项目的时候也踩过这个坑!咱们来用数据驱动+批量生成的思路重构,不仅能把代码量砍到原来的十分之一,还能为后续的精灵移动逻辑打好基础。
核心思路:用网格数据结构替代单独变量
本质上,六边形棋盘是一个有规律的网格,每个瓦片的位置、样式都可以通过行列计算出来。我们可以用一个二维数组来存储所有瓦片的信息(包括Konva节点、位置、状态),这样既不用单独定义变量,还能快速定位任意瓦片。
1. 先定义六边形的通用配置
把所有重复的参数抽出来,后续改样式、尺寸只需要改这里:
// 六边形基础配置 const hexConfig = { radius: 30, // 外接圆半径 strokeWidth: 2, defaultFill: '#e0e0e0', stroke: '#333' }; // 计算六边形的布局参数(关键!六边形网格的错位规律依赖这些) hexConfig.width = hexConfig.radius * 2; hexConfig.height = Math.sqrt(3) * hexConfig.radius; hexConfig.colOffset = hexConfig.width * 0.75; // 列之间的水平间距 hexConfig.rowOffset = hexConfig.height / 2; // 行之间的垂直间距(因为上下六边形重叠一半)
2. 批量生成六边形瓦片并构建网格
用嵌套循环生成所有瓦片,同时把每个瓦片的信息存入二维数组:
// 定义棋盘的行列数 const gridRows = 8; const gridCols = 10; // 初始化网格数组,用来存储所有瓦片数据 const hexGrid = []; // 循环生成每一行每一列的瓦片 for (let row = 0; row < gridRows; row++) { hexGrid[row] = []; for (let col = 0; col < gridCols; col++) { // 计算当前瓦片的坐标:奇数行需要额外偏移半个列间距 const x = col * hexConfig.colOffset + (row % 2 === 1 ? hexConfig.colOffset / 2 : 0); const y = row * hexConfig.rowOffset; // 创建Konva六边形节点 const hexNode = new Konva.RegularPolygon({ x: x, y: y, sides: 6, radius: hexConfig.radius, fill: hexConfig.defaultFill, stroke: hexConfig.stroke, strokeWidth: hexConfig.strokeWidth, // 绑定行列数据,方便后续交互时快速定位 id: `hex-${row}-${col}`, name: 'hex-tile', data: { row, col } }); // 把瓦片信息存入网格数组 hexGrid[row][col] = { node: hexNode, row, col, isOccupied: false, // 标记是否被精灵占据 tileType: 'grass' // 可扩展瓦片类型:草地、山地、水域等 }; } }
3. 批量添加瓦片到舞台
不用一个个手动add,循环把所有瓦片加入图层即可:
// 假设你已经创建了Konva舞台和图层 const gameLayer = new Konva.Layer(); // 批量添加所有瓦片到图层 hexGrid.forEach(row => { row.forEach(tile => { gameLayer.add(tile.node); }); }); stage.add(gameLayer);
这样一来,原来1000多行的代码直接压缩到几十行,维护起来超省心——要改所有瓦片的颜色?直接改hexConfig.defaultFill;要找第3行第5列的瓦片?直接用hexGrid[2][4]就能拿到。
为后续精灵移动提前铺路
现在网格数据结构已经搭建好了,实现类似国际象棋的移动逻辑就变得很简单:
示例:创建可移动精灵
// 创建一个棋子精灵 const gamePiece = new Konva.Circle({ x: hexGrid[0][0].node.x(), y: hexGrid[0][0].node.y(), radius: hexConfig.radius * 0.6, fill: '#ff4444', name: 'game-piece', // 绑定当前位置数据 data: { currentRow: 0, currentCol: 0 } }); gameLayer.add(gamePiece); // 标记初始位置为被占据 hexGrid[0][0].isOccupied = true;
示例:实现点击瓦片移动的逻辑
// 给所有瓦片绑定点击事件 gameLayer.find('.hex-tile').forEach(tile => { tile.on('click', () => { const targetRow = tile.data('row'); const targetCol = tile.data('col'); const targetTile = hexGrid[targetRow][targetCol]; // 先判断移动是否合法:目标瓦片未被占据 + 符合移动规则 if (!targetTile.isOccupied && isValidMove(gamePiece.data('currentRow'), gamePiece.data('currentCol'), targetRow, targetCol)) { // 更新精灵位置 gamePiece.position({ x: targetTile.node.x(), y: targetTile.node.y() }); // 更新网格状态:释放原位置,占据新位置 hexGrid[gamePiece.data('currentRow')][gamePiece.data('currentCol')].isOccupied = false; targetTile.isOccupied = true; // 更新精灵的当前位置数据 gamePiece.data('currentRow', targetRow); gamePiece.data('currentCol', targetCol); // 重绘图层 gameLayer.draw(); } }); }); // 自定义移动规则:示例为六边形网格的相邻移动(类似国际象棋的王) function isValidMove(fromRow, fromCol, toRow, toCol) { const rowDiff = Math.abs(fromRow - toRow); const colDiff = Math.abs(fromCol - toCol); // 六边形相邻的规则: // 1. 行差为1时,列差最多为1(因为奇数行错位,上下相邻的列可能相同或差1) // 2. 行差为0时,列差必须为1(同一行左右相邻) if (rowDiff === 1) { return colDiff <= 1; } else if (rowDiff === 0) { return colDiff === 1; } return false; }
额外优化建议
- 把瓦片创建封装成单独的函数
createHexTile(row, col),代码结构更清晰; - 如果棋盘很大(比如超过100个瓦片),可以用Konva的
cache()方法给瓦片做缓存,提升渲染性能; - 把移动规则抽成独立模块,比如不同类型的精灵对应不同的
isValidMove函数,扩展性更强。
内容的提问来源于stack exchange,提问作者j a




