using System; using System.Collections.Generic; using System.IO; using System.Linq; using Data; using UnityEngine; namespace Managers { /// /// 音频管理器,负责加载、管理和提供从定义数据中解析出的音频剪辑。 /// /// /// 该管理器是一个单例,并在启动过程中实现 ILaunchManager 接口, /// 用于处理游戏或应用启动时音频资源的加载和初始化。 /// public class AudioManager : Utils.Singleton, ILaunchManager { /// /// 默认音频剪辑,在找不到对应音频时返回。 /// public AudioClip defaultAudioClip; /// /// 存储所有已加载的音频剪辑,按音频名称(全局唯一的DefName)索引。 /// public Dictionary audioClips = new(); /// /// 获取当前启动步骤的描述。 /// public string StepDescription { get; private set; } = "音频管理器正在准备中..."; /// /// 初始化音频管理器,加载默认音频剪辑并处理所有 AudioDef 定义。 /// public void Init() { if (audioClips.Count > 0) { // 如果已经有数据,则跳过初始化,防止重复加载。 return; } defaultAudioClip = Resources.Load("Default/DefaultAudio"); // 假设存在一个默认音频路径 if (defaultAudioClip == null) { Debug.LogWarning("AudioManager: 无法加载默认音频 'Resources/Default/DefaultAudio'。请确保文件存在。"); } InitAudioDef(); } /// /// 根据 AudioDef 定义初始化并加载所有音频剪辑。 /// public void InitAudioDef() { // 缓存已加载的物理文件路径对应的 AudioClip,避免重复加载 var audioCache = new Dictionary(); var audioDefs = Managers.DefineManager.Instance.QueryDefinesByType(); if (audioDefs == null || !audioDefs.Any()) { Debug.Log($"AudioManager: 在定义管理器中未找到任何音频定义。({nameof(AudioDef)})"); return; } foreach (var audioDef in audioDefs) { if (string.IsNullOrEmpty(audioDef.path) || string.IsNullOrEmpty(audioDef.defName)) { Debug.LogWarning($"AudioManager: 跳过音频定义 (DefName: '{audioDef?.defName ?? "未知"}'),因为它包含空路径或DefName。(路径: '{audioDef?.path ?? ""}')"); continue; } try { string cacheKey; AudioClip audioClip = null; if (audioDef.path.StartsWith("res:")) { // 处理 Unity Resources 路径 var resPath = audioDef.path.Substring(4).Replace('\\', '/').TrimStart('/'); cacheKey = "res://" + resPath.ToLower(); // 缓存键使用小写路径 // 检查音频缓存 if (!audioCache.TryGetValue(cacheKey, out audioClip)) { var cleanPath = Path.ChangeExtension(resPath, null); // 去掉扩展名 audioClip = Resources.Load(cleanPath); if (audioClip) audioCache[cacheKey] = audioClip; } } else if (audioDef.path.Contains(':')) // 包含 PackageID:Path 或其他自定义前缀 { var splitIndex = audioDef.path.IndexOf(':'); var packageID = audioDef.path.Substring(0, splitIndex); var relativePath = audioDef.path.Substring(splitIndex + 1); var packageRoot = Managers.DefineManager.Instance.GetPackagePath(packageID); if (string.IsNullOrEmpty(packageRoot)) { Debug.LogWarning($"AudioManager: 音频定义 '{audioDef.defName}' (路径: '{audioDef.path}'): 引用的包ID '{packageID}' 未找到或没有根路径。跳过音频加载。"); continue; } var fullPath = Path.Combine(packageRoot, relativePath).Replace('\\', '/'); cacheKey = "file://" + fullPath.ToLower(); // 缓存键使用小写路径 // 检查音频缓存 if (!audioCache.TryGetValue(cacheKey, out audioClip)) { // === 重要的实现细节 === // 在真实的 Unity 项目中,直接从文件系统同步加载 AudioClip 通常不推荐, // 且 Unity 不提供直接的同步方法。通常会使用 UnityWebRequest.GetAudioClip (异步) // 或 Asset Bundles。为了与 ImageManager 的 ConfigProcessor.LoadTextureByIO // 保持一致的同步风格,此处我们假设 Configs.ConfigProcessor 存在一个同步的 // LoadAudioClipByIO 方法。 audioClip = Configs.ConfigProcessor.LoadAudioByIO(fullPath).Result; if (audioClip) audioCache[cacheKey] = audioClip; } } else { // 无前缀:使用当前定义所在包的路径 var pack = Managers.DefineManager.Instance.GetDefinePackage(audioDef); if (pack == null) { Debug.LogError($"AudioManager: 音频定义 '{audioDef.defName}' (路径: '{audioDef.path}'): 源音频未找到对应的定义包。无法确定完整路径。跳过。"); continue; } var fullPath = Path.Combine(pack.packRootPath, audioDef.path).Replace('\\', '/'); cacheKey = "file://" + fullPath.ToLower(); // 缓存键使用小写路径 // 检查音频缓存 if (!audioCache.TryGetValue(cacheKey, out audioClip)) { audioClip = Configs.ConfigProcessor.LoadAudioByIO(fullPath).Result; if (audioClip) audioCache[cacheKey] = audioClip; } } // 资源加载失败 if (audioClip == null) { Debug.LogError($"AudioManager: 未能加载音频定义关联的音频剪辑: '{audioDef.defName}' (路径: '{audioDef.path}')。请验证路径和文件是否存在。"); continue; } audioClips[audioDef.defName] = audioClip; // 使用 DefName 作为唯一键存储 } catch (Exception ex) { Debug.LogError( $"AudioManager: 处理音频定义时出错: '{audioDef.defName}' (路径: '{audioDef.path}')。异常: {ex.GetType().Name}: {ex.Message}\n堆栈跟踪: {ex.StackTrace}"); } } } /// /// 清理所有已加载的音频剪辑数据。 /// /// /// 此方法会清空 字典,释放对 AudioClip 对象的引用。 /// 它不会卸载 ,因为它通常通过 Resources.Load 加载, /// 其生命周期由 Unity 的资源管理系统控制,当不再被引用时会自动卸载。 /// 对于通过 ConfigProcessor.LoadAudioClipByIO 加载的 AudioClip,如果它们不是通过 Unity API /// 创建的 Unity.Object 类型,则可能需要额外的内存释放逻辑,但在本示例中,我们假设 /// ConfigProcessor 会返回一个被 Unity 管理的 AudioClip。 /// public void Clear() { // 如果需要显式卸载从 Resources.Load 或外部文件加载的 AssetBundle 资源, // 可能需要 Resources.UnloadAsset(clip) 或 AssetBundle.UnloadAllAssets(true)。 // 但对于单独的 AudioClip 引用,通常在不再被引用时Unity会自动清理。 audioClips.Clear(); StepDescription = "音频管理器数据已清理。"; // 更新状态 } /// /// 重新加载所有音频数据。 /// /// /// 此方法会首先调用 清理所有数据,然后调用 重新初始化。 /// public void Reload() { Clear(); Init(); } /// /// 根据 对象获取对应的音频剪辑。 /// /// 包含音频名称的 对象。 /// 如果找到对应的音频剪辑,则返回该剪辑;否则返回 public AudioClip GetAudioClip(AudioDef audioDef) { if (audioDef == null) { Debug.LogWarning("AudioManager: 请求的 AudioDef 为空。返回默认音频。"); return defaultAudioClip; } return GetAudioClip(audioDef.defName); } /// /// 根据音频名称(全局唯一的DefName)获取对应的音频剪辑。 /// /// 音频剪辑的名称。 /// 如果找到对应的音频剪辑,则返回该剪辑;否则返回 public AudioClip GetAudioClip(string name) { if (string.IsNullOrEmpty(name)) { Debug.LogWarning("AudioManager: 请求的音频名称为空或null。返回默认音频。"); return defaultAudioClip; } if (audioClips.TryGetValue(name, out var clip)) return clip; // 如果未找到,返回默认音频剪辑 Debug.LogWarning($"AudioManager: 未找到名称为 '{name}' 的音频剪辑。返回默认音频。"); return defaultAudioClip; } } }