此篇文章准备了将近两周的时间,写了改,改了删。之前有朋友反馈,上一个文章太冗长了,影响阅读体验,这一讲就走个精简路线。所以只要不是很重要的内容就都删减掉了。

文章分两个部分,第一部分是原理,第二部分是实战。

原理部分,初学者尽量去理解就好,不用跟着敲,列出的代码都是示意代码。

实战部分是留给初学者的,希望敲完代码,这样有助于理解前边的原理。

当然原理不是很难。

第一部分:原理

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

更多内容

Unity 游戏框架:资源管理神器 ResKit的更多相关文章

  1. Unity 游戏框架搭建 2018 (一) 架构、框架与 QFramework 简介

    约定 还记得上版本的第二十四篇的约定嘛?现在出来履行啦~ 为什么要重制? 之前写的专栏都是按照心情写的,在最初的时候笔者什么都不懂,而且文章的发布是按照很随性的一个顺序.结果就是说,大家都看完了,都还 ...

  2. Unity 游戏框架搭建 (十六) v0.0.1 架构调整

    背景: 前段时间用Xamarin.OSX开发一些工具,遇到了两个问题. QFramework的大部分的类耦合了Unity的API,这样导致不能在其他CLR平台使用QFramework. QFramew ...

  3. Unity 游戏框架搭建 (十三) 无需继承的单例的模板

    之前的文章中介绍的Unity 游戏框架搭建 (二) 单例的模板和Unity 游戏框架搭建 (三) MonoBehaviour单例的模板有一些问题. 存在的问题: 只要继承了单例的模板就无法再继承其他的 ...

  4. Unity 游戏框架搭建 (十) QFramework v0.0.2小结

    从框架搭建系列的第一篇文章开始到现在有四个多月时间了,这段时间对自己来说有很多的收获,好多小伙伴和前辈不管是在评论区还是私下里给出的建议非常有参考性,在此先谢过各位. 说到是一篇小节,先列出框架的概要 ...

  5. Unity 游戏框架搭建 (六) 关于框架的一些好文和一些思考

      在进行项目架构阶段,游戏框架可以解决一部分问题.剩下的架构问题还需要根据不同的项目解决.总之游戏框架是游戏架构的一部分. 关于锤子和钉子:   最近又拿起了<代码大全>和<暗时间 ...

  6. 凉鞋:我所理解的框架 【Unity 游戏框架搭建】

    前言 架构和框架这些概念听起来很遥远,让很多初学者不明觉厉.会产生"等自己技术牛逼了再去做架构或者搭建框架"这样的想法.在这里笔者可以很肯定地告诉大家,初学者是完全可以去做这些事情 ...

  7. Unity 游戏框架搭建 (十七) 静态扩展GameObject实现链式编程

    本篇本来是作为原来 优雅的QChain的第一篇的内容,但是QChain流产了,所以收录到了游戏框架搭建系列.本篇介绍如何实现GameObject的链式编程. 链式编程的实现技术之一是C#的静态扩展.静 ...

  8. Unity 游戏框架:UI 管理神器 UI Kit

    UI Kit 快速入门 首先我们来进行 UI Kit 的快速入门 制作一个界面的,步骤如下: 准备 生成代码 逻辑编写 运行 1. 准备 先创建一个场景 TestUIHomePanel. 删除 Hie ...

  9. Unity 游戏框架搭建 (三) MonoBehaviour单例的模板

      上一篇文章讲述了如何设计C#单例的模板.也随之抛出了问题: 如何设计接收MonoBehaviour生命周期的单例的模板? 如何设计? 先分析下需求:   1.约束脚本实例对象的个数.   2.约束 ...

随机推荐

  1. R内的gsub()函数

    今天遇到了一个问题就是,如果数据里面有逗号,那么如何转换他们.就像下面的这样: > exercise9_1$地区生产总值 [1] 16,251.93 11,307.28 24,515.76 11 ...

  2. MyBatis连接MySQL8配置

    <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</a ...

  3. Windows 下 LaTeX 手动安装宏包(package)以及生成帮助文档的整套流程

    本文简单介绍如何手动安装一个 LaTeX 宏包. 一般来说,下载的 TeX 发行版已经自带了很多宏包,可以满足绝大部分需求,但是偶尔我 们也可能碰到需要使用的宏包碰巧没有安装的情况,这时我们就需要自己 ...

  4. 对javascript EventLoop事件循环机制不一样的理解

    前置知识点: 浏览器原理,浏览器内核5种线程及协作,JS引擎单线程设计推荐阅读: 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理 [FE]浏览器渲染引擎「内核」 js异步编程,Promise ...

  5. Golang: chan定义问题(7)

    通常都是定义读写双向的 chan,定义单向 chan 问题. 专栏的介绍可以参考 <GotchaGolang专栏>,代码可以看<宝库-Gotcha>. 通过 只写 chan 传 ...

  6. 对话|首席研究员童欣:从长远看,AR的应用范围远比VR广泛

    ​童欣博士现任微软亚洲研究院网络图形组首席研究员.1993年毕业于浙江大学计算机系,获工学学士学位:1996年获浙江大学计算机系硕士学位:1999年获清华大学计算机系博士学位,同年加入微软亚洲研究院. ...

  7. DDL库和表的管理

    库和表的管理 一. 库的管理 /* 语法: create database [if not exists]库名; */ #.创建库Books CREATE DATABASE IF NOT EXISTS ...

  8. Python 爬虫 selenium 笔记

    1. selenium 安装, 与文档 pip install selenium Selenium with Python中文翻译文档 selenium官网英文文档 2. selenium 的第一个示 ...

  9. 微信小程序状态管理工具 JStore

    微信小程序状态管理工具 JStore 闲着没事做,就想着给微信小程序写一个状态管理工具,名叫 JStore,这个状态管理工具是仿照 vuex 的几个方法来写的,所以有 vuex 的基础同学很容易理解. ...

  10. Linq的整型或实体类null引发的报错问题

    经常在程序中遇到两个空值报错问题: 问题1:int类型如果为不可空,假如传进去null,会报错 问题2:EF的获得单个实体的Model如果为空,那么后面如果跟上属性会报错 解决问题1: 一般属性都设为 ...