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

八叉树

八叉树是用于描述三维空间的树状数据结构,可以将其看作是四叉树在三维空间上的扩展。八叉树的节点通过一个立方体区域进行表示,如果节点对应的立方体区域内没有任何物体,那么将该节点作为八叉树的叶节点,否则,该立方体区域将被继续划分为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]);
    }
}
相关推荐
向宇it10 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
Heaphaestus,RC11 小时前
【Unity3D】获取 GameObject 的完整层级结构
unity·c#
芋芋qwq11 小时前
Unity UI射线检测 道具拖拽
ui·unity·游戏引擎
tealcwu12 小时前
【Unity服务】关于Unity LevelPlay的基本情况
unity·游戏引擎
大眼睛姑娘15 小时前
Unity3d场景童话梦幻卡通Q版城镇建筑植物山石3D模型游戏美术素材
unity·游戏美术
鹿野素材屋19 小时前
Unity Dots下的动画合批工具:GPU ECS Animation Baker
unity·游戏引擎
小春熙子1 天前
Unity图形学之着色器之间传递参数
unity·游戏引擎·技术美术·着色器
Java Fans1 天前
在Unity中实现电梯升降功能的完整指南
unity·游戏引擎
GrimRaider2 天前
[Unity]TileMap开发,TileMap地图缝隙问题
unity·游戏引擎·tilemap
无敌最俊朗@2 天前
unity3d——基础篇小项目(开始界面)
unity·游戏引擎