Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user