オブジェクトプールモードの簡単な理解と使用(Unityで実装)

1つは、オブジェクトプールの概念です。

オブジェクトプールモデルは、ゲーム開発のための独自の設計モデルではありません。その設計アイデアは、他の開発のデータベース接続プールやスレッドプールの設計アイデアと同じです。

基本的な考え方は、使用直後に削除するのではなく、プールに戻し、必要に応じて取り出すことです。オブジェクトプールモードの出現は、主に2つのポイントを最適化します。

1.オブジェクトが頻繁に作成および削除されて、メモリのジッターと頻繁なGC(ガベージコレクション)が発生するのを防ぎます

2.オブジェクトの初期化コストが高い

 

ただし、従来のソフトウェア開発のオブジェクトは通常、軽量から中程度の重量であるため、オブジェクトの割り当て/解放のオーバーヘッドはごくわずかであり、従来のソフトウェア開発には単純なオブジェクトプールアプリケーションは比較的少ないです。一般に、データベース接続プールやスレッドプールなど、特定のオブジェクトは②用に最適化されており、再利用と接続数を同時に解決します。しかし、ゲーム開発の過程では、多くのゲームオブジェクトが頻繁に作成および削除され、ゲームオブジェクトには多くのオブジェクトが含まれるため、単純なオブジェクトプールテクノロジでさえ、より多くのアプリケーションシナリオがあります。

2、オブジェクトプール操作

以下は、オブジェクトプールの基本的な操作のテキストによる紹介です。

借用:素人の言葉で言えば、プールからオブジェクトを取得することです。オブジェクトを取得するのが初めての場合は、プールを初期化する必要があります。プールにオブジェクトがもうない場合は、オブジェクトを作成します

戻り値:素人の用語では、アイテムは元々削除されるために使用されていましたが、オブジェクトプールが適用された後、プール内の数が事前設定された最大数を超えない限り、オブジェクトはプールに返されます(防止するため)爆発によるメモリが多すぎる)、プール内の数が事前設定された最大数よりも大きい場合は、直接削除します

ウォームアップ:特定の数のオブジェクトをプリロードすることを意味します。個人的には、これはオブジェクトプールの最も重要な部分の1つだと思います。予熱を行わない場合、最初にオブジェクトを作成するときに、初期化が直接含まれます。プレイヤーは、ゲームで0.1秒間ストールするのではなく、読み込みインターフェイスで1秒長く待つ方がよいことは理解しやすいです。特に競争の激しいゲームでは、プレイヤーはコンピューターを壊したいと思うでしょう(笑)。したがって、ウォームアップオブジェクトプールの最適化を行わないと、半分しか行われないと思います。

縮小:逆にウォームアップするようなものです。戻ると、設定されたしきい値を超えると、プールには戻されずに直接削除されると言われていますが、実際には、削除すると時間もかかる可能性があります。そのため、最初に削除してから、ゲームの途中で読み込みインターフェイスを通過するたびにメモリプールを削除して減らすことができます。インターフェイスをロードする前にメモリがバーストするのではないかと心配している場合は、削除する必要のある追加のしきい値を設定できます。その効果は、戻ったときに上記で記述したものと同じです。(デモではこの機能を実行しませんでした)

リセット:新しく作成された各オブジェクトは、「新規」のときに新しく作成されたオブジェクトと同じである必要があり、最後に使用された状態を明らかに持つことはできないため、オブジェクトがプールから出るたびに、後遺症で処理する必要があります。リセットを配置します。単一性では、オブジェクトの手動初期化の内容は、剛体をクリアする力などを含め、オブジェクトのOnEnable()に書き込まれます。OnEnable()とStart()の違いは、Start()がオブジェクトが最初に有効化された最初の場合のみ。フレーム実行、OnEnableは、オブジェクトが再度有効化されるたびに実行されます。

3つの特定の実験

以下は私が自分で行った小さなデモ実験です。これは小さなデモ実験なので、あまり堅牢ではありません。コメントでも表現しましたので、完全性と健全性についてはあまり文句を言わないでください。 。しかし、私のコードに主要な最適化につながる原則的な問題がある場合は、指摘して学習してください。

以下はObjectPoolのスクリプトです。これらの関数は、UnityのネイティブのInstantiate andDestroyのように見えるように設計されています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    //自身单例
    public static ObjectPool me;

    //池的存储
    //TODO 此处value使用自定义封装类型而不是单纯的queue更健全
    private Dictionary<string, Queue<GameObject>> pool;

    //每个池中最大数量
    //TODO 应该每个池设置每个池单独的数量
    private int maxCount = int.MaxValue;
    public int MaxCount
    {
        get { return maxCount; }
        set
        {
            maxCount = Mathf.Clamp(value, 0, int.MaxValue);
        }
    }

    //初始化
    void Awake()
    {
        me = this;
        pool = new Dictionary<string, Queue<GameObject>>();

    }

    /// <summary>
    /// 从池中获取物体
    /// </summary>
    /// <param name="go">需要取得的物体</param>
    /// <param name="position"></param>
    /// <param name="rotation"></param>
    /// <returns></returns>
    public GameObject GetObject(GameObject go,Vector3 position,Quaternion rotation)
    {
        //如果未初始化过 初始化池
        if(!pool.ContainsKey(go.name))
        {
            pool.Add(go.name, new Queue<GameObject>());
        }
        //如果池空了就创建新物体
        if(pool[go.name].Count == 0)
        {
            GameObject newObject = Instantiate(go, position, rotation);
            newObject.name = go.name;/*
            确认名字一样,防止系统加一个(clone),或序号累加之类的
            实际上为了更健全可以给每一个物体加一个key,防止对象的name一样但实际上不同
             */

            return newObject;
        }
        //从池中获取物体
        GameObject nextObject=pool[go.name].Dequeue();
        nextObject.SetActive(true);//要先启动再设置属性,否则可能会被OnEnable重置
        nextObject.transform.position = position;
        nextObject.transform.rotation = rotation;
        return nextObject;
    }

    /// <summary>
    /// 把物体放回池里
    /// </summary>
    /// <param name="go">需要放回队列的物品</param>
    /// <param name="t">延迟执行的时间</param>
    /// TODO 应该做个检查put的gameobject的池有没有创建过池
    public void PutObject(GameObject go,float t)
    {
        if (pool[go.name].Count >= MaxCount)
            Destroy(go,t);
        else
            StartCoroutine(ExecutePut(go,t));
    }

    private IEnumerator ExecutePut(GameObject go, float t)
    {
        yield return new WaitForSeconds(t);
        go.SetActive(false);
        pool[go.name].Enqueue(go);
    }

    /// <summary>
    /// 物体预热/预加载
    /// </summary>
    /// <param name="go">需要预热的物体</param>
    /// <param name="number">需要预热的数量</param>
    /// TODO 既然有预热用空间换时间 应该要做一个清理用时间换空间的功能
    public void Preload(GameObject go,int number)
    {
        if (!pool.ContainsKey(go.name))
        {
            pool.Add(go.name, new Queue<GameObject>());
        }
        for (int i = 0; i < number; i++)
        {
            GameObject newObject = Instantiate(go);
            newObject.name = go.name;//确认名字一样,防止系统加一个(clone),或序号累加之类的
            newObject.SetActive(false);
            pool[go.name].Enqueue(newObject);
        }
    }

}

次に、テストスクリプトGameManagerがあります

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public class GameManager : MonoBehaviour
{

    public GameObject testObject;
    // Start is called before the first frame update
    void Start()
    {
        //预热
       ObjectPool.me.Preload(testObject, 500);
    }

    //无对象池测试
    public void TestOfNotOP()
    {
        StartCoroutine(CreateOfNotOP());
    }


    private IEnumerator CreateOfNotOP()
    {
        //统计500帧所用时间
        float t = 0.0f;
        //每一帧生成一个对象,定时2秒后自动消除
        for (int i = 0; i < 500; i++)
        {
            int x = Random.Range(-30, 30);
            int y = Random.Range(-30, 30);
            int z = Random.Range(-30, 30);
            GameObject newObject=Instantiate(testObject, new Vector3(x, y, z),Quaternion.identity);
            Destroy(newObject, 2.0f);

            yield return null;
            t += Time.deltaTime;
        }
        Debug.Log("无对象池500帧使用秒数:"+t);
    }

    //使用对象池测试
    public void TestOfOP()
    {

        StartCoroutine(CreateOfOP());
    }

    private IEnumerator CreateOfOP()
    {
        //统计500帧所用时间
        float t = 0.0f;
        //每一帧生成一个对象,定时2秒后自动消除
        for (int i = 0; i < 500; i++)
        {
            int x = Random.Range(-30, 30);
            int y = Random.Range(-30, 30);
            int z = Random.Range(-30, 30);
            GameObject newObject = ObjectPool.me.GetObject(testObject, new Vector3(x, y, z), Quaternion.identity);
            ObjectPool.me.PutObject(newObject, 2.0f);
            yield return null;
            t += Time.deltaTime;
        }
        Debug.Log("使用对象池500帧使用秒数:"+t);
    }
}

Canvasに2つのボタンを作成し、GameManagerとして使用される空のGameObjectを作成し、ObjectPool(オブジェクトプールスクリプト)とGameManager(テストスクリプト)をバインドし、2つのボタンのOnClickイベントがそれぞれTestOfNotOP()とTestOfOP()をリッスンするようにしました。

最後に、リソースストアからダウンロードしたパーティクルエフェクトを使用してテストします

(詳細なテストビデオについては、私のBステーションを参照してください)

オブジェクトプールを使用しない場合、500フレームの再生(フレームごとに1つのパーティクルエフェクトが生成される)は約9.8〜9.9秒であり、オブジェクトプールを適用した後の500フレームの再生は約9.4〜9.5秒であることがわかります。まだ最適化された結果があります。古いバージョンのUnityの場合は、さらに最適化される可能性があります。これはかなり前のことですが、Yusongはhttp://www.xuanyusong.com/archives/2925についても不満を述べています。最適化する必要があります。一般に、すべての最適化手法、最適化コスト、および結果を測定する必要があります。

おすすめ

転載: blog.csdn.net/sun124608666/article/details/112602188