IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> Unity中的资源管理-对象池技术(1) -> 正文阅读

[游戏开发]Unity中的资源管理-对象池技术(1)

本文分享Unity中的资源管理-对象池技术(1)

接下来几天, 作者会按照自己的理解写几篇关于Unity中的资源管理相关的文章.

大概会涉及到:

  • 对象池: 分为普通类GameObject(主要是预制)的对象池

  • 引用计数技术

  • Unity中的资源基本概念, 分类, 基本使用: 包含Resources, AssetDatabase

  • Unity中的Ab包: 包含Ab包的介绍, 生成, 加载和卸载

  • Unity中使用Profiler进行内存分析

  • 一整套资源管理方案

今天分享对象池技术的第一部分: 普通类的对象池.

什么是对象池以及为什么需要对象池

在正式开始之前, 我们需要搞明白为什么需要对象池.

对象的实例化可能是一个比较繁重的工作, 比如预制的实例化, 需要将在背后做很多事情, 如加载其依赖的资源, 实例化这些资源, 给资源对象赋值, 给预制对象赋值, 最终得到预制的实例化对象. 针对同一个预制, 如果需要多次使用和销毁其实例化对象, 会造成资源浪费和卡顿, 我们可以在不使用的时候将其实例化对象保存下来而不是直接销毁, 在下次使用时直接给予缓存的对象而不需要进行实例化, 这样可以充分利用资源, 也可以加速进程.

简单的说对象池就是将不需要使用的多个对象存下来, 下次需要使用时直接给予而不再进行实例化的技术.

在我们游戏开发中最常见的对象池技术的实例就是子弹的使用.

想象一下, 如果不使用对象池, 我们就会频繁的创建和销毁对象, 这对性能是很大的影响.

对象池是一个典型的空间换时间的技术, 为了玩家流畅的体验, 我们一般会在进入场景前预加载和预创建好一定数量的对象存放在对象池中, 然后在游戏过程中直接拿来使用即可.

简单的对象池实现

首先, 我们先实现一个满足基本需求的简单对象池: 可以产生对象, 回收对象, 销毁对象.

public class SimpleObjectPool<T> where T : class, new() {
    protected readonly Stack<T> m_ObjPool;

    protected int m_MaxCount;
    protected int m_OnceReleaseCount;

    public SimpleObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        m_ObjPool = new Stack<T>(initCapacity);

        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;
    }

    public T Spawn() {
        var obj = m_ObjPool.Count <= 0 ? CreateObj() : m_ObjPool.Pop();
        return obj;
    }

    public void Recycle(T obj) {
        if (m_ObjPool.Count >= m_MaxCount) {
            ReleaseOverflowObj();
        }

        m_ObjPool.Push(obj);
    }

    private T CreateObj() {
        var obj = new T();
        return obj;
    }

    /// <summary>
    /// 移除多余对象
    /// </summary>
    private void ReleaseOverflowObj() {
        Debug.Log("[ReleaseOverflowObj] 已达最大回收数量: " + m_ObjPool.Count);

        var removeCount = Math.Min(m_OnceReleaseCount, m_ObjPool.Count);
        while(--removeCount >= 0) {
            var obj = m_ObjPool.Pop();
        }

        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }
}

我们使用泛型来创建对象池, 这样可以很好的兼容大部分类.

第一行public class SimpleObjectPool<T> where T : class, new(), 其中where T : class, new()代表对于容纳的结构要有两个约束: 结构T必须是类(class), 并且拥有无参构造函数(new()).

我们使用栈来当做对象的容器, 因为我们会频繁的添加和删除, 而栈对于这两个操作的复杂度都是O(1)级别的.

常见的另一个实现是使用数组, 数组的每个位置代表一个插槽, 待回收的对象插入插槽, 并返回索引, 需要使用时依靠索引来获取, Xlua的对象池就是使用这种方式.

SpawnRecycle接口分别代表产生和回收对象, 逻辑比较简单.

我们的实现还有一个最大的容量限制, 达到最大容量后才会销毁配置数量的对象.

我们需要更多

上面的实现在一些简单的场景是够用了, 但是在日常开发中我们经常需要知道对象的生成和销毁时机, 比如在真正销毁时做一些清理操作.

下面我们添加一些新的功能: 在对象的产生, 回收, 销毁时通知调用方, 并且可以根据调用方提供的方式来创建对象.

public class ObjectPool<T> : IDisposable where T : class, new() {
    public delegate T NewObj();

    protected readonly Stack<T> m_ObjPool;
    protected int m_MaxCount;
    protected int m_OnceReleaseCount;

    protected UnityAction<T> m_BeforeSpawn, m_AfterRecycle, m_AfterRelease;
    protected NewObj m_NewObjDelegate;

    public ObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10,
                      UnityAction<T> beforeSpawn = null, UnityAction<T> afterRecycle = null, UnityAction<T> afterRelease = null, 
                      NewObj newObj = null) {

        m_ObjPool = new Stack<T>(initCapacity);

        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;

        m_BeforeSpawn = beforeSpawn;
        m_AfterRecycle = afterRecycle;
        m_AfterRelease = afterRelease;
        m_NewObjDelegate = newObj;
    }

    public void Dispose() {
        if (m_AfterRelease != null) {
            var array = m_ObjPool.ToArray();
            foreach(var obj in array) {
                m_AfterRelease(obj);
            }
        }

        m_ObjPool.Clear();

        m_BeforeSpawn = m_AfterRecycle = m_AfterRelease = null;
        m_NewObjDelegate = null;
    }

    private T CreateObj() {
        var obj = m_NewObjDelegate != null ? m_NewObjDelegate() : new T();
        return obj;
    }

    /// <summary>
    /// 移除多余对象
    /// </summary>
    private void ReleaseOverflowObj() {
        Debug.Log("[ReleaseOverflowObj] 已达最大回收数量: " + m_ObjPool.Count);

        var removeCount = Math.Min(m_OnceReleaseCount, m_ObjPool.Count);
        while(--removeCount >= 0) {
            var obj = m_ObjPool.Pop();
            m_AfterRelease?.Invoke(obj);
        }

        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }

    public T Spawn() {
        var obj = m_ObjPool.Count <= 0 ? CreateObj() : m_ObjPool.Pop();
        m_BeforeSpawn?.Invoke(obj);

        return obj;
    }

    public void Recycle(T obj) {
        if (m_ObjPool.Count >= m_MaxCount) {
            ReleaseOverflowObj();
        }

        m_ObjPool.Push(obj);
        m_AfterRecycle?.Invoke(obj);
    }
}

可以看到, 我们增加了几个委托, 并在对应的位置调用:

  • UnityAction<T> m_BeforeSpawn: 产生对象之前通知
  • UnityAction<T> m_AfterRecycle: 回收对象之前通知
  • UnityAction<T> m_AfterRelease: 销毁对象之前通知
  • NewObj m_NewObjDelegate: 生成对象

最后实现接口IDisposable, 在对象池销毁时清理这些委托, 养成良好的清理委托的习惯, 避免在如Xlua中使用时造成无法正常垃圾回收.

顺便说一句: m_AfterRecycle?.Invoke(obj);的等价于if (m_AfterRecycle != null) m_AfterRecycle(obj);

使用示例

使用很简单, 也就是需要的时候申请, 不需要的时候回收而已.

public class Animal {
    public int id;
    public string name;

    public void Reset() {
        id = 0;
        name = string.Empty;
    }
}

public class ObjectPoolTest {
    public static void Test() {
        var objectPool = new ObjectPool<Animal>(1, 10, 5,
            animal => {
                Debug.Log(string.Format("before spawn code: {0} ", animal.GetHashCode()));
            }, 
            animal => {
                Debug.Log(string.Format("after recycle code: {0}, name: {1}", animal.GetHashCode(), animal.name));

                animal.Reset();
            },
            animal => {
                Debug.Log(string.Format("after release code: {0}", animal.GetHashCode()));
            });

        var index = 1;
        var lst = new List<Animal>();
        for(var i = 0; i < 20; i++) {
            var animal = objectPool.Spawn();
            animal.id = i;
            animal.name = "name: " + index++;

            lst.Add(animal);
        }

        for(var i = 18; i > 0; i--) {
            var animal = lst[i];
            objectPool.Recycle(animal);

            lst.Remove(animal);
        }

        for(var i = 0; i < 2; i++) {
            var animal = objectPool.Spawn();
            animal.id = i;
            animal.name = "name: " + index++;

            lst.Add(animal);
        }

        foreach(var animal in lst) {
            objectPool.Recycle(animal);
        }

        objectPool.Dispose();
    }
}

我们产生了一些对象, 然后使用, 然后回收, 然后又产生了一些对象, 最后回收所有对象.

整个过程会达到最大数量的对象, 然后一直回收利用. 这里就不贴输出了, 有兴趣的同学可以自行尝试.

最后

今天分享了普通类的对象池, 下一篇文章我们将使用对象池的技术来管理Unity中的GameObject对象.

好了, 今天就这样, 希望对大家有所帮助.

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-12-16 18:01:25  更:2021-12-16 18:01:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 20:34:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码