This commit is contained in:
2026-06-07 11:49:55 +08:00
parent ff0f3bde54
commit 1897658a00
98 changed files with 9903 additions and 13907 deletions

View File

@@ -0,0 +1,157 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using BaseGames.UI;
using BaseGames.UI.Theme;
namespace BaseGames.Editor.UI
{
/// <summary>
/// 输入提示控件脚手架:生成 / 放置 <c>UI_Control_InputPrompt</c> 预制件
/// —— 一个「按键图标 + 文字」的可复用提示(如菜单底部「Ⓐ 确定」「Ⓑ 返回」)。
///
/// <para>图标走已有的设备适配链路(零代码):</para>
/// 控件内 <see cref="InputIconImage"/> 据 ActionName 经 <c>IInputIconService</c> 解析
/// <b>当前设备</b>(键鼠/PS/Xbox/Switch+ 改键后的实际绑定 → 自动显示正确按键图标,
/// 设备切换/改键时自动刷新。<b>前提:美术把按键 sprite 填入 ICN_Keyboard/Xbox/PlayStation/Switch 资产。</b>
///
/// <para>策划/美术用法:</para>
/// 菜单「向选中节点放置 ▸ InputPrompt」放进任意 Canvas
/// 在 Icon 的 InputIconImage 填 ActionName如 Interact/JumpLabel 填提示文字(可挂 LocalizedText
///
/// 菜单BaseGames/UI/控件库/...
/// </summary>
public static class UIInputPromptScaffold
{
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
private const string PfPrompt = "UI_Control_InputPrompt";
[MenuItem("BaseGames/UI/控件库/生成输入提示控件UI_Control_InputPrompt")]
public static void GeneratePrompt()
{
EnsureFolder(ControlsDir);
BuildPrompt();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"[UIInputPrompt] 已生成 {ControlsDir}/{PfPrompt}.prefab。\n" +
"用法:菜单「向选中节点放置 ▸ InputPrompt」放进 UIIcon 填 ActionName、Label 填文字。\n" +
"图标随当前设备自动切换;需美术把按键 sprite 填入 ICN_Keyboard/Xbox/PlayStation/Switch 资产。");
}
[MenuItem("BaseGames/UI/控件库/向选中节点放置 ▸ InputPrompt")]
private static void PlacePrompt()
{
string path = $"{ControlsDir}/{PfPrompt}.prefab";
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null)
{
EditorUtility.DisplayDialog("控件库",
$"未找到预制件:{path}\n请先执行「生成输入提示控件UI_Control_InputPrompt」。", "确定");
return;
}
Transform parent = Selection.activeTransform;
if (parent == null)
{
var canvas = Object.FindObjectOfType<Canvas>();
parent = canvas != null ? canvas.transform : null;
}
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
if (instance == null) return;
Undo.RegisterCreatedObjectUndo(instance, $"Place {PfPrompt}");
Selection.activeGameObject = instance;
EditorGUIUtility.PingObject(instance);
}
// ── 构建 ─────────────────────────────────────────────────────────────
private static void BuildPrompt()
{
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
// 根:横向布局(图标 + 文字),自适应内容宽度
var root = NewUI(PfPrompt, 120f, 44f);
var h = root.AddComponent<HorizontalLayoutGroup>();
h.spacing = 8f; h.childAlignment = TextAnchor.MiddleLeft;
h.childForceExpandWidth = false; h.childForceExpandHeight = false;
h.childControlWidth = true; h.childControlHeight = true;
var fitter = root.AddComponent<ContentSizeFitter>();
fitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
// 图标Image + InputIconImage同物体InputIconImage 要求 Image
var iconGo = NewUIChild(root.transform, "Icon", out _);
var iconImg = iconGo.AddComponent<Image>();
iconImg.preserveAspect = true;
iconImg.sprite = Knob(); // 编辑期占位图;运行时由 InputIconImage 替换为当前设备按键图(无图则自动隐藏)
var iconLe = iconGo.AddComponent<LayoutElement>();
iconLe.preferredWidth = 40f; iconLe.preferredHeight = 40f;
iconLe.minWidth = 32f; iconLe.minHeight = 32f;
var iconComp = iconGo.AddComponent<InputIconImage>();
SetEnum(iconComp, "_mode", (int)InputIconImage.LookupMode.ByActionName);
SetString(iconComp, "_actionName", "Interact"); // 占位 ActionName策划按需改
// 文字:提示标签(可挂 LocalizedText默认占位
var lblGo = NewUIChild(root.transform, "Label", out _);
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
lbl.text = "提示"; lbl.fontSize = 24f; lbl.alignment = TextAlignmentOptions.MidlineLeft;
lbl.color = new Color(0.80f, 0.80f, 0.82f, 1f);
lbl.raycastTarget = false;
var lblLe = lblGo.AddComponent<LayoutElement>();
lblLe.preferredHeight = 40f;
SetEnum(lblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
_ = theme; // 配色随就近父级 UIThemeApplier本控件不自带 applier避免嵌套重复应用
string path = $"{ControlsDir}/{PfPrompt}.prefab";
PrefabUtility.SaveAsPrefabAsset(root, path);
Object.DestroyImmediate(root);
}
// ── 助手 ─────────────────────────────────────────────────────────────
private static GameObject NewUI(string name, float w, float h)
{
var go = new GameObject(name, typeof(RectTransform));
((RectTransform)go.transform).sizeDelta = new Vector2(w, h);
return go;
}
private static GameObject NewUIChild(Transform parent, string name, out RectTransform rt)
{
var go = new GameObject(name, typeof(RectTransform));
rt = (RectTransform)go.transform;
rt.SetParent(parent, false);
return go;
}
private static void SetEnum(Component c, string prop, int value)
{
var so = new SerializedObject(c);
var p = so.FindProperty(prop);
if (p != null) { p.enumValueIndex = value; so.ApplyModifiedPropertiesWithoutUndo(); }
}
private static void SetString(Component c, string prop, string value)
{
var so = new SerializedObject(c);
var p = so.FindProperty(prop);
if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); }
}
private static Sprite Knob() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/Knob.psd");
private static void EnsureFolder(string dir)
{
string[] parts = dir.Split('/');
string cur = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = $"{cur}/{parts[i]}";
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
cur = next;
}
}
}
}