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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 17:01:26