unity 四叉树管理场景
声明:参考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 四叉树管理场景的更多相关文章
- unity内存管理(转)
转自:https://www.cnblogs.com/zsb517/p/5724908.html Unity3D 里有两种动态加载机制:一个是Resources.Load,另外一个通过AssetBun ...
- Unity跳转场景进度条制作教程(异步加载)
Unity跳转场景进度条制作 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享 ...
- 演示unity内存管理机制的缺陷
概述 这是最近做项目时发现的一个内存管理机制上的一个缺陷,但是我并不知道这究竟是不是一个bug,因为他可以造成内存泄漏,但是却能避开野指针. 详细 代码下载:http://www.demodashi. ...
- Unity多个场景叠加或大场景处理方法小结
本文章由cartzhang编写.转载请注明出处. 全部权利保留. 文章链接: http://blog.csdn.net/cartzhang/article/details/47614153 作者:ca ...
- Unity学习(十三)场景优化之四叉树
http://blog.sina.com.cn/s/blog_89d90b7c0102wyfw.html 四叉树是在二维图片中定位像素的唯一适合的算法.因为二维空间(图经常被描述的方式)中,平面像素可 ...
- unity内存管理
最近一直在研究unity的内存加载,因为它是游戏运行的重中之重,如果不深入理解和合理运用,很可能导致项目因内存太大而崩溃. 详细说一下细节概念:AssetBundle运行时加载:来自文件就用Creat ...
- HoloLens开发手记 - Unity之Persistence 场景保持
Persistence 场景保持是HoloLens全息体验的一个关键特性,当用户离开原场景中时,原场景中全息对象会保持在特定位置,当用户回到原场景时,能够准确还原原场景的全息内容.WorldAncho ...
- 【Unity入门】场景、游戏物体和组件的概念
版权声明:本文为博主原创文章,转载请注明出处. 游戏和电影一样,是通过每一个镜头的串联来实现的,而这样的镜头我们称之为“场景”.一个游戏一般包含一个到多个场景,这些场景里面实现了不同的功能,把它们组合 ...
- 【Unity入门】场景编辑与场景漫游快捷键
版权声明:本文为博主原创文章,转载请注明出处. 打开Unity主窗口,选择顶部菜单栏的“GameObject”->“3D Object”->“Plane”在游戏场景里面添加一个面板对象.然 ...
随机推荐
- Java基础介绍运行机制笔记
1. 基础知识点图解 编程语言核心结构:变量.基本语法.分支.循环.数组.…… Java面向对象的核心逻辑:OOP.封装.继承.多态.接口…… 开发Java SE高级应用程序:异常.集合.I/O.多线 ...
- java虚拟机-JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)
一.背景 1.1 永久代(PermGen)在哪里? 根据,hotspot jvm结构如下(虚拟机栈和本地方法栈合一起了): 上图引自网络,但有个问题:方法区和heap堆都是线程共享的内存区域. 关于方 ...
- HDU 6011:Lotus and Characters(贪心)
http://acm.hdu.edu.cn/showproblem.php?pid=6011 题意:共有n种字符,每种字符有一个val和一个cnt,代表这个字符的价值和数量.可以制造的总价值是:第一个 ...
- 充气娃娃什么感觉?Python告诉你
上期为大家介绍了requests库的基本信息以及使用requests库爬取某东的商品页,收到了很多同学的反馈说期待猪哥的更新,猪哥感到非常开心,今天就带大家来玩一把刺激的! 一.需求背景 在实际开发过 ...
- async与await详解
async和await只是编译器功能.编译器会用Task类创建代码.如果不适用这两个关键字,也可以用C#4.0和Task类实现同样的功能,只是没有那么方便. 题主在概念上确实混淆的不行,但是确实asy ...
- tomcat配置https以及配置完成后提示服务器缺少中间证书(已解决)
#### tomcat配置https 准备工作 下载好证书文件,下载的时候可以选择为tomcat文件.我这下载下来是压缩包.解压后就是下图的样子. 以.key结尾的文件是证书的key 以.pem结尾的 ...
- fiddle知识点六、如何使用fiddle进行模拟弱网
为什么要模拟弱网 随着互联网的快速发展,越来越多的应用核心功能需要网络进行实现.同一应用在2G.3G.4G和WiFi的不停网络下,响应各有不同.但是因为现在的网络普遍为4G网络,为了保证应用在不同的网 ...
- Java 读写 excel 实战完全解析
本文微信公众号「AndroidTraveler」首发. 背景 时值毕业季,很多毕业生初入职场. 因此,这边也写了一些新手相关的 Android 技术点. 比如上一篇的 Android 开发你需要了解的 ...
- Linux版本划分——基于打包方式
基于Dpkg (Debian系) Debian GNU / Linux是一种强调使用自由软件的发行版.它支持多种硬件平台.Debian及其派生发行版使用deb软件包格式,并使用dpkg及其前端作为包管 ...
- mysqli_query 的定义和用法
定义和用法 mysqli_query() 函数执行某个针对数据库的查询. 语法 mysqli_query(connection,query,resultmode); 参数 描述 connecti ...