KonvaJS/Canvas带障碍物的动态战争迷雾精准显示问题求助
2D棋盘战争迷雾的障碍物遮挡修正方案
问题背景
用KonvaJS实现了2D方格棋盘,标记物可移动,已完成手动添加/移除战争迷雾功能。需求是标记物移动时自动显示周围一定区域,同时要考虑墙体遮挡。当前实现存在两个问题:
- 按标记物相对于墙体的位置缩放显示区域,会导致本该可见的区域(如紫色线标注的方格)被错误隐藏
- 仅移除显示区域与墙体的交集,会出现显示区域穿透墙体看到另一侧的情况
现有核心代码:
const tokenPosition = { x: 10, y: 10 }; const haveIntersection = (r1, r2) => !( r2.x > r1.x + r1.width || // Compares top left with top right r2.x + r2.width < r1.x || // Compares top right with top left r2.y > r1.y + r1.height || // Compare bottom left with bottom right r2.y + r2.height < r1.y // Compare bottom right with bottom left ); walls.forEach(wall => { const redArea = { x: wall.x, y: wall.y, width: wall.width, height: wall.height }; // blueArea has the same properties as redArea if (haveIntersection(blueArea, redArea)) { const tokenToTheRight = tokenPosition.x > wall.x + wall.width; const tokenToTheLeft = tokenPosition.x < wall.x; const tokenToTheTop = tokenPosition.y < wall.y; const tokenToTheBottom = tokenPosition.y > wall.y + wall.height; if (tokenToTheRight) { let diff = wall.x + wall.width - blueArea.x; blueArea.x = wall.x + wall.width; blueArea.width = blueArea.width - diff; } if (tokenToTheLeft) { let diff = blueArea.x + blueArea.width - wall.x; blueArea.width = blueArea.width - diff; } if (tokenToTheTop) { let diff = blueArea.y + blueArea.height - wall.y; blueArea.height = blueArea.height - diff; } if (tokenToTheBottom) { let diff = wall.y + wall.height - blueArea.y; blueArea.y = wall.y + wall.height; blueArea.height = blueArea.height - diff; } } });
修正方案与替代实现
问题根源
当前代码的核心问题是整体修改显示区域的宽高,但墙体通常只遮挡显示区域的局部,直接缩放会误删墙体旁本该可见的区域。比如标记物在墙体左上方,墙体挡住显示区域右下角,代码会直接缩小显示区域的高度或宽度,导致右侧/下方所有区域被隐藏,而非仅移除被墙体遮挡的部分。
方案1:修正区域裁剪逻辑
不再整体修改显示区域,而是计算显示区域与墙体的交集,拆分显示区域并保留标记物可见的部分:
const tokenPosition = { x: 10, y: 10 }; // 计算两个矩形的交集 const getIntersection = (r1, r2) => { const x = Math.max(r1.x, r2.x); const y = Math.max(r1.y, r2.y); const width = Math.min(r1.x + r1.width, r2.x + r2.width) - x; const height = Math.min(r1.y + r1.height, r2.y + r2.height) - y; return width > 0 && height > 0 ? { x, y, width, height } : null; }; // 用数组存储多个可见区域,初始为原显示区域 let visibleAreas = [JSON.parse(JSON.stringify(blueArea))]; walls.forEach(wall => { const redArea = { x: wall.x, y: wall.y, width: wall.width, height: wall.height }; const newVisibleAreas = []; visibleAreas.forEach(area => { const intersection = getIntersection(area, redArea); if (!intersection) { newVisibleAreas.push(area); return; } // 判断标记物是否能看到交集区域的另一侧 const tokenCanSeeLeft = tokenPosition.x < intersection.x; const tokenCanSeeRight = tokenPosition.x > intersection.x + intersection.width; const tokenCanSeeTop = tokenPosition.y < intersection.y; const tokenCanSeeBottom = tokenPosition.y > intersection.y + intersection.height; // 拆分区域,保留可见部分 // 上方未被遮挡的区域 if (area.y < intersection.y) { newVisibleAreas.push({ x: area.x, y: area.y, width: area.width, height: intersection.y - area.y }); } // 下方未被遮挡的区域 if (area.y + area.height > intersection.y + intersection.height) { newVisibleAreas.push({ x: area.x, y: intersection.y + intersection.height, width: area.width, height: (area.y + area.height) - (intersection.y + intersection.height) }); } // 左侧未被遮挡的区域(标记物看不到右侧时保留) if (area.x < intersection.x && !tokenCanSeeRight) { newVisibleAreas.push({ x: area.x, y: intersection.y, width: intersection.x - area.x, height: intersection.height }); } // 右侧未被遮挡的区域(标记物看不到左侧时保留) if (area.x + area.width > intersection.x + intersection.width && !tokenCanSeeLeft) { newVisibleAreas.push({ x: intersection.x + intersection.width, y: intersection.y, width: (area.x + area.width) - (intersection.x + intersection.width), height: intersection.height }); } }); visibleAreas = newVisibleAreas; }); // 后续绘制时遍历visibleAreas数组,逐个绘制每个可见区域
方案2:射线投射法(方格棋盘更精准)
针对方格棋盘,用射线检测逐个判断每个方格是否在标记物视野内,彻底解决遮挡问题:
const gridSize = 32; // 单个方格的像素尺寸 const viewRange = 5; // 视野覆盖的方格数 // 获取标记物所在的方格坐标 const tokenGridX = Math.floor(tokenPosition.x / gridSize); const tokenGridY = Math.floor(tokenPosition.y / gridSize); // 遍历视野范围内的所有方格 for (let dx = -viewRange; dx <= viewRange; dx++) { for (let dy = -viewRange; dy <= viewRange; dy++) { const targetGridX = tokenGridX + dx; const targetGridY = tokenGridY + dy; // 跳过超出棋盘边界的方格 if (!isValidGrid(targetGridX, targetGridY)) continue; // 计算射线起点(标记物方格中心)和终点(目标方格中心) const startX = tokenPosition.x + gridSize / 2; const startY = tokenPosition.y + gridSize / 2; const endX = targetGridX * gridSize + gridSize / 2; const endY = targetGridY * gridSize + gridSize / 2; // 检查射线是否被墙体阻挡 let isVisible = true; for (const wall of walls) { if (rayIntersectsRect(startX, startY, endX, endY, wall)) { isVisible = false; break; } } // 设置方格的迷雾状态 if (isVisible) { revealGrid(targetGridX, targetGridY); // 自定义函数:显示方格 } else { hideGrid(targetGridX, targetGridY); // 自定义函数:隐藏方格 } } } // 判断射线是否与矩形墙体相交 function rayIntersectsRect(sx, sy, ex, ey, rect) { const rectLeft = rect.x; const rectRight = rect.x + rect.width; const rectTop = rect.y; const rectBottom = rect.y + rect.height; // 检查射线是否与矩形的四条边相交 return lineIntersects(sx, sy, ex, ey, rectLeft, rectTop, rectRight, rectTop) || lineIntersects(sx, sy, ex, ey, rectRight, rectTop, rectRight, rectBottom) || lineIntersects(sx, sy, ex, ey, rectRight, rectBottom, rectLeft, rectBottom) || lineIntersects(sx, sy, ex, ey, rectLeft, rectBottom, rectLeft, rectTop); } // 判断两条线段是否相交 function lineIntersects(x1, y1, x2, y2, x3, y3, x4, y4) { const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); if (denom === 0) return false; const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom; const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom; return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1; } // 自定义辅助函数:检查方格是否在棋盘内 function isValidGrid(x, y) { return x >= 0 && x < gridWidth && y >= 0 && y < gridHeight; }
方案对比
- 方案1:基于区域拆分,修改成本低,适配原有区域绘制逻辑,但复杂墙体布局下可能存在边缘误差
- 方案2:射线检测精度高,能处理各种遮挡场景,适合方格棋盘,但需要逐个方格检测,性能受视野范围影响
内容的提问来源于stack exchange,提问作者Adam Silva
相关产品推荐
相关产品推荐

