1. 程式人生 > >Unity 遊戲框架:資源管理神器 ResKit

Unity 遊戲框架:資源管理神器 ResKit

此篇文章準備了將近兩週的時間,寫了改,改了刪。之前有朋友反饋,上一個文章太冗長了,影響閱讀體驗,這一講就走個精簡路線。所以只要不是很重要的內容就都刪減掉了。

文章分兩個部分,第一部分是原理,第二部分是實戰。

原理部分,初學者儘量去理解就好,不用跟著敲,列出的程式碼都是示意程式碼。

實戰部分是留給初學者的,希望敲完程式碼,這樣有助於理解前邊的原理。

當然原理不是很難。

第一部分:原理

ResKit 中值得一說的 Feature 如下:

  • 引用計數器
  • 可以不傳 BundleName

當然也有很多不值得一提的 Feature。

  • 比如統一的載入 API,一個 ResLaoder.LoadSync API 可以載入 Resources/AssetBundle/PersistentDataPath/StreamingAssets,甚至是網路資源。這個很多開源方案都有實現。

  • Simulation Mode,這個功能最早源自於 Unity 官方推出的 AssetBundleManager,不過不維護了,被 AssetBundleBrowser 替代了。但是這個功能真的很方便就加入到 ResKit 裡了。

  • 物件池,是管理記憶體的一種手段,在 ResKit 中作用不是很大,但是是一個非常值得學習的優化工具,所以也列到本次 chat 裡了。

接下來就按著以上順序聊聊這些 Feature。

首先先看下 ResKit 示例程式碼。

// 在載入 AssetBundle 資源之前,要進行一次(只需一次) 初始化操作
ResMgr.Init();

// 申請一個載入器物件(從物件池中)
var loader = ResLoader.Allocate<ResLoader>();

// 載入 prefab
var smObjPrefab = loader.LoadSync<GameObject>("smObj");

// 載入 Bg
var bgTexture = loader.LoadSync<Texture2D>("Bg");

// 通過 AssetBundleName 載入 logo
var logoTexture = loader.LoadSync<Texture2D>("hometextures","logo");

// 當 GameOject Destroy 時候進行呼叫,這裡進行一個資源回收操作,會將每個引用計數器減一。
loader.Recycle2Cache();
loader = null;

最先登場的是 ResMgr。

ResMgr.Init() 進行了讀取配置操作。在遊戲啟動或者是熱更新完成之後可以呼叫一次。

第二登場的是 ResLoader。字如其意,就是資源載入器。使用者大部分時間都是和它打交道。

ResLoader.LoadSync 預設載入 AssetBundle 中的資源。

如果想載入 Resources 目錄下的資源,程式碼如下所示:

var someObjPrefab = loader.LoadSync<GameObject>("resources://someObj");

當然也可以擴充套件成支援載入網路資源等。

這裡關鍵的一點是 resources://

這個字首。和 https://http:// 類似,是一個路徑,和 wwwfile:// 的原理是一樣的。

實現比較簡單。核心程式碼如下:

        public static IRes Create(string assetName)
        {
            short assetType = 0;
            if (assetName.StartsWith("resources://"))
            {
                assetType = ResType.Internal;
            }
            else if (assetName.StartsWith("NetImage:"))
            {
                assetType = ResType.NetImageRes;
            }
            else if (assetName.StartsWith("LocalImage:"))
            {
                assetType = ResType.LocalImageRes;
            }
            else
            {
                var data = ResDatas.Instance.GetAssetData(assetName);
                if (data == null)
                {
                    Log.E("Failed to Create Res. Not Find AssetData:" + assetName);
                    return null;
                }

                assetType = data.AssetType;
            }

            return Create(assetName, assetType);
        }

這裡除了支援 resources:// 還支援網路圖片 NetImage: 和本地圖片 LocalImage: 的載入。

同樣也可以支援自定義格式,通過特殊的字首去決定資源如何載入、從哪裡載入、非同步還是同步等等,一個靈活的系提供應該如此。這裡不多說,文章的下半部分的實戰環節仔細講解。

接下來說一個比較重要的概念,引用計數。

雖然在以上的程式碼中看不出來引用計數的影子,但是他是整個 ResKit 的核心。

先進行一個簡單的入門。貼上筆者的專欄:Unity 遊戲框架搭建 (二十二) 簡易引用計數器。文章不長,足夠入門。

在講引用計數在 ResKit 中的應用之前,還要再重新介紹一次 ResMgr。

在開頭介紹了 ResMgr.Init 進行了讀取配置操作。配置檔案中則記錄的是 AssetBundle 和 Asset 資訊。

那 ResMgr 的職責是什麼呢?

字如其意就是管理資源。

以筆者的習慣,稱為 XXXMgr 或者 YYYManager 的都是一個容器,叫容器是因為裡邊維護了 List 或 Dictionary 等,用來對外提供查詢 API 和進行一些簡單的邏輯處理。

這裡的 ResMgr 就是 Res 的容器。Res 是一個類,它持有一個真正的資源物件(UnityEngine.Object)。有的 Res 是從 Resources 里加載進來的,在 ResKit 中叫做 InternalRes。有的 Res 從 AssetBundle 里加載進來的,這裡叫做 AssetRes/AssetBundleRes。它們都繼承一個共同的父類 Res,之間的區別只是載入的方式不同。

程式碼如下:InternalRes.cs

public class InternalRes : Res
{
    ...
    public void LoadSync()
    {
        mAsset = Resources.Load(AssetName);    
    }
    ...
}

AssetRes.cs

public class AssetRes : Res
{
    ...
    public void LoadSync()
    {
        mAsset = mAssetBundle.LoadAsset(AssetName);
    }
    ...
}

這裡每個 Res 都實現了引用計數器。那麼在什麼時候進行引用計數+1(Retain)操作呢?

ResMgr 對 ResLoader 提供一個 GetRes API,當 ResLoader 呼叫它的時候會對獲取的 Res 進行 Retain 操作。

這裡瞭解下 ResLoader 載入一個資源的步驟就知道了。

var smObjPrefab = loader.LoadSync<GameObject>("smObj");

以上這步操作做了一下事情:

  1. 判斷 loader 裡的 List<Res> 中有沒有載入過 Name 為 "smObj" 的 Res,如果載入過直接返回這個資源。
  2. 接著通過 GetRes 這個 API 從 ResMgr 獲取這個資源,獲取之後對這個 Res 進行 Retain 操作,之後將 Res 儲存到 loader 的 List<Res> 中,之後再返回給使用者。
  3. 在 ResMgr.GetRes 中,先判判斷容器中有沒有該 Res,如果沒有就建立一個出來加入到容器中,再返回給 ResLoader。

理解起來不難,程式碼如下:

ResLoader.cs

    public class ResLoader
    {       
        /// <summary>
        /// 持有的
        /// </summary>
        private List<Res> mResList = new List<Res>();

        public T Load<T>(string assetName, string assetBundleName = null) where T : Object
        {
            var loadedRes = mResList.Find(loadedAsset => loadedAsset.Name == assetName);

            if (loadedRes != null)
            {
                return loadedRes as T;
            }

            loadedRes = ResMgr.GetRes(assetName, assetBundleName);

            mResList.Add(loadedRes);

            return loadedRes.Asset as T;

        }
        ...
    }

ResMgr.cs

    public class ResMgr
    {
        /// <summary>
        /// 共享的 
        /// </summary>
        public static List<Res> SharedLoadedReses = new List<Res>();

        
        public static Res GetRes(string resName, string assetBundleName)
        {
            var retRes = SharedLoadedReses.Find(loadedAsset => loadedAsset.Name == resName);

            if (retRes != null)
            {
                retRes.Retain();

                return retRes;
            }

            if (resName.StartsWith("resources://"))
            {
                retRes = new ResourcesRes(resName);
            }
            else
            {
                retRes = new AssetRes(resName, assetBundleName);
            }

            retRes.Load();

            SharedLoadedReses.Add(retRes);

            retRes.Retain();

            return retRes;
        }
        ...
    }

到這裡,總結下:

  • ResLoader 中有個 ResList,用來快取從 ResMgr 中獲取的 Res。也就是使用者載入過的資源都會在 ResLoader 中快取一次,這裡快取的當然只是這個物件,並不是真正的資源。使用者可以同 Res 物件訪問真實的資源。
  • ResMgr 中有個 SharedLoadedReses,用來儲存共享的 Res。
  • 所以 ResMgr 中的 Res 會共享給,每個 ResLoader。
  • ResLoader 是 ResMgr 的一個分身,只不過每個分身從 ResMgr 獲取資源,都會對資源進行 Retain 操作。

什麼時候進行資源的 Release 操作呢?

就是當 ResLoader 呼叫 Recycle2Cache 時:

public class ResLoader
{
    ...
    public void Recycle2Cache()
    {
        foreach (var asset in mResList)
        {
            asset.Release();
        }
        
        mResList.Clear();
        mResList = null;
    }
}

程式碼比較簡單了。就是遍歷 List<Res>,對每個 Res 進行一個 Release 操作。當 每個資源引用計數 Release 到 0 的時候會呼叫對應的 Unload 操作。各個 Res 子類之間的 Unload 的區別 LoadSync 一樣。這裡不介紹了。

下面介紹下這種設計的優點。

ResKit 中提倡每個需要動態載入的 MonoBehaviour 或者單元都申請一個 ResLoader 物件。當該 MonoBehaviour 進行 OnDestroy 時,呼叫 Recycle2Cache。

比較理想的使用方式如下:

public class UIHomePanel : MonoBehaivour
{
    private ResLoader mResLoader = ResLoader.Allocate();
    
    void Start()
    {
        var someObjPrefab = mResLoader.LoadSync<GameOBject>("someObj")
        
        // 載入 Bg
        var bgTexture = mResLoader.LoadSync<Texture2D>("Bg");

        // 通過 AssetBundleName 載入 logo
        var logoTexture = mResLoader.LoadSync<Texture2D>("hometextures","logo");
    }
    
    void OnDestroy()
    {
        mResLoader.Recycle2Cache();
        mResLoader = null
    }
}

在該 Panel Destroy 時,進行資源的引用計數減一操作。這裡載入過的資源並不一定真正進行解除安裝,而是當引用計數減為 0 時才進行真正的解除安裝操作。這個好處非常明顯了,就是減少大腦負荷,不用記住某個資源在那裡載入過,需要在哪裡進行真正的解除安裝。主要保證每個 ResLoader 的申請,都對應一個 Recycle2Cache 就好。

這就是引用計數器的力量。引用計數的應用先說到這裡。

下面說說,不用傳入 AssetBundleName 就能載入資源這個功能。

目前市面上大部分開源資源管理模組都不支援這個功能,當然筆者不是原創。原創是筆者的前同事,給出原創 git 連結:Qarth

我們一般的方案, 每個從 AssetBundle 載入的資源,必須傳入 AssetBundleName。

例如:

ResourcesManager.LoadSync<Texture2D>("images","bg");
AssetBundleManager.LoadSync<Texture2D>("images","bg");

這是常見的解決方案。實現起來和原理也比較簡單。而且它已經足夠解決大部分問題了。

但是用的時候會有一點限制。

在一個專案的初期,美術的資源還沒有全部給出。專案中美術資源相關的目錄結構還不是最終版。這時候載入方式全部像上邊一樣寫。

在整個專案週期中,目錄結構發生了幾次變化。每次變化資源就會被打進不同的 AssetBundle 中,導致都要更改一次載入資源相關的程式碼,可能改成如下:

ResourcesManager.LoadSync<Texture2D>("homes","bg");
AssetBundleManager.LoadSync<Texture2D>("homes","bg");

而字串的更改往往比較麻煩,載入錯誤不會被編譯器識別,只有當專案執行之後才可能發現。這就會造成大量人力浪費。

而 ResKit 初期就處於這個階段。

當時的解決方案是程式碼生成。生成 Bundle 名字常量和各個資源常量。這樣當一發生目錄結構的改變,就會報出來很多編譯錯誤。這樣生成的編譯錯誤一個一個地改就好了。

生成程式碼如下:

namespace QAssetBundle
{
    
    public class Assetobj_prefab
    {
        public const string BundleName = "ASSETOBJ_PREFAB";
        public const string ASSETOBJ = "ASSETOBJ";
    }
    public class Gamelogic
    {
        public const string BundleName = "GAMELOGIC";
        public const string BGLAYER = "BGLAYER";
        public const string DEATHZONE = "DEATHZONE";
        public const string LANDEFFECT = "LANDEFFECT";
        public const string MAGNETITELAYER = "MAGNETITELAYER";
        public const string PLAYER = "PLAYER";
        public const string RAINPREFAB2D = "RAINPREFAB2D";
        public const string STAGELAYER = "STAGELAYER";
        public const string SUN = "SUN";
    }
    public class Gameplay_prefab
    {
        public const string BundleName = "GAMEPLAY_PREFAB";
        public const string GAMEPLAY = "GAMEPLAY";
    }
    ...
}

初期 ResKit 以這個方案減少了很多工作量,和專案風險。

關於只傳入 AssetName 不傳 AssetBundleName 支援的構想在很早就有了,只不過不敢確定到底可不可行,會不會造成效能上的瓶頸什麼的,直到後來發現了 Qarth,確定是可行的方案。

實現非常簡單,只要生成一個配置表就夠了。

配置檔案如下:

AssetTable.json

{
    "AssetBundleInfos": [
        {
            "AssetInfos": [
                {
                    "OwnerAssetBundleName": "images",
                    "AssetName": "Square"
                },
                {
                    "OwnerAssetBundleName": "images",
                    "AssetName": "SquareA"
                },
                {
                    "OwnerAssetBundleName": "images",
                    "AssetName": "SquareB"
                }
            ],
            "AssetBundleName": "images"
        }
    ]
}

有了這個檔案,就可以根據 AssetName 查詢對應的 AssetBundleName 就好了。

這樣直到專案優化階段會很少去進行程式碼的改動,畢竟專案開發的每一分鐘都很寶貴。

使用這種方式會有一些限制,就是資源不能同名。同名時就會載入第一個查詢到的資源和對應的 AssetBundle。

當發生不同 AssetBundle 之間有相同資源時,只要傳入 AssetBundleName 就可以解決。這種操作在日常開發中佔極少數。多語言包可能是一種常見的需求。

還有一種是不同型別的資源同名,比如 prefab 型別 和 TextureD 型別的資源使用了同一個名字,這裡筆者想了幾種解決方案:

1. 可傳入副檔名。根據副檔名決定載入哪個資源,比如:

AssetBundleManager.LoadSync<Texture2D>("bg.jpg");
AssetBundleManager.LoadSync<GameObject>("bg.prefab");

實現起來也是比較容易的事情。

2. 根據傳入泛型型別去判斷,比如:

mResLoader.LoadSync<Texture2D>("bg"); // 載入 紋理型別的,名為 bg 的資源。
mResLoader.LoadSync<GameObject>("bg"); // 載入 GameObject 型別的,名為 bg 的資源。

實現也是比較簡單。

以上兩種都需要在生成配置階段,對每個資源生成更多的資訊。

還有一種是 Qarth 的方案。

就是在對每個 AssetBundle,都加入了啟用和未啟用兩個狀態。

示意程式碼如下。

ResMgr.ActivateAssetBundle("images");
mResLoader.LoadSync<Texture2D>("bg"); 
ResMgr.DectivateAssetBundle("images");

ResMgr.ActivateAssetBundle("home");
mResLoader.LoadSync<GameObject>("bg"); 
ResMgr.ActivateAssetBundle("home");

這也是一個非常不錯的方案。

目前以上三種 ResKit 都沒有支援,筆者也在糾結當中,也許三種都支援,也許只支援 1 ~ 2 種方式。可能會有更好的,隨著 QFramework 發展慢慢來吧。

值得一說的 Features 都聊完了。

接下來開始實踐部分,如果對原理部分有問題歡迎在文章下邊留言探討,或者私聊我也行,聯絡方式在 QFramework 主頁上能找到。

第二部分:實踐

讓我們先一切歸零。下面筆者展示資源管理模組的演化過程。

v0.0.1 Resources API 入門

public class UIHomePanel: MonoBehaviour
{
    private Texture2D mBgTexture = null;
    
    void Start()
    {
        mBgTexture = Resources.Load<Texture2D>("bg");
        // do something
    }
    
    void OnDestroy()
    {
        Resources.UnloadAsset(mBgTexture);
        mBgTexture = null;
    }
}

程式碼很容易理解,就是開啟 UIHomePanel 時動態載入背景資源,當關閉時進行資源的解除安裝操作。

如果隨著需求增多,這個頁面可能需要動態載入的資源也會增多。

public class UIHomePanel: MonoBehaviour
{
    private Texture2D mBgTexture = null;
    private Texture2D mLogoTexture = null;
    private Texture2D mBgEnTexture = null;
    
    void Start()
    {
        mBgTexture = Resources.Load<Texture2D>("bg");
        // do something
        
        mLogoTexture = Resources.Load<Texture2D>("logo");
        // do something
        
        mBgEnTexture = Resources.Load<Texture2D>("bg_en");
        // do something
    }
    
    void OnDestroy()
    {
        Resources.UnloadAsset(mBgTexture);
        mBgTexture = null;
        Resources.UnloadAsset(mLogoTexture);
        mBgTexture = null;
        Resources.UnloadAsset(mBgEnTexture);
        mBgTexture = null;
    }
}

這樣程式碼量就增多了,成員變數的程式碼和需要解除安裝的程式碼隨著資源載入數量正比例增長。這裡要儘量避免這種型別的增長。因為宣告那麼多成員變數不是很好的事情。解決方案很簡單,引入一個 List,使用 List 來記錄本頁面載入過的資源。

v0.0.1 引入 List<UnityEngine.Object>

public class UIHomePanel: MonoBehaviour
{
    private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
    
    void Start()
    {
        var bgTexture = Resources.Load<Texture2D>("bg");
        mLoadedAssets.Add(bgTexture);
        // do something
        
        var logoTexture = Resources.Load<Texture2D>("logo");
        mLoadedAssets.Add(logoTexture);
        // do something
        
        var bgEnTexture = Resources.Load<Texture2D>("bg_en");
        mLoadedAssets.Add(bgEnTexture);
        // do something
    }
    
    void OnDestroy()
    {
        mLoadedAssets.ForEach(loadedAsset => {
            Resources.UnloadAsset(loadedAsset);
        });
        
        mLoadedAssets.Clear();
        mLoadedAssets = null;
    }
}

這樣載入和解除安裝相關的程式碼就固定了。

這時候又有一個問題,資源的重複載入和解除安裝。可能程式碼如下:

public class UIHomePanel: MonoBehaviour
{
    private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
    
    void Start()
    {
        var bgTexture = Resources.Load<Texture2D>("bg");
        mLoadedAssets.Add(bgTexture);
        // do something
        
        var logoTexture = Resources.Load<Texture2D>("logo");
        mLoadedAssets.Add(logoTexture);
        // do something
        
        var bgEnTexture = Resources.Load<Texture2D>("bg_en");
        mLoadedAssets.Add(bgEnTexture);
        // do something
        
        OtherFunction();
    }
    
    void OtherFunction()
    {
        // 重複載入了,也會導致重複解除安裝。
        var logoTexture = Resources.Load<Texture2D>("logo");
        mLoadedAssets.Add(logoTexture);
        // do something
    }
    
    void OnDestroy()
    {
        mLoadedAssets.ForEach(loadedAsset => {
            Resources.UnloadAsset(loadedAsset);
        });
        
        mLoadedAssets.Clear();
        mLoadedAssets = null;
    }
}

對於 Resources 這個 API 來說問題不大,但是 AssetBundle 就可能比較危險了,重複載入 AssetBundle 會導致閃退。所以比較危險了,還是要避免。

v0.0.2 新增重複載入判斷

public class UIHomePanel: MonoBehaviour
{
    private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
    
    void Start()
    {
        var bgTexture = LoadAsset<Texture2D>("bg");
        // do something
        
        var logoTexture = LoadAsset<Texture2D>("logo");
        // do something
        
        var bgEnTexture = LoadAsset<Texture2D>("bg_en");
        // do something
        
        OtherFunction();
    }
    
    void OtherFunction()
    {
        // 重複載入了,也會導致重複解除安裝。
        var logoTexture = LoadAsset<Texture2D>("logo");
        // do something
    }
    
    T LoadAsset<T>(string assetName) where T: UnityEngine.Object
    {
        var retAsset = mLoadedAssets.Find(loadedAsset=>loadedAsset.name == assetName);
        
        if (resAsset)
        {
            return resAsset as T;
        }
        
        retAsset = Resources.Load<T>(assetName);
        mLoadedAssets.Add(retAsset);
        
        return retAsset;
    }
    
    void OnDestroy()
    {
        mLoadedAssets.ForEach(loadedAsset => {
            Resources.UnloadAsset(loadedAsset);
        });
        
        mLoadedAssets.Clear();
        mLoadedAssets = null;
    }
}

新增 LoadAset 方法,帶來了一個意外的好處,就是載入資源部分的程式碼也變得精簡了。

這樣就可以避免重複載入和解除安裝了,當然重複載入和解除安裝僅限於在 UIHomePanel 內。還是沒法避免多個頁面之間的對統一個資源的重複載入和解除安裝的。要解決這個問題會相對麻煩,我們分幾個版本慢慢迭代。

第一個要做的就是,先把這套載入解除安裝策略封裝好,總不能讓每個載入資源的頁面或者指令碼都寫一遍這套策略。

v0.0.3 ResLoader

策略的複用有很多種,繼承、封裝成服務類物件等等。

封裝成一個服務類物件會好搞一些,就是 ResLoader。

public class ResLoader
{
    private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();

    public T LoadAsset<T>(string assetName) where T: UnityEngine.Object
    {
        var retAsset = mLoadedAssets.Find(loadedAsset=>loadedAsset.name == assetName);
        
        if (resAsset)
        {
            return resAsset as T;
        }
        
        retAsset = Resources.Load<T>(assetName);
        mLoadedAssets.Add(retAsset);
        
        return retAsset;
    }
    
    public void UnloadAll()
    {
        mLoadedAssets.ForEach(loadedAsset => {
            Resources.UnloadAsset(loadedAsset);
        });
        
        mLoadedAssets.Clear();
        mLoadedAssets = null;
    }
}

而 UIHomePanel 則會變成如下:

public class UIHomePanel: MonoBehaviour
{
    ResLoader mResLoader = new ResLoader();
    
    void Start()
    {
        var bgTexture = mResLoader.LoadAsset<Texture2D>("bg");
        // do something
        
        var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
        // do something
        
        var bgEnTexture = mResLoader.LoadAsset<Texture2D>("bg_en");
        // do something
        
        OtherFunction();
    }
    
    void OtherFunction()
    {
        // 重複載入了,也會導致重複解除安裝。
        var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
        // do something
    }
    
    void OnDestroy()
    {
        mResLoader.UnloadAll();
        mResLoader = null;
    }
}

使用程式碼精簡了很多。

ResLoader 只是記錄下當前頁面或者指令碼載入過的資源,這樣不夠用,還需要一個記錄全域性載入過資源的容器。

v0.0.4 SharedLoadedAssets & Res

實現很簡單,給 ResLoader 建立一個靜態 List<UnityEngine.Object>

ResLoader.cs

public class ResLoader
{
    private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
    
    private static List<UnityEngine.Object> mSharedLoadedAssets = new List<UnityEngine.Object>();

    public T LoadAsset<T>(string assetName) where T: UnityEngine.Object
    {
        var retAsset = mLoadedAssets.Find(loadedAsset=>loadedAsset.name == assetName);
        
        if (resAsset)
        {
            return resAsset as T;
        }
        
        retAsset = Resources.Load<T>(assetName);
        mLoadedAssets.Add(retAsset);
        
        return retAsset;
    }
    
    public void UnloadAll()
    {
        mLoadedAssets.ForEach(loadedAsset => {
            Resources.UnloadAsset(loadedAsset);
        });
        
        mLoadedAssets.Clear();
        mLoadedAssets = null;
    }
}

這個 mSharedLoadedAssets 有什麼作用呢 ?

它相當於一個全域性資源池,而 ResLoader 獲取資源,都要從資源池中獲取,何時進行載入和解除安裝取決於某個資源是否是第一次載入、某個資源解除安裝時是不是不被所有 ResLoader 引用。

所以資源的載入和解除安裝,應該取決於被引用的次數,第一次被引用就載入該資源,最後一次引用釋放則進行解除安裝。

但是 UnityEngine.Object 沒有提供引用計數功能。

所以需要在 UnityEngine.Object 基礎上抽象一個類 Res:

這個 Res,要實現一個引用計數的功能。

而且為了未來分出來不同型別的資源(例如 ResourcesRes 和 AssetBundleRes/AssetRes 等),Res 也要管理自己的載入解除安裝操作。

程式碼如下:

Res.cs

    public class Res
    {
        public string Name
        {
            get { return mAsset.name; }
        }
        
        public Res(Object asset)
        {
            mAsset = asset;
        }
        
        private Object mAsset;
        
        private int mReferenceCount = 0;

        public void Retain()
        {
            mReferenceCount++;
        }

        public void Release()
        {
            mReferenceCount--;

            if (mReferenceCount == 0)
            {
                Resources.UnloadAsset(mAsset);

                ResLoader.SharedLoadedReses.Remove(this);

                mAsset = null;
            }
        }
    }

對應的 ResLoader 變為如下:

    public class ResLoader
    {
        /// <summary>
        /// 共享的 
        /// </summary>
        public static List<Res> SharedLoadedReses = new List<Res>();
        
        
        /// <summary>
        /// 持有的
        /// </summary>
        private List<Res> mResList = new List<Res>();

        public T LoadAsset<T>(string assetName) where T : Object
        {
            var loadedRes = mResList.Find(loadedAsset=>loadedAsset.Name == assetName);
            
            if (loadedRes != null)
            {
                return loadedRes as T;
            }

            loadedRes = SharedLoadedReses.Find(loadedAsset => loadedAsset.Name == assetName);

            if (loadedRes != null)
            {
                loadedRes.Retain();
                
                mResList.Add(loadedRes);
                
                return loadedRes as T;
            }
            
            var asset = Resources.Load<T>(assetName);

            loadedRes = new Res(asset);

            SharedLoadedReses.Add(loadedRes);
            
            loadedRes.Retain();

            mResList.Add(loadedRes);

            return asset;
        }

        public void UnloadAll()
        {
            foreach (var asset in mResList)
            {
                asset.Release();
            }

            mResList.Clear();
            mResList = null;
        }
    }

ResLoader 的程式碼,希望大家仔細研讀下,尤其是 LoadAsset 方法。載入步驟在原理部分有簡單講過,這裡不多說了。而 UnloadAll,則是不是進行真正的解除安裝,而是對資源進行一次釋放。

使用程式碼 UIHomePanel.cs 不變:

public class UIHomePanel: MonoBehaviour
{
    ResLoader mResLoader = new ResLoader();
    
    void Start()
    {
        var bgTexture = mResLoader.LoadAsset<Texture2D>("bg");
        // do something
        
        var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
        // do something
        
        var bgEnTexture = mResLoader.LoadAsset<Texture2D>("bg_en");
        // do something
        
        OtherFunction();
    }
    
    void OtherFunction()
    {
        // 重複載入了,也會導致重複解除安裝。
        var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
        // do something
    }
    
    void OnDestroy()
    {
        mResLoader.UnloadAll();
        mResLoader = null;
    }
}

這裡在寫其他的示例程式碼,載入相同的資源:

public class UIOtherPanel: MonoBehaviour
{
    ResLoader mResLoader = new ResLoader();
    
    void Start()
    {
        var bgTexture = mResLoader.LoadAsset<Texture2D>("bg");
        // do something
        
        var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
        // do something
        
        var bgEnTexture = mResLoader.LoadAsset<Texture2D>("bg_en");
        // do something
        
        OtherFunction();
    }
    
    void OtherFunction()
    {
        // 重複載入了,也會導致重複解除安裝。
        var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
        // do something
    }
    
    void OnDestroy()
    {
        mResLoader.UnloadAll();
        mResLoader = null;
    }
}

這樣就算 UIHomePanel 和 UIOtherPanel 同時開啟,也不會發生資源的重複載入或解除安裝了。

到這裡一個基本的管理模型完成了,可以將版本號升級為 v0.1.1,算是一個最小可執行版本(MVP)。不管在使用還是結構上都是一個非常好的方案。而 ResKit 就是以這個管理模型為基礎,慢慢完善功能的,至今為止支援了非常多的專案。

本片的文章就到這裡

那麼剩下的不值得一提的功能歡迎研讀本 chat 的示例工程和 QFramework 中的 ResKit 模組。

轉載請註明地址:涼鞋的筆記:liangxiegame.com

更多內容

  • QFramework 地址:https://github.com/liangxiegame/QFramework
  • QQ 交流群:623597263
  • Unity 進階小班:
    • 主要訓練內容:
      • 框架搭建訓練(第一年)
      • 跟著案例學 Shader(第一年)
      • 副業的孵化(第二年、第三年)
    • 權益、授課形式等具體詳情請檢視《小班產品手冊》:https://liangxiegame.com/master/intro
  • 關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。