【Unity 框架】QFramework v1.0 使用指南 工具篇:05. ResKit 资源管理&开发解决方案 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏

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

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

1. 资源标记

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

image.png

标记完了,

标记成功后,我们可以看到如下结果:

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

image.png

  1. 该资源的 AssetLabel 中的名字如下
    image.png

这样就标记成功了。

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

2.资源加载

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

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

资源加载成功。

模拟模式与非模拟模式

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

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

异步加载

异步加载代码如下:

// 添加到加载队列
mResLoader.Add2Load("TestObj",(succeed,res)=>{
    if (succeed) 
    {
        res.Asset.As<GameObject>()
						.Instantiate();
    }
});

// 执行异步加载
mResLoader.LoadAsync();

与 LoadSync 不同的是,异步加载是分两步的,第一步是添加到加载队列,第二步是执行异步加载。

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

异步加载

代码如下:

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

加载场景

注意:标记场景时要确保,一个场景是一个 AssetBundle。

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 中的资源

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;
		}
	}
}

关联对象管理

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 加载

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;
		}
	}
}

加载网络图片

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 加载图片

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 的用法,如下:

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
点击生成代码即可,生成后结果如下。
image.png

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

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

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

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

ResLoader 推荐用法

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

代码如下:

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

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

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

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

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

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

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

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

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

申请 ResLoader 的消耗

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

WebGL 注意事项补充

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

异步初始化

StartCoroutine(ResKit.InitAsync());
// 或者
ResKit.InitAsync().ToAction().StartGlobal();

异步加载资源

  • 先 Add2Load
  • 再调用 LoadAsync()

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

更多内容

posted @ 2022-10-17 11:52  凉鞋的笔记  阅读(447)  评论(0编辑  收藏  举报