碰撞检测系列——矩形与矩形碰撞/相交

57 阅读5分钟

image.png

此文章是碰撞检测系列的第五篇,矩形和矩形碰撞检测/相交,此系列主要包含了多种形状的碰撞/相交检测方法。

预览

先查看效果吧,点击这里

碰撞/相交检测方法

点与矩形碰撞/相交基础上理解矩形与矩形碰撞/相交相对会容易些,只是判断条件会长一些。假设有两个矩形r1和r2,我们需要做以下的判断:

r1的右边是否在r2左边的右面?
r1的左边是否在r2右边的左面?
r1的底边是否在r2顶边的下面?
r1的顶边是否在r2底边的上面?

为了更直观,还是来张图片看看

image.png

让我们先试试一条边的方法,用r2的左边来测试r1的右边:

const r1RightEdge = r1x + r1w;
if (r1RightEdge >= r2x) {
    // r1的右边超过r2左边
}

就是这样的逻辑,咱们检测所有的边

if (r1x + r1w >= r2x &&     // r1右边超过r2的左边
  r1x <= r2x + r2w &&       // r1的左边超过r2右边
  r1y + r1h >= r2y &&       // r1的顶边超过r2的底边
  r1y <= r2y + r2h) {       // r1的底边超过r2的顶边
    return true;
}
return false;

虽然这里的数学是简单的加法,但是这种碰撞检测也算的上复杂的。可以通过练习,在你的脑海中描绘出这样的画面。在编写代码时,在一张纸上画一下草稿,逻辑会更清晰。

封装成函数工具是有必要的,完整的判断方法,如下:

/**
 * 
 * @param {Object} r1 矩形对象{x,y,w,h} x/y: 矩形左上角坐标; w:宽; h:高
 * @param {Object} r2 矩形对象{x,y,w,h} x/y: 矩形左上角坐标; w:宽; h:高
 * @returns boolean
 */
function rectangleRectangle(r1,r2) {
    // are the sides of one rectangle touching the other?
    if (r1.x + r1.w >= r2.x &&    // r1右边超过r2的左边
        r1.x <= r2.x + r2.w &&    // r1的左边超过r2右边
        r1.y + r1.h >= r2.y &&    // r1的顶边超过r2的底边
        r1.y <= r2.y + r2.h) {    // r1的底边超过r2的顶边
            return true;
    }
    return false;
}

主要代码

在我的demo中,当点与矩形碰撞/相交改变固定圆的颜色,可以点上面预览进去试试。这里是部分核心代码,详细代码结构解析点击这里 这里主要是渲染和交互代码,由于baseShape和cursorShape默认形状是圆,这里opt中参数需设置baseShape和cursorShape为rect,关于rect的位置和宽高,程序中有默认的计算方法,详情见代码。还有必须配置的hitFunc函数

const init = readyInit({
	baseShape: "rect",
	cursorShape: "rect",
	hitFunc: (e, drawOpt,opt) => {
		const w = opt.cursorW || 20;
		const h = opt.cursorH || 20;
		const cursorRect = { x:e.x - w/2, y:e.y - h/2, w, h }
		return hit.rectangleRectangle(drawOpt,cursorRect)
	}
})

// 图形渲染以及交互
function check(opt) {
    const ctx = utils.getCtx();
    const canvas = ctx.canvas;
    const zoom = opt.zoom || 1;
    const width = canvas.width / zoom;
    const height = canvas.height / zoom;
    const cp = { x: Math.round(width / 2), y: Math.round(height / 2) }
    ctx.scale(zoom, zoom)
    // 基础图形的绘制参数准备开始
    const radius = opt.radius || 10;
    const baseShape = opt.baseShape || 'circle'
    let drawOpt = opt.drawOpt;;
    if (baseShape === 'circle') {
        drawOpt = {...cp, r:radius}
    } else if (baseShape === 'rect') {
        const w = opt.w || 400;
        const h = opt.h || 200;
        drawOpt = {x:(width - w) / 2, y:(height - h) / 2, w, h}
    }
    // 基础图形的绘制参数准备结束

    // 渲染方法
    function render(colliding) {
        utils.cleanCanvas(ctx)
        ctx.fillStyle = '#0095d9E0';
        ctx.strokeStyle = '#0095d9E0';
        if (colliding) {
            // 碰撞时绘制效果
            if (opt.fillRectColliding) {
                // 碰撞时,改变背景图颜色(两点碰撞时使用,由于点太小,效果不明显)
                ctx.save()
                ctx.fillStyle = "#f6ad49";
                ctx.fillRect(0, 0, width, height);
                ctx.restore()
            } else {
                // 碰撞时,改变基础图形绘制颜色
                ctx.fillStyle = "#f6ad49E0";
                ctx.strokeStyle = "#f6ad49E0";
            }
        }
        // 相交的辅助点绘制,不是每个demo都会有
        const hitPoints = hit.hitPoints;
        if (hitPoints) {
            ctx.save()
            ctx.fillStyle = "red";
            hitPoints.forEach(p => {
                drawUtils.circle(ctx, { x: p.x, y: p.y, r: 16 })
            });
            ctx.restore()
        }
        
        ctx.lineWidth = 20;
        ctx.lineJoin = "round";
        ctx.lineCap = "round";
        // 基础图形绘制
        const drawFunc = drawUtils[baseShape];
        if (drawFunc) {
            drawFunc(ctx,drawOpt)
        }
        delete hit.hitPoints;
    }
    const radius1 = opt.radius1 || 10;
    const cursorShape = opt.cursorShape || 'circle'
    canvas.addEventListener('mousemove', (e) => {
        // 调用每个demo配置的hitFunc,检测碰撞结果
        const colliding = opt.hitFunc ? opt.hitFunc(e, drawOpt, opt) : false;
        // 移动鼠标重绘
        render(colliding);

        // 绘制鼠标图形,也就是移动的图形
        ctx.fillStyle = '#6a6868E0';
        if (cursorShape === 'rect') { 
            const w = opt.cursorW || 20;
            const h = opt.cursorH || 20;
            drawUtils.rect(ctx, { x:e.x / zoom - w/2, y:e.y / zoom - h/2, w, h })
        } else if (cursorShape === 'line') {
            ctx.strokeStyle = "#6a6868E0";
            ctx.lineWidth = 20;
            ctx.lineJoin = "round";
            ctx.lineCap = "round";
            drawUtils.line(ctx, [opt.cursorStartPoint,{ x:e.x, y:e.y}])
        } else if (cursorShape === 'polygon') {
            const { x, y } = e;
            const points = [
                { x: x - 20, y: y - 20 },
                { x: x + 40, y: y - 10 },
                { x: x + 60, y: y + 20 },
                { x: x - 20, y: y + 20 },
                {x: x - 40, y: y},
            ]
            drawUtils.polygon(ctx, points)
        } else {
            drawUtils.circle(ctx, { x:e.x / zoom, y:e.y / zoom, r:radius1 })
        }
        
    })
    render();
}

代码涉及到矩形的绘制,被抽取为一个工具方法,放在init.js文件中的drawUtils工具对象中

function rect(ctx, {x, y, w, h}) {
    ctx.fillRect(x, y, w, h);
}

扩展

圆与圆碰撞/相交类似,矩形/矩形碰撞可也以用来给较复杂的形状当“bounding boxes”。这个当然也会涉及到牺牲精度,提示性能的问题,这是一种平衡选择。这在我们能接触到交互中很常见,例如在游戏中,遇到不规则的元素时,虽然鼠标没有精准的在元素上,仍然会触发一些事件(划过、点击等)。这其实就是精度与性能上的取舍,同时也是实际正确和用户感觉正确之间找到适当的平衡

代码下载

以上代码只是主要代码并不是完整代码,由于完整代码较多就不贴出来了,有需要可以点击这里,这是GitHub的代码库,详细代码结构解析点击这里