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”在游戏场景里面添加一个面板对象.然 ...
随机推荐
- 20152016-acmicpc-neerc-northern-subregional-contest J:Journey to the "The World's Start"(单调队列+DP+二分)
http://codeforces.com/gym/100801/attachments 题意:给出n-1张不同的票,票价分别为 pi,每张票每次最多可以坐 r 个站(1<=r<n),并且 ...
- c++学习书籍推荐《深入理解C++11 C++11新特性解析与应用》下载
百度云及其他网盘下载地址:点我 编辑推荐 <深入理解C++11:C++11新特性解析与应用>编辑推荐:C++标准委员会成员和IBM XL编译器中国开发团队共同撰写,权威性毋庸置疑.系统.深 ...
- git rebase VS git merge? 更优雅的 git 合并方式值得拥有
写在前面 如果你不能很好的应用 Git,那么这里为你提供一个非常棒的 Git 在线练习工具 Git Online ,你可以更直观的看到你所使用的命令会产生什么效果 另外,你在使用 Git 合并分支时只 ...
- Bzoj 2318 Spoj4060 game with probability Problem
2318: Spoj4060 game with probability Problem Time Limit: 1 Sec Memory Limit: 128 MBSubmit: 524 Sol ...
- Netty使用Google Protocol Buffer完成服务器高性能数据传输
一.什么是Google Protocol Buffer(protobuf官方网站) 下面是官网给的解释: Protocol buffers are a language-neutral, platfo ...
- 使用GDAL实现DEM的地貌晕渲图(二)
1. 问题 之前我在<使用GDAL实现DEM的地貌晕渲图(一)>这篇文章里面讲述了DEM晕渲图的生成原理与实现,大体上来讲是通过计算DEM格网点的法向量与日照方向的的夹角,来确定该格网点的 ...
- HelloDjango 启动!免费带你学Django全栈!
欢迎 追梦 入伙 HelloGitHub-Team,同时为我们带来了完全免费的 HelloDjango 系列教程,全网首发于 HelloGitHub 公众号.让想你的系列文章被跟多人看到,那就来加入我 ...
- Java项目案例之---登录和修改(JSP)
登录和修改(JSP) 通过案例学习jsp的常用知识点: 1.创建一个Map集合,用于存放学生信息,将学生信息存入Map中 2.通过page将需要的包导入 3.用request.getParameter ...
- 踩坑 Spring Cloud Hystrix 线程池队列配置
背景: 有一次在生产环境,突然出现了很多笔还款单被挂起,后来排查原因,发现是内部系统调用时出现了Hystrix调用异常.在开发过程中,因为核心线程数设置的比较大,没有出现这种异常.放到了测试环境,偶尔 ...
- [git] 基础命令笔记
--内容整理自廖雪峰的GIT教程-- git status 查看当前工作区状态,显示未跟踪的文件以及未上传的修改记录 git init 使当前文件夹变成Git可以管理的仓库 git add xxx 将 ...