声明:参考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. 剑指offer第二版-6.从尾到头打印链表

    描述:输入一个链表的头节点,从尾到头打印每个节点的值. 思路:从尾到头打印,即为“先进后出”,则可以使用栈来处理:考虑递归的本质也是一个栈结构,可递归输出. 考点:对链表.栈.递归的理解. packa ...

  2. 用Python玩数据-笔记整理-第一章

    第一个程序:print >>>print("Hallo World!") >>>Hallo World! mystring = "Ha ...

  3. WinForm控件之【ComboBox】

    基本介绍 下拉文本框应用较为广泛,在winfrom控件当中使用设置也是相对的简单,主要用于存在多种选择的单选操作场景. 常设置属性.事件 DataSource:绑定加载项的数据源,设置属性Displa ...

  4. hive merge into 批量更新测试

    一.使用条件 hive2.2.0及之后的版本支持使用merge into 语法,使用源表数据批量目标表的数据.使用该功能还需做如下配置 1.参数配置 set hive.support.concurre ...

  5. MyBatis 存储过程

    From<MyBatis从入门到精通> <!-- 6.2 存储过程 6.2.1 第一个存储过程 delimiter ;; create procedure 'select_user_ ...

  6. 【题解】【合并序列(水题)P1628】

    原题链接 这道题目如果连字符串的基本操作都没学建议不要做. 学了的很简单就可以切,所以感觉没什么难度- 主要讲一下在AC基础上的优化(可能算不上剪枝) 很明显,这道题我们要找的是前缀,那么在字符串数组 ...

  7. python爬虫笔记之re.IGNORECASE

    re.IGNORECASE有什么用?re.IGNORECASE是什么意思?(原谅我抓下seo..) 这里自己总结下: re.IGNORECASE是compile函数中的一个匹配模式. re.IGNOR ...

  8. nginx的access.log 和 error.log

    nginx 常用的配置文件有两种: access.log 和 error.log access.log 的作用是 记录用户所有的访问请求,不论状态码,包括200 ,404,500等请求,404,500 ...

  9. 和朱晔一起复习Java并发(五):并发容器和同步器

    本节我们先会来复习一下java.util.concurrent下面的一些并发容器,然后再会来简单看一下各种同步器. ConcurrentHashMap和ConcurrentSkipListMap的性能 ...

  10. 【转】Spring事务详解

    1.事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,那在没有Spring帮我们管理事务 ...