声明:参考https://blog.csdn.net/mobilebbki399/article/details/79491544和《游戏编程模式》

当场景元素过多时,需要实时的显示及隐藏物体使得性能提示,但是物体那么多,怎么知道哪些物体需要显示,哪些物体不需要显示的。当然,遍历物体判断该物体是否可以显示是最容易想到的方法,但是每次更新要遍历所有物体的代价很高,有没有其他可以替代的方法呢,当然有,四叉树就是其中一个方法。

假设场景是一维的,所有物体从左到右排成一条线,那么用二分法就可以快速找出距离自己一定范围内的物体。

同样四叉树的原理像二分一样,只是二分法处理的是一维世界, 四叉树处理的是二维世界,再往上三维世界用八叉树处理,这里用四叉树管理,八叉树暂时不讨论,原理类似。

这里先展示效果:

四叉树结构:

根节点是整个场景区域,然后分成四块:左上右上左下右下,分别作为根节点的儿子,然后每个儿子又分成四块重复之前步骤,这就是一棵四叉树。

每个节点保存四个儿子节点的引用,并且有存放在自己节点的物体列表,为什么物体不全部存放在叶子节点呢?因为有可能某个物体比较大,刚好在两个块的边界上。

这时候有两种做法:

1、这个物体同时插入两个节点的物体列表中

2、这个物体放在两个几点的父亲节点的物体列表中

第一种方法管理起来比较麻烦,所以在此采用第二种方法。

首先定义场景物体的数据类:

 [System.Serializable]
public class ObjData
{
[SerializeField]
public string sUid;//独一无二的id,通过guid创建
[SerializeField]
public string resPath;//prefab路径
[SerializeField]
public Vector3 pos;//位置
[SerializeField]
public Quaternion rotation;//旋转
public ObjData(string resPath, Vector3 pos, Quaternion rotation)
{
this.sUid = System.Guid.NewGuid().ToString();
this.resPath = resPath;
this.pos = pos;
this.rotation = rotation;
}
}

定义节点的接口:

 public interface INode
{
Bounds bound { get; set; }
/// <summary>
/// 初始化插入一个场景物体
/// </summary>
/// <param name="obj"></param>
void InsertObj(ObjData obj);
/// <summary>
/// 当触发者(主角)移动时显示/隐藏物体
/// </summary>
/// <param name="camera"></param>
void TriggerMove(Camera camera);
void DrawBound();
}

定义节点:

 public class Node : INode
{
public Bounds bound { get; set; } private int depth;
private Tree belongTree;
private Node[] childList;
private List<ObjData> objList; public Node(Bounds bound, int depth, Tree belongTree)
{
this.belongTree = belongTree;
this.bound = bound;
this.depth = depth;
objList = new List<ObjData>();
} public void InsertObj(ObjData obj)
{} public void TriggerMove(Camera camera)
{} private void CerateChild()
{}
}

一棵完整的树:

 public class Tree : INode
{
public Bounds bound { get; set; }
private Node root;
public int maxDepth { get; }
public int maxChildCount { get; } public Tree(Bounds bound)
{
this.bound = bound;
this.maxDepth = ;
this.maxChildCount = ;
root = new Node(bound, , this);
} public void InsertObj(ObjData obj)
{
root.InsertObj(obj);
} public void TriggerMove(Camera camera)
{
root.TriggerMove(camera);
} public void DrawBound()
{
root.DrawBound();
}
}

初始化场景物体时,对于每个物体,需要插入四叉树中:判断该物体属于根节点的哪个儿子中,如果有多个儿子都可以包含这个物体,那么这个物体属于该节点,否则属于儿子,进入儿子中重复之前的步骤。

代码如下:

 public void InsertObj(ObjData obj)
{
Node node = null;
bool bChild = false; if(depth < belongTree.maxDepth && childList == null)
{
//如果还没到叶子节点,可以拥有儿子且儿子未创建,则创建儿子
CerateChild();
}
if(childList != null)
{
for (int i = ; i < childList.Length; ++i)
{
Node item = childList[i];
if (item == null)
{
break;
}
if (item.bound.Contains(obj.pos))
{
if (node != null)
{
bChild = false;
break;
}
node = item;
bChild = true;
}
}
} if (bChild)
{
//只有一个儿子可以包含该物体,则该物体
node.InsertObj(obj);
}
else
{
objList.Add(obj);
}
}

当role走动的时候,需要从四叉树中找到并创建摄像机可以看到的物体

 public void TriggerMove(Camera camera)
{
//刷新当前节点
for(int i = ; i < objList.Count; ++i)
{
//进入该节点中意味着该节点在摄像机内,把该节点保存的物体全部创建出来
ResourcesManager.Instance.LoadAsync(objList[i]);
} if(depth == )
{
ResourcesManager.Instance.RefreshStatus();
} //刷新子节点
if (childList != null)
{
for(int i = ; i < childList.Length; ++i)
{
if (childList[i].bound.CheckBoundIsInCamera(camera))
{
childList[i].TriggerMove(camera);
}
}
}
}

游戏运行的一开始,先构造四叉树,并把场景物体的数据插入四叉树中由四叉树管理数据:

 [System.Serializable]
public class Main : MonoBehaviour
{
[SerializeField]
public List<ObjData> objList = new List<ObjData>();
public Bounds mainBound; private Tree tree;
private bool bInitEnd = false; private Role role; public void Awake()
{
tree = new Tree(mainBound);
for(int i = ; i < objList.Count; ++i)
{
tree.InsertObj(objList[i]);
}
role = GameObject.Find("Role").GetComponent<Role>();
bInitEnd = true;
}
...
}

每次玩家移动则创建物体:

 [System.Serializable]
public class Main : MonoBehaviour
{
... private void Update()
{
if (role.bMove)
{
tree.TriggerMove(role.mCamera);
}
}
... }

怎么计算出某个节点的bound是否与摄像机交叉呢?

我们知道,渲染管线是局部坐标系=》世界坐标系=》摄像机坐标系=》裁剪坐标系=》ndc-》屏幕坐标系,其中在后三个坐标系中可以很便捷的得到某个点是否处于摄像机可视范围内。

在此用裁剪坐标系来判断,省了几次坐标转换,判断某个点在摄像机可视范围内方法如下:

将该点转换到裁剪空间,得到裁剪空间中的坐标为vec(x,y,z,w),那么如果-w<x<w&&-w<y<w&&-w<z<w,那么该点在摄像机可视范围内。

对bound来说,它有8个点,当它的8个点同时处于摄像机裁剪块上方/下方/前方/后方/左方/右方,那么该bound不与摄像机可视范围交叉

代码如下:

 public static bool CheckBoundIsInCamera(this Bounds bound, Camera camera)
{
System.Func<Vector4, int> ComputeOutCode = (projectionPos) =>
{
int _code = ;
if (projectionPos.x < -projectionPos.w) _code |= ;
if (projectionPos.x > projectionPos.w) _code |= ;
if (projectionPos.y < -projectionPos.w) _code |= ;
if (projectionPos.y > projectionPos.w) _code |= ;
if (projectionPos.z < -projectionPos.w) _code |= ;
if (projectionPos.z > projectionPos.w) _code |= ;
return _code;
}; Vector4 worldPos = Vector4.one;
int code = ;
for (int i = -; i <= ; i += )
{
for (int j = -; j <= ; j += )
{
for (int k = -; k <= ; k += )
{
worldPos.x = bound.center.x + i * bound.extents.x;
worldPos.y = bound.center.y + j * bound.extents.y;
worldPos.z = bound.center.z + k * bound.extents.z; code &= ComputeOutCode(camera.projectionMatrix * camera.worldToCameraMatrix * worldPos);
}
}
}
return code == ? true : false;
}

以上是物体的创建,物体的消失放在resourcesmanager中。

建立两个字典分别保存当前显示的物体,和当前隐藏的物体

 public class ResourcesManager : MonoBehaviour
{
public static ResourcesManager Instance; ...
private Dictionary<string, SceneObj> activeObjDic;//<suid,SceneObj>
private Dictionary<string, SceneObj> inActiveObjDic;//<suid,SceneObj>
...
}

开启一段协程,每过一段时间就删除在隐藏字典中的物体:

 private IEnumerator IEDel()
{
while (true)
{
bool bDel = false;
foreach(var pair in InActiveObjDic)
{
...
Destroy(pair.Value.obj);
}
InActiveObjDic.Clear();
if (bDel)
{
Resources.UnloadUnusedAssets();
}
yield return new WaitForSeconds(delTime);
}
}

每次triggerMove创建物体后刷新资源状态,将此次未进入节点(status = old)的物体从显示字典中移到隐藏字典中,并将此次进入节点(status = new)的物体标记为old为下次创建做准备

 public void RefreshStatus()
{
DelKeysList.Clear();
foreach (var pair in ActiveObjDic)
{
SceneObj sceneObj = pair.Value;
if(sceneObj.status == SceneObjStatus.Old)
{
DelKeysList.Add(pair.Key);
}
else if(sceneObj.status == SceneObjStatus.New)
{
sceneObj.status = SceneObjStatus.Old;
}
}
for(int i = ; i < DelKeysList.Count; ++i)
{
MoveToInActive(ActiveObjDic[DelKeysList[i]].data);
}
}

至此,比较简单的四叉树就完毕了。

更复杂的四叉树还需要实现物体在节点之间移动,比如物体是动态的可能从某个节点块移动到另个节点块;物体不消失而用LOD等,在此就不讨论了

项目地址:https://github.com/MCxYY/unity-Multi-tree-manage-scenario

unity 四叉树管理场景的更多相关文章

  1. unity内存管理(转)

    转自:https://www.cnblogs.com/zsb517/p/5724908.html Unity3D 里有两种动态加载机制:一个是Resources.Load,另外一个通过AssetBun ...

  2. Unity跳转场景进度条制作教程(异步加载)

    Unity跳转场景进度条制作 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享 ...

  3. 演示unity内存管理机制的缺陷

    概述 这是最近做项目时发现的一个内存管理机制上的一个缺陷,但是我并不知道这究竟是不是一个bug,因为他可以造成内存泄漏,但是却能避开野指针. 详细 代码下载:http://www.demodashi. ...

  4. Unity多个场景叠加或大场景处理方法小结

    本文章由cartzhang编写.转载请注明出处. 全部权利保留. 文章链接: http://blog.csdn.net/cartzhang/article/details/47614153 作者:ca ...

  5. Unity学习(十三)场景优化之四叉树

    http://blog.sina.com.cn/s/blog_89d90b7c0102wyfw.html 四叉树是在二维图片中定位像素的唯一适合的算法.因为二维空间(图经常被描述的方式)中,平面像素可 ...

  6. unity内存管理

    最近一直在研究unity的内存加载,因为它是游戏运行的重中之重,如果不深入理解和合理运用,很可能导致项目因内存太大而崩溃. 详细说一下细节概念:AssetBundle运行时加载:来自文件就用Creat ...

  7. HoloLens开发手记 - Unity之Persistence 场景保持

    Persistence 场景保持是HoloLens全息体验的一个关键特性,当用户离开原场景中时,原场景中全息对象会保持在特定位置,当用户回到原场景时,能够准确还原原场景的全息内容.WorldAncho ...

  8. 【Unity入门】场景、游戏物体和组件的概念

    版权声明:本文为博主原创文章,转载请注明出处. 游戏和电影一样,是通过每一个镜头的串联来实现的,而这样的镜头我们称之为“场景”.一个游戏一般包含一个到多个场景,这些场景里面实现了不同的功能,把它们组合 ...

  9. 【Unity入门】场景编辑与场景漫游快捷键

    版权声明:本文为博主原创文章,转载请注明出处. 打开Unity主窗口,选择顶部菜单栏的“GameObject”->“3D Object”->“Plane”在游戏场景里面添加一个面板对象.然 ...

随机推荐

  1. seo外链发布之论坛外链

    目前最常见的seo外链方式有5种,之前大发迹创业项目网写文章分享过,详情可以查看文章<[网站SEO优化]最常见的五种软文外链发布方式!>,这篇文章不说其他的几种发外链,就来讲讲通过论坛建设 ...

  2. centos7安装hadoop完全分布式集群

    groupadd test             //新建test工作组 useradd -g test phpq        //新建phpq用户并增加到test工作组 userdel 选项 用 ...

  3. ServiceFabric极简文档-5.1 编程模型选择

    项目中:actor用的服务是无状态服务:ASP.NET Core用的是无状态ASP.NET Core模板. ​

  4. TLS示例开发-golang版本

    目录 前言 制作自签名证书 CA 服务器证书相关 客户端证书相关 证书如何验证 在浏览器中导入证书 导入证书 修改域名 golang服务端 目录 main.go 测试 参考 前言 在进行项目总结的时候 ...

  5. Intel FPGA 专用时钟引脚是否可以用作普通输入,输出或双向IO使用?

    原创 by DeeZeng FPGA 的 CLK pin 是否可以用作普通输入 ,输出或双向IO 使用?    这些专用Clock input pin 是否可以当作 inout用,需要看FPGA是否支 ...

  6. Baozi Leetcode solution 1036: Escape a Large Maze

    Problem Statement In a 1 million by 1 million grid, the coordinates of each grid square are (x, y) w ...

  7. Excel催化剂开源第44波-窗体在Show模式下受Excel操作影响变为最小化解决方式

    在Excel催化剂的许多功能中,都会开发窗体用于给用户更友好的交互使用,但有一个问题,困扰许久,在窗体上运行某些代码后,中途弹出下MessageBox对话框给用户做一些简单的提示或交互时,发现程序运行 ...

  8. 个人永久性免费-Excel催化剂功能第26波-正确的Excel密码管理之道

    Excel等文档肩负着我们日常大量的信息存储和传递工作,难免出现数据安全的问题,OFFICE自带的密码设置,在什么样的场景下才有必要使用?网上所宣称的OFFICE文档密码保护不安全,随时可被破解,究竟 ...

  9. HHyperledger Fabric 之 TLS (fabric-java-sdk)使用grpcs方式访问fabric

    我在很多fabric的技术群中,很多使用javasdk连接fabric的同友,初始的时候很多都没有成功的使用TLS进行区块链交易: 是sdk不支持,还是我们没有找到解决方案? 其实不然,我这里使用的是 ...

  10. Python学习4——条件、循环及其他语句总结

    多种语句 打印语句: 导入语句: 赋值语句: 代码块: 条件语句: 断言: 循环: 推导: pass.dal.exec和eval :  学习到的新函数:(以下函数的应用代码均在IDLE测试通过) ch ...