为开发游戏做准备—判断线段相交(JavaScript 版本)

·  阅读 81
为开发游戏做准备—判断线段相交(JavaScript 版本)

有的时候我们任性,喜欢做一些自己喜欢的事,那么既然是做自己喜欢的事怎么有会说任性呢?这是因为这些我们花了时间和精力去做的事可能对收益也好、对成长也好并没有什么帮助,所以说做这个有些任性。

game_developer.webp

现在的工作或者未来的工作可能都不会跟游戏开发有任何关系,不过自己还总是冒出 idea,冲动想写点游戏。这可能是男孩的任性吧,其实每个男人不管多大他可能都是一个大男孩。不知道别人怎样。反正我是一个有些任性的大男孩,喜欢游戏、喜欢吹点小牛的大男孩。

play_game.jpeg

游戏中有许多算法,今天我们就介绍其中一个判断两条线段相交,这个是一个比较基础、但是却比较实用的算法,会被应用学多游戏场景下。废话不多说,首先我们搭建环境。

开发环境很简单,只要有一个浏览器和你应手的写代码的 IDE 就行。

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Segment Intersection</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
    </style>
</head>
<body>
    <canvas id="mCanvas"></canvas>
</body>
</html>
复制代码

我们创建 Html ,在其中创建了 canvas Html 元素,通常我们游戏是由一张一张连续的画面所组成的,canvas 就是绘制画面的画布,接下来大部分工作都是通过操作这个画布来完成的。

定义画布

创建画布并对其进行适当设置,设置画布的宽度和高度。画布为了让我们对其进行操作,就提供了 Context 对象,所以首先要获取到 Context 这个对象,通过调用 Context 对象提供方法,就可以在画布上任意绘制。

mCanvas.width = window.innerWidth;
mCanvas.height = window.innerHeight;
const ctx = mCanvas.getContext('2d');
复制代码

绘制线段

首先我们来定义 A、B、C 和 D 这几个点,连接 A 和 B 两点形成一条线段,用 AB 来表示、连接 C 和 D 两点形成另一条线段。接下来我们就是要将AB 和 CD 这两条线段绘制在画布上。

const A = { x: 200, y: 150 };
const B = { x: 150, y: 260 };
const C = { x: 50, y: 100 };
const D = { x: 260, y: 200 };

ctx.beginPath();
ctx.moveTo(A.x, A.y);
ctx.lineTo(B.x, B.y);
ctx.moveTo(C.x, C.y);
ctx.lineTo(D.x, D.y);

ctx.stroke();
复制代码

屏幕快照 2022-07-08 下午5.54.44.png

绘制标注

绘制好这些线段后,还需要将 A、B、C 和 D 这 4 个点显式地标注出来。具体实现我们用了一个函数 drawDot 来实现。

function drawDot(point, label) {
    ctx.beginPath();
    ctx.fillStyle = "white";
    ctx.arc(point.x, point.y, 10, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
    ctx.fillStyle = "black";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.font = "bold 14px Arial";
    ctx.fillText(label, point.x, point.y);
}
复制代码

通过下面两行代码可以标注文字和绘制点在水平和竖直方向上对其居中。

ctx.textAlign = "center";
ctx.textBaseline = "middle";
复制代码
drawDot(A, "A");
drawDot(B, "B");
drawDot(C, "C");
drawDot(D, "D");
复制代码

效果如下图

屏幕快照 2022-07-08 下午6.05.12.png

绘制线段上的插值

const M = {
    x: lerp(A.x,B.x,0.5),
    y: lerp(A.y,B.y,0.5)
}

function lerp(A,B,t){
    return A + (B-A)*t
}

drawDot(M,"M")
复制代码

这里定义了一个 lerp 函数主要到 A 和 B 间的一个中间值,中间值位置是由 t 来确定,t 是一个 0 到 1 范围的数据,如果取 0 那么返回就是 A 的值,如何取 1 时候返回就是 B 的值。这里我们取 0.5 也就是位于 A 和 B 中间的值。

屏幕快照 2022-07-08 下午6.11.29.png

添加动画

接下来添加动画,通过改变 t 的值来改变 M 在 AB 线段以及其延长线上位置来便于观察

let t = 0;
animate();
function animate() {
    ctx.clearRect(0, 0, mCanvas.width, mCanvas.height);
    ctx.beginPath();
    ctx.moveTo(A.x, A.y);
    ctx.lineTo(B.x, B.y);
    ctx.moveTo(C.x, C.y);
    ctx.lineTo(D.x, D.y);

    ctx.stroke();

    drawDot(A, "A");
    drawDot(B, "B");
    drawDot(C, "C");
    drawDot(D, "D");

    const M = {
        x: lerp(A.x, B.x, t),
        y: lerp(A.y, B.y, t)
    }
    drawDot(M, "M");
    t += 0.02;

    requestAnimationFrame(animate);
}
复制代码

屏幕快照 2022-07-08 下午6.25.25.png

从图上来看随着 t 不断增大,M 会在 AB 延长线上继续向左下方移动。移出AB的范围,这是我们想用另一种颜色来表示移动出 AB 范围以外的 M 点。

drawDot(M, "M", t < 0 || t > 1);
复制代码
function drawDot(point, label, isRed) {
    ctx.beginPath();
    ctx.fillStyle = isRed ? "red" : "white";
    ...
}
复制代码

接下来就是数学公式

Ix=Ax+(BxAx)t=Cx+(DxCx)uIy=Ay+(ByAy)t=Cy+(DyCy)uI_x = A_x + (B_x - A_x)t = C_x + (D_x - C_x)u\\ I_y = A_y + (B_y - A_y)t = C_y + (D_y - C_y)u\\

是上面Ax+(BxAx)t=Cx+(DxCx)uA_x + (B_x - A_x)t = C_x + (D_x - C_x)u 等式进行化简 CxC_x 移动到等式左边得到

AxCx+(BxAx)t=(DxCx)uA_x - C_x + (B_x - A_x)t =(D_x - C_x)u

这是如果等式两边都除以 (DxCx)(D_x - C_x) 就可以计算用 t 表示 uu,因为 (DxCx)(D_x - C_x) 有可能是 0 如果这时与 x 轴垂直的线段。那么 DxD_xCxC_x 的值就是相等的。

Ay+(ByAy)t=Cy+(DyCy)uA_y + (B_y - A_y)t = C_y + (D_y - C_y)u

将等式右侧的 CyC_y 移动到等式左侧得到如下式子

(AyCy)+(ByAy)t=(DyCy)u(A_y - C_y)+ (B_y - A_y)t = (D_y - C_y)u

然后对上面式子两边乘以 (DxCx)(D_x - C_x) 就得到下面的公式

(AyCy)(DxCx)+(ByAy)(DxCx)t=(DyCy)(DxCx)u(A_y - C_y)(D_x - C_x)+ (B_y - A_y)(D_x - C_x)t = (D_y - C_y)(D_x - C_x)u
(DxCx)u=AxCx+(BxAx)t(D_x - C_x)u = A_x - C_x + (B_x - A_x)t

(DxCx)u(D_x - C_x)u 带入到 (AyCy)(DxCx)+(ByAy)(DxCx)t=(DyCy)(DxCx)u(A_y - C_y)(D_x - C_x)+ (B_y - A_y)(D_x - C_x)t = (D_y - C_y)(D_x - C_x)u 就可以求得 tt

这样一来我们就可以计算出 tt

t=(DxCx)(AyCy)(DyCy)(AxCx)(DyCy)(BxAx)(DxCx)(ByAy)t = \frac{(D_x - C_x)(A_y - C_y) - (D_y - C_y)(A_x - C_x)}{(D_y - C_y)(B_x - A_x) - (D_x - C_x)(B_y - A_y)}

也是就是我们实现计算两条线段交点的一个方法。

function getIntersection(A, B, C, D) {
    const top = (D.x - C.x) * (A.y - C.y) - (D.y - C.y) * (A.x - C.x)
    const bottom = (D.y - C.y) * (B.x - A.x) - (D.x - C.x) * (B.y - A.y)

    const t = top / bottom;
    return {
        x: lerp(A.x, B.x, t),
        y: lerp(A.y, B.y, t)
    }
}
        
复制代码

屏幕快照 2022-07-08 下午9.05.26.png

屏幕快照 2022-07-08 下午9.18.55.png

验证

接下来工作就是验证 getIntersection 这个函数在各种场景是否好用了,那么我们写一个可以通过移动鼠标来移动线段 AB ,然后观测当我们随意移动 AB 时候是否可以看出 AB 与 CD 之间交点是否显示正确。

const mouse = {x:0,y:0};
document.onmousemove=(event=>{
    mouse.x = event.x;
    mouse.y = event.y;
});

function animate() {
    const radius = 50;
    A.x = mouse.x;
    A.y = mouse.y + radius;
    B.x = mouse.x;
    B.y = mouse.y - radius;

    ...
}
复制代码

屏幕快照 2022-07-08 下午9.19.06.png

不过这样做,移动 AB 线段始终是和竖直方向,希望能够成任意角度来和 CD 进行相交测试。

let angle = 0;
复制代码

然后在 animate 函数将 angle 这个参数添加到位置中

let angle = 0;
document.onmousemove=(event=>{
    mouse.x = event.x;
    mouse.y = event.y;
})

// let t = 0;
animate();
function animate() {
    const radius = 50;
    A.x = mouse.x + Math.cos(angle)*radius;
    A.y = mouse.y - Math.sin(angle)*radius;
    B.x = mouse.x - Math.cos(angle)*radius
    B.y = mouse.y + Math.sin(angle)*radius;

    angle+=0.02;
复制代码

屏幕快照 2022-07-09 上午6.31.00.png

优化代码

到现在为止还遗留两个问题,第一个问题当两条线平行时候,分母 bottom 为 0,还有就是我们希望 I 只在两条线段相交的位置显示。

function getIntersection(A, B, C, D) {
    const top = (D.x - C.x) * (A.y - C.y) - (D.y - C.y) * (A.x - C.x)
    const bottom = (D.y - C.y) * (B.x - A.x) - (D.x - C.x) * (B.y - A.y)

    if(bottom != 0){
        const t = top / bottom;
        if(t>=0 && t<=1){
            return {
                x: lerp(A.x, B.x, t),
                y: lerp(A.y, B.y, t),
                offset:t
            }
        }
    }
    return null
}
复制代码

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

分类:
前端
收藏成功!
已添加到「」, 点击更改