unity 四叉树管理场景
声明:参考https://www.geek-share.com/detail/2731522698.html和《游戏编程模式》
当场景元素过多时,需要实时的显示及隐藏物体使得性能提示,但是物体那么多,怎么知道哪些物体需要显示,哪些物体不需要显示的。当然,遍历物体判断该物体是否可以显示是最容易想到的方法,但是每次更新要遍历所有物体的代价很高,有没有其他可以替代的方法呢,当然有,四叉树就是其中一个方法。
假设场景是一维的,所有物体从左到右排成一条线,那么用二分法就可以快速找出距离自己一定范围内的物体。
同样四叉树的原理像二分一样,只是二分法处理的是一维世界, 四叉树处理的是二维世界,再往上三维世界用八叉树处理,这里用四叉树管理,八叉树暂时不讨论,原理类似。
这里先展示效果:
[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; } }View Code 定义节点的接口:
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(); }View Code
定义节点:
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() {} }View Code
一棵完整的树:
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 = 5; this.maxChildCount = 4; root = new Node(bound, 0, this); } public void InsertObj(ObjData obj) { root.InsertObj(obj); } public void TriggerMove(Camera camera) { root.TriggerMove(camera); } public void DrawBound() { root.DrawBound(); } }View Code
初始化场景物体时,对于每个物体,需要插入四叉树中:判断该物体属于根节点的哪个儿子中,如果有多个儿子都可以包含这个物体,那么这个物体属于该节点,否则属于儿子,进入儿子中重复之前的步骤。
代码如下:
public void InsertObj(ObjData obj) { Node node = null; bool bChild = false; if(depth < belongTree.maxDepth && childList == null) { //如果还没到叶子节点,可以拥有儿子且儿子未创建,则创建儿子 CerateChild(); } if(childList != null) { for (int i = 0; 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); } }View Code
当role走动的时候,需要从四叉树中找到并创建摄像机可以看到的物体
public void TriggerMove(Camera camera) { //刷新当前节点 for(int i = 0; i < objList.Count; ++i) { //进入该节点中意味着该节点在摄像机内,把该节点保存的物体全部创建出来 ResourcesManager.Instance.LoadAsync(objList[i]); } if(depth == 0) { ResourcesManager.Instance.RefreshStatus(); } //刷新子节点 if (childList != null) { for(int i = 0; i < childList.Length; ++i) { if (childList[i].bound.CheckBoundIsInCamera(camera)) { childList[i].TriggerMove(camera); } } } }View Code
游戏运行的一开始,先构造四叉树,并把场景物体的数据插入四叉树中由四叉树管理数据:
[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 = 0; i < objList.Count; ++i) { tree.InsertObj(objList[i]); } role = GameObject.Find("Role").GetComponent<Role>(); bInitEnd = true; } ... }View Code
每次玩家移动则创建物体:
[System.Serializable] public class Main : MonoBehaviour { ... private void Update() { if (role.bMove) { tree.TriggerMove(role.mCamera); } } ... }View Code
怎么计算出某个节点的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 = 0; if (projectionPos.x < -projectionPos.w) _code |= 1; if (projectionPos.x > projectionPos.w) _code |= 2; if (projectionPos.y < -projectionPos.w) _code |= 4; if (projectionPos.y > projectionPos.w) _code |= 8; if (projectionPos.z < -projectionPos.w) _code |= 16; if (projectionPos.z > projectionPos.w) _code |= 32; return _code; }; Vector4 worldPos = Vector4.one; int code = 63; for (int i = -1; i <= 1; i += 2) { for (int j = -1; j <= 1; j += 2) { for (int k = -1; k <= 1; k += 2) { 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 == 0 ? true : false; }View Code
以上是物体的创建,物体的消失放在resourcesmanager中。
建立两个字典分别保存当前显示的物体,和当前隐藏的物体
public class ResourcesManager : MonoBehaviour { public static ResourcesManager Instance; ... private Dictionary<string, SceneObj> activeObjDic;//<suid,SceneObj> private Dictionary<string, SceneObj> inActiveObjDic;//<suid,SceneObj> ... }View Code
开启一段协程,每过一段时间就删除在隐藏字典中的物体:
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); } }View Code
每次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 = 0; i < DelKeysList.Count; ++i) { MoveToInActive(ActiveObjDic[DelKeysList[i]].data); } }View Code
至此,比较简单的四叉树就完毕了。
更复杂的四叉树还需要实现物体在节点之间移动,比如物体是动态的可能从某个节点块移动到另个节点块;物体不消失而用LOD等,在此就不讨论了
项目地址:https://github.com/MCxYY/unity-Multi-tree-manage-scenario
- 引擎技术研究之场景管理------四叉树与视椎剔除技术
- 四叉树管理场景
- 引擎技术研究之场景管理------四叉树与视椎剔除技术
- 三维场景管理之四叉树和八叉树
- UIFramework之Unity4.x 场景管理策略
- Unity学习(十三)场景优化之四叉树
- [图形学] 游戏中的场景管理 - 四叉树 八叉树
- 引擎技术研究之场景管理------四叉树与视椎剔除技术
- 【Unity】四叉树/八叉树管理和动态加载场景物件
- Unity使用git管理项目时场景中丢失脚本的问题
- Unity导出的Xcode项目,iOS端管理摄像头的方法
- 自制Unity小游戏TankHero-2D(5)声音+爆炸+场景切换+武器弹药
- Unity中场景加载出现Level 'Game' (-1) couldn't be loaded
- Unity5.5加载场景并显示进度条
- Unity 5 官方打包管理工具 Asset Bundle Manager
- Unity中查找脚本被哪些Prefab或场景引用
- 【笨木头Unity】入门之旅006:Demo之四处找死(一)_场景和主角
- Unity场景渲染之混合光照模式下的 Baked Indrect(十)
- Unity 编辑器扩展 场景视图内控制对象
- 在unity中用鼠标实现在场景中拖动物体,用鼠标滚轮实现缩放