基于八叉树实现在三维空间中寻路

八叉树

八叉树是用于描述三维空间的树状数据结构,可以将其看作是四叉树在三维空间上的扩展。八叉树的节点通过一个立方体区域进行表示,如果节点对应的立方体区域内没有任何物体,那么将该节点作为八叉树的叶节点,否则,该立方体区域将被继续划分为8个等分的子区域,也就是说该节点包含8个子节点。通过递归的方式不断细分节点区域,直至区域内不再有任何物体或者节点大小已经达到了指定的最小规模。

定义节点类时,需要声明表示节点中心坐标和节点大小的字段,以及用于存储子节点的数组,代码如下:

csharp 复制代码
using UnityEngine;
using System.Collections.Generic;

public class OctreeNode
{
    public Vector3 center; //节点中心
	public float nodeSize; //节点大小
	public OctreeNode[] subNodes; //子节点
    private readonly float minNodeSize;
}

在构造函数中,通过盒体重叠检测方法检测节点区域内是否有碰撞器,如果有,需要继续划分子区域,子节点的大小等于当前节点大小的一半,子节点的中心坐标会在各个坐标轴上偏移当前节点大小的四分之一。如果当前节点大小的一半小于指定的最小节点大小,不再继续划分,代码如下:

csharp 复制代码
public bool isPassable;
public OctreeNode(Vector3 center, float nodeSize, float minNodeSize)
{
    this.center = center;
    this.nodeSize = nodeSize;
    this.minNodeSize = minNodeSize;

    //子节点大小等于当前节点大小的1/2
    float subNodeSize = nodeSize * .5f;
    //盒体重叠检测,如果未检测到任何碰撞器,表示该节点是可通行的
	isPassable = Physics.OverlapBox(center, 
	nodeSize * .5f * Vector3.one).Length == 0;
    //如果该区域内没有任何碰撞器,不需要继续划分
    //如果有碰撞,且子节点大小大于最小节点大小,继续划分
    if (!isPassable && subNodeSize > minNodeSize)
	{
    //子节点中心坐标会在各坐标轴上偏移当前节点大小的1/4
        float quarter = nodeSize * .25f;
        subNodes = new OctreeNode[8];
        int index = -1;
        for (int x = -1; x <= 1; x += 2)
        {
            for (int y = -1; y <= 1; y += 2)
            {
                for (int z = -1; z <= 1; z += 2)
                {
                    subNodes[++index] = new OctreeNode(
                        center + quarter * new Vector3(x, y, z),
                        subNodeSize, minNodeSize);
                }
            }
        }
    }
}

在八叉树的类构造函数中创建八叉树的根节点,通过递归完成空间区域的划分,代码如下:

csharp 复制代码
using UnityEngine;
using System.Collections.Generic;

public class Octree
{
    //根节点
    public OctreeNode rootNode;

    public Octree(Vector3 center, float rootNodeSize, float minLeafSize)
    {
        rootNode = new OctreeNode(center, rootNodeSize, minLeafSize);
    }
}

空间寻路

通过八叉树数据结构,可以实现可飞行游戏单位在三维空间中进行寻路的功能。将A*寻路算法应用到此处,需要修改邻节点的搜索方式,此处通过与周围的节点构建链接的方式获取邻节点。在构建节点间的链接之前,需要先获取所有的叶节点,也就是没有子节点的节点,通过递归的方式获取,代码如下:

csharp 复制代码
//叶节点集合
private readonly List<OctreeNode> leavesNodes;
//获取叶节点
private void GetLeavesNodes(OctreeNode node)
{
    //该节点没有子节点,那么它就是叶节点
    if (node.subNodes == null)
        leavesNodes.Add(node);
    else
    {
        //遍历子节点
        for (int i = 0; i < node.subNodes.Length; i++)
        {
            //递归获取叶节点
            GetLeavesNodes(node.subNodes[i]);
        }
    }
}

在节点类中,通过列表存储链接节点,在与其它节点构建链接时,需要两个节点都是可通行的节点,并且两个节点之间没有碰撞器阻挡,代码如下:

csharp 复制代码
public List<OctreeNode> connections; //链接节点集合

//构建与指定节点的链接
public void BuildConnection(OctreeNode node)
{
    connections ??= new List<OctreeNode>();
    if (node == null) return;
    if (connections.Contains(node)) return;
	if (!isPassable || !node.isPassable) return;
	//通过球体投射检测方法,检测两个节点间是否有碰撞器
    Vector3 direction = node.center - center;
    if (Physics.SphereCast(center, minNodeSize * .5f,
        direction, out _, direction.magnitude)) return;
    connections.Add(node);
}

在八叉树中,通过双层for循环遍历叶节点集合寻找要构建链接的目标。如果从节点A的中心点向节点B的中心点方向形成的射线,与以节点B的中心点和节点大小形成的边界盒相交,并且相交的距离小于相应值,可以判断节点A和B是相邻的,此时,节点B是节点A构建链接的目标,代码如下:

csharp 复制代码
//构建叶节点间的链接
private void BuildNodeConnections()
{
    Vector3[] directionArray = new Vector3[22]
    {
            //前后左右上下
            new Vector3(0f, 0f, 1f), new Vector3(0f, 0f, -1f),
            new Vector3(-1f, 0f, 0f), new Vector3(1f, 0f, 0f),
            new Vector3(0f, 1f, 0f), new Vector3(0f, -1f, 0f),
            //前下、前上、后下、后上
            new Vector3(0f, -1f, 1f), new Vector3(0f, 1f, 1f),
            new Vector3(0f, -1f, -1f), new Vector3(0f, 1f, -1f),
            //右上、右下、左上、左下
            new Vector3(1f, 1f, 0f), new Vector3(1f, -1f, 0f),
            new Vector3(-1f, 1f, 0f), new Vector3(-1f, -1f, 0f),
            //右前、右后、左前、左后
            new Vector3(1f, 0f, 1f), new Vector3(1f, 0f, -1f),
            new Vector3(-1f, 0f, 1f), new Vector3(-1f, 0f, -1f),
            //斜向
            new Vector3(1f, 1f, 1f), new Vector3(-1f, 1f, 1f),
            new Vector3(1f, 1f, -1f), new Vector3(-1f, 1f, -1f),
    };
    for (int i = 0; i < leavesNodes.Count; i++)
    {
        OctreeNode currentNode = leavesNodes[i];
        List<OctreeNode> neighbourNodes = new List<OctreeNode>();
        //遍历其它叶节点,以寻找当前节点的邻节点
        for (int j = 0; j < leavesNodes.Count; j++)
        {
            if (i == j) continue;
            OctreeNode targetNode = leavesNodes[j];
            for (int k = 0; k < directionArray.Length; k++)
            {
                Vector3 direction = directionArray[k];
                //当前节点中心向各个方向的射线
                Ray ray = new Ray(currentNode.center, direction);
                //射线与以目标节点中心和大小形成的边界盒是否相交
                if (new Bounds(targetNode.center, targetNode.nodeSize
                    * Vector3.one).IntersectRay(ray, out float distance))
                {
                    //相交时判断距离是否小于当前节点大小的一半
                    //如果小于,将目标节点作为当前节点的邻节点
                    if (distance < currentNode.nodeSize
                        * .5f * direction.magnitude + .01f)
                        neighbourNodes.Add(targetNode);
                }
            }
        }
        //遍历找到的邻节点,构建链接
        for (int n = 0; n < neighbourNodes.Count; n++)
            currentNode.BuildConnection(neighbourNodes[n]);
    }
}
相关推荐
一只一只15 小时前
Unity之UGUI Button按钮组件详细使用教程
unity·游戏引擎·ugui·button·ugui button
WarPigs18 小时前
Unity阴影
unity·游戏引擎
一只一只18 小时前
Unity之Invoke
unity·游戏引擎·invoke
tealcwu21 小时前
【Unity踩坑】Simulate Touch Input From Mouse or Pen 导致检测不到鼠标点击和滚轮
unity·计算机外设·游戏引擎
ThreePointsHeat1 天前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
迪普阳光开朗很健康1 天前
UnityScrcpy 可以让你在unity面板里玩手机的插件
unity·游戏引擎
陈言必行2 天前
Unity 之 设备性能分级与游戏画质设置与设备自动适配指南
游戏·unity·游戏引擎
CreasyChan2 天前
Unity DOTS技术栈详解
unity·c#·游戏引擎
在路上看风景2 天前
1.1 Unity资源生命周期管理与内存机制
unity
CreasyChan2 天前
Unity的ECS(Entity Component System)架构详解
unity·架构·游戏引擎