物件池與享元模式

被迫吃冰淇淋的小学生發表於2024-03-15

在我人生的第一次面試中,面試官問我:

可以舉幾個例子嗎?關於Unity中可能用到的設計模式。

我當時對設計模式的學習並沒有很深入,因此回答了:物件池應該是享元模式。

但很快遭到了他的否決,我再次提議,應該是享元,但他還是搖了搖頭。

畢竟是公司主程,我只能低下頭表示,自己會回去看看。

在9個月之後的下午,公司專案中,我被分到的功能,需要使用塵封已久的物件池,我就心血來潮寫下來這篇文章

來糾正我當時的錯誤

關於享元模式與物件池的區別

我比較喜歡舉例子,其實兩者的區別很大

區別一:物件的使用

比如-127~128的int,如果是享元模式,那當我們去取"0"的時候,就是把原原本本的0給你了;

如果是物件池,那當我們去取"0"的時候,會去池中看看我還有沒有多餘的"0",有就給你,沒就new一個

區別二:物件的狀態(內部與外部)

這就沒有簡單的例子了

比如租房,房子是內部,是不變的,中介是外部的,是你去租房時,傳給房子的引數

如果是享元模式,當我們帶笨笨中介去房源時,就會得到1000的房租,而狡猾中介是2333
當然內部資料也是能修改的,比如我給房子刷上超級油漆,那帶不同外部中介去,就會得到不同房租

而物件池,你帶中介去,他說房源有人正在看,給你現場造個房子,全是外部資料的影子

區別三:根本就不是一種存在

區別二里也看得出來,是不可能去現場造房子的

享元模式是一種資料處理用的模式,屬於結構型模式,注重外部物件與內部物件之間的銜接。

物件池是一種物件使用方式,即構造型模式,注重物件的使用

相同點

都是為了最佳化記憶體而生的

3/15補充

今天又看到了一個好玩的例子

比如我這有一片樹林,我準備建立一棵樹。

如果是物件池,我會從我的倉庫中拿出一顆樹,更改它的屬性後放在這。如果我的倉庫中沒有我想要的樹先建立一棵樹再拿出來。

如果是享元模式,我會找到樹林的母樹,再照著母樹一顆樹。
這時我之後的行為可分為兩種:
一種是類似於使用不同顏色的畫筆畫樹,以此得到不同的紙片(改變外部屬性),
一種是類似於砍倒這棵母樹(影響內部屬性或改變行為)。
進行前者時一切正常,只有我操作的物體才會受到影響,
而後者則會讓後來者再畫樹時,得到完全不一樣的圖案。

物件池原始碼

物件池單例類

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

interface IResetable
{
    void OnReset();
}
/*
 * 使用方法
 * 1.	所有頻繁建立回收的物體,用物件池建立回收
 *      GameObjectPool.Instance.CreatObject("型別", 物件, 位置, 旋轉);
 *      GameObjectPool.Instance.CollectObject(物件);
 * 2.	需要建立物件時執行,如果每次建立都需要對物件進行初始化,那就讓物件實現IResetable介面。
 */
public class GameObjectPool : MonoSingleton<GameObjectPool>
{
    private Dictionary<string, List<GameObject>> cache;

    //初始化
    public override void Init()
    {
        base.Init();
        cache = new Dictionary<string, List<GameObject>>();
    }

    /// <summary>
    /// 為場景顯示物體
    /// </summary>
    /// <param name="key">物體類別</param>
    /// <param name="prefab">物體預製體</param>
    /// <param name="pos">建立時位置</param>
    /// <param name="rotate">建立時角度</param>
    /// <returns></returns>
    public GameObject CreatObject(string key, GameObject prefab, Vector3 pos, Quaternion rotate)
    {
        GameObject go = FindUsableGO(key);
        if (go == null) go = AddObject(key, prefab);
        //設定物體並return
        go.transform.position = pos;
        go.transform.rotation = rotate;
        go.SetActive(true);
        foreach (var item in go.GetComponents<IResetable>())
        {
            item.OnReset();
        }
        return go;
    }

    //找到可用的物體
    private GameObject FindUsableGO(string key)
    {
        if (cache.ContainsKey(key))
            return cache[key].Find(g => !g.activeInHierarchy);
        return null;
    }

    //新增物體
    private GameObject AddObject(string key, GameObject prefab)
    {
        GameObject go = Instantiate(prefab);
        if (!cache.ContainsKey(key)) cache.Add(key, new List<GameObject>());
        cache[key].Add(go);
        return go;
    }

    /// <summary>
    /// 回收物體至物件池
    /// </summary>
    /// <param name="go">物體</param>
    /// <param name="delay">延遲時間</param>
    public void CollectObject(GameObject go, float delay = 0)
    {
        go.SetActive(false);
    }

    /// <summary>
    /// 移除該類別
    /// </summary>
    /// <param name="key">類別</param>
    public void Clear(string key)
    {   //如果用for,記得從後往前刪,list會自動補上
        foreach (GameObject item in cache[key])
        {
            Destroy(item);
        }
        cache.Remove(key);
    }

    /// <summary>
    /// 移除物件池所有物體
    /// </summary>
    public void ClearAll()
    {
        //foreach 只讀元素
        //遍歷 字典 集合
        //foreach (KeyValuePair<string, List<GameObject>> item in cache)
        //{//異常:無效操作(因為把元素刪了,IEnumerable需要用被刪的那個元素去MoveNext())
        //    Clear(item.Key);//刪除了字典記錄,cache.Remove(key)
        //}

        //做法就是建一個新的,和刪掉的元素不相干
        List<string> keys = new List<string>(cache.Keys);//之所以能賦值,是因為他們都有IEnumerable
        foreach (string item in keys)
        {
            Clear(item);//刪了字典裡的key,列表裡的key沒動。
        }
    }
}

相關文章