2025-09-28 15:02:57 +08:00
|
|
|
|
using System;
|
2025-09-03 19:59:22 +08:00
|
|
|
|
using System.Collections.Generic;
|
2025-09-28 15:02:57 +08:00
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Reflection;
|
2025-10-10 14:08:23 +08:00
|
|
|
|
using System.Threading.Tasks; // 引入 Task for async/await
|
2025-09-28 15:02:57 +08:00
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
|
using UnityEngine;
|
2025-10-10 14:08:23 +08:00
|
|
|
|
using Utils;
|
2025-09-03 19:59:22 +08:00
|
|
|
|
|
|
|
|
|
|
namespace Managers
|
|
|
|
|
|
{
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 用于标记那些包含可存储数据的单例类。
|
|
|
|
|
|
/// 实现此接口的单例应在SaveManager初始化后注册自身。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public interface ISavableSingleton
|
2025-09-03 19:59:22 +08:00
|
|
|
|
{
|
|
|
|
|
|
}
|
2025-09-28 15:02:57 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 用于标记在单例类中需要被SaveManager存储的属性。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
|
2025-09-28 15:02:57 +08:00
|
|
|
|
public class SavableAttribute : Attribute
|
|
|
|
|
|
{
|
|
|
|
|
|
public SavableAttribute(string key = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Key = key;
|
|
|
|
|
|
}
|
2025-10-10 14:08:23 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取或设置用于存储的唯一键。如果为空,则使用属性名称。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string Key { get; }
|
2025-09-28 15:02:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-10 14:08:23 +08:00
|
|
|
|
public class SaveManager : Singleton<SaveManager>, ILaunchManager
|
2025-09-28 15:02:57 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 存档文件名
|
|
|
|
|
|
private const string SaveFileName = "game_save.json";
|
|
|
|
|
|
|
2025-10-10 14:08:23 +08:00
|
|
|
|
// 存储所有需要保存状态的单例实例
|
|
|
|
|
|
private readonly Dictionary<Type, ISavableSingleton> _savableSingletons = new();
|
|
|
|
|
|
|
2025-09-28 15:02:57 +08:00
|
|
|
|
// 存档路径
|
2025-10-03 00:31:34 +08:00
|
|
|
|
private static string SaveFilePath => Path.Combine("Save", SaveFileName);
|
2025-09-28 15:02:57 +08:00
|
|
|
|
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 指示管理器是否已完成初始化。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public bool Completed { get; set; } // 新增:实现 ILaunchManager 接口的 Completed 属性
|
|
|
|
|
|
|
2025-09-28 15:02:57 +08:00
|
|
|
|
public string StepDescription => "加载存档中";
|
|
|
|
|
|
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 实现ILaunchManager接口的Init方法,在启动时加载存档。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>一个表示异步操作完成的 Task。</returns>
|
|
|
|
|
|
public async Task Init() // 接口变更:方法签名变为 async Task
|
|
|
|
|
|
{
|
|
|
|
|
|
// 如果已经完成初始化,则直接返回,避免重复执行
|
|
|
|
|
|
if (Completed) return;
|
|
|
|
|
|
|
|
|
|
|
|
LoadGame(); // 启动时加载存档
|
|
|
|
|
|
|
|
|
|
|
|
Completed = true; // 在加载逻辑完成后设置 Completed 为 true
|
|
|
|
|
|
// 由于 Init 方法内部当前没有真正的异步操作,不需要显式 await Task.CompletedTask;
|
|
|
|
|
|
// 编译器会为同步的 async Task 方法自动生成一个已完成的 Task。
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 实现ILaunchManager接口的Clear方法,Clear是重载不做任何事。
|
|
|
|
|
|
/// 根据用户指示,此方法不应改变管理器自身的 Completed 状态,且不执行任何清理操作。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Clear()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 按照用户指示,此方法为空实现,不影响 SaveManager 的完成状态或数据。
|
|
|
|
|
|
}
|
2025-09-28 15:02:57 +08:00
|
|
|
|
|
2025-10-03 00:31:34 +08:00
|
|
|
|
~SaveManager()
|
|
|
|
|
|
{
|
|
|
|
|
|
SaveGame(); // 在销毁时保存游戏
|
|
|
|
|
|
}
|
2025-10-10 14:08:23 +08:00
|
|
|
|
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 注册一个需要保存状态的单例实例。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="singleton">实现ISavableSingleton接口的单例实例。</param>
|
|
|
|
|
|
public void RegisterSavable(ISavableSingleton singleton)
|
|
|
|
|
|
{
|
|
|
|
|
|
var singletonType = singleton.GetType();
|
|
|
|
|
|
if (_savableSingletons.TryAdd(singletonType, singleton))
|
|
|
|
|
|
{
|
|
|
|
|
|
// DEBUG: Debug.Log($"SaveManager: 已注册可保存单例: {singletonType.Name}");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"SaveManager: 单例 {singletonType.Name} 已注册。");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 移除一个已注册的单例实例。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="singleton">要移除的单例实例。</param>
|
|
|
|
|
|
public void UnregisterSavable(ISavableSingleton singleton)
|
|
|
|
|
|
{
|
|
|
|
|
|
var singletonType = singleton.GetType();
|
|
|
|
|
|
if (_savableSingletons.Remove(singletonType))
|
|
|
|
|
|
{
|
|
|
|
|
|
// DEBUG: Debug.Log($"SaveManager: 已移除可保存单例: {singletonType.Name}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 保存游戏数据到文件。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void SaveGame()
|
|
|
|
|
|
{
|
|
|
|
|
|
var dataToSave = new Dictionary<string, JToken>();
|
|
|
|
|
|
foreach (var singletonEntry in _savableSingletons)
|
|
|
|
|
|
{
|
|
|
|
|
|
var singletonType = singletonEntry.Key;
|
|
|
|
|
|
var singletonInstance = singletonEntry.Value;
|
|
|
|
|
|
foreach (var prop in GetSavableProperties(singletonType))
|
|
|
|
|
|
{
|
|
|
|
|
|
var key = GetSavableKey(prop, singletonType);
|
|
|
|
|
|
var value = prop.GetValue(singletonInstance);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
dataToSave[key] = JToken.FromObject(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError(
|
|
|
|
|
|
$"SaveManager: 无法将属性 '{key}' (类型: {prop.PropertyType.Name}) 转换为 JToken: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-10-03 00:31:34 +08:00
|
|
|
|
var directoryPath = Path.GetDirectoryName(SaveFilePath);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath))
|
|
|
|
|
|
Directory.CreateDirectory(directoryPath);
|
2025-10-10 14:08:23 +08:00
|
|
|
|
// DEBUG: Debug.Log($"SaveManager: 文件夹 '{directoryPath}' 已创建。");
|
2025-09-28 15:02:57 +08:00
|
|
|
|
var settings = new JsonSerializerSettings
|
|
|
|
|
|
{
|
|
|
|
|
|
Formatting = Formatting.Indented,
|
|
|
|
|
|
TypeNameHandling = TypeNameHandling.Auto
|
|
|
|
|
|
};
|
|
|
|
|
|
var jsonString = JsonConvert.SerializeObject(dataToSave, settings);
|
|
|
|
|
|
File.WriteAllText(SaveFilePath, jsonString);
|
|
|
|
|
|
// DEBUG: Debug.Log($"SaveManager: 游戏数据已保存到 '{SaveFilePath}'。");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"SaveManager: 保存游戏数据到 '{SaveFilePath}' 失败: {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 从文件加载游戏数据。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void LoadGame()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!File.Exists(SaveFilePath))
|
|
|
|
|
|
// DEBUG: Debug.Log($"SaveManager: 未找到存档文件 '{SaveFilePath}'。将使用默认值启动。");
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var jsonString = File.ReadAllText(SaveFilePath);
|
|
|
|
|
|
|
|
|
|
|
|
var settings = new JsonSerializerSettings
|
|
|
|
|
|
{
|
|
|
|
|
|
TypeNameHandling = TypeNameHandling.Auto
|
|
|
|
|
|
};
|
|
|
|
|
|
var savedData = JsonConvert.DeserializeObject<Dictionary<string, JToken>>(jsonString, settings);
|
|
|
|
|
|
if (savedData == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("SaveManager: 存档文件为空或已损坏。将使用默认值启动。");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var singletonEntry in _savableSingletons)
|
|
|
|
|
|
{
|
|
|
|
|
|
var singletonType = singletonEntry.Key;
|
|
|
|
|
|
var singletonInstance = singletonEntry.Value;
|
|
|
|
|
|
foreach (var prop in GetSavableProperties(singletonType))
|
|
|
|
|
|
{
|
|
|
|
|
|
var key = GetSavableKey(prop, singletonType);
|
|
|
|
|
|
if (savedData.TryGetValue(key, out var jToken))
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var value = jToken.ToObject(prop.PropertyType, JsonSerializer.Create(settings));
|
|
|
|
|
|
prop.SetValue(singletonInstance, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception innerEx)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError(
|
|
|
|
|
|
$"SaveManager: 无法反序列化类型 '{singletonType.Name}' 的属性 '{key}' (类型: {prop.PropertyType.Name}): {innerEx.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
Debug.LogWarning(
|
|
|
|
|
|
$"SaveManager: 存档文件中未找到类型 '{singletonType.Name}' 的属性 '{key}'。将保留当前值。");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (JsonException ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError($"SaveManager: 解析存档文件失败 (JSON 错误): {ex.Message}");
|
|
|
|
|
|
File.Delete(SaveFilePath); // 建议删除损坏的存档文件
|
|
|
|
|
|
Debug.LogWarning("SaveManager: 已删除损坏的存档文件以防止进一步问题。");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError(
|
|
|
|
|
|
$"SaveManager: 从 '{SaveFilePath}' 加载游戏数据失败 (文件 I/O 或其他错误): {ex.Message}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 获取一个类型中所有带有SavableAttribute的公共实例属性。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private IEnumerable<PropertyInfo> GetSavableProperties(Type type)
|
|
|
|
|
|
{
|
|
|
|
|
|
return type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
|
|
|
|
.Where(prop => prop.CanRead && prop.CanWrite && prop.IsDefined(typeof(SavableAttribute), true));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 根据属性和其所属类型生成一个唯一的存储键。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private string GetSavableKey(PropertyInfo prop, Type singletonType)
|
|
|
|
|
|
{
|
|
|
|
|
|
var savableAttr = prop.GetCustomAttribute<SavableAttribute>();
|
|
|
|
|
|
return savableAttr?.Key ?? $"{singletonType.Name}.{prop.Name}";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-10 14:08:23 +08:00
|
|
|
|
/// 获取给定类型的默认值。
|
2025-09-28 15:02:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private object GetDefaultValue(Type type)
|
|
|
|
|
|
{
|
2025-10-10 14:08:23 +08:00
|
|
|
|
if (type.IsValueType) return Activator.CreateInstance(type);
|
2025-09-28 15:02:57 +08:00
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|