Unity 分帧加载和分块加载

#分帧加载和分块加载
在我们实际做项目的时候,往往会遇见需要创建大量数据的时候,这时如果在一帧里面大量创建数据,那我们的游戏就会发生卡顿从而降低了用户的体验。
为了解决这种情况,可以使用使用分帧加载使得每帧只加载固定数量的数据来解决,也可以使用分块加载来实现加载当前场景所需的数据。

分帧加载

我们得到的信息是来自于服务器的,当我们需要显示信息的时候,不可能从服务器拿了一条显示一条信息。我们应该定义一个数组先把所有的信息进行保存,通过遍历来显示信息。

public class fpsload : MonoBehaviour
{
    List<string> InfoList;
    int InfoListIndex = 0;

    void start(){
        StratCoroutine(GetFromInfoList() );
    }
    IEnumerator GetFromInfoList()
    {
        foreach(string item in InfoList)
        {
            InfoListIndex++;
            if(InfoListIndex % 5 == 0) //每一帧提取5条数据
            Debug.Log("Load");
            yield return null; //在下一帧执行
        }
    }
}

分块加载

  1. 第一种,划分场景
    当我们需要一次性加载许多地图信息的时候,无疑会发生卡顿,我们此时可以把场景分为多个部分定位好它们的位置信息进行分块的加载。

假设设计一个场景Scene把它分为两个部分LoadScene1和LoadScene2。

//假设当前离开场景LoadScene1
public class blockload : MonoBehaviour
{
    
    void start(){
    }

    IEnumerator LoadScene2()
    {
        //通过SceneManager.LoadSceneAsync实现分块加载不同场景,5.x以前为Application.LoadLevelAdditiveAsync
        AsyncOperation async = SceneManager.LoadSceneAsyn("LoadScene2", LoadSceneMode.Additive);
    }
}
  1. 第二种,分割地图
    对地图进行分块加载,如果说场景里的地形非常大,加上里面的各种模型、贴图、碰撞、渲染等,这将是一笔很庞大的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;
        }
    }

}

图片中绿色区域的9个chunk代表展示在场景中的地形,而玩家则处于区域中心位置为A的chunk,红色区域中的25个chunk代表缓存区域,这个区域会根据玩家的远离被卸载掉,也可能因玩家的靠近则呈现出来。
在这里插入图片描述

玩家从chunk A移动到chunk E时,红色区域标记为U区域的chunk被卸载(unload)、标记为S的chunk被展示(show),蓝色区域标记为L的chunk则被加载(load),绿色区域标记为H的chunk被隐藏(hide)。
在这里插入图片描述

首先根据玩家位置获取玩家所在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>(); //expec为实际要操作chunk列表
        //当前中心点的行列
        int currentRow = currentVector.rowNum;
        int currentCol = currentVector.colNum;
        
        //-2——>2,实际要操作区域,即绿红组成的区域
        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); //移除实际块列表和当前块列表中相同的块元素,注:移除完毕后,实际块列表中的元素

                ChunkState actualState = GetChunkStateByRelativePosition(pos, currentPos);  //先获取chunk的实际状态,GetChunkStateByRelativePosition()作用是获取实际状态,在后面定义

                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="relative">参照块坐标</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://blog.csdn.net/weixin_33835690/article/details/87446398
参考:https://blog.csdn.net/qq_30825027/article/details/89324097

  • 16
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值