Unity物件池技術(原理+實戰)
阿新 • • 發佈:2019-01-28
寫在前面
很早就聽說過物件池技術……然而一直到這幾天才真正去了解= =。還得感謝Jasper Flick的部落格,這裡推薦他的Unity C# Tutorials系列,目前我只看了前幾篇,收穫還是挺大的~本篇部落格也是基於這個系列中的一篇——Object Pools,加上個人的一些理解,對Unity的物件池技術進行簡單介紹。
物件池簡介
顧名思義,物件池是存放物件的緩衝區。使用者可以從緩衝區中放入/取出物件。一類物件池存放一類特定的物件。那麼物件池有什麼用呢?在遊戲中,經常會有產生/銷燬大量同類遊戲物件的需求,比如遊戲中源源不斷的敵人、頻繁重新整理的寶箱、乃至一些遊戲特效(風、雨等)。如果沒有一種比較好的機制來管理這些物件的產生和銷燬,而是一昧的Instantiate和Destroy,將使你的遊戲效能
物件池實現
簡而言之,就是當需要使用一個物件的時候,直接從該類物件的物件池中取出(SetActive(true)),如果物件池中無可用物件,再進行Instantitate。而當不再需要該物件時,不直接進行Destroy,而是SetActive(false)並將其回收到物件池中。下面直接貼下程式碼:
PooledObject.cs
using UnityEngine;
/// <summary>
/// 所有需要使用物件池機制的物件的基類
/// </summary>
public class PooledObject : MonoBehaviour
{
// 歸屬的池
public ObjectPool Pool { get; set; }
// 場景中某個具體的池(不可序列化)
[System.NonSerialized]
private ObjectPool poolInstanceForPrefab;
/// <summary>
/// 回收物件到物件池中
/// </summary>
public void ReturnToPool()
{
if (Pool)
{
Pool.AddObject(this);
}
else
{
Destroy(gameObject);
}
}
/// <summary>
/// 返回物件池中可用物件的例項
/// </summary>
public T GetPooledInstance<T>() where T : PooledObject
{
if (!poolInstanceForPrefab)
{
poolInstanceForPrefab = ObjectPool.GetPool(this);
}
return (T)poolInstanceForPrefab.GetObject();
}
}
ObjectPool.cs
using UnityEngine;
using System.Collections.Generic;
public class ObjectPool : MonoBehaviour
{
// 池中物件prefab
private PooledObject prefab;
// 儲存可用物件的緩衝區
private List<PooledObject> availableObjects = new List<PooledObject>();
/// <summary>
/// 從池中取出物件,返回該物件
/// </summary>
public PooledObject GetObject()
{
PooledObject obj;
int lastAvailableIndex = availableObjects.Count - 1;
if (lastAvailableIndex >= 0)
{
obj = availableObjects[lastAvailableIndex];
availableObjects.RemoveAt(lastAvailableIndex);
obj.gameObject.SetActive(true);
}
else // 池中無可用obj
{
obj = Instantiate<PooledObject>(prefab);
obj.transform.SetParent(transform, false);
obj.Pool = this;
}
return obj;
}
/// <summary>
/// 向池中放入obj
/// </summary>
public void AddObject(PooledObject obj)
{
obj.gameObject.SetActive(false);
availableObjects.Add(obj);
}
/// <summary>
/// 【靜態方法】建立並返回物件所屬的物件池
/// </summary>
public static ObjectPool GetPool(PooledObject prefab)
{
GameObject obj;
ObjectPool pool;
// 編輯器模式下檢查是否有同名pool存在,防止重複建立pool
if (Application.isEditor)
{
obj = GameObject.Find(prefab.name + " Pool");
if (obj)
{
pool = obj.GetComponent<ObjectPool>();
if (pool)
{
return pool;
}
}
}
obj = new GameObject(prefab.name + " Pool");
DontDestroyOnLoad(obj);
pool = obj.AddComponent<ObjectPool>();
pool.prefab = prefab;
return pool;
}
}
實戰:七彩噴泉
【注:以下譯至前面提到的Object Pools一文,有部分刪減】
1.實現效果:
2.生成大量物體
- 首先新建指令碼Stuff.cs,程式碼如下:
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class Stuff : MonoBehaviour {
Rigidbody body;
void Awake () {
body = GetComponent<Rigidbody>();
}
}
- 建立Cube和Sphere,掛上Stuff指令碼。並將它們做成Prefab
- 接下來需要建立StuffSpawner(孵化器),並掛上StuffSpawner指令碼,程式碼如下:
using UnityEngine;
public class StuffSpawner : MonoBehaviour {
public float timeBetweenSpawns;
public Stuff[] stuffPrefabs;
float timeSinceLastSpawn;
void FixedUpdate () {
timeSinceLastSpawn += Time.deltaTime;
if (timeSinceLastSpawn >= timeBetweenSpawns) {
timeSinceLastSpawn -= timeBetweenSpawns;
SpawnStuff();
}
}
void SpawnStuff () {
Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
Stuff spawn = Instantiate<Stuff>(prefab);
spawn.transform.localPosition = transform.position;
}
}
- 現在我們有了孵化器,可以在一個點產生Cube和Sphere,但這還不夠。我們可以給這些stuff一個初始速度及方向。
public float velocity;
void SpawnStuff () {
Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
Stuff spawn = Instantiate<Stuff>(prefab);
spawn.transform.localPosition = transform.position;
spawn.Body.velocity = transform.up * velocity;
}
- 執行一下可以發現一個個物體上升又下降,周而復始。如果你傾斜一下孵化器,會讓它看上去更像流動的物體。事實上,如果我們把多個孵化器分佈在一個環上,將得到類似噴泉的效果。因此,新建一個空物體StuffSpawnerRing,掛上如下指令碼:
using UnityEngine;
public class StuffSpawnerRing : MonoBehaviour {
public int numberOfSpawners;
public float radius, tiltAngle;
public StuffSpawner spawnerPrefab;
void Awake () {
for (int i = 0; i < numberOfSpawners; i++) {
CreateSpawner(i);
}
}
}
void CreateSpawner (int index) {
Transform rotater = new GameObject("Rotater").transform;
rotater.SetParent(transform, false);
rotater.localRotation =
Quaternion.Euler(0f, index * 360f / numberOfSpawners, 0f);
StuffSpawner spawner = Instantiate<StuffSpawner>(spawnerPrefab);
spawner.transform.SetParent(rotater, false);
spawner.transform.localPosition = new Vector3(0f, 0f, radius);
spawner.transform.localRotation = Quaternion.Euler(tiltAngle, 0f, 0f);
}
- 現在將場景中的Spawner做成prefab並刪除,調整SpawnerRing的引數
3.新增銷燬區(KillZone)
- 我們現在得到了無止盡生成的下落的物體。為了防止程式卡頓,我們需要引入銷燬區。所有進入銷燬區的物體都要被銷燬。
建立一個帶有Box Collider的物體,設定為觸發器,為Collider設定一個非常大的size(如1000),並將其放置在噴泉下方某個位置。最後給該物體新增一個Tag以便能被正確識別
重新編輯Stuff.cs,新增觸發器事件處理
void OnTriggerEnter (Collider enteredCollider) {
if (enteredCollider.CompareTag("Kill Zone")) {
Destroy(gameObject);
}
}
- 看看現在的效果:
4.加入可變因素
- 目前我們的噴泉缺少隨機性,我們可以用隨機值代替固定值。因為我們要處理多個數據,所以讓我們建立一個結構體來更好地實現隨機化。
using UnityEngine;
[System.Serializable]
public struct FloatRange {
public float min, max;
public float RandomInRange {
get {
return Random.Range(min, max);
}
}
}
- 隨機化生成時間
public FloatRange timeBetweenSpawns;
float currentSpawnDelay;
void FixedUpdate () {
timeSinceLastSpawn += Time.deltaTime;
if (timeSinceLastSpawn >= currentSpawnDelay) {
timeSinceLastSpawn -= currentSpawnDelay;
currentSpawnDelay = timeBetweenSpawns.RandomInRange;
SpawnStuff();
}
}
- 隨機化物體scale和rotation
public FloatRange timeBetweenSpawns, scale;
void SpawnStuff () {
Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
Stuff spawn = Instantiate<Stuff>(prefab);
spawn.transform.localPosition = transform.position;
spawn.transform.localScale = Vector3.one * scale.RandomInRange;
spawn.transform.localRotation = Random.rotation;
spawn.Body.velocity = transform.up * velocity;
}
- 隨機化物體速度大小
public FloatRange timeBetweenSpawns, scale, randomVelocity;
void SpawnStuff () {
…
spawn.Body.velocity = transform.up * velocity +
Random.onUnitSphere * randomVelocity.RandomInRange;
}
- 隨機化物體角速度
void SpawnStuff () {
…
spawn.Body.velocity = transform.up * velocity +
Random.onUnitSphere * randomVelocity.RandomInRange;
spawn.Body.angularVelocity =
Random.onUnitSphere * angularVelocity.RandomInRange;
}
- 隨機化材質(實現七彩)
public Material[] stuffMaterials;
void CreateSpawner (int index) {
…
spawner.stuffMaterial = stuffMaterials[index % stuffMaterials.Length];
}
5.應用物件池進行管理
- 讓Stuff繼承PooledObject(PooledObject程式碼見前),修改觸發器事件,進入銷燬區時不Destroy,而是呼叫ReturnToPool方法。
- 接下來,我們需要改變StuffSpawner來讓它使用物件池來建立物件,而不是直接Instanstiate。如何做到呢?某種程度上我們需要擁有每個prefab的池,但我們不想要重複的池,也就是說所有孵化器都共享他們。當然,如果我們能直接從一個prefab得到一個池化的例項而不用考慮那些池本身將更加方便。
void SpawnStuff () {
Stuff prefab = stuffPrefabs[Random.Range(0, stuffPrefabs.Length)];
Stuff spawn = prefab.GetPooledInstance<Stuff>();
…
}
其他
- 並非所有的物件都適合使用物件池來管理。需要在“物件生成的開銷”以及“維護物件池的開銷”之間進行權衡。
- 為避免在場景切換時重新生成pool,從而帶來效能損耗,可在程式碼中加入DontDestroyOnLoad(pool)
- 同樣,在場景切換時,應該將原場景中的物件回收進相應物件池中。即在OnLevelWasLoaded方法中呼叫ReturnToPool方法