(client) feat:健康给予,路径优化,结算界面,商店界面 (#60)

Co-authored-by: m0_75251201 <m0_75251201@noreply.gitcode.com>
Reviewed-on: http://47.107.252.169:3000/Roguelite-Game-Developing-Team/Gen_Hack-and-Slash-Roguelite/pulls/60
This commit is contained in:
2025-10-10 14:08:23 +08:00
parent 9a797479ff
commit 16b49f3d3a
1900 changed files with 114053 additions and 34157 deletions

View File

@@ -1,67 +1,93 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Data;
using Configs; // 假设 Configs 命名空间包含了 ConfigProcessor
using Data; // 假设 Data 命名空间包含了 AudioDef
using UnityEngine;
using Utils; // 假设 Utils 命名空间包含了 Singleton
using System.Threading.Tasks; // 导入异步命名空间
namespace Managers
{
/// <summary>
/// 音频管理器,负责加载、管理和提供从定义数据中解析出的音频剪辑。
/// 音频管理器,负责加载、管理和提供从定义数据中解析出的音频剪辑。
/// </summary>
/// <remarks>
/// 该管理器是一个单例,并在启动过程中实现 ILaunchManager 接口,
/// 用于处理游戏或应用启动时音频资源的加载和初始化。
/// 该管理器是一个单例,并在启动过程中实现 ILaunchManager 接口,
/// 用于处理游戏或应用启动时音频资源的加载和初始化。
/// </remarks>
public class AudioManager : Utils.Singleton<AudioManager>, ILaunchManager
public class AudioManager : Singleton<AudioManager>, ILaunchManager
{
/// <summary>
/// 默认音频剪辑,在找不到对应音频时返回
/// </summary>
public AudioClip defaultAudioClip;
/// <summary>
/// 存储所有已加载的音频剪辑按音频名称全局唯一的DefName索引。
/// 存储所有已加载的音频剪辑按音频名称全局唯一的DefName索引
/// </summary>
public Dictionary<string, AudioClip> audioClips = new();
/// <summary>
/// 获取当前启动步骤的描述
/// 默认音频剪辑,在找不到对应音频时返回
/// </summary>
public string StepDescription { get;} = "音频管理器正在加载中";
public AudioClip defaultAudioClip;
public bool Completed { get; set; } // 从 Loaded 更名为 Completed
/// <summary>
/// 初始化音频管理器,加载默认音频剪辑并处理所有 AudioDef 定义
/// 获取当前启动步骤的描述
/// </summary>
public void Init()
public string StepDescription { get; } = "音频管理器正在加载中";
/// <summary>
/// 初始化音频管理器,加载默认音频剪辑并处理所有 AudioDef 定义。
/// </summary>
public async Task Init() // 更改为异步方法
{
if (audioClips.Count > 0)
// 如果已经初始化完成,则跳过初始化,防止重复加载。
if (Completed)
{
// 如果已经有数据,则跳过初始化,防止重复加载。
return;
await Task.CompletedTask; // 返回一个已完成的Task
return;
}
defaultAudioClip = Resources.Load<AudioClip>("Default/DefaultAudio");
if (defaultAudioClip == null)
{
if (!defaultAudioClip)
Debug.LogWarning("AudioManager: 无法加载默认音频 'Resources/Default/DefaultAudio'。请确保文件存在。");
}
InitAudioDef();
await InitAudioDef(); // await 等待异步的音频定义加载完成
Completed = true; // 标记为完成
await Task.CompletedTask; // 返回一个已完成的Task
}
/// <summary>
/// 根据 AudioDef 定义初始化并加载所有音频剪辑。
/// 清理所有已加载的音频剪辑数据
/// </summary>
public void InitAudioDef()
/// <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 = Managers.DefineManager.Instance.QueryDefinesByType<AudioDef>();
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;
}
@@ -69,16 +95,17 @@ namespace Managers
{
if (string.IsNullOrEmpty(audioDef.path) || string.IsNullOrEmpty(audioDef.defName))
{
Debug.LogWarning($"AudioManager: 跳过音频定义 (DefName: '{audioDef?.defName ?? ""}')因为它包含空路径或DefName。(路径: '{audioDef?.path ?? ""}')");
Debug.LogWarning(
$"AudioManager: 跳过音频定义 (DefName: '{audioDef?.defName ?? ""}')因为它包含空路径或DefName。(路径: '{audioDef?.path ?? ""}')");
continue;
}
try
{
string cacheKey;
AudioClip audioClip = null;
if (audioDef.path.StartsWith("res:"))
AudioClip audioClip = null;
if (audioDef.path.StartsWith("res:"))
{
// 处理 Unity Resources 路径
var resPath = audioDef.path.Substring(4).Replace('\\', '/').TrimStart('/');
@@ -88,7 +115,7 @@ namespace Managers
if (!audioCache.TryGetValue(cacheKey, out audioClip))
{
var cleanPath = Path.ChangeExtension(resPath, null); // 去掉扩展名
audioClip = Resources.Load<AudioClip>(cleanPath);
audioClip = Resources.Load<AudioClip>(cleanPath); // Resources.Load 是同步的
if (audioClip)
audioCache[cacheKey] = audioClip;
}
@@ -99,10 +126,11 @@ namespace Managers
var packageID = audioDef.path.Substring(0, splitIndex);
var relativePath = audioDef.path.Substring(splitIndex + 1);
var packageRoot = Managers.DefineManager.Instance.GetPackagePath(packageID);
var packageRoot = DefineManager.Instance.GetPackagePath(packageID);
if (string.IsNullOrEmpty(packageRoot))
{
Debug.LogWarning($"AudioManager: 音频定义 '{audioDef.defName}' (路径: '{audioDef.path}'): 引用的包ID '{packageID}' 未找到或没有根路径。跳过音频加载。");
Debug.LogWarning(
$"AudioManager: 音频定义 '{audioDef.defName}' (路径: '{audioDef.path}'): 引用的包ID '{packageID}' 未找到或没有根路径。跳过音频加载。");
continue;
}
@@ -112,13 +140,7 @@ namespace Managers
// 检查音频缓存
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;
audioClip = await ConfigProcessor.LoadAudioByIO(fullPath); // 异步等待
if (audioClip)
audioCache[cacheKey] = audioClip;
}
@@ -126,19 +148,21 @@ namespace Managers
else
{
// 无前缀:使用当前定义所在包的路径
var pack = Managers.DefineManager.Instance.GetDefinePackage(audioDef);
var pack = DefineManager.Instance.GetDefinePackage(audioDef);
if (pack == null)
{
Debug.LogError($"AudioManager: 音频定义 '{audioDef.defName}' (路径: '{audioDef.path}'): 源音频未找到对应的定义包。无法确定完整路径。跳过。");
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;
audioClip = await ConfigProcessor.LoadAudioByIO(fullPath); // 异步等待
if (audioClip)
audioCache[cacheKey] = audioClip;
}
@@ -147,7 +171,8 @@ namespace Managers
// 资源加载失败
if (audioClip == null)
{
Debug.LogError($"AudioManager: 未能加载音频定义关联的音频剪辑: '{audioDef.defName}' (路径: '{audioDef.path}')。请验证路径和文件是否存在。");
Debug.LogError(
$"AudioManager: 未能加载音频定义关联的音频剪辑: '{audioDef.defName}' (路径: '{audioDef.path}')。请验证路径和文件是否存在。");
continue;
}
@@ -160,43 +185,24 @@ namespace Managers
}
}
}
/// <summary>
/// 清理所有已加载的音频剪辑数据。
/// 重新加载所有音频数据。
/// </summary>
/// <remarks>
/// 此方法会清空 <see cref="audioClips"/> 字典,释放对 AudioClip 对象的引用
/// 它不会卸载 <see cref="defaultAudioClip"/>,因为它通常通过 Resources.Load 加载,
/// 其生命周期由 Unity 的资源管理系统控制,当不再被引用时会自动卸载。
/// 对于通过 ConfigProcessor.LoadAudioClipByIO 加载的 AudioClip如果它们不是通过 Unity API
/// 创建的 Unity.Object 类型,则可能需要额外的内存释放逻辑,但在本示例中,我们假设
/// ConfigProcessor 会返回一个被 Unity 管理的 AudioClip。
/// 此方法会首先调用 <see cref="Clear()" /> 清理所有数据,然后调用 <see cref="Init()" /> 重新初始化
/// </remarks>
public void Clear()
public async Task Reload() // 更改为异步方法
{
// 如果需要显式卸载从 Resources.Load 或外部文件加载的 AssetBundle 资源,
// 可能需要 Resources.UnloadAsset(clip) 或 AssetBundle.UnloadAllAssets(true)。
// 但对于单独的 AudioClip 引用通常在不再被引用时Unity会自动清理。
audioClips.Clear();
Clear();
await Init(); // await 等待异步的 Init 完成
}
/// <summary>
/// 重新加载所有音频数据
/// 根据 <see cref="AudioDef" /> 对象获取对应的音频剪辑
/// </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>
/// <param name="audioDef">包含音频名称的 <see cref="AudioDef" /> 对象。</param>
/// <returns>如果找到对应的音频剪辑,则返回该剪辑;否则返回 <see cref="defaultAudioClip" />。</returns>
public AudioClip GetAudioClip(AudioDef audioDef)
{
if (audioDef == null)
@@ -204,14 +210,15 @@ namespace Managers
Debug.LogWarning("AudioManager: 请求的 AudioDef 为空。返回默认音频。");
return defaultAudioClip;
}
return GetAudioClip(audioDef.defName);
}
/// <summary>
/// 根据音频名称全局唯一的DefName获取对应的音频剪辑。
/// 根据音频名称全局唯一的DefName获取对应的音频剪辑。
/// </summary>
/// <param name="name">音频剪辑的名称。</param>
/// <returns>如果找到对应的音频剪辑,则返回该剪辑;否则返回 <see cref="defaultAudioClip"/>。</returns>
/// <returns>如果找到对应的音频剪辑,则返回该剪辑;否则返回 <see cref="defaultAudioClip" />。</returns>
public AudioClip GetAudioClip(string name)
{
if (string.IsNullOrEmpty(name))