提交模块化内容

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
+138
View File
@@ -0,0 +1,138 @@
# 🧱 Common 公共模块
------
## 📖 概述
Common 模块是项目中最基础、最核心的模块,包含通用工具类、配置文件、扩展方法及基础系统组件。**所有其他模块都应依赖此模块**,务必在拉取其他模块前先行安装。
------
## 📁 功能总览
- ✅ 项目配置管理
- ✅ 单例模式基类(支持非Mono与Mono行为)
- ✅ 常用工具类(加解密、日志、时间、设备信息)
- ✅ 扩展方法(数组、字符串、枚举等)
- ✅ 编辑器工具(自动生成配置文件)
- ✅ GM工具相关支持
------
## ⚙️ 核心系统详解
### 🧩 配置文件系统(GameConfig, NetworkConfig
📌 通过菜单栏 `SwhiteGames > Create GameConfig` 可在 `Resources/` 目录下自动创建 `GameConfig` 文件,包含以下配置字段:
| 字段名 | 含义说明 |
| ------------- | ----------------------------------------- |
| `PackageName` | 游戏包名 |
| `IsRelease` | 是否为正式发布包(关联宏 `GAME_RELEASE` |
| `EnabledLog` | 是否启用日志打印 |
📦 获取配置示例:
```csharp
var enabledLog = ConfigManager.GameConfig?.enabledLog;
var packageName = ConfigManager.GameConfig?.packageName;
```
------
📌 通过菜单栏 `SwhiteGames > Create NetworkConfig` 可在 `Resources/` 目录下自动创建 `NetworkConfig` 文件,包含以下配置字段:
| 字段名 | 含义说明 |
| ------------- |----------|
| `showNetworkLog` | 是否启用日志打印 |
| `debugHost` | 测试环境Host |
| `releaseHost` | 正式环境Host |
| `connectionMode` | 连接模式 |
📦 获取配置示例:
```csharp
var connectionMode = ConfigManager.NetworkConfig?.connectionMode;
var showNetworkLog = ConfigManager.NetworkConfig?.showNetworkLog;
```
------
### 🔁 单例系统
- **SingletonMonoBehaviour**MonoBehaviour 单例,自动创建 GameObject,跨场景不销毁
```csharp
// MonoBehaviour
public class AudioManager : SingletonMonoBehaviour<AudioManager> { ... }
```
------
### 🛠️ 工具类一览(Helper
| 类名 | 功能说明 |
| ----------------- | ------------------------------------------- |
| `Log` | ✅日志打印(开发调试利器) |
| `Cryptor` | 🔐 加密解密支持(对称加密) |
| `TimeHelper` | 🕒 时间戳转换与格式化处理 |
| `DeviceHelper` | 📱 获取设备唯一标识 |
| `RandomHelper` | 🎲 权重随机、随机打乱等 |
| `SerializeHelper` | 🔄 JSON 序列化 / 反序列化(可使用As<T>()替换) |
| `MD5Helper` | 🔑 快速生成MD5签名 |
| `Base64Helper` | 🧬 Base64编解码 |
------
### 📌 日志打印统一用法
统一使用 `Log` 工具类进行日志输出,支持彩色分级,正式环境中可自动关闭日志输出。
```csharp
using SGModule.Common.Helper;
Log.Info("标签", "信息内容"); // ✅绿色输出
Log.Warning("标签", "警告内容"); // ⚠️橙色输出
Log.Error("标签", "错误内容"); // ❌红色输出
```
------
### ✨ 扩展方法一览
- **EnumExtensions**:获取枚举描述 `GameState.Ready.GetDescription()`
- **StringExtensions**:空值检查 `str.IsNullOrWhiteSpace()`
- **ObjectExtensions**:类型安全转换 `"123".As<int>()`
- **ListExtensions**:列表随机元素 `items.Random()`
- **ArrayExtensions**:数组相关操作
------
## 🧠 最佳实践
| 模块 | 建议做法 |
| ---------- | ------------------------------------------------------------ |
| 管理器类 | Mono 类继承 `SingletonMonoBehaviour` |
| 日志打印 | 全局使用 `Log` 统一管理,按模块分类 |
| 类型转换 | 使用 `.As<T>()` 替代传统强转,异常更少 |
| 加密数据 | 所有敏感数据使用 `Cryptor` 加密后存储 |
| 时间处理 | 使用 `TimeHelper` 保证时区一致性 |
| 随机操作 | 使用 `RandomHelper` 替代 `UnityEngine.Random` |
| 数据键管理 | 通过 `KeyRegistry` 注册和使用,避免硬编码 |
------
## ⚠️ 注意事项
-`SingletonMonoBehaviour` 不要手动放入场景,会自动创建
- 🔐 加密密钥需妥善保管,避免泄露
- 🌏 时间处理请注意本地时区与 UTC 的转换
- 🎲 请勿使用 `UnityEngine.Random`,统一使用 `RandomHelper`
------
## 💡 扩展建议
- 🧩 新增扩展方法时,请分类放入 `Extensions/` 并补充完整 XML 注释
- 🧰 新增工具类保持静态类设计,考虑线程安全
- 📖 键系统支持从配置文件中加载,并支持按模块分组管理
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0a5954894fd434e4f8726239c65e8497
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1255dc405c83ca54ba0fe153fde0fd3c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c7910bd51ff93d347891de376c8c6d3c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,75 @@
using System.IO;
using SGModule.Common.Helper;
using UnityEditor;
using UnityEngine;
namespace SwhiteGames.Editor {
public class GameConfigCreator : EditorWindow {
public string packName = "";
public bool enabledLog = true;
public bool hardwareAcceleration = true;
private string _configFileName = "GameConfig";
private void OnGUI() {
GUILayout.Label("GameConfig Settings", EditorStyles.boldLabel);
_configFileName = EditorGUILayout.TextField("Character Name", _configFileName);
packName = EditorGUILayout.TextField(string.IsNullOrEmpty(packName) ? "请输入包名..." : "包名",
packName);
enabledLog = EditorGUILayout.Toggle("开启日志", enabledLog);
#if UNITY_ANDROID
hardwareAcceleration = EditorGUILayout.Toggle("硬件加速", hardwareAcceleration);
#endif
if (GUILayout.Button("Create")) {
if (string.IsNullOrEmpty(packName)) {
EditorUtility.DisplayDialog("请填写正确配置",
$"packName {!string.IsNullOrEmpty(packName)}",
"确定");
return;
}
CreateGameConfig();
Close();
}
}
[MenuItem("SwhiteGames/Create GameConfig")]
public static void ShowWindow() {
GetWindow<GameConfigCreator>("Create GameConfig");
}
private void CreateGameConfig() {
// 确保 Resources 文件夹存在
var resourcesPath = Path.Combine("Assets", "Resources");
if (!Directory.Exists(resourcesPath)) {
Directory.CreateDirectory(resourcesPath);
AssetDatabase.Refresh(); // 刷新资源数据库
Log.Common.Info("Resources 文件夹已创建。");
}
// 检查同名文件
var assetPath = Path.Combine(resourcesPath, _configFileName + ".asset");
if (File.Exists(assetPath)) {
// 如果同名文件已存在,弹出提示并结束逻辑
EditorUtility.DisplayDialog("文件已存在", $"配置文件 '{_configFileName}' 已存在于 Resources 文件夹中。", "确定");
return;
}
// 创建并设置 config 实例
var gameConfig = CreateInstance<GameConfig>();
gameConfig.packageName = packName;
gameConfig.enabledLog = enabledLog;
gameConfig.hardwareAcceleration = hardwareAcceleration;
// 将 ScriptableObject 实例保存到 Resources 文件夹中
AssetDatabase.CreateAsset(gameConfig, assetPath);
AssetDatabase.SaveAssets();
Log.Common.Info("GameConfig 配置文件已成功创建在 " + assetPath);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 88d46a9d0224446fb77e9864ebe4d4f5
timeCreated: 1731660781
@@ -0,0 +1,39 @@
using UnityEditor;
using UnityEngine;
namespace SwhiteGames.Editor {
[CustomEditor(typeof(GameConfig))]
public class GameConfigEditor : UnityEditor.Editor {
public override void OnInspectorGUI() {
DrawDefaultInspector(); // 可选:绘制默认 Inspector
var gameConfig = (GameConfig) target;
// 包名
gameConfig.packageName = EditorGUILayout.TextField("包名", gameConfig.packageName);
// isRelease 选项
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.Toggle(new GUIContent("isRelease", "是否为发布版本, 由玩家设置中脚本定义符 GAME_RELEASE 决定"),
gameConfig.isRelease);
EditorGUI.EndDisabledGroup();
// 启用日志(随 isRelease 联动)
EditorGUI.BeginDisabledGroup(gameConfig.isRelease);
gameConfig.enabledLog =
EditorGUILayout.Toggle(new GUIContent("启用日志", "发布版本默认不可启动日志"), gameConfig.enabledLog);
EditorGUI.EndDisabledGroup();
#if UNITY_ANDROID
// 硬件加速
gameConfig.hardwareAcceleration = EditorGUILayout.Toggle("硬件加速", gameConfig.hardwareAcceleration);
#endif
// 标记对象已修改,确保更改能保存
if (GUI.changed) {
EditorUtility.SetDirty(gameConfig);
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3d35d44cfe6b42dd8abc4fcee72c69c8
timeCreated: 1743589873
@@ -0,0 +1,94 @@
#if UNITY_ANDROID && UNITY_EDITOR
using System.IO;
using System.Xml;
using SGModule.Common;
using SGModule.Common.Helper;
using UnityEditor.Android;
using UnityEngine;
namespace SwhiteGames.Editor {
public class HardwareAccelerationManager : IPostGenerateGradleAndroidProject {
public int callbackOrder => 99;
private static bool EnableHardwareAcceleration => ConfigManager.GameConfig.hardwareAcceleration;
public void OnPostGenerateGradleAndroidProject(string basePath) {
var manifestPath = GetManifestPath(basePath);
if (!File.Exists(manifestPath)) {
Log.Warning("HardwareAcceleration", $"Manifest not found at {manifestPath}");
return;
}
var manifest = new AndroidManifestHelper(manifestPath);
var changed = manifest.SetHardwareAccelerated(EnableHardwareAcceleration);
if (changed) {
manifest.Save();
Log.Info("HardwareAcceleration", $"Updated manifest with hardwareAccelerated={EnableHardwareAcceleration}");
}
}
private string GetManifestPath(string basePath) {
return Path.Combine(basePath, "src/main/AndroidManifest.xml");
}
}
internal class AndroidManifestHelper {
private readonly XmlDocument _doc;
private readonly XmlNamespaceManager _nsMgr;
private const string AndroidNs = "http://schemas.android.com/apk/res/android";
private readonly string _manifestPath;
public AndroidManifestHelper(string path) {
_manifestPath = path;
_doc = new XmlDocument();
_doc.Load(path);
_nsMgr = new XmlNamespaceManager(_doc.NameTable);
_nsMgr.AddNamespace("android", AndroidNs);
}
public bool SetHardwareAccelerated(bool enabled) {
var activity = GetMainActivity();
if (activity == null) {
Debug.LogWarning("[HardwareAcceleration] Could not find main activity");
return false;
}
return SetHardwareAccelerated(activity, enabled);
}
private bool SetHardwareAccelerated(XmlNode activity, bool enabled) {
var value = enabled ? "true" : "false";
var attr = (activity as XmlElement)?.GetAttributeNode("hardwareAccelerated", AndroidNs);
if (attr == null) {
attr = _doc.CreateAttribute("android", "hardwareAccelerated", AndroidNs);
activity.Attributes?.Append(attr);
attr.Value = value;
return true;
}
if (attr.Value != value) {
attr.Value = value;
return true;
}
return false;
}
private XmlNode GetMainActivity() {
return _doc.SelectSingleNode(
"/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and " +
"intent-filter/category/@android:name='android.intent.category.LAUNCHER']",
_nsMgr);
}
public void Save() {
_doc.Save(_manifestPath);
}
}
}
#endif
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dffc1d0f59e04033a4372c6e86992176
timeCreated: 1753164058
@@ -0,0 +1,78 @@
using System.IO;
using SGModule.Common.Helper;
using UnityEditor;
using UnityEngine;
namespace SGModule.Editor {
public class NetworkConfigCreator : EditorWindow {
private const string DefaultHost = "sandbox-api.swhitegames.tech";
private const string ConfigFileName = "NetworkConfig";
public bool showNetworkLog = true;
public string debugHost = DefaultHost;
public string releaseHost = "";
public ConnectionMode model = ConnectionMode.Http;
private void OnGUI() {
showNetworkLog = EditorGUILayout.Toggle("显示日志", showNetworkLog);
debugHost = EditorGUILayout.TextField("Debug Host:", debugHost);
releaseHost = EditorGUILayout.TextField("Release Host:", releaseHost);
model = (ConnectionMode) EditorGUILayout.EnumPopup("连接模式:", model);
if (GUILayout.Button("Create NetworkConfig")) {
if (string.IsNullOrEmpty(debugHost) || string.IsNullOrEmpty(releaseHost)) {
EditorUtility.DisplayDialog("请填写正确配置",
$"Debug Host {(string.IsNullOrEmpty(debugHost) ? "" : "")}, Release Host {(string.IsNullOrEmpty(releaseHost) ? "" : "")}",
"确定");
return;
}
CreateNetworkConfig();
Close();
}
}
[MenuItem("SwhiteGames/Create NetworkConfig")]
public static void ShowWindow() {
GetWindow<NetworkConfigCreator>("Create NetworkConfig");
}
private void CreateNetworkConfig() {
// 确保 Resources 文件夹存在
var resourcesPath = Path.Combine("Assets", "Resources");
if (!Directory.Exists(resourcesPath)) {
Directory.CreateDirectory(resourcesPath);
AssetDatabase.Refresh(); // 刷新资源数据库
Log.Common.Info("Resources 文件夹已创建。");
}
// 检查同名文件
var assetPath = Path.Combine(resourcesPath, ConfigFileName + ".asset");
if (File.Exists(assetPath)) {
// 如果同名文件已存在,弹出提示并结束逻辑
EditorUtility.DisplayDialog("文件已存在", $"配置文件 '{ConfigFileName}' 已存在于 Resources 文件夹中。", "确定");
return;
}
// 检查同名文件
// string assetPath = Path.Combine(resourcesPath, _configFileName + ".asset");
// string uniqueAssetPath = AssetDatabase.GenerateUniqueAssetPath(assetPath); // 生成唯一路径
// 创建并设置 config 实例
var networkConfig = CreateInstance<NetworkConfig>();
networkConfig.showNetworkLog = showNetworkLog;
networkConfig.debugHost = debugHost;
networkConfig.releaseHost = releaseHost;
networkConfig.connectionMode = model;
// 将 ScriptableObject 实例保存到 Resources 文件夹中
AssetDatabase.CreateAsset(networkConfig, assetPath);
AssetDatabase.SaveAssets();
Log.Common.Info("NetworkConfig 配置文件已成功创建在 " + assetPath);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 534fe38ef6ad460493d6cb0a852a7fb5
timeCreated: 1731480153
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 306dc076af5dda247803b0ad907f4968
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3c18f1f3b9ea744f5b3d4a66286b726c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,66 @@
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#import <UIKit/UIKit.h>
extern "C" {
// 保存数据到 Keychain
bool SaveToKeychain(const char* key, const char* value) {
NSString* keyString = [NSString stringWithUTF8String:key];
NSString* valueString = [NSString stringWithUTF8String:value];
NSData* valueData = [valueString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary* query = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : keyString,
};
SecItemDelete((__bridge CFDictionaryRef)query);
NSDictionary* attributes = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : keyString,
(__bridge id)kSecValueData : valueData,
};
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
return status == errSecSuccess;
}
// 从 Keychain 中获取数据
const char* GetFromKeychain(const char* key) {
NSString* keyString = [NSString stringWithUTF8String:key];
NSDictionary* query = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : keyString,
(__bridge id)kSecReturnData : @YES,
(__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne,
};
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status == errSecSuccess) {
NSData* valueData = (__bridge_transfer NSData*)result;
NSString* valueString = [[NSString alloc] initWithData:valueData encoding:NSUTF8StringEncoding];
return strdup([valueString UTF8String]);
}
return NULL;
}
// 删除 Keychain 中的数据
bool DeleteFromKeychain(const char* key) {
NSString* keyString = [NSString stringWithUTF8String:key];
NSDictionary* query = @{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount : keyString,
};
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
return status == errSecSuccess;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9bc4268c12974c68bd77c36016e019ed
timeCreated: 1736750208
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 53a17396f989d874b9b047cca52f77f8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f92c00a864347c945bec347302a061e0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,57 @@
using SGModule.Common.Helper;
using UnityEngine;
namespace SGModule.Common.Base {
public class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour {
private static T _instance;
/// <summary>
/// 初始化完成标记,有需要的地方可以使用
/// </summary>
protected static bool IsInitComplete;
// 存储所有单例的父对象
private static GameObject _singletonParent;
public static T Instance {
get {
if (_instance == null) {
// 尝试找到已存在的实例
_instance = FindObjectOfType<T>();
// 如果没有找到,则创建一个新的实例
if (_instance == null) {
// 确保所有单例对象放到同一个父对象下
if (_singletonParent == null) {
_singletonParent = GameObject.Find("Singletons") ?? new GameObject("Singletons");
DontDestroyOnLoad(_singletonParent); // 不在场景切换时销毁
}
var singletonObject = new GameObject(typeof(T).Name);
singletonObject.transform.SetParent(_singletonParent.transform); // 设置父对象
_instance = singletonObject.AddComponent<T>();
}
}
return _instance;
}
}
// 确保在场景中只存在一个实例
protected virtual void Awake() {
if (_instance == null) {
_instance = this as T;
}
else if (_instance != this) {
Log.Common.Warning($"An instance of {typeof(T)} already exists! Destroying this instance.");
Destroy(gameObject); // 如果有其他实例,销毁当前对象
}
}
protected virtual void OnDestroy() {
if (_instance == this) {
_instance = null; // 清除引用
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1a32d260789150d449d0313a73dcc5b1
timeCreated: 1728986962
@@ -0,0 +1,30 @@
using SGModule.Common.Helper;
using UnityEngine;
namespace SGModule.Common {
public static class ConfigManager {
private const string GameConfigFilePath = "GameConfig";
private const string NetworkConfigFilePath = "NetworkConfig";
static ConfigManager() {
GameConfig = Resources.Load<GameConfig>(GameConfigFilePath);
if (GameConfig == null) {
Log.Common.Error($"加载失败:未找到配置文件 {GameConfigFilePath},请确保它放在 Resources 文件夹下并命名正确");
}
NetworkConfig = Resources.Load<NetworkConfig>(NetworkConfigFilePath);
if (NetworkConfig == null) {
Log.Common.Error($"加载失败:未找到配置文件 {NetworkConfigFilePath},请确保它放在 Resources 文件夹下并命名正确");
}
}
public static GameConfig GameConfig {
get;
}
public static NetworkConfig NetworkConfig {
get;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8639bb4e64f34c3db2c6537cfdac4974
timeCreated: 1731316175
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f468ddf1f1fc08446865eb4fb41b74aa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
using SGModule.Common.Helper;
using SGModule.Common.Interface;
namespace SGModule.Common.Extensions {
public static class ArrayExtensions {
public static T Random<T>(this T[] items, T fallback = default) where T : class, IWeighted {
if (items == null || items.Length == 0) {
return fallback;
}
return RandomHelper.RandomByWeight(items, fallback);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea3b1bd83aa74285b19f87301391b906
timeCreated: 1749197978
@@ -0,0 +1,12 @@
using System;
using System.ComponentModel;
namespace SGModule.Common.Extensions {
public static class EnumExtensions {
public static string GetDescription(this Enum value) {
var field = value.GetType().GetField(value.ToString());
var attribute = (DescriptionAttribute) Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute));
return attribute?.Description ?? value.ToString(); // 如果没有描述,就使用枚举名称
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4fb5c1462c4f411bb8c08fec74c0bb60
timeCreated: 1731403821
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using SGModule.Common.Helper;
using SGModule.Common.Interface;
namespace SGModule.Common.Extensions {
public static class ListExtensions {
public static T Random<T>(this IEnumerable<T> items, T fallback = default) where T : class, IWeighted {
if (items == null) {
return fallback;
}
var list = items as IList<T> ?? items.ToList();
if (list.Count == 0) {
return fallback;
}
return RandomHelper.RandomByWeight(list, fallback);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f42389145cf940a0b4202dbd516228c4
timeCreated: 1749198436
@@ -0,0 +1,302 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Newtonsoft.Json;
using SGModule.Common.Helper;
namespace SGModule.Common.Extensions {
/// <summary>
/// 提供通用对象类型转换的扩展方法,支持数值类型、字符串、枚举、集合、时间等多种类型间的智能转换。
/// </summary>
public static class ObjectExtensions {
/// <summary>
/// 将对象强制转换为目标类型 <typeparamref name="T" />,转换失败时返回默认值。
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="value">待转换的对象</param>
/// <param name="defaultValue">转换失败时返回的默认值</param>
/// <param name="format">可选格式字符串(如用于时间或数值格式化)</param>
/// <param name="formatProvider">格式化提供者,默认为当前区域设置</param>
/// <returns>转换后的目标类型值,或 <paramref name="defaultValue" />。</returns>
/// <example>
/// 字符串到基本类型
/// <code>
/// "123".As&lt;int&gt;(); // 输出 123
/// "45.67".As&lt;float&gt;(); // 输出 45.67f
/// "true".As&lt;bool&gt;(); // 输出 true
/// "2023-01-01".As&lt;DateTime&gt;(); // 输出 2023-01-01
/// "".As&lt;int&gt;(); // 输出 0
/// ((string)null).As&lt;int&gt;(); // 输出 0
/// "abc".As&lt;int&gt;(); // 输出 0(无法转换)
/// "1.23E+5".As&lt;float&gt;(); // 输出 123000f
/// .....
/// </code>
/// </example>
public static T As<T>(this object value, T defaultValue = default, string format = null, IFormatProvider formatProvider = null) {
return (T) As(value, typeof(T), defaultValue, format, formatProvider);
}
/// <summary>
/// 将对象转换为指定的目标类型。
/// 支持基础类型转换、字符串与 JSON 互转、枚举解析、集合映射、Base64 处理等。
/// </summary>
/// <param name="value">要转换的对象</param>
/// <param name="targetType">目标类型</param>
/// <param name="defaultValue">转换失败时的返回值</param>
/// <param name="format">可选的格式字符串(用于时间、数字格式化)</param>
/// <param name="formatProvider">格式提供者(区域信息),默认为当前区域</param>
/// <returns>转换结果或默认值</returns>
public static object As(this object value, Type targetType, object defaultValue = null, string format = null, IFormatProvider formatProvider = null) {
if (value == null || value == DBNull.Value) {
return defaultValue;
}
formatProvider ??= CultureInfo.CurrentCulture;
try {
// 1. 类型兼容直接返回
if (targetType.IsInstanceOfType(value)) {
return value;
}
// 2. 数值类型之间转换(int, float, decimal 等)
if (value.IsNumericType() && targetType.IsNumericType()) {
return Convert.ChangeType(value, targetType, formatProvider);
}
// 3. 字符串 ⇄ 字节数组
if (targetType == typeof(byte[]) && value is string str) {
return Encoding.UTF8.GetBytes(str);
}
if (targetType == typeof(string) && value is byte[] bytes) {
return Encoding.UTF8.GetString(bytes);
}
// 4. 转为字符串
if (targetType == typeof(string)) {
if (value is string s) {
return s;
}
if (value.IsNumericType()) {
return string.Format(formatProvider, format ?? "{0}", value);
}
if (value is DateTime dt) {
return string.IsNullOrEmpty(format) ? dt.ToString(formatProvider) : dt.ToString(format, formatProvider);
}
try {
return JsonConvert.SerializeObject(value);
}
catch {
return defaultValue;
}
}
// 5. 从字符串解析为目标类型
if (value is string strValue) {
if (string.IsNullOrWhiteSpace(strValue)) {
return defaultValue;
}
// 字符串 → 枚举
if (targetType.IsEnum) {
try {
return Enum.Parse(targetType, strValue, true);
}
catch {
return defaultValue;
}
}
// 字符串 → 数值
if (targetType.IsNumericType()) {
return Convert.ChangeType(strValue, targetType, formatProvider);
}
// 字符串 → 时间
if (targetType == typeof(DateTime)) {
return string.IsNullOrEmpty(format)
? DateTime.Parse(strValue, formatProvider)
: DateTime.ParseExact(strValue, format, formatProvider);
}
// 字符串 → 自定义类(JSON
if (targetType.IsClass && targetType != typeof(string)) {
try {
return JsonConvert.DeserializeObject(strValue, targetType);
}
catch {
return defaultValue;
}
}
}
// 6. 集合类型转换(如 List<string> -> List<int>
if (targetType.IsGenericListOrArray() && value is IEnumerable enumerable) {
var elementType = targetType.GetElementType() ?? targetType.GetGenericArguments()[0];
var list = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
foreach (var item in enumerable) {
var converted = typeof(ObjectExtensions)
.GetMethod("As", new[] { typeof(object), typeof(Type), typeof(object), typeof(string), typeof(IFormatProvider) })
?.Invoke(null, new[] { item, elementType, null, format, formatProvider });
list.Add(converted);
}
return targetType.IsArray ? list.ToArray(elementType) : list;
}
// 7. 尝试通过 TypeConverter 转换(主要处理结构体、基础类型)
if (targetType.IsValueType || targetType == typeof(string)) {
var strVal = value.ToString();
if (!string.IsNullOrWhiteSpace(strVal)) {
var converter = TypeDescriptor.GetConverter(targetType);
if (converter.CanConvertFrom(typeof(string))) {
return converter.ConvertFrom(null, formatProvider as CultureInfo, strVal);
}
}
}
// 8. Fallback:通过 JSON 转换
return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(value), targetType);
}
catch {
return defaultValue;
}
}
#region 🔧
private static bool IsNumericType(this Type type) {
return Type.GetTypeCode(type) switch {
TypeCode.Byte or TypeCode.SByte or TypeCode.UInt16 or TypeCode.UInt32 or
TypeCode.UInt64 or TypeCode.Int16 or TypeCode.Int32 or TypeCode.Int64 or
TypeCode.Decimal or TypeCode.Double or TypeCode.Single => true,
_ => false
};
}
private static bool IsNumericType(this object value) {
return value?.GetType().IsNumericType() ?? false;
}
private static bool IsGenericListOrArray(this Type type) {
return type != null && (type.IsArray ||
(type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>)
|| type.GetGenericTypeDefinition() == typeof(IList<>))));
}
private static Array ToArray(this IList list, Type elementType) {
var array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
#endregion
}
public static class ObjectExtensionsTest {
public static void AsTestRun() {
TestCase("int 到 int", 123.As<int>(), 123);
TestCase("float 到 int", 123.1f.As<int>(), 123);
TestCase("double 到 decimal", 99.99.As<decimal>(), 99.99m);
TestCase("int 到 long", 100.As<long>(), 100L);
TestCase("字符串 到 int", "123".As<int>(), 123);
TestCase("字符串 到 float", "45.67".As<float>(), 45.67f);
TestCase("字符串 到 bool", "true".As<bool>(), true);
TestCase("字符串 到 DateTime", "2023-01-01".As<DateTime>(), new DateTime(2023, 1, 1));
TestCase("空字符串 到 int", "".As<int>(), 0);
TestCase("null字符串 到 int", ((string) null).As<int>(), 0);
TestCase("非法字符串 到 int", "abc".As<int>(), 0);
TestCase("科学计数法字符串 到 float", "1.23E+5".As<float>(), 123000f);
TestCase("int 到 字符串", 42.As<string>(), "42");
TestCase("float 到 字符串(en-US", 3.14159f.As<string>(formatProvider: new CultureInfo("en-US")), "3.14159");
TestCase("float 到 字符串(de-DE", 3.14159f.As<string>(formatProvider: new CultureInfo("de-DE")), "3,14159");
TestCase("DateTime 到 字符串(格式化)", new DateTime(2024, 5, 1).As<string>(format: "yyyy-MM-dd"), "2024-05-01");
TestCase("null 到 int", ((object) null).As<int>(), 0);
TestCase("null 到 string", ((object) null).As<string>(), null);
TestCase("DBNull 到 int", DBNull.Value.As<int>(), 0);
TestCase("枚举 到 string", Color.Red.As<string>(), "Red");
TestCase("字符串 到 枚举", "Green".As<Color>(), Color.Green);
var json = "{\"Name\":\"Alice\",\"Age\":30}";
TestCase("JSON 字符串 到 Person", json.As<Person>().Name, "Alice");
TestCase("对象 到 JSON 字符串", new Person { Name = "Alice", Age = 30 }.As<string>(), JsonConvert.SerializeObject(new Person { Name = "Alice", Age = 30 }));
TestCase("字符串数组 到 List<int>", new[] { "1", "2", "3" }.As<List<int>>(), new List<int> { 1, 2, 3 });
TestCase("List<float> 到 float[]", new List<float> { 1.1f, 2.2f }.As<float[]>(), new[] { 1.1f, 2.2f });
TestCase("空集合 到 int[]", new string[] { }.As<int[]>(), new int[] { });
var str = "hello";
var bytes = Encoding.UTF8.GetBytes(str);
TestCase("byte[] 到 base64 string", bytes.As<string>(), str);
TestCase("base64 string 到 byte[]", BitConverter.ToString(str.As<byte[]>()), BitConverter.ToString(bytes));
TestCase("字符串小数(德语) 到 float", "1,23".As<float>(formatProvider: new CultureInfo("de-DE")), 1.23f);
TestCase("字符串小数(英语) 到 float", "1.23".As<float>(formatProvider: new CultureInfo("en-US")), 1.23f);
TestCase("非法 JSON 到 Person(失败返回 null", "{invalid}".As<Person>(), null);
TestCase("不能转的对象 到 int", new Person { Name = "A" }.As<int>(), 0);
TestCase("不能转的对象 到 stringfallback 到 JsonConvert", new Person { Name = "A", Age = 20 }.As<string>(), JsonConvert.SerializeObject(new Person { Name = "A", Age = 20 }));
}
private static void TestCase<T>(string description, T actual, T expected) {
var isSuccess = CommonUtils.DeepEquals(actual, expected);
var actualStr = FormatValue(actual);
var expectedStr = FormatValue(expected);
var msg = $"{description} => 结果: {actualStr}, 期望: {expectedStr}";
if (isSuccess) {
Log.Info("As<T>()", msg);
}
else {
Log.Warning("As<T>()", msg);
}
}
private static string FormatValue<T>(T value) {
if (value == null) {
return "null";
}
if (value is byte[] byteArray) {
return $"[{string.Join(", ", byteArray)}]";
}
if (value is int[] intArray) {
return $"[{string.Join(", ", intArray)}]";
}
return value.ToString();
}
private enum Color {
Red,
Green,
Blue
}
private class Person {
public string Name {
get;
set;
}
public int Age {
get;
set;
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e6571543817d44e79cbd4f48a4d3616a
timeCreated: 1749716272
@@ -0,0 +1,7 @@
namespace SGModule.Common.Extensions {
public static class StringExtensions {
public static bool IsNullOrWhiteSpace(this string str) {
return string.IsNullOrWhiteSpace(str);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 562d871c5a8d49db9d78fb2921475549
timeCreated: 1731309251
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 483f1710e0c24e53a773d154310180e8
timeCreated: 1731657933
@@ -0,0 +1,367 @@
using System;
using System.Collections.Generic;
using SGModule.Common;
using SGModule.Common.Base;
using SGModule.Common.Helper;
using UnityEngine;
public class GMTool : SingletonMonoBehaviour<GMTool> {
private readonly int buttonHeight = 50;
private readonly int fontSize = 35; // 字体大小
private readonly Dictionary<string, string> inputFields = new();
private readonly List<GMToolItem> items = new();
private readonly int spacing = 10; // 控件之间的间距
private GUIStyle buttonStyle;
private int currentX; // 当前 X 坐标
private int currentY; // 当前 Y 坐标
private GUIStyle inputFieldStyle;
private GUIStyle labelStyle;
private bool toggleDisplay; // 控制显示与隐藏
private int windowWidth; // 屏幕宽度
protected override void Awake() {
base.Awake();
if (!ConfigManager.GameConfig.isRelease) {
IsInitComplete = true;
}
}
private void OnGUI() {
if (!IsInitComplete) {
return;
}
// 初始化样式
if (buttonStyle == null || labelStyle == null || inputFieldStyle == null) {
InitializeStyles();
}
// 初始化布局
StartLayout();
// 显示开关按钮
AddButton(toggleDisplay ? "关闭 GM 工具" : "打开 GM 工具", () => {
toggleDisplay = !toggleDisplay;
});
if (!toggleDisplay) {
return;
}
// 添加控件示例
AddLabel("GM 工具");
// AddButton("初始化网络模块", () =>
// {
// var gameConfig = ConfigManager.GetInstance.GetGameConfig();
// NetworkKit.Instance.InitData(gameConfig.packageName, gameConfig.isRelease);
// });
// AddButton("登录", () =>
// {
// NetworkKit.Instance.LoginRequest("Test", false, (arg0, model) =>
// {
// Debug.LogError($"登录结果 {arg0}");
// });
// });
// AddButton($"加载配置模块初始化", () =>
// {
// ConfigLoader.Instance.Init(NetworkKit.Instance.GetLoginModel().setting,
// NetworkKit.Instance.GetLoginModel().cdn_url, new List<ConfigModel>()
// {
// new CommonModel("Common"),
// new PrizeWheelDataModel("PrizeWheelData"),
// },
// state =>
// {
// Debug.LogError($"配置加载状态{state}");
// },
// (errorName, message) =>
// {
// Debug.LogError($"配置解析错误 {errorName} 错误信息:{message}");
// }
// );
// });
// AddButton($"读取Common配置", () =>
// {
// var model = ConfigLoader.Instance.GetConfig<CommonModel>();
// Debug.LogError(model);
// });
// AddButton($"读取PrizeWheelDataModel配置", () =>
// {
// var model = ConfigLoader.Instance.GetConfig<PrizeWheelDataModel>();
// Debug.LogError(model.DataList.Count);
// });
// AddButton($"测试解析为自定义配置", () =>
// {
// Debug.LogError(ConfigLoader.Instance.ParesPersonalizedConfig(new PrizeWheelDataModel("PrizeWheelData"), "prize"));
// });
// AddButton($"读取自定义配置", () =>
// {
// var personalizedConfig = ConfigLoader.Instance.GetPersonalizedConfig<PrizeWheelDataModel>("prize");
// Debug.LogError(personalizedConfig.DataList.Count);
// });
UpdateItems();
// AddButton("退出游戏", Application.Quit);
}
private void InitializeStyles() {
buttonStyle = new GUIStyle(GUI.skin.button) {
fontSize = fontSize,
alignment = TextAnchor.MiddleCenter,
normal = { textColor = Color.green }
};
labelStyle = new GUIStyle(GUI.skin.label) {
fontSize = fontSize,
normal = { textColor = Color.red }
};
inputFieldStyle = new GUIStyle(GUI.skin.textField) {
fontSize = fontSize,
alignment = TextAnchor.MiddleLeft
};
}
private void StartLayout() {
currentX = spacing;
currentY = spacing;
windowWidth = Screen.width;
}
private void AddButton(string text, Action onClick) {
// 计算按钮宽度,基于标题内容自适应
var buttonWidth = Mathf.Max(150, Mathf.CeilToInt(buttonStyle.CalcSize(new GUIContent(text)).x) + 20);
// 换行检查
if (currentX + buttonWidth + spacing > windowWidth) {
currentX = spacing;
currentY += buttonHeight + spacing;
}
// 绘制按钮
if (GUI.Button(new Rect(currentX, currentY, buttonWidth, buttonHeight), text, buttonStyle)) {
onClick?.Invoke();
}
// 更新下一个控件的位置
currentX += buttonWidth + spacing;
}
private void AddLabel(string text) {
// // 计算标签宽度和高度
// Vector2 size = labelStyle.CalcSize(new GUIContent(text));
// int labelWidth = Mathf.CeilToInt(size.x);
// int labelHeight = Mathf.CeilToInt(size.y);
//
// // 换行检查
// if (currentX + labelWidth + spacing > windowWidth)
// {
// currentX = spacing;
// currentY += buttonHeight + spacing;
// }
//
// // 绘制标签
// GUI.Label(new Rect(currentX, currentY, labelWidth, labelHeight), text, labelStyle);
//
// // 更新下一个控件的位置
// currentX += labelWidth + spacing;
// 启用自动换行 可用
labelStyle.wordWrap = true;
var size = labelStyle.CalcSize(new GUIContent(text));
var labelWidth = Mathf.CeilToInt(size.x);
var labelHeight = Mathf.CeilToInt(size.y);
// 计算标签宽度和高度
var maxWidth = windowWidth - spacing * 2; // 标签最大宽度
// float labelWidth = maxWidth; // 长文本直接占满一行
var needNewRow = labelWidth / maxWidth > 0.6f;
if (needNewRow) {
labelWidth = maxWidth;
labelHeight = (int) labelStyle.CalcHeight(new GUIContent(text), labelWidth);
}
// 换行逻辑:如果当前行剩余宽度不足,提前换行
if (currentX + labelWidth + spacing > windowWidth) {
currentX = spacing; // 回到左侧
currentY += buttonHeight + spacing; // 换到下一行
}
// 绘制标签
GUI.Label(new Rect(currentX, currentY, labelWidth, labelHeight), text, labelStyle);
// 更新位置:占满一整行,因此重置到新行
if (needNewRow) {
currentX = spacing;
currentY += labelHeight + spacing;
}
else {
currentX += labelWidth + spacing;
}
}
private void AddInputField(string key, string placeholder, string buttonText, Action<string> onSubmit) {
var inputFieldWidth = Mathf.Max(200, Mathf.CeilToInt(inputFieldStyle.CalcSize(new GUIContent(placeholder)).x) + 20);
var buttonWidth = Mathf.Max(100, Mathf.CeilToInt(buttonStyle.CalcSize(new GUIContent(buttonText)).x) + 20);
if (currentX + inputFieldWidth + buttonWidth + spacing * 2 > windowWidth) {
currentX = spacing;
currentY += buttonHeight + spacing;
}
if (!inputFields.TryGetValue(key, out var inputField)) {
inputFields.Add(key, "");
}
inputField = GUI.TextField(new Rect(currentX, currentY, inputFieldWidth, buttonHeight), inputField, inputFieldStyle);
currentX += inputFieldWidth + spacing;
if (GUI.Button(new Rect(currentX, currentY, buttonWidth, buttonHeight), buttonText, buttonStyle)) {
onSubmit?.Invoke(inputField);
}
inputFields[key] = inputField;
currentX += buttonWidth + spacing;
}
private void AddSeparator(string text = "") {
// 换行以确保分割线占据整行
currentX = spacing;
currentY += buttonHeight + spacing;
// // 分割线高度和宽度
// int separatorHeight = Mathf.CeilToInt(fontSize * 1.5f); // 动态设置高度
// int separatorWidth = windowWidth - spacing * 2;
//
// // 计算文字尺寸
// Vector2 textSize = labelStyle.CalcSize(new GUIContent(text));
// int textWidth = Mathf.CeilToInt(textSize.x);
//
// // 绘制分割线
// Rect lineRect = new Rect(currentX, currentY + separatorHeight / 2, separatorWidth, 1);
// GUI.Box(lineRect, GUIContent.none);
//
//
// // 绘制文字
// if (!string.IsNullOrEmpty(text))
// {
// GUI.Label(new Rect((windowWidth - textWidth) / 2, currentY, textWidth, separatorHeight), text, labelStyle);
// }
// 分割线宽度和高度
var separatorWidth = windowWidth - spacing * 2;
var separatorHeight = Mathf.CeilToInt(fontSize * 1.5f); // 分割线高度(文字高度)
var lineHeight = Mathf.CeilToInt(fontSize * 0.1f); // 线条高度
if (string.IsNullOrEmpty(text)) {
// 如果没有文字,绘制整行分割线
DrawLine(new Rect(currentX, currentY + separatorHeight / 2 - lineHeight / 2, separatorWidth, lineHeight));
}
else {
// 计算文字尺寸
var textSize = labelStyle.CalcSize(new GUIContent(text));
var textWidth = Mathf.CeilToInt(textSize.x);
// 计算分割线两侧长度
var lineWidth = (separatorWidth - textWidth - spacing * 2) / 2;
// 绘制左侧线条
DrawLine(new Rect(currentX, currentY + separatorHeight / 2 - lineHeight / 2, lineWidth, lineHeight));
// 绘制文字
GUI.Label(new Rect(currentX + lineWidth + spacing, currentY, textWidth, separatorHeight), text, labelStyle);
// 绘制右侧线条
DrawLine(new Rect(currentX + lineWidth + textWidth + spacing * 2, currentY + separatorHeight / 2 - lineHeight / 2, lineWidth,
lineHeight));
}
// 更新位置
currentY += separatorHeight + spacing;
}
private void DrawLine(Rect rect) {
// 设置线条颜色
var originalColor = GUI.color;
GUI.color = Color.white; // 可以改成其他颜色
// 绘制线条
GUI.DrawTexture(rect, Texture2D.whiteTexture);
// 恢复原始颜色
GUI.color = originalColor;
}
public void AddItem(GMToolItem item) {
items.Add(item);
}
private void UpdateItems() {
foreach (var item in items) {
switch (item.Type) {
case GUIType.Label:
AddLabel(item.TextFunc.Invoke());
break;
case GUIType.Button:
AddButton(item.TextFunc.Invoke(), () => {
item.OnClick?.Invoke(null);
});
break;
case GUIType.InputField:
AddInputField(item.Key, item.Placeholder, item.TextFunc.Invoke(), s => {
item.OnClick?.Invoke(s);
});
break;
case GUIType.Separator:
AddSeparator(item.TextFunc?.Invoke());
break;
default:
Log.Common.Error($"Unsupported GUIType: {item.Type}");
break;
}
}
}
}
public enum GUIType {
Label,
Button,
InputField,
Separator // 分割线类型
}
public class GMToolItem {
public string Key;
public Action<string> OnClick;
public string Placeholder;
public Func<string> TextFunc;
public GUIType Type;
/// <summary>
/// </summary>
/// <param name="type">控件类型</param>
/// <param name="textFunc">显示的文本,传入委托可以动态变化</param>
/// <param name="onClick">点击委托,只有Button与InputField有效,Button可忽略字符串参数,InputField输入的内容会传入</param>
/// <param name="key">InputField 需要用到的key,记得保持唯一性,如果重复有可能会导致重复的输入框同步一样的内容</param>
/// <param name="placeholder">提示占位文本</param>
public GMToolItem(GUIType type, Func<string> textFunc, Action<string> onClick = null, string key = "", string placeholder = "") {
Type = type;
TextFunc = textFunc;
OnClick = onClick;
Key = key;
Placeholder = placeholder;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d06d4a3e6b703474f8fd425530b85b87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c8ac58d9307eb1c45849414a44d923ca
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,26 @@
using System;
using System.Text;
namespace SGModule.Common.Helper {
public static class Base64Helper {
public static string Encode(string source) {
return Base64Encode(Encoding.UTF8, source);
}
public static string Decode(string result) {
return Base64Decode(Encoding.UTF8, result);
}
private static string Base64Encode(Encoding encoding, string source) {
var bytes = encoding.GetBytes(source);
var encode = Convert.ToBase64String(bytes);
return encode;
}
private static string Base64Decode(Encoding encoding, string result) {
var bytes = Convert.FromBase64String(result);
var decode = encoding.GetString(bytes);
return decode;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0078c84c6f3f4d0ba79a2ef151bf0545
timeCreated: 1731309598
@@ -0,0 +1,21 @@
using Newtonsoft.Json.Linq;
namespace SGModule.Common.Helper {
// 混合通用方法工具类(放置一些通用的静态函数)
public static class CommonUtils {
/// <summary>
/// 深度比较两个对象是否相等,支持复杂结构(如 List、Dictionary、嵌套对象)
/// </summary>
public static bool DeepEquals<T>(T a, T b) {
if (a == null && b == null) {
return true;
}
if (a == null || b == null) {
return false;
}
return JToken.DeepEquals(JToken.FromObject(a), JToken.FromObject(b));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cb253d0247f54d55b83685a0e7c51b70
timeCreated: 1749455481
@@ -0,0 +1,35 @@
using System.Text;
namespace SGModule.Common.Helper {
public class Cryptor {
public static string Encrypt(string data, string key) {
var keyMD5 = MD5Helper.MD5String1(key);
var str = Base64Helper.Encode(data + keyMD5);
var bytes = Encoding.UTF8.GetBytes(str);
for (int i = 0, j = bytes.Length - 1; i < j; i += 1, j -= 1) {
if (i % 2 == 0) {
(bytes[i], bytes[j]) = (bytes[j], bytes[i]);
}
}
var loginData = Encoding.UTF8.GetString(bytes);
return loginData;
}
public static string Decrypt(string data, string key) {
var bytes = Encoding.UTF8.GetBytes(data);
for (int i = 0, j = bytes.Length - 1; i < j; i += 1, j -= 1) {
if (i % 2 == 0) {
(bytes[i], bytes[j]) = (bytes[j], bytes[i]);
}
}
var str = Encoding.UTF8.GetString(bytes);
var str1 = Base64Helper.Decode(str);
var keyMD5 = MD5Helper.MD5String1(key);
var result = str1.Replace(keyMD5, string.Empty);
return result;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 957dffab93b34167b72cdfd2b0e709fe
timeCreated: 1731309567
@@ -0,0 +1,23 @@
using UnityEngine;
namespace SGModule.Common.Helper {
public static class DeviceHelper {
private const string Idfv = "IDFV";
public static string GetDeviceID(string deviceName = "TestUser001") {
#if UNITY_IOS && !UNITY_EDITOR
deviceName = Keychain.Get(Idfv);
if (string.IsNullOrEmpty(deviceName))
{
deviceName = SystemInfo.deviceUniqueIdentifier;
Keychain.Save(Idfv, deviceName);
}
return deviceName;
#elif UNITY_EDITOR
return deviceName;
#endif
return SystemInfo.deviceUniqueIdentifier;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 121d8e1769529b440bc85f03a485ec10
@@ -0,0 +1,115 @@
using System;
using UnityEngine;
namespace SGModule.Common.Helper {
public static class Log {
// 预定义模块实例
public static readonly ModuleLogger Common = new("Common");
public static readonly ModuleLogger Net = new("Net", ConfigManager.NetworkConfig?.showNetworkLog ?? false);
public static readonly ModuleLogger MarkdownKit = new("MarkdownKit");
public static readonly ModuleLogger ApplePay = new("ApplePay");
public static readonly ModuleLogger ConfigLoader = new("ConfigLoader");
public static readonly ModuleLogger NetKit = new("NetKit");
public static readonly ModuleLogger DataStorage = new("DataStorage");
public static readonly ModuleLogger IAP = new("IAP");
private static bool IsEnabled => ConfigManager.GameConfig?.enabledLog ?? false;
public static void Info(string label, string msg, bool showStack = true) {
if (IsEnabled) {
InternalLog(LogType.Log, label, msg, LogColors.Info, showStack);
}
}
public static void Warning(string label, string msg, bool showStack = true) {
if (IsEnabled) {
InternalLog(LogType.Warning, label, msg, LogColors.Warning, showStack);
}
}
public static void Error(string label, string msg, bool showStack = true) {
if (IsEnabled) {
InternalLog(LogType.Error, label, msg, LogColors.Error, showStack);
}
}
public static void Exception(string label, Exception ex, bool showStack = true) {
if (!IsEnabled || ex == null) {
return;
}
var msg = $"Exception: {ex.Message}";
if (showStack && !string.IsNullOrEmpty(ex.StackTrace)) {
msg += $"\n{ex.StackTrace}";
}
InternalLog(LogType.Error, label, msg, LogColors.Exception, showStack);
}
internal static void InternalLog(LogType type, string label, string msg, string color, bool showStack) {
var formattedMsg = $"<color={color}>[{label}]</color> {msg}";
var original = Application.GetStackTraceLogType(type);
if (!showStack) {
Application.SetStackTraceLogType(type, StackTraceLogType.None);
}
Debug.unityLogger.Log(type, formattedMsg);
if (!showStack) {
Application.SetStackTraceLogType(type, original);
}
}
}
public class ModuleLogger {
private readonly bool _isEnabled;
private readonly string _label;
public ModuleLogger(string label, bool isEnabled = true) {
_label = label;
_isEnabled = isEnabled;
}
private bool IsEnabled => (ConfigManager.GameConfig?.enabledLog ?? false) && _isEnabled;
public void Info(string msg, bool showStack = true) {
if (IsEnabled) {
Log.InternalLog(LogType.Log, _label, msg, LogColors.Info, showStack);
}
}
public void Warning(string msg, bool showStack = true) {
if (IsEnabled) {
Log.InternalLog(LogType.Warning, _label, msg, LogColors.Warning, showStack);
}
}
public void Error(string msg, bool showStack = true) {
if (IsEnabled) {
Log.InternalLog(LogType.Error, _label, msg, LogColors.Error, showStack);
}
}
public void Exception(Exception ex, bool showStack = true) {
if (!IsEnabled || ex == null) {
return;
}
var msg = $"Exception: {ex.Message}";
if (showStack && !string.IsNullOrEmpty(ex.StackTrace)) {
msg += $"\n{ex.StackTrace}";
}
Log.InternalLog(LogType.Error, _label, msg, LogColors.Exception, showStack);
}
}
public static class LogColors {
public const string Info = "#4CAF50";
public const string Warning = "#CC9A06";
public const string Error = "#CC423B";
public const string Exception = "red"; // 可选保留原文
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 451867446e034f4194f1319933be9cdd
timeCreated: 1748248695
@@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace SGModule.Common.Helper {
public class MD5Helper {
public static string GetFileMD5(string file) {
try {
var fs = new FileStream(file, FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
var retVal = md5.ComputeHash(fs);
fs.Close();
var sb = new StringBuilder();
foreach (var str in retVal) {
sb.Append(str.ToString("X2"));
}
return sb.ToString();
}
catch (Exception ex) {
throw new Exception("GetFileMD5 fail error: " + ex.Message);
}
}
/// <summary>
/// 获取字符串的MD5值
/// </summary>
public static string GetStringMD5(string str) {
if (string.IsNullOrEmpty(str)) {
return null;
}
MD5 md5 = new MD5CryptoServiceProvider();
var bytResult = md5.ComputeHash(Encoding.UTF8.GetBytes(str));
var strResult = BitConverter.ToString(bytResult);
strResult = strResult.Replace("-", string.Empty);
return strResult;
}
public static string MD5String1(string text) {
var buffer = Encoding.Default.GetBytes(text);
var check = new MD5CryptoServiceProvider();
var somme = check.ComputeHash(buffer);
var result = new StringBuilder();
foreach (var a in somme) {
var value = a.ToString("X");
if (a < 16) {
result.Append(0);
result.Append(value);
}
else {
result.Append(value);
}
}
return result.ToString().ToLower();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0d8c3bafdc1d41ee87e8c72f54f42007
timeCreated: 1731309631
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace SGModule.Common.Helper {
public static class ModuleVersion {
public static void Show() {
var path = Path.Combine(Application.dataPath, "../gupm.toml");
if (!File.Exists(path)) {
Log.Warning("Submodules", $"找不到配置文件: {path}");
return;
}
var insideSubmodules = false;
var submodules = new Dictionary<string, string>();
var lines = File.ReadAllLines(path);
foreach (var rawLine in lines) {
var line = rawLine.Trim();
// 进入 [Submodules] 区块
if (!insideSubmodules) {
if (line.Equals("[Submodules]", StringComparison.OrdinalIgnoreCase)) {
insideSubmodules = true;
}
continue;
}
// 如果遇到下一个区块就退出(可选)
if (line.StartsWith("[") && line.EndsWith("]")) {
break;
}
// 忽略空行和注释
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) {
continue;
}
// 使用 = 来切分
var parts = line.Split(new[] { '=' }, 2);
if (parts.Length != 2) {
continue;
}
var key = parts[0].Trim();
var value = parts[1].Trim().Trim('"');
submodules[key] = value;
}
foreach (var kv in submodules) {
Log.Info("Submodules", $"{kv.Key} : {kv.Value}");
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b578d006bf7b47b8a7ec2d5b48a53c2b
timeCreated: 1748947020
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SGModule.Common.Interface;
namespace SGModule.Common.Helper {
public static class RandomHelper {
private static readonly Random _random = new();
public static T RandomByWeight<T>(IList<T> items, T fallback) where T : class, IWeighted {
var totalWeight = items.Sum(item => item.Weight);
if (totalWeight <= 0) {
return fallback;
}
var roll = _random.Next(0, totalWeight);
var cumulative = 0;
foreach (var item in items) {
cumulative += item.Weight;
if (roll < cumulative) {
return item;
}
}
return items.Last();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 45bcdda204c1483c84aaaf0d6ec0f251
timeCreated: 1749198281
@@ -0,0 +1,40 @@
using System;
using Newtonsoft.Json;
namespace SGModule.Common.Helper {
public static class SerializeHelper {
private static readonly JsonSerializerSettings _defaultUseJsonSettings = new() {
Formatting = Formatting.None,
DateFormatString = "yyyy/MM/dd HH:mm:ss"
};
private static readonly JsonSerializerSettings _jsonIndentedSettings = new() {
Formatting = Formatting.Indented,
DateFormatString = "yyyy/MM/dd HH:mm:ss"
};
static SerializeHelper() {
JsonConvert.DefaultSettings = () => _defaultUseJsonSettings;
}
public static string ToJson(object obj) {
return JsonConvert.SerializeObject(obj, _defaultUseJsonSettings);
}
public static string ToJsonIndented(object obj) {
return JsonConvert.SerializeObject(obj, _jsonIndentedSettings);
}
public static string ToJson(object obj, Type type) {
return JsonConvert.SerializeObject(obj, type, _defaultUseJsonSettings);
}
public static string ToJson<T>(object obj) {
return ToJson(obj, typeof(T));
}
public static T ToObject<T>(string json) {
return JsonConvert.DeserializeObject<T>(json);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9879900ede554789a516d519840ef577
timeCreated: 1731311911
@@ -0,0 +1,54 @@
using System;
namespace SGModule.Common.Helper {
public static class TimeHelper {
/// <summary>
/// 将 Unix 时间戳转换为本地时间
/// </summary>
/// <param name="timestamp">时间戳(秒)</param>
/// <returns>可读的本地时间字符串</returns>
public static string ConvertToLocalTime(long timestamp) {
// Unix 时间戳起始时间
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// 转换为本地时间
var localTime = epoch.AddSeconds(timestamp).ToLocalTime();
return localTime.ToString("yyyy-MM-dd HH:mm:ss");
}
/// <summary>
/// 将 Unix 时间戳转换为 UTC 时间
/// </summary>
/// <param name="timestamp">时间戳(秒)</param>
/// <returns>可读的 UTC 时间字符串</returns>
public static string ConvertToUTCTime(long timestamp) {
// Unix 时间戳起始时间
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
// 转换为 UTC 时间
var utcTime = epoch.AddSeconds(timestamp);
return utcTime.ToString("yyyy-MM-dd HH:mm:ss");
}
public static string FormatTime(int totalSeconds) {
if (totalSeconds < 60) {
return $"{totalSeconds}s"; // 小于60秒,直接显示秒
}
if (totalSeconds < 3600) {
var minutes = totalSeconds / 60;
var seconds = totalSeconds % 60;
return $"{minutes}m {seconds}s"; // 小于1小时,显示分秒
}
else {
var hours = totalSeconds / 3600;
var remainingSeconds = totalSeconds % 3600;
var minutes = remainingSeconds / 60;
var seconds = remainingSeconds % 60;
return $"{hours}h {minutes}m {seconds}s"; // 超过1小时,显示时分秒
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f65b3d4cbd7c456bb9373c261aef0932
timeCreated: 1731662312
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f8e5eb462e8045f19663f3e4bb29c5e6
timeCreated: 1749198053
@@ -0,0 +1,7 @@
namespace SGModule.Common.Interface {
public interface IWeighted {
int Weight {
get;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c91fe8993478411f8d6a81ce2074a336
timeCreated: 1749198110
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 26e8d993c96816f42bf326d832cd0f68
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
using System;
using System.Runtime.InteropServices;
using SGModule.Common.Helper;
public static class Keychain {
public static bool Save(string key, string value) {
#if UNITY_IOS
return SaveToKeychain(key, value);
#endif
Log.Common.Warning("Keychain is only supported on iOS.");
return false;
}
public static string Get(string key) {
#if UNITY_IOS
var resultPtr = GetFromKeychain(key);
if (resultPtr != IntPtr.Zero) {
var result = Marshal.PtrToStringAuto(resultPtr);
return result;
}
return null;
#endif
Log.Common.Warning("Keychain is only supported on iOS.");
return null;
}
public static bool Delete(string key) {
#if UNITY_IOS
return DeleteFromKeychain(key);
#endif
Log.Common.Warning("Keychain is only supported on iOS.");
return false;
}
#if UNITY_IOS
[DllImport("__Internal")]
private static extern bool SaveToKeychain(string key, string value);
[DllImport("__Internal")]
private static extern IntPtr GetFromKeychain(string key);
[DllImport("__Internal")]
private static extern bool DeleteFromKeychain(string key);
#endif
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: acd6a7aa2034443549cb53d6ae963143
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5d82ad3c27862f549a7cd0bc3f5b4e9a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
using UnityEngine;
// [CreateAssetMenu(fileName = "GameConfig", menuName = "GameData/GameConfig")]
public class GameConfig : ScriptableObject {
[Header("全局配置")] [HideInInspector] public string packageName;
#if GAME_RELEASE
private const bool _isRelease = true;
#else
private const bool _isRelease = false;
#endif
public bool isRelease => _isRelease;
private bool _enabledLog = true;
public bool enabledLog {
get => !isRelease && _enabledLog;
set => _enabledLog = !isRelease && value;
}
[HideInInspector] public bool hardwareAcceleration = true;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c836612725633cc4093d7ac06e7d9ef1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using UnityEngine;
// [CreateAssetMenu(fileName = "NetworkConfig", menuName = "GameData/NetworkConfig")]
public class NetworkConfig : ScriptableObject {
[Header("Network Config")] [Header("显示日志")]
public bool showNetworkLog = true;
[Header("Url配置")] public string debugHost;
public string releaseHost;
[Header("连接模式")] public ConnectionMode connectionMode = ConnectionMode.Http;
}
public enum ConnectionMode {
Http, // 基于 HTTP 协议的链接
WebSocket // 基于 WebSocket 协议的链接
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6161e194ecfb437f89a55349867a492e
timeCreated: 1731317249