﻿# 05. ResKit 资源管理&开发解决方案

## Res Kit 简介
Res Kit，是资源管理&快速开发解决方案

**特性如下:**
* 可以使用一个 API 从  dataPath、Resources、StreammingAssetPath、PersistentDataPath、网络等地方加载资源。
* 基于引用计数，简化资源加载和卸载。
* 拥抱游戏开发流程中的不同阶段
  * 开发阶段不用打 AB 直接从 dataPath 加载。
  * 测试阶段支持只需打一次 AB 即可。
* 可选择生成资源名常量代码，减少拼写错误。
* 异步加载队列支持
* 对于 AssetBundle 资源，可以只通过资源名而不是 AssetBundle 名 + 资源名 加载资源，简化 API 使用。


## Res Kit 快速入门
我们知道，在一般情况下，有两种方式可以让我们实现动态加载资源：
* Resources
* AssetBundle

在 Res Kit 中，推荐使用 AssetBundle 的方式进行加载，因为 Res Kit 所封装的 AssetBundle 方式，比 Resources 的方式更好用。

除了 Res Kit 中的 AsseBundle 方式更易用外，AssetBundle 本身相比 Resources 有更多的优点，比如更小的包体，支持热更等。

废话不多说，我们看下 Res Kit 的基本使用。

Res Kit 在开发阶段，分为两步。
* 标记资源
* 写代码

在开始之前，我们要确保，当前的 Res Kit 环境为模拟模式。

按下快捷键 ctrl + e 或者 ctrl + shift + r ，我们可以看到如下面板:

![image.png](https://file.liangxiegame.com/d6d1ac25-4c60-4b42-81ec-51b1628b640a.png)

确保模拟模式勾选之后，我们就可以进入使用流程了。

### 1. 资源标记

在 Asset 目录下，只需对需要标记的文件或文件夹右键->@ResKit- AssetBundle Mark，如下所示：

![image.png](https://file.liangxiegame.com/2d793421-94cb-457f-80da-ee976f700f02.png)

标记完了，

标记成功后，我们可以看到如下结果：

1. 该资源标记的选项为勾选状态

![image.png](https://file.liangxiegame.com/1ced7efd-a328-4c5e-a76a-4a85020acdd2.png)

2. 该资源的 AssetLabel 中的名字如下
   ![image.png](https://file.liangxiegame.com/a7e20396-e553-4ead-8291-e4395fe53b30.png)

这样就标记成功了。

这里注意，一次标记就是一个 AssetBundle，如果想要让 AssetBundle 包含多个资源，可以将多个资源放到一个文件夹中，然后标记文件夹。


### 2.资源加载
接下来我们直接写资源加载的代码即可，代码如下，具体的代码含义，看注释即可。。

```csharp
using UnityEngine;

namespace QFramework.Example
{
    public class ResKitExample : MonoBehaviour
    {
        // 每个脚本都需要
        private ResLoader mResLoader = ResLoader.Allocate();

        private void Start()
        {
            // 项目启动只调用一次即可
            ResKit.Init();
            
            // 通过资源名 + 类型搜索并加载资源（更方便）
            var prefab = mResLoader.LoadSync<GameObject>("AssetObj");
            var gameObj = Instantiate(prefab);
            gameObj.name = "这是使用通过 AssetName 加载的对象";

            // 通过 AssetBundleName 和 资源名搜索并加载资源（更精确）
            prefab = mResLoader.LoadSync<GameObject>("assetobj_prefab", "AssetObj");
            gameObj = Instantiate(prefab);
            gameObj.name = "这是使用通过 AssetName  和 AssetBundle 加载的对象";
        }

        private void OnDestroy()
        {
            // 释放所有本脚本加载过的资源
            // 释放只是释放资源的引用
            // 当资源的引用数量为 0 时，会进行真正的资源卸载操作
            mResLoader.Recycle2Cache();
            mResLoader = null;
        }
    }
}
```

将此脚本挂到任意 GameObject 上，运行后，结果如下:


![image.png](https://file.liangxiegame.com/04cd1727-b7ad-436d-988c-80b70c0fc106.png)

资源加载成功。

## 模拟模式与非模拟模式

### AssetBundle 的不便之处
在使用 Res Kit 之前，相信大家多多少少接触过 AssetBundle。 有的童鞋可能是在项目中用过 AssetBundle，有的童鞋可能只是简单学习过 AssetBundle。总之，AssetBundle 在不通过 Res Kit 使用之前，总结下来就两个字：麻烦。

AssetBundle 麻烦在哪里呢？

首先 AssetBundle，需要打包才能在运行时加载资源。而打包需要我们写编辑器扩展脚本，在编辑器扩展脚本中还要处理平台和路径相关的逻辑。

在运行时，还需要根据平台和路径去加载对应的 AssetBundle。

这些操作想想就比较头痛。

既然 AssetBundle 这么麻烦，我们为什么还要用 AssetBundle 呢？

因为 AssetBundle 可以给项目带来更好的性能，而且 AssetBundle 支持热更新。

有了这两个优势，AssetBundle 就成了很多项目的必然选择。

而 Res Kit 中，为了解决频繁打包的问题，引入了一个概念：模拟模式（Simulation Mode）。

#### 模拟模式（Simulation Mode）

**什么是模拟模式？**

顾名思义，就是模拟加载 AssetBundle 的模式，这里只是模拟，并没有真正去加载 AssetBundle，而是去加载 Application.dataPath 目录下的资源，也就是 Assets 目录下的资源。

**这样做有什么好处呢？**

好处就是每当有资源修改的时候，就不用再打 AB 包了，就可以在运行时加载到修改后的资源。

如果是非模拟模式下，每当有资源修改时，就需要再打一次 AB 包，才能加载到修改后的资源。

所以一个模拟模式，解决了频繁打 AB 包的问题，从而在开发阶段提高我们的开发效率。

那么在使用 Res Kit 的时候，模拟模式对应的阶段是开发阶段，那么非模拟模式对应的是什么阶段呢？

答案就是真机阶段。

### 开发阶段、真机阶段
开发阶段、真机阶段并不是 Unity 提供的概念，而是笔者在迭代 Res Kit 中提出的两个概念。

这两个概念很容易理解：
* 开发阶段：开发逻辑的阶段，需要编写大量的逻辑，大部分情况下都在 Unity Editor 环境下开发。
* 真机阶段：需要在真机上运行的阶段，这个阶段主要是做大量的测试或者真正发布了。

相信有点规模的项目都会分阶段出来的，比如开发阶段、测试阶段、生产阶段等等，大家理解起来应该不难。

接下来简单分析一下开发阶段、真机阶段的特点。

**开发阶段**
在开发阶段，开发者需要写大量的逻辑，而且资源的目录还没有稳定，一般在开发过程中会有很大的变化。
如果每次资源的修改都需要打 AB 包的话，会非常影响开发进度。

**真机阶段**
真机阶段，一般就是一个版本的逻辑都写完了，只需要做一些测试和 debug 工作。在这个阶段，资源目录都稳定了，不需要做很大的调整。

在真机阶段，每次打 App 包之前，只需要 Build 一次 AB 即可。

当然，在 Unity Editor 环境中，可以取消勾选模拟模式，这样在 Unity Editor 环境下可以加载真正的 AssetBundle 包。

在上一篇文章所说的，拥抱各个开发阶段指的就是为开发阶段、和真机阶段做了考虑。

此篇的内容就这些。

### 小结
* 开发阶段：
  *  模拟模式
* 真机阶段：
  * 每次打 App 包之前，打一次 AB 包。
  * 可以在 Unity Editor 环境下，取消勾选模拟模式，这时在运行时加载的资源则是真正的 AssetBundle 资源


## 如何打 AssetBundle（真机模式）


![image.png](https://file.liangxiegame.com/bcc21643-8c4a-4f6f-b3a9-db1ec3071119.png)

取消勾选模拟模式情况下，点击打 AB 包 即可。


## 异步加载
异步加载代码如下:
``` csharp
// 添加到加载队列
mResLoader.Add2Load("TestObj",(succeed,res)=>{
    if (succeed) 
    {
        res.Asset.As<GameObject>()
						.Instantiate();
    }
});

// 执行异步加载
mResLoader.LoadAsync();
```
与 LoadSync 不同的是，异步加载是分两步的，第一步是添加到加载队列，第二步是执行异步加载。

这样做是为了支持同时异步加载多个资源的。

## 异步加载
代码如下:
```csharp
using System.Collections;
using UnityEngine;

namespace QFramework.Example
{
    public class AsyncLoadExample : MonoBehaviour
    {
        IEnumerator Start()
        {
            yield return ResKit.InitAsync();

            var resLoader = ResLoader.Allocate();
            
            resLoader.Add2Load<GameObject>("AssetObj 1",(b, res) =>
            {
                if (b)
                {
                    res.Asset.As<GameObject>().Instantiate();
                }
            });

            // AssetBundleName + AssetName
            resLoader.Add2Load<GameObject>("assetobj 2_prefab","AssetObj 2",(b, res) =>
            {
                if (b)
                {
                    res.Asset.As<GameObject>().Instantiate();
                }
            });
            
            resLoader.Add2Load<GameObject>("AssetObj 3",(b, res) =>
            {
                if (b)
                {
                    res.Asset.As<GameObject>().Instantiate();
                }
            });

            resLoader.LoadAsync(() =>
            {
                // 加载成功 5 秒后回收
                ActionKit.Delay(5.0f, () =>
                {
                    resLoader.Recycle2Cache();

                }).Start(this);
            });
        }

    }
}
```

结果如下:

![image.png](https://file.liangxiegame.com/8ad406e4-f59c-43d2-bd4a-e7de57560958.png)


## 加载场景

注意：标记场景时要确保，一个场景是一个 AssetBundle。

```csharp
using UnityEngine;

namespace QFramework.Example
{
	public class LoadSceneExample : MonoBehaviour
	{
		private ResLoader mResLoader = null;

		void Start()
		{
			ResKit.Init();

			mResLoader = ResLoader.Allocate();

			// 同步加载
			mResLoader.LoadSceneSync("SceneRes");

			// 异步加载
			mResLoader.LoadSceneAsync("SceneRes");

			// 异步加载
			mResLoader.LoadSceneAsync("SceneRes", onStartLoading: operation =>
			{
				// 做一些加载操作
			});
		}

		private void OnDestroy()
		{
			mResLoader.Recycle2Cache();
			mResLoader = null;
		}
	}
}
```


## 加载 Resources 中的资源

```csharp
using UnityEngine;
using UnityEngine.UI;

namespace QFramework.Example
{
	public class LoadResourcesResExample : MonoBehaviour
	{
		public RawImage RawImage;
		
		private ResLoader mResLoader = ResLoader.Allocate();
		
		private void Start()
		{
			//  加载 Resources 目录里的资源不用调用 ResKit.Init
			
			RawImage.texture = mResLoader.LoadSync<Texture2D>("resources://TestTexture");
		}

		private void OnDestroy()
		{
			Debug.Log("On Destroy ");
			mResLoader.Recycle2Cache();
			mResLoader = null;
		}
	}
}
```


## 关联对象管理

```csharp
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

namespace QFramework.Example
{
    public class ResLoaderRelateUnloadAssetExample : MonoBehaviour
    {
        // Use this for initialization
        IEnumerator Start()
        {
            var image = transform.Find("Image").GetComponent<Image>();

            ResKit.Init();

            var resLoader = ResLoader.Allocate();
            
            var texture2D = resLoader.LoadSync<Texture2D>("TextureExample1");

            // create Sprite 扩展
            var sprite = Sprite.Create(texture2D, new Rect(0, 0, texture2D.width, texture2D.height), Vector2.one * 0.5f);

            image.sprite = sprite;

            // 添加关联的 Sprite
            resLoader.AddObjectForDestroyWhenRecycle2Cache(sprite);

            yield return new WaitForSeconds(5.0f);
            
            // 当释放时 sprite 也会销毁
            resLoader.Recycle2Cache();
            resLoader = null;
        }
    }
}
```

## SpriteAtlas 加载

```csharp
using System.Collections;
using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;

namespace QFramework
{
	/// <inheritdoc />
	/// <summary>
	/// 参考:http://www.cnblogs.com/TheChenLin/p/9763710.html
	/// </summary>
	public class TestSpriteAtlas : MonoBehaviour
	{
		[SerializeField] private Image mImage;

		// Use this for initialization
		private IEnumerator Start()
		{
			var loader = ResLoader.Allocate();

			ResKit.Init();

			var spriteAtlas = loader.LoadSync<SpriteAtlas>("spriteatlas");
			var square = spriteAtlas.GetSprite("shop");
			
			loader.AddObjectForDestroyWhenRecycle2Cache(square);

			mImage.sprite = square;

			yield return new WaitForSeconds(5.0f);

			loader.Recycle2Cache();
			loader = null;
		}
	}
}
```

## 加载网络图片

```csharp
using UnityEngine;
using UnityEngine.UI;

namespace QFramework.Example
{
    public class NetImageExample : MonoBehaviour
    {
        ResLoader mResLoader = ResLoader.Allocate();

        // Use this for initialization
        void Start()
        {
            var image = transform.Find("Image").GetComponent<Image>();
            
            mResLoader.Add2Load<Texture2D>(
                "http://pic.616pic.com/ys_b_img/00/44/76/IUJ3YQSjx1.jpg".ToNetImageResName(),
                (b, res) =>
                {
                    if (b)
                    {
                        var texture = res.Asset as Texture2D;

                        var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height),
                            Vector2.one * 0.5f);
                        image.sprite = sprite;
                        mResLoader.AddObjectForDestroyWhenRecycle2Cache(sprite);
                    }
                });
            
            mResLoader.LoadAsync();
        }
        
        private void OnDestroy()
        {
            mResLoader.Recycle2Cache();
            mResLoader = null;
        }
    }
}
```


## 从 PersistentDataPath 加载图片
```csharp
namespace QFramework.Example
{
	using System.Collections;
	using UnityEngine.UI;
	using UnityEngine;
	
	public class ImageLoaderExample : MonoBehaviour
	{
		private ResLoader mResLoader = null;

		private IEnumerator Start()
		{
			ResMgr.Init();
			
			mResLoader = ResLoader.Allocate();

			// local image
			var localImageUrl = "file://" + Application.persistentDataPath + "/Workspaces/lM1wmsLQtfzRQc6fsdEU.jpg";

			mResLoader.Add2Load(localImageUrl.ToLocalImageResName(),
				delegate(bool b, IRes res)
				{
					Debug.LogError(b);
					if (b)
					{
						var texture2D = res.Asset as Texture2D;
						transform.Find("Image").GetComponent<Image>().sprite = Sprite.Create(texture2D,
							new Rect(0, 0, texture2D.width, texture2D.height), Vector2.one * 0.5f);
					}
				});
			
			mResLoader.LoadAsync();
			
			
			yield return new WaitForSeconds(5.0f);
			mResLoader.Recycle2Cache();
			mResLoader = null;
		}
	}
}
```


## 自定义 Res

ResKit 提供了 自定义 Res ，通过自定义 Res 可以非常方便地自定义 Res 的加载来源，比如 PersistentDataPath、StreamingAssetPath、AssetBundle 等，甚至是内存中的 GameObject 等资产，还可以集成 Addressables 或者其他的资源管理方案，ResKit 内置支持的 AssetBundle、Resources、网络图片加载、PersistentDataPath 图片加载都是通过自定义 Res 的方式扩展而来。

我们看下自定义 Res 的用法，如下:
```csharp
using UnityEngine;

namespace QFramework
{
    public class CustomResExample : MonoBehaviour
    {
        // 自定义的 Res
        public class MyRes : Res
        {
            public MyRes(string name)
            {
                mAssetName = name;
            }

            // 同步加载（自己实现）
            public override bool LoadSync()
            {
                // Asset = 加载的结果给 Asset 赋值 
                State = ResState.Ready;
                return true;
            }

            // 异步加载(自己实现)
            public override void LoadAsync()
            {
                // Asset = 加载的结果给 Asset 赋值 
                State = ResState.Ready;
            }
            

            // 释放资源（自己实现)
            protected override void OnReleaseRes()
            {
                // 卸载操作
                // Asset = null
                State = ResState.Waiting;
            }
        }

        // 自定义的 Res 创建器（包含识别功能）
        public class MyResCreator : IResCreator
        {
            // 识别
            public bool Match(ResSearchKeys resSearchKeys)
            {
                return resSearchKeys.AssetName.StartsWith("myres://");
            }

            // 创建
            public IRes Create(ResSearchKeys resSearchKeys)
            {
                return new MyRes(resSearchKeys.AssetName);
            }
        }

        void Start()
        {
            // 添加创建器
            ResFactory.AddResCreator<MyResCreator>();

            var resLoader = ResLoader.Allocate();

            var resSearchKeys = ResSearchKeys.Allocate("myres://hello_world");
            
            var myRes =  resLoader.LoadResSync(resSearchKeys);
            
            resSearchKeys.Recycle2Cache();
            
            Debug.Log(myRes.AssetName);
            Debug.Log(myRes.State);
        }
    }
}
```

非常简单。

## 代码生成

Res Kit 支持代码生成，生成按钮的位置如下所示:
![image.png](https://file.liangxiegame.com/e482f08e-2e8e-4b43-84bf-f32722cc5f5c.png)
点击生成代码即可，生成后结果如下。
![image.png](http://file.liangxiegame.com/0ea13581-4960-4bc8-bbf1-b49a03455271.png)

生成了 QAssets 代码文件，代码内容如下:

```csharp
namespace QAssetBundle
{
  
    public class Testobj_prefab
    {
        public const string BundleName = "testobj_prefab";
        public const string TESTOBJ = "testobj";
    }
    public class Testsprite_png
    {
        public const string BundleName = "testsprite_png";
        public const string TESTSPRITE = "testsprite";
    }
}

```

生成了代码，那么在写资源加载的代码的时候就会爽的飞起，如下图示:
![image.png](http://file.liangxiegame.com/7b8ae854-aafe-49d8-9318-5f7d1190c8cc.png)

图中，给出了资源名字的提示。

这样就不容易出现字符串的拼写错误了。

## ResLoader 推荐用法

ResLoader 的推荐用法，是一个需要加载的单元申请一个 ResLoader。

代码如下:

```csharp
using QF.Res;
using QF.Extensions;
using UnityEngine;

namespace QF.Example 
{
	public class TestResKit : MonoBehaviour 
	{
		/// <summary>
		/// 每一个需要加载资源的单元（脚本、界面）申请一个 ResLoader
		/// ResLoader 本身会记录该脚本加载过的资源
		/// </summary>
		/// <returns></returns>
		ResLoader mResLoader = ResLoader.Allocate ();
  
    ...
  
        void Destroy()
		{
			// 释放所有本脚本加载过的资源
			// 释放只是释放资源的引用
			// 当资源的引用数量为 0 时，会进行真正的资源卸载操作
			mResLoader.Recycle2Cache();
			mResLoader = null;
		}
	}
}
```

在以上代码中，TestResKit 是一个需要加载资源的单元。

**这个单元是什么意思呢？**

其实很简单，单元可以是 UIPanel （界面），或者任何需要加载资源服务的 MonoBehaviour。

## ResLoader 的职责

ResLoader 的职责字如其意，就是负责加载资源的，即资源加载器。

一个 ResLoader 会记录所有它加载过的资源。

这样它在释放资源的时候只需要根据加载记录，进行释放即可。

ResLoader 与 单元（Test 脚本）的示意图如下:
![image.png](http://file.liangxiegame.com/296b0166-bdea-47d5-ac87-4b55c91df16f.png)

这里我们要注意，ResLoader 不是进行真正的资源加载操作，而是进行资源的引用获取。

真正的资源加载是在 ResMgr 中完成，这个过程用户是无法感知的到的。

ResLoader 获取资源引用的过程如下:

1. 从 ResLoader 的引用记录中查询是否已经获取了引用，如果之前已经在 ResLoader 记录过资源引用则返回资源。否则执行 2.
2. 从 ResMgr 中查询是否已经有资源对象，如果有资源对象，返回资源，并在 ResLoader 中记录引用，同时对资源对象进行引用计数 +1 操作，否则执行 3.
3. 让 ResMgr 进行资源加载，同时创建资源对象，剩下的步骤同 2。

大致的访问资源的过程就是如此，不理解的童鞋不要紧，因为对使用上来说不重要。

我们只需要知道，建议每个需要加载的脚本申请一个 ResLoader，是为了更方便地让大家进行资源管理。

不管这个脚本加载过多少个东西，也不管别的脚本加载过多少，只需要各自脚本释放自己的 ResLoader 即可。

因为每个资源对象对集成了引用计数的。

## 申请 ResLoader 的消耗

几乎没有消耗，因为 ResLoader 是从对象池中申请的。


## WebGL 注意事项补充

在 WebGL 平台 ResKit 加载 AssetBundle 资源只支持异步加载。


异步初始化
```csharp
StartCoroutine(ResKit.InitAsync());
// 或者
ResKit.InitAsync().ToAction().StartGlobal();
```

异步加载资源
* 先 Add2Load
* 再调用 LoadAsync()


好了，ResKit 的功能就全部介绍完了。

## 更多内容

*   转载请注明地址：[liangxiegame.com](https://liangxiegame.com) （首发） 微信公众号：凉鞋的笔记
*   QFramework 主页：[qframework.cn](https://qframework.cn)
*   QFramework 交流群: 623597263
*   QFramework Github 地址: [https://github.com/liangxiegame/qframework](https://github.com/liangxiegame/qframework)
*   QFramework Gitee 地址：[https://gitee.com/liangxiegame/QFramework](https://gitee.com/liangxiegame/QFramework)
*   GamePix 独立游戏学院 & Unity 进阶小班地址：[https://www.gamepixedu.com/](https://www.gamepixedu.com/)




