UI系统
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.UI.MainMenu;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="DataDrivenMainMenuController"/> 自定义 Inspector。
|
||||
///
|
||||
/// 菜单按钮运行时据 UI_MainMenuConfig 生成,编辑器下 MenuPanel 默认为空。
|
||||
/// 本编辑器提供「预览 / 清除」按钮:在编辑器内据表生成按钮,让美术直观查看布局、
|
||||
/// 就近调整 UI_MainMenu_Button 预制件样式与 MenuPanel 布局参数。预览按钮运行时会被自动清空重建。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(DataDrivenMainMenuController))]
|
||||
public class DataDrivenMainMenuControllerEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
var ctrl = (DataDrivenMainMenuController)target;
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.HelpBox(
|
||||
"菜单按钮在运行时据 UI_MainMenuConfig 自动生成(编辑器下 MenuPanel 为空属正常)。\n" +
|
||||
"点「预览」在编辑器内据表生成按钮以查看 / 调整样式与布局;样式改 UI_MainMenu_Button 预制件 / UI_Theme_Default。\n" +
|
||||
"预览按钮仅用于编辑器查看,进入 Play 会自动清空并据表重建。",
|
||||
MessageType.Info);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("▶ 预览菜单(编辑器内)", GUILayout.Height(26)))
|
||||
PreviewMenu(ctrl);
|
||||
if (GUILayout.Button("✕ 清除预览", GUILayout.Height(26), GUILayout.Width(120)))
|
||||
{
|
||||
ctrl.ClearMenu();
|
||||
MarkDirty(ctrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PreviewMenu(DataDrivenMainMenuController ctrl)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorUtility.DisplayDialog("预览菜单", "请退出 Play 模式后再使用编辑器预览。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
ctrl.BuildMenu(); // 内部 ClearMenu + 据表实例化按钮(编辑器用 DestroyImmediate)
|
||||
|
||||
// 标签:运行时 SetKey 在无服务时显示 key,这里改用编辑器预览显示本地化译文
|
||||
foreach (var loc in ctrl.GetComponentsInChildren<LocalizedText>(true))
|
||||
loc.UpdateEditorPreview();
|
||||
|
||||
MarkDirty(ctrl);
|
||||
}
|
||||
|
||||
private static void MarkDirty(DataDrivenMainMenuController ctrl)
|
||||
{
|
||||
EditorUtility.SetDirty(ctrl);
|
||||
if (!Application.isPlaying)
|
||||
EditorSceneManager.MarkSceneDirty(ctrl.gameObject.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a79a1b78119328418b64ed2c384ea9c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.UI.Settings;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="DataDrivenSettingsPanel"/> 自定义 Inspector。
|
||||
///
|
||||
/// 设置行运行时据 SettingsSchemaSO 生成,编辑器下行容器默认为空。
|
||||
/// 本编辑器提供「预览 / 清除」按钮:在编辑器内据表生成行,让美术直观查看分节与布局、
|
||||
/// 就近调整 UI_Setting_*Row 预制件样式与容器布局参数。预览行运行时会被自动清空重建。
|
||||
/// (与 <see cref="DataDrivenMainMenuControllerEditor"/> 同一套预览模式。)
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(DataDrivenSettingsPanel))]
|
||||
public class DataDrivenSettingsPanelEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
var panel = (DataDrivenSettingsPanel)target;
|
||||
EditorGUILayout.Space(8);
|
||||
EditorGUILayout.HelpBox(
|
||||
"设置行在运行时据 SettingsSchemaSO 自动生成(编辑器下行容器为空属正常)。\n" +
|
||||
"点「预览」在编辑器内据表生成行以查看 / 调整样式与布局;样式改 UI_Setting_*Row 预制件 / UI_Theme_Default。\n" +
|
||||
"预览行仅用于编辑器查看(不绑定 ISettingsService),进入 Play 会自动清空并据表重建。",
|
||||
MessageType.Info);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("▶ 预览设置行(编辑器内)", GUILayout.Height(26)))
|
||||
PreviewRows(panel);
|
||||
if (GUILayout.Button("✕ 清除预览", GUILayout.Height(26), GUILayout.Width(120)))
|
||||
{
|
||||
panel.Clear();
|
||||
MarkDirty(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PreviewRows(DataDrivenSettingsPanel panel)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorUtility.DisplayDialog("预览设置行", "请退出 Play 模式后再使用编辑器预览。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
panel.Build(); // 内部 Clear + 据表实例化行(编辑器无 ISettingsService,仅生成行 + 标签)
|
||||
|
||||
// 标签:运行时 SetKey 在无服务时显示 key,这里改用编辑器预览显示本地化译文
|
||||
foreach (var loc in panel.GetComponentsInChildren<LocalizedText>(true))
|
||||
loc.UpdateEditorPreview();
|
||||
|
||||
MarkDirty(panel);
|
||||
}
|
||||
|
||||
private static void MarkDirty(DataDrivenSettingsPanel panel)
|
||||
{
|
||||
EditorUtility.SetDirty(panel);
|
||||
if (!Application.isPlaying)
|
||||
EditorSceneManager.MarkSceneDirty(panel.gameObject.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9afba7ce47e6eeb41a8d08751bd4ff13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -350,7 +350,6 @@ namespace BaseGames.Editor.UI
|
||||
var cancelBtn = MakeButton(boxGo.transform, "CancelButton", "取消", new Vector2( 110, -90), out TMP_Text cancelLabel);
|
||||
|
||||
var dialog = GetOrAddComponent<ConfirmDialogController>(dialogGo);
|
||||
AssignRef(dialog, "_root", dialogGo);
|
||||
AssignRef(dialog, "_titleText", title);
|
||||
AssignRef(dialog, "_bodyText", body);
|
||||
AssignRef(dialog, "_confirmLabel", confirmLabel);
|
||||
|
||||
@@ -359,6 +359,7 @@ namespace BaseGames.Editor.UI
|
||||
private static GameObject BuildSettingHeader(List<string> report)
|
||||
{
|
||||
var go = NewUI(PfSettingHeader, 480, 32);
|
||||
var hdrLe = go.AddComponent<LayoutElement>(); hdrLe.preferredHeight = 36f; hdrLe.minHeight = 32f;
|
||||
var lblGo = NewUIChild(go.transform, "Label", out var rt);
|
||||
Stretch(rt);
|
||||
var lbl = lblGo.AddComponent<TextMeshProUGUI>();
|
||||
@@ -414,6 +415,7 @@ namespace BaseGames.Editor.UI
|
||||
private static GameObject SettingRowRoot(string name, out TMP_Text label)
|
||||
{
|
||||
var go = NewUI(name, 480, 44);
|
||||
var rowLe = go.AddComponent<LayoutElement>(); rowLe.preferredHeight = 44f; rowLe.minHeight = 40f;
|
||||
var h = go.AddComponent<HorizontalLayoutGroup>();
|
||||
h.spacing = 12; h.childAlignment = TextAnchor.MiddleLeft;
|
||||
h.childForceExpandWidth = false; h.childForceExpandHeight = false;
|
||||
@@ -539,6 +541,10 @@ namespace BaseGames.Editor.UI
|
||||
if (s_theme != null) AssignRef(uiBtn, "_theme", s_theme);
|
||||
var view = go.AddComponent<MainMenuButtonView>();
|
||||
|
||||
// LayoutElement:放入竖排布局(VerticalLayoutGroup childControlHeight)时保持固定行高,避免被压扁
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
le.preferredHeight = 64f; le.minHeight = 56f;
|
||||
|
||||
var iconGo = NewUIChild(go.transform, "Icon", out var irt);
|
||||
irt.anchorMin = new Vector2(0, 0.5f); irt.anchorMax = new Vector2(0, 0.5f); irt.pivot = new Vector2(0, 0.5f);
|
||||
irt.anchoredPosition = new Vector2(14, 0); irt.sizeDelta = new Vector2(32, 32);
|
||||
|
||||
157
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs
Normal file
157
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs
Normal 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/Jump),Label 填提示文字(可挂 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」放进 UI,Icon 填 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UIInputPromptScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67910478514f5d342b774044378cd877
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
275
Assets/_Game/Scripts/Editor/UI/UILoadingScreenScaffold.cs
Normal file
275
Assets/_Game/Scripts/Editor/UI/UILoadingScreenScaffold.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载界面脚手架:一键生成 / 更新 <c>UI_LoadingScreen</c> 预制件 + 默认 <c>UI_LoadingConfig</c> 数据表,
|
||||
/// 并补充默认中/英文案。对齐项目「Prefab(样式/美术)+ Config(数据)+ Theme(配色)」范式。
|
||||
///
|
||||
/// <para>生成后策划美术编辑方式:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>美术:双击 <c>UI_LoadingScreen.prefab</c> 进 Prefab Mode,改背景图 / 进度条 sprite / 字体 / 加装饰 / 调布局。</item>
|
||||
/// <item>策划:选 <c>UI_LoadingConfig.asset</c>,改提示文案 key、标题 key、加载时长/手感。</item>
|
||||
/// <item>配色:改 <c>UI_Theme_Default</c>(带 UIThemeRole 的元素自动套用)。</item>
|
||||
/// </list>
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成加载界面(预制件 + 默认配置)
|
||||
/// </summary>
|
||||
public static class UILoadingScreenScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PrefabName = "UI_LoadingScreen";
|
||||
private const string ConfigName = "UI_LoadingConfig";
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成加载界面(预制件 + 默认配置)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir);
|
||||
EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var config = EnsureDefaultConfig(report);
|
||||
SeedLocalization(report);
|
||||
BuildPrefab(theme, config, report);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var sb = new System.Text.StringBuilder("[UILoadingScreen] 加载界面已生成/更新:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold Persistent Scene」把预制件实例化进 Persistent。");
|
||||
sb.AppendLine("美术改样式 → 编辑 UI_LoadingScreen 预制件;策划改内容 → 编辑 UI_LoadingConfig;配色 → UI_Theme_Default。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
// ── 预制件构建 ───────────────────────────────────────────────────────
|
||||
private static void BuildPrefab(UIThemeSO theme, LoadingScreenConfigSO config, List<string> report)
|
||||
{
|
||||
// 根:全屏 Overlay Canvas(常驻 active;LoadingScreenManager 挂此处,靠保持 active 才能订阅事件)
|
||||
var root = new GameObject(PrefabName,
|
||||
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
var canvas = root.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 99;
|
||||
var scaler = root.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
var mgr = root.AddComponent<LoadingScreenManager>();
|
||||
|
||||
// LoadingRoot:铺满全屏,初始 inactive(由管理器 Show/Hide 切换)。
|
||||
// 主题应用器挂在此处:每次 Show(SetActive true)时 OnEnable 应用主题配色。
|
||||
var loadingRootGo = NewUIChild(root.transform, "LoadingRoot", out var loadingRootRt);
|
||||
Stretch(loadingRootRt);
|
||||
var applier = loadingRootGo.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
// Background(铺满):美术可换 sprite,或复制多个变体作随机背景池
|
||||
var bgGo = NewUIChild(loadingRootGo.transform, "Background", out var bgRt);
|
||||
Stretch(bgRt);
|
||||
var bgImg = bgGo.AddComponent<Image>();
|
||||
bgImg.color = new Color(0.04f, 0.05f, 0.07f, 1f);
|
||||
SetEnum(bgGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
|
||||
|
||||
// ProgressBarTrack(底部居中)+ ProgressBarFill
|
||||
var trackGo = NewUIChild(loadingRootGo.transform, "ProgressBarTrack", out var trackRt);
|
||||
SetRect(trackRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 90f), new Vector2(760f, 14f));
|
||||
var trackImg = trackGo.AddComponent<Image>();
|
||||
trackImg.sprite = Standard(); trackImg.type = Image.Type.Sliced;
|
||||
trackImg.color = new Color(1f, 1f, 1f, 0.12f);
|
||||
trackImg.raycastTarget = false;
|
||||
|
||||
var fillGo = NewUIChild(trackGo.transform, "ProgressBarFill", out var fillRt);
|
||||
Stretch(fillRt);
|
||||
var fillImg = fillGo.AddComponent<Image>();
|
||||
fillImg.sprite = Standard();
|
||||
fillImg.type = Image.Type.Filled;
|
||||
fillImg.fillMethod = Image.FillMethod.Horizontal;
|
||||
fillImg.fillOrigin = (int)Image.OriginHorizontal.Left;
|
||||
fillImg.fillAmount = 0f;
|
||||
fillImg.color = new Color(0.85f, 0.80f, 0.55f, 1f);
|
||||
fillImg.raycastTarget = false;
|
||||
SetEnum(fillGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Accent);
|
||||
|
||||
// Title(居中偏上)
|
||||
var titleGo = NewUIChild(loadingRootGo.transform, "Title", out var titleRt);
|
||||
SetRect(titleRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0f, 60f), new Vector2(1200f, 80f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.text = "Loading"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 56f;
|
||||
title.raycastTarget = false;
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// TipText(进度条上方)
|
||||
var tipGo = NewUIChild(loadingRootGo.transform, "TipText", out var tipRt);
|
||||
SetRect(tipRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 130f), new Vector2(1200f, 48f));
|
||||
var tip = tipGo.AddComponent<TextMeshProUGUI>();
|
||||
tip.text = ""; tip.alignment = TextAlignmentOptions.Center; tip.fontSize = 28f;
|
||||
tip.raycastTarget = false;
|
||||
SetEnum(tipGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
|
||||
|
||||
// 绑定管理器视图引用 + 数据/事件(让预制件自足,掉哪都能用)
|
||||
AssignRef(mgr, "_loadingRoot", loadingRootGo);
|
||||
AssignRef(mgr, "_progressFill", fillImg);
|
||||
AssignRef(mgr, "_titleText", title);
|
||||
AssignRef(mgr, "_tipText", tip);
|
||||
AssignArray(mgr, "_backgroundArts", new Object[] { bgImg });
|
||||
AssignRef(mgr, "_config", config);
|
||||
AssignRef(mgr, "_onLoadingStarted", FindAsset<VoidEventChannelSO>("EVT_LoadingStarted"));
|
||||
AssignRef(mgr, "_onLoadingComplete", FindAsset<VoidEventChannelSO>("EVT_LoadingComplete"));
|
||||
AssignRef(mgr, "_onLoadingProgressUpdated", FindAsset<FloatEventChannelSO>("EVT_LoadingProgressUpdated"));
|
||||
|
||||
// 关键:Canvas 根保持 active,仅 LoadingRoot 初始隐藏(否则管理器被自己关闭、永不订阅事件)
|
||||
loadingRootGo.SetActive(false);
|
||||
|
||||
string path = $"{PrefabDir}/{PrefabName}.prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
}
|
||||
|
||||
// ── 默认配置资产 ─────────────────────────────────────────────────────
|
||||
private static LoadingScreenConfigSO EnsureDefaultConfig(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{ConfigName}.asset";
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<LoadingScreenConfigSO>(path);
|
||||
bool created = cfg == null;
|
||||
if (created)
|
||||
{
|
||||
cfg = ScriptableObject.CreateInstance<LoadingScreenConfigSO>();
|
||||
AssetDatabase.CreateAsset(cfg, path);
|
||||
}
|
||||
if (created) // 仅新建时填默认,避免覆盖策划已有编辑
|
||||
{
|
||||
var so = new SerializedObject(cfg);
|
||||
so.FindProperty("_titleKey").stringValue = "LOADING_TITLE";
|
||||
var tips = so.FindProperty("_tipKeys");
|
||||
string[] keys = { "LOADING_TIP_EXPLORE", "LOADING_TIP_SAVE", "LOADING_TIP_COMBAT" };
|
||||
tips.arraySize = keys.Length;
|
||||
for (int i = 0; i < keys.Length; i++) tips.GetArrayElementAtIndex(i).stringValue = keys[i];
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认标题/提示 key)");
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// ── 默认本地化文案(中/英;已存在的 key 不覆盖)─────────────────────────
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string>
|
||||
{
|
||||
{ "LOADING_TITLE", "加载中…" },
|
||||
{ "LOADING_TIP_EXPLORE", "多探索每个角落,常有隐藏的道路与收集物。" },
|
||||
{ "LOADING_TIP_SAVE", "在存档点休息可保存进度并恢复状态。" },
|
||||
{ "LOADING_TIP_COMBAT", "把握闪避与格挡的时机,是战斗的关键。" },
|
||||
};
|
||||
var en = new Dictionary<string, string>
|
||||
{
|
||||
{ "LOADING_TITLE", "Loading…" },
|
||||
{ "LOADING_TIP_EXPLORE", "Explore every corner — hidden paths and collectibles await." },
|
||||
{ "LOADING_TIP_SAVE", "Rest at save points to save progress and restore yourself." },
|
||||
{ "LOADING_TIP_COMBAT", "Timing your dodge and parry is the key to combat." },
|
||||
};
|
||||
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
|
||||
report.Add($"本地化新增 {added} 条(已存在的不覆盖;可用「BaseGames/Localization/表格编辑器」补译)。");
|
||||
}
|
||||
|
||||
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
|
||||
{
|
||||
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
|
||||
int added = 0;
|
||||
foreach (var p in kv)
|
||||
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
|
||||
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
|
||||
return added;
|
||||
}
|
||||
|
||||
// ── 通用助手 ─────────────────────────────────────────────────────────
|
||||
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 Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot,
|
||||
Vector2 anchoredPos, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
|
||||
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
|
||||
}
|
||||
|
||||
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 AssignRef(Object target, string prop, Object value)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null) { Debug.LogWarning($"[UILoadingScreen] 未找到属性 {target.GetType().Name}.{prop}"); return; }
|
||||
p.objectReferenceValue = value;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void AssignArray(Object target, string prop, Object[] values)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null || !p.isArray) return;
|
||||
p.arraySize = values.Length;
|
||||
for (int i = 0; i < values.Length; i++) p.GetArrayElementAtIndex(i).objectReferenceValue = values[i];
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static T FindAsset<T>(string name) where T : Object
|
||||
{
|
||||
foreach (var guid in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}"))
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (asset != null && asset.name == name) return asset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Sprite Standard() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2513f08dc745f2a45b2209eb81f2ca05
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
265
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs
Normal file
265
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI.MainMenu;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 新游戏难度选择脚手架:生成 / 更新 <c>UI_NewGameModePanel</c> 预制件 + 默认 <c>UI_NewGameModeConfig</c> 表 + 中英文案。
|
||||
/// 该面板是 MainMenu Canvas 的子面板(非独立 Canvas),由 SaveSlotController 经 ShowAsync 模态弹出。
|
||||
///
|
||||
/// 美术 → 改 UI_NewGameModePanel 预制件(暗化/对话框/标题/说明/返回样式,难度按钮样式见 UI_MainMenu_Button);
|
||||
/// 策划 → 改 UI_NewGameModeConfig(增删/重排难度、改标签/说明)。
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成新游戏难度选择(预制件 + 默认配置)
|
||||
/// </summary>
|
||||
public static class UINewGameModeScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PrefabName = "UI_NewGameModePanel";
|
||||
private const string ConfigName = "UI_NewGameModeConfig";
|
||||
private const string BtnPrefabPath = "Assets/_Game/Prefabs/UI/Controls/UI_MainMenu_Button.prefab";
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成新游戏难度选择(预制件 + 默认配置)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir);
|
||||
EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var config = EnsureDefaultConfig(report);
|
||||
SeedLocalization(report);
|
||||
BuildPrefab(theme, config, report);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var sb = new System.Text.StringBuilder("[UINewGameMode] 难度选择已生成/更新:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold MainMenu Scene」实例化进 MainMenu(接 SaveSlotController._modeSelect)。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
private static void BuildPrefab(UIThemeSO theme, NewGameModeConfigSO config, List<string> report)
|
||||
{
|
||||
// 根:铺满全屏的面板(非 Canvas;作为 MainMenu Canvas 子节点)。初始隐藏,由导航器激活。
|
||||
var root = new GameObject(PrefabName, typeof(RectTransform), typeof(CanvasGroup));
|
||||
var rootRt = (RectTransform)root.transform;
|
||||
Stretch(rootRt);
|
||||
var ctrl = root.AddComponent<NewGameModeController>();
|
||||
var applier = root.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
// 暗化遮罩(铺满,挡点击)
|
||||
var overlayGo = NewUIChild(root.transform, "Overlay", out var overlayRt);
|
||||
Stretch(overlayRt);
|
||||
var overlay = overlayGo.AddComponent<Image>();
|
||||
overlay.color = new Color(0f, 0f, 0f, 0.6f); overlay.raycastTarget = true;
|
||||
|
||||
// 对话框
|
||||
var boxGo = NewUIChild(root.transform, "DialogBox", out var boxRt);
|
||||
SetRect(boxRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
Vector2.zero, new Vector2(760f, 520f));
|
||||
var box = boxGo.AddComponent<Image>();
|
||||
box.sprite = Standard(); box.type = Image.Type.Sliced;
|
||||
box.color = new Color(0.10f, 0.11f, 0.15f, 0.98f);
|
||||
SetEnum(boxGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Graphic_Background);
|
||||
|
||||
// 标题
|
||||
var titleGo = NewUIChild(boxGo.transform, "Title", out var titleRt);
|
||||
SetRect(titleRt, new Vector2(0f, 1f), new Vector2(1f, 1f), new Vector2(0.5f, 1f),
|
||||
new Vector2(0f, -50f), new Vector2(-60f, 60f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.text = "选择难度"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 40f;
|
||||
title.raycastTarget = false;
|
||||
var titleLoc = titleGo.AddComponent<LocalizedText>();
|
||||
SetString(titleLoc, "_key", "MODE_SELECT_TITLE");
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// 难度按钮容器(竖排)
|
||||
var optionGo = NewUIChild(boxGo.transform, "OptionContainer", out var optionRt);
|
||||
SetRect(optionRt, new Vector2(0.5f, 1f), new Vector2(0.5f, 1f), new Vector2(0.5f, 1f),
|
||||
new Vector2(0f, -130f), new Vector2(460f, 200f));
|
||||
var vlg = optionGo.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.spacing = 12f; vlg.childAlignment = TextAnchor.UpperCenter;
|
||||
vlg.childControlWidth = true; vlg.childControlHeight = true;
|
||||
vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
|
||||
|
||||
// 说明文本(选中项时显示)
|
||||
var descGo = NewUIChild(boxGo.transform, "DescText", out var descRt);
|
||||
SetRect(descRt, new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 130f), new Vector2(-80f, 80f));
|
||||
var desc = descGo.AddComponent<TextMeshProUGUI>();
|
||||
desc.text = ""; desc.alignment = TextAlignmentOptions.Center; desc.fontSize = 22f;
|
||||
desc.color = new Color(0.82f, 0.6f, 0.6f, 1f); desc.raycastTarget = false; desc.enableWordWrapping = true;
|
||||
SetEnum(descGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Secondary);
|
||||
|
||||
// 返回按钮
|
||||
var backGo = NewUIChild(boxGo.transform, "Btn_Back", out var backRt);
|
||||
SetRect(backRt, new Vector2(0.5f, 0f), new Vector2(0.5f, 0f), new Vector2(0.5f, 0f),
|
||||
new Vector2(0f, 46f), new Vector2(260f, 60f));
|
||||
var backImg = backGo.AddComponent<Image>();
|
||||
backImg.sprite = Standard(); backImg.type = Image.Type.Sliced; backImg.color = new Color(1f, 1f, 1f, 0.06f);
|
||||
var backBtn = backGo.AddComponent<Button>(); backBtn.targetGraphic = backImg;
|
||||
var backLblGo = NewUIChild(backGo.transform, "Label", out var backLblRt);
|
||||
Stretch(backLblRt);
|
||||
var backLbl = backLblGo.AddComponent<TextMeshProUGUI>();
|
||||
backLbl.text = "返回"; backLbl.alignment = TextAlignmentOptions.Center; backLbl.fontSize = 28f;
|
||||
backLblGo.AddComponent<LocalizedText>();
|
||||
SetString(backLblGo.GetComponent<LocalizedText>(), "_key", "BTN_BACK");
|
||||
SetEnum(backLblGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Primary);
|
||||
|
||||
// 绑定控制器
|
||||
AssignRef(ctrl, "_config", config);
|
||||
AssignRef(ctrl, "_container", optionRt);
|
||||
AssignRef(ctrl, "_canvasGroup", root.GetComponent<CanvasGroup>());
|
||||
AssignRef(ctrl, "_titleText", titleLoc);
|
||||
AssignRef(ctrl, "_descText", desc);
|
||||
AssignRef(ctrl, "_btnBack", backBtn);
|
||||
|
||||
var btnPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BtnPrefabPath);
|
||||
var btnView = btnPrefab != null ? btnPrefab.GetComponent<MainMenuButtonView>() : null;
|
||||
if (btnView == null) report.Add($"未找到按钮预制件 {BtnPrefabPath}(请先运行「生成主菜单」)。");
|
||||
AssignRef(ctrl, "_buttonPrefab", btnView);
|
||||
|
||||
root.SetActive(false); // 结果面板由导航器激活
|
||||
|
||||
string path = $"{PrefabDir}/{PrefabName}.prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
}
|
||||
|
||||
private static NewGameModeConfigSO EnsureDefaultConfig(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{ConfigName}.asset";
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<NewGameModeConfigSO>(path);
|
||||
bool created = cfg == null;
|
||||
if (created) { cfg = ScriptableObject.CreateInstance<NewGameModeConfigSO>(); AssetDatabase.CreateAsset(cfg, path); }
|
||||
|
||||
if (created)
|
||||
{
|
||||
var items = new (DifficultyLevel lvl, string label, string desc)[]
|
||||
{
|
||||
(DifficultyLevel.Normal, "MODE_NORMAL", ""),
|
||||
(DifficultyLevel.SteelSoul, "MODE_STEELSOUL", "MODE_STEELSOUL_DESC"),
|
||||
};
|
||||
var so = new SerializedObject(cfg);
|
||||
var prop = so.FindProperty("_items");
|
||||
prop.arraySize = items.Length;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var el = prop.GetArrayElementAtIndex(i);
|
||||
el.FindPropertyRelative("level").enumValueIndex = (int)items[i].lvl;
|
||||
el.FindPropertyRelative("labelKey").stringValue = items[i].label;
|
||||
el.FindPropertyRelative("descKey").stringValue = items[i].desc;
|
||||
}
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认 {items.Length} 项:普通 / 钢铁之魂)");
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string>
|
||||
{
|
||||
{ "MODE_SELECT_TITLE", "选择难度" },
|
||||
{ "MODE_NORMAL", "普通" },
|
||||
{ "MODE_STEELSOUL", "钢铁之魂" },
|
||||
{ "MODE_STEELSOUL_DESC", "一命模式:死亡即清空存档,请谨慎选择。" },
|
||||
{ "BTN_BACK", "返回" },
|
||||
};
|
||||
var en = new Dictionary<string, string>
|
||||
{
|
||||
{ "MODE_SELECT_TITLE", "Select Mode" },
|
||||
{ "MODE_NORMAL", "Normal" },
|
||||
{ "MODE_STEELSOUL", "Steel Soul" },
|
||||
{ "MODE_STEELSOUL_DESC", "One life. Death wipes the save — choose with care." },
|
||||
{ "BTN_BACK", "Back" },
|
||||
};
|
||||
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
|
||||
report.Add($"本地化新增 {added} 条(已存在不覆盖)。");
|
||||
}
|
||||
|
||||
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
|
||||
{
|
||||
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
|
||||
int added = 0;
|
||||
foreach (var p in kv)
|
||||
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
|
||||
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
|
||||
return added;
|
||||
}
|
||||
|
||||
// ── 助手 ─────────────────────────────────────────────────────────────
|
||||
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 Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot,
|
||||
Vector2 anchoredPos, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
|
||||
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (c == null) return;
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void AssignRef(Object target, string prop, Object value)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null) { Debug.LogWarning($"[UINewGameMode] 未找到属性 {target.GetType().Name}.{prop}"); return; }
|
||||
p.objectReferenceValue = value;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static Sprite Standard() => AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UINewGameModeScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df82a9fb6a00ca740badcfaefc083560
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
255
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs
Normal file
255
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using BaseGames.UI;
|
||||
using BaseGames.UI.MainMenu;
|
||||
using BaseGames.UI.Theme;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Localization;
|
||||
using BaseGames.Editor.Localization;
|
||||
|
||||
namespace BaseGames.Editor.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 暂停界面脚手架:生成 / 更新 <c>UI_PauseScreen</c> 预制件 + 默认 <c>UI_PauseMenuConfig</c> 表 + 中英文案。
|
||||
/// 对齐「Prefab(样式)+ Config(数据)+ Theme(配色)」范式。
|
||||
///
|
||||
/// <para>编辑方式:</para>
|
||||
/// 美术 → 双击 <c>UI_PauseScreen.prefab</c> 改暗化背景/标题/布局/按钮样式(按钮样式见 UI_MainMenu_Button);
|
||||
/// 策划 → 选 <c>UI_PauseMenuConfig.asset</c> 增删/重排/改标签/改动作。
|
||||
///
|
||||
/// 菜单:BaseGames/UI/控件库/生成暂停界面(预制件 + 默认配置)
|
||||
/// </summary>
|
||||
public static class UIPauseScreenScaffold
|
||||
{
|
||||
private const string PrefabDir = "Assets/_Game/Prefabs/UI";
|
||||
private const string ControlsDir = "Assets/_Game/Prefabs/UI/Controls";
|
||||
private const string ConfigDir = "Assets/_Game/Data/UI";
|
||||
private const string ThemePath = "Assets/_Game/Data/UI/Themes/UI_Theme_Default.asset";
|
||||
private const string PrefabName = "UI_PauseScreen";
|
||||
private const string ConfigName = "UI_PauseMenuConfig";
|
||||
private const string BtnPrefabPath = "Assets/_Game/Prefabs/UI/Controls/UI_MainMenu_Button.prefab";
|
||||
|
||||
[MenuItem("BaseGames/UI/控件库/生成暂停界面(预制件 + 默认配置)")]
|
||||
public static void GeneratePrefab()
|
||||
{
|
||||
EnsureFolder(PrefabDir);
|
||||
EnsureFolder(ConfigDir);
|
||||
var report = new List<string>();
|
||||
|
||||
var theme = AssetDatabase.LoadAssetAtPath<UIThemeSO>(ThemePath);
|
||||
var config = EnsureDefaultConfig(report);
|
||||
SeedLocalization(report);
|
||||
BuildPrefab(theme, config, report);
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var sb = new System.Text.StringBuilder("[UIPauseScreen] 暂停界面已生成/更新:\n");
|
||||
foreach (var r in report) sb.AppendLine(" • " + r);
|
||||
sb.AppendLine("用法:重跑「BaseGames/Scene/Setup/Scaffold Persistent Scene」实例化进 Persistent(作为 PanelId.Pause 面板)。");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
|
||||
// ── 预制件构建 ───────────────────────────────────────────────────────
|
||||
private static void BuildPrefab(UIThemeSO theme, PauseMenuConfigSO config, List<string> report)
|
||||
{
|
||||
// 根:全屏 Overlay Canvas(sortOrder 50:HUD 之上、加载/Splash 之下)
|
||||
var root = new GameObject(PrefabName,
|
||||
typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster), typeof(CanvasGroup));
|
||||
var canvas = root.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 50;
|
||||
var scaler = root.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1920f, 1080f);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
var ctrl = root.AddComponent<DataDrivenPauseMenuController>();
|
||||
|
||||
// 暗化背景(铺满,挡住下层点击)
|
||||
var dimGo = NewUIChild(root.transform, "Dim", out var dimRt);
|
||||
Stretch(dimRt);
|
||||
var dimImg = dimGo.AddComponent<Image>();
|
||||
dimImg.color = new Color(0f, 0f, 0f, 0.72f);
|
||||
dimImg.raycastTarget = true;
|
||||
|
||||
// 主题应用器挂根(每次 OnEnable 应用配色到带 UIThemeRole 的子节点)
|
||||
var applier = root.AddComponent<UIThemeApplier>();
|
||||
if (theme != null) AssignRef(applier, "_theme", theme);
|
||||
|
||||
// 标题
|
||||
var titleGo = NewUIChild(root.transform, "Title", out var titleRt);
|
||||
SetRect(titleRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0f, 220f), new Vector2(800f, 90f));
|
||||
var title = titleGo.AddComponent<TextMeshProUGUI>();
|
||||
title.text = "暂停"; title.alignment = TextAlignmentOptions.Center; title.fontSize = 64f;
|
||||
title.raycastTarget = false;
|
||||
titleGo.AddComponent<LocalizedText>(); // key 由脚手架下面绑 PAUSE_TITLE
|
||||
SetString(titleGo.GetComponent<LocalizedText>(), "_key", "PAUSE_TITLE");
|
||||
SetEnum(titleGo.AddComponent<UIThemeRole>(), "_kind", (int)UIThemeRoleKind.Text_Header);
|
||||
|
||||
// 按钮容器(居中竖排;运行时据表生成按钮)
|
||||
var menuGo = NewUIChild(root.transform, "MenuPanel", out var menuRt);
|
||||
SetRect(menuRt, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f),
|
||||
new Vector2(0f, -40f), new Vector2(520f, 360f));
|
||||
var vlg = menuGo.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.spacing = 12f; vlg.childAlignment = TextAnchor.MiddleCenter;
|
||||
vlg.childControlWidth = true; vlg.childControlHeight = true;
|
||||
vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
|
||||
|
||||
// 绑定控制器
|
||||
AssignRef(ctrl, "_config", config);
|
||||
AssignRef(ctrl, "_container", menuRt);
|
||||
AssignRef(ctrl, "_canvasGroup", root.GetComponent<CanvasGroup>());
|
||||
|
||||
var btnPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(BtnPrefabPath);
|
||||
var btnView = btnPrefab != null ? btnPrefab.GetComponent<MainMenuButtonView>() : null;
|
||||
if (btnView == null) report.Add($"未找到按钮预制件 {BtnPrefabPath}(请先运行「生成主菜单」)。");
|
||||
AssignRef(ctrl, "_buttonPrefab", btnView);
|
||||
|
||||
// resume 频道与 GameManager 同源:实际资产名 EVT_PauseResumed(EVT_ResumeRequested 不存在)
|
||||
AssignRef(ctrl, "_onResumeRequested", FindAsset<VoidEventChannelSO>("EVT_PauseResumed")
|
||||
?? FindAsset<VoidEventChannelSO>("EVT_ResumeRequested"));
|
||||
AssignRef(ctrl, "_onSceneLoadRequest", FindAsset<SceneLoadRequestEventChannelSO>("EVT_SceneLoadRequest"));
|
||||
|
||||
string path = $"{PrefabDir}/{PrefabName}.prefab";
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
Object.DestroyImmediate(root);
|
||||
report.Add(path);
|
||||
}
|
||||
|
||||
// ── 默认配置 ─────────────────────────────────────────────────────────
|
||||
private static PauseMenuConfigSO EnsureDefaultConfig(List<string> report)
|
||||
{
|
||||
string path = $"{ConfigDir}/{ConfigName}.asset";
|
||||
var cfg = AssetDatabase.LoadAssetAtPath<PauseMenuConfigSO>(path);
|
||||
bool created = cfg == null;
|
||||
if (created) { cfg = ScriptableObject.CreateInstance<PauseMenuConfigSO>(); AssetDatabase.CreateAsset(cfg, path); }
|
||||
|
||||
if (created)
|
||||
{
|
||||
var items = new (string key, PauseMenuAction a)[]
|
||||
{
|
||||
("PAUSE_RESUME", PauseMenuAction.Resume),
|
||||
("MENU_SETTINGS", PauseMenuAction.OpenSettings),
|
||||
("PAUSE_MAIN_MENU", PauseMenuAction.ReturnToMainMenu),
|
||||
("MENU_QUIT", PauseMenuAction.Quit),
|
||||
};
|
||||
var so = new SerializedObject(cfg);
|
||||
var prop = so.FindProperty("_items");
|
||||
prop.arraySize = items.Length;
|
||||
for (int i = 0; i < items.Length; i++)
|
||||
{
|
||||
var el = prop.GetArrayElementAtIndex(i);
|
||||
el.FindPropertyRelative("labelKey").stringValue = items[i].key;
|
||||
el.FindPropertyRelative("action").enumValueIndex = (int)items[i].a;
|
||||
el.FindPropertyRelative("sceneKey").stringValue = "";
|
||||
}
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report.Add($"{path}(默认 {items.Length} 项)");
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// ── 默认本地化(已存在的 key 不覆盖)────────────────────────────────────
|
||||
private static void SeedLocalization(List<string> report)
|
||||
{
|
||||
var zh = new Dictionary<string, string>
|
||||
{
|
||||
{ "PAUSE_TITLE", "暂停" },
|
||||
{ "PAUSE_RESUME", "继续" },
|
||||
{ "PAUSE_MAIN_MENU", "返回主菜单" },
|
||||
};
|
||||
var en = new Dictionary<string, string>
|
||||
{
|
||||
{ "PAUSE_TITLE", "Paused" },
|
||||
{ "PAUSE_RESUME", "Resume" },
|
||||
{ "PAUSE_MAIN_MENU", "Return to Title" },
|
||||
};
|
||||
int added = MergeWriteUI(Language.ChineseSimplified, zh) + MergeWriteUI(Language.English, en);
|
||||
report.Add($"本地化新增 {added} 条(已存在不覆盖;MENU_SETTINGS/MENU_QUIT 复用主菜单译文)。");
|
||||
}
|
||||
|
||||
private static int MergeWriteUI(Language lang, Dictionary<string, string> kv)
|
||||
{
|
||||
var dict = LocalizationFileIO.Read(lang, LocalizationTable.UI);
|
||||
int added = 0;
|
||||
foreach (var p in kv)
|
||||
if (!dict.ContainsKey(p.Key)) { dict[p.Key] = p.Value; added++; }
|
||||
if (added > 0) LocalizationFileIO.Write(lang, LocalizationTable.UI, dict);
|
||||
return added;
|
||||
}
|
||||
|
||||
// ── 助手 ─────────────────────────────────────────────────────────────
|
||||
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 Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
private static void SetRect(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 pivot,
|
||||
Vector2 anchoredPos, Vector2 size)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax; rt.pivot = pivot;
|
||||
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (c == null) return;
|
||||
var so = new SerializedObject(c);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p != null) { p.stringValue = value; so.ApplyModifiedPropertiesWithoutUndo(); }
|
||||
}
|
||||
|
||||
private static void AssignRef(Object target, string prop, Object value)
|
||||
{
|
||||
var so = new SerializedObject(target);
|
||||
var p = so.FindProperty(prop);
|
||||
if (p == null) { Debug.LogWarning($"[UIPauseScreen] 未找到属性 {target.GetType().Name}.{prop}"); return; }
|
||||
p.objectReferenceValue = value;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static T FindAsset<T>(string name) where T : Object
|
||||
{
|
||||
foreach (var guid in AssetDatabase.FindAssets($"{name} t:{typeof(T).Name}"))
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if (asset != null && asset.name == name) return asset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/UI/UIPauseScreenScaffold.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d74eff677fd2a04ab74c05b8cced069
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user