赞
踩
要谈到地形为什么要分块加载,很明显,是为了实现对性能上的提升,如果说场景里的地形非常大,加上里面的各种模型、贴图、碰撞、渲染等等 这将是一笔很庞大的cpu、gpu、内存开销,所以我们需要对地形进行分割成多个地形块,再进行合理的加载卸载来达到性能开销上的平衡。
话不多说,我们先来上图,来看看具体的效果
虽然看起来简陋一些,但是基本的加载地形功能是初步完成了。
首先分块加载的核心思想是:将整个大地形,分割为 n x n 的正方形小块chunk(我们在接下来的内容里,把这些小块统称为chunk)。在这里呢,我们为了效率,就使用一个免费开源的分割地形的小工具叫做Terrain Slicing,如果大家有更多的需求,可以尝试对这个小工具的源码进行优化和扩展或者重写,这里具体如何用代码去分割地形就不再深究了,具体使用大致如下图:
这里我们首先把地形分割成16x16的形式(具体你的地形多大,可根据地形具体大小分割成合理的数量),后续我们会用一个chunk对象去管理每一个chunk实体,用具有键值对的数据结构(例如字典)把chunk对象按照chunk所处的行列位置去保存起来(这里我们就以下图这种排列方式去保存,键为位置ChunkVector2,值为Chunk对象),这样方便我们后面对chunk对象的获取以及对chunk对象的操作(例如chunk对象中chunk实体的加载、卸载、缓存)。
public class Chunk { /// <summary> /// 在块列表中所处的位置 /// </summary> ChunkVector2 m_position; /// <summary> /// 块的实体 /// </summary> GameObject m_body; /// <summary> /// 块的资源路径 /// </summary> string m_resPath; /// <summary> /// 块当前的状态 /// </summary> ChunkState m_currentState = ChunkState.UnLoad; /// <summary> /// 创建一个块对象 /// </summary> /// <param name="rowNum">在块列表中的第几行</param> /// <param name="colNum">在块列表中的第几列</param> public Chunk(int rowNum, int colNum) { m_position = new ChunkVector2(rowNum, colNum); m_resPath = string.Format("TerrainPrefab/Terrain_Slice_{0}_{1}", (rowNum + 1), (colNum + 1)); } public Chunk(ChunkVector2 position) : this(position.rowNum, position.colNum) { } public void Display() {}; public void Cache(){}; public void Unload(){}; /// <summary> /// 更新自身状态 /// </summary> /// <param name="state"></param> public void Update(ChunkState state) { if (m_currentState == state) { Debug.LogErrorFormat(" {0} is already {1} ", m_position, m_currentState); return; } switch (state) { case ChunkState.Display: Display(); break; case ChunkState.Cache: Cache(); break; case ChunkState.UnLoad: Unload(); break; } } }
图片取自于https://blog.csdn.net/jxw167/article/details/81483685这里
我的思路主要来源于这个图片,图片中天蓝色区域的9个chunk代表展示在场景中的地形,而玩家则处于区域中心位置为A的chunk,红色区域中的25个chunk代表缓存区域,这个区域会根据玩家的远离被卸载掉,也可能因玩家的靠近则呈现出来,就比如
从玩家从chunk A移动到chunk E的时候,红色区域标记为U区域的chunk被卸载(unload),蓝色区域标记为L区域的chunk则被加载(load),标记为H区域的chunk被隐藏(hide),标记为S区域的chunk被展示出来(show)
首先根据玩家位置获取玩家所在chunk的位置,这里chunk的位置指的是在整个地图的第几行第几列
/// <summary>
/// 获取块坐标
/// </summary>
/// <param name="position">玩家的具体vector3位置</param>
/// <returns></returns>
ChunkVector2 GetCurrentChunkVector(Vector3 position)
{
int col = (int)(position.x / m_chunkLength);
int row = (int)(position.z / m_chunkLength);
return new ChunkVector2(row, col);
}
然后通过当前的chunk位置来获得周围其他的chunk,并把这些chunk加入列表,此时我们就获得了实际要操作的chunk列表。
/// <summary> /// 获取实际块列表 /// </summary> /// <param name="currentVector">当前中心块位置</param> /// <returns></returns> List<ChunkVector2> GetActualChunkList(ChunkVector2 currentVector) { List<ChunkVector2> expectChunkPosList = new List<ChunkVector2>(); int currentRow = currentVector.rowNum; int currentCol = currentVector.colNum; for (int i = -2; i <= 2; i++) { for (int j = -2; j <= 2; j++) { int expRow = currentRow + i; int expCol = currentCol + j; if (expRow < 0 || expCol < 0 || expRow > m_row-1 || expCol > m_col-1) continue; expectChunkPosList.Add(new ChunkVector2(expRow, expCol)); } } return expectChunkPosList; }
然后将实际的chunk列表与当前的chunk列表做对比并去更新当前的chunk列表,在更新当前块列表的过程中,则对相应的chunk做相应的处理,最终使得当前chunk列表与实际chunk列表保持一致
/// <summary> /// 对比当前块列表与实际块列表,并更新当前块列表 /// </summary> /// <param name="actulChunkList">实际块列表</param> /// <param name="currentPos">当前中心块位置</param> private void UpdateCurrentChunkList(List<ChunkVector2> actulChunkList, ChunkVector2 currentPos) { for (int i = 0; i < m_currentChunkList.Count; i++) { ChunkVector2 pos = m_currentChunkList[i]; Chunk chunk = m_chunkMap[pos]; if (!actulChunkList.Contains(pos))//实际块列表里若不存在当前列表的指定元素 则卸载删除 { chunk.Unload();//卸载不存在于实际块列表的块 m_currentChunkList.RemoveAt(i);//移除当前块列表中不存在与实际块列表的块 i--;//在遍历列表时删除列表元素 记得索引-1 否则无法正确遍历 } else { actulChunkList.Remove(pos);//实际块列表移除和当前块列表中相同的元素 注:移除完毕后,实际块列表中的元素 //先获取chunk的实际状态 ChunkState actualState = GetChunkStateByRelativePosition(pos, currentPos); chunk.Update(actualState); } } for (int i = 0; i < actulChunkList.Count; i++) { ChunkVector2 pos = actulChunkList[i]; Chunk chunk = m_chunkMap[pos]; //先获取chunk的实际状态 ChunkState actualState = GetChunkStateByRelativePosition(pos, currentPos); //使用实际状态去更新当前状态 chunk.Update(actualState); m_currentChunkList.Add(pos);//这里添加完以后,当前块列表将与实际块列表保持一致 } Resources.UnloadUnusedAssets(); }
获得实际chunk的位置列表后,通过以当前chunk位置为参照位置,判读出周围chunk对象应该属于具体哪种状态
/// <summary> /// 获取指定块的相对状态 /// </summary> /// <param name="specified">指定块</param> /// <param name="relativeVector">参照块坐标</param> /// <returns>相对块状态</returns> ChunkState GetChunkStateByRelativePosition(ChunkVector2 specified, ChunkVector2 relative) { int rowAmount = Mathf.Abs(specified.rowNum - relative.rowNum); int colAmount = Mathf.Abs(specified.colNum - relative.colNum); if (rowAmount > 2 || colAmount > 2) { return ChunkState.UnLoad; } if (rowAmount == 2 || colAmount == 2) { return ChunkState.Cache; } if (rowAmount <= 1 || colAmount <= 1) { return ChunkState.Display; } return ChunkState.UnLoad; }
至此就初步完成指定chunk的加载、卸载、缓存。
首先这是我第一次写技术博客,也希望这篇文章可以给你带来一些收获,如果有哪里写的不好,欢迎有经验的朋友可以给出一些建议和分享。
为了可以更好的让大家理解,我决定把我的代码分享给大家,
https://github.com/jiajiadelequ/Large-Terrain
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。