using UnityEngine; using System.Collections.Generic; using System; using UnityEngine.U2D; using System.IO; using UnityEngine.Video; using Spine.Unity; using UnityEngine.UI; using Cysharp.Threading.Tasks; using System.Threading; using ProjSG.Resource; using YooAsset; #if UNITY_EDITOR using UnityEditor; #endif using LaunchCommon; public class ResManager : Singleton { //不下载时本地安卓测试路径 // public string assetBundlesPath = Application.dataPath + "/../AssetBundles/Android/"; // public static string bytesFolderName = "logicbytes/"; public string StreamingAssetPath { get { return ResourcesPath.Instance.StreamingAssetPath; } } public string ExternalStorePath { get { return ResourcesPath.Instance.ExternalStorePath; } } //用于editor 下的资源路径 public string ResourcesOutPath { get { return ResourcesPath.ResourcesOutPath; } } public string CONFIG_FODLER { get { return ResourcesPath.CONFIG_FODLER; } } public string ResourcesOutAssetPath { get { return ResourcesPath.ResourcesOutAssetPath; } } private static readonly Dictionary fileExtensionDict = new Dictionary() { {typeof(GameObject), "prefab"}, {typeof(Sprite), "png"}, {typeof(Texture2D), "jpg"}, {typeof(Texture), "jpg"}, {typeof(Shader), "shader"}, {typeof(TextAsset), "txt"}, {typeof(AudioClip), "wav"}, {typeof(Font), "ttf"}, {typeof(Material), "mat"}, {typeof(VideoClip), "mp4"}, {typeof(SpriteAtlas), "spriteatlasv2"}, {typeof(SkeletonDataAsset), "asset"}, }; public void Init() { } public void Release() { } private string GetExtension(Type type) { if (fileExtensionDict.TryGetValue(type, out string extension)) return "." + extension; else { Debug.LogErrorFormat("GetExtension() => 不支持的资源类型: {0}.", type.Name); return ""; } } #if UNITY_EDITOR public string GetFullDirectory(string directory) { string fullDirectory = Path.Combine(ResourcesOutPath, directory); return fullDirectory; } public string FindFilePath(string directory, string fileName) { string[] files = Directory.GetFiles(GetFullDirectory(directory), fileName, SearchOption.AllDirectories); if (files.Length > 0) { return files[0]; } return string.Empty; } public string GetRelativePath(string absolutePath) { string relativePath = absolutePath.Replace(ResourcesOutPath, "Assets/ResourcesOut/"); return relativePath; } #endif // ==================================================================== // 同步方法(仅非 WebGL 平台) // ==================================================================== #if !UNITY_WEBGL //needExt 是否需要函数内部添加后缀 public T LoadAsset(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(directory, name, needExt); } //needExt 是否需要函数内部添加后缀 private T LoadAssetInternal(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("\\", "/"); if (!AssetSource.isUseAssetBundle) { #if UNITY_EDITOR asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path); #endif } else { asset = YooAssetService.Instance.LoadAssetSync(path); } if (asset == null) { Debug.LogErrorFormat("LoadAsset() => 加载不到资源: {0}", 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(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(); } private Sprite LoadSprite(string atlasName, string spriteName) { if (!AssetSource.isUseAssetBundle) { SpriteAtlas atlas = LoadAsset("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(path); } } #endif // ==================================================================== // 异步加载方法(所有平台统一) // ==================================================================== //needExt 是否需要函数内部添加后缀(回调版本) public void LoadAssetAsync(string directory, string name, Action callBack, bool needExt = true) where T : UnityEngine.Object { directory = directory.Replace("\\", "/"); name = name.Replace("\\", "/"); // 特殊处理 因为有一层图集的关系 directory要传入的应该是atlas的名字 if (typeof(T) == typeof(Sprite)) { LoadSpriteAsync(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(directory, name, callBack, needExt); } public async UniTask LoadAssetAsync(string directory, string name) where T : UnityEngine.Object { return await LoadAssetAsync(directory, name, needExt: true); } public async UniTask LoadAssetAsync(string directory, string name, bool needExt = true, CancellationToken ct = default) where T : UnityEngine.Object { directory = directory.Replace("\\", "/"); name = name.Replace("\\", "/"); if (typeof(T) == typeof(Sprite)) { return await LoadSpriteAsyncUniTask(directory, name, ct) as T; } if (typeof(T) == typeof(SkeletonDataAsset)) { if (name.Contains("/")) { directory += name.Substring(0, name.LastIndexOf("/")); name = name.Substring(name.LastIndexOf("/") + 1); } } var path = ($"Assets/ResourcesOut/{directory}/{name}" + (needExt ? GetExtension(typeof(T)) : "")) .Replace("//", "/").Trim().Replace("\\", "/"); // 音频文件扩展名统一转小写,避免 .WAV/.MP3 大小写不匹配 if (typeof(T) == typeof(AudioClip)) { var pathExt = System.IO.Path.GetExtension(path); if (!string.IsNullOrEmpty(pathExt) && pathExt != pathExt.ToLower()) path = path.Substring(0, path.Length - pathExt.Length) + pathExt.ToLower(); } if (!AssetSource.isUseAssetBundle) { #if UNITY_EDITOR return UnityEditor.AssetDatabase.LoadAssetAtPath(path); #else return null; #endif } return await YooAssetService.Instance.LoadAssetAsync(path, ct: ct); } // ==================================================================== // 异步内部实现(统一) // ==================================================================== private void LoadSpriteAsync(string atlasName, string spriteName, Action callBack) where T : UnityEngine.Object { if (!AssetSource.isUseAssetBundle) { // Editor 模式下可直接加载 sprite LoadAssetAsyncInternal(atlasName, spriteName, callBack); } else { // AB 模式下直接加载单独的 Sprite 文件(YooAsset 自动处理 SpriteAtlas 依赖) LoadAssetAsyncInternal(atlasName, spriteName, (isLoaded, sprite) => { callBack?.Invoke(isLoaded, sprite); }); } } private void LoadAssetAsyncInternal(string directory, string name, Action 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(path); callBack?.Invoke(asset != null, asset); #endif } else { CoLoadViaYooAsset(path, callBack).Forget(); } } private async UniTaskVoid CoLoadViaYooAsset(string path, Action callBack, CancellationToken ct = default) where T : UnityEngine.Object { try { var asset = await YooAssetService.Instance.LoadAssetAsync(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(所有平台统一) // ==================================================================== /// /// 异步加载配置文件。 /// AB 模式使用 YooAsset 异步加载 TextAsset,非 AB 模式直接读文件。 /// public async UniTask 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(); } // 非 AB 模式: 直接读文件(Editor 开发模式) #if UNITY_EDITOR string path = ResourcesPath.CONFIG_FODLER + "/" + name + (needExt ? ".txt" : ""); return await UniTask.RunOnThreadPool(() => File.ReadAllLines(path)); #else return Array.Empty(); #endif } // ==================================================================== // 缓存加载 & Sprite 异步加载(所有平台) // ==================================================================== /// /// 异步加载资源并走缓存层(缓存命中直接返回,未命中则加载并缓存)。 /// public async UniTask LoadAssetCachedAsync(string directory, string name, bool needExt = true, CancellationToken ct = default) where T : UnityEngine.Object { directory = directory.Replace("\\", "/"); name = name.Replace("\\", "/"); var path = ($"Assets/ResourcesOut/{directory}/{name}" + (needExt ? GetExtension(typeof(T)) : "")) .Replace("//", "/").Trim().Replace("\\", "/"); // 音频文件扩展名统一转小写 if (typeof(T) == typeof(AudioClip)) { var pathExt2 = System.IO.Path.GetExtension(path); if (!string.IsNullOrEmpty(pathExt2) && pathExt2 != pathExt2.ToLower()) path = path.Substring(0, path.Length - pathExt2.Length) + pathExt2.ToLower(); } if (!AssetSource.isUseAssetBundle) { #if UNITY_EDITOR return UnityEditor.AssetDatabase.LoadAssetAtPath(path); #else return null; #endif } return await ResourceCacheManager.Instance.GetOrLoadAsync(path); } private async UniTask LoadSpriteAsyncUniTask(string atlasName, string spriteName, CancellationToken ct = default) { if (!AssetSource.isUseAssetBundle) { var atlas = await LoadAssetAsync("Sprite", atlasName.Replace("Sprite/", ""), ct: ct); return atlas?.GetSprite(spriteName); } else { var path = $"Assets/ResourcesOut/{atlasName}/{spriteName}.png" .Replace("//", "/").Trim().Replace("\\", "/"); var sprite = await YooAssetService.Instance.LoadAssetAsync(path, ct: ct); if (sprite == null) Debug.LogWarning($"[ResManager] Sprite load returned NULL: path={path}"); return sprite; } } }