游戏中的对象池技术探索(一)

前言

对象池技术在游戏开发中的应用非常普遍,它是一种高效管理对象实例的技术,能够避免频繁和重复创建对象所带来的性能开销。本篇文章我们就来探索一下如何在游戏开发中设计通用对象池,使之易于使用和扩展。

代码

代码目录结构
  • ObjectPool
    • Base
    • Interface
    • Settings

ObjectPool作为本模块的根目录,用于存储模块子目录和具体的对象池脚本。Base目录用于存储对象池抽象基类,用于规范对象池的设计。Interface目录用于存储对象池相关的接口,用于未来扩展。Settings目录用于存储创建对象池的参数脚本以及对象池的设置。

Base目录

BasePool.cs

using System;
using System.Collections.Generic;

/// <summary>
/// 对象池基类
/// </summary>
/// <typeparam name="T">对象类型</typeparam>
public abstract class BasePool<T>
{
    /// <summary>
    /// 对象池所生产对象的总数量
    /// </summary>
    public int totalCount { get; protected set; }

    /// <summary>
    /// 对象池当前空闲对象的数量
    /// </summary>
    public int freeCount => _pool.Count;

    /// <summary>
    /// 是否为固定容量的对象池
    /// <para>默认值:False</para>
    /// </summary>
    public readonly bool isFixed;

    /// <summary>
    /// 对象池容量
    /// <para>默认值:PoolConstant.DEFAULT_CAPACITY</para>
    /// </summary>
    public readonly int capacity;

    /// <summary>
    /// 对象创建逻辑
    /// <para>提示:用来自定义对象的创建逻辑</para>
    /// </summary>
    public Func<T> overrideCreate;

    /// <summary>
    /// 对象重置逻辑
    /// <para>提示:用来自定义对象的重置逻辑</para>
    /// </summary>
    public Func<T, T> overrideReset;

    /// <summary>
    /// 池对象
    /// </summary>
    protected readonly Stack<T> _pool;

    /// <summary>
    /// 对象类型是否为可释放对象类型
    /// </summary>
    protected static bool _isDisposable => _staticIsDisposable;
    static readonly bool _staticIsDisposable = typeof(IDisposable).IsAssignableFrom(typeof(T));

    /// <summary>
    /// 对象类型名称
    /// </summary>
    protected static string _typeName => _staticTypeName;
    static readonly string _staticTypeName = typeof(T).Name;

    protected BasePool()
    {
        _pool = new Stack<T>(PoolConstant.DEFAULT_CAPACITY);
        capacity = PoolConstant.DEFAULT_CAPACITY;
    }

    protected BasePool(int capacity)
    {
        if (capacity <= 0) throw new ArgumentException("Pool:The capacity is not allowed to be less than or equal to zero for the pool.");

        _pool = new Stack<T>(capacity);
        this.capacity = capacity;
    }

    protected BasePool(int capacity, bool isFixed)
    {
        if (capacity <= 0) throw new ArgumentException("Pool:The capacity is not allowed to be less than or equal to zero for the pool.");

        _pool = new Stack<T>(capacity);
        this.capacity = capacity;
        this.isFixed = isFixed;
    }

    /// <summary>
    /// 重置对象并返回
    /// </summary>
    protected abstract T Reset(T item);

    /// <summary>
    /// 创建对象
    /// </summary>
    protected abstract T Create();

    /// <summary>
    /// 获取对象
    /// </summary>
    public abstract T Get();

    /// <summary>
    /// 释放对象
    /// </summary>
    public abstract void Release(T item);

    /// <summary>
    /// 清空对象池
    /// </summary>
    public abstract void Clear();
}
Interface目录

......

Settings目录

PoolConstant.cs

public static class PoolConstant
{
    // 对象池默认容量
    public const int DEFAULT_CAPACITY = 10;
}

UnityObjectPoolSettings.cs

using UnityEngine;

/// <summary>
/// Unity对象池设置
/// </summary>
public class UnityObjectPoolSettings<T> where T : Object
{
    /// <summary>
    /// 对象池初始容量
    /// </summary>
    public int capacity = PoolConstant.DEFAULT_CAPACITY;

    /// <summary>
    /// 对象池是否持久化
    /// </summary>
    public bool isPersistant = true;

    /// <summary>
    /// 对象池是否固定容量
    /// </summary>
    public bool isFixed;

    /// <summary>
    /// 对象池容器
    /// </summary>
    public GameObject container;

    /// <summary>
    /// 对象原型
    /// </summary>
    public T original;

    /// <summary>
    /// 对象默认名称
    /// </summary>
    public string defaultName;

    /// <summary>
    /// 获取时激活对象
    /// </summary>
    public bool activeWhenGet = true;
}
具体的对象池 

ClassPool.cs

using System;

/// <summary>
/// Class 类型对象池
/// </summary>
/// <typeparam name="T">具体的 Class 类型</typeparam>
public class ClassPool<T> : BasePool<T>
where T : class
{
    public ClassPool() { }
    public ClassPool(int capacity) : base(capacity) { }
    public ClassPool(int capacity, bool isFixed) : base(capacity, isFixed) { }

    public override void Clear()
    {
        if (_isDisposable)
        {
            while (_pool.Count > 0)
            {
                if (_pool.Pop() is IDisposable ds)
                    ds?.Dispose();
            }
        }
        else _pool.Clear();

        totalCount = 0;
    }

    public override T Get()
    {
        T item;

        if (freeCount > 0) item = _pool.Pop();
        else
        {
            item = Create();
            totalCount++;
        }

        return item;
    }

    public override void Release(T item)
    {
        if (item == null) return;
        _pool.Push(Reset(item));
    }

    protected override T Reset(T item)
    {
        T v_item;
        if (overrideReset != null) v_item = overrideReset(item);
        else v_item = item;
        return v_item == null ? item : v_item;
    }

    protected override T Create()
    {
        if (isFixed && totalCount == capacity)
            throw new InvalidOperationException("Pool:The number of objects in the object pool has reached the upper limit.");

        T item;
        if (overrideCreate != null) item = overrideCreate();
        else item = Activator.CreateInstance<T>();

        if (item == null) throw new InvalidOperationException("Pool:The item created is null.");
        return item;
    }
}

UnityObjectPool.cs

using System;
using UnityEngine;

/// <summary>
/// Unity对象池
/// </summary>
/// <typeparam name="T">Unity对象类型</typeparam>
public class UnityObjectPool<T> : ClassPool<T>, IDisposable
where T : UnityEngine.Object
{
    protected readonly GameObject _container;
    protected readonly T _original;
    protected readonly string _defaultName;
    protected readonly bool _activeWhenGet;
    bool _isDisposed;

    public UnityObjectPool()
    {
        _container = new GameObject($"{_typeName}Pool");
        MonoBehaviour.DontDestroyOnLoad(_container);
        _activeWhenGet = true;
    }

    public UnityObjectPool(int capacity) : base(capacity)
    {
        _container = new GameObject($"{_typeName}Pool");
        MonoBehaviour.DontDestroyOnLoad(_container);
        _activeWhenGet = true;
    }

    public UnityObjectPool(int capacity, bool isFixed) : base(capacity, isFixed)
    {
        _container = new GameObject($"{_typeName}Pool");
        MonoBehaviour.DontDestroyOnLoad(_container);
        _activeWhenGet = true;
    }

    public UnityObjectPool(UnityObjectPoolSettings<T> settings) :
    base(settings == null ? PoolConstant.DEFAULT_CAPACITY : settings.capacity, settings != null && settings.isFixed)
    {
        if (settings == null)
        {
            _container = new GameObject($"{_typeName}Pool");
            MonoBehaviour.DontDestroyOnLoad(_container);
            return;
        }

        _container = settings.container;
        _original = settings.original;
        _defaultName = settings.defaultName;
        _activeWhenGet = settings.activeWhenGet;

        if (settings.isPersistant) MonoBehaviour.DontDestroyOnLoad(_container);
    }

    /// <summary>
    /// 释放对象池
    /// <para>提示:释放后对象池将无法继续使用</para>
    /// </summary>
    public void Dispose()
    {
        if (_isDisposed) return;

        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public sealed override void Clear()
    {
        if (_isDisposed) return;
        DoClear();
    }

    public sealed override void Release(T item)
    {
        if (_isDisposed) return;
        DoRelease(item);
    }

    public sealed override T Get()
    {
        if (_isDisposed) return null;
        return DoGet();
    }

    protected virtual void DoClear()
    {
        T item;
        while (_pool.Count > 0)
        {
            item = _pool.Pop();
            MonoBehaviour.Destroy(item);
        }

        totalCount = 0;
    }

    protected virtual void DoRelease(T item) { base.Release(item); }
    protected virtual T DoGet() { return base.Get(); }

    void Dispose(bool disposing)
    {
        if (_isDisposed) return;
        _isDisposed = true;

        if (disposing)
        {
            Clear();
            MonoBehaviour.Destroy(_container);
        }
    }

    ~UnityObjectPool()
    {
        Dispose(false);
    }
}

GameObjectPool.cs

using System;
using UnityEngine;

/// <summary>
/// GameObject 对象池
/// </summary>
public sealed class GameObjectPool : UnityObjectPool<GameObject>
{
    public GameObjectPool() { }
    public GameObjectPool(int capacity) : base(capacity) { }
    public GameObjectPool(int capacity, bool isFixed) : base(capacity, isFixed) { }
    public GameObjectPool(UnityObjectPoolSettings<GameObject> settings) : base(settings) { }

    protected override GameObject DoGet()
    {
        if (!_activeWhenGet) return base.DoGet();
        else
        {
            GameObject item = base.DoGet();
            item.SetActive(true);
            return item;
        }
    }

    protected override GameObject Reset(GameObject item)
    {
        GameObject v_item;
        if (overrideReset != null) v_item = overrideReset(item);
        else
        {
            v_item = item;
            v_item.SetActive(false);
        }

        if (v_item == null) throw new InvalidOperationException("Pool:The item being reset is null.");
        return v_item;
    }

    protected override GameObject Create()
    {
        if (isFixed && totalCount == capacity)
            throw new InvalidOperationException("Pool:The number of objects in the object pool has reached the upper limit.");

        GameObject item;
        if (overrideCreate != null) item = overrideCreate();
        else
        {
            if (_original == null) item = new GameObject();
            else item = MonoBehaviour.Instantiate(_original);

            if (item != null)
            {
                if (!string.IsNullOrEmpty(_defaultName)) item.name = _defaultName;
                item.transform.SetParent(_container.transform);
                item.SetActive(false);
            }
        }

        if (item == null) throw new InvalidOperationException("Pool:The item being created is null.");
        return item;
    }
}

MonoPool.cs

using System;
using UnityEngine;

/// <summary>
/// Monobehaviour 类型对象池
/// </summary>
/// <typeparam name="T">具体的 Monobehaviour 类型</typeparam>
public sealed class MonoPool<T> : UnityObjectPool<T>
where T : MonoBehaviour
{
    public MonoPool() { }
    public MonoPool(int capacity) : base(capacity) { }
    public MonoPool(int capacity, bool isFixed) : base(capacity, isFixed) { }
    public MonoPool(UnityObjectPoolSettings<T> settings) : base(settings) { }

    protected override T DoGet()
    {
        if (!_activeWhenGet) return base.DoGet();
        else
        {
            T item = base.DoGet();
            item.enabled = true;
            return item;
        }
    }

    protected override T Reset(T item)
    {
        T v_item;
        if (overrideReset != null) v_item = overrideReset(item);
        else
        {
            v_item = item;
            v_item.enabled = false;
        }

        if (v_item == null) throw new InvalidOperationException("Pool:The item being reset is null.");
        return v_item;
    }

    protected override T Create()
    {
        if (isFixed && totalCount == capacity)
            throw new InvalidOperationException("Pool:The number of objects in the object pool has reached the upper limit.");

        T item;
        if (overrideCreate != null) item = overrideCreate();
        else
        {
            if (_original == null) item = _container.AddComponent<T>();
            else item = MonoBehaviour.Instantiate(_original);

            if (item != null)
            {
                if (!string.IsNullOrEmpty(_defaultName)) item.name = _defaultName;
                item.enabled = false;
            }
        }

        if (item == null) throw new InvalidOperationException("Pool:The item being created is null.");
        return item;
    }
}

测试

using System;
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

public class ObjectPoolTest
{
    // 子弹
    public class Bullet : MonoBehaviour
    {
        public Action<Bullet> onDestroy;
        public DamageModel damageModel;

        public void OnCustomTriggerEnter(string tag)
        {
            if (tag == "Head")
            {
                Debug.Log("Attack Head:" + damageModel.AttackHead());
                onDestroy?.Invoke(this);
            }
            else if (tag == "Body")
            {
                Debug.Log("Attack Body:" + damageModel.AttackBody());
                onDestroy?.Invoke(this);
            }
        }
    }

    // 伤害计算模型
    public class DamageModel
    {
        public int damage;

        public int AttackHead()
        {
            return damage * 2;
        }

        public int AttackBody()
        {
            return damage;
        }
    }

    static readonly GameObjectPool bulletPool = new GameObjectPool();
    static readonly ClassPool<DamageModel> damagePool = new ClassPool<DamageModel>();
    static readonly string[] tags = { "Head", "Body" };

    static ObjectPoolTest()
    {
        bulletPool.overrideReset = ResetBullet;
        damagePool.overrideReset = ResetDamageModel;
    }

    static GameObject ResetBullet(GameObject go)
    {
        if (go.TryGetComponent(out Bullet bullet))
        {
            damagePool.Release(bullet.damageModel);
            bullet.damageModel = null;
            bullet.onDestroy = null;
        }
        go.SetActive(false);
        return go;
    }

    static DamageModel ResetDamageModel(DamageModel dm)
    {
        dm.damage = 0;
        return dm;
    }

    Bullet GetBullet()
    {
        GameObject go = bulletPool.Get();
        if (!go.TryGetComponent(out Bullet bullet)) bullet = go.AddComponent<Bullet>();
        DamageModel damageModel = damagePool.Get();
        damageModel.damage = UnityEngine.Random.Range(10, 100);
        bullet.damageModel = damageModel;
        bullet.onDestroy = OnBulletDestroy;
        return bullet;
    }

    void OnBulletDestroy(Bullet bullet)
    {
        Debug.Log("Bullet is being destroied.");
        bulletPool.Release(bullet.gameObject);
    }

    [UnityTest]
    public IEnumerator ObjectPool_Test()
    {
        int index = 0;
        WaitForSeconds waitForSeconds = new WaitForSeconds(0.5f);
        Stack<Bullet> temp = new Stack<Bullet>();
        while (index < 9)
        {
            Debug.Log($"正在进行第{index + 1}次射击...");

            int sendBulletCount = UnityEngine.Random.Range(1, 5);
            for (int i = 0; i < sendBulletCount; i++)
            {
                Debug.Log($"正在生成第{i + 1}颗子弹...");
                temp.Push(GetBullet());
            }

            Debug.Log($"生产子弹总量:{bulletPool.totalCount},子弹库存:{bulletPool.freeCount}");

            int j = 0;
            while (temp.Count > 0)
            {
                Debug.Log($"正在发射第{j + 1}颗子弹...");
                temp.Pop().OnCustomTriggerEnter(tags[UnityEngine.Random.Range(0, 1)]);
                j++;
            }

            yield return waitForSeconds;
            index++;
        }
        yield return null;
        Assert.IsTrue(true);
    }
}

 上述代码基于Unity Test Framework进行测试,模拟了9次射击,每次随机发射1-5颗子弹,随机设置每个子弹的基本伤害为10-100,用对象池技术管理子弹游戏对象实例和伤害计算模型实例。

分析

BasePool作为所有对象池的抽象基类,规范对象池的必要属性和方法。PoolConstant记录对象池所用的常量值。UnityObjectPoolSettings作为Unity对象池特有的对象池设置参数,在创建Unity对象池时传递。ClassPool作为C#类对象池。UnityObjectPool作为Unity对象池,继承自ClassPool。GameObjectPool作为Unity游戏对象池。MonoPool作为Monobehaviour对象池。

版本改进

......

系列文章

......

如果这篇文章对你有帮助,请给作者点个赞吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值