Files
Gen_Hack-and-Slash-Roguelite/Client/Assets/Scripts/Managers/AudioManager.cs

233 lines
11 KiB
C#
Raw 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 Data;
using UnityEngine;
namespace Managers
{
/// <summary>
/// 音频管理器,负责加载、管理和提供从定义数据中解析出的音频剪辑。
/// </summary>
/// <remarks>
/// 该管理器是一个单例,并在启动过程中实现 ILaunchManager 接口,
/// 用于处理游戏或应用启动时音频资源的加载和初始化。
/// </remarks>
public class AudioManager : Utils.Singleton<AudioManager>, ILaunchManager
{
/// <summary>
/// 默认音频剪辑,在找不到对应音频时返回。
/// </summary>
public AudioClip defaultAudioClip;
/// <summary>
/// 存储所有已加载的音频剪辑按音频名称全局唯一的DefName索引。
/// </summary>
public Dictionary<string, AudioClip> audioClips = new();
/// <summary>
/// 获取当前启动步骤的描述。
/// </summary>
public string StepDescription { get; private set; } = "音频管理器正在准备中...";
/// <summary>
/// 初始化音频管理器,加载默认音频剪辑并处理所有 AudioDef 定义。
/// </summary>
public void Init()
{
if (audioClips.Count > 0)
{
// 如果已经有数据,则跳过初始化,防止重复加载。
return;
}
defaultAudioClip = Resources.Load<AudioClip>("Default/DefaultAudio"); // 假设存在一个默认音频路径
if (defaultAudioClip == null)
{
Debug.LogWarning("AudioManager: 无法加载默认音频 'Resources/Default/DefaultAudio'。请确保文件存在。");
}
InitAudioDef();
}
/// <summary>
/// 根据 AudioDef 定义初始化并加载所有音频剪辑。
/// </summary>
public void InitAudioDef()
{
// 缓存已加载的物理文件路径对应的 AudioClip避免重复加载
var audioCache = new Dictionary<string, AudioClip>();
var audioDefs = Managers.DefineManager.Instance.QueryDefinesByType<AudioDef>();
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<AudioClip>(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}");
}
}
}
/// <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()
{
// 如果需要显式卸载从 Resources.Load 或外部文件加载的 AssetBundle 资源,
// 可能需要 Resources.UnloadAsset(clip) 或 AssetBundle.UnloadAllAssets(true)。
// 但对于单独的 AudioClip 引用通常在不再被引用时Unity会自动清理。
audioClips.Clear();
StepDescription = "音频管理器数据已清理。"; // 更新状态
}
/// <summary>
/// 重新加载所有音频数据。
/// </summary>
/// <remarks>
/// 此方法会首先调用 <see cref="Clear()"/> 清理所有数据,然后调用 <see cref="Init()"/> 重新初始化。
/// </remarks>
public void Reload()
{
Clear();
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;
}
}
}