声明:参考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. POJ 3183:Stump Removal(模拟)

    http://poj.org/problem?id=3183 题意:有n个树桩,分别有一个高度h[i],要用Bomb把树桩都炸掉,如果炸的位置的两边树桩高度小于Bomb炸的树桩高度,那么小于树桩高度的 ...

  2. POI 设置Excel单元格背景色(setFillForegroundColor)

    背景介绍:使用Java开发信息系统项目,项目中往往会涉及到报表管理部分,而Excel表格首当其冲称为最合适的选择,但是对单元格操作时对于设置单元格的背景颜色却很少提及,本文旨在方便单元格背景颜色设计. ...

  3. spring的context:exclude-filter 与 context:include-filter

    1 在主容器中(applicationContext.xml),将Controller的注解打消掉 <context:component-scan base-package="com& ...

  4. 使用Optional摆脱NPE的折磨

    在目前的工作中,我对Java中的Stream和Lambda表达式都使用得很多,之前也写了两篇文章来总结对应的知识. 024:Java流实现Shell:cat 1.log | grep a | sort ...

  5. 【DFS例题】等式

    题目如下: 这道题依然是一道dfs(要求输出方案数很明显用dfs呐) 首先一个模板贴上来: void dfs()//参数用来表示状态 { if(到达终点状态) { ...//根据题意添加 return ...

  6. Scrum 使用絮叨

    关于Scrum 的一些絮叨 Scrum 的推行的基础在于全员参与,全员协作,包含Dev, QA(quality assurance) ,BA(Business analyst)以及Supporter ...

  7. Dubbo服务注册与发现

    目录 一.分布式基本理论 1.1.分布式基本定义 1.2 架构发展演变 1.3.RPC简介 二.Dubbo理论简介 三.Dubbo环境搭建 3.1 Zookeeper搭建 3.2 Dubbo管理页面搭 ...

  8. 动手造轮子:实现一个简单的 EventBus

    动手造轮子:实现一个简单的 EventBus Intro EventBus 是一种事件发布订阅模式,通过 EventBus 我们可以很方便的实现解耦,将事件的发起和事件的处理的很好的分隔开来,很好的实 ...

  9. Excel催化剂开源第8波-VSTO开发之异步调用方法

    在VSTO开发过程中,因其和普通的Winform开发有点差别,具体细节笔者也说不清楚,大概是VSTO的插件是寄生在Excel中,不属于独立的进程之类的,其异步方法调用时,未能如Winform那样直接用 ...

  10. Spring_简单入门(学习笔记1)

    Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架. 具体介绍参考 一:IoC(Inversion of Control)控制反转,将创建对象实例反转给spri ...