目录
1.简介
最近在研究四叉树的二维空间碰撞检测,随笔记录一下
四叉树顾名思义,就是每科树都包含4个分支,每个分支可以看做一个区域象限,用来存放一些数据的空间索引
这里只讨论四叉树在二维空间的碰撞检测,Unity的实现方式
2.树的结构
// 区域象限定义
public enum QuadrantType
{
/// <summary>
/// 左上
/// </summary>
LT = 0,
/// <summary>
/// 右上
/// </summary>
RT = 1,
/// <summary>
/// 右下
/// </summary>
RB = 2,
/// <summary>
/// 左下
/// </summary>
LB = 3,
}
public enum QuadrantBitType
{
/// <summary>
/// 左上
/// </summary>
LT = 1 << 0,
/// <summary>
/// 右上
/// </summary>
RT = 1 << 1,
/// <summary>
/// 右下
/// </summary>
RB = 1 << 2,
/// <summary>
/// 左下
/// </summary>
LB = 1 << 3,
}
public interface IRect
{
/// <summary>
/// 矩形的中心坐标x
/// </summary>
float x { get; set; }
/// <summary>
/// 矩形的中心坐标y
/// </summary>
float y { get; set; }
/// <summary>
/// 矩形的宽
/// </summary>
float width { get; set; }
/// <summary>
/// 矩形的高
/// </summary>
float height { get; set; }
}
public interface IMark
{
/// <summary>
/// 对象待比较标记(1:待比较)
/// </summary>
int mark { get; set; }
}
public class QTreeComparer<T> : IComparer<QTree<T>> where T : IRect
{
public int Compare(QTree<T> x, QTree<T> y)
{
if (x.depth > y.depth)
return 1;
else if (x.depth == y.depth)
return 0;
else
return -1;
}
}
// 四叉树结构
public class QTree<T> : IRect where T : IRect
{
public float x { get; set; }
public float y { get; set; }
public float width { get; set; }
public float height { get; set; }
public int depth; // 树的深度
public int childCount; // 对象数量
public bool isLeaf; // 是否叶子节点
public List<T> childList; // 对象引用
public QTree<T>[] childNodes; // 子节点数组(4个)
public QTree()
{
Init();
}
public QTree(int depth)
{
Init();
this.depth = depth;
}
private void Init()
{
childList = new List<T>();
isLeaf = true;
childCount = 0;
}
public void InitRect(float x, float y, float width, float height)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public void Clear()
{
if (isLeaf)
{
childList.Clear();
childList = null;
}
else
{
for (int i = 0; i < childNodes.Length; ++i)
{
childNodes[i].Clear();
childNodes[i] = null;
}
childNodes = null;
}
}
}
3.构造原则
1.默认构造的QTree树节点都是叶子节点
2.对象只会放入叶子节点
3.每个叶子节点可以存放的对象上限是 MAXCHILDCOUNT,下同(在深度没有达到最大值的情况下,否则不限)
4.当叶子节点存放的对象数量超过 MAXCHILDCOUNT,则分离生成子树(4个区域象限),并把自身存储的所有对象存放到子树中,然后标记为非叶子节点,清除自身所有保存的对象引用
5.根据对象的坐标及宽高插入QTree树中(边界对象可能会加入多个QTree树中)
4.重要接口
/// <summary>
/// 插入四叉树
/// </summary>
public void InsertQTree<T>(QTree<T> node, T t) where T : IRect, IMark
{
if (node.isLeaf)
{
if (node.depth < maxDepth && node.childCount + 1 > maxChildCount)
{
// 分裂树(象限)
SplitQTree(node);
InsertQTree(node, t);
}
else
{
node.childList.Add(t);
node.childCount++;
}
}
else
{
int indexs = GetTargetQuadrantIndex<T>(node, t);
if (indexs > 0)
{
int indexArea = indexs & (int)QuadrantBitType.LT;
if (indexArea == (int)QuadrantBitType.LT)
InsertQTree<T>(node.childNodes[(int)QuadrantType.LT], t);
indexArea = indexs & (int)QuadrantBitType.RT;
if (indexArea == (int)QuadrantBitType.RT)
InsertQTree<T>(node.childNodes[(int)QuadrantType.RT], t);
indexArea = indexs & (int)QuadrantBitType.RB;
if (indexArea == (int)QuadrantBitType.RB)
InsertQTree<T>(node.childNodes[(int)QuadrantType.RB], t);
indexArea = indexs & (int)QuadrantBitType.LB;
if (indexArea == (int)QuadrantBitType.LB)
InsertQTree<T>(node.childNodes[(int)QuadrantType.LB], t);
}
}
}
/// <summary>
/// 分离树(象限)
/// </summary>
private void SplitQTree<T>(QTree<T> node) where T : IRect, IMark
{
node.isLeaf = false;
float width = node.width * 0.5f;
float height = node.height * 0.5f;
node.childNodes = new QTree<T>[4];
// 左上
node.childNodes[(int)QuadrantType.LT] = CreateChildNode<T>(node.x - width * 0.5f, node.y + height * 0.5f, width, height, node.depth + 1);
// 右上
node.childNodes[(int)QuadrantType.RT] = CreateChildNode<T>(node.x + width * 0.5f, node.y + height * 0.5f, width, height, node.depth + 1);
// 右下
node.childNodes[(int)QuadrantType.RB] = CreateChildNode<T>(node.x + width * 0.5f, node.y - height * 0.5f, width, height, node.depth + 1);
// 左下
node.childNodes[(int)QuadrantType.LB] = CreateChildNode<T>(node.x - width * 0.5f, node.y - height * 0.5f, width, height, node.depth + 1);
for(int i = node.childCount - 1; i >= 0 ; --i)
{
InsertQTree<T>(node, node.childList[i]);
node.childList.RemoveAt(i);
node.childCount--;
}
}
/// <summary>
/// 创建子树(叶子节点)
/// </summary>
private QTree<T> CreateChildNode<T>(float x, float y, float width, float height, int depth) where T : IRect, IMark
{
QTree<T> node = new QTree<T>();
node.depth = depth;
node.InitRect(x, y, width, height);
return node;
}
/// <summary>
/// 查询树,并返回包含所有树节点的列表
/// </summary>
public void QueryQTreeRetrunList<T>(QTree<T> node, ref List<QTree<T>> qTreeList) where T : IRect, IMark
{
qTreeList.Add(node);
if (!node.isLeaf)
{
for(int i = (int)QuadrantType.LT ; i <= (int)QuadrantType.LB ; ++i)
{
QueryQTreeRetrunList<T>(node.childNodes[i], ref qTreeList);
}
}
}
/// <summary>
/// 查询树,并返回包含所有树节点的列表(按depth升序排列)
/// </summary>
public void QueryQTreeReturnRiseList<T>(QTree<T> node, ref List<QTree<T>> qTreeList) where T : IRect, IMark
{
QueryQTreeRetrunList<T>(node, ref qTreeList);
qTreeList.Sort(new QTreeComparer<T>());
}
/// <summary>
/// 获取目标所在的象限索引列表
/// </summary>
public int GetTargetQuadrantIndex<T>(QTree<T> node, T target) where T : IRect, IMark
{
float halfWidth = node.width * 0.5f;
float halfHeight = node.height * 0.5f;
float min_x = target.x - target.width * 0.5f;
float min_y = target.y - target.height * 0.5f;
float max_x = target.x + target.width * 0.5f;
float max_y = target.y + target.height * 0.5f;
// 不在当前节点范围内返回null
if (min_x > node.x + halfWidth || max_x < node.x - halfWidth
|| min_y > node.y + halfHeight || max_y < node.y - halfHeight)
return 0;
int indexs = 0;
bool isLeft = min_x <= node.x ? true : false;
bool isRight = max_x > node.x ? true : false;
bool isBottom = min_y <= node.y ? true : false;
bool isTop = max_y > node.y ? true : false;
if (isLeft)
{
// 左上
if (isTop)
indexs = indexs | (int)QuadrantBitType.LT;
// 左下
if (isBottom)
indexs = indexs | (int)QuadrantBitType.LB;
}
if (isRight)
{
// 右上
if (isTop)
indexs = indexs | (int)QuadrantBitType.RT;
// 右下
if (isBottom)
indexs = indexs | (int)QuadrantBitType.RB;
}
return indexs;
}
/// <summary>
/// 返回目标周围的可能碰撞对象列表
/// </summary>
public void FindTargetAroundObjs<T>(QTree<T> node, T target) where T : IRect, IMark
{
if (node.isLeaf)
{
for (int i = 0; i < node.childList.Count; i++)
{
node.childList[i].mark = 1;
}
}
else
{
int indexs = GetTargetQuadrantIndex<T>(node, target);
if (indexs > 0)
{
int indexArea = indexs & (int)QuadrantBitType.LT;
if (indexArea == (int)QuadrantBitType.LT)
FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.LT], target);
indexArea = indexs & (int)QuadrantBitType.RT;
if (indexArea == (int)QuadrantBitType.RT)
FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.RT], target);
indexArea = indexs & (int)QuadrantBitType.RB;
if (indexArea == (int)QuadrantBitType.RB)
FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.RB], target);
indexArea = indexs & (int)QuadrantBitType.LB;
if (indexArea == (int)QuadrantBitType.LB)
FindTargetAroundObjs<T>(node.childNodes[(int)QuadrantType.LB], target);
}
}
}
5.示例
如上图:黄色方块为跟随鼠标移动的目标对象,绿色方块为待与黄色方块做碰撞计算的对象,红色方块为已发生碰撞的对象
6.压测
下图为最大深度4、根节点深度1,10000个对象的插入、查询测试:
如上图:10000个对象的四叉树插入平均耗时14ms,查询计算对象700多个大概5ms,300多个大概1.5ms
7.更新
优化了如下两个接口,移除了List造成的GC
1.优化了GetTargetQuadrantIndex接口,移除了原有返回的List<int>,改为int,位枚举保存
2.新增了IMark接口:用来标记待比较对象,FindTargetAroundObjs后,在调用段通过for循环找出所有的待比较对象
调用段示例:
public class Element : IRect, IMark
{
public int id { get; set; }
public ColorType color { get; set; }
public bool isMoving { get; private set; }
public float from_x { get; private set; }
public float from_y { get; private set; }
public float to_x { get; private set; }
public float to_y { get; private set; }
public float duration { get; private set; }
public float x { get; set; }
public float y { get; set; }
public float width { get; set; }
public float height { get; set; }
public int mark { get; set; }
private float factorSpeed;
private float factor;
public void Init(int id, float x, float y, float width, float height)
{
this.id = id;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = 0;
}
public void Move(float from_x, float from_y, float to_x, float to_y, float duration)
{
if (duration <= 0)
return;
//Debug.Log($"id = {id}, duration = {duration}, from_x = {from_x}, from_y = {from_y}, to_x = {to_x}, to_y = {to_y}");
this.from_x = from_x;
this.from_y = from_y;
this.to_x = to_x;
this.to_y = to_y;
this.duration = duration;
isMoving = true;
factorSpeed = 1f / duration;
factor = 0;
x = from_x;
y = from_y;
}
public void Update(float deltaTime)
{
factor += deltaTime * factorSpeed;
if (factor >= 1f)
{
isMoving = false;
x = to_x;
y = to_y;
}
else
{
x = to_x * factor + from_x * (1f - factor);
y = to_y * factor + from_y * (1f - factor);
}
}
}
private void FindTargetAroundObjs()
{
QTreeManager.Insatnce.FindTargetAroundObjs<Element>(root, mouseUIElement.M_Element);
List<Element> objs = new List<Element>();
Element element = null;
for (int i = 0 ; i < mElementList.Count ; i++)
{
if (mElementList[i].mark == 1)
{
element = mElementList[i];
objs.Add(element);
}
}
if (objs != null && objs.Count > 0)
{
for (int i = 0; i < mElementList.Count; ++i)
{
mElementList[i].color = mElementList[i].mark == 1 ? ColorType.Green : ColorType.Default;
mElementList[i].mark = 0;
}
CalculateCollision(mouseUIElement.M_Element, objs);
for (int i = 0; i < mUIElementList.Count; ++i)
{
mUIElementList[i].RefreshColor();
}
}
else
ResetElementColor();
mouseUIElement.SetVisable(CheckoutIsCollision(mouseUIElement.M_Element, rootElement));
}
详见源码: