提交模块化内容

This commit is contained in:
2026-06-04 10:22:38 +08:00
parent 6b8b282347
commit fcf9128dd3
623 changed files with 38437 additions and 2 deletions
+70
View File
@@ -0,0 +1,70 @@
# ⚙️ ConfigLoader 配置加载模块
此模块负责配置文件的加载与检测,依赖于 `Common` 通用模块,请确保已安装该模块。
### 主要功能
- 📂 配置文件加载
- 🔍 配置字段缺失检测
- ❌ 配置字段冗余检测
- ⚙️ 自动化插件安装
- 📝 日志标准化输出
------
## ⚠️ 注意事项
导入项目后,会弹出插件安装提示框。点击“确定”后,插件将自动安装;若点击“取消”,脚本重新编译时仍会再次弹出提示。
- 插件安装需要一定时间,请耐心等待。安装完成后会有弹窗提示。
- 安装完成后,会自动生成配置文件夹 `Configs`,请将游戏配置文件复制到此文件夹中。
- **请勿修改配置文件名**。如需替换配置文件,请先删除旧文件,确保文件夹中每个配置只有一份。
- 游戏首次运行时,会自动将该文件夹复制到游戏的数据存储位置。
------
## 🚀 使用指南
在登录成功后,调用 `ConfigLoader``Init` 方法传入所需参数,具体参数请查看方法注释。
> 注意:
>
> - **List 类型**的配置需继承 `ConfigModel<T, T0>`,不再需要之前的 `IDataList` 接口。
> - **非 List 类型**配置继承 `ConfigModel<T>`。
### 初始化示例
```csharp
var loginModel = NetworkKit.Instance.GetLoginModel();
ConfigLoader.Instance.Init(
loginModel.setting,
loginModel.cdn_url,
new List<ConfigModel> {
new CommonModel("Common"),
new SignDailyRewardModel("SignDailyReward"),
new TurntableModel("turntable"),
// 其它配置...
},
state => {
Debug.Log($"配置加载状态: {state}");
},
(errorName, message) => {
Debug.LogError($"配置解析错误: {errorName},错误信息:{message}");
});
```
------
## 🔍 读取配置示例
```csharp
// 读取基础配置
var model = ConfigLoader.Instance.GetConfig<CommonModel>();
// 解析自定义配置
ConfigLoader.Instance.ParesPersonalizedConfig(new PrizeWheelDataModel("PrizeWheelData"), "prize");
// 获取自定义配置
var personalizedConfig = ConfigLoader.Instance.GetPersonalizedConfig<PrizeWheelDataModel>("prize");
```
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b25e03cedceda0f4db9aebc261a5bf0a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2c34977ebbc9d4440826289429a2adc3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c03491d9ce1584a408c18056b93f91d2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,188 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SGModule.Common.Helper;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
#if USE_ADDRESSABLES
using UnityEditor.AddressableAssets;
#endif
namespace SGModule.Editor {
[InitializeOnLoad]
public static class AddressablesManager {
private const string AddressablesPackageName = "com.unity.addressables";
private const string AddressablesSymbol = "USE_ADDRESSABLES";
private static AddRequest _addRequest;
// 安装标记,避免重复弹窗
private static bool _isInstallingAddressables;
static AddressablesManager() {
EditorApplication.projectChanged += OnProjectChanged; // 监听项目变更
CheckAndSetupAddressables();
}
private static string DefaultConfigsPath => "Assets/Configs"; // 默认的 Addressables 配置路径
private static void CheckAndSetupAddressables() {
if (IsAddressablesInstalled()) {
Log.Info("ConfigLoader", "Addressables 已安装,正在初始化...");
AddScriptingDefineSymbol(AddressablesSymbol);
#if USE_ADDRESSABLES
EnsureAddressablesConfigured();
#endif
}
else {
RemoveScriptingDefineSymbol(AddressablesSymbol);
if (!_isInstallingAddressables) {
PromptToInstallAddressables();
}
}
}
private static bool IsAddressablesInstalled() {
return PackageInfo.GetAllRegisteredPackages().Any(package => package.name == AddressablesPackageName);
}
private static void PromptToInstallAddressables() {
var install = EditorUtility.DisplayDialog(
"Addressables 未安装",
"当前插件依赖 Addressables 功能,请安装以确保正常使用。\n\n是否立即安装 Addressables",
"安装",
"取消");
if (install) {
InstallAddressables();
}
else {
Log.Warning("ConfigLoader", "用户取消安装 Addressables,部分功能可能无法正常使用!");
}
}
private static void InstallAddressables() {
if (_isInstallingAddressables) {
return; // 防止重复安装
}
Log.Info("ConfigLoader", "开始安装 Addressables...");
_isInstallingAddressables = true; // 标记正在安装,避免重复弹窗
_addRequest = Client.Add(AddressablesPackageName);
EditorApplication.update += MonitorAddRequest;
}
private static void MonitorAddRequest() {
if (_addRequest == null || !_addRequest.IsCompleted) {
return;
}
EditorApplication.update -= MonitorAddRequest;
if (_addRequest.Status == StatusCode.Success) {
Log.Info("ConfigLoader", "Addressables 安装成功!");
EditorUtility.DisplayDialog("安装完成", "Addressables 安装成功,请稍候等待 Unity 刷新。", "确定");
CheckAndSetupAddressables();
}
else if (_addRequest.Status >= StatusCode.Failure) {
Log.Error("ConfigLoader", $"安装 Addressables 失败:{_addRequest.Error.message}");
EditorUtility.DisplayDialog("安装失败", $"安装 Addressables 失败:{_addRequest.Error.message}", "确定");
}
_isInstallingAddressables = false; // 重置标记
}
private static void AddScriptingDefineSymbol(string symbol) {
var symbols = new List<string>(PlayerSettings
.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';'));
if (symbols.Contains(symbol)) {
return;
}
symbols.Add(symbol);
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,
string.Join(";", symbols));
Log.Info("ConfigLoader", $"已添加脚本定义符号: {symbol}");
}
private static void RemoveScriptingDefineSymbol(string symbol) {
var symbols = new List<string>(PlayerSettings
.GetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup).Split(';'));
if (symbols.Contains(symbol)) {
symbols.Remove(symbol);
PlayerSettings.SetScriptingDefineSymbolsForGroup(EditorUserBuildSettings.selectedBuildTargetGroup,
string.Join(";", symbols));
Log.Info("ConfigLoader", $"已移除脚本定义符号: {symbol}");
}
}
private static void OnProjectChanged() {
Log.Info("ConfigLoader", "检测到项目变更,重新检查 Addressables 状态...");
CheckAndSetupAddressables();
}
#if USE_ADDRESSABLES
private static void EnsureAddressablesConfigured() {
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null) {
Log.Info("ConfigLoader", "正在初始化 Addressables 设置...");
settings = AddressableAssetSettingsDefaultObject.GetSettings(true); // 自动创建配置
}
// 确保默认的 Configs 文件夹存在并被添加到 Addressables
var needRefresh = false;
if (!Directory.Exists(DefaultConfigsPath)) {
Directory.CreateDirectory(DefaultConfigsPath);
Log.Info("ConfigLoader", $"已创建文件夹: {DefaultConfigsPath}");
needRefresh = true;
}
if (!IsFolderInAddressables(DefaultConfigsPath)) {
var guid = AssetDatabase.AssetPathToGUID(DefaultConfigsPath);
var group = settings.DefaultGroup;
settings.CreateOrMoveEntry(guid, group)?.SetLabel("config", true, true);
needRefresh = true;
}
if (needRefresh) {
AssetDatabase.Refresh();
}
}
private static bool IsFolderInAddressables(string folderPath) {
var settings = AddressableAssetSettingsDefaultObject.Settings;
foreach (var group in settings.groups)
{
if (group)
{
foreach (var entry in group.entries)
{
if (AssetDatabase.GUIDToAssetPath(entry.guid) == folderPath)
{
return true;
}
}
}
}
return false;
}
#endif
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 383ac67f695b4c6e94ba988c9303cd5b
timeCreated: 1733734050
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 34c8928adac69ec44ac63b385ef330d1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 53a14b320d93d7c439c59c8190a874ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,418 @@
using System;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using SGModule.Common;
using SGModule.Common.Base;
using SGModule.Common.Extensions;
using SGModule.Common.Helper;
using UnityEngine;
using UnityEngine.Events;
namespace SGModule.ConfigLoader
{
public enum ConfigLoaderState
{
None,
Failed,
JsonEmptyError,
Successful
}
public class ConfigLoader : SingletonMonoBehaviour<ConfigLoader>
{
private const int MaxErrorCount = 6;
private const string ConfigFileNameKey = "ConfigFileName";
private readonly Dictionary<Type, object> _configData = new();
private readonly Dictionary<string, ConfigModel> _userDefinedConfig = new();
private string _cdnConfigFileName;
private string _cdnUrl;
private string _configJson;
private int _initConfigErrorCount;
private bool _isConfigLoaded;
private Dictionary<string, object> _jsonDictionary = new();
// 暴露只读接口
public IReadOnlyDictionary<string, object> JsonDictionary => _jsonDictionary;
/// <summary>
/// 初始化配置模块方法
/// </summary>
/// <param name="cdnConfigFileName">登录获得的配置文件名</param>
/// <param name="cdnUrl">下载配置的地址</param>
/// <param name="configs">需要初始化解析的配置</param>
/// <param name="configLoadCallback">配置加载回调</param>
/// <param name="parseErrorHandler">配置解析错误回调</param>
public void Init(string cdnConfigFileName, string cdnUrl, List<ConfigModel> configs,
UnityAction<ConfigLoaderState> configLoadCallback = null,
UnityAction<string, string> parseErrorHandler = null)
{
_cdnConfigFileName = cdnConfigFileName;
_cdnUrl = cdnUrl;
IsInitComplete = true;
CheckConfigFile(state =>
{
if (state == ConfigLoaderState.Successful)
{
InitConfigList(configs, parseErrorHandler);
}
configLoadCallback?.Invoke(state);
});
}
/// <summary>
/// 批量解析配置文件
/// </summary>
/// <param name="configs"></param>
/// <param name="errorCallback"></param>
private void InitConfigList(List<ConfigModel> configs, UnityAction<string, string> errorCallback = null)
{
foreach (var config in configs)
{
Pares(config, errorCallback);
}
}
/// <summary>
/// 检查并加载配置文件
/// </summary>
/// <param name="callback"></param>
private void CheckConfigFile(UnityAction<ConfigLoaderState> callback)
{
if (!IsInitComplete)
{
Log.ConfigLoader.Warning("配置加载模块模块未初始化");
return;
}
if (_initConfigErrorCount > MaxErrorCount)
{
callback?.Invoke(ConfigLoaderState.Failed);
return;
}
if (IsFirstLaunch())
{
FirstLaunchCopyConfig(callback); // 进行首次启动的操作
return;
}
bool needDownloadConfigFile = false;
if ((!string.IsNullOrEmpty(_cdnConfigFileName)) && PlayerPrefs.GetString(ConfigFileNameKey, "") != _cdnConfigFileName) needDownloadConfigFile = true;
//需要下载配置的话等待下载更新
if (needDownloadConfigFile)
{
DownloadConfig(callback);
return;
}
ReadLocalConfig(callback);
}
/// <summary>
/// 判断是首次打开游戏
/// </summary>
/// <returns></returns>
private bool IsFirstLaunch()
{
return !PlayerPrefs.HasKey("FirstLaunch");
}
/// <summary>
/// 设置首次启动游戏
/// </summary>
private void SetFirstLaunch()
{
PlayerPrefs.SetInt("FirstLaunch", 1);
}
/// <summary>
/// 首次启动游戏拷贝配置文件到PersistentData目录下
/// </summary>
private void FirstLaunchCopyConfig(UnityAction<ConfigLoaderState> callback)
{
SetFirstLaunch();
FileNetworkManager.Instance.CopyStreamingAssetsToPersistentDataPath(result =>
{
var isSuccess = false;
if (result)
{
var names =
FileNetworkManager.Instance.GetFileNamesFromPersistentDataPath(FileNetworkManager.FolderName);
if (names.Length > 0)
{
PlayerPrefs.SetString(ConfigFileNameKey, names[0]); //项目当前文件中的配置版本
isSuccess = true;
}
}
else
{
Log.ConfigLoader.Error("拷贝文件失败 请检查Configs文件夹是否有配置文件");
}
if (!isSuccess)
{
_initConfigErrorCount++;
}
CheckConfigFile(callback);
});
}
/// <summary>
/// 下载配置文件
/// </summary>
private void DownloadConfig(UnityAction<ConfigLoaderState> callback)
{
FileNetworkManager.Instance.ReadData($"{_cdnUrl}/config/{_cdnConfigFileName}", result =>
{
if (!string.IsNullOrEmpty(result))
{
PlayerPrefs.SetString(ConfigFileNameKey, _cdnConfigFileName); //更新当前配置文件文件名
FileNetworkManager.Instance.WriteToPersistentData(FileNetworkManager.Instance.GetConfigFOlderPath(),
_cdnConfigFileName, result);
ReloadConfig(result, callback);
}
else
{
_initConfigErrorCount++;
Log.ConfigLoader.Error("下载配置文件失败");
ReadLocalConfig(callback);
}
});
}
/// <summary>
/// 读取本地配置文件
/// </summary>
private void ReadLocalConfig(UnityAction<ConfigLoaderState> callback)
{
var savedCfgName = PlayerPrefs.GetString(ConfigFileNameKey);
var path = Path.Combine(FileNetworkManager.Instance.GetConfigFOlderPath(), savedCfgName);
FileNetworkManager.Instance.ReadData(path, result =>
{
if (!string.IsNullOrEmpty(result))
{
ReloadConfig(result, callback);
}
else
{
_initConfigErrorCount++;
Log.ConfigLoader.Error("读取本地数据异常");
//读取本地配置文件失败,重新从默认位置拷贝原始配置文件到配置文件夹下
FirstLaunchCopyConfig(callback);
}
});
}
/// <summary>
/// 检查是否有新的配置
/// </summary>
/// <returns></returns>
private bool HasNewConfig()
{
if (!PlayerPrefs.HasKey(ConfigFileNameKey))
{
return true;
}
var savedCfgName = PlayerPrefs.GetString(ConfigFileNameKey);
var needDownloadConfigFile = false;
if (!string.IsNullOrEmpty(_cdnConfigFileName))
{
//如果本地Player Prefs里没有保存配置文件名
if (string.IsNullOrEmpty(savedCfgName))
{
needDownloadConfigFile = true;
}
else
{
//与CDN上的对比名称
if (!savedCfgName.Equals(_cdnConfigFileName))
{
needDownloadConfigFile = true;
}
}
}
return needDownloadConfigFile;
}
/// <summary>
/// 重新加载配置
/// </summary>
/// <param name="json"></param>
/// <param name="callback"></param>
private void ReloadConfig(string json, [NotNull] UnityAction<ConfigLoaderState> callback)
{
ValidateConfigFile(json, callback);
}
/// <summary>
/// 检查json
/// </summary>
/// <param name="json"></param>
/// <param name="callback"></param>
private void ValidateConfigFile(string json, UnityAction<ConfigLoaderState> callback)
{
if (json.IsNullOrWhiteSpace())
{
callback?.Invoke(ConfigLoaderState.JsonEmptyError);
return;
}
if (!json.StartsWith("{"))
{
var gameConfig = ConfigManager.GameConfig;
json = Cryptor.Decrypt(json, gameConfig.packageName);
}
_configJson = json;
var dictionary = SerializeHelper.ToObject<Dictionary<string, object>>(_configJson);
_jsonDictionary = dictionary;
_isConfigLoaded = true;
callback?.Invoke(ConfigLoaderState.Successful);
// ParseGameConfig(dictionary, callback);
}
/// <summary>
/// 解析配置文件
/// </summary>
/// <param name="config">配置数据结构</param>
/// <param name="errorCallback"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T ParseNewConfig<T>(T config, UnityAction<string, string> errorCallback = null) where T : ConfigModel
{
if (!_isConfigLoaded)
{
Log.ConfigLoader.Warning("配置文件未加载完成");
return default;
}
if (_configData.TryGetValue(config.GetType(), out var obj))
{
Log.ConfigLoader.Warning("当前配置已经成功解析了");
return obj as T;
}
return Pares(config, errorCallback) as T;
}
/// <summary>
/// 解析配置文件
/// </summary>
/// <param name="config">配置数据结构</param>
/// <param name="errorCallback"></param>
/// <param name="addToConfigData">是否保存起来</param>
/// <returns></returns>
private ConfigModel Pares(ConfigModel config, UnityAction<string, string> errorCallback = null,
bool addToConfigData = true)
{
var configModel = config.Parse(JsonDictionary);
if (configModel != null)
{
if (addToConfigData)
{
var type = configModel.GetType();
// if (!_configData.ContainsKey(type))
// {
// _configData.Add(type, configModel);
// }
// else
// {
// Debug.LogWarning($"请注意,重复解析配置文件{type}");
// _configData[type] = configModel;
// }
_configData[type] = configModel;
}
}
else
{
errorCallback?.Invoke(config.GetType().Name, "解析异常");
Log.ConfigLoader.Warning("解析异常");
}
return configModel as ConfigModel;
}
/// <summary>
/// 解析自定义名称配置文件,将配置解析为自定义名称的配置文件并保存
/// </summary>
/// <param name="config">配置数据结构</param>
/// <param name="configName">数据结构类型</param>
/// <param name="errorCallback"></param>
/// <returns></returns>
public bool ParesPersonalizedConfig(ConfigModel config, string configName,
UnityAction<string, string> errorCallback = null)
{
if (_userDefinedConfig.ContainsKey(configName))
{
Log.ConfigLoader.Warning("存在相同名称的自定义配置");
return false;
}
var configModel = Pares(config, errorCallback, false);
if (configModel != null)
{
_userDefinedConfig.Add(configName, configModel);
return true;
}
Log.ConfigLoader.Warning($"解析配置重新异常{config} {configName}");
return false;
}
/// <summary>
/// 获取自定义配置数据
/// </summary>
/// <param name="configName">配置名称</param>
/// <typeparam name="T">对应数据结构</typeparam>
/// <returns></returns>
public T GetPersonalizedConfig<T>(string configName)
{
if (_userDefinedConfig.TryGetValue(configName, out var value))
{
if (value is T configModel)
{
return configModel;
}
Log.ConfigLoader.Warning($"有{configName}的自定义配置,但类型不对,期望类型是{typeof(T).Name} 现有类型{value.GetType().Name} ");
return default;
}
return default;
}
public T GetConfig<T>()
{
return _configData.TryGetValue(typeof(T), out var value) ? (T)value : default;
}
public void AddConfig(ConfigModel configModel)
{
var type = configModel.GetType();
_configData[type] = configModel;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0cda67e479db4e8fbd10667fba04b197
timeCreated: 1731984367
@@ -0,0 +1,236 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SGModule.Common.Base;
using SGModule.Common.Helper;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Networking;
using UnityEngine.ResourceManagement.AsyncOperations;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if USE_ADDRESSABLES
#endif
namespace SGModule.ConfigLoader {
public class FileNetworkManager : SingletonMonoBehaviour<FileNetworkManager> {
public const string FolderName = "Configs";
private const string FolderLabel = "config";
private string _configFolderPath;
protected override void Awake() {
base.Awake();
_configFolderPath = Path.Combine(Application.persistentDataPath, FolderName);
}
public string GetConfigFOlderPath() {
return _configFolderPath;
}
// 示例解析文件列表方法(你可以根据实际需求实现)
private List<string> ParseFileList(string data) {
// 假设 data 是文件列表的文本,解析文件名
// 根据实际情况解析,例如通过换行符分割
return data.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
public void CopyStreamingAssetsToPersistentDataPath(Action<bool> onComplete = null) {
// 如果目标文件夹不存在,创建它
if (!Directory.Exists(_configFolderPath)) {
Directory.CreateDirectory(_configFolderPath);
}
StartCoroutine(CopyFile(onComplete));
}
private void HandleInitializationError() {
// 检查文件是否存在
var path = $"{Application.dataPath}/Library/com.unity.addressables/aa/Android/settings.json";
if (!File.Exists(path)) {
Log.ConfigLoader.Warning(
$"Settings file not found at: {path}. Rebuilding Addressables may be required.");
}
// 提示用户或执行其他逻辑
// 比如:显示弹窗或退出程序
}
private IEnumerator CopyFile(Action<bool> onComplete = null) {
#if USE_ADDRESSABLES
var handle = Addressables.LoadResourceLocationsAsync(FolderLabel);
yield return handle;
if (handle.Status == AsyncOperationStatus.Succeeded) {
// 查找以 ".json" 结尾的文件
var jsonLocation = handle.Result.FirstOrDefault(loc => loc.PrimaryKey.EndsWith(".txt"));
if (jsonLocation != null) {
var jsonFileName = Path.GetFileName(jsonLocation.PrimaryKey);
// 加载 JSON 文件
var textAssetAsync = Addressables.LoadAssetAsync<TextAsset>(jsonLocation);
yield return textAssetAsync;
if (textAssetAsync.Status == AsyncOperationStatus.Succeeded) {
var jsonFile = textAssetAsync.Result;
Log.ConfigLoader.Info($"Loaded JSON Name:{jsonFileName} content: " + jsonFile.text);
var destFilePath = Path.Combine(_configFolderPath, jsonFileName);
File.WriteAllBytes(destFilePath, jsonFile.bytes);
onComplete?.Invoke(true);
}
else {
Log.ConfigLoader.Error("Failed to load JSON file");
onComplete?.Invoke(false);
}
}
else {
Log.ConfigLoader.Error("No JSON file found in folder");
onComplete?.Invoke(false);
}
}
else {
Log.ConfigLoader.Error("Failed to load folder resources");
onComplete?.Invoke(false);
}
#else
Log.Error( "没有 Addressables 插件,请检查 ");
yield break;
#endif
}
private IEnumerator CopyFile(string sourceFile, string destFile, Action<bool> onComplete = null) {
var filePath = "file://" + sourceFile;
using (var www = UnityWebRequest.Get(filePath)) {
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success) {
File.WriteAllBytes(destFile, www.downloadHandler.data);
Log.ConfigLoader.Info($"File copied to {destFile}");
onComplete?.Invoke(true);
}
else {
Log.ConfigLoader.Error("Failed to copy file: " + www.error);
onComplete?.Invoke(false);
}
}
}
/// <summary>
/// 读取或下载
/// </summary>
/// <param name="path"></param>
/// <param name="callback"></param>
public void ReadData(string path, Action<string> callback) {
StartCoroutine(ReadDataEnumerator(path, callback));
}
/// <summary>
/// 读取或下载
/// </summary>
/// <param name="path"></param>
/// <param name="callback"></param>
/// <returns></returns>
private IEnumerator ReadDataEnumerator(string path, Action<string> callback) {
string fullPath;
// 判断是网络URL还是本地文件路径
if (path.StartsWith("http://") || path.StartsWith("https://")) {
fullPath = path; // 网络URL
#if UNITY_EDITOR
if (path.StartsWith("http://") && PlayerSettings.insecureHttpOption == InsecureHttpOption.NotAllowed) {
Log.ConfigLoader.Error("发起了 HTTP 链接,但设置了不允许非Https连接,请检查设置!!!");
callback?.Invoke(null);
yield break;
}
#endif
}
else {
// 本地文件路径
fullPath = "file://" + path;
}
// 使用UnityWebRequest读取数据
using (var webRequest = UnityWebRequest.Get(fullPath)) {
yield return webRequest.SendWebRequest();
if (webRequest.result == UnityWebRequest.Result.ConnectionError ||
webRequest.result == UnityWebRequest.Result.ProtocolError) {
Log.ConfigLoader.Error($"Error while reading file: {webRequest.error} path: {fullPath}");
callback?.Invoke(null); // 返回null表示出错
}
else {
var data = webRequest.downloadHandler.text;
Log.ConfigLoader.Info($"Data read successfully:\n {data}");
callback?.Invoke(data); // 通过回调传出数据
}
}
}
/// <summary>
/// 将内容写入指定文件,并删除原有文件。
/// </summary>
/// <param name="folderName">文件夹名称</param>
/// <param name="fileName">文件名称</param>
/// <param name="content">要写入的内容</param>
public void WriteToPersistentData(string folderName, string fileName, string content) {
// 获取持久化数据路径
var folderPath = Path.Combine(Application.persistentDataPath, folderName);
// 如果文件夹不存在,则创建它
if (!Directory.Exists(folderPath)) {
Directory.CreateDirectory(folderPath);
}
// 删除原有文件
var existingFiles = Directory.GetFiles(folderPath);
foreach (var file in existingFiles) {
File.Delete(file);
}
// 完整文件路径
var filePath = Path.Combine(folderPath, fileName);
// 写入新文件
File.WriteAllText(filePath, content);
Log.ConfigLoader.Info($"File written to: {filePath}");
}
/// <summary>
/// 提取指定目录下所有文件文件名
/// </summary>
/// <param name="subdirectory"></param>
/// <returns></returns>
public string[] GetFileNamesFromPersistentDataPath(string subdirectory) {
var directoryPath = Path.Combine(Application.persistentDataPath, subdirectory);
if (Directory.Exists(directoryPath))
// 获取目录中的所有文件名
{
return Directory.GetFiles(directoryPath)
.Select(Path.GetFileName) // 只提取文件名
.ToArray();
}
Log.ConfigLoader.Warning($"Directory does not exist: {directoryPath}");
return Array.Empty<string>(); // 返回空数组
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 35da25a8aaf547b8accde3b52c38a887
timeCreated: 1731982304
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ed771f8b80c42a24e8b44eb1aa5d12bd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fb3fbe8a7d2b0ea4c822d6cefeee3708
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using SGModule.Common.Helper;
using UnityEngine.Events;
namespace SGModule.ConfigLoader {
public class ConfigModel<T, TD> : ConfigModel where T : class {
public ConfigModel(string key) : base(key) {
DataList = new List<TD>();
}
public List<TD> DataList {
get;
set;
}
public override object Parse(IReadOnlyDictionary<string, object> dictionary,
UnityAction<string, string> errorCallback = null) {
return LoadConfigList<T, TD>(dictionary, errorCallback);
}
protected override T1 LoadConfigList<T1, TD1>(IReadOnlyDictionary<string, object> dictionary,
UnityAction<string, string> errorCallback = null) {
ParseConfig(dictionary, out List<TD> data, errorCallback);
DataList = data;
return this as T1;
}
}
public class ConfigModel<T> : ConfigModel {
public ConfigModel(string key) : base(key) {
}
public override object Parse(IReadOnlyDictionary<string, object> dictionary,
UnityAction<string, string> errorCallback = null) {
return LoadConfig<T>(dictionary, errorCallback);
}
}
public class ConfigModel {
protected readonly string Key;
public bool IsList = false;
protected ConfigModel(string key) {
Key = key;
}
public virtual object Parse(IReadOnlyDictionary<string, object> dictionary,
UnityAction<string, string> errorCallback = null) {
return null;
}
protected T LoadConfig<T>(IReadOnlyDictionary<string, object> dictionary,
UnityAction<string, string> errorCallback = null) {
if (ParseConfig(dictionary, out T data, errorCallback)) {
return data;
}
return default;
}
protected virtual T LoadConfigList<T, TD>(IReadOnlyDictionary<string, object> dictionary,
UnityAction<string, string> errorCallback = null) where T : class {
return default;
}
protected bool ParseConfig<T>(IReadOnlyDictionary<string, object> dictionary, out T obj,
UnityAction<string, string> errorCallback = null) {
obj = default;
if (!dictionary.TryGetValue(Key, out var data)) {
errorCallback?.Invoke(Key, "Excel Not find Sheet");
Log.ConfigLoader.Warning($"Excel Not find Sheet: {Key}");
return false;
}
var config = data.ToString();
try {
var tempObj = SerializeHelper.ToObject<T>(config);
if (tempObj != null) {
#if !GAME_RELEASE
CheckFields(Key, tempObj, config, FieldCheckType.Missing);
CheckFields(Key, tempObj, config, FieldCheckType.Extra);
#endif
obj = tempObj;
return true;
}
}
catch (Exception ex) {
errorCallback?.Invoke(Key, ex.Message);
// 在此处添加异常处理逻辑,例如日志记录
Log.ConfigLoader.Error("Error deserializing config data for type " + typeof(T).Name + ": " + ex.Message);
}
return false;
}
private void CheckFields<T>(string key, T tempObj, string configValue, FieldCheckType checkType) {
List<string> fields;
if (tempObj is IEnumerable<object> tempArray) {
fields = CheckFieldsInArray(tempArray.FirstOrDefault()?.GetType(), configValue, checkType);
}
else {
fields = CheckFields(tempObj.GetType(), configValue, checkType);
}
if (fields.Count <= 0) {
return;
}
var fieldType = checkType == FieldCheckType.Missing ? "Missing" : "Extra";
Log.ConfigLoader.Warning($"Excel Sheet: {key} | {fieldType} fields in JSON: " + string.Join(", ", fields.Distinct()));
}
private static List<string> CheckFieldsInArray(Type objType, string jsonString, FieldCheckType checkType) {
var fields = new List<string>();
var jsonArray = JArray.Parse(jsonString);
foreach (var item in jsonArray) {
fields.AddRange(CheckFields(objType, item.ToString(), checkType));
}
return fields;
}
private static List<string> CheckFields(Type objType, string jsonString, FieldCheckType checkType) {
var resultFields = new List<string>();
try {
var jsonObject = SerializeHelper.ToObject<Dictionary<string, object>>(jsonString);
var fieldNames = objType.GetFields(BindingFlags.Public | BindingFlags.Instance)
.Select(f => f.Name)
.ToHashSet();
switch (checkType) {
// 检查目标类中存在但 JSON 中缺失的字段
case FieldCheckType.Missing:
resultFields.AddRange(fieldNames.Where(field => !jsonObject.ContainsKey(field)));
break;
// 检查 JSON 中存在但目标类中不存在的字段
case FieldCheckType.Extra:
resultFields.AddRange(jsonObject.Keys.Where(key => !fieldNames.Contains(key)));
break;
default:
throw new ArgumentOutOfRangeException(nameof(checkType), checkType, null);
}
}
catch (Exception ex) {
Log.ConfigLoader.Error("Error checking fields for type " + objType.Name + ": " + ex.Message);
}
return resultFields;
}
private enum FieldCheckType {
Missing, // 检查缺失字段
Extra // 检查多余字段
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f1224984e6984416abd0e625b5b842ac
timeCreated: 1732095296
@@ -0,0 +1,13 @@
namespace SGModule.ConfigLoader {
// 列表形式的 数据Model类, 演示
public class TestListModel : ConfigModel<TestListModel, TestListOneModel> {
public TestListModel(string key) : base(key) {
}
}
public class TestListOneModel {
public int Filed1;
public float Filed2;
public int Filed3;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ed458f2acffd4b5b813691f3bd548d50
timeCreated: 1732007715
@@ -0,0 +1,10 @@
namespace SGModule.ConfigLoader {
// KV形式的 数据Model类, 演示
public class TestSingleModel : ConfigModel<TestSingleModel> {
public int Field1;
public float Field2;
public TestSingleModel(string key) : base(key) {
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 820d755e7f654e158e3d67e5965cea16
timeCreated: 1732007669