Files

239 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Configs; // 假设 Configs 命名空间包含了 ConfigProcessor
using Data; // 假设 Data 命名空间包含了 AudioDef
using UnityEngine;
using Utils; // 假设 Utils 命名空间包含了 Singleton
using System.Threading.Tasks; // 导入异步命名空间
namespace Managers
{
/// <summary>
/// 音频管理器,负责加载、管理和提供从定义数据中解析出的音频剪辑。
/// </summary>
/// <remarks>
/// 该管理器是一个单例,并在启动过程中实现 ILaunchManager 接口,
/// 用于处理游戏或应用启动时音频资源的加载和初始化。
/// </remarks>
public class AudioManager : Singleton<AudioManager>, ILaunchManager
{
/// <summary>
/// 存储所有已加载的音频剪辑按音频名称全局唯一的DefName索引。
/// </summary>
public Dictionary<string, AudioClip> audioClips = new();
/// <summary>
/// 默认音频剪辑,在找不到对应音频时返回。
/// </summary>
public AudioClip defaultAudioClip;
public bool Completed { get; set; } // 从 Loaded 更名为 Completed
/// <summary>
/// 获取当前启动步骤的描述。
/// </summary>
public string StepDescription { get; } = "音频管理器正在加载中";
/// <summary>
/// 初始化音频管理器,加载默认音频剪辑并处理所有 AudioDef 定义。
/// </summary>
public async Task Init() // 更改为异步方法
{
// 如果已经初始化完成,则跳过初始化,防止重复加载。
if (Completed)
{
await Task.CompletedTask; // 返回一个已完成的Task
return;
}
defaultAudioClip = Resources.Load<AudioClip>("Default/DefaultAudio");
if (!defaultAudioClip)
Debug.LogWarning("AudioManager: 无法加载默认音频 'Resources/Default/DefaultAudio'。请确保文件存在。");
await InitAudioDef(); // await 等待异步的音频定义加载完成
Completed = true; // 标记为完成
await Task.CompletedTask; // 返回一个已完成的Task
}
/// <summary>
/// 清理所有已加载的音频剪辑数据。
/// </summary>
/// <remarks>
/// 此方法会清空 <see cref="audioClips" /> 字典,释放对 AudioClip 对象的引用。
/// 它不会卸载 <see cref="defaultAudioClip" />,因为它通常通过 Resources.Load 加载,
/// 其生命周期由 Unity 的资源管理系统控制,当不再被引用时会自动卸载。
/// 对于通过 ConfigProcessor.LoadAudioClipByIO 加载的 AudioClip如果它们不是通过 Unity API
/// 创建的 Unity.Object 类型,则可能需要额外的内存释放逻辑,但在本示例中,我们假设
/// ConfigProcessor 会返回一个被 Unity 管理的 AudioClip。
/// </remarks>
public void Clear()
{
audioClips.Clear();
Completed = false; // 清理后,重置 Completed 状态
}
/// <summary>
/// 根据 AudioDef 定义初始化并加载所有音频剪辑。
/// </summary>
public async Task InitAudioDef() // 更改为异步方法
{
// 缓存已加载的物理文件路径对应的 AudioClip避免重复加载
var audioCache = new Dictionary<string, AudioClip>();
var audioDefs = DefineManager.Instance.QueryDefinesByType<AudioDef>();
if (audioDefs == null || !audioDefs.Any())
{
Debug.Log($"AudioManager: 在定义管理器中未找到任何音频定义。({nameof(AudioDef)})");
await Task.CompletedTask; // 返回一个已完成的Task
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<AudioClip>(cleanPath); // Resources.Load 是同步的
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 = 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))
{
audioClip = await ConfigProcessor.LoadAudioByIO(fullPath); // 异步等待
if (audioClip)
audioCache[cacheKey] = audioClip;
}
}
else
{
// 无前缀:使用当前定义所在包的路径
var pack = 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 = await ConfigProcessor.LoadAudioByIO(fullPath); // 异步等待
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}");
}
}
}
/// <summary>
/// 重新加载所有音频数据。
/// </summary>
/// <remarks>
/// 此方法会首先调用 <see cref="Clear()" /> 清理所有数据,然后调用 <see cref="Init()" /> 重新初始化。
/// </remarks>
public async Task Reload() // 更改为异步方法
{
Clear();
await Init(); // await 等待异步的 Init 完成
}
/// <summary>
/// 根据 <see cref="AudioDef" /> 对象获取对应的音频剪辑。
/// </summary>
/// <param name="audioDef">包含音频名称的 <see cref="AudioDef" /> 对象。</param>
/// <returns>如果找到对应的音频剪辑,则返回该剪辑;否则返回 <see cref="defaultAudioClip" />。</returns>
public AudioClip GetAudioClip(AudioDef audioDef)
{
if (audioDef == null)
{
Debug.LogWarning("AudioManager: 请求的 AudioDef 为空。返回默认音频。");
return defaultAudioClip;
}
return GetAudioClip(audioDef.defName);
}
/// <summary>
/// 根据音频名称全局唯一的DefName获取对应的音频剪辑。
/// </summary>
/// <param name="name">音频剪辑的名称。</param>
/// <returns>如果找到对应的音频剪辑,则返回该剪辑;否则返回 <see cref="defaultAudioClip" />。</returns>
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;
}
}
}