一、 OctreeNode
1. 基本概念
八叉树[1]的直观理解还算简单,我们有一个Box,把这个Box八等分,那我们就有了八个Box。如果我们再把这八个再划分一下,那就能得到64个Box。

递归划分,形成了一个"树"状结构
只不过,我们并不会对每个子节点再进行划分,满足"一定条件"的子节点,才会被再次划分。
cs
public class OctreeNode
{
//空间内包含的物体
public List<GameObject> areaObjects;
//空间中心
public Vector3 center;
//空间尺寸
public float size;
private const int kidCount = 8;
private OctreeNode[] kids;
public OctreeNode(Vector3 center, float size)
{
kids = new OctreeNode[kidCount];
this.center = center;
this.size = size;
areaObjects = new List<GameObject>();
}
public OctreeNode top0
{
get
{
return kids[0];
}
set
{
kids[0] = value;
}
}
public OctreeNode top1
{
get
{
return kids[1];
}
set
{
kids[1] = value;
}
}
public OctreeNode top2
{
get
{
return kids[2];
}
set
{
kids[2] = value;
}
}
public OctreeNode top3
{
get
{
return kids[3];
}
set
{
kids[3] = value;
}
}
public OctreeNode bottom0
{
get
{
return kids[4];
}
set
{
kids[4] = value;
}
}
public OctreeNode bottom1
{
get
{
return kids[5];
}
set
{
kids[5] = value;
}
}
public OctreeNode bottom2
{
get
{
return kids[6];
}
set
{
kids[6] = value;
}
}
public OctreeNode bottom3
{
get
{
return kids[7];
}
set
{
kids[7] = value;
}
}
//获取当前空间内记录的物体数量
public int objectCount => areaObjects.Count;
//unity gizmos可视化代码
public void DrawGizmos()
{
Gizmos.DrawWireCube(center, Vector3.one * size);
}
//判断空间是否包含某个点
public bool Contains(Vector3 position)
{
var halfSize = size * 0.5f;
return Mathf.Abs(position.x - center.x) <= halfSize &&
Mathf.Abs(position.y - center.y) <= halfSize &&
Mathf.Abs(position.z - center.z) <= halfSize;
}
//清理当前空间内物体
public void ClearArea()
{
areaObjects.Clear();
}
//记录物体
public void AddGameobject(GameObject obj)
{
areaObjects.Add(obj);
}
}
二、 Octree Manager
1. 场景物体的生成,管理
cs
public class TutorialOctree : MonoBehaviour
{
//生成物体数量
[Range(0, 500)]
public int genCount = 100;
//Octree 构建最大深度
[Range(1, 8)]
public int buildDepth = 3;
//Octree 的根节点
public OctreeNode root;
//物体生成范围
[Range(1, 300)]
public float range = 100;
//记录生成的场景物体
private List<GameObject> sceneObjects;
private void Start()
{
GenSceneObjects();
OctreePartion();
}
private void GenSceneObjects()
{
var genRange = range * 0.5f;
sceneObjects = new List<GameObject>();
for (int i = 0; i < genCount; i++)
{
//CreatePrimitive 是 Unity 的 API,用于创建基础几何体
//PrimitiveType.Cube 表示创建一个立方体
var obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
//Random.Range(min, max) 返回 [min, max] 区间的随机值(float版本包含最大值)
obj.transform.position = new Vector3(Random.Range(-genRange, genRange),
Random.Range(-genRange, genRange),
Random.Range(-genRange, genRange));
//hideFlags 是 Unity 对象的隐藏标志
//HideFlags.HideInHierarchy 表示在 Hierarchy 窗口(层级视图)中不显示该物体
obj.hideFlags = HideFlags.HideInHierarchy;
sceneObjects.Add(obj);
}
}
private void OctreePartion()
{
var initialOrigin = Vector3.zero;
root = new OctreeNode(initialOrigin, range);
root.areaObjects = sceneObjects;
GenerateOctree(root, range, buildDepth);
}
private void GenerateOctree(OctreeNode root, float range, float depth)
{
if (depth <= 0) return;
//计算grid的中心、尺寸
var halfRange = range / 2.0f;
var rootOffset = halfRange / 2.0f;
var rootCenter = root.center;
//1. 创建8个子节点
var origin = rootCenter + new Vector3(-1, 1, -1) * rootOffset;
root.top0 = new OctreeNode(origin, halfRange);
origin = rootCenter + new Vector3(1, 1, -1) * rootOffset;
root.top1 = new OctreeNode(origin, halfRange);
origin = rootCenter + new Vector3(1, 1, 1) * rootOffset;
root.top2 = new OctreeNode(origin, halfRange);
origin = rootCenter + new Vector3(-1, 1, 1) * rootOffset;
root.top3 = new OctreeNode(origin, halfRange);
origin = rootCenter + new Vector3(-1, -1, -1) * rootOffset;
root.bottom0 = new OctreeNode(origin, halfRange);
origin = rootCenter + new Vector3(1, -1, -1) * rootOffset;
root.bottom1 = new OctreeNode(origin, halfRange);
origin = rootCenter + new Vector3(1, -1, 1) * rootOffset;
root.bottom2 = new OctreeNode(origin, halfRange);
origin = rootCenter + new Vector3(-1, -1, 1) * rootOffset;
root.bottom3 = new OctreeNode(origin, halfRange);
//2. 遍历当前空间对象,分配对象到子节点
PartitionSceneObjects(root);
//3. 判断子节点对象数量,如果过多,则继续递归划分。
if (root.top0.objectCount >= 2)
GenerateOctree(root.top0, halfRange, depth - 1);
if (root.top1.objectCount >= 2)
GenerateOctree(root.top1, halfRange, depth - 1);
if (root.top2.objectCount >= 2)
GenerateOctree(root.top2, halfRange, depth - 1);
if (root.top3.objectCount >= 2)
GenerateOctree(root.top3, halfRange, depth - 1);
if (root.bottom0.objectCount >= 2)
GenerateOctree(root.bottom0, halfRange, depth - 1);
if (root.bottom1.objectCount >= 2)
GenerateOctree(root.bottom1, halfRange, depth - 1);
if (root.bottom2.objectCount >= 2)
GenerateOctree(root.bottom2, halfRange, depth - 1);
if (root.bottom3.objectCount >= 2)
GenerateOctree(root.bottom3, halfRange, depth - 1);
}
//将空间中的物体划分到子节点
private void PartitionSceneObjects(OctreeNode root)
{
var objcets = root.areaObjects;
foreach (var obj in objcets)
{
if (root.top0.Contains(obj.transform.position))
{
root.top0.AddGameobject(obj);
}
else if (root.top1.Contains(obj.transform.position))
{
root.top1.AddGameobject(obj);
}
else if (root.top2.Contains(obj.transform.position))
{
root.top2.AddGameobject(obj);
}
else if (root.top3.Contains(obj.transform.position))
{
root.top3.AddGameobject(obj);
}
else if (root.bottom0.Contains(obj.transform.position))
{
root.bottom0.AddGameobject(obj);
}
else if (root.bottom1.Contains(obj.transform.position))
{
root.bottom1.AddGameobject(obj);
}
else if (root.bottom2.Contains(obj.transform.position))
{
root.bottom2.AddGameobject(obj);
}
else if (root.bottom3.Contains(obj.transform.position))
{
root.bottom3.AddGameobject(obj);
}
}
}
2.扩展(Gizmos)


一般因为需要将该劣放在Editor文件夹内,所以使用特性的方法可以将业务逻辑和调试脚本分开,由于其针对的是编辑器组件的方法,需要设置为Static方法。
如
关于GizmoType的介绍以及Gizmos常用的方法。


3. Gizmos可视化
为了检查我们的生成结果,额外写一些Unity Gizmos的东西来可视化一下。
定义一个OctreeDebugMode枚举,在TutorialOctree中添加一些用于可视化配置的字段。
cs
public enum OctreeDebugMode
{
AllDepth,
TargetDepth
}
public class TutorialOctree : MonoBehaviour
{
//是否显示八叉树
public bool showOctree = true;
//可视化类型
public OctreeDebugMode octreeDebugMode;
//可视化深度
[Range(0, 8)]
public int displayDepth = 3;
//不同深度的可视化颜色
public Color[] displayColor;
private void OnDrawGizmos()
{
if (root == null) return;
if (showOctree && displayDepth <= buildDepth)
{
//显示所有深度的空间范围
if (octreeDebugMode == OctreeDebugMode.AllDepth)
{
Gizmos.color = new Color(1, 1, 1, 0.2f);
DrawNode(root, displayDepth);
}
//只显示指定深度的空间范围
else if (octreeDebugMode == OctreeDebugMode.TargetDepth)
{
if (displayColor.Length > displayDepth)
{
var color = displayColor[displayDepth];
color.a = 0.2f;
Gizmos.color = color;
DrawTargetDepth(root, displayDepth);
}
}
}
}
//绘制指定深度
private void DrawTargetDepth(OctreeNode node, int depth)
{
if (node == null) return;
if (depth <= 0)
{
node.DrawGizmos();
return;
}
var nextDepth = depth - 1;
var kid = node.top0;
DrawTargetDepth(kid, nextDepth);
kid = node.top1;
DrawTargetDepth(kid, nextDepth);
kid = node.top2;
DrawTargetDepth(kid, nextDepth);
kid = node.top3;
DrawTargetDepth(kid, nextDepth);
kid = node.bottom0;
DrawTargetDepth(kid, nextDepth);
kid = node.bottom1;
DrawTargetDepth(kid, nextDepth);
kid = node.bottom2;
DrawTargetDepth(kid, nextDepth);
kid = node.bottom3;
DrawTargetDepth(kid, nextDepth);
}
//绘制所有深度
private void DrawNode(OctreeNode node, int depth)
{
if (node == null) return;
if (depth > 0 && depth < displayColor.Length)
{
var color = displayColor[depth];
color.a = 0.5f;
Gizmos.color = color;
node.DrawGizmos();
}
var kid = node.top0;
DrawNode(kid, depth - 1);
kid = node.top1;
DrawNode(kid, depth - 1);
kid = node.top2;
DrawNode(kid, depth - 1);
kid = node.top3;
DrawNode(kid, depth - 1);
kid = node.bottom0;
DrawNode(kid, depth - 1);
kid = node.bottom1;
DrawNode(kid, depth - 1);
kid = node.bottom2;
DrawNode(kid, depth - 1);
kid = node.bottom3;
DrawNode(kid, depth - 1);
}
}
至此,基本的八叉树生成与可视化构建完毕,在脚本面板上配置一下参数,运行看看:
可视化所有深度
可视化目标深度


2D正交的视图更加直观,上:目标深度;下:所有深度
优势与劣势
| 优势 | 劣势 |
|---|---|
| ✅ 快速空间查询(O(log n)) | ❌ 实现复杂 |
| ✅ 自动适应物体分布 | ❌ 需要维护树结构 |
| ✅ 内存相对可控 | ❌ 动态物体需要更新 |
| ✅ 完美支持3D空间 | ❌ 边界情况处理麻烦 |
总结
这段代码的核心逻辑:
-
递归分割:将空间不断切成8等分
-
分配物体:每个物体放入对应的子节点
-
条件终止:深度用尽或物体数量少于阈值
一句话概括: 八叉树就像把一个大房间不断切成8个小房间,每个小房间再继续切,直到每个房间里的物体足够少,这样查找物体时只需要检查相关的小房间,而不是整个大房间。