You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native游戏拖拽Tile避免重叠问题求助

React Native 游戏Tile拖拽重叠问题排查与修复

我用React Native开发一款游戏,将一组Tile对象绝对定位渲染,吸附点是2行16列的网格。每个Tile有Width、height和coordinates属性。拖拽释放后,Tile偶尔会出现重叠,推测是把第二行(y>0)的元素拖到第一行(y===0)时触发,但不确定原因。

当Tile被移至已占用的吸附点时,连锁反应函数需要让连锁中的其他Tile左右移动或换行到行首/行尾,相关代码如下:

连锁反应处理函数

// Function to handle chain reaction
const handleChainReaction = (newTiles, movedTile) => {
  let tilesToCheck = [movedTile];

  while (tilesToCheck.length > 0) {
    let currentTile = tilesToCheck.shift();

    let occupiedIndex;
    do {
      occupiedIndex = newTiles.findIndex(
        (t) =>
          t.x === currentTile.x &&
          t.y === currentTile.y &&
          t.key !== currentTile.key
      );

      if (occupiedIndex !== -1) {
        let displacedTile = { ...newTiles[occupiedIndex] };
        displacedTile.x += TILE_WIDTH; // Move the tile to the right

        // Check for horizontal boundary
        if (displacedTile.x >= CONTAINER_WIDTH) {
          displacedTile.x = 0;
          displacedTile.y += TILE_HEIGHT;

          // Check for vertical boundary
          if (displacedTile.y >= TOTAL_HEIGHT) {
            displacedTile.y = 0;
          }
        }

        newTiles[occupiedIndex] = displacedTile;

        // Add the displaced tile to the check queue
        tilesToCheck.push(displacedTile);
      }
    } while (occupiedIndex !== -1);
  }

  return newTiles;
};

PanResponder 代码

const panResponder = useMemo(
  () =>
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderMove: (e, gestureState) => {
        const newX = initialPan.x + gestureState.dx;
        const newY = initialPan.y + gestureState.dy;
        pan.setValue({ x: newX, y: newY });
      },
      onPanResponderRelease: (e, gestureState) => {
        const finalX = initialPan.x + gestureState.dx;
        const finalY = initialPan.y + gestureState.dy;
        const closestSnapPoint = findClosestSnapPoint(
          finalX,
          finalY,
          snapPoints
        );

        let newTiles = [...tiles]; // Clone the tiles array
        const movedTileIndex = newTiles.findIndex(t => t.key === tile.key);
        let movedTile = { ...newTiles[movedTileIndex], x: closestSnapPoint.x, y: closestSnapPoint.y };
        newTiles[movedTileIndex] = movedTile;
        newTiles = handleChainReaction(newTiles, movedTile);

        checkGroups(newTiles);

        // Animate to the closest snap point
        Animated.spring(pan, {
          toValue: { x: closestSnapPoint.x, y: closestSnapPoint.y },
          useNativeDriver: false,
          friction: 30,
          tension: 60,
        }).start();

        // Update the initial position for the next drag
        initialPan.x = closestSnapPoint.x;
        initialPan.y = closestSnapPoint.y;
      },
    }),
  [tiles]
);

吸附点生成代码

for (let row = 0; row < 2; row++) {
  for (let col = 0; col < 16; col++) {
    const x = col * TILE_WIDTH;
    const y = row * TILE_HEIGHT;
    snaps.push({ x, y });
  }
}

Tile对象示例

{"color": "blue", "groupIndex": 0, "groupLength": 0, "isInGroup": false, "key": "n13-cblue", "label": 13, "number": 13, "x": 576.675, "y": 53.823}

问题根源分析

  1. 浮点精度误差:Tile的x/y值为浮点数,但位置判断用严格相等===比较,微小的浮点误差会导致无法检测到位置重叠,最终出现Tile叠加。
  2. 换行逻辑不严谨:仅处理向右移动到行尾换行到下一行的场景,未考虑2行网格的循环边界逻辑,且没有处理反向挤压的情况。
  3. 连锁反应队列重复处理:do-while循环会重复检查同一位置,可能导致Tile被多次移动,或遗漏新的位置冲突。

修复方案

1. 用网格坐标替代浮点值判断位置

通过将x/y转换为整数网格行列,避免浮点精度问题:

// 新增工具函数:转换为网格坐标
const getGridPos = (x, y) => ({
  col: Math.round(x / TILE_WIDTH),
  row: Math.round(y / TILE_HEIGHT)
});

// 修改handleChainReaction中的位置判断逻辑
occupiedIndex = newTiles.findIndex(t => {
  const currGrid = getGridPos(currentTile.x, currentTile.y);
  const tileGrid = getGridPos(t.x, t.y);
  return currGrid.col === tileGrid.col && currGrid.row === tileGrid.row && t.key !== currentTile.key;
});

2. 完善连锁位移与换行逻辑

基于网格行列处理位移,确保2行网格的循环边界正确:

// 修改handleChainReaction中的位移代码
if (occupiedIndex !== -1) {
  let displacedTile = { ...newTiles[occupiedIndex] };
  const displacedGrid = getGridPos(displacedTile.x, displacedTile.y);

  // 向右移动一格
  displacedGrid.col += 1;
  // 超出列数则换行
  if (displacedGrid.col >= 16) {
    displacedGrid.col = 0;
    displacedGrid.row += 1;
    // 超出行数则回到第一行
    displacedGrid.row = displacedGrid.row >= 2 ? 0 : displacedGrid.row;
  }

  // 转换回实际坐标
  displacedTile.x = displacedGrid.col * TILE_WIDTH;
  displacedTile.y = displacedGrid.row * TILE_HEIGHT;

  newTiles[occupiedIndex] = displacedTile;
  tilesToCheck.push(displacedTile);
}

3. 优化连锁队列处理逻辑

移除do-while循环,改为单次检查后加入队列,避免重复处理同一位置:

while (tilesToCheck.length > 0) {
  let currentTile = tilesToCheck.shift();
  const occupiedIndex = newTiles.findIndex(t => {
    const currGrid = getGridPos(currentTile.x, currentTile.y);
    const tileGrid = getGridPos(t.x, t.y);
    return currGrid.col === tileGrid.col && currGrid.row === tileGrid.row && t.key !== currentTile.key;
  });

  if (occupiedIndex !== -1) {
    let displacedTile = { ...newTiles[occupiedIndex] };
    const displacedGrid = getGridPos(displacedTile.x, displacedTile.y);

    displacedGrid.col += 1;
    if (displacedGrid.col >= 16) {
      displacedGrid.col = 0;
      displacedGrid.row += 1;
      displacedGrid.row = displacedGrid.row >= 2 ? 0 : displacedGrid.row;
    }

    displacedTile.x = displacedGrid.col * TILE_WIDTH;
    displacedTile.y = displacedGrid.row * TILE_HEIGHT;

    newTiles[occupiedIndex] = displacedTile;
    tilesToCheck.push(displacedTile);
  }
}

4. 确保拖拽后的吸附坐标精确

在释放拖拽时,直接使用网格计算后的精确坐标,避免浮点误差:

// 修改onPanResponderRelease中的movedTile赋值
const closestSnapPoint = findClosestSnapPoint(finalX, finalY, snapPoints);
const snapGrid = getGridPos(closestSnapPoint.x, closestSnapPoint.y);
const preciseX = snapGrid.col * TILE_WIDTH;
const preciseY = snapGrid.row * TILE_HEIGHT;

let movedTile = { ...newTiles[movedTileIndex], x: preciseX, y: preciseY };

内容的提问来源于stack exchange,提问作者Abdul Sadik Yalcin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 19:24:54