276 lines
14 KiB
C#
276 lines
14 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|
||
}
|