八叉树
八叉树是用于描述三维空间的树状数据结构,可以将其看作是四叉树在三维空间上的扩展。八叉树的节点通过一个立方体区域进行表示,如果节点对应的立方体区域内没有任何物体,那么将该节点作为八叉树的叶节点,否则,该立方体区域将被继续划分为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]);
}
}