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();

// 加载 prefab
var smObjPrefab = loader.LoadSync("smObj");

// 加载 Bg
var bgTexture = loader.LoadSync("Bg");

// 通过 AssetBundleName 加载 logo
var logoTexture = loader.LoadSync("hometextures","logo");

// 当 GameOject Destroy 时候进行调用,这里进行一个资源回收操作,会将每个引用计数器减一。
loader.Recycle2Cache();
loader = null;
最先登场的是 ResMgr。

ResMgr.Init() 进行了读取配置操作。在游戏启动或者是热更新完成之后可以调用一次。

第二登场的是 ResLoader。字如其意,就是资源加载器。用户大部分时间都是和它打交道。

ResLoader.LoadSync 默认加载 AssetBundle 中的资源。

如果想加载 Resources 目录下的资源,代码如下所示:

var someObjPrefab = loader.LoadSync("resources://someObj");
当然也可以扩展成支持加载网络资源等。

这里关键的一点是 resources:// 这个前缀。和 https:// 或 http:// 类似,是一个路径,和 www 的 file:// 的原理是一样的。

实现比较简单。核心代码如下:

    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("smObj");
以上这步操作做了一下事情:

判断 loader 里的 List 中有没有加载过 Name 为 "smObj" 的 Res,如果加载过直接返回这个资源。
接着通过 GetRes 这个 API 从 ResMgr 获取这个资源,获取之后对这个 Res 进行 Retain 操作,之后将 Res 保存到 loader 的 List 中,之后再返回给用户。
在 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 进行一个 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("images","bg");
AssetBundleManager.LoadSync("images","bg");
这是常见的解决方案。实现起来和原理也比较简单。而且它已经足够解决大部分问题了。

但是用的时候会有一点限制。

在一个项目的初期,美术的资源还没有全部给出。项目中美术资源相关的目录结构还不是最终版。这时候加载方式全部像上边一样写。

在整个项目周期中,目录结构发生了几次变化。每次变化资源就会被打进不同的 AssetBundle 中,导致都要更改一次加载资源相关的代码,可能改成如下:

ResourcesManager.LoadSync("homes","bg");
AssetBundleManager.LoadSync("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("bg.jpg");
AssetBundleManager.LoadSync("bg.prefab");
实现起来也是比较容易的事情。

  1. 根据传入泛型类型去判断,比如:

mResLoader.LoadSync("bg"); // 加载 纹理类型的,名为 bg 的资源。
mResLoader.LoadSync("bg"); // 加载 GameObject 类型的,名为 bg 的资源。
实现也是比较简单。

以上两种都需要在生成配置阶段,对每个资源生成更多的信息。

还有一种是 Qarth 的方案。

就是在对每个 AssetBundle,都加入了激活和未激活两个状态。

示意代码如下。

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

ResMgr.ActivateAssetBundle("home");
mResLoader.LoadSync("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
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。

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

原文地址https://www.cnblogs.com/liangxiegame/p/12572581.html

相关文章
|
3月前
|
图形学 C#
超实用!深度解析Unity引擎,手把手教你从零开始构建精美的2D平面冒险游戏,涵盖资源导入、角色控制与动画、碰撞检测等核心技巧,打造沉浸式游戏体验完全指南
【8月更文挑战第31天】本文是 Unity 2D 游戏开发的全面指南,手把手教你从零开始构建精美的平面冒险游戏。首先,通过 Unity Hub 创建 2D 项目并导入游戏资源。接着,编写 `PlayerController` 脚本来实现角色移动,并添加动画以增强视觉效果。最后,通过 Collider 2D 组件实现碰撞检测等游戏机制。每一步均展示 Unity 在 2D 游戏开发中的强大功能。
176 6
|
2月前
|
测试技术 C# 图形学
掌握Unity调试与测试的终极指南:从内置调试工具到自动化测试框架,全方位保障游戏品质不踩坑,打造流畅游戏体验的必备技能大揭秘!
【9月更文挑战第1天】在开发游戏时,Unity 引擎让创意变为现实。但软件开发中难免遇到 Bug,若不解决,将严重影响用户体验。调试与测试成为确保游戏质量的最后一道防线。本文介绍如何利用 Unity 的调试工具高效排查问题,并通过 Profiler 分析性能瓶颈。此外,Unity Test Framework 支持自动化测试,提高开发效率。结合单元测试与集成测试,确保游戏逻辑正确无误。对于在线游戏,还需进行压力测试以验证服务器稳定性。总之,调试与测试贯穿游戏开发全流程,确保最终作品既好玩又稳定。
103 4
|
3月前
|
图形学 缓存 算法
掌握这五大绝招,让您的Unity游戏瞬间加载完毕,从此告别漫长等待,大幅提升玩家首次体验的满意度与留存率!
【8月更文挑战第31天】游戏的加载时间是影响玩家初次体验的关键因素,特别是在移动设备上。本文介绍了几种常见的Unity游戏加载优化方法,包括资源的预加载与异步加载、使用AssetBundles管理动态资源、纹理和模型优化、合理利用缓存系统以及脚本优化。通过具体示例代码展示了如何实现异步加载场景,并提出了针对不同资源的优化策略。综合运用这些技术可以显著缩短加载时间,提升玩家满意度。
148 5
|
2月前
|
前端开发 图形学 开发者
【独家揭秘】那些让你的游戏瞬间鲜活起来的Unity UI动画技巧:从零开始打造动态按钮,提升玩家交互体验的绝招大公开!
【9月更文挑战第1天】在游戏开发领域,Unity 是最受欢迎的游戏引擎之一,其强大的跨平台发布能力和丰富的功能集让开发者能够迅速打造出高质量的游戏。优秀的 UI 设计对于游戏至关重要,尤其是在手游市场,出色的 UI 能给玩家留下深刻的第一印象。Unity 的 UGUI 系统提供了一整套解决方案,包括 Canvas、Image 和 Button 等组件,支持添加各种动画效果。
135 3
|
2月前
|
设计模式 存储 人工智能
深度解析Unity游戏开发:从零构建可扩展与可维护的游戏架构,让你的游戏项目在模块化设计、脚本对象运用及状态模式处理中焕发新生,实现高效迭代与团队协作的完美平衡之路
【9月更文挑战第1天】游戏开发中的架构设计是项目成功的关键。良好的架构能提升开发效率并确保项目的长期可维护性和可扩展性。在使用Unity引擎时,合理的架构尤为重要。本文探讨了如何在Unity中实现可扩展且易维护的游戏架构,包括模块化设计、使用脚本对象管理数据、应用设计模式(如状态模式)及采用MVC/MVVM架构模式。通过这些方法,可以显著提高开发效率和游戏质量。例如,模块化设计将游戏拆分为独立模块。
183 3
|
3月前
|
图形学 开发者 存储
超越基础教程:深度拆解Unity地形编辑器的每一个隐藏角落,让你的游戏世界既浩瀚无垠又细节满满——从新手到高手的全面技巧升级秘籍
【8月更文挑战第31天】Unity地形编辑器是游戏开发中的重要工具,可快速创建复杂多变的游戏环境。本文通过比较不同地形编辑技术,详细介绍如何利用其功能构建广阔且精细的游戏世界,并提供具体示例代码,展示从基础地形绘制到植被与纹理添加的全过程。通过学习这些技巧,开发者能显著提升游戏画面质量和玩家体验。
151 3
|
2月前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
208 0
|
2月前
|
vr&ar 图形学 API
Unity与VR控制器交互全解:从基础配置到力反馈应用,多角度提升虚拟现实游戏的真实感与沉浸体验大揭秘
【8月更文挑战第31天】虚拟现实(VR)技术迅猛发展,Unity作为主流游戏开发引擎,支持多种VR硬件并提供丰富的API,尤其在VR控制器交互设计上具备高度灵活性。本文详细介绍了如何在Unity中配置VR支持、设置控制器、实现按钮交互及力反馈,结合碰撞检测和物理引擎提升真实感,助力开发者创造沉浸式体验。
154 0
|
2月前
|
图形学 开发者
【独家揭秘】Unity游戏开发秘籍:从基础到进阶,掌握材质与纹理的艺术,打造超现实游戏视效的全过程剖析——案例教你如何让每一面墙都会“说话”
【8月更文挑战第31天】Unity 是全球领先的跨平台游戏开发引擎,以其高效性能和丰富的工具集著称,尤其在提升游戏视觉效果方面表现突出。本文通过具体案例分析,介绍如何利用 Unity 中的材质与纹理技术打造逼真且具艺术感的游戏世界。材质定义物体表面属性,如颜色、光滑度等;纹理则用于模拟真实细节。结合使用两者可显著增强场景真实感。以 FPS 游戏为例,通过调整材质参数和编写脚本动态改变属性,可实现自然视觉效果。此外,Unity 还提供了多种高级技术和优化方法供开发者探索。
53 0
|
3月前
|
开发者 图形学 API
从零起步,深度揭秘:运用Unity引擎及网络编程技术,一步步搭建属于你的实时多人在线对战游戏平台——详尽指南与实战代码解析,带你轻松掌握网络化游戏开发的核心要领与最佳实践路径
【8月更文挑战第31天】构建实时多人对战平台是技术与创意的结合。本文使用成熟的Unity游戏开发引擎,从零开始指导读者搭建简单的实时对战平台。内容涵盖网络架构设计、Unity网络API应用及客户端与服务器通信。首先,创建新项目并选择适合多人游戏的模板,使用推荐的网络传输层。接着,定义基本玩法,如2D多人射击游戏,创建角色预制件并添加Rigidbody2D组件。然后,引入网络身份组件以同步对象状态。通过示例代码展示玩家控制逻辑,包括移动和发射子弹功能。最后,设置服务器端逻辑,处理客户端连接和断开。本文帮助读者掌握构建Unity多人对战平台的核心知识,为进一步开发打下基础。
123 0

热门文章

最新文章