Unity でのリソース管理 - オブジェクト プール テクノロジー(3)

この記事では、Unity でのリソース管理 - オブジェクト プール テクノロジ (3) を共有します。

前回 2 回の記事で共通クラスの 2 つのオブジェクト プールの実装を学習しましたが、今回は Unity におけるGameObject (主にプレハブでインスタンス化されたオブジェクト)のオブジェクト プールを紹介します。

GameObjectPoolの必要性

ほとんどの場合、ゲームのフロントエンドの開発には、サーバーの開発のように大量のオブジェクトの作成と破棄が含まれないことがわかっていますが、一般的なコンテナーにはせいぜい数十から数百のオブジェクトが含まれます。オブジェクトの数が多すぎない パフォーマンスへのプレッシャーが大きいため、通常のクラスのオブジェクト プールはクライアント側ではあまり使用されません 一般に、リソース管理などのいくつかの機会に、膨大な数の通常のクラスの使用が必要になりますクラス オブジェクトを使用する一方で、ゲーム フロントエンドは、多数の弾丸、敵、モンスター、UI など、多数または複雑なゲーム オブジェクトの使用でより多くのオブジェクト プールを使用します。

一般にゲーム オブジェクトには表示関連のパーツが含まれるため、通常のクラス オブジェクトよりも構築と破棄の操作が複雑になり、より多くのリソースを占有し、コストが増加します。 、ゲーム オブジェクトのオブジェクト プールを使用するのが一般的な解決策です。

Unity では、テクスチャ、アトラス、シェーダー、マテリアル、サウンド、アニメーションなど、ほとんどのリソースは通常、手動でインスタンス化する必要はありません。通常、プレハブのインスタンス化を通じて使用されるため、 GameObjectのオブジェクト プールと呼ばれます。プレハブでインスタンス化されたオブジェクトのオブジェクト プール。

オブジェクトプールの最適化

最初の記事ではオブジェクト プールの実装を紹介しましたが、ゲーム オブジェクトのオブジェクト プールをさらに紹介する前に、実装を少し最適化してより一般的なものにする必要があります。

主な最適化ポイントは次のとおりです。

  • オブジェクト プールのインターフェイスを抽象化します。IObjectPool
  • オブジェクト作成の委任と各種トリガーポイントの委任を標準化し、可変長パラメータを追加

オブジェクト プール インターフェイス:IObjectPool

生成とリサイクルを、より一般化するためのインターフェイスとしてカプセル化します。

オブジェクト生成メソッドが可変長パラメーターを受け入れられるようになりました。

public interface IObjectPool<T> : IDisposable where T : class, new() {
    T Spawn(params object[] param);
    void Recycle(T obj);
}

各種手数料

デリゲートをカプセル化して公開し、可変長パラメーターを受け入れることができます。

public class ObjectPoolDelegate<T>
{
    public delegate T NewObjFunc(params object[] param);
    public delegate void ObjectPoolAction(T obj, params object[] param);
}

protected ObjectPoolDelegate<T>.ObjectPoolAction m_BeforeSpawnAction, m_AfterRecycleAction, m_AfterReleaseAction;
protected ObjectPoolDelegate<T>.NewObjFunc newObjFunc;

完全なコード

他の部分はあまり変更されていませんが、完全を期すためにコードをここに掲載しておきます。

public interface IObjectPool<T> : IDisposable where T : class, new() {
    T Spawn(params object[] param);
    void Recycle(T obj);
}

public class ObjectPoolDelegate<T>
{
    public delegate T NewObjFunc(params object[] param);
    
    public delegate void ObjectPoolAction(T obj, params object[] param);
}

public class ObjectPool<T> : IObjectPool<T>, IDisposable where T : class, new() {
    protected Stack<T> m_ObjPool;
    protected Dictionary<int, T> m_ObjDict;
    
    protected int m_MaxCount;
    protected int m_OnceReleaseCount;
    
    protected ObjectPoolDelegate<T>.ObjectPoolAction m_BeforeSpawnAction, m_AfterRecycleAction, m_AfterReleaseAction;
    protected ObjectPoolDelegate<T>.NewObjFunc m_NewObjFunc;
    
    public ObjectPool(int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10,
        ObjectPoolDelegate<T>.ObjectPoolAction beforeSpawnAction = null, 
        ObjectPoolDelegate<T>.ObjectPoolAction afterRecycleAction = null, 
        ObjectPoolDelegate<T>.ObjectPoolAction afterReleaseAction = null, 
        ObjectPoolDelegate<T>.NewObjFunc newObjFunc = null) {
        
        m_ObjPool = new Stack<T>(initCapacity);
        m_ObjDict = new Dictionary<int, T>(initCapacity);
        
        m_MaxCount = maxCount;
        m_OnceReleaseCount = onceReleaseCount;
        
        m_BeforeSpawnAction = beforeSpawnAction;
        m_AfterRecycleAction = afterRecycleAction;
        m_AfterReleaseAction = afterReleaseAction;
        m_NewObjFunc = newObjFunc;
    }

    public void Dispose() {
        if (m_AfterReleaseAction != null) {
            var array = m_ObjDict.Select(s=>s.Value);
            foreach(var obj in array) {
                m_AfterReleaseAction(obj);
            }
        }
        
        m_ObjPool.Clear();
        m_ObjDict.Clear();

        m_BeforeSpawnAction = m_AfterRecycleAction = m_AfterReleaseAction = null;
        m_NewObjFunc = null;
    }
    
    /// <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();
            Release(obj);
        }
        
        Debug.Log("[ReleaseOverflowObj] 当前池中数量: " + m_ObjPool.Count);
    }

    public T CreateObj(params object[] param) {
        var obj = m_NewObjFunc != null ? m_NewObjFunc(param) : new T();
        m_ObjDict.Add(obj.GetHashCode(), obj);
        return obj;
    }
    
    public bool Release(T obj) {
        m_ObjDict.Remove(obj.GetHashCode());
        m_AfterReleaseAction?.Invoke(obj);
        return true;
    }

    public int Count() {
        return m_ObjPool.Count;
    }

    public T Spawn(params object[] param) {
        var obj = m_ObjPool.Count <= 0 ? CreateObj(param) : m_ObjPool.Pop();
        m_BeforeSpawnAction?.Invoke(obj, param);

        return obj;
    }

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

        if (m_ObjPool.Contains(obj)) return;
        
        m_ObjPool.Push(obj);
        m_AfterRecycleAction?.Invoke(obj);
    }
    
    public void Dispose() {
        if (m_AfterReleaseAction != null) {
            var array = m_ObjDict.Select(s=>s.Value);
            foreach(var obj in array) {
                m_AfterReleaseAction(obj);
            }
        }

        m_ObjPool.Clear();
        m_ObjDict.Clear();

        m_BeforeSpawnAction = m_AfterRecycleAction = m_AfterReleaseAction = null;
        m_NewObjFunc = null;
    }
}

ゲームオブジェクトプール

ゲーム オブジェクトのオブジェクト プールには、通常のオブジェクト プールと比較して独自の特徴があります。

  • 保存されるオブジェクトのタイプは次のとおりです。GameObject

  • より多くの状況に対応するには、オブジェクト作成のデリゲートを設け、オブジェクト作成の作業を外部に引き渡す必要がある

  • オブジェクトのマウント ポイントを指定する必要があります ( Transform)。マウント ポイントはデフォルトで非表示になります ( SetActive(false))。オブジェクト プール内のオブジェクトがその下にマウントされると、デフォルトで非表示になり、再度呼び出す必要はありません。SetActive(false)パフォーマンスを向上させるために

  • オブジェクト プール インターフェイスを実装し、オブジェクト プールを継承する代わりに共通のオブジェクト プールを保持します。

  • 各デリゲートでゲーム オブジェクトに関連するいくつかの基本操作 (位置/回転の設定、親ノードの設定、オブジェクトの破棄など) を実行します。

詳細については、次のコードとコメントを参照してください。

using NewObjFunc = ObjectPoolDelegate<GameObject>.NewObjFunc;
using ObjectPoolAction = ObjectPoolDelegate<GameObject>.ObjectPoolAction;

// 实现对象池接口, 使用聚合的方式实现, 降低依赖性
public class GameObjectPool : IObjectPool<GameObject> {
    // 持有一个普通的对象池
    private ObjectPool<GameObject> m_ObjectPool;
    
    // 挂载点
    private Transform m_PoolParent;

    // 创建对象的委托
    private NewObjFunc m_NewObjFunc;
    
    // 产生之前, 回收之后, 释放之后的委托
    private ObjectPoolAction m_BeforeSpawnAction, m_AfterRecycleAction, m_AfterReleaseAction;

    //---------------------------------------------
    // 公开委托属性供外部调用
    public ObjectPoolAction beforeSpawnAction {
        set => m_BeforeSpawnAction = value;
    }

    public ObjectPoolAction afterRecycleAction {
        set => m_AfterRecycleAction = value;
    }

    public ObjectPoolAction afterReleaseAction {
        set => m_AfterReleaseAction = value;
    }
    //---------------------------------------------

    // 只提供子类使用, 延迟对象池的实例化
    protected GameObjectPool() {

    }

    // 封装对象池实例化和初始化
    protected void InitPool(NewObjFunc newObjFunc, Transform poolParent, int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        Assert.IsNotNull(newObjFunc, "实例化对象函数不能为空!");
        Assert.IsNotNull(poolParent, "挂载节点不能为空!");

        m_PoolParent = poolParent;
        m_NewObjFunc = newObjFunc;

        // 不可见挂载点
        m_PoolParent.gameObject.SetActive(false);
        
        // 持有普通对象池, 并劫持各种委托
        m_ObjectPool = new ObjectPool<GameObject>(initCapacity, maxCount, onceReleaseCount, BeforeSpawn, AfterRecycle, AfterRelease, NewObj);
    }

    public GameObjectPool(NewObjFunc newObjFunc, Transform poolParent, int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        InitPool(newObjFunc, poolParent, initCapacity, maxCount, onceReleaseCount);
    }

    //---------------------------------------------
    // 对象池接口, 直接使用持有的对象池接口
    public GameObject Spawn(params object[] param) {
        var obj = m_ObjectPool.Spawn(param);
        return obj;
    }

    public void Recycle(GameObject obj) {
        m_ObjectPool.Recycle(obj);
    }

    // 清理委托和卸载对象池
    public virtual void Dispose() {
        m_ObjectPool?.Dispose();

        m_NewObjFunc = null;
        m_AfterReleaseAction = null;
    }
    //---------------------------------------------

    public GameObject CreateObj(params object[] param) {
        var obj = m_ObjectPool.CreateObj(param);
        return obj;
    }

    public bool Release(GameObject obj) {
        return m_ObjectPool.Release(obj);
    }

    // --------------------------------------------------------------------------------
    // 解析可变长参数, 依次为: 父节点, 位置, 旋转 
    protected void ParseParams(out Transform parent, out Vector3 pos, out Quaternion quaternion, params object[] param) {
        parent = m_PoolParent;
        pos = Vector3.zero;
        quaternion = Quaternion.identity;

        if (param != null) {
            if (param.Length > 0)
                parent = (Transform)param[0];

            if (param.Length > 1)
                pos = (Vector3)param[1];

            if (param.Length > 2)
                quaternion = (Quaternion)param[2];
        }
    }

    //---------------------------------------------
    // 对象池接口, 直接使用持有的对象池接口
    private GameObject NewObj(params object[] param) {
        ParseParams(out var parent, out var pos, out var quaternion, param);

        var obj = m_NewObjFunc(parent, pos, quaternion);
        return obj;
    }

    // 产生之前初始化
    protected virtual void BeforeSpawn(GameObject obj, params object[] param) {
        ParseParams(out var parent, out var pos, out var quaternion, param);

        var transform = obj.transform;
        transform.parent = parent;
        transform.position = pos;
        transform.rotation = quaternion;

        m_BeforeSpawnAction?.Invoke(obj, param);
    }

    // 回收后挂载不可见节点
    protected virtual void AfterRecycle(GameObject obj, params object[] param) {
        obj.transform.parent = m_PoolParent;

        m_AfterRecycleAction?.Invoke(obj, param);
    }

    // 销毁对象
    protected virtual void AfterRelease(GameObject obj, params object[] param) {
        Object.Destroy(obj);

        m_AfterReleaseAction?.Invoke(obj, param);
    }
    //---------------------------------------------
}

コードは比較的単純なので、ここでは繰り返しません。

プレハブオブジェクトプール

ゲーム オブジェクトのオブジェクト プールを使用して、プレハブ オブジェクトのオブジェクト プールを実装できます (ほとんどの場合、プレハブ オブジェクトのオブジェクト プールを使用します)。

プレハブは非常に特別な存在です。それは単なる永続的なゲーム オブジェクトですが、さまざまなコンポーネントをマウントし、他のさまざまなゲーム オブジェクトに対応できるため、さまざまなゲームの理解を大幅に簡素化します。オブジェクトのメンテナンスは、基本的にほとんどの場合、支払う必要があるだけです。プレハブとそのインスタンス化されたオブジェクトに注目してください。

ここでは、プレハブ オブジェクト自体がゲーム オブジェクトの一種であるため、継承を使用して実装します。わずかな違いは、オブジェクトを構築するときにプレハブからインスタンス化されることです。

public class PrefabObjectPool : GameObjectPool {
    private GameObject m_Prefab;

    public PrefabObjectPool(GameObject prefab, Transform poolParent, int initCapacity = 5, int maxCount = 500, int onceReleaseCount = 10) {
        Assert.IsNotNull(prefab, "预制不能为空!");
        Assert.IsNotNull(poolParent, "挂载节点不能为空!");

        m_Prefab = prefab;
        InitPool(NewObj, poolParent, initCapacity, maxCount, onceReleaseCount);
    }

    // --------------------------------------------------------------------------------
    private GameObject NewObj(params object[] param) {
        ParseParams(out var parent, out var pos, out var quaternion, param);

        var obj = Object.Instantiate(m_Prefab, pos, quaternion, parent);
        return obj;
    }
}

GameObjectPoolでは、デフォルトのコンストラクターを提供し、初期化メソッドとして保持される共通オブジェクト プールのインスタンス化と初期化をカプセル化するため、実装が非常に簡単になることに注意してくださいPrefabObjectPool

PrefabObjectPoolプレハブ オブジェクトを受け入れ、新しいオブジェクトの作成時にプレハブからインスタンス化するオブジェクト作成メソッドをオーバーライドします。

上記の内容であれば普通に使えます。

使用例

public class ObjectPoolTest : MonoBehaviour {
    public Button spawnButton;
    public Button recycleButton;

    private GameObject m_PrefabPool;
    private GameObjectPool m_Pool;
    private Dictionary<int, GameObject> m_Objects = new Dictionary<int, GameObject>();

    private void Awake() {
        // ObjectPoolTest2.Test();
        var parentTrans = new GameObject("parentTrans");
        var prefabPool = new GameObject("PrefabPool");
        m_PrefabPool = prefabPool;

        var asset = AssetDatabase.LoadAssetAtPath<GameObject>("obj.prefab");

        var objectPool = new PrefabObjectPool(asset, prefabPool.transform, 10, 10, 2);
        // var objectPool = new GameObjectPool(s => {
        //     var obj = Instantiate(asset);
        //     return obj;
        // }, prefabPool.transform, 10, 10, 2);

        m_Pool = objectPool;
        objectPool.afterReleaseAction = OnObjectRelease;

        if (spawnButton != null) {
            var count = 0;
            spawnButton.onClick.AddListener(() => {
                var obj = m_Pool.Spawn(parentTrans.transform);
                m_Objects.Add(obj.GetHashCode(), obj);

                obj.name = "Obj: " + count++;
            });
        }

        if (recycleButton != null) {
            recycleButton.onClick.AddListener(() => {
                var obj = m_Objects.FirstOrDefault();
                if (obj.Value != null) {
                    m_Pool.Recycle(obj.Value);
                    m_Objects.Remove(obj.Key);
                }
            });
        }
    }

    private void OnObjectRelease(GameObject obj, params object[] param) {
        m_Objects.Remove(obj.GetHashCode());
    }

    private void OnDestroy() {
        if (m_PrefabPool != null) {
            Destroy(m_PrefabPool);
            m_PrefabPool = null;
        }

        m_Pool.Dispose();
    }
}

ロジックは非常に単純で、クリックしてSpawnオブジェクトを生成し、それを使用し、クリックしてRecycleオブジェクトをリサイクルします。

ここに画像の説明を挿入
ここに画像の説明を挿入

要約する

今日はゲーム オブジェクトのオブジェクト プールを紹介しましたが、後でオブジェクト プール マネージャーを実装して複数のオブジェクト プールを管理することもできます。これは、リソース管理ソリューションの最終セットで再び導入される予定です。

今日は著者が大量のコードを投稿して申し訳ありません。実際、オブジェクト プールの基本理論は前の 2 つの記事で詳しく紹介されています。この記事ではインフラストラクチャ上のアプリケーションについて詳しく説明します。

オブジェクト プールの中心的な概念は非常にシンプルですが、それをうまく使用するのは簡単ではありません。私たちはオブジェクト プール テクノロジをリソース管理にも広く使用し、徐々に誰もがそれに慣れるようになるでしょう。

次回は、もう一つのよく使われる技術である引用カウントについて紹介します。多くの学生がこれに触れたことがあると思います。著者はその基礎理論を整理し、その応用を紹介します。興味のある学生は引き続き注目してください。 。

さて、今日の内容は以上です、皆さんのお役に立てれば幸いです。

おすすめ

転載: blog.csdn.net/woodengm/article/details/122003136