yyl
2026-04-24 97de31e9a015cf139f5293a22e1575a43dfb6733
Main/ResModule/ResManager.cs
@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using System.Collections.Generic;
using System;
using UnityEngine.U2D;
@@ -10,10 +10,6 @@
using System.Threading;
using ProjSG.Resource;
using YooAsset;
#if UNITY_EDITOR
@@ -131,142 +127,130 @@
    }
#endif
    //  // needExt 是否需要函数内部添加后缀
    // [System.Obsolete("US2: Use LoadAssetAsync<T>(directory, name, needExt) returning UniTask<T> instead.")]
    // public T LoadAsset<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
    // {
    //     directory = directory.Replace("\\", "/");
    //     name = name.Replace("\\", "/");
    //     //  特殊处理 因为有一层图集的关系 directory要传入的应该是atlas的名字
    //     if (typeof(T) == typeof(Sprite))
    //     {
    //         return LoadSprite(directory, name) as T;
    //     }
    //     else if (typeof(T) == typeof(SkeletonDataAsset))
    //     {
    //         //文件目录调整,name中包含了路径
    //         if (name.Contains("/"))
    //         {
    //             directory += name.Substring(0, name.LastIndexOf("/"));
    //             name = name.Substring(name.LastIndexOf("/") + 1);
    //         }
    // ====================================================================
    // 同步方法(仅非 WebGL 平台)
    // ====================================================================
#if !UNITY_WEBGL
    //needExt 是否需要函数内部添加后缀
    public T LoadAsset<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
    {
        directory = directory.Replace("\\", "/");
        name = name.Replace("\\", "/");
        //  特殊处理 因为有一层图集的关系 directory要传入的应该是atlas的名字
        if (typeof(T) == typeof(Sprite))
        {
            return LoadSprite(directory, name) as T;
        }
        else if (typeof(T) == typeof(SkeletonDataAsset))
        {
            //文件目录调整,name中包含了路径
            if (name.Contains("/"))
            {
                directory += name.Substring(0, name.LastIndexOf("/"));
                name = name.Substring(name.LastIndexOf("/") + 1);
            }
        }
    //     }
        return LoadAssetInternal<T>(directory, name, needExt);
    }
    //     return LoadAssetInternal<T>(directory, name, needExt);
    // }
    //needExt 是否需要函数内部添加后缀
    private T LoadAssetInternal<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
    {
        T asset = null;
        var path = ($"Assets/ResourcesOut/{directory}/{name}" + (needExt ? GetExtension(typeof(T)) : "")).Replace("//", "/").Trim().Replace("\\", "/");
    // //needExt 是否需要函数内部添加后缀
    // private T LoadAssetInternal<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
    // {
    //     // 已禁用同步加载,强制业务全部走异步API。
    //     throw new NotSupportedException("同步资源加载已禁用,请使用异步接口");
    // }
        if (!AssetSource.isUseAssetBundle)
        {
#if UNITY_EDITOR
            asset = UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
#endif
        }
        else
        {
            asset = YooAssetService.Instance.LoadAssetSync<T>(path);
        }
//     [System.Obsolete("US2: Use LoadConfigAsync returning UniTask<string[]> instead.")]
//     public string[] LoadConfig(string name)
//     {
//         string path = string.Empty;
// #if UNITY_EDITOR
//         if (!AssetSource.isUseAssetBundle)
//         {
//             path = ResourcesPath.CONFIG_FODLER + "/" + name + ".txt";
//         }
//         else
// #endif
//         {
//             path = AssetVersionUtility.GetAssetFilePath($"config/{name}.txt");
//         }
        if (asset == null)
        {
            Debug.LogErrorFormat("LoadAsset() => 加载不到资源: {0}", path);
        }
//         return File.ReadAllLines(path);
//     }
        return asset;
    }
    public string[] LoadConfig(string name)
    {
#if UNITY_EDITOR
        if (!AssetSource.isUseAssetBundle)
        {
            string path = ResourcesPath.CONFIG_FODLER + "/" + name + ".txt";
            return File.ReadAllLines(path);
        }
#endif
        // AB 模式:通过 YooAsset 同步加载 TextAsset
        var location = $"Assets/ResourcesOut/Config/{name}.txt";
        var textAsset = YooAssetService.Instance.LoadAssetSync<TextAsset>(location);
        if (textAsset != null && !string.IsNullOrEmpty(textAsset.text))
            return textAsset.text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
        Debug.LogError($"[ResManager] LoadConfig failed for '{name}'");
        return Array.Empty<string>();
    }
    private Sprite LoadSprite(string atlasName, string spriteName)
    {
        if (!AssetSource.isUseAssetBundle)
        {
            SpriteAtlas atlas = LoadAsset<SpriteAtlas>("Sprite", atlasName.Replace("Sprite/", ""));
            if (null == atlas)
            {
                return null;
            }
            return atlas.GetSprite(spriteName);
        }
        else
        {
            // YooAsset 使用完整路径直接加载 Sprite
            var path = $"Assets/ResourcesOut/{atlasName}/{spriteName}.png".Replace("//", "/");
            return YooAssetService.Instance.LoadAssetSync<Sprite>(path);
        }
    }
#endif
    // ====================================================================
    // 异步加载方法(所有平台统一)
    // ====================================================================
    //needExt 是否需要函数内部添加后缀(回调版本)
    public void LoadAssetAsync<T>(string directory, string name, Action<bool, UnityEngine.Object> callBack, bool needExt = true) where T : UnityEngine.Object
    {
        directory = directory.Replace("\\", "/");
        name = name.Replace("\\", "/");
        //  特殊处理 因为有一层图集的关系 directory要传入的应该是atlas的名字
        if (typeof(T) == typeof(Sprite))
        {
            LoadSpriteAsync<T>(directory, name, callBack);
            return;
        }
        else if (typeof(T) == typeof(SkeletonDataAsset))
        {
            //文件目录调整,name中包含了路径
            if (name.Contains("/"))
            {
                directory += name.Substring(0, name.LastIndexOf("/"));
                name = name.Substring(name.LastIndexOf("/") + 1);
            }
        }
        LoadAssetAsyncInternal<T>(directory, name, callBack, needExt);
    }
    public async UniTask<T> LoadAssetAsync<T>(string directory, string name) where T : UnityEngine.Object
    {
        return await LoadAssetAsync<T>(directory, name, needExt: true);
    }
    public async UniTask<T> LoadAssetAsync<T>(string directory, string name, bool needExt = true) where T : UnityEngine.Object
    {
        return await LoadAssetAsync<T>(directory, name, needExt, CancellationToken.None);
    }
    private void LoadSpriteAsync<T>(string atlasName, string spriteName, Action<bool, UnityEngine.Object> callBack) where T : UnityEngine.Object
    {
        if (!AssetSource.isUseAssetBundle)
        {
            // Editor 模式下可直接加载 sprite
            LoadAssetAsyncInternal<T>(atlasName, spriteName, callBack);
        }
        else
        {
            // AB 模式下直接加载单独的 Sprite 文件(YooAsset 自动处理 SpriteAtlas 依赖)
            LoadAssetAsyncInternal<Sprite>(atlasName, spriteName, (isLoaded, sprite) =>
            {
                callBack?.Invoke(isLoaded, sprite);
            });
        }
    }
    private void LoadAssetAsyncInternal<T>(string directory, string name, Action<bool, UnityEngine.Object> callBack, bool needExt = true) where T : UnityEngine.Object
    {
        var path = string.Concat($"Assets/ResourcesOut/{directory}/{name}", (needExt ? GetExtension(typeof(T)) : "")).Replace("//", "/");
        if (!AssetSource.isUseAssetBundle)
        {
#if UNITY_EDITOR
            var asset = UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
            callBack?.Invoke(asset != null, asset);
#endif
        }
        else
        {
            // US1: Route through YooAssetService async
            CoLoadViaYooAsset<T>(path, callBack).Forget();
        }
    }
    private async UniTaskVoid CoLoadViaYooAsset<T>(string path, Action<bool, UnityEngine.Object> callBack, CancellationToken ct = default) where T : UnityEngine.Object
    {
        try
        {
            var asset = await YooAssetService.Instance.LoadAssetAsync<T>(path, ct: ct);
            callBack?.Invoke(asset != null, asset);
        }
        catch (Exception ex)
        {
            Debug.LogError($"[ResManager] Async load via YooAsset failed: {ex.Message}");
            callBack?.Invoke(false, null);
        }
    }
    public void UnloadAsset(string directory, string assetName)
    {
        directory = directory.Replace("\\", "/").TrimEnd('/');
        assetName = assetName.Replace("\\", "/");
        string path = ($"Assets/ResourcesOut/{directory}/{assetName}").Replace("//", "/");
        YooAssetService.Instance.UnloadAsset(path);
    }
    public string GetAssetFilePath(string _assetKey)
    {
        var path = Path.Combine(ExternalStorePath, _assetKey);
        if (!File.Exists(path))
        {
            path = Path.Combine(StreamingAssetPath, _assetKey);
        }
        return path;
    }
    // ====================================================================
    // US1: New UniTask-based async variants
    // ====================================================================
    /// <summary>
    /// 异步加载资源(UniTask 版本,US1 新增)。
    /// </summary>
    public async UniTask<T> LoadAssetAsync<T>(string directory, string name, bool needExt = true, CancellationToken ct = default) where T : UnityEngine.Object
    {
        directory = directory.Replace("\\", "/");
@@ -307,8 +291,126 @@
        return await YooAssetService.Instance.LoadAssetAsync<T>(path, ct: ct);
    }
    // ====================================================================
    // 异步内部实现(统一)
    // ====================================================================
    private void LoadSpriteAsync<T>(string atlasName, string spriteName, Action<bool, UnityEngine.Object> callBack) where T : UnityEngine.Object
    {
        if (!AssetSource.isUseAssetBundle)
        {
            // Editor 模式下可直接加载 sprite
            LoadAssetAsyncInternal<T>(atlasName, spriteName, callBack);
        }
        else
        {
            // AB 模式下直接加载单独的 Sprite 文件(YooAsset 自动处理 SpriteAtlas 依赖)
            LoadAssetAsyncInternal<Sprite>(atlasName, spriteName, (isLoaded, sprite) =>
            {
                callBack?.Invoke(isLoaded, sprite);
            });
        }
    }
    private void LoadAssetAsyncInternal<T>(string directory, string name, Action<bool, UnityEngine.Object> callBack, bool needExt = true) where T : UnityEngine.Object
    {
        var path = string.Concat($"Assets/ResourcesOut/{directory}/{name}", (needExt ? GetExtension(typeof(T)) : "")).Replace("//", "/");
        if (!AssetSource.isUseAssetBundle)
        {
#if UNITY_EDITOR
            var asset = UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
            callBack?.Invoke(asset != null, asset);
#endif
        }
        else
        {
            CoLoadViaYooAsset<T>(path, callBack).Forget();
        }
    }
    private async UniTaskVoid CoLoadViaYooAsset<T>(string path, Action<bool, UnityEngine.Object> callBack, CancellationToken ct = default) where T : UnityEngine.Object
    {
        try
        {
            var asset = await YooAssetService.Instance.LoadAssetAsync<T>(path, ct: ct);
            callBack?.Invoke(asset != null, asset);
        }
        catch (Exception ex)
        {
            Debug.LogError($"[ResManager] Async load via YooAsset failed: {ex.Message}");
            callBack?.Invoke(false, null);
        }
    }
    public void UnloadAsset(string directory, string assetName)
    {
        if (!AssetSource.isUseAssetBundle)
            return;
        directory = directory.Replace("\\", "/").TrimEnd('/');
        assetName = assetName.Replace("\\", "/");
        string path = ($"Assets/ResourcesOut/{directory}/{assetName}").Replace("//", "/");
        YooAssetService.Instance.UnloadAsset(path);
    }
    public string GetAssetFilePath(string _assetKey)
    {
        var path = Path.Combine(ExternalStorePath, _assetKey);
        if (!File.Exists(path))
        {
            path = Path.Combine(StreamingAssetPath, _assetKey);
        }
        return path;
    }
    // ====================================================================
    // LoadConfigAsync(所有平台统一)
    // ====================================================================
    /// <summary>
    /// US4: 异步加载资源并走缓存层(缓存命中直接返回,未命中则加载并缓存)。
    /// 异步加载配置文件。
    /// AB 模式使用 YooAsset 异步加载 TextAsset,非 AB 模式直接读文件。
    /// </summary>
    public async UniTask<string[]> LoadConfigAsync(string name, bool needExt = true, CancellationToken ct = default)
    {
        if (AssetSource.isUseAssetBundle)
        {
            if (name.EndsWith(".txt") && needExt)
            {
                name = name.Substring(0, name.Length - 4);
            }
            var location = $"Assets/ResourcesOut/Config/{name}" + (needExt ? ".txt" : "");
            try
            {
                var asset = await YooAssetService.Instance.LoadAssetAsync(
                    location, typeof(TextAsset), 0, ct) as TextAsset;
                if (asset != null && !string.IsNullOrEmpty(asset.text))
                    return asset.text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
            }
            catch (Exception ex)
            {
                Debug.LogError($"[ResManager] LoadConfigAsync YooAsset failed for '{name}': {ex.Message}");
            }
            return Array.Empty<string>();
        }
        // 非 AB 模式: 直接读文件(Editor 开发模式)
#if UNITY_EDITOR
        string path = ResourcesPath.CONFIG_FODLER + "/" + name + (needExt ? ".txt" : "");
        return await UniTask.RunOnThreadPool(() => File.ReadAllLines(path));
#else
        return Array.Empty<string>();
#endif
    }
    // ====================================================================
    // 缓存加载 & Sprite 异步加载(所有平台)
    // ====================================================================
    /// <summary>
    /// 异步加载资源并走缓存层(缓存命中直接返回,未命中则加载并缓存)。
    /// </summary>
    public async UniTask<T> LoadAssetCachedAsync<T>(string directory, string name, bool needExt = true, CancellationToken ct = default) where T : UnityEngine.Object
    {
@@ -317,7 +419,7 @@
        var path = ($"Assets/ResourcesOut/{directory}/{name}" + (needExt ? GetExtension(typeof(T)) : ""))
            .Replace("//", "/").Trim().Replace("\\", "/");
        // 音频文件扩展名统一转小写,避免 .WAV/.MP3 大小写不匹配
        // 音频文件扩展名统一转小写
        if (typeof(T) == typeof(AudioClip))
        {
            var pathExt2 = System.IO.Path.GetExtension(path);
@@ -348,58 +450,10 @@
            var path = $"Assets/ResourcesOut/{atlasName}/{spriteName}.png"
                .Replace("//", "/").Trim().Replace("\\", "/");
            var sprite = await YooAssetService.Instance.LoadAssetAsync<Sprite>(path, ct: ct);
#if UNITY_WEBGL
            if (sprite == null)
                Debug.LogWarning($"[ResManager][WebGL-Diag] Sprite load returned NULL: path={path}");
#endif
                Debug.LogWarning($"[ResManager] Sprite load returned NULL: path={path}");
            return sprite;
        }
    }
    /// <summary>
    /// 异步加载配置文件(UniTask 版本)。
    /// AB 模式使用 YooAsset RawFile 异步加载,非 AB 模式直接读文件。
    /// </summary>
    public async UniTask<string[]> LoadConfigAsync(string name, bool needExt = true, CancellationToken ct = default)
    {
        // AB 模式(含 WebGL): 使用 YooAsset 加载配置文件
        if (AssetSource.isUseAssetBundle)
        {
            // 判断一下是否原来已经包含.txt 配合 needExt 做兼容,避免调用方传入重复后缀导致路径错误
            if (name.EndsWith(".txt") && needExt)
            {
                name = name.Substring(0, name.Length - 4);
            }
            var location = $"Assets/ResourcesOut/Config/{name}" + (needExt ? ".txt" : "");
            try
            {
#if UNITY_WEBGL
                // WebGL: WebServerFileSystem 不支持 LoadRawFileAsync,改用 LoadAssetAsync<TextAsset>
                // .txt 文件在 Unity 中以 TextAsset 形式导入,WebGL 支持此加载方式
                var asset = await ProjSG.Resource.YooAssetService.Instance.LoadAssetAsync(
                    location, typeof(UnityEngine.TextAsset), 0, ct) as UnityEngine.TextAsset;
                if (asset != null && !string.IsNullOrEmpty(asset.text))
                    return asset.text.Split(new[] { "\r\n", "\n" }, System.StringSplitOptions.None);
#else
                var textAsset = await ProjSG.Resource.YooAssetService.Instance.LoadAssetAsync<TextAsset>(location, 0, ct);
                if (textAsset != null && !string.IsNullOrEmpty(textAsset.text))
                    return textAsset.text.Split(new[] { "\r\n", "\n" }, System.StringSplitOptions.None);
#endif
            }
            catch (System.Exception ex)
            {
                UnityEngine.Debug.LogError($"[ResManager] LoadConfigAsync YooAsset failed for '{name}': {ex.Message}");
            }
            return System.Array.Empty<string>();
        }
        // 非 AB 模式: 直接读文件(Editor 开发模式)
#if UNITY_EDITOR
        string path = ResourcesPath.CONFIG_FODLER + "/" + name + (needExt ? ".txt" : "");
        return await UniTask.RunOnThreadPool(() => File.ReadAllLines(path));
#else
        return System.Array.Empty<string>();
#endif
    }
}
}