Files
5.24BuYu/Assets/MyGame/Editor/FlameStrikeVariantBuilder.cs
T
2026-10-20 18:30:03 +08:00

695 lines
32 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.
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Coffee.UIExtensions;
using TMPro;
using UnityEditor;
using UnityEngine;
/// <summary>
/// 烈焰风暴「结算面板(Settle)」/「奖励框(AwardFrame)」的 UI 化变体生成器。
///
/// 背景:XJMH 的爆奖面板都是「Art 源预制体 → MyGame 变体」两层。Art 源是给 3D 场景用的,
/// 直接当 UI 用会有三个问题:粒子不在 Canvas 里渲染、卡面数字没接、没有 BJEndUI 写分数。
/// 同门 Boss(铁拳 / 伽马风暴 / 破坏球 / 幸运轮 …)都在
/// <c>Assets/MyGame/aa/Game/XJMH/Boss/BJ/BJEnd</c> 下留了做好的变体,本脚本就是照抄它们的做法。
///
/// 从已有变体(PW_Effect_Settle_IronFist_Self_XingJiXunHang)反解出来的加工步骤:
/// 1. 所有 ParticleSystemRenderer:m_Enabled = 0(改由 UIParticle 渲染)
/// 2. 所有 ParticleSystemRenderer:m_SortingLayer = 0(对齐 Default 层)
/// 3. 根下新增 <c>UIParticle</c> 子节点:UIParticle 组件 + m_Particles 收全所有粒子
/// + m_Scale3D = (10,10,10)
/// ⚠ 这个节点自己的 localScale 在运行时会被 UIParticle 覆盖掉
/// (AutoScalingMode.Transform → UpdateTransformScale() 把它设成 1/parentLossyScale),
/// 所以模板里写 0 也只是"随便写";真正决定大小的是「各粒子节点 localScale × m_Scale3D」。
/// 3b. 各粒子节点 localScale 归一化:模板把源里的 30 / 46.176 / 48.395 统一改成 5
/// (配 m_Scale3D=10,等效放大 ≈1.67×,是同门统一口径)
/// 4. 根 RectTransform:anchor/pivot = 0.5,sizeDelta = 100×100,位置归零
/// 5. 分数文本(<c>Node/Textparent/Node</c>):字体换 SourceHanSans、spriteAsset 换金币图
/// 6. <c>Textparent</c> 下新增 <c>Node (1)</c> 文本(倍数用),scale 0.7 / 位置 (0,8) / 字号 36
/// 7. 根上挂 BJEndUI:tMP_score = 源里的分数文本,tMP_beishu = 新增的 Node (1)
///
/// 奖励框(AwardFrame)另走一条:不碰粒子,只在根上挂 BJImage(tmp_CoinNum = <c>Num</c> 文本)。
///
/// ⚠️ 本脚本不会去改 Art 源预制体(除非你点「就地补齐选中预制体」)。生成的是变体,
/// 这样以后美术更新 Art 源,MyGame 变体还能跟着走。
/// </summary>
public class FlameStrikeVariantBuilder : EditorWindow
{
// ---------------------------------------------------------------- 路径常量
private const string ArtEffectDir = "Assets/Art/XJMH/pw/pw_prefab/game/effect";
private const string BJEndDir = "Assets/MyGame/aa/Game/XJMH/Boss/BJ/BJEnd";
private const string BJScoreDir = "Assets/MyGame/aa/Game/XJMH/Boss/BJ/BJScore";
/// <summary>变体文件名后缀(与同门 _XingJiXunHang / _XieZi 一致)</summary>
private const string Suffix = "_LieYanFengBao";
// ---------------------------------------------------------------- 资源 guid(与同门变体完全一致)
private const string GoldSpriteGuid = "b5a3b539bfcb41e46ad594b9b14d7732";
private const string SilverSpriteGuid = "e034cf5e78470b44f8a1c4d42d5039d6";
private const string FontGuid = "fdd40eef2e75f6d46b51c289c46fe60b";
// ---------------------------------------------------------------- 模板参考值(从 BJEnd 已有变体抄的)
private const float TemplateParticleNodeScale = 5f; // 粒子节点 localScale(源里是 30 / 46.176 / 48.395)
private const float TemplateBeishuScale = 0.7f; // 倍数文本 localScale
private static readonly Vector2 TemplateBeishuPos = new Vector2(0f, 8f);
private static readonly Vector2 TemplateBeishuSize = new Vector2(200f, 50f);
private const float TemplateBeishuFontSize = 36f;
private static readonly Vector3 TemplateUiParticleScale3D = new Vector3(10f, 10f, 10f);
// ---------------------------------------------------------------- 选项
private bool _addUiParticle = true;
private bool _disableParticleRenderers = true;
private bool _resetSortingLayer = true;
private bool _scaleParticleNodes = true; // 模板口径:粒子节点归一化到 5
private bool _proportionalScale = true; // 按比例归一化(保留节点之间的相对大小)
private float _particleNodeScale = TemplateParticleNodeScale;
private bool _fixRootRect = true;
private bool _applyTextStyle = true;
private bool _addBjEndUI = true;
private bool _createBeishuNode = true;
private bool _addBjImage = true;
private bool _overwrite;
private Vector2 _scroll;
[MenuItem("Tools/烈焰风暴/结算面板 & 奖励框 变体生成器")]
private static void Open()
{
var w = GetWindow<FlameStrikeVariantBuilder>(false, "烈焰风暴变体", true);
w.minSize = new Vector2(460f, 520f);
w.Show();
}
private void OnGUI()
{
_scroll = EditorGUILayout.BeginScrollView(_scroll);
EditorGUILayout.LabelField("数据源(Art 源预制体)", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"源都在 Assets/Art/XJMH/pw/pw_prefab/game/effect 下:\n" +
" · PW_Effect_Settle_FlameStrike_Self → " + BJEndDir + "\n" +
" · PW_Effect_Settle_FlameStrike_Other → " + BJEndDir + "\n" +
" · PW_Effect_AwardFrame_FlameStrike → " + BJScoreDir + "\n" +
"生成的变体名会带后缀 " + Suffix, MessageType.None);
EditorGUILayout.Space();
EditorGUILayout.LabelField("加工步骤(对齐 BJEnd 里已有变体)", EditorStyles.boldLabel);
_disableParticleRenderers = EditorGUILayout.ToggleLeft(
"关闭所有 ParticleSystemRenderer(改由 UIParticle 渲染)", _disableParticleRenderers);
_resetSortingLayer = EditorGUILayout.ToggleLeft(
"所有粒子 Renderer 的 SortingLayer 归零(Default)", _resetSortingLayer);
_addUiParticle = EditorGUILayout.ToggleLeft(
"添加 / 复用 UIParticle 节点,并自动收全所有粒子(Scale3D = 10,10,10)", _addUiParticle);
_fixRootRect = EditorGUILayout.ToggleLeft(
"修正根 RectTransform(anchor/pivot 0.5、sizeDelta 100×100、位置归零)", _fixRootRect);
_applyTextStyle = EditorGUILayout.ToggleLeft(
"分数文本套用模板样式(SourceHanSans + 金币 spriteAsset)", _applyTextStyle);
using (new EditorGUI.IndentLevelScope())
{
_scaleParticleNodes = EditorGUILayout.ToggleLeft(
$"把各粒子节点的 localScale 归一化到 {TemplateParticleNodeScale}(模板口径)", _scaleParticleNodes);
using (new EditorGUI.DisabledScope(!_scaleParticleNodes))
{
_proportionalScale = EditorGUILayout.ToggleLeft(
"按比例归一化(保留节点之间的相对大小,推荐)", _proportionalScale);
_particleNodeScale = EditorGUILayout.FloatField(
_proportionalScale ? "主缩放归一化到" : "所有节点统一改成", _particleNodeScale);
}
EditorGUILayout.HelpBox(
"源里各粒子节点的 localScale 是 30(12 个)+ 1(2 个)。模板把 30 归一化成 5," +
"再配 m_Scale3D = 10 → 等效 50,与其它 Boss 面板同一口径。\n" +
"「按比例」= 30→5、1→0.1667(推荐,不会把原本小的节点放大 5 倍);\n" +
"关掉则所有节点都写 " + _particleNodeScale + "。\n" +
"⚠ UIParticle 节点自己的 scale 运行时会被强制改成 1/parentLossyScale(AutoScalingMode.Transform)," +
"在预制体里调它是没用的,别白费功夫。", MessageType.Info);
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("挂脚本", EditorStyles.boldLabel);
_addBjEndUI = EditorGUILayout.ToggleLeft(
"BJEndUI(结算面板:写倍数 + 最终分数)", _addBjEndUI);
using (new EditorGUI.IndentLevelScope())
{
_createBeishuNode = EditorGUILayout.ToggleLeft(
"在 Textparent 下新建 Node (1) 作为倍数文本", _createBeishuNode);
}
_addBjImage = EditorGUILayout.ToggleLeft(
"BJImage(奖励框:写金币数)", _addBjImage);
EditorGUILayout.Space();
_overwrite = EditorGUILayout.ToggleLeft("允许覆盖已存在的变体文件", _overwrite);
EditorGUILayout.Space();
if (GUILayout.Button("生成:结算面板 Self + Other", GUILayout.Height(30f)))
{
BuildSettlePanel(true);
BuildSettlePanel(false);
}
if (GUILayout.Button("生成:结算面板 Self", GUILayout.Height(24f))) BuildSettlePanel(true);
if (GUILayout.Button("生成:结算面板 Other", GUILayout.Height(24f))) BuildSettlePanel(false);
EditorGUILayout.Space();
if (GUILayout.Button("生成:奖励框 AwardFrame", GUILayout.Height(24f))) BuildAwardFrame();
EditorGUILayout.Space();
EditorGUILayout.LabelField("备用", EditorStyles.boldLabel);
if (GUILayout.Button("就地补齐选中的预制体(不生成变体,直接改它自己)", GUILayout.Height(24f)))
{
PatchSelectedInPlace();
}
EditorGUILayout.Space();
if (GUILayout.Button("检查资源到位情况", GUILayout.Height(24f))) ReportAssets();
EditorGUILayout.EndScrollView();
}
// ================================================================ 生成入口
private void BuildSettlePanel(bool isSelf)
{
string srcName = isSelf ? "PW_Effect_Settle_FlameStrike_Self" : "PW_Effect_Settle_FlameStrike_Other";
string srcPath = ArtEffectDir + "/" + srcName + ".prefab";
string dstPath = BJEndDir + "/" + srcName + Suffix + ".prefab";
CreateVariant(srcPath, dstPath, root => ModifySettlePanel(root, isSelf));
}
private void BuildAwardFrame()
{
const string srcName = "PW_Effect_AwardFrame_FlameStrike";
string srcPath = ArtEffectDir + "/" + srcName + ".prefab";
string dstPath = BJScoreDir + "/" + srcName + Suffix + ".prefab";
CreateVariant(srcPath, dstPath, ModifyAwardFrame);
}
// ================================================================ 变体创建
/// <summary>把 Art 源实例化成变体并保存到目标路径(SaveAsPrefabAsset 作用在预制体实例上 → 生成变体)。</summary>
private void CreateVariant(string srcPath, string dstPath, Action<GameObject> modify)
{
var src = AssetDatabase.LoadAssetAtPath<GameObject>(srcPath);
if (src == null)
{
Debug.LogError($"[烈焰风暴变体] 找不到源预制体:{srcPath}");
return;
}
EnsureFolder(Path.GetDirectoryName(dstPath).Replace('\\', '/'));
if (File.Exists(dstPath) && !_overwrite)
{
Debug.LogError($"[烈焰风暴变体] {dstPath} 已存在。要覆盖请先勾选「允许覆盖已存在的变体文件」," +
"或直接在 Project 里改名后再生成。");
return;
}
var inst = PrefabUtility.InstantiatePrefab(src) as GameObject;
if (inst == null)
{
Debug.LogError($"[烈焰风暴变体] 实例化失败:{srcPath}");
return;
}
try
{
inst.name = Path.GetFileNameWithoutExtension(dstPath);
modify?.Invoke(inst);
PrefabUtility.SaveAsPrefabAsset(inst, dstPath, out bool success);
if (!success)
{
Debug.LogError($"[烈焰风暴变体] 保存失败:{dstPath}");
return;
}
AssetDatabase.SaveAssets();
var saved = AssetDatabase.LoadAssetAtPath<GameObject>(dstPath);
var type = PrefabUtility.GetPrefabAssetType(saved);
if (type == PrefabAssetType.Variant)
Debug.Log($"[烈焰风暴变体] ✔ 已生成变体:{dstPath}");
else
Debug.LogWarning($"[烈焰风暴变体] {dstPath} 已生成,但类型是 {type} 而不是 Variant " +
"(说明它没继承 Art 源,请检查)");
if (saved != null) EditorGUIUtility.PingObject(saved);
}
finally
{
DestroyImmediate(inst);
}
}
// ================================================================ 结算面板加工
/// <summary>结算面板(Settle_FlameStrike_Self / _Other)的 UI 化 + BJEndUI。</summary>
private void ModifySettlePanel(GameObject root, bool isSelf)
{
var sb = new StringBuilder();
sb.AppendLine($"[烈焰风暴变体] 处理 {(isSelf ? "Self" : "Other")} 结算面板:{root.name}");
// ---- 1/2. 粒子 Renderer:关掉自带渲染 + 层归零
var psList = root.GetComponentsInChildren<ParticleSystem>(true);
int rendererCount = 0;
foreach (var ps in psList)
{
var psr = ps.GetComponent<ParticleSystemRenderer>();
if (psr == null) continue;
if (_disableParticleRenderers) psr.enabled = false;
if (_resetSortingLayer) psr.sortingLayerID = 0;
rendererCount++;
}
sb.AppendLine($" 粒子系统 {psList.Length} 个,Renderer 处理 {rendererCount} 个" +
$"(enabled={!_disableParticleRenderers}、sortingLayer 归零={_resetSortingLayer})");
// ---- 3. UIParticle 节点
if (_addUiParticle)
{
bool created = EnsureUiParticle(root, psList, out int particleCount);
sb.AppendLine(created
? $" 新建 UIParticle 节点,收粒子 {particleCount} 个,Scale3D={TemplateUiParticleScale3D}"
: $" 复用已有 UIParticle 节点,刷新粒子列表 {particleCount} 个,Scale3D={TemplateUiParticleScale3D}");
// 粒子节点缩放归一化:模板把源里的 30 / 46.176 / 48.395 统一改成 5,配 m_Scale3D=10 用。
// ⚠ 这一步与 UIParticle 是新建还是复用无关 —— 真正决定大小的是「粒子节点 scale × m_Scale3D」,
// UIParticle 节点自己的 scale 运行时会被 AutoScalingMode.Transform 覆盖,两种情况都要归一化。
if (_scaleParticleNodes)
sb.AppendLine(NormalizeParticleNodeScales(psList));
else
sb.AppendLine(" 粒子节点缩放:保持源里的原值(未归一化 —— 与其它 Boss 面板口径不一致,请预览确认)");
}
// ---- 4. 根 RectTransform
if (_fixRootRect)
{
var rt = root.GetComponent<RectTransform>();
if (rt != null)
{
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.sizeDelta = new Vector2(100f, 100f);
rt.anchoredPosition = Vector2.zero;
rt.localPosition = Vector3.zero;
rt.localRotation = Quaternion.identity;
sb.AppendLine(" 根 RectTransform:anchor/pivot 0.5、sizeDelta 100×100、位置归零");
}
}
// ---- 5. 分数文本样式
var scoreTmp = FindScoreTmp(root);
if (scoreTmp == null)
{
Debug.LogError($"[烈焰风暴变体] {root.name} 里找不到分数文本(预期路径 Node/Textparent/Node),BJEndUI 未挂");
}
else if (_applyTextStyle)
{
var font = LoadAsset<TMP_FontAsset>(FontGuid);
var gold = LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid);
if (font != null) scoreTmp.font = font;
if (gold != null) scoreTmp.spriteAsset = gold;
sb.AppendLine($" 分数文本 {GetPath(scoreTmp.transform)}:字体=" +
$"{(font != null ? font.name : "未改")}、spriteAsset={(gold != null ? gold.name : "未改")}");
}
// ---- 6/7. 倍数文本 + BJEndUI
if (_addBjEndUI)
{
if (scoreTmp == null)
{
sb.AppendLine(" 跳过 BJEndUI(没有分数文本可接)");
}
else
{
TextMeshProUGUI beishuTmp = null;
if (_createBeishuNode)
{
beishuTmp = EnsureBeishuNode(scoreTmp);
sb.AppendLine($" 倍数文本:{GetPath(beishuTmp.transform)}(scale {TemplateBeishuScale}," +
$"pos {TemplateBeishuPos},字号 {TemplateBeishuFontSize})");
}
var endUI = root.GetComponent<BJEndUI>();
if (endUI == null) endUI = root.AddComponent<BJEndUI>();
endUI.tMP_score = scoreTmp;
endUI.tMP_beishu = beishuTmp;
endUI.goldSprite = LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid);
endUI.SilverSprite = LoadAsset<TMP_SpriteAsset>(SilverSpriteGuid);
endUI.img_BaoJiangZhong = null;
endUI.useScoreAnim = false;
endUI.autoDestroyDelay = 5f;
sb.AppendLine($" 已挂 BJEndUI(tMP_score={scoreTmp.name}、" +
$"tMP_beishu={(beishuTmp != null ? beishuTmp.name : "空")}," +
$"gold/SilverSprite 已接,autoDestroyDelay=5)");
}
}
Debug.Log(sb.ToString());
}
// ================================================================ 奖励框加工
/// <summary>奖励框(AwardFrame_FlameStrike):不碰粒子,只挂 BJImage(写金币数)。</summary>
private void ModifyAwardFrame(GameObject root)
{
var sb = new StringBuilder();
sb.AppendLine($"[烈焰风暴变体] 处理奖励框:{root.name}");
if (_fixRootRect)
{
var rt = root.GetComponent<RectTransform>();
if (rt != null)
{
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.sizeDelta = new Vector2(100f, 100f);
rt.anchoredPosition = Vector2.zero;
rt.localPosition = Vector3.zero;
rt.localRotation = Quaternion.identity;
sb.AppendLine(" 根 RectTransform:anchor/pivot 0.5、sizeDelta 100×100、位置归零");
}
}
if (_addBjImage)
{
var coinTmp = FindCoinNumTmp(root);
if (coinTmp == null)
{
Debug.LogError($"[烈焰风暴变体] {root.name} 里找不到金币数字文本(预期在 Num 节点下),BJImage 未挂");
}
else
{
var bjImage = root.GetComponent<BJImage>();
if (bjImage == null) bjImage = root.AddComponent<BJImage>();
bjImage.tmp_CoinNum = coinTmp;
bjImage.goldSprite = LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid);
bjImage.silverSprite = LoadAsset<TMP_SpriteAsset>(SilverSpriteGuid);
sb.AppendLine($" 已挂 BJImage(tmp_CoinNum={GetPath(coinTmp.transform)},gold/silverSprite 已接)");
}
}
Debug.Log(sb.ToString());
}
// ================================================================ 就地补齐(改选中预制体本身)
private void PatchSelectedInPlace()
{
var path = AssetDatabase.GetAssetPath(Selection.activeObject);
if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
Debug.LogError("[烈焰风暴变体] 请先在 Project 窗口里选中一个 .prefab 再点这个按钮");
return;
}
if (PrefabUtility.GetPrefabAssetType(Selection.activeObject) == PrefabAssetType.Variant)
{
Debug.LogError($"[烈焰风暴变体] {path} 是变体,请直接在变体上改;就地补齐只支持普通预制体");
return;
}
var contents = PrefabUtility.LoadPrefabContents(path);
if (contents == null) return;
try
{
bool isAwardFrame = contents.name.IndexOf("AwardFrame", StringComparison.OrdinalIgnoreCase) >= 0;
if (isAwardFrame) ModifyAwardFrame(contents);
else ModifySettlePanel(contents, contents.name.IndexOf("Self", StringComparison.OrdinalIgnoreCase) >= 0);
PrefabUtility.SaveAsPrefabAsset(contents, path, out bool ok);
if (ok) Debug.Log($"[烈焰风暴变体] ✔ 已就地补齐:{path}");
else Debug.LogError($"[烈焰风暴变体] 就地保存失败:{path}");
}
finally
{
PrefabUtility.UnloadPrefabContents(contents);
}
AssetDatabase.SaveAssets();
}
// ================================================================ 资源检查
private void ReportAssets()
{
var sb = new StringBuilder("[烈焰风暴] 资源到位检查\n");
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_Settle_FlameStrike_Self.prefab");
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_Settle_FlameStrike_Other.prefab");
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_AwardFrame_FlameStrike.prefab");
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_Wheel.prefab");
CheckPrefab(sb, BJEndDir + "/PW_Effect_Wheel_LieYanFengBao.prefab");
CheckPrefab(sb, BJEndDir + "/PW_Effect_Settle_FlameStrike_Self" + Suffix + ".prefab");
CheckPrefab(sb, BJEndDir + "/PW_Effect_Settle_FlameStrike_Other" + Suffix + ".prefab");
CheckPrefab(sb, BJScoreDir + "/PW_Effect_AwardFrame_FlameStrike" + Suffix + ".prefab");
CheckPrefab(sb, "Assets/MyGame/aa/Game/XJMH/Boss/LieYanFengBao/PW_Enemy_115.prefab");
CheckPrefab(sb, "Assets/MyGame/aa/Game/XJMH/Prefabs/Bullit/PW_Bullet_FlameRound.prefab");
CheckPrefab(sb, "Assets/Art/XJMH/pw/pw_spine/pw_spine_plane/pw_spine_plane_lyfb/PW_Spine_Plane_LYFB_SkeletonData.asset");
sb.AppendLine();
sb.AppendLine("音效(tbaudio.json 里应为 69009-69012):");
foreach (var n in new[] { "PW_FlameStrike_Explosion", "PW_FlameStrike_Die",
"PW_FlameStrike_WheelExplosion", "PW_FlameStrike_WheelFly" })
{
var p = "Assets/Audios/XJMH/" + n + ".ogg";
sb.AppendLine(" " + (File.Exists(p) ? "✔" : "✘") + " " + p);
}
sb.AppendLine();
sb.AppendLine("挂脚本可用性:");
sb.AppendLine(" " + (LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid) != null ? "✔" : "✘") + " 金币 spriteAsset");
sb.AppendLine(" " + (LoadAsset<TMP_SpriteAsset>(SilverSpriteGuid) != null ? "✔" : "✘") + " 银币 spriteAsset");
sb.AppendLine(" " + (LoadAsset<TMP_FontAsset>(FontGuid) != null ? "✔" : "✘") + " SourceHanSans 字体");
Debug.Log(sb.ToString());
}
private static void CheckPrefab(StringBuilder sb, string path)
{
bool exists = File.Exists(path);
sb.AppendLine((exists ? "✔ " : "✘ ") + path + (exists ? "" : " ← 缺失"));
}
// ================================================================ 工具方法
/// <summary>确保根下有一个 UIParticle 节点,并把所有粒子收进 m_Particles。</summary>
private static bool EnsureUiParticle(GameObject root, ParticleSystem[] psList, out int particleCount)
{
var ui = root.GetComponentInChildren<UIParticle>(true);
bool created = false;
if (ui == null)
{
var go = new GameObject("UIParticle", typeof(RectTransform), typeof(CanvasRenderer), typeof(UIParticle));
var rt = (RectTransform)go.transform;
rt.SetParent(root.transform, false);
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = Vector2.zero;
rt.sizeDelta = Vector2.zero;
rt.localScale = Vector3.one; // 运行时会被 UIParticle 覆盖成 1/parentLossyScale,这里给 1 只是编辑器里好看
ui = go.GetComponent<UIParticle>();
created = true;
}
// UIParticle 的序列化字段都是 private → 用 SerializedObject 写
var so = new SerializedObject(ui);
var arr = so.FindProperty("m_Particles");
if (arr != null)
{
arr.arraySize = psList.Length;
for (int i = 0; i < psList.Length; i++)
arr.GetArrayElementAtIndex(i).objectReferenceValue = psList[i];
}
var scale3D = so.FindProperty("m_Scale3D");
if (scale3D != null) scale3D.vector3Value = TemplateUiParticleScale3D;
so.ApplyModifiedPropertiesWithoutUndo();
particleCount = psList.Length;
if (arr == null)
Debug.LogWarning("[烈焰风暴变体] 没找到 UIParticle.m_Particles 字段(插件版本可能不同),请手动拖粒子列表");
return created;
}
/// <summary>
/// 把各粒子节点的 localScale 归一化到模板口径(源里的 30 → 5,配 m_Scale3D=10 使用)。
///
/// 按比例模式:取出现次数最多的缩放当「主缩放」,factor = 目标 / 主缩放,所有节点乘同一倍率
/// → 30→5、1→0.1667,不会把原本就小的节点放大 5 倍(烈焰风暴源里正好是 12 个 30 + 2 个 1)。
/// 非按比例:所有节点直接写死目标值(模板做法的字面复刻)。
/// </summary>
private string NormalizeParticleNodeScales(ParticleSystem[] psList)
{
if (psList == null || psList.Length == 0) return " 粒子节点缩放:没有粒子系统,跳过";
var counts = new Dictionary<float, int>();
foreach (var ps in psList)
{
float v = ps.transform.localScale.x;
counts.TryGetValue(v, out int c);
counts[v] = c + 1;
}
float dominant = 1f;
int best = -1;
foreach (var kv in counts)
{
if (kv.Value > best) { best = kv.Value; dominant = kv.Key; }
}
if (Mathf.Approximately(dominant, 0f)) dominant = 1f;
float factor = _proportionalScale ? _particleNodeScale / dominant : 0f;
var sb = new StringBuilder();
sb.Append(_proportionalScale
? $" 粒子节点归一化:主缩放 {dominant}({best}/{psList.Length} 个)→ {_particleNodeScale},倍率 {factor:0.####}"
: $" 粒子节点归一化:全部统一写成 {_particleNodeScale}");
int shown = 0;
foreach (var ps in psList)
{
var old = ps.transform.localScale;
var nv = _proportionalScale ? old * factor : Vector3.one * _particleNodeScale;
ps.transform.localScale = nv;
SetConstrainProportionsScale(ps.transform, true);
if (shown++ < 16)
sb.Append($"\n {GetPath(ps.transform)}: {old.x:0.####} → {nv.x:0.####}");
}
return sb.ToString();
}
/// <summary>在 Textparent 下确保存在倍数文本节点(模板里叫 Node (1))。</summary>
private static TextMeshProUGUI EnsureBeishuNode(TextMeshProUGUI scoreTmp)
{
var parent = scoreTmp.transform.parent;
if (parent == null)
{
Debug.LogWarning("[烈焰风暴变体] 分数文本没有父节点,倍数文本挂在它自己下面");
parent = scoreTmp.transform;
}
var exist = parent.Find("Node (1)");
if (exist != null)
{
var t = exist.GetComponent<TextMeshProUGUI>();
if (t != null) return t;
}
var go = new GameObject("Node (1)", typeof(RectTransform));
var rt = (RectTransform)go.transform;
rt.SetParent(parent, false);
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = TemplateBeishuPos;
rt.sizeDelta = TemplateBeishuSize;
rt.localScale = Vector3.one * TemplateBeishuScale;
var tmp = go.AddComponent<TextMeshProUGUI>();
tmp.font = scoreTmp.font; // 运行时 AddComponent 的文本必须显式给字体,否则不显示
tmp.spriteAsset = scoreTmp.spriteAsset;
tmp.fontSize = TemplateBeishuFontSize;
tmp.alignment = TextAlignmentOptions.Center;
tmp.color = Color.white;
tmp.raycastTarget = false;
tmp.text = "225"; // 占位,运行时由 BJEndUI.Init 覆写
return tmp;
}
/// <summary>分数文本:优先 Node/Textparent/Node,再退到 Textparent 下的第一个 TMP。</summary>
private static TextMeshProUGUI FindScoreTmp(GameObject root)
{
var t = root.transform.Find("Node/Textparent/Node") ?? root.transform.Find("Textparent/Node");
if (t != null)
{
var tmp = t.GetComponent<TextMeshProUGUI>();
if (tmp != null) return tmp;
}
var tp = FindDeep(root.transform, "Textparent");
if (tp != null)
{
var tmp = tp.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null) return tmp;
}
Debug.LogWarning($"[烈焰风暴变体] {root.name}:没找到 Textparent,退用整棵树里第一个 TMP");
return root.GetComponentInChildren<TextMeshProUGUI>(true);
}
/// <summary>奖励框的金币数字文本(Num 节点下的 TMP)。</summary>
private static TextMeshProUGUI FindCoinNumTmp(GameObject root)
{
var num = FindDeep(root.transform, "Num");
if (num != null)
{
var tmp = num.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null) return tmp;
}
return root.GetComponentInChildren<TextMeshProUGUI>(true);
}
private static Transform FindDeep(Transform root, string name)
{
if (root == null) return null;
if (string.Equals(root.name, name, StringComparison.OrdinalIgnoreCase)) return root;
for (int i = 0; i < root.childCount; i++)
{
var r = FindDeep(root.GetChild(i), name);
if (r != null) return r;
}
return null;
}
private static string GetPath(Transform t)
{
if (t == null) return "?";
var s = t.name;
var p = t.parent;
while (p != null) { s = p.name + "/" + s; p = p.parent; }
return s;
}
private static void SetConstrainProportionsScale(Transform t, bool value)
{
var so = new SerializedObject(t);
var p = so.FindProperty("m_ConstrainProportionsScale");
if (p != null)
{
p.boolValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
}
private static T LoadAsset<T>(string guid) where T : UnityEngine.Object
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(path))
{
Debug.LogWarning($"[烈焰风暴变体] guid 解析不到资源:{guid}");
return null;
}
return AssetDatabase.LoadAssetAtPath<T>(path);
}
private static void EnsureFolder(string folder)
{
if (AssetDatabase.IsValidFolder(folder)) return;
var parts = folder.Split('/');
var cur = parts[0];
for (int i = 1; i < parts.Length; i++)
{
var next = cur + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
cur = next;
}
}
}
#endif