友情提示
- 请先看这里 👉 链接
- 代码的GitHub地址 👉 链接
- 阅读本文建议将代码回退到
2db5b341bd905125a9847dc97d01f68356630534
这个commit,以去掉一些与本文功能无关的代码。
1. 前言
假设我们现在在canvas上画了很多个节点(DisplayObject),这些节点形成了一套层级关系,我们将鼠标移动到canvas元素上并点击了一下左键,这个时候,如何判断鼠标点到了哪个节点呢?
碰撞检测
是非常重要的一个功能,因为下一篇要讲的事件系统
,就是基于碰撞检测
做的。本文将讲述如何实现碰撞检测。
2. 两种方案
2.1 像素标记法
像素标记法
的做法是:除了用户看得见的那个canvas元素(以下称canvasA),渲染引擎会在内部维护一个离屏的canvas元素(以下称canvasB),每个要被绘制的元素(DisplayObject),都会被赋予一种独一无二的颜色,这些元素除了在canvasA上绘制一次,还要在canvasB上绘制一次(用上面提到的那个独一无二的颜色绘制),并且绘制的顺序是一样的(先序遍历对象树),在canvasA上触发点击事件时,我们会拿到鼠标落在canvasA上的坐标,然后去canvasB上取到这个坐标对应的那一个像素的色值,由于每个要被绘制的元素都被赋予了一种独一无二的色值,所以这个时候我们就已经可以根据这个色值来得到对应的元素了。
2.2 射线法(奇偶环绕法则)
射线法
是针对单个多边形的一种碰撞检测方式
假设现在有一个封闭的多边形,射线法
的逻辑是:先维护一个计数器count,然后从要检测的那个点发出一条无限远的射线,这条射线每穿过一次这个多边形的一条边,就让count加1,如果最后count是一个奇数,那么我们就判断这个点在这个多边形的内部。
2.3 两种方案的对比
这两种方案各有利弊,像素标记法
的逻辑比较简单,它利用了canvas的能力来做碰撞检测这件事情,Konva
这个库就用了像素标记法
,但像素标记法
的缺点就是每个图形都需要绘制2次,这对于性能的影响是非常大的,如果要绘制的图形比较多,那么绘制时间会明显地上升许多。射线法
的逻辑要更复杂一些,它不利用canvas的能力,而是完全自己来实现碰撞检测,对编码的要求相对于像素标记法
要高一些,但是射线法
的性能比像素标记法
要高很多,pixiJS
就使用了射线法
来做碰撞检测。
2.4 该用哪种方案?
用哪种方案没有一个明确的答案,还是要根据实际情况来做选择,本文将会实现这两种方案,但是和pixijs一样,在本文的渲染引擎中最终会采用射线法
这种方案,像素标记法
的代码会被放到另一个分支feat/hit-test-by-marking-pixel
上,而主分支上只有射线法
这种方案的代码,后续的事件系统也是基于射线法
这种方案的。
3. 像素标记法实现
3.1 分配一个独一无二的颜色
在Container类上挂载一个uniqueColor
属性,每次实例化这个类都会生成一个随机的16进制颜色
typescript
public uniqueColor: string = randomHexCreator()
3.2 创建离屏canvas元素
在CanvasRenderer类上创建一个离屏canvas元素
typescript
private offscreenCanvas = document.createElement('canvas')
3.3 创建map用来记录色值对应的元素
在Container类上创建静态属性hitTestMap
typescript
public static hitTestMap: { [anyKey: string]: Container } = {}
3.4 在离屏canvas元素上绘制
在调用Graphics类的renderCanvas函数完成绘制后,还需要要调用renderOnOffscreenCanvas函数来在离屏canvas元素上再次绘制一次
typescript
private renderOnOffscreenCanvas(render: CanvasRenderer) {
this.startPoly()
const offscreenCtx = render.offscreenCtx
const { a, b, c, d, tx, ty } = this.transform.worldTransform
offscreenCtx.setTransform(a, b, c, d, tx, ty)
const graphicsData = this._geometry.graphicsData
offscreenCtx.fillStyle = this.uniqueColor // 填充色为被赋予的独一无二的颜色
offscreenCtx.globalAlpha = 1 // 所有填充的不透明度都是1
// 用map将uniqueColor和要绘制的图形对应起来
if (!Container.hitTestMap[this.uniqueColor]) {
Container.hitTestMap[this.uniqueColor] = this
}
for (let i = 0; i < graphicsData.length; i++) {
const data = graphicsData[i]
const { fillStyle, shape } = data
offscreenCtx.beginPath()
if (shape instanceof Rectangle) {
const rectangle = shape
const { x, y, width, height } = rectangle
if (fillStyle.visible) {
offscreenCtx.fillRect(x, y, width, height)
}
}
if (shape instanceof Circle) {
const circle = shape
const { x, y, radius } = circle
offscreenCtx.arc(x, y, radius, 0, 2 * Math.PI)
if (fillStyle.visible) {
offscreenCtx.fill()
}
}
if (shape instanceof RoundedRectangle) {
const roundedRectangle = shape
const { x, y, width, height, radius } = roundedRectangle
offscreenCtx.moveTo(x + radius, y)
offscreenCtx.arc(
x + radius,
y + radius,
radius,
Math.PI * 1.5,
Math.PI,
true
)
offscreenCtx.lineTo(x, y + height - radius)
offscreenCtx.arc(
x + radius,
y + height - radius,
radius,
Math.PI,
Math.PI / 2,
true
)
offscreenCtx.lineTo(x + width - radius, y + height)
offscreenCtx.arc(
x + width - radius,
y + height - radius,
radius,
Math.PI / 2,
0,
true
)
offscreenCtx.lineTo(x + width, y + radius)
offscreenCtx.arc(
x + width - radius,
y + radius,
radius,
0,
Math.PI * 1.5,
true
)
offscreenCtx.closePath()
if (fillStyle.visible) {
offscreenCtx.fill()
}
}
if (shape instanceof Ellipse) {
const ellipse = shape
const { x, y, radiusX, radiusY } = ellipse
offscreenCtx.ellipse(x, y, radiusX, radiusY, 0, 0, Math.PI * 2)
if (fillStyle.visible) {
offscreenCtx.fill()
}
}
if (shape instanceof Polygon) {
const polygon = shape
const { points, closeStroke } = polygon
offscreenCtx.moveTo(points[0], points[1])
for (let i = 2; i < points.length; i += 2) {
offscreenCtx.lineTo(points[i], points[i + 1])
}
if (closeStroke) {
offscreenCtx.closePath()
}
if (fillStyle.visible) {
offscreenCtx.fill()
}
}
}
}
renderOnOffscreenCanvas
函数和renderCanvas
的执行逻辑差不多,都是遍历_geometry属性将所有子图形绘制出来,但是它们有一些区别:
- 由于线条不参与碰撞检测,所以
renderOnOffscreenCanvas
没有stroke。 renderOnOffscreenCanvas
在绘制时,不透明度(ctx.globalAlpha)都是1,色值为那个被赋予的独一无二的色值- 在绘制的时候,将当前元素与那个独一无二的色值对应起来(放在
hitTestMap
里)
3.5 监听点击事件,并在点击后拿到对应的点击的元素
我们会在用户能看到的那个canvas元素上监听事件,而不是离屏canvas元素
typescript
this.canvasEle.addEventListener('click', (e) => {
// 取到鼠标落点对应的那个像素
const [r, g, b] = this.offscreenCtx.getImageData(
e.offsetX,
e.offsetY,
1,
1
).data
// 将10进制转化成16进制,注意有可能出现转化后只有1位的情况,这个时候需要补一个0
const hexR =
r.toString(16).length === 1 ? '0' + r.toString(16) : r.toString(16)
const hexG =
g.toString(16).length === 1 ? '0' + g.toString(16) : g.toString(16)
const hexB =
b.toString(16).length === 1 ? '0' + b.toString(16) : b.toString(16)
const color = `#${hexR}${hexG}${hexB}`
// 去map上取到对应的碰撞对象
const target = Container.hitTestMap[color]
if (target?.uniqueColor) {
message.success(`点到了色值为${target.uniqueColor}的元素`)
} else {
message.error(`没有点到任何元素`)
}
})
用getImageData
函数来取到鼠标点到的那个像素
3.6 测试
在画布上绘制了绿色、黑色、粉色三个元素
typescript
const g1 = new Graphics().beginFill('green').drawRect(200, 200, 200, 200)
const g2 = new Graphics().beginFill('black').drawRect(200, 200, 100, 100)
const g3 = new Graphics().beginFill('pink').drawRect(250, 250, 100, 100)
const c = new Container()
c.addChild(g2)
c.addChild(g3)
app.stage.addChild(g1)
app.stage.addChild(c)
可以看到,点击了对应的元素之后,拿到了这个元素的被分配的那个独一无二的色值,如果没有点到任何元素,target为null。
4. 射线法实现
针对复杂的多边形,可以采用射线法来做碰撞检测,对于规则图形则用不到射线法,比如圆,碰撞检测的方式是:判断待检测点与圆心的距离是否小于圆的半径就行了。
4.1 原理(奇偶环绕法则)
维护一个计数器count,计数器的初始值为0,然后从待检测点发出一条射线,这条射线每穿过封闭图形的边一次,就让count加1,如果最后count为奇数,则判断该点在封闭图形内部,如果为偶数,则判断该点在封闭图形外部。
举个几个例子:
例子1: 红点为待检测点,从该点发出一条射线,这条射线穿过了封闭图形的边一次,count最后的结果为1,为奇数,所以红点在这个封闭图形的内部。
例子2: 从红点发出的射线穿过了封闭图形的边3次,count为3,为奇数,所以判断红点在封闭图形的内部
例子3: 从红点发出的射线穿过了封闭图形的边2次,这个时候判断红点在封闭图形的外部。
4.2 如何实现
判断射线与曲线线段是否相交,是比较困难的,但是判断射线与直线线段相交,相对就简单许多,在上一篇文章说了,除了一些规则的曲线图形(完整的圆、椭圆),其他的不规则的曲线图形,都会用直边多边形来代替。
所以,射线法其实只需要处理直边多边形的情况,如下:
具体做法就是,用for循环判断这个直边多边形的每一条边,如果相交则让count+1,循环结束后就能得到count了。
所以,问题就来到了:如何判断一条射线与一条线段是否相交
4.3 如何判断射线与线段是否相交
我们会从待检测点发出一条水平向右的无限远的射线
这里是图片
首先我们可以排除一些一定不相交的情况:
1: 线段在射线上方
2: 线段在射线下方
3: 线段的两个端点都在待检测点 的左边
排除了以上3种一定不相交 的情况后,接下来会有一种一定相交 的情况,也就是线段的2个端点都在待检测点 的右边:
最后,还剩下了1种情况:线段的一个端点在待检测点 的左边,另一个端点在待检测点的右边,这个时候可能相交,也可能不相交:
-
不相交:
-
相交:
这种情况下,计算出射线所处的直线 与线段的交点的x坐标 ,然后判断这个交点的x坐标 是否大于待检测点的x坐标 ,如果是,则说明射线与线段相交了,反之则没有相交。 上图中,除了未知点X(P1P2和射线的交点),其他所有点的坐标,都是已知的,我们可以得到线段P2O的长度和线段P1O的长度的比值,等于线段P2Q的长度和线段XQ的长度的比值,
即: <math xmlns="http://www.w3.org/1998/Math/MathML"> P 2 O P 1 O = P 2 Q X Q \frac{P2O}{P1O}=\frac{P2Q}{XQ} </math>P1OP2O=XQP2Q
接下来我们就可以算出线段XQ的长度了,然后再用点P2的x坐标减去这个长度,就得到了未知点X的x坐标了,如果未知点X的x坐标大于待检测点P的x坐标,说明射线与线段相交了,否则,射线与线段没有相交。
4.4 代码实现
4.4.1 判断线段与射线是否相交(Polygon.isIntersect)
typescript
private isIntersect(
px: number,
py: number,
p1x: number,
p1y: number,
p2x: number,
p2y: number
) {
// 线段在射线上方
if (p1y > py && p2y > py) {
return false
}
// 线段在射线下方
if (p1y < py && p2y < py) {
return false
}
// 线段的两个端点都在待检测点的左边
if (p1x < px && p2x < px) {
return false
}
// 线段的2个端点都在待检测点的右边
if (p1x > px && p2x > px) {
return true
}
const p2o = p1y - p2y
const p1o = p2x - p1x
const p2q = py - p2y
const x = p2x - (p1o / p2o) * p2q
if (x > px) {
return true
} else {
return false
}
}
4.4.2 判断待检测点是否在一个多边形内部(Polygon.contains)
遍历多边形的每一条边,然后用isIntersect函数分别判断。
typescript
public contains(p: Point): boolean {
const len = this.points.length
let count = 0
// points数组的每两个元素为一个顶点的坐标
for (let i = 2; i <= len - 2; i += 2) {
const p1x = this.points[i - 2]
const p1y = this.points[i - 1]
const p2x = this.points[i]
const p2y = this.points[i + 1]
if (this.isIntersect(p.x, p.y, p1x, p1y, p2x, p2y)) {
count++
}
}
// 还需要判断最后一个点和第一个点的连线是否与射线相交
const p1x = this.points[0]
const p1y = this.points[1]
const p2x = this.points[len - 2]
const p2y = this.points[len - 1]
if (this.isIntersect(p.x, p.y, p1x, p1y, p2x, p2y)) {
count++
}
if (count % 2 === 0) {
return false
} else {
return true
}
}
pixijs的代码实现更简洁一些,这里贴一下pixijs的代码(Polygon.contains):
typescript
contains(x: number, y: number): boolean
{
let inside = false;
// use some raycasting to test hits
// https://github.com/substack/point-in-polygon/blob/master/index.js
const length = this.points.length / 2;
for (let i = 0, j = length - 1; i < length; j = i++)
{
const xi = this.points[i * 2];
const yi = this.points[(i * 2) + 1];
const xj = this.points[j * 2];
const yj = this.points[(j * 2) + 1];
const intersect = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * ((y - yi) / (yj - yi))) + xi);
if (intersect)
{
inside = !inside;
}
}
return inside;
}
至此,多边形的碰撞检测就实现了。
5. 补充所有类型的图形的碰撞检测方法
目前所有图形都继承自Shape这个抽象类,Shape要求每个子类都实现contains函数以作为碰撞检测的基础,在这一节将会实现所有图形的contains函数
5.1 圆Circle
只要待检测点离圆心的距离小于半径,就判断待检测点在该封闭图形的内部
5.1.1 代码实现
typescript
public contains(p: Point): boolean {
if (
(p.x - this.x) * (p.x - this.x) + (p.y - this.y) * (p.y - this.y) <
this.radius * this.radius
) {
return true
} else {
return false
}
}
5.2 矩形Rectangle
代码实现:
typescript
public contains(p: Point): boolean {
if (
p.x > this.x &&
p.x < this.x + this.width &&
p.y > this.y &&
p.y < this.y + this.height
) {
return true
} else {
return false
}
}
5.3 椭圆Ellipse
椭圆的方程是:
<math xmlns="http://www.w3.org/1998/Math/MathML"> x 2 a 2 + y 2 b 2 = 1 \frac{x^2}{a^2}+\frac{y^2}{b^2}=1 </math>a2x2+b2y2=1
只要 <math xmlns="http://www.w3.org/1998/Math/MathML"> x 2 a 2 + y 2 b 2 < 1 \frac{x^2}{a^2}+\frac{y^2}{b^2}<1 </math>a2x2+b2y2<1,我们就判断待检测点落在椭圆的内部
5.3.1 代码实现
typescript
public contains(p: Point): boolean {
if (
((p.x - this.x) * (p.x - this.x)) / (this.radiusX * this.radiusX) +
((p.y - this.y) * (p.y - this.y)) / (this.radiusY * this.radiusY) <
1
) {
return true
} else {
return false
}
}
5.4 圆角矩形RoundedRectangle
先判断是否落在这个大矩形里(红色区域),如果不在,说明待检测点在圆角矩形外部。
如果通过了第一轮检测,则进入下一轮检测,第二轮检测会判断待检测点是否落在4个圆角外(红色区域),如果是,说明待检测点在圆角矩形外部,反之则说明在内部。
5.4.1 代码
typescript
public contains(p: Point): boolean {
const con1 =
p.x > this.x &&
p.x < this.x + this.width &&
p.y > this.y &&
p.y < this.y + this.height
if (!con1) {
return false
}
// 判断左上角
const c1x = this.x + this.radius
const c1y = this.y + this.radius
if (p.x < c1x && p.y < c1y) {
if (
(p.x - c1x) * (p.x - c1x) + (p.y - c1y) * (p.y - c1y) <
this.radius * this.radius
) {
return true
} else {
return false
}
}
// 判断左下角
const c2x = this.x + this.radius
const c2y = this.y + this.height - this.radius
if (p.x < c2x && p.y > c2y) {
if (
(p.x - c2x) * (p.x - c2x) + (p.y - c2y) * (p.y - c2y) <
this.radius * this.radius
) {
return true
} else {
return false
}
}
// 判断右上角
const c3x = this.x + this.width - this.radius
const c3y = this.y + this.radius
if (p.x > c3x && p.y < c3y) {
if (
(p.x - c3x) * (p.x - c3x) + (p.y - c3y) * (p.y - c3y) <
this.radius * this.radius
) {
return true
} else {
return false
}
}
// 判断右下角
const c4x = this.x + this.width - this.radius
const c4y = this.y + this.height - this.radius
if (p.x > c4x && p.y < c4y) {
if (
(p.x - c4x) * (p.x - c4x) + (p.y - c4y) * (p.y - c4y) <
this.radius * this.radius
) {
return true
} else {
return false
}
}
return true
}
至此,所有图形的碰撞检测方法都有了
6. Graphics类的碰撞检测函数
前面讲述了Graphics类支持的所有的图形的碰撞检测函数,接下来就是补充Graphics类的碰撞检测函数了。原理比较简单,遍历Graphics类实例的所有子图形,如果碰撞到了其中一个,则相当于碰撞到了Graphics类实例。
6.1 Container.containsPoint函数
首先我们在Container类上挂载一个containsPoint函数,这个函数是用来判断某个点是否与当前类的实例产生了碰撞,Container类的子类也会实现这个函数。
typescript
public containsPoint(p: Point) {
return false
}
由于Container自身没有可以碰撞的内容,所以它直接返回false。
6.2 Graphics类的containsPoint函数
Graphics类的图形放在_geometry(GraphicsGeometry)属性里,所以我们先补全这个类的containsPoint函数。
GraphicsGeometry.containsPoint函数:
typescript
public containsPoint(p: Point): boolean {
for (let i = 0; i < this.graphicsData.length; i++) {
const { shape, fillStyle } = this.graphicsData[i]
if (!fillStyle.visible) {
continue
}
if (shape.contains(p)) {
return true
}
}
return false
}
Graphics.containsPoint函数:
typescript
public containsPoint(p: Point): boolean {
return this._geometry.containsPoint(p)
}
至此,Graphics类的碰撞检测逻辑已经完成。
7. 引入了层级关系的碰撞检测
前面一步步地描述了如何对不规则图形以及规则图形进行碰撞检测,接下来会引入层级关系,实现真正可用的碰撞检测。
7.1 原理
本系列的文章的第一篇中提到了:渲染引擎在拿到这棵带有层级关系的对象树(根节点为stage)后,会采用先序遍历
的方式来渲染这棵树,这意味着,父节点会比子节点先渲染,而相同层级的兄弟节点,则按照zIndex来排序,zIndex越大的兄弟节点越晚被渲染,zIndex相同的兄弟节点则按照数组中的顺序来渲染,这也是层级关系的核心所在。
对于碰撞检测,同样需要遍历这棵对象树,只不过遍历的顺序不一样了。
对于碰撞检测的遍历顺序,只有一条原则:谁处于层级关系的更高层,谁先被检测(这也是为什么我们能使用像素标记法来做碰撞检测)。这一点和渲染的顺序是反过来的。这其实也非常好理解,假设在桌子上放了一堆纸,这些纸形成了一个层级关系,然后在这堆纸所在的区域随机滴一滴墨水,这滴墨水肯定是滴在尽可能上层的那张纸上。
可以得到:我们会后序遍历这棵对象树,越晚被渲染出来的元素,越早进行碰撞检测。
7.2 代码实现
代码:
typescript
let hasFoundTarget = false
let hitTarget: Container | null = null
const hitTestRecursive = (curTarget: Container, globalPos: Point) => {
// 如果对象不可见
if (!curTarget.visible) {
return
}
if (hasFoundTarget) {
return
}
// 深度优先遍历子元素
for (let i = curTarget.children.length - 1; i >= 0; i--) {
const child = curTarget.children[i]
hitTestRecursive(child, globalPos)
}
if (hasFoundTarget) {
return
}
// 最后检测自身
const p = curTarget.worldTransform.applyInverse(globalPos)
if (curTarget.containsPoint(p)) {
hitTarget = curTarget
hasFoundTarget = true
}
}
const hitTest = (root: Container, globalPos: Point) => {
hasFoundTarget = false
hitTarget = null
hitTestRecursive(root, globalPos)
return hitTarget
}
8. 测试
8.1 实现一个cursor: pointer效果
测试方式:在canvas元素上监听pointermove事件,触发pointermove事件后拿到鼠标的坐标,然后调用hitTest函数进行碰撞检测,只要碰撞到了任何目标,都将canvas元素的鼠标指针改成cursor: pointer
。 测试代码:
typescript
// 在Application类的构造函数里执行这段逻辑
this.view.addEventListener('pointermove', (e) => {
const target = hitTest(this.stage, new Point(e.offsetX, e.offsetY))
if (target) {
this.view.style.cursor = 'pointer'
} else {
this.view.style.cursor = 'auto'
}
})
测试图形就用上一篇文章末尾的那个带有各种曲线的图形,代码如下:
typescript
const path = new Graphics()
.lineStyle(3, 'purple')
.beginFill('pink', 0.6)
.moveTo(100, 100)
.lineTo(300, 100)
.arc(300, 300, 200, Math.PI * 1.5, Math.PI * 2)
.bezierCurveTo(500, 400, 600, 500, 700, 500)
.lineTo(600, 300)
.arcTo(700, 100, 800, 300, 150)
.quadraticCurveTo(900, 100, 1100, 200)
.closePath()
app.stage.addChild(path)
看一下效果:
可以看到,鼠标移到了我们绘制的图形的上方后,鼠标指针就变成了cursor:pointer
效果,如果鼠标没有碰撞到任何元素,则回到了cursor:auto
效果。
8.2 实现一个点击效果
测试方式:在画布上绘制一些图形(Graphics类的实例),然后给这些图形监听click事件(on('click')
)(DisplayObject类继承自Eventemitter)。 在canvas元素上监听click事件,触发click事件后,拿到鼠标落点的坐标,然后调用hitTest函数进行碰撞检测,碰撞到了哪个图形,就执行哪个图形的click事件(emit('click')
)
在canvas元素上监听click事件:
typescript
this.view.addEventListener('click', (e) => {
const target = hitTest(this.stage, new Point(e.offsetX, e.offsetY))
if (target) {
target.emit('click')
}
})
在画布上绘制一些图形,然后给这些图形监听click事件:
typescript
const c = new Container()
const redRect = new Graphics()
.beginFill('red')
.drawRect(400, 300, 200, 200)
.on('click', () => {
message.success(<span style={{ color: 'red' }}>点击了红色的矩形</span>)
})
c.addChild(redRect)
const bluePoly = new Graphics()
.beginFill('blue', 0.7)
.moveTo(100, 200)
.lineTo(400, 0)
.lineTo(1000, 300)
.lineTo(900, 600)
.closePath()
.on('click', () => {
message.success(<span style={{ color: 'blue' }}>点击了蓝色的多边形</span>)
})
c.addChild(bluePoly)
const path = new Graphics()
.lineStyle(3, 'purple')
.beginFill('pink', 0.6)
.moveTo(100, 100)
.lineTo(300, 100)
.arc(300, 300, 200, Math.PI * 1.5, Math.PI * 2)
.bezierCurveTo(500, 400, 600, 500, 700, 500)
.lineTo(600, 300)
.arcTo(700, 100, 800, 300, 150)
.quadraticCurveTo(900, 100, 1100, 200)
.closePath()
.on('click', () => {
message.success(<span style={{ color: 'pink' }}>点击了粉色的多边形</span>)
})
const greenCircle = new Graphics()
.beginFill('green')
.drawCircle(200, 400, 200)
.on('click', () => {
message.success(<span style={{ color: 'green' }}>点击了绿色的圆</span>)
})
app.stage.addChild(c)
app.stage.addChild(path)
app.stage.addChild(greenCircle)
测试效果:
点击红色矩形:
点击蓝色多边形:
点击绿色圆:
点击粉色多边形:
可以看到,触发点击事件后,hitTest函数都能够在对象树中找到正确的对象。
9. 结语
本文讲述了拿到鼠标落点坐标后,如何在层级关系树中定位到碰撞的节点,有2种方法:像素标记法
和射线法
,像素标记法
利用了canvas的绘制能力,而射线法
基于奇偶环绕法则 和后序遍历对象树。两种方法各有利弊,像素标记法的实现简单,但是性能比较低;射线法实现起来更困难一些,但是性能高上许多,大家可以根据实际情况来选择自己的方案。
本文的内容到此就结束了,下一篇文章将会基于本文的内容来讲述事件系统的实现。
谢谢观看🙏,如果觉得本文还不错,就点个赞吧👍。