最近在写2维空间的碰撞检测功能,遇到了检测性能消耗过大的问题,随后研究解决方案,学习四叉树原理并实践解决问题。
关于四叉树
也被称为四向划分树,是一种常见的空间数据结构,用于划分二维空间。它将空间递归地划分为四个象限,每个象限又可以继续划分为四个象限,以此类推,直到达到某个停止条件。
四叉树特点:
- 树的根节点表示整个空间,在根节点下划分为四个象限(通常是左上、右上、左下和右下)。
- 每个节点可以是四个子节点之一,或者是叶节点。
- 叶节点存储实际的数据(如点、线、面等)。
- 父节点和子节点之间的关系是通过位置和大小来确定的。
- 四叉树的应用非常广泛,特别是在计算机图形学、地理信息系统(GIS)和碰撞检测等领域。它可以有效地处理问题,如空间查询、区域划分、近邻搜索等。
四叉树优点:
- 空间效率高:四叉树可以动态地适应数据的分布,只划分需要划分的区域,避免了不必要的空间占用。
- 查询效率高:四叉树可以通过减少搜索区域来加速查询操作,特别是对于大规模的数据集。
- 简单易懂:四叉树的基本原理和操作相对简单,易于理解和实现。
四叉树缺点:
- 数据分布不均匀时可能导致树的深度增加,增加查询的时间复杂度。
- 插入和删除操作可能导致树的不平衡,需要进行平衡操作来维护树的性能。
- 对于高维数据,四叉树的效率可能会降低,因为树的深度会增加。
个人理解
- 四叉树主要是对于2d的容器 进行查找一个元素,快速定位当前的位置以及附近的元素
- 大大的减少了2维空间内 需要查询2个物体是否碰撞的循环条件
- 通过进行 四块 四块的分割 快速查询 当前物体 是在那个 小四块区域内 与相邻的 元素
- 没有在其他的区块中 就不进行 查询
- 为什么叫四叉树呢 和二叉树一样 为了高效快速的查找到目标
- 可以 一对多 也可以 多对多
源码:
(function() {
function Quadtree(bounds, max_objects = 10, max_levels = 4, level = 0) {
this.max_objects = max_objects;
this.max_levels = max_levels;
this.level = level;
this.bounds = bounds;
this.objects = [];
this.nodes = [];
}
Quadtree.prototype.split = function() {
const nextLevel = this.level + 1;
const { x, y, width, height } = this.bounds;
const subWidth = width / 2;
const subHeight = height / 2;
this.nodes[0] = new Quadtree({ x: x + subWidth, y, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
this.nodes[1] = new Quadtree({ x, y, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
this.nodes[2] = new Quadtree({ x, y: y + subHeight, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
this.nodes[3] = new Quadtree({ x: x + subWidth, y: y + subHeight, width: subWidth, height: subHeight }, this.max_objects, this.max_levels, nextLevel);
};
Quadtree.prototype.getIndex = function(pRect) {
const indexes = [];
const verticalMidpoint = this.bounds.x + this.bounds.width / 2;
const horizontalMidpoint = this.bounds.y + this.bounds.height / 2;
const startIsNorth = pRect.y < horizontalMidpoint;
const startIsWest = pRect.x < verticalMidpoint;
const endIsEast = pRect.x + pRect.width > verticalMidpoint;
const endIsSouth = pRect.y + pRect.height > horizontalMidpoint;
if (startIsNorth) {
if (endIsEast) indexes.push(0);
if (startIsWest) indexes.push(1);
} else if (endIsSouth) {
if (startIsWest) indexes.push(2);
if (endIsEast) indexes.push(3);
}
return indexes;
};
Quadtree.prototype.insert = function(pRect) {
if (this.nodes.length) {
const indexes = this.getIndex(pRect);
for (let i = 0; i < indexes.length; i++) {
this.nodes[indexes[i]].insert(pRect);
}
return;
}
this.objects.push(pRect);
if (this.objects.length > this.max_objects && this.level < this.max_levels) {
if (!this.nodes.length) this.split();
for (let i = 0; i < this.objects.length; i++) {
const indexes = this.getIndex(this.objects[i]);
for (let k = 0; k < indexes.length; k++) {
this.nodes[indexes[k]].insert(this.objects[i]);
}
}
this.objects.length = 0; // 清空数组但保持引用
}
};
Quadtree.prototype.retrieve = function(pRect) {
const indexes = this.getIndex(pRect);
const returnObjects = []; // 新建数组用于存储结果
const seenObjects = new Set(); // 使用 Set 去重
// 如果有子节点,检索其对象
if (this.nodes.length) {
for (let i = 0; i < indexes.length; i++) {
const childObjects = this.nodes[indexes[i]].retrieve(pRect);
for (let j = 0; j < childObjects.length; j++) {
if (!seenObjects.has(childObjects[j])) { // 检查是否已存在
returnObjects.push(childObjects[j]);
seenObjects.add(childObjects[j]); // 添加到 Set
}
}
}
}
// 添加当前节点的对象,去重
for (let i = 0; i < this.objects.length; i++) {
if (!seenObjects.has(this.objects[i])) {
returnObjects.push(this.objects[i]);
}
}
return returnObjects;
};
Quadtree.prototype.clear = function() {
this.objects.length = 0; // 清空数组但保持引用
for (let i = 0; i < this.nodes.length; i++) {
this.nodes[i].clear();
}
this.nodes.length = 0; // 清空节点
};
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = Quadtree;
} else {
window.Quadtree = Quadtree;
}
})();
相关属性解释:
- max_objects: 每个节点可以包含的最大对象数。当节点中的对象数超过此值时,节点将分裂。
- max_levels: 树的最大深度。当达到此深度时,即使节点中的对象数超过 max_objects,也不会再分裂。
- level: 当前节点的深度(从0开始)。
- bounds: 当前节点所代表的矩形区域(包括x, y坐标、宽度和高度)。
- objects: 存储在当前节点中的对象数组。
- nodes: 存储当前节点的四个子节点的数组(如果节点已分裂)
- split(): 将当前节点分裂成四个子节点,每个子节点代表当前节点区域的一个四分之一。
- getIndex(pRect): 根据给定的矩形(pRect)的位置和大小,确定它应该被插入到哪个子节点中(如果有的话)。由于一个矩形可能跨越多个子区域,因此此方法返回一个索引数组,而不是单个索引。
- insert(pRect): 将给定的矩形(pRect)插入到四叉树中。如果节点已分裂,则根据 getIndex 方法的结果将矩形插入到相应的子节点中。如果节点中的对象数超过了 max_objects 并且未达到 max_levels,则节点将分裂并重新分配对象。
- retrieve(pRect): 检索与给定矩形(pRect)相交的所有对象。这包括当前节点中的对象以及所有相关子节点中的对象。使用 Set 来确保结果中不包含重复的对象。
- clear(): 清空四叉树中的所有对象和子节点。
使用方法
先定义一个四叉树实例
var myTree = new Quadtree({
x: 0,
y: 0,
width: 1920,
height: 1080
}, 4);
如果插入元素 也需要包括x、y、width、height属性
myTree.insert({
x: 200,
y: 300,
width: 100,
height: 300,
})
查询 你想要检测元素,是否和其他元素碰撞
const ele={
x: 100,
y: 300,
width: 300,
height: 400,
}
var candidates = myTree.retrieve();
返回一个ele附近的的元素的集合
之后在candidates数组中元素 进行判断 是否相交
大大的减少了 需要判断的计算量 数据越多能体现