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

458 lines
20 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 System.Threading.Tasks; // 引入 Task
using Configs;
using Data;
using UnityEngine;
using Utils;
namespace Managers
{
/// <summary>
/// 包图像管理器,负责加载、管理和提供从定义数据中解析出的纹理和精灵。
/// </summary>
/// <remarks>
/// 该管理器是一个单例,并在启动过程中实现 ILaunchManager 接口,
/// 用于处理游戏或应用启动时图像资源的加载和初始化。
/// </remarks>
public class PackagesImageManager : Singleton<PackagesImageManager>, ILaunchManager
{
/// <summary>
/// 默认精灵,在找不到对应图像时返回。
/// </summary>
public Sprite defaultSprite;
/// <summary>
/// 存储所有已加载的纹理按图像名称全局唯一的DefName索引。
/// </summary>
public Dictionary<string, Texture2D> packagesImages = new();
/// <summary>
/// 存储所有已创建的精灵按精灵名称全局唯一的DefName或其带索引后缀索引。
/// </summary>
public Dictionary<string, Sprite> sprites = new();
/// <summary>
/// 指示管理器是否已完成初始化。
/// </summary>
public bool Completed { get; set; } // 接口变更Loaded 变为 Completed
/// <summary>
/// 获取当前启动步骤的描述。
/// </summary>
public string StepDescription { get; } = "图像管理器正在加载中";
/// <summary>
/// 初始化图像管理器,加载默认精灵并处理所有 ImageDef 定义。
/// </summary>
/// <remarks>
/// <para>
/// **修正后的逻辑**:移除 <c>packagesImages.Count > 0</c> 的重复加载检查。
/// <c>Init</c> 方法现在将始终执行完整的初始化流程,确保所有组件都被正确加载。
/// 如果需要重新加载,应调用 <c>Reload()</c> 方法。
/// </para>
/// </remarks>
public async Task Init() // 接口变更:方法签名变为 async Task
{
// 如果已经完成初始化,则直接返回,避免重复执行
if (Completed) return;
// 默认精灵加载通常是快速的,无需异步
defaultSprite = Resources.Load<Sprite>("Default/DefaultImage");
if (!defaultSprite) Debug.LogWarning("无法加载默认精灵 'Resources/Default/DefaultImage'。请确保文件存在。");
await InitImageDef(); // 执行图像定义异步加载逻辑,并等待其完成
Completed = true; // 在初始化逻辑完成后设置 Completed 为 true
}
/// <summary>
/// 清理所有已加载的纹理和精灵数据。
/// </summary>
public void Clear()
{
packagesImages.Clear();
sprites.Clear();
Completed = false; // 清理后应将 Completed 置为 false
}
/// <summary>
/// 异步加载纹理,支持 Resources 和文件系统路径,并自带缓存。
/// </summary>
/// <param name="imageDef">图像定义。</param>
/// <param name="textureCache">纹理缓存。</param>
/// <returns>加载的 Texture2D如果没有找到或加载失败则为 null。</returns>
private async Task<Texture2D> LoadTextureAsync(ImageDef imageDef, Dictionary<string, Texture2D> textureCache)
{
Texture2D texture;
string cacheKey;
string fullPath; // 用于记录完整路径,方便日志
// 1. 处理 Resources 路径
if (imageDef.path.StartsWith("res:"))
{
var resPath = imageDef.path.Substring(4).Replace('\\', '/').TrimStart('/');
cacheKey = "res://" + resPath.ToLower();
if (textureCache.TryGetValue(cacheKey, out texture)) return texture; // 从缓存获取
var cleanPath = Path.ChangeExtension(resPath, null); // 去掉扩展名
ResourceRequest request = Resources.LoadAsync<Texture2D>(cleanPath);
await request; // 等待资源异步加载
texture = request.asset as Texture2D;
if (texture) textureCache[cacheKey] = texture;
fullPath = $"Resources/{cleanPath}"; // 模拟完整路径用于日志
}
// 2. 处理带有包ID前缀的文件系统路径
else if (imageDef.path.Contains(':'))
{
var splitIndex = imageDef.path.IndexOf(':');
var packageID = imageDef.path.Substring(0, splitIndex);
var relativePath = imageDef.path.Substring(splitIndex + 1);
var packageRoot = DefineManager.Instance.GetPackagePath(packageID); // 不检查单例
if (string.IsNullOrEmpty(packageRoot))
{
Debug.LogWarning(
$"图像定义 '{imageDef.defName}' (包ID: {imageDef.packID}): 引用的包ID '{packageID}' 未找到或没有根路径。跳过图像加载。");
return null;
}
fullPath = Path.Combine(packageRoot, relativePath).Replace('\\', '/');
cacheKey = "file://" + fullPath.ToLower();
if (textureCache.TryGetValue(cacheKey, out texture)) return texture; // 从缓存获取
// 异步读取文件字节,然后回到主线程创建 Texture2D
byte[] bytes = await Task.Run(() =>
{
try
{
if (!File.Exists(fullPath))
{
Debug.LogWarning($"文件不存在。图像定义 '{imageDef.defName}' (路径: '{fullPath}')。");
return null;
}
return File.ReadAllBytes(fullPath);
}
catch (Exception e)
{
Debug.LogError($"异步读取文件字节失败。图像定义 '{imageDef.defName}' (路径: '{fullPath}')。异常: {e.Message}");
return null;
}
});
if (bytes != null && bytes.Length > 0)
{
// Texture2D 的创建和 LoadImage/Apply 必须在主线程执行。
// 因为当前方法是在主线程调用,且 await 确保了执行上下文回归,所以这里是安全的。
texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); // 创建临时Texture2D格式RGBA32兼容LoadImage
texture.LoadImage(bytes); // 加载图片数据
texture.Apply(); // 应用改动
if (texture) textureCache[cacheKey] = texture;
}
}
// 3. 处理无前缀的文件系统路径(使用图像定义所在包的根路径)
else
{
var pack = DefineManager.Instance.GetDefinePackage(imageDef); // 不检查单例
if (pack == null)
{
Debug.LogError(
$"图像定义 '{imageDef.defName}' (包ID: {imageDef.packID}): 源图像未找到对应的定义包。无法确定 '{imageDef.path}' 的完整路径。跳过。");
return null;
}
fullPath = Path.Combine(pack.packRootPath, imageDef.path).Replace('\\', '/');
cacheKey = "file://" + fullPath.ToLower();
if (textureCache.TryGetValue(cacheKey, out texture)) return texture; // 从缓存获取
byte[] bytes = await Task.Run(() =>
{
try
{
if (!File.Exists(fullPath))
{
Debug.LogWarning($"文件不存在。图像定义 '{imageDef.defName}' (路径: '{fullPath}')。");
return null;
}
return File.ReadAllBytes(fullPath);
}
catch (Exception e)
{
Debug.LogError($"异步读取文件字节失败。图像定义 '{imageDef.defName}' (路径: '{fullPath}')。异常: {e.Message}");
return null;
}
});
if (bytes != null && bytes.Length > 0)
{
texture = new Texture2D(2, 2, TextureFormat.RGBA32, false);
texture.LoadImage(bytes);
texture.Apply();
if (texture) textureCache[cacheKey] = texture;
}
}
// 如果 texture 仍然为 null打印统一的失败日志
if (texture == null)
{
Debug.LogError($"未能加载图像纹理。定义: '{imageDef.defName}' (路径: '{imageDef.path}', 完整路径: '{fullPath}')。");
}
return texture;
}
/// <summary>
/// 根据 ImageDef 定义异步初始化并加载所有纹理和精灵。
/// </summary>
public async Task InitImageDef() // 方法签名改为 async Task
{
// 在每次 InitImageDef 调用前,清空旧数据,以确保重新初始化是干净的。
packagesImages.Clear();
sprites.Clear();
var textureCache = new Dictionary<string, Texture2D>();
var imageDefList = DefineManager.Instance.QueryDefinesByType<ImageDef>()?.ToList(); // 使用 ToList() 确保在迭代时集合稳定
if (imageDefList == null || !imageDefList.Any())
{
Debug.Log($"在定义管理器中未找到任何图像定义。({nameof(ImageDef)})");
return;
}
// 预过滤无效定义,避免为无效项创建加载任务
var validImageDefs = imageDefList.Where(ima =>
!string.IsNullOrEmpty(ima.path) && !string.IsNullOrEmpty(ima.packID)
).ToList();
if (!validImageDefs.Any())
{
Debug.LogWarning("所有图像定义都包含空路径或包ID没有可加载的图像。");
return;
}
// 创建所有纹理加载任务,实现并行加载
var loadingTasks = new List<Task<(ImageDef def, Texture2D texture)>>();
foreach (var ima in validImageDefs)
{
// 调用之前新增的异步辅助方法
loadingTasks.Add(LoadTextureAsync(ima, textureCache)
.ContinueWith(task => (ima, task.Result), TaskScheduler.FromCurrentSynchronizationContext())); // 确保 ContinueWith 的后续操作也在主线程
}
// 等待所有纹理加载任务完成
var loadedResults = await Task.WhenAll(loadingTasks);
// 处理加载结果 (此部分仍在主线程执行,因为后续操作涉及 Unity API)
foreach (var result in loadedResults)
{
var ima = result.def;
var texture = result.texture;
if (texture == null)
{
// 具体的错误信息已在 LoadTextureAsync 中记录,这里只需跳过
continue;
}
var processedTexture = texture;
// 如果需要翻转,创建翻转后的纹理
if (ima.flipX || ima.flipY)
{
var flippedTex = FlipTexture(texture, ima.flipX, ima.flipY); // FlipTexture 也必须在主线程
if (flippedTex)
processedTexture = flippedTex;
else
Debug.LogError(
$"未能翻转图像定义关联的纹理: '{ima.defName}' (路径: '{ima.path}', 包ID: '{ima.packID}'),将使用原始纹理。");
}
packagesImages[ima.defName] = processedTexture;
// 分割纹理为精灵,此操作也应在主线程
SplitTextureIntoSprites(ima.defName, processedTexture, ima.hCount, ima.wCount, ima.pixelsPerUnit,
ima.flipX, ima.flipY);
}
}
/// <summary>
/// 将纹理按指定行数和列数分割成多个精灵,并存储起来。
/// </summary>
/// <param name="baseName">精灵的基础名称全局唯一的DefName。</param>
/// <param name="texture">要分割的 <see cref="Texture2D" /> 对象。</param>
/// <param name="rows">水平分割的行数。</param>
/// <param name="cols">垂直分割的列数。</param>
/// <param name="pixelsPerUnit">每个单元的像素数用于Sprite.Create。</param>
/// <param name="flipX">是否沿X轴翻转此参数用于调整索引。</param>
/// <param name="flipY">是否沿Y轴翻转此参数用于调整索引。</param>
/// <remarks>
/// <para>
/// **修正后的逻辑**:恢复 <c>flipX</c> 和 <c>flipY</c> 参数及其在索引计算中的使用。
/// 这确保了精灵的编号逻辑与原始设计保持一致,避免了游戏资源加载错误。
/// </para>
/// </remarks>
private void SplitTextureIntoSprites(
string baseName,
Texture2D texture,
int rows,
int cols,
int pixelsPerUnit,
bool flipX = false,
bool flipY = false)
{
if (!texture)
{
Debug.LogError($"SplitTextureIntoSprites: '{baseName}' 提供的纹理为空。无法分割。");
return;
}
rows = Mathf.Max(1, rows);
cols = Mathf.Max(1, cols);
var textureWidth = texture.width;
var textureHeight = texture.height;
var fullSpriteRect = new Rect(0, 0, textureWidth, textureHeight);
var fullSprite = Sprite.Create(texture, fullSpriteRect, new Vector2(0.5f, 0.5f), pixelsPerUnit);
fullSprite.name = baseName;
sprites[baseName] = fullSprite;
if (rows == 1 && cols == 1) return;
if (textureWidth % cols != 0 || textureHeight % rows != 0)
{
Debug.LogWarning(
$"'{baseName}' 的纹理尺寸 ({textureWidth}x{textureHeight}) 不能被指定的行数 ({rows}) 和列数 ({cols}) 完美整除。子精灵将不会生成或可能不正确。仅显示完整精灵。");
return;
}
var tileWidth = (float)textureWidth / cols;
var tileHeight = (float)textureHeight / rows;
for (var row = 0; row < rows; row++)
for (var col = 0; col < cols; col++)
{
Rect spriteRect = new(col * tileWidth, row * tileHeight, tileWidth, tileHeight);
var sprite = Sprite.Create(texture, spriteRect, new Vector2(0.5f, 0.5f), pixelsPerUnit);
// 此处的 flipX/Y 参数用于调整索引,以符合游戏或美术的特定约定。
var index = (flipY ? row : rows - row - 1) * cols + (flipX ? cols - 1 - col : col);
var spriteName = $"{baseName}_{index}";
sprite.name = spriteName;
sprites[spriteName] = sprite;
}
}
/// <summary>
/// 根据指定的翻转方向创建一个新的、翻转后的纹理。
/// </summary>
/// <param name="originalTexture">原始纹理。</param>
/// <param name="flipX">是否沿X轴翻转。</param>
/// <param name="flipY">是否沿Y轴翻转。</param>
/// <returns>一个新的、翻转后的Texture2D实例如果不需要翻转则返回原纹理如果纹理不可读返回null。</returns>
public static Texture2D FlipTexture(Texture2D originalTexture, bool flipX, bool flipY)
{
if (!flipX && !flipY) return originalTexture;
if (!originalTexture)
{
Debug.LogError("FlipTexture: 原始纹理为null无法翻转。");
return null;
}
if (!originalTexture.isReadable)
{
Debug.LogError($"FlipTexture: 纹理 '{originalTexture.name}' (或其来源) 不可读。 " +
"请确保在Unity导入设置中勾选 'Read/Write Enabled' 选项。无法进行翻转。");
return null;
}
var width = originalTexture.width;
var height = originalTexture.height;
// 逻辑修改点:将 originalTexture.format 替换为 TextureFormat.RGBA32以确保 GetPixels32/SetPixels32 兼容。
// 同时,根据原纹理是否有 mipmap 来决定新纹理是否具有 mipmap。
var flippedTexture = new Texture2D(width, height, TextureFormat.RGBA32, originalTexture.mipmapCount > 1);
flippedTexture.name = originalTexture.name + (flipX ? "_flippedX" : "") + (flipY ? "_flippedY" : "");
var originalPixels = originalTexture.GetPixels32();
var flippedPixels = new Color32[width * height];
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++)
{
var originalIndex = y * width + x;
var targetX = flipX ? width - 1 - x : x;
var targetY = flipY ? height - 1 - y : y;
var flippedIndex = targetY * width + targetX;
// 确保索引有效,但通常在循环边界内不会溢出
flippedPixels[flippedIndex] = originalPixels[originalIndex];
}
flippedTexture.SetPixels32(flippedPixels);
flippedTexture.Apply(); // Apply() 必须在主线程调用
return flippedTexture;
}
/// <summary>
/// 重新加载所有图像数据。
/// </summary>
public async Task Reload() // 改造为异步方法
{
Clear();
await Init(); // 异步等待 Init 完成
}
/// <summary>
/// 根据 <see cref="Data.ImageDef" /> 对象获取对应的精灵。
/// </summary>
/// <param name="ima">图像定义对象。</param>
/// <returns>对应的精灵,如果 <paramref name="ima" /> 为空,则返回默认精灵。</returns>
public Sprite GetSprite(ImageDef ima)
{
return ima == null ? defaultSprite : GetSprite(ima.defName);
}
/// <summary>
/// 根据精灵名称全局唯一的DefName获取对应的精灵。
/// </summary>
/// <param name="name">要获取的精灵名称。</param>
/// <returns>对应的精灵,如果名称为空或找不到,则返回默认精灵。</returns>
public Sprite GetSprite(string name)
{
if (string.IsNullOrEmpty(name))
{
Debug.LogWarning("GetSprite: 尝试获取的精灵名称为空或null。返回默认精灵。");
return defaultSprite;
}
return sprites.GetValueOrDefault(name, defaultSprite);
}
/// <summary>
/// 根据基础名称全局唯一的DefName和索引获取被分割的子精灵。
/// </summary>
/// <param name="name">精灵的基础名称。</param>
/// <param name="index">子精灵的索引。</param>
/// <returns>对应的子精灵,如果找不到,则返回默认精灵。</returns>
public Sprite GetSprite(string name, int index)
{
var fullName = $"{name}_{index}";
return GetSprite(fullName);
}
/// <summary>
/// 根据精灵名称数组获取对应的精灵数组。
/// </summary>
/// <param name="names">精灵名称数组。</param>
/// <returns>对应的精灵数组,如果输入为空,则返回包含默认精灵的数组。</returns>
public Sprite[] GetSprites(string[] names)
{
if (names == null || names.Length == 0)
return Array.Empty<Sprite>();
return names.Select(name => GetSprite(name)).ToArray();
}
}
}