feat: add world chat UI

This commit is contained in:
ud18010
2026-07-28 11:54:07 +08:00
parent 09c89aadf0
commit 48aed26725
28 changed files with 5308 additions and 99 deletions
@@ -24,6 +24,7 @@ namespace XGame.EditorTools
// 不能用相对路径(Unity 工作目录是工程目录),需基于 dataPath 解析为绝对路径。
private const string JsonRootRelative = "Doc/UIPrefabCreater";
private const string TextureRoot = "Assets/Game/Art/UI/Texture";
private static readonly int UiLayer = LayerMask.NameToLayer("UI");
private static readonly int[] RequiredRefResolution = { 2048, 1024 };
// 解析 JSON 根目录绝对路径:优先 仓库根/Doc/UIPrefabCreater,回退 工程根/Doc/UIPrefabCreater
@@ -49,7 +50,7 @@ namespace XGame.EditorTools
// 已识别但本期未实现的组件:遇到只告警、不报错(让 LoginPanelControls.json 仍能通过)
private static readonly HashSet<string> KnownUnimplemented = new HashSet<string>
{
"Toggle", "TMP_InputField", "RotationAnimation"
"Toggle", "RotationAnimation"
};
[MenuItem("Tools/UI/Generate Prefab From JSON...")]
@@ -258,6 +259,7 @@ namespace XGame.EditorTools
string parentName = node["parent"]?.Type == JTokenType.Null ? null : (string)node["parent"];
GameObject go = new GameObject(name, typeof(RectTransform));
ApplyUiLayer(go);
map[name] = go;
if (parentName == null)
{
@@ -534,6 +536,21 @@ namespace XGame.EditorTools
// onClick 仅作事件标记,不在 Prefab 写死回调
break;
}
case "TMP_InputField":
{
TMP_InputField input = GetOrAdd<TMP_InputField>(go);
Image target = go.GetComponent<Image>();
if (target != null) input.targetGraphic = target;
if (comp["characterLimit"] != null) input.characterLimit = (int)comp["characterLimit"];
input.contentType = ParseInputContentType((string)comp["contentType"]);
input.lineType = ParseInputLineType((string)comp["lineType"]);
string textName = (string)comp["textComponent"];
string placeholderName = (string)comp["placeholderComponent"];
string placeholderText = (string)comp["placeholder"];
deferred.Add(() => BindTmpInputField(go, input, map, textName, placeholderName, placeholderText));
break;
}
case "Shadow":
{
Shadow shadow = GetOrAdd<Shadow>(go);
@@ -630,7 +647,154 @@ namespace XGame.EditorTools
}
}
private static void BindTmpInputField(
GameObject go,
TMP_InputField input,
Dictionary<string, GameObject> map,
string textName,
string placeholderName,
string placeholderText)
{
if (go == null || input == null)
{
return;
}
RectTransform textViewport = EnsureInputTextViewport(go);
input.textViewport = textViewport;
TextMeshProUGUI text = ResolveOrCreateInputText(
go,
map,
textName,
textViewport,
"Text",
string.Empty,
"#23384CFF");
TextMeshProUGUI placeholder = ResolveOrCreateInputText(
go,
map,
placeholderName,
textViewport,
"Placeholder",
placeholderText ?? string.Empty,
"#7A8DA1FF");
input.textComponent = text;
input.placeholder = placeholder;
if (placeholder != null && !string.IsNullOrEmpty(placeholderText))
{
placeholder.text = placeholderText;
}
}
private static RectTransform EnsureInputTextViewport(GameObject go)
{
Transform existing = go.transform.Find("Text Area");
GameObject textArea = existing == null ? null : existing.gameObject;
if (textArea == null)
{
textArea = new GameObject("Text Area", typeof(RectTransform), typeof(RectMask2D));
ApplyUiLayer(textArea);
textArea.transform.SetParent(go.transform, false);
RectTransform rt = textArea.GetComponent<RectTransform>();
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = new Vector2(24f, 0f);
rt.offsetMax = new Vector2(-24f, 0f);
}
RectTransform viewport = textArea.GetComponent<RectTransform>();
if (textArea.GetComponent<RectMask2D>() == null)
{
textArea.AddComponent<RectMask2D>();
}
return viewport;
}
private static TextMeshProUGUI ResolveOrCreateInputText(
GameObject inputRoot,
Dictionary<string, GameObject> map,
string nodeName,
RectTransform viewport,
string fallbackName,
string defaultText,
string defaultColor)
{
TextMeshProUGUI text = null;
if (!string.IsNullOrEmpty(nodeName) && map.TryGetValue(nodeName, out GameObject mapped) && mapped != null)
{
text = mapped.GetComponent<TextMeshProUGUI>();
if (text == null)
{
text = mapped.AddComponent<TextMeshProUGUI>();
}
if (mapped.transform.parent != viewport)
{
mapped.transform.SetParent(viewport, false);
}
StretchToFill(mapped.GetComponent<RectTransform>());
}
if (text == null)
{
Transform existing = viewport.Find(fallbackName);
GameObject textGo = existing == null ? null : existing.gameObject;
if (textGo == null)
{
textGo = new GameObject(fallbackName, typeof(RectTransform));
ApplyUiLayer(textGo);
textGo.transform.SetParent(viewport, false);
}
StretchToFill(textGo.GetComponent<RectTransform>());
text = GetOrAdd<TextMeshProUGUI>(textGo);
text.text = defaultText ?? string.Empty;
}
ConfigureInputText(text, defaultColor);
return text;
}
private static void ConfigureInputText(TextMeshProUGUI text, string color)
{
if (text == null)
{
return;
}
if (text.fontSize <= 0f)
{
text.fontSize = 28f;
}
text.alignment = text.alignment == TextAlignmentOptions.TopLeft ? TextAlignmentOptions.Left : text.alignment;
text.raycastTarget = false;
if (!string.IsNullOrEmpty(color))
{
text.color = ParseColor(color, text.color);
}
}
// ---------- 资源/解析辅助 ----------
private static void StretchToFill(RectTransform rt)
{
if (rt == null)
{
return;
}
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.pivot = new Vector2(0.5f, 0.5f);
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
private static void ApplyUiLayer(GameObject go)
{
if (go != null && UiLayer >= 0)
{
go.layer = UiLayer;
}
}
private static Type FindComponentType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
@@ -739,6 +903,36 @@ namespace XGame.EditorTools
}
}
private static TMP_InputField.ContentType ParseInputContentType(string contentType)
{
switch (contentType)
{
case "IntegerNumber": return TMP_InputField.ContentType.IntegerNumber;
case "DecimalNumber": return TMP_InputField.ContentType.DecimalNumber;
case "Alphanumeric": return TMP_InputField.ContentType.Alphanumeric;
case "Name": return TMP_InputField.ContentType.Name;
case "EmailAddress": return TMP_InputField.ContentType.EmailAddress;
case "Password": return TMP_InputField.ContentType.Password;
case "Pin": return TMP_InputField.ContentType.Pin;
case "Custom": return TMP_InputField.ContentType.Custom;
case "Standard":
default:
return TMP_InputField.ContentType.Standard;
}
}
private static TMP_InputField.LineType ParseInputLineType(string lineType)
{
switch (lineType)
{
case "MultiLineSubmit": return TMP_InputField.LineType.MultiLineSubmit;
case "MultiLineNewline": return TMP_InputField.LineType.MultiLineNewline;
case "SingleLine":
default:
return TMP_InputField.LineType.SingleLine;
}
}
private static IEnumerable<string> EnumStrings(JToken token)
{
if (token is JArray a)