Unity 在GameFrameWork中实现异步加载Luban全表

1,020 阅读3分钟

前言

针对游戏中Luban生成的表过多时,直接加载可能卡住主线程的问题,使用异步加载显得很有必要,可以避免在游戏初始的读表过程卡住主线程。

针对Luban Next版本,在Unity GameFrameWork下新增一个LubanComponent实现异步读表的过程。

同时,对于GameFrameWork打包后获取资源路径的问题,不能直接获取到Luban导出表的路径,需要借助GameFrameWork的GameEntry.Resource.LoadAsset函数拿到Luban导出的表。

本篇博客主要用于解决上述两个问题。

具体流程

1. 修改Luban的代码生成模板

本来想新增一个自定义模板的,但是因为Luban的官方文档不清晰,经过尝试也没啥用,所以直接修改的原来的代码生成模板。 (吐槽一句,Luban是个很好用的工具,但是写的文档策划看不懂,程序员也看不懂,或许是我太菜了的缘故)

首先,找到Luban的自定义代码生成模板所在,用作演示的路径如下,因为每个人的Luban工具代码不一样,所以需要找到自己的。

Pasted image 20250114002130.png

然后,替换tables.sbn中的代码为如下代码

using Luban;
using SimpleJSON;
using System.Threading.Tasks;

{{namespace_with_grace_begin __namespace}}
public partial class {{__name}}
{
    {{~for table in __tables ~}}
{{~if table.comment != '' ~}}
    /// <summary>
    /// {{escape_comment table.comment}}
    /// </summary>
{{~end~}}
    public {{table.full_name}} {{format_property_name __code_style table.name}} {get; private set;}
    {{~end~}}

    public async Task LoadAsync(System.Func<string, Task<JSONNode>> loader)
    {
        {{~for table in __tables ~}}
        {{format_property_name __code_style table.name}} = new {{table.full_name}}(await loader("{{table.output_data_file}}"));
        {{~end~}}
        ResolveRef();
    }
    
    private void ResolveRef()
    {
        {{~for table in __tables ~}}
        {{format_property_name __code_style table.name}}.ResolveRef(this);
        {{~end~}}
    }
}

{{namespace_with_grace_end __namespace}}

使用gen.bat重新生成代码。

2. LubanComponent的实现

具体代码如下

    public class LubanComponent : GameFrameworkComponent
    {
        private cfg.Tables m_LubanTable = new cfg.Tables();
        public cfg.Tables LubanTable => m_LubanTable;
        
        private Dictionary<string, bool> m_LoadedFlag = new Dictionary<string, bool>();

        private Dictionary<string, TaskCompletionSource<TextAsset>> m_LubanTableTcs = new Dictionary<string, TaskCompletionSource<TextAsset>>();

        public async void LoadLubanTable(string loadFlagKey, object userData)
        {
            m_LoadedFlag.Clear();
            m_LubanTableTcs.Clear();

            await m_LubanTable.LoadAsync(LoadLuban);

            if (IsLoadSuccessful())
            {
                // 触发Luban表加载成功事件
                GameEntry.Event.Fire(this, LoadLubanTableSuccessEventArgs.Create(loadFlagKey, userData));
            }
            else
            {
                // 触发Luban表加载失败事件
                GameEntry.Event.Fire(this, LoadLubanTableFailureEventArgs.Create(loadFlagKey, userData));
            }

        }

        private bool IsLoadSuccessful()
        {
            foreach (var flag in m_LoadedFlag)
            {
                if (!flag.Value)
                {
                    return false;
                }
            }

            return true;
        }

        private async Task<JSONNode> LoadLuban(string file)
        {
            return JSON.Parse((await LoadLubanTableAssetAsync(file)).text);
        }

        private Task<TextAsset> LoadLubanTableAssetAsync(string assetName)
        {
            var tcs = new TaskCompletionSource<TextAsset>();
            string assetFullName = AssetUtility.GetLubanTableAsset(assetName);
            m_LubanTableTcs.Add(assetFullName, tcs);
            m_LoadedFlag.Add(assetFullName, false);

            GameEntry.Resource.LoadAsset(assetFullName, 
            new LoadAssetCallbacks(
                (string assetName, object asset, float duration, object userData) =>
                {
                    m_LubanTableTcs.TryGetValue(assetName, out tcs);
                    if (tcs != null)
                    {
                        m_LoadedFlag[assetName] = true;
                        tcs.SetResult((TextAsset)asset);
                        m_LubanTableTcs.Remove(assetName);
                        Log.Info($"加载Luban表{assetName}成功");
                    }
                },
                (string assetName, LoadResourceStatus status, string errorMessage, object userData) =>
                {
                    m_LubanTableTcs.TryGetValue(assetName, out tcs);
                    if (tcs != null)
                    {
                        tcs.SetCanceled();
                        m_LubanTableTcs.Remove(assetName);
                    }

                    Log.Error($"从{assetName}加载Luban表失败,失败信息:{errorMessage}");
                }
            ));

            return tcs.Task;
        }
    }

其中LoadLubanTableSuccessEventArgs和LoadLubanTableFailureEventArgs均继承自GameEventArgs,具体实现这里按下不表,逻辑可以参考框架原来的一些GameEventArgs,以及烟雨大佬的博客

实现之后,可以在GameEntry加入LubanComponent方便后续拿取。

3. 流程中加载Luban全表的代码

我们在ProcedurePreload完成Luban的全表加载,加载代码如下

        private void PreloadResources()
        {
            LoadLubanTable("LubanGeneratedTables");
        }

        private void LoadLubanTable(string directory)
        {
            string directoryName = AssetUtility.GetAssetRootDirectory(directory);
            m_LoadedFlag.Add(directoryName, false);
            GameEntry.Luban.LoadLubanTable(directoryName, this);
        }

        private void OnLoadLubanTableSuccess(object sender, GameEventArgs e)
        {
            LoadLubanTableSuccessEventArgs ne = (LoadLubanTableSuccessEventArgs) e;
            if (ne.UserData != this)
            {
                return;
            }

            m_LoadedFlag[ne.LoadFlagKey] = true;
            Log.Info("Preload luban success" + ne.LoadFlagKey);
        }

        private void OnLoadLubanTableFailure(object sender, GameEventArgs e)
        {
            LoadLubanTableFailureEventArgs ne = (LoadLubanTableFailureEventArgs) e;
            if (ne.UserData != this)
            {
                return;
            }

            Log.Error("Can not preload luban");
        }

通过下面的代码拿取想要的表的值

GameEntry.Luban.LubanTable.TbdataTest.DataList[0].Key

结语

整个代码思路参考此开源项目中的Luban部分:github.com/DangoRyn/Un…,感谢大佬的开源

因为此开源项目中的Luban是classic版本,因此针对这个情况做出一定的更新