Files
zeling_v2/Assets/_Game/Scripts/Editor/UI/UILoadingScreenScaffold.cs
2026-06-07 11:49:55 +08:00

276 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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常驻 activeLoadingScreenManager 挂此处,靠保持 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 切换)。
// 主题应用器挂在此处:每次 ShowSetActive 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;
}
}
}
}