Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
@@ -0,0 +1,68 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public static class CharacterConfigGenerator
{
private const string ActorPrefabFolder = "Assets/Game/Art/Actor/Prefab";
private const string ConfigPath = "Assets/Resources/Config/CharacterConfig.json";
[MenuItem("Tools/Character/Generate Character Config")]
public static void Generate()
{
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { ActorPrefabFolder });
EditorCharacterConfigData config = new EditorCharacterConfigData();
config.spawnPosition = Vector3.zero;
config.spawnEuler = new Vector3(0f, 180f, 0f);
for (int i = 0; i < prefabGuids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
string id = Path.GetFileNameWithoutExtension(path);
config.characters.Add(new EditorCharacterConfigItem
{
id = id,
displayName = id,
prefabPath = path
});
}
config.characters.Sort((left, right) => string.CompareOrdinal(left.id, right.id));
if (config.characters.Count > 0)
{
config.defaultCharacterId = config.characters[0].id;
}
string fullPath = Path.GetFullPath(Path.Combine(Application.dataPath, "../", ConfigPath));
string directory = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(fullPath, JsonUtility.ToJson(config, true));
AssetDatabase.Refresh();
Debug.Log("Generated character config: " + ConfigPath + " (" + config.characters.Count + " characters)");
}
[Serializable]
private class EditorCharacterConfigData
{
public string defaultCharacterId;
public Vector3 spawnPosition;
public Vector3 spawnEuler;
public List<EditorCharacterConfigItem> characters = new List<EditorCharacterConfigItem>();
}
[Serializable]
private class EditorCharacterConfigItem
{
public string id;
public string displayName;
public string prefabPath;
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ee26a5c25ec473ba3d60c7327241cee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,773 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
namespace XGame.EditorTools
{
public class ValidationResult
{
public bool Ok = true;
public readonly List<string> Errors = new List<string>();
public readonly List<string> Warnings = new List<string>();
}
public static class JsonUIPrefabBuilder
{
// Doc/UIPrefabCreater 位于仓库根(Unity 工程目录 Client/ 的上一级),
// 不能用相对路径(Unity 工作目录是工程目录),需基于 dataPath 解析为绝对路径。
private const string JsonRootRelative = "Doc/UIPrefabCreater";
private const string TextureRoot = "Assets/Game/Art/UI/Texture";
private static readonly int[] RequiredRefResolution = { 2048, 1024 };
// 解析 JSON 根目录绝对路径:优先 仓库根/Doc/UIPrefabCreater,回退 工程根/Doc/UIPrefabCreater
private static string ResolveJsonRoot()
{
// Application.dataPath = <repo>/Client/Assets
string projectDir = Directory.GetParent(Application.dataPath).FullName; // <repo>/Client
string repoDir = Directory.GetParent(projectDir)?.FullName ?? projectDir; // <repo>
string atRepo = Path.Combine(repoDir, JsonRootRelative);
if (Directory.Exists(atRepo))
{
return atRepo;
}
string atProject = Path.Combine(projectDir, JsonRootRelative);
return atProject;
}
public static string ResolveDefaultJsonRoot()
{
return ResolveJsonRoot();
}
// 已识别但本期未实现的组件:遇到只告警、不报错(让 LoginPanelControls.json 仍能通过)
private static readonly HashSet<string> KnownUnimplemented = new HashSet<string>
{
"Toggle", "TMP_InputField", "RotationAnimation"
};
[MenuItem("Tools/UI/Generate Prefab From JSON...")]
public static void GenerateOne()
{
string path = EditorUtility.OpenFilePanel("选择 UI JSON", ResolveJsonRoot(), "json");
if (string.IsNullOrEmpty(path))
{
return;
}
string output = BuildFromJsonFile(path);
if (!string.IsNullOrEmpty(output))
{
Debug.Log("[UIGen] 生成成功: " + output);
}
}
[MenuItem("Tools/UI/Generate All Prefabs From JSON")]
public static void GenerateAll()
{
IReadOnlyList<string> outputs = BuildAll(ResolveJsonRoot());
Debug.Log("[UIGen] 批量生成完成,成功 " + outputs.Count + " 个。");
}
public static void BuildGameLobbyFromCommandLine()
{
string jsonPath = Path.Combine(ResolveJsonRoot(), "GameLobbyControls.json");
string output = BuildFromJsonFile(jsonPath);
if (string.IsNullOrEmpty(output))
{
throw new Exception("[UIGen] GameLobbyControls.json build failed.");
}
}
public static void BuildMatchWaitingFromCommandLine()
{
string jsonPath = Path.Combine(ResolveJsonRoot(), "MatchWaitingControls.json");
string output = BuildFromJsonFile(jsonPath);
if (string.IsNullOrEmpty(output))
{
throw new Exception("[UIGen] MatchWaitingControls.json build failed.");
}
}
public static void BuildAllFromCommandLine()
{
string jsonRoot = GetCommandLineValue("-uiJsonRoot");
if (string.IsNullOrEmpty(jsonRoot))
{
jsonRoot = ResolveJsonRoot();
}
else if (!Path.IsPathRooted(jsonRoot))
{
string projectDir = Directory.GetParent(Application.dataPath).FullName;
string repoDir = Directory.GetParent(projectDir)?.FullName ?? projectDir;
jsonRoot = Path.Combine(repoDir, jsonRoot);
}
IReadOnlyList<string> outputs = BuildAll(jsonRoot);
if (outputs.Count == 0)
{
throw new Exception("[UIGen] no prefab was built.");
}
}
public static ValidationResult Validate(string jsonPath)
{
ValidationResult vr = new ValidationResult();
if (!File.Exists(jsonPath))
{
Fail(vr, "文件不存在: " + jsonPath);
return vr;
}
JObject root;
try
{
root = JObject.Parse(File.ReadAllText(jsonPath));
}
catch (Exception ex)
{
Fail(vr, "JSON 解析失败: " + ex.Message);
return vr;
}
foreach (string field in new[] { "schemaVersion", "prefabName", "prefabPath", "canvas", "assets", "nodes" })
{
if (root[field] == null)
{
Fail(vr, "缺少顶层字段: " + field);
}
}
if (!vr.Ok)
{
return vr;
}
string prefabName = (string)root["prefabName"];
string prefabPath = (string)root["prefabPath"];
if (!prefabPath.Replace("\\", "/").StartsWith("Assets/"))
{
Fail(vr, "prefabPath 必须位于 Assets/ 下: " + prefabPath);
}
JToken refRes = root["canvas"]?["canvasScaler"]?["referenceResolution"];
if (refRes == null || (int)refRes[0] != RequiredRefResolution[0] || (int)refRes[1] != RequiredRefResolution[1])
{
Fail(vr, "referenceResolution 必须为 [2048, 1024]");
}
JArray nodes = root["nodes"] as JArray;
if (nodes == null || nodes.Count == 0)
{
Fail(vr, "nodes 为空");
return vr;
}
HashSet<string> names = new HashSet<string>();
List<string> roots = new List<string>();
foreach (JToken node in nodes)
{
string name = (string)node["name"];
if (string.IsNullOrEmpty(name))
{
Fail(vr, "存在无 name 的节点");
continue;
}
if (!names.Add(name))
{
Fail(vr, "节点名重复: " + name);
}
if (node["rect"] == null)
{
Fail(vr, "节点缺少 rect: " + name);
}
if (node["parent"] == null || node["parent"].Type == JTokenType.Null)
{
roots.Add(name);
}
}
foreach (JToken node in nodes)
{
JToken parent = node["parent"];
if (parent != null && parent.Type != JTokenType.Null && !names.Contains((string)parent))
{
Fail(vr, "父节点不存在: " + (string)parent + " (来自 " + (string)node["name"] + ")");
}
}
if (roots.Count != 1)
{
Fail(vr, "根节点数量必须为 1,实际 " + roots.Count);
}
else if (roots[0] != prefabName)
{
Fail(vr, "prefabName 必须与根节点名一致: " + prefabName + " vs " + roots[0]);
}
foreach (string b in EnumStrings(root["bindings"]))
{
if (!names.Contains(b))
{
Fail(vr, "bindings 节点不存在: " + b);
}
}
foreach (JToken ev in (root["events"] as JArray) ?? new JArray())
{
string n = (string)ev["node"];
if (!names.Contains(n))
{
Fail(vr, "events 节点不存在: " + n);
}
}
return vr;
}
public static string BuildFromJsonFile(string jsonPath)
{
ValidationResult vr = Validate(jsonPath);
foreach (string w in vr.Warnings)
{
Debug.LogWarning("[UIGen] " + Path.GetFileName(jsonPath) + ": " + w);
}
if (!vr.Ok)
{
foreach (string e in vr.Errors)
{
Debug.LogError("[UIGen] " + Path.GetFileName(jsonPath) + ": " + e);
}
return null;
}
JObject root = JObject.Parse(File.ReadAllText(jsonPath));
string prefabName = (string)root["prefabName"];
string prefabPath = (string)root["prefabPath"];
JArray nodes = (JArray)root["nodes"];
GameObject rootGo = null;
try
{
Dictionary<string, GameObject> map = new Dictionary<string, GameObject>();
List<Action> deferred = new List<Action>();
foreach (JToken node in nodes)
{
string name = (string)node["name"];
string parentName = node["parent"]?.Type == JTokenType.Null ? null : (string)node["parent"];
GameObject go = new GameObject(name, typeof(RectTransform));
map[name] = go;
if (parentName == null)
{
rootGo = go;
}
else if (map.TryGetValue(parentName, out GameObject parentGo))
{
go.transform.SetParent(parentGo.transform, false);
}
ApplyRect(go.GetComponent<RectTransform>(), node["rect"] as JObject);
bool active = node["active"] == null || (bool)node["active"];
foreach (JToken comp in (node["components"] as JArray) ?? new JArray())
{
ApplyComponent(go, comp as JObject, map, deferred, Path.GetFileName(jsonPath));
}
go.SetActive(active);
}
foreach (Action a in deferred)
{
a();
}
string dir = Path.GetDirectoryName(prefabPath);
if (!AssetDatabase.IsValidFolder(dir))
{
Directory.CreateDirectory(dir);
AssetDatabase.Refresh();
}
GameObject saved = PrefabUtility.SaveAsPrefabAsset(rootGo, prefabPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[UIGen] " + prefabName + " 生成完成 -> " + prefabPath);
return saved == null ? null : prefabPath;
}
finally
{
if (rootGo != null)
{
UnityEngine.Object.DestroyImmediate(rootGo);
}
}
}
public static IReadOnlyList<string> BuildAll(string jsonRoot)
{
List<string> outputs = new List<string>();
int failed = 0;
if (!Directory.Exists(jsonRoot))
{
Debug.LogError("[UIGen] JSON 目录不存在: " + jsonRoot);
return outputs;
}
foreach (string file in Directory.GetFiles(jsonRoot, "*.json", SearchOption.AllDirectories))
{
// 只处理含 prefabName/nodes 的 UI 描述文件,跳过其它 json
string text;
try { text = File.ReadAllText(file); } catch { continue; }
if (!text.Contains("\"prefabName\"") || !text.Contains("\"nodes\""))
{
Debug.Log("[UIGen] 跳过非 UI JSON: " + file);
continue;
}
string output = BuildFromJsonFile(file);
if (string.IsNullOrEmpty(output))
{
failed++;
}
else
{
outputs.Add(output);
}
}
Debug.Log("[UIGen] BuildAll 完成 Success:" + outputs.Count + " Failed:" + failed);
return outputs;
}
public static IReadOnlyList<string> BuildChanged(string jsonRoot, string reason, bool force = false)
{
List<string> outputs = new List<string>();
int skipped = 0;
int failed = 0;
if (!Directory.Exists(jsonRoot))
{
Debug.LogError("[UIGen] JSON 目录不存在: " + jsonRoot);
return outputs;
}
foreach (string file in Directory.GetFiles(jsonRoot, "*.json", SearchOption.AllDirectories))
{
if (!TryReadUiJson(file, out JObject root))
{
skipped++;
continue;
}
string prefabPath = (string)root["prefabPath"];
if (!force && !IsJsonNewerThanPrefab(file, prefabPath))
{
skipped++;
continue;
}
string output = BuildFromJsonFile(file);
if (string.IsNullOrEmpty(output))
{
failed++;
}
else
{
outputs.Add(output);
Debug.Log("[UIGen:" + reason + "] " + Path.GetFileName(file) + " -> " + output);
}
}
if (outputs.Count > 0 || failed > 0)
{
Debug.Log("[UIGen:" + reason + "] BuildChanged 完成 Success:" + outputs.Count + " Failed:" + failed + " Skipped:" + skipped);
}
return outputs;
}
private static bool TryReadUiJson(string file, out JObject root)
{
root = null;
try
{
string text = File.ReadAllText(file);
if (!text.Contains("\"prefabName\"") || !text.Contains("\"nodes\""))
{
return false;
}
root = JObject.Parse(text);
return root["prefabName"] != null && root["prefabPath"] != null && root["nodes"] != null;
}
catch
{
return false;
}
}
private static bool IsJsonNewerThanPrefab(string jsonPath, string prefabPath)
{
if (string.IsNullOrEmpty(prefabPath))
{
return false;
}
string normalizedPrefabPath = prefabPath.Replace("\\", "/");
string fullPrefabPath = Path.IsPathRooted(normalizedPrefabPath)
? normalizedPrefabPath
: Path.Combine(Directory.GetParent(Application.dataPath).FullName, normalizedPrefabPath);
if (!File.Exists(fullPrefabPath))
{
return true;
}
return File.GetLastWriteTimeUtc(jsonPath) > File.GetLastWriteTimeUtc(fullPrefabPath).AddMilliseconds(100);
}
// ---------- RectTransform ----------
private static void ApplyRect(RectTransform rt, JObject rect)
{
if (rect == null)
{
return;
}
rt.anchorMin = ToVec2(rect["anchorMin"], new Vector2(0, 0));
rt.anchorMax = ToVec2(rect["anchorMax"], new Vector2(1, 1));
rt.pivot = ToVec2(rect["pivot"], new Vector2(0.5f, 0.5f));
if (rect["offsetMin"] != null && rect["offsetMax"] != null)
{
rt.offsetMin = ToVec2(rect["offsetMin"], Vector2.zero);
rt.offsetMax = ToVec2(rect["offsetMax"], Vector2.zero);
}
else
{
rt.sizeDelta = ToVec2(rect["sizeDelta"], Vector2.zero);
rt.anchoredPosition = ToVec2(rect["anchoredPosition"], Vector2.zero);
}
}
// 用 Unity 重载的 == 判空,避免 C# ?? 在 Unity 对象上的“假 null”陷阱
private static T GetOrAdd<T>(GameObject go) where T : Component
{
T c = go.GetComponent<T>();
if (c == null)
{
c = go.AddComponent<T>();
}
return c;
}
// ---------- 组件 ----------
private static void ApplyComponent(GameObject go, JObject comp, Dictionary<string, GameObject> map, List<Action> deferred, string fileName)
{
if (comp == null)
{
return;
}
string type = (string)comp["type"];
switch (type)
{
case "Canvas":
{
Canvas c = GetOrAdd<Canvas>(go);
c.renderMode = RenderMode.ScreenSpaceOverlay;
if (comp["sortingOrder"] != null) c.sortingOrder = (int)comp["sortingOrder"];
break;
}
case "CanvasScaler":
{
CanvasScaler cs = GetOrAdd<CanvasScaler>(go);
cs.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
cs.referenceResolution = ToVec2(comp["referenceResolution"], new Vector2(2048, 1024));
cs.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
cs.matchWidthOrHeight = comp["matchWidthOrHeight"] != null ? (float)comp["matchWidthOrHeight"] : 0.5f;
break;
}
case "GraphicRaycaster":
GetOrAdd<GraphicRaycaster>(go);
break;
case "CanvasGroup":
{
CanvasGroup cg = GetOrAdd<CanvasGroup>(go);
if (comp["alpha"] != null) cg.alpha = (float)comp["alpha"];
if (comp["interactable"] != null) cg.interactable = (bool)comp["interactable"];
if (comp["blocksRaycasts"] != null) cg.blocksRaycasts = (bool)comp["blocksRaycasts"];
break;
}
case "Image":
{
Image img = GetOrAdd<Image>(go);
string spriteName = (string)comp["sprite"];
Sprite sprite = ResolveSprite(spriteName, fileName);
img.sprite = sprite;
img.type = string.Equals((string)comp["imageType"], "Sliced", StringComparison.OrdinalIgnoreCase) ? Image.Type.Sliced : Image.Type.Simple;
if (comp["preserveAspect"] != null) img.preserveAspect = (bool)comp["preserveAspect"];
img.raycastTarget = comp["raycastTarget"] == null || (bool)comp["raycastTarget"];
img.color = ParseColor((string)comp["color"], Color.white);
break;
}
case "TextMeshProUGUI":
{
TextMeshProUGUI t = GetOrAdd<TextMeshProUGUI>(go);
t.text = (string)comp["text"] ?? string.Empty;
if (comp["fontSize"] != null) t.fontSize = (float)comp["fontSize"];
t.fontStyle = string.Equals((string)comp["fontStyle"], "Bold", StringComparison.OrdinalIgnoreCase) ? FontStyles.Bold : FontStyles.Normal;
t.alignment = ParseAlignment((string)comp["alignment"]);
t.color = ParseColor((string)comp["color"], Color.white);
t.raycastTarget = comp["raycastTarget"] != null && (bool)comp["raycastTarget"];
break;
}
case "Button":
{
Button btn = GetOrAdd<Button>(go);
Image target = go.GetComponent<Image>();
if (target != null) btn.targetGraphic = target;
if (string.Equals((string)comp["transition"], "SpriteSwap", StringComparison.OrdinalIgnoreCase))
{
btn.transition = Selectable.Transition.SpriteSwap;
SpriteState state = btn.spriteState;
state.highlightedSprite = ResolveSprite((string)comp["spriteHighlighted"], fileName);
state.pressedSprite = ResolveSprite((string)comp["spritePressed"], fileName);
state.disabledSprite = ResolveSprite((string)comp["spriteDisabled"], fileName);
btn.spriteState = state;
}
else
{
btn.transition = Selectable.Transition.ColorTint;
}
// onClick 仅作事件标记,不在 Prefab 写死回调
break;
}
case "Shadow":
{
Shadow shadow = GetOrAdd<Shadow>(go);
shadow.effectColor = ParseColor((string)comp["effectColor"], new Color(0.06f, 0.12f, 0.2f, 0.75f));
shadow.effectDistance = ToVec2(comp["effectDistance"], new Vector2(1f, -1f));
if (comp["useGraphicAlpha"] != null) shadow.useGraphicAlpha = (bool)comp["useGraphicAlpha"];
break;
}
case "Outline":
{
Outline outline = GetOrAdd<Outline>(go);
outline.effectColor = ParseColor((string)comp["effectColor"], new Color(0.06f, 0.12f, 0.2f, 0.75f));
outline.effectDistance = ToVec2(comp["effectDistance"], new Vector2(1f, -1f));
if (comp["useGraphicAlpha"] != null) outline.useGraphicAlpha = (bool)comp["useGraphicAlpha"];
break;
}
case "ScrollRect":
{
ScrollRect sr = GetOrAdd<ScrollRect>(go);
sr.horizontal = comp["horizontal"] != null && (bool)comp["horizontal"];
sr.vertical = comp["vertical"] == null || (bool)comp["vertical"];
string viewportName = (string)comp["viewport"];
string contentName = (string)comp["content"];
deferred.Add(() =>
{
if (!string.IsNullOrEmpty(viewportName) && map.TryGetValue(viewportName, out GameObject vp))
sr.viewport = vp.GetComponent<RectTransform>();
if (!string.IsNullOrEmpty(contentName) && map.TryGetValue(contentName, out GameObject ct))
sr.content = ct.GetComponent<RectTransform>();
});
break;
}
case "RectMask2D":
GetOrAdd<RectMask2D>(go);
break;
case "Mask":
{
GetOrAdd<Image>(go);
Mask m = GetOrAdd<Mask>(go);
if (comp["showMaskGraphic"] != null) m.showMaskGraphic = (bool)comp["showMaskGraphic"];
break;
}
case "VerticalLayoutGroup":
{
VerticalLayoutGroup v = GetOrAdd<VerticalLayoutGroup>(go);
if (comp["spacing"] != null) v.spacing = (float)comp["spacing"];
v.padding = ParsePadding(comp["padding"]);
v.childAlignment = ParseAnchor((string)comp["childAlignment"]);
if (comp["childControlWidth"] != null) v.childControlWidth = (bool)comp["childControlWidth"];
if (comp["childControlHeight"] != null) v.childControlHeight = (bool)comp["childControlHeight"];
if (comp["childForceExpandWidth"] != null) v.childForceExpandWidth = (bool)comp["childForceExpandWidth"];
if (comp["childForceExpandHeight"] != null) v.childForceExpandHeight = (bool)comp["childForceExpandHeight"];
break;
}
case "ContentSizeFitter":
{
ContentSizeFitter f = GetOrAdd<ContentSizeFitter>(go);
f.verticalFit = ParseFit((string)comp["verticalFit"]);
f.horizontalFit = ParseFit((string)comp["horizontalFit"]);
break;
}
case "LayoutElement":
{
LayoutElement le = GetOrAdd<LayoutElement>(go);
if (comp["minHeight"] != null) le.minHeight = (float)comp["minHeight"];
if (comp["minWidth"] != null) le.minWidth = (float)comp["minWidth"];
if (comp["preferredHeight"] != null) le.preferredHeight = (float)comp["preferredHeight"];
if (comp["preferredWidth"] != null) le.preferredWidth = (float)comp["preferredWidth"];
break;
}
case "MonoBehaviour":
{
string typeName = (string)comp["typeName"];
deferred.Add(() =>
{
Type componentType = FindComponentType(typeName);
if (componentType == null)
{
Debug.LogWarning("[UIGen] " + fileName + ": 找不到脚本组件: " + typeName + " (" + go.name + ")");
}
else if (go.GetComponent(componentType) == null)
{
go.AddComponent(componentType);
}
});
break;
}
default:
if (KnownUnimplemented.Contains(type))
Debug.LogWarning("[UIGen] " + fileName + ": 组件未实现,已跳过: " + type + " (" + go.name + ")");
else
Debug.LogWarning("[UIGen] " + fileName + ": 未知组件类型: " + type + " (" + go.name + ")");
break;
}
}
// ---------- 资源/解析辅助 ----------
private static Type FindComponentType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
return null;
}
Type type = Type.GetType(typeName);
if (type != null && typeof(Component).IsAssignableFrom(type))
{
return type;
}
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
type = assembly.GetType(typeName);
if (type != null && typeof(Component).IsAssignableFrom(type))
{
return type;
}
}
return null;
}
private static Sprite ResolveSprite(string spriteName, string fileName)
{
if (string.IsNullOrEmpty(spriteName))
{
return null;
}
string[] guids = AssetDatabase.FindAssets(spriteName, new[] { TextureRoot });
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.Equals(Path.GetFileNameWithoutExtension(path), spriteName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
if (sprite != null)
{
return sprite;
}
Debug.LogWarning("[UIGen] " + fileName + ": 贴图存在但非 Sprite 类型,回退纯色: " + spriteName + " (" + path + ")");
return null;
}
Debug.LogWarning("[UIGen] " + fileName + ": 找不到 Sprite,回退纯色: " + spriteName);
return null;
}
private static Color ParseColor(string hex, Color fallback)
{
if (!string.IsNullOrEmpty(hex) && ColorUtility.TryParseHtmlString(hex, out Color c))
{
return c;
}
return fallback;
}
private static Vector2 ToVec2(JToken arr, Vector2 fallback)
{
if (arr is JArray a && a.Count >= 2)
{
return new Vector2((float)a[0], (float)a[1]);
}
return fallback;
}
private static RectOffset ParsePadding(JToken token)
{
if (token is JArray a && a.Count >= 4)
{
return new RectOffset((int)a[0], (int)a[1], (int)a[2], (int)a[3]);
}
return new RectOffset(0, 0, 0, 0);
}
private static TextAlignmentOptions ParseAlignment(string a)
{
switch (a)
{
case "Left": return TextAlignmentOptions.Left;
case "Right": return TextAlignmentOptions.Right;
default: return TextAlignmentOptions.Center;
}
}
private static TextAnchor ParseAnchor(string a)
{
switch (a)
{
case "UpperLeft": return TextAnchor.UpperLeft;
case "UpperCenter": return TextAnchor.UpperCenter;
case "MiddleCenter": return TextAnchor.MiddleCenter;
default: return TextAnchor.UpperCenter;
}
}
private static ContentSizeFitter.FitMode ParseFit(string f)
{
switch (f)
{
case "PreferredSize": return ContentSizeFitter.FitMode.PreferredSize;
case "MinSize": return ContentSizeFitter.FitMode.MinSize;
default: return ContentSizeFitter.FitMode.Unconstrained;
}
}
private static IEnumerable<string> EnumStrings(JToken token)
{
if (token is JArray a)
{
foreach (JToken t in a)
{
yield return (string)t;
}
}
}
private static void Fail(ValidationResult vr, string msg)
{
vr.Ok = false;
vr.Errors.Add(msg);
}
private static string GetCommandLineValue(string key)
{
string[] args = Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length - 1; i++)
{
if (string.Equals(args[i], key, StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
}
return null;
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb4bea5d0c797ba44a1d1f7f0f6b1859
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using XGame.MiniGame;
namespace XGame.Editor
{
public static class PcMiniGameSmokeMenu
{
[MenuItem("XWorld/PC Smoke/Create Lobby Flow Launcher")]
public static void CreateLobbyLauncher()
{
var go = GameObject.Find("PcLobbySmoke");
if (go == null) go = new GameObject("PcLobbySmoke");
var launcher = go.GetComponent<PcLobbySmokeLauncher>();
if (launcher == null) launcher = go.AddComponent<PcLobbySmokeLauncher>();
launcher.GatewayUrl = "ws://127.0.0.1:5005/ws?pid=1";
Selection.activeObject = go;
EditorGUIUtility.PingObject(go);
}
[MenuItem("XWorld/PC Smoke/Create MiniGame Smoke Launcher")]
public static void CreateLauncher()
{
var go = GameObject.Find("PcMiniGameSmoke");
if (go == null) go = new GameObject("PcMiniGameSmoke");
var launcher = go.GetComponent<PcMiniGameSmokeLauncher>();
if (launcher == null) launcher = go.AddComponent<PcMiniGameSmokeLauncher>();
launcher.GatewayUrl = "ws://127.0.0.1:5005/ws?pid=1";
launcher.GameId = "rps";
launcher.Version = 1;
Selection.activeObject = go;
EditorGUIUtility.PingObject(go);
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b321899c22e57e429d29651b9cd3da1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,20 @@
{
"name": "XWorld.Link.Editor",
"rootNamespace": "",
"references": [
"GUID:460855e1f37306f4889afbf96eb004f0",
"GUID:7fcefd2f2ffb2fa44811389367ebb809",
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 119d45324677fde49923efc4ea54b313
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,269 @@
using System.IO;
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.TextCore.LowLevel;
using Object = UnityEngine.Object;
public static class XWorldUIFontInstaller
{
private const string SourceFontPath = "Assets/Game/Art/UI/Font/siyuan_CN_Bold.otf";
private const string TmpFontAssetPath = "Assets/Game/Art/UI/Font/siyuan_CN_Bold SDF.asset";
private const string TmpSettingsPath = "Assets/TextMesh Pro/Resources/TMP Settings.asset";
private static bool s_IsInstalling;
[InitializeOnLoadMethod]
private static void InstallDefaultFontOnLoad()
{
EditorApplication.delayCall += () =>
{
if (!File.Exists(SourceFontPath))
return;
InstallDefaultFont();
};
}
[MenuItem("XWorld/UI/Install Default Font", false, 702)]
public static void InstallDefaultFont()
{
if (s_IsInstalling)
return;
s_IsInstalling = true;
try
{
AssetDatabase.ImportAsset(SourceFontPath, ImportAssetOptions.ForceUpdate);
Font sourceFont = AssetDatabase.LoadAssetAtPath<Font>(SourceFontPath);
if (sourceFont == null)
{
Debug.LogError($"Default UI font source is missing: {SourceFontPath}");
return;
}
Type tmpFontAssetType = FindType("TMPro.TMP_FontAsset");
if (tmpFontAssetType == null)
{
Debug.LogError("Unable to find TMPro.TMP_FontAsset. Make sure TextMeshPro is installed.");
return;
}
Object fontAsset = AssetDatabase.LoadAssetAtPath(TmpFontAssetPath, tmpFontAssetType);
if (fontAsset == null)
fontAsset = CreateDynamicTmpFontAsset(sourceFont, tmpFontAssetType);
if (fontAsset == null)
return;
SetTmpDefaultFont(fontAsset);
ApplyFontToKnownPrefabs(fontAsset, tmpFontAssetType);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"Installed default UI font: {TmpFontAssetPath}");
}
finally
{
s_IsInstalling = false;
}
}
[MenuItem("XWorld/UI/Install Default Font", true)]
private static bool ValidateInstallDefaultFont()
{
return File.Exists(SourceFontPath);
}
private static Object CreateDynamicTmpFontAsset(Font sourceFont, Type tmpFontAssetType)
{
string directory = Path.GetDirectoryName(TmpFontAssetPath);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
const int samplingPointSize = 90;
const int atlasPadding = 9;
const int atlasSize = 2048;
const GlyphRenderMode renderMode = GlyphRenderMode.SDFAA;
Type atlasPopulationModeType = FindType("TMPro.AtlasPopulationMode");
if (atlasPopulationModeType == null)
{
Debug.LogError("Unable to find TMPro.AtlasPopulationMode.");
return null;
}
MethodInfo createFontAsset = tmpFontAssetType.GetMethod(
"CreateFontAsset",
BindingFlags.Public | BindingFlags.Static,
null,
new[]
{
typeof(Font),
typeof(int),
typeof(int),
typeof(GlyphRenderMode),
typeof(int),
typeof(int),
atlasPopulationModeType,
typeof(bool)
},
null);
if (createFontAsset == null)
{
Debug.LogError("Unable to find TMP_FontAsset.CreateFontAsset overload.");
return null;
}
object dynamicMode = Enum.ToObject(atlasPopulationModeType, 1);
Object fontAsset = createFontAsset.Invoke(
null,
new object[]
{
sourceFont,
samplingPointSize,
atlasPadding,
renderMode,
atlasSize,
atlasSize,
dynamicMode,
true
}) as Object;
if (fontAsset == null)
{
Debug.LogError($"Unable to create TMP font asset from: {SourceFontPath}");
return null;
}
fontAsset.name = "siyuan_CN_Bold SDF";
AssetDatabase.CreateAsset(fontAsset, TmpFontAssetPath);
Texture2D[] atlasTextures = tmpFontAssetType.GetProperty("atlasTextures")?.GetValue(fontAsset) as Texture2D[];
Texture2D texture = atlasTextures != null && atlasTextures.Length > 0 ? atlasTextures[0] : null;
if (texture != null)
{
texture.name = "siyuan_CN_Bold Atlas";
AssetDatabase.AddObjectToAsset(texture, fontAsset);
}
Material material = tmpFontAssetType.GetField("material")?.GetValue(fontAsset) as Material;
if (material != null)
{
material.name = "siyuan_CN_Bold Atlas Material";
AssetDatabase.AddObjectToAsset(material, fontAsset);
}
SetCreationSettings(fontAsset, tmpFontAssetType, samplingPointSize, atlasPadding, atlasSize, renderMode);
EditorUtility.SetDirty(fontAsset);
return fontAsset;
}
private static void SetCreationSettings(Object fontAsset, Type tmpFontAssetType, int samplingPointSize, int atlasPadding, int atlasSize, GlyphRenderMode renderMode)
{
PropertyInfo creationSettingsProperty = tmpFontAssetType.GetProperty("creationSettings");
if (creationSettingsProperty == null)
return;
object settings = Activator.CreateInstance(creationSettingsProperty.PropertyType);
SetField(settings, "sourceFontFileGUID", AssetDatabase.AssetPathToGUID(SourceFontPath));
SetField(settings, "pointSize", samplingPointSize);
SetField(settings, "pointSizeSamplingMode", 0);
SetField(settings, "padding", atlasPadding);
SetField(settings, "packingMode", 0);
SetField(settings, "atlasWidth", atlasSize);
SetField(settings, "atlasHeight", atlasSize);
SetField(settings, "characterSetSelectionMode", 7);
SetField(settings, "characterSequence", string.Empty);
SetField(settings, "renderMode", (int)renderMode);
SetField(settings, "includeFontFeatures", false);
creationSettingsProperty.SetValue(fontAsset, settings);
}
private static void SetTmpDefaultFont(Object fontAsset)
{
Object settings = AssetDatabase.LoadAssetAtPath<Object>(TmpSettingsPath);
if (settings == null)
{
Debug.LogWarning($"TMP Settings asset is missing: {TmpSettingsPath}");
return;
}
SerializedObject serializedSettings = new SerializedObject(settings);
serializedSettings.FindProperty("m_defaultFontAsset").objectReferenceValue = fontAsset;
serializedSettings.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(settings);
}
private static void ApplyFontToKnownPrefabs(Object fontAsset, Type tmpFontAssetType)
{
Type tmpTextType = FindType("TMPro.TMP_Text");
Type tmpInputFieldType = FindType("TMPro.TMP_InputField");
Object fontMaterial = tmpFontAssetType.GetField("material")?.GetValue(fontAsset) as Object;
string[] searchFolders =
{
"Assets/Game/Art/UI/Prefab",
"Assets/Base/Prefab"
};
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", searchFolders);
foreach (string prefabGuid in prefabGuids)
{
string prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
if (prefab == null)
continue;
bool changed = false;
foreach (Component component in prefab.GetComponentsInChildren<Component>(true))
{
if (component == null)
continue;
Type componentType = component.GetType();
if (tmpTextType != null && tmpTextType.IsAssignableFrom(componentType))
{
changed |= SetSerializedObjectReference(component, "m_fontAsset", fontAsset);
changed |= SetSerializedObjectReference(component, "m_sharedMaterial", fontMaterial);
}
if (tmpInputFieldType != null && tmpInputFieldType.IsAssignableFrom(componentType))
changed |= SetSerializedObjectReference(component, "m_GlobalFontAsset", fontAsset);
}
if (changed)
EditorUtility.SetDirty(prefab);
}
}
private static bool SetSerializedObjectReference(Object target, string propertyName, Object value)
{
SerializedObject serializedObject = new SerializedObject(target);
SerializedProperty property = serializedObject.FindProperty(propertyName);
if (property == null || property.objectReferenceValue == value)
return false;
property.objectReferenceValue = value;
serializedObject.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(target);
return true;
}
private static void SetField(object target, string fieldName, object value)
{
target.GetType().GetField(fieldName)?.SetValue(target, value);
}
private static Type FindType(string fullName)
{
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type type = assembly.GetType(fullName);
if (type != null)
return type;
}
return null;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f1a5bda3e6b34a1da46c829607835e64
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b9f130fd0cb4eec47b186c37fdf7dc09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: