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