You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于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

火山引擎 最新活动