找回密码
 立即注册
查看: 572|回复: 0

[笔记] Unity FairyGUI读取Addressables UI资源及优化

[复制链接]
发表于 2021-11-26 12:51 | 显示全部楼层 |阅读模式
一、FairyGUI简介

官方链接:https://www.fairygui.com/
1、官方描述


  • 零代码:重视设计师体验,摒弃了脚本和配置文件这些需要代码思维的操作。策划和美术都可以单独制作出生产级别的UI界面。
  • 高性能:运行性能处于同类产品领先水平,更为 DrawCall 优化提供了独特的策略。
  • 多国语言 和分支:内置的分支和多语言机制,完全所见即所得地支持多语言版本和多渠道版本,为游戏出海提供有力支持。




2、个人看法

1、FairyGUI在产品中,已经运用两年多,在这期间也尝试过换UGUI来进行代替,但是UGUI需要实现动画、窗口切换等都需要编写代码,而FairyGUI只需要在编辑器可视化配置就行,在综合考虑之下,还是继续使用FairyGUI来作为UI方案。
2、在我们产品开发过程中,整体的界面风格需要不断进行优化,调整。基本一个月就要改动一次UI细节,制作一些复杂的UI动效,大量的交互效果(如放大、改变颜色、改变图片等等),这些我只需要在FairyGUI简单配置即可。它的便捷体现在它对UI开发的程序员非常友好。
设计师想这些都进行浮动,在FairyGUI中可视化创建动效,这个操作完全是设计师自己实现,整体编辑器布局,快捷键跟设计师常用软件相差不大。


按钮交互动效,往图片或按钮添加属性即可,还有很多在细节上对UI开发者非常友好的功能....


3、当然并不是UGUI一无是处,如果自身有一套完整的UGUI框架、屏幕自适应、UI优化方案,那完全可以继续使用UGUI,没必要去尝试FairyGUI。它更适合初创团队,因为在UI开发中费时费力,而它可以节省非常多的时间。
4、它的缺点在于开发者需要去学习一款新的编辑器、在多人同时开发UI时,需要定好UI相关协议,怎么在FairyGUI编辑器中多人开发、其他程序员要实现UI功能时需要懂一些基本的FairyGUI知识。
二、Addressables简介

该博主对Addressables进行了非常详细的描述https://www.jianshu.com/p/e79b2eef97bf
总结出一句话就是:Addressables是AssetBundle的升级版,它比AssetBundle更好用,AssetBundle资源更新中对开发者也非常友好。
自身有一套完整的AssetBundle方案(自动打包、发布、更新下载等),没必要更换Addressables。如果没有一套属于自己的资源更新方案,那么建议使用Addressables来作为自己的资源更新方案


三、加载Addressable中的FairyGUI资源

3.1 FairyGUI资源导出


  • 在FairyGUI编辑器中发布资源



  • FairGUI会自动将图片打成图集



  • 图集简单优化
需要去除图集中的Read/Write Enabled、Generate Mip Maps选项,目的是优化图集,减少内存消耗。


至此UI资源已经准备完毕。
3.2 标记UI资源


  • 安装Addressables插件
打开Windows-->Packages Manager


在Uniry Registry中找到Addressables Install



  • 标记UI资源
安装完Addressables后,我们就可以在文件夹或文件中标记为Addressable资源。
在这里我们对UI文件夹标记Addressable,如图所示:






  • 在Addressable窗口中打UI标签
打开Addressables窗口


默认情况下,它是位于Default Group中的,我们可以新建一个组,只放UI资源。选择UI文件夹


选择UI文件夹,我们可以创建标签,创建标签的用途是我们可以通过代码查找标签,一次把所有的UI资源都寻找到。



  • 注意事项
需要注意UI资源的路径,后面需要代码进行加载


至此,准备工作已经准备完毕,下面开始代码编写
3.3 加载UI资源


  • 需要两个字典,用于存储资源
private readonly Dictionary> AssetsList =       new Dictionary>();
private readonly Dictionary> KeyList = new Dictionary>();

  • 预载入并生成包名对应的所有资源地址的列表
通过Addressables.LoadResourceLocationsAsync("UI")获取到Addressables资源内该标签的所有资源
private IEnumerator Preload()
{
    AsyncOperationHandle<IList<IResourceLocation>> handle = Addressables.LoadResourceLocationsAsync("UI");
    yield return handle;
    IList<IResourceLocation> locations = handle.Result;

    //获得所有Label为UI的资源的地址
    foreach (IResourceLocation location in locations)
    {
        string key = location.PrimaryKey.Substring(3);
        key = key.Substring(0, key.IndexOf('_'));
        //key为FairyGUI的包名
        List<string> addresses;
        if (!KeyList.ContainsKey(key))
        {
            addresses = new List<string>();
            KeyList.Add(key, addresses);
        }
        else
        {
            addresses = KeyList[key];
        }

        //将资源地址添加到包名对应的地址列表中
        addresses.Add(location.PrimaryKey);
    }

    Addressables.Release(handle);
}

  • 加载所有包
private IEnumerator DoAddPackages()
{
    if (KeyList.Count <= 0)
    {
        //预载入并生成包名对应的所有资源地址的列表
        yield return Preload();
    }

    //加载所有包
    foreach (var item in KeyList)
    {
        //报名对应的资源列表,进行遍历载入
        List<string> addresses = item.Value;
        foreach (string address in addresses)
        {
            if (AssetsList.ContainsKey(address))
            {
                //目标资源已经缓存则不需要再次载入
                continue;
            }

            AsyncOperationHandle<Object> handle = Addressables.LoadAssetAsync<Object>(address);
            yield return handle;
            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                //载入后缓存
                AssetsList.Add(address, handle);
            }
        }

        UnityEngine.Debug.Log("key:" + item.Key);

        //执行FairyGUI的添加包函数
        UIPackage.AddPackage(item.Key, LoadFunc);
    }

}

  • 执行FairyGUI的添加包函数
LoadFunc方法可以将资源中的.bytesUI源文件,加入到UIPackage中,当执行它完毕后,从Addressables中的UI资源就算加载成功。
// 给FairyGUI的工具函数,用于提供实际的资源给FairyGUI系统
private object LoadFunc(string name, string extension, System.Type type, out DestroyMethod method)
{
    method = DestroyMethod.None;
    string key = $"UI/{name}{extension}";

    //从已载入并缓存的资源列表中查询并返回资源
    return AssetsList.ContainsKey(key) ? AssetsList[key].Result : null;
}

  • 内部原理
如果对UIPackage.Addpackage中的方法很好奇,点击查看代码后
public static UIPackage AddPackage(string assetPath, LoadResource loadFunc)
{
    if (_packageInstById.ContainsKey(assetPath))
        return _packageInstById[assetPath];

    DestroyMethod dm;
    TextAsset asset = (TextAsset)loadFunc(assetPath + "_fui", ".bytes", typeof(TextAsset), out dm);
    if (asset == null)
    {
        if (Application.isPlaying)
            throw new Exception("FairyGUI: Cannot load ui package in '" + assetPath + "'");
        else
            Debug.LogWarning("FairyGUI: Cannot load ui package in '" + assetPath + "'");
    }

    ByteBuffer buffer = new ByteBuffer(asset.bytes);

    UIPackage pkg = new UIPackage();
    pkg._loadFunc = loadFunc;
    pkg._assetPath = assetPath;
    if (!pkg.LoadPackage(buffer, assetPath))
        return null;

    _packageInstById[pkg.id] = pkg;
    _packageInstByName[pkg.name] = pkg;
    _packageInstById[assetPath] = pkg;
    _packageList.Add(pkg);
    return pkg;
}
3.4 释放UI资源


  • 官方文档
资源加载与释放,将链接教程重点细读,它里面设计到优化、自适应等方案

  • Addressables中的释放
当我们调用UIPackage.RemovePackage("package")将UI资源从FairyGUI中释放时,需要注意的是我们还需要将Addressables中的相应资源也进行释放。
还记得我们在之前加载资源时,有一个叫AssetsList字典
private readonly Dictionary<string, AsyncOperationHandle<Object>> AssetsList =
    new Dictionary<string, AsyncOperationHandle<Object>>();
需要调用Addressables.Release(handle)才可以将其释放。
四、Image图片资源加载与释放

在整套UI开发中,我们还会存在部分不同需要代码进行替换;比如背包的相关图片、人物英雄图等等...
在FairyGUI中,是可以将这些图片导入到它自己的编辑器中,导出时该图片会自动形成一个图片集。理论上来说你可以打很多图集,就类似UGUI的图集,但是对于我们来说,想要将图片放入到Addressables中,用一套资源方式来管理UI资源,以及更加灵活的资源释放。
那么这就涉及到FairyGUI如何读取图片资源,以及资源释放问题了。
官方GLoader教程中,告诉我们可以自定义创建,只需要写好资源的加载方式、卸载方式即可。

  • 创建自定义GLoader
public delegate void LoadCompleteCallback(NTexture texture);
public delegate void LoadErrorCallback(string error);

public class ExperimentTextureManager : MonoBehaviour
{
    static ExperimentTextureManager _instance;

    public static ExperimentTextureManager inst
    {
        get
        {
            if (_instance == null)
            {
                GameObject go = new GameObject("ExperimentTextureManager");
                DontDestroyOnLoad(go);
                _instance = go.AddComponent<ExperimentTextureManager>();
            }

            return _instance;
        }
    }

    public const int POOL_CHECK_TIME = 30;
    public const int MAX_POOL_SIZE = 0;

    List<LoadItem> _items;
    bool _started;
    Hashtable _pool;

    private Dictionary<string, AsyncOperationHandle<Texture2D>> texturePools;

    void Awake()
    {
        _items = new List<LoadItem>();
        _pool = new Hashtable();
        texturePools = new Dictionary<string, AsyncOperationHandle<Texture2D>>();

        //StartCoroutine(FreeIdleIcons());
    }

    public static void LoadIcon(string url,
        LoadCompleteCallback onSuccess,
        LoadErrorCallback onFail)
    {
        inst.loadIcon(url, onSuccess, onFail);
    }

    public void loadIcon(string url,
        LoadCompleteCallback onSuccess,
        LoadErrorCallback onFail)
    {
        LoadItem item = new LoadItem();
        item.url = url;
        item.onSuccess = onSuccess;
        item.onFail = onFail;
        _items.Add(item);
        if (!_started)
            StartCoroutine(Run());
    }

    public static void ReleaseIconAll()
    {
        inst.releaseIconAll();
    }

    void releaseIconAll()
    {
        ArrayList toRemove = null;
        foreach (DictionaryEntry de in _pool)
        {
            string key = (string)de.Key;
            NTexture texture = (NTexture)de.Value;

            if (texture.refCount == 0)
            {
                if (toRemove == null)
                    toRemove = new ArrayList();

                toRemove.Add(key);
                texture.Dispose();

                Addressables.Release(texturePools[key]);

                //Debug.Log("释放资源:" + key);
            }
        }

        if (toRemove != null)
        {
            foreach (string key in toRemove)
            {
                //Debug.Log("Remove资源:" + key);

                _pool.Remove(key);
                texturePools.Remove(key);
            }

        }

        Resources.UnloadUnusedAssets();
        Caching.ClearCache();
    }

    IEnumerator Run()
    {
        _started = true;

        LoadItem item = null;
        while (true)
        {
            if (_items.Count > 0)
            {
                item = _items[0];
                _items.RemoveAt(0);
            }
            else
                break;

            if (_pool.ContainsKey(item.url))
            {
                NTexture texture = (NTexture)_pool[item.url];

                texture.refCount++;

                if (item.onSuccess != null)
                    item.onSuccess(texture);

                continue;
            }

            string url = item.url;

            if (texturePools.ContainsKey(url))
            {
                NTexture texture = new NTexture(texturePools[url].Result);
                texture.destroyMethod = DestroyMethod.Unload;

                texture.refCount++;

                _pool[item.url] = texture;

                if (item.onSuccess != null)
                    item.onSuccess(texture);
               
                continue;
            }

            AsyncOperationHandle<Texture2D> handle = Addressables.LoadAssetAsync<Texture2D>(url);

            yield return handle;

            if (handle.Status == AsyncOperationStatus.Succeeded)
            {
                NTexture texture = new NTexture(handle.Result);
                texture.destroyMethod = DestroyMethod.Unload;
                texture.refCount++;

                _pool[item.url] = texture;

                if (item.onSuccess != null)
                    item.onSuccess(texture);

                texturePools.Add(url, handle);
            }
            else
            {
                if (item.onFail != null)
                    item.onFail(handle.OperationException.Message);
            }
        }

        _started = false;
    }

    IEnumerator FreeIdleIcons()
    {
        while (true)
        {
            yield return new WaitForSeconds(POOL_CHECK_TIME); //check the pool every 30 seconds

            int cnt = _pool.Count;
            if (cnt > MAX_POOL_SIZE)
            {
                ArrayList toRemove = null;
                foreach (DictionaryEntry de in _pool)
                {
                    string key = (string)de.Key;
                    NTexture texture = (NTexture)de.Value;

                    if (texture.refCount == 0)
                    {
                        if (toRemove == null)
                            toRemove = new ArrayList();

                        toRemove.Add(key);
                        texture.Unload();

                        texture.Dispose();

                        //Addressables.ResourceManager.Release(texturePools[key]);
                        Addressables.Release(texturePools[key]);

                        Debug.Log("释放资源:" + key);

                        cnt--;
                        if (cnt <= 0)
                            break;
                    }
                }

                if (toRemove != null)
                {
                    foreach (string key in toRemove)
                    {
                        Debug.Log("Remove资源:" + key);

                        _pool.Remove(key);
                        texturePools.Remove(key);
                    }

                }

                Resources.UnloadUnusedAssets();
                Caching.ClearCache();
            }
        }
    }
}

public class LoadItem
{
    public string url;
    public LoadCompleteCallback onSuccess;
    public LoadErrorCallback onFail;
}

public class ExperimentGLoader : GLoader
{
    protected override void LoadExternal()
    {
        ExperimentTextureManager.LoadIcon(this.url, OnLoadSuccess, OnLoadFail);
    }

    protected override void FreeExternal(NTexture texture)
    {
        texture.refCount--;
    }

    void OnLoadSuccess(NTexture texture)
    {
        if (string.IsNullOrEmpty(this.url))
            return;

        this.onExternalLoadSuccess(texture);
    }

    void OnLoadFail(string error)
    {
        Debug.Log("load " + this.url + " failed: " + error);
        this.onExternalLoadFailed();
    }
}
只需要重点关注ExperimentTextureManager中的Run()和releaseIconAll(),Run方法里面执行的是从Addressable资源中进行读取,根据路径加载资源

  • 加载自定义GLoader
需要在Awake()中将自定义的Loader脚本设置到FairyGUI中
UIObjectFactory.SetLoaderExtension(typeof(ExperimentGLoader));

  • 加载图片到GLoader中
那么怎么将图片加入到GLoader中呢?如下所示:
string url = $"Experiment/Image/{veesData.number}.jpg";

if (!string.IsNullOrEmpty(url))
    com.GetChild("n8").asCom.GetChild("n0").asLoader.url = url;
url:是Addressables中该图片地址
它是怎么进行工作的?
当我们设置GLoader中的url时,内部调用GLoader方法,进而ExperimentGLoader中
public class ExperimentGLoader : GLoader
{
    protected override void LoadExternal()
    {
        ExperimentTextureManager.LoadIcon(this.url, OnLoadSuccess, OnLoadFail);
    }
}
继续深入走... 协程调用Run()方法,在前面介绍中提到Run()方法主要是从Addressables进行读取,这里不对Run方法进行详解,具体代码可复制创建自定义GLoader这节代码,进行详读。
public static void LoadIcon(string url,
            LoadCompleteCallback onSuccess,
            LoadErrorCallback onFail)
        {
            inst.loadIcon(url, onSuccess, onFail);
        }

        public void loadIcon(string url,
            LoadCompleteCallback onSuccess,
            LoadErrorCallback onFail)
        {
            LoadItem item = new LoadItem();
            item.url = url;
            item.onSuccess = onSuccess;
            item.onFail = onFail;
            _items.Add(item);
            if (!_started)
                StartCoroutine(Run());
        }

  • 释放图片资源
既然知道资源怎么加载后,那我们就可以知道资源是如何释放的;
参考ExperimentTextureManager类中的releaseIconAll()方法即可。需要在FairyGUI中将资源释放、同时也要在Addressables中将资源进行释放。
public static void ReleaseIconAll()
        {
            inst.releaseIconAll();
        }

        void releaseIconAll()
        {
            ArrayList toRemove = null;
            foreach (DictionaryEntry de in _pool)
            {
                string key = (string)de.Key;
                NTexture texture = (NTexture)de.Value;

                if (texture.refCount == 0)
                {
                    if (toRemove == null)
                        toRemove = new ArrayList();

                    toRemove.Add(key);
                    texture.Dispose();

                    Addressables.Release(texturePools[key]);

                    //Debug.Log("释放资源:" + key);
                }
            }

            if (toRemove != null)
            {
                foreach (string key in toRemove)
                {
                    //Debug.Log("Remove资源:" + key);

                    _pool.Remove(key);
                    texturePools.Remove(key);
                }

            }

            Resources.UnloadUnusedAssets();
            Caching.ClearCache();
        }

  • 资源池
在上述代码介绍中,我们为了防止同一个资源加载多次,一般我们会用一个资源池(Pools)
private Dictionary<string, AsyncOperationHandle<Texture2D>> texturePools;
避免多次从Addressables进行加载,节省内存。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 12:31 , Processed in 0.096858 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表