OctreeNode

一、 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空间 ❌ 边界情况处理麻烦

总结

这段代码的核心逻辑:

  1. 递归分割:将空间不断切成8等分

  2. 分配物体:每个物体放入对应的子节点

  3. 条件终止:深度用尽或物体数量少于阈值

一句话概括: 八叉树就像把一个大房间不断切成8个小房间,每个小房间再继续切,直到每个房间里的物体足够少,这样查找物体时只需要检查相关的小房间,而不是整个大房间。

相关推荐
WarPigs3 小时前
Unity协程返回值的解决方案
unity·游戏引擎
QfC92C02p5 小时前
C# 中的 Span 和内存:.NET 中的高性能内存处理
java·c#·.net
Yuri X-20216 小时前
VS2022实战测试题——2
程序人生·c#·个人开发·visual studio
公子小六6 小时前
基于.NET的Windows窗体编程之WinForms布局简介
windows·microsoft·c#·.net
zaim16 小时前
计算机的错误计算(二百二十六)
java·python·c#·c·错数·mpmath
William_cl7 小时前
[特殊字符]C# ASP.NET 架构封神之路:分层 + 仓储 + EFCore,写出企业级可维护代码!
架构·c#·asp.net
tq6J5Yg147 小时前
.NET 10 & C# 14 New Features 新增功能介绍-带修饰符的简单 lambda 参数
开发语言·c#·.net
fe7tQnVan7 小时前
从玩具到生产:基于 ChromaDB 打造工程级 RAG 系统
开发语言·c#
ySq0REx017 小时前
.NET 10 & C# 14 New Features 新增功能介绍-.NET CLI工具改进
开发语言·c#·.net