#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Coffee.UIExtensions;
using TMPro;
using UnityEditor;
using UnityEngine;
///
/// 烈焰风暴「结算面板(Settle)」/「奖励框(AwardFrame)」的 UI 化变体生成器。
///
/// 背景:XJMH 的爆奖面板都是「Art 源预制体 → MyGame 变体」两层。Art 源是给 3D 场景用的,
/// 直接当 UI 用会有三个问题:粒子不在 Canvas 里渲染、卡面数字没接、没有 BJEndUI 写分数。
/// 同门 Boss(铁拳 / 伽马风暴 / 破坏球 / 幸运轮 …)都在
/// Assets/MyGame/aa/Game/XJMH/Boss/BJ/BJEnd 下留了做好的变体,本脚本就是照抄它们的做法。
///
/// 从已有变体(PW_Effect_Settle_IronFist_Self_XingJiXunHang)反解出来的加工步骤:
/// 1. 所有 ParticleSystemRenderer:m_Enabled = 0(改由 UIParticle 渲染)
/// 2. 所有 ParticleSystemRenderer:m_SortingLayer = 0(对齐 Default 层)
/// 3. 根下新增 UIParticle 子节点: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. 分数文本(Node/Textparent/Node):字体换 SourceHanSans、spriteAsset 换金币图
/// 6. Textparent 下新增 Node (1) 文本(倍数用),scale 0.7 / 位置 (0,8) / 字号 36
/// 7. 根上挂 BJEndUI:tMP_score = 源里的分数文本,tMP_beishu = 新增的 Node (1)
///
/// 奖励框(AwardFrame)另走一条:不碰粒子,只在根上挂 BJImage(tmp_CoinNum = Num 文本)。
///
/// ⚠️ 本脚本不会去改 Art 源预制体(除非你点「就地补齐选中预制体」)。生成的是变体,
/// 这样以后美术更新 Art 源,MyGame 变体还能跟着走。
///
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";
/// 变体文件名后缀(与同门 _XingJiXunHang / _XieZi 一致)
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(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);
}
// ================================================================ 变体创建
/// 把 Art 源实例化成变体并保存到目标路径(SaveAsPrefabAsset 作用在预制体实例上 → 生成变体)。
private void CreateVariant(string srcPath, string dstPath, Action modify)
{
var src = AssetDatabase.LoadAssetAtPath(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(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);
}
}
// ================================================================ 结算面板加工
/// 结算面板(Settle_FlameStrike_Self / _Other)的 UI 化 + BJEndUI。
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(true);
int rendererCount = 0;
foreach (var ps in psList)
{
var psr = ps.GetComponent();
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();
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(FontGuid);
var gold = LoadAsset(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();
if (endUI == null) endUI = root.AddComponent();
endUI.tMP_score = scoreTmp;
endUI.tMP_beishu = beishuTmp;
endUI.goldSprite = LoadAsset(GoldSpriteGuid);
endUI.SilverSprite = LoadAsset(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());
}
// ================================================================ 奖励框加工
/// 奖励框(AwardFrame_FlameStrike):不碰粒子,只挂 BJImage(写金币数)。
private void ModifyAwardFrame(GameObject root)
{
var sb = new StringBuilder();
sb.AppendLine($"[烈焰风暴变体] 处理奖励框:{root.name}");
if (_fixRootRect)
{
var rt = root.GetComponent();
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();
if (bjImage == null) bjImage = root.AddComponent();
bjImage.tmp_CoinNum = coinTmp;
bjImage.goldSprite = LoadAsset(GoldSpriteGuid);
bjImage.silverSprite = LoadAsset(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(GoldSpriteGuid) != null ? "✔" : "✘") + " 金币 spriteAsset");
sb.AppendLine(" " + (LoadAsset(SilverSpriteGuid) != null ? "✔" : "✘") + " 银币 spriteAsset");
sb.AppendLine(" " + (LoadAsset(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 ? "" : " ← 缺失"));
}
// ================================================================ 工具方法
/// 确保根下有一个 UIParticle 节点,并把所有粒子收进 m_Particles。
private static bool EnsureUiParticle(GameObject root, ParticleSystem[] psList, out int particleCount)
{
var ui = root.GetComponentInChildren(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();
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;
}
///
/// 把各粒子节点的 localScale 归一化到模板口径(源里的 30 → 5,配 m_Scale3D=10 使用)。
///
/// 按比例模式:取出现次数最多的缩放当「主缩放」,factor = 目标 / 主缩放,所有节点乘同一倍率
/// → 30→5、1→0.1667,不会把原本就小的节点放大 5 倍(烈焰风暴源里正好是 12 个 30 + 2 个 1)。
/// 非按比例:所有节点直接写死目标值(模板做法的字面复刻)。
///
private string NormalizeParticleNodeScales(ParticleSystem[] psList)
{
if (psList == null || psList.Length == 0) return " 粒子节点缩放:没有粒子系统,跳过";
var counts = new Dictionary();
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();
}
/// 在 Textparent 下确保存在倍数文本节点(模板里叫 Node (1))。
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();
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();
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;
}
/// 分数文本:优先 Node/Textparent/Node,再退到 Textparent 下的第一个 TMP。
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();
if (tmp != null) return tmp;
}
var tp = FindDeep(root.transform, "Textparent");
if (tp != null)
{
var tmp = tp.GetComponentInChildren(true);
if (tmp != null) return tmp;
}
Debug.LogWarning($"[烈焰风暴变体] {root.name}:没找到 Textparent,退用整棵树里第一个 TMP");
return root.GetComponentInChildren(true);
}
/// 奖励框的金币数字文本(Num 节点下的 TMP)。
private static TextMeshProUGUI FindCoinNumTmp(GameObject root)
{
var num = FindDeep(root.transform, "Num");
if (num != null)
{
var tmp = num.GetComponentInChildren(true);
if (tmp != null) return tmp;
}
return root.GetComponentInChildren(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(string guid) where T : UnityEngine.Object
{
var path = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(path))
{
Debug.LogWarning($"[烈焰风暴变体] guid 解析不到资源:{guid}");
return null;
}
return AssetDatabase.LoadAssetAtPath(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