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についても不満を述べています。最適化する必要があります。一般に、すべての最適化手法、最適化コスト、および結果を測定する必要があります。