有的时候我们任性,喜欢做一些自己喜欢的事,那么既然是做自己喜欢的事怎么有会说任性呢?这是因为这些我们花了时间和精力去做的事可能对收益也好、对成长也好并没有什么帮助,所以说做这个有些任性。
现在的工作或者未来的工作可能都不会跟游戏开发有任何关系,不过自己还总是冒出 idea,冲动想写点游戏。这可能是男孩的任性吧,其实每个男人不管多大他可能都是一个大男孩。不知道别人怎样。反正我是一个有些任性的大男孩,喜欢游戏、喜欢吹点小牛的大男孩。
游戏中有许多算法,今天我们就介绍其中一个判断两条线段相交,这个是一个比较基础、但是却比较实用的算法,会被应用学多游戏场景下。废话不多说,首先我们搭建环境。
开发环境很简单,只要有一个浏览器和你应手的写代码的 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();
绘制标注
绘制好这些线段后,还需要将 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");
效果如下图
绘制线段上的插值
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 中间的值。
添加动画
接下来添加动画,通过改变 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);
}
从图上来看随着 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";
...
}
接下来就是数学公式
是上面 等式进行化简 移动到等式左边得到
这是如果等式两边都除以 就可以计算用 t 表示 ,因为 有可能是 0 如果这时与 x 轴垂直的线段。那么 和 的值就是相等的。
将等式右侧的 移动到等式左侧得到如下式子
然后对上面式子两边乘以 就得到下面的公式
将 带入到 就可以求得
这样一来我们就可以计算出
也是就是我们实现计算两条线段交点的一个方法。
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)
}
}
验证
接下来工作就是验证 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;
...
}
不过这样做,移动 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;
优化代码
到现在为止还遗留两个问题,第一个问题当两条线平行时候,分母 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
}
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。