- 0.1:关于四叉树
- 0.2:一、使用场景
- 0.3:二、实现四叉树
- 0.4:三、根据使用场景测试
- 0.5:四、使用情景和分析
关于四叉树
定义和作用
四叉树(Quad Tree)是一种空间索引树,四叉树的每一个节点都代表着一块矩形区域。我们知道在平面直角坐标系中,平面可以被分为第一二三四象限,四叉树的每一个节点也类似,可以分裂为四个子节点,子节点在满足条件的情况下可以继续分裂,这样构成了一个四元的树状结构,就是四叉树。
四叉树的作用
通常使用树结构能够带来高效且简单的检索效果,四叉树也不例外,四叉树主要用于二维空间的检索(相应的,三维空间可以了解下八叉树,理论上来讲,对于N维空间的检索可以根据这个原理构成2^N叉树,此外多维空间的检索还有kd树)。
四叉树的操作
1、节点分裂
当满足特定条件时,为了获得更好的检索效率,四叉树的节点会发生分裂,分裂的做法是:以当前节点的矩形为中心, 划分出四个等大的矩形,作为四个子节点,每个节点深度=当前节点深度+1,根节点深度为0;并且将当前节点的元素重新插入到对应的子节点。
2、插入元素
1)平面的元素多种多样,点,线,图形,但都能够做一个统一,第一步都是要找出图形所覆盖的节点,这里以矩形为例
2)从根节点开始,如果当前节点无子节点,将元素插入到当前节点。如果存在子节点K,并且元素能够完全被子节K点所“包围”,就将元素插入到子节点K,对于子节点K进行上述递归操作;如果元素跨越了多个子节点,那就将元素存储在当前节点
3)如果某一节点元素超过特定值,并且深度小于四叉树允许的最大深度,分裂当前节点。
3、检索
1)对于给定检索区域,从根节点起,如果检索区域与当前节点有交集,返回当前节点的所有元素。
2)如果当前节点还有子节点,递归检索与检索区域有交集的子节点。
一、使用场景
我们有一个很大的场景,场景有许许多多的敌人。红色的点代表是玩家,黑色的点代表是敌人。在这样的一个大量敌人的情景下,我们不可能在玩家或敌人寻找身边的攻击对象时穷尽所有的对象。因为我们要建立空间分区,只遍历某个对应区的对象。在图下中,红点中遍历红框中的黑点对象,其他一律不遍历,从而节约性能。
二、实现四叉树
在四叉树中我们只在叶子节点存放数据
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 叶子节点类,负责坐标与数据的映射(T一般是gameobject即可) /// </summary> /// <typeparam name="T"></typeparam> public class QuadTreeLeaf<T> { private Vector2 pos; private T refObject; public QuadTreeLeaf(Vector2 pos, T obj) { this.pos = pos; refObject = obj; } public T LeafObject { get { return refObject; } } public Vector2 Pos { get { return pos; } set { pos = value; } } }
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 四叉树节点 /// </summary> /// <typeparam name="T"></typeparam> public class QuadTreeNode<T> { /// <summary> /// 节点拥有的叶子节点 /// </summary> protected List<QuadTreeLeaf<T>> items; /// <summary> /// 节点拥有的分支 /// </summary> protected QuadTreeNode<T>[] branch; /// <summary> /// 节点空间最大容量,受minSize影响 /// </summary> protected int maxItems; /// <summary> /// 节点空间分割的最小大小(最小宽度,高度) /// </summary> protected float minSize; /// <summary> /// 节点的空间 /// </summary> public Rect bounds; public QuadTreeNode(float x, float y, float width, float height, int maximumItems, float minSize = -1) { bounds = new Rect(x, y, width, height); maxItems = maximumItems; this.minSize = minSize; items = new List<QuadTreeLeaf<T>>(); } public bool HasChildren() { if (branch != null) return true; else return false; } /// <summary> /// 将节点空间分割4份 /// </summary> protected void Split() { if (minSize != -1) { if (bounds.width <= minSize && bounds.height <= minSize) { return; } } float nsHalf = bounds.height - bounds.height / 2; float ewHalf = bounds.width - bounds.width / 2; branch = new QuadTreeNode<T>[4]; branch[0] = new QuadTreeNode<T>(bounds.x, bounds.y, ewHalf, nsHalf, maxItems, minSize); branch[1] = new QuadTreeNode<T>(bounds.x + ewHalf, bounds.y, ewHalf, nsHalf, maxItems, minSize); branch[2] = new QuadTreeNode<T>(bounds.x, bounds.y + nsHalf, ewHalf, nsHalf, maxItems, minSize); branch[3] = new QuadTreeNode<T>(bounds.x + ewHalf, bounds.y + nsHalf, ewHalf, nsHalf, maxItems, minSize); foreach (var item in items) { AddNode(item); } items.Clear(); } /// <summary> /// 根据坐标获得相应的子空间 /// (返回包含该坐标的子节点) /// </summary> /// <param name="pos"></param> /// <returns></returns> protected QuadTreeNode<T> GetChild(Vector2 pos) { if (bounds.Contains(pos)) { if (branch != null) { for (int i = 0; i < branch.Length; i++) if (branch[i].bounds.Contains(pos)) return branch[i].GetChild(pos); } else return this; } return null; } /// <summary> /// 增加叶子节点数据 /// </summary> /// <param name="leaf"></param> /// <returns></returns> private bool AddNode(QuadTreeLeaf<T> leaf) { if (branch == null) { this.items.Add(leaf); if (this.items.Count > maxItems) Split(); return true; } else { QuadTreeNode<T> node = GetChild(leaf.Pos); if (node != null) { return node.AddNode(leaf); } } return false; } public bool AddNode(Vector2 pos, T obj) { return AddNode(new QuadTreeLeaf<T>(pos, obj)); } /// <summary> /// /// </summary> /// <param name="pos">可以是空间任意位置,只是根据这个位置找到所在的空间去删除对象</param> /// <param name="obj"></param> /// <returns></returns> public bool RemoveNode(Vector2 pos, T obj) { if (branch == null) { for (int i = 0; i < items.Count; i++) { QuadTreeLeaf<T> qtl = items[i]; if (qtl.LeafObject.Equals(obj)) { items.RemoveAt(i); return true; } } } else { QuadTreeNode<T> node = GetChild(pos); if (node != null) { return node.RemoveNode(pos, obj); } } return false; } public bool UpdateNode(Vector2 pos, T obj) { // TODO 参找RemoveNode return false; } /// <summary> /// 得到在一个Rect内的所有数据(根据两个rect是否有交集) /// </summary> /// <param name="rect"></param> /// <param name="nodes"></param> /// <returns></returns> public int GetNode(Rect rect, ref List<T> nodes) { if (branch == null) { foreach (QuadTreeLeaf<T> item in items) { if (rect.Contains(item.Pos)) { nodes.Add(item.LeafObject); } } } else { for (int i = 0; i < branch.Length; i++) { if (branch[i].bounds.Overlaps(rect)) branch[i].GetNode(rect, ref nodes); } } return nodes.Count; } /// <summary> /// 根据坐标得到坐标附近节点的数据(圆心+半径) /// </summary> /// <param name="pos"></param> /// <param name="ShortestDistance">离坐标最短距离</param> /// <param name="list"></param> /// <returns></returns> public int GetNodeRecRange(Vector2 pos, float ShortestDistance, ref List<T> list) { float distance; if (branch == null) { foreach (QuadTreeLeaf<T> leaf in this.items) { distance = Vector2.Distance(pos, leaf.Pos); if (distance < ShortestDistance) { list.Add(leaf.LeafObject); } } } else { for (int i = 0; i < branch.Length; i++) { float childDistance = branch[i].bounds.PointToBorderDistance(pos); if (childDistance < ShortestDistance * ShortestDistance) { branch[i].GetNodeRecRange(pos, ShortestDistance, ref list); } } } return list.Count; } public void DrawBounds() { if(HasChildren()) { foreach(var i in branch) { i.DrawBounds(); } } else { Gizmos.color = Color.black; Gizmos.DrawWireCube(bounds.center, bounds.size); } } }
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// Rect的扩展方法类 /// </summary> public static class RectExtension { /// <summary> /// 计算点到Rect的border的距离,若点在Rect内则返回0 /// </summary> /// <param name="rect"></param> /// <param name="pos"></param> /// <returns></returns> public static float PointToBorderDistance(this Rect rect, Vector2 pos) { float xdisance; float ydisance; if (rect.x <= pos.x && pos.x <= rect.xMax) { xdisance = 0; } else { xdisance = Mathf.Min(Mathf.Abs(pos.x - rect.xMax), Mathf.Abs(pos.x - rect.x)); } if (rect.y <= pos.y && pos.y <= rect.yMax) { ydisance = 0; } else { ydisance = Mathf.Min(Mathf.Abs(pos.y - rect.yMax), Mathf.Abs(pos.y - rect.y)); } return xdisance * xdisance + ydisance * ydisance; } }
三、根据使用场景测试
1.构建一棵树。
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 构建四叉树 /// 挂在任意物体上,也可以在其他代码中实现初始化 /// </summary> public class TreeManager : MonoBehaviour { public static QuadTreeNode<GameObject> quadRoot = new QuadTreeNode<GameObject>(-5, -5, 10, 10, 2,3); }
2.给每个需要加入树的物体挂载该脚本。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class QuadTreeObject : MonoBehaviour { void Start() { //向树中插入该物体 TreeManager.quadRoot.AddNode(new Vector2(transform.position.x, transform.position.z), this.gameObject); } }
3.Player脚本,使用查找圆心加半径的方式找到区域内的物体。
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 查找距离Player指定范围内的物体 /// </summary> public class Player : MonoBehaviour { [Range(1,5)] public float detectRange = 1f; private List<GameObject> detectedGameObjects = new List<GameObject>(); [Header(header: "材质")] [SerializeField] private Material outMaterial; [SerializeField] private Material inMaterial; private Vector3 oldPos; private bool hasMoved = false; private void Start() { oldPos = transform.position; } private void Update() { hasMoved = oldPos != transform.position; oldPos = transform.position; //有位置变化再进行查询 if(hasMoved) { foreach (var i in detectedGameObjects) { i.GetComponent<Renderer>().material = outMaterial; } detectedGameObjects.Clear(); TreeManager.quadRoot.GetNodeRecRange(new Vector2(transform.position.x, transform.position.z), detectRange, ref detectedGameObjects); foreach (var i in detectedGameObjects) { i.GetComponent<Renderer>().material = inMaterial; } } } private void OnDrawGizmos() { if(Application.isPlaying) { Gizmos.color = Color.black; //在QuadTreeNode中使用的是vector2,绘制时在xy平面所以需要旋转90度。 Gizmos.matrix = Matrix4x4.TRS(Vector3.zero, transform.rotation, Vector3.one); TreeManager.quadRoot.DrawBounds(); Gizmos.color = Color.red; Gizmos.matrix = Matrix4x4.identity; Gizmos.DrawWireSphere(transform.position, detectRange); } } }
四、使用情景和分析
如果在相同位置有多个物体存在,一定要设置好Node的最小尺寸,否则会无限分割。
目前只是实现了数据的插入、删除和查询,还有物体移动后树的更新没有实现,如果一个物体横跨两个节点的情况也没有处理。
适用的情况是物体数量很大(至少要超过100个,否则暴力遍历就可以了)。
物体分布均匀的情况下,我们可以采用另外一种方式会更有效,那就是对区域进行网格划分,用二维数组的下标代表区域编号。
如果遇到要划分的区域不规则,我们也可以使用多个树进行拟合。