Unity 解决QFramework WebGL报错

Posted 牙膏上的小苏打2333

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity 解决QFramework WebGL报错相关的知识,希望对你有一定的参考价值。

Unity 解决QFramework WebGL报错

🥝错误描述

在新建工程中只导入了QFramework,Build WebGL(空场景) 会看到如下错误:

Exception: Non-Public Constructor() not found! in QFramework.SafeObjectPool`1[QFramework.DelayAction]
  at QFramework.SingletonCreator.CreateNonPublicConstructorObject[T] () [0x00000] in <00000000000000000000000000000000>:0

☕原因

根据QFramework 作者提供的线索:“关闭代码剔除”

🍤 解决方案

但但但是项目中需要开启它,这时候要在相关的类加上[UnityEngine.Scripting.Preserve] 告诉编辑器本大爷罩着这个类,你不许剔除它哦😎!

当然不需要把所有的脚本都加上,但又不想一个一个加怎么办呢?
💡.建个空工程导入QFramework包
💡.Project中搜索Example全选删除。(APIExampleCodeAttribute 不要删)
💡.打开脚本搜索public class ,替换:[UnityEngine.Scripting.Preserve] public class 。 (注意class后带一个空格)
💡.替换后有些类还有问题比如带seale、abstract的,按类似的方法改就行了。

在vs中使用查找替换功能:Ctrl + F ,可以实现快速替换。


添加Preserve后打包并没增加多少,可以忽略不计。

💡下载

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 ,我们可以看到如下面板:

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

1. 资源标记

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

标记完了,

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

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

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

这样就标记成功了。

这里注意,一次标记就是一个 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 上,运行后,结果如下:

资源加载成功。

模拟模式与非模拟模式

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(真机模式)

取消勾选模拟模式情况下,点击打 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);
            );
        

    

结果如下:

加载场景

注意:标记场景时要确保,一个场景是一个 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<

以上是关于Unity 解决QFramework WebGL报错的主要内容,如果未能解决你的问题,请参考以下文章

Unity 框架QFramework v1.0 使用指南 架构篇:20. QFramework.cs 的更多内容 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏

unity发布webgl遇到的问题和解决办法

unity发布webgl遇到的问题和解决办法

Unity 框架QFramework v1.0 使用指南 架构篇:19. 心中有架构 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏

Unity 框架QFramework v1.0 使用指南 工具篇:09. SingletonKit 单例模板套件 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏

OpenSourceC#Unity框架-QFramework