特效处理

This commit is contained in:
2026-10-20 18:30:03 +08:00
parent 40bd235e67
commit bee2d42356
131 changed files with 837 additions and 456 deletions
@@ -0,0 +1,179 @@
using System.Text;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEngine;
/// <summary>
/// AA 依赖链修复迁移工具(一次性)。
///
/// 【背景】Addressables 加载 bundle 时会递归加载其依赖 bundle,而构建报告显示存在
/// 跨玩法依赖链,导致"未登录就加载鱼包 / 进 A 玩法拉 B 玩法鱼包":
/// defaultlocalgroup → fish_xjmh → fish_hhwk (启动链:默认音频一碰 D LG 全链拖入)
/// fish_flzy → fish_hhwk (进 FLZY 房拉 HHWK 鱼包)
/// gameuires → fish_xjmh (进任何房间拉 XJMH 鱼包)
///
/// 【修复内容】(对应 2026-10-08 16:41 构建报告分析出的确切依赖边)
/// 1. PW_Enemy_4000.prefab(XJMH Boss,误在 D LG)→ Fish_XJMH 组 断 D LG→fish_xjmh
/// 2. XJMHCreateFish.asset(XJMH 配置,误在 D LG)→ Fish_XJMH 组
/// 3. 新建 SharedRes 组:Game177_baigujing.png(HHWK 图鉴贴图被 XJMH 小丑扑克引用)、
/// fish_4003 Variant.png(HHWK 图鉴贴图被 FLZY 闪电引用)→ SharedRes
/// 断 fish_xjmh→fish_hhwk、fish_flzy→fish_hhwk(两边都只依赖小共享包)
/// 4. GameMainPanelXJMH 引用的 7 个 XJMH 资源(Boss出场特效/帮助Item/技能sprite)
/// → GameUIRes 组 断 gameuires→fish_xjmh
/// (若鱼包资产也引用它们,产生 fish_xjmh→gameuires 边——进房必载面板,无害)
///
/// 【使用】Unity 菜单 Tools → AA依赖修复 → 执行修复并重建Addressables,
/// 完成后用 AABundleMonitor 悬浮窗验证:启动仅 4 个包、进房只加当前玩法鱼包。
/// </summary>
public static class AAFixDependencyMigration
{
[MenuItem("Tools/AA依赖修复/执行修复并重建Addressables")]
public static void Run()
{
if (!EditorUtility.DisplayDialog("AA依赖修复",
"将执行以下迁移(断掉跨玩法 bundle 依赖链):\n\n" +
"1. PW_Enemy_4000 / XJMHCreateFish → Fish_XJMH 组\n" +
"2. 新建 SharedRes 组,移入 2 张被跨玩法引用的贴图\n" +
"3. GameMainPanelXJMH 引用的 7 个资源 → GameUIRes 组\n\n" +
"迁移完成后可选择立即重建 Addressables。\n继续?",
"执行", "取消"))
return;
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogError("[AA依赖修复] 找不到 AddressableAssetSettings");
return;
}
var log = new StringBuilder();
// ---------- ① D LG 里误放的 XJMH 资产 → Fish_XJMH(断 D LG→fish_xjmh 启动链) ----------
var fishXjmh = settings.FindGroup("Fish_XJMH");
if (fishXjmh == null) { Debug.LogError("[AA依赖修复] 找不到 Fish_XJMH 组"); return; }
MoveWithLabels(settings, fishXjmh,
"Assets/MyGame/aa/Game/XJMH/Boss/XingJiXunHang/PW_Enemy_4000.prefab",
new[] { "FishPrefab", "XJMH" }, log);
MoveWithLabels(settings, fishXjmh,
"Assets/MyGame/aa/UI/Game/XJMH/XJMHCreateFish.asset",
new[] { "XJMH" }, log);
// ---------- ② 新建 SharedRes 组,移入被跨玩法引用的贴图(断 xjmh/flzy→hhwk) ----------
var shared = settings.FindGroup("SharedRes");
if (shared == null)
{
shared = CreateGroupLike(settings, "SharedRes", "Fish_XJMH");
log.AppendLine("[组] 已创建 SharedRes(打包配置复制自 Fish_XJMH)");
}
// 保留原 TuJian/HHWK 标签:HHWK 玩法的图鉴 Sprite 交集加载(TuJian+HHWK)仍能命中,
// 只是从 SharedRes 包里取;XJMH/FLZY 对它们的引用改为依赖小共享包
MoveKeepLabels(settings, shared, "Assets/Art/HHWK/res/texture/dynamic/Game177_baigujing.png", log);
MoveKeepLabels(settings, shared, "Assets/Art/HHWK/TuJianTexture/fish_4003 Variant.png", log);
// ---------- ③ GameMainPanelXJMH 引用的资源 → GameUIRes(断 gameuires→fish_xjmh) ----------
var gameUi = settings.FindGroup("GameUIRes");
if (gameUi == null) { Debug.LogError("[AA依赖修复] 找不到 GameUIRes 组"); return; }
string[] guiTargets =
{
"Assets/Art/XJMH/pw/pw_prefab/game/effect/PW_Effect_Boss_Appear.prefab",
"Assets/Art/XJMH/pw/pw_prefab/item/PW_HelpItem1.prefab",
"Assets/Art/XJMH/pw/pw_prefab/item/PW_HelpItem2.prefab",
"Assets/Art/XJMH/pw/pw_prefab/item/PW_SetLockItem.prefab",
"Assets/Art/XJMH/pw/pw_sprite/common/zd_jn_sd.png",
"Assets/Art/XJMH/pw/pw_sprite/common/zd_jn_ss.png",
"Assets/Art/XJMH/pw/pw_sprite/common/zd_jn_zd.png",
};
foreach (var t in guiTargets)
MoveKeepLabels(settings, gameUi, t, log);
AssetDatabase.SaveAssets();
Debug.Log("[AA依赖修复] 迁移完成:\n" + log);
// ---------- ④ 重建 ----------
if (EditorUtility.DisplayDialog("AA依赖修复",
"迁移完成:\n" + log + "\n现在重建 Addressables?(约几分钟)",
"立即重建", "稍后手动重建"))
{
Debug.Log("[AA依赖修复] 开始重建 Addressables...");
AddressableAssetSettings.BuildPlayerContent(out var result);
if (result == null || !string.IsNullOrEmpty(result.Error))
{
Debug.LogError("[AA依赖修复] 构建失败: " + (result != null ? result.Error : "无返回结果"));
EditorUtility.DisplayDialog("AA依赖修复", "构建失败: " + (result != null ? result.Error : "无返回结果"), "确定");
}
else
{
Debug.Log("[AA依赖修复] 构建完成");
EditorUtility.DisplayDialog("AA依赖修复",
"构建完成!\n\n验证步骤:\n" +
"1. Play 启动到登录页 → 悬浮窗应只有 4 个包(builtinshaders/LoginUIRes/DefaultLocalGroup/Turret)\n" +
"2. 进任一玩法房间 → 只新增该玩法的鱼包 + gameuires", "确定");
}
}
}
/// <summary>移动资产到目标组并覆盖标签</summary>
private static void MoveWithLabels(AddressableAssetSettings settings, AddressableAssetGroup target,
string path, string[] labels, StringBuilder log)
{
string guid = AssetDatabase.AssetPathToGUID(path);
if (string.IsNullOrEmpty(guid))
{
log.AppendLine("[跳过] 找不到资产: " + path);
return;
}
var entry = settings.CreateOrMoveEntry(guid, target);
if (entry == null)
{
log.AppendLine("[跳过] 移动失败: " + path);
return;
}
entry.labels.Clear();
foreach (var l in labels)
entry.labels.Add(l);
log.AppendLine($"[移动] {path}\n → {target.name}(标签: {string.Join(",", labels)})");
}
/// <summary>移动资产到目标组,保留原有标签</summary>
private static void MoveKeepLabels(AddressableAssetSettings settings, AddressableAssetGroup target,
string path, StringBuilder log)
{
string guid = AssetDatabase.AssetPathToGUID(path);
if (string.IsNullOrEmpty(guid))
{
log.AppendLine("[跳过] 找不到资产: " + path);
return;
}
var entry = settings.CreateOrMoveEntry(guid, target);
if (entry == null)
{
log.AppendLine("[跳过] 移动失败: " + path);
return;
}
log.AppendLine($"[移动] {path}\n → {target.name}(保留标签: {string.Join(",", entry.labels)})");
}
/// <summary>按源组的打包配置(BundledAssetGroupSchema/ContentUpdateGroupSchema)创建新组。
/// AddSchema(schema) 内部会复制 schema 并正确挂到新组上,无需手动 CopySerialized。</summary>
private static AddressableAssetGroup CreateGroupLike(AddressableAssetSettings settings,
string newName, string srcGroupName)
{
var src = settings.FindGroup(srcGroupName);
var group = settings.CreateGroup(newName, false, false, false, null);
var srcBundled = src.GetSchema<BundledAssetGroupSchema>();
if (srcBundled != null)
group.AddSchema(srcBundled, false);
var srcContent = src.GetSchema<ContentUpdateGroupSchema>();
if (srcContent != null)
group.AddSchema(srcContent, false);
return group;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89e9a25fe5c7fe6408466344b43fd403
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+114
View File
@@ -0,0 +1,114 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Text.RegularExpressions;
/// <summary>
/// 大厅图集切片烘焙工具(绕开 TextureImporter 管线,纯 Unity API):
/// 1. 读取 NGUI 图集 prefab 的 mSprites 坐标
/// 2. Sprite.Create 从大图切出 56 个 sprite,AssetDatabase.CreateAsset 落盘
/// 3. 大厅 prefab 里 108 个 Image 按节点语义规则重绑切片
/// 菜单:Tools / 烘焙大厅图集切片
/// </summary>
public static class BakeLobbySlicesTool
{
const string AtlasPrefabPath = "Assets/dataresources/atlas/a大厅/atlas_bylobbyclassic/Atlas_ByLobbyClassic.prefab";
const string AtlasTexPath = "Assets/dataresources/atlas/a大厅/atlas_bylobbyclassic/Atlas_ByLobbyClassic.png";
const string OutDir = "Assets/BakeLobbySlicesAssets";
const string LobbyPrefabPath = "Assets/bundles/by_lobby/bundles/panel/BY_LbPlayMainUGUIPanel.prefab";
[MenuItem("Tools/烘焙大厅图集切片")]
public static void Bake()
{
// ---------- 1. 读图集坐标 ----------
var atlasObj = AssetDatabase.LoadAssetAtPath<Object>(AtlasPrefabPath);
if (atlasObj == null) { Debug.LogError("[烘焙] 图集 prefab 加载失败: " + AtlasPrefabPath); return; }
var so = new SerializedObject(atlasObj);
var mSprites = so.FindProperty("mSprites");
if (mSprites == null || !mSprites.isArray || mSprites.arraySize == 0)
{ Debug.LogError("[烘焙] mSprites 读取失败 (arraySize=" + (mSprites == null ? -1 : mSprites.arraySize) + ")"); return; }
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(AtlasTexPath);
if (tex == null) { Debug.LogError("[烘焙] 大图加载失败,请先等 Unity 刷新完成: " + AtlasTexPath); return; }
Debug.Log($"[烘焙] 图集 {mSprites.arraySize} 切片, 大图 {tex.width}x{tex.height}");
// ---------- 2. 切片落盘 ----------
if (!AssetDatabase.IsValidFolder(OutDir)) AssetDatabase.CreateFolder("Assets", "BakeLobbySlicesAssets");
var made = new Dictionary<string, Sprite>();
for (int i = 0; i < mSprites.arraySize; i++)
{
var el = mSprites.GetArrayElementAtIndex(i);
string name = el.FindPropertyRelative("name").stringValue;
var rect = new Rect(el.FindPropertyRelative("x").floatValue, el.FindPropertyRelative("y").floatValue,
el.FindPropertyRelative("width").floatValue, el.FindPropertyRelative("height").floatValue);
var border = new Vector4(el.FindPropertyRelative("borderLeft").floatValue, el.FindPropertyRelative("borderBottom").floatValue,
el.FindPropertyRelative("borderRight").floatValue, el.FindPropertyRelative("borderTop").floatValue);
var sp = Sprite.Create(tex, rect, new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect, border);
sp.name = name;
var path = $"{OutDir}/{name}.asset";
AssetDatabase.DeleteAsset(path);
AssetDatabase.CreateAsset(sp, path);
made[name] = sp;
}
Debug.Log($"[烘焙] 生成 {made.Count} 个 sprite asset → {OutDir}");
// ---------- 3. 重绑大厅 prefab ----------
var root = PrefabUtility.LoadPrefabContents(LobbyPrefabPath);
int bound = 0, missing = 0;
try
{
var images = root.GetComponentsInChildren<UnityEngine.UI.Image>(true);
foreach (var img in images)
{
var sp = PickSlice(img.gameObject.name, made);
if (sp != null) { img.sprite = sp; bound++; }
else missing++;
}
PrefabUtility.SaveAsPrefabAsset(root, LobbyPrefabPath);
}
finally { PrefabUtility.UnloadPrefabContents(root); }
Debug.Log($"[烘焙] prefab 重绑 {bound} 个 Image, 未匹配 {missing} 个 —— 完成!");
}
// 节点名 → 切片(与此前 Python 修复用的语义规则一致,最后有兜底)
static Sprite PickSlice(string n, Dictionary<string, Sprite> m)
{
string s = null;
if (Regex.IsMatch(n, @"Frame_bg$")) s = "topbar_frame";
else if (n.Contains("Frame_lobby")) s = "topbar_def2";
else if (n.Contains("topbar_btn")) s = "topbar_btn";
else if (n.Contains("BtnClassic") || n == "Classic") s = "ic_tab_n1";
else if (n.Contains("BtnSuper") || n == "Super") s = "ic_tab_n2";
else if (n.Contains("BtnSiuxian") || n == "Siuxian") s = "ic_tab_n3";
else if (n.Contains("BtnFold")) s = "ic_tab_n4";
else if (n.StartsWith("tab_sel")) s = "tab_sel1";
else if (n.Contains("tab_bg")) s = "tab_bg";
else if (n.Contains("share_btn_bg")) s = "nor_frame_b";
else if (n.Contains("icon_more")) s = "icon_more";
else if (n.Contains("redDot")) s = "com_redDot";
else if (n.Contains("arrow") || n.Contains("jiantou")) s = "arrow";
else if (n == "bg_arrow") s = "bg_arrow";
else if (Regex.IsMatch(n, @"^(gem|gold|money|energy|bp)_bg")) s = "icon_bg";
else if (n.EndsWith("_spr")) s = "icon_bg2";
else if (n.Contains("headIcon")) s = "s_frame_p";
else if (n.Contains("player_level") || n.Contains("vip_spr")) s = "s_frame_r";
else if (n.EndsWith("_btn")) s = "btn";
else if (n.Contains("Frame") || n.Contains("frame")) s = "nor_frame_b2";
else if (n.StartsWith("eff_") || n.Contains("_eff")) s = "effect_1";
else if (n.Contains("lizi") || n.Contains("glow") || n.Contains("star") || n.Contains("sanguang")) s = "tx_mask";
else if (Regex.IsMatch(n, @"^bg( \(\d+\))?$")) s = "nor_frame_b";
else if (Regex.IsMatch(n, @"^Background( \(\d+\))?$")) s = "nor_frame_b2";
else if (n.Contains("ContentRoot")) s = "icon_bg";
else if (n.Contains("GodGuardTab") || n.Contains("ActivityCenter")) s = "tab_bg02";
else if (n.Contains("propIcon")) s = "icon_bg3";
else if (n.Contains("propTip")) s = "ls_txk_1";
else if (n.Contains("setup_bg")) s = "icon_bg3";
else if (n.Contains("musicOpen")) s = "icon_cardSystem_Bubble01";
else if (n.Contains("hlby_") || n.Contains("hong_spr")) s = "icon_cardSystem_Bubble02";
else if (n == "zi" || n.Contains("Label") || n.EndsWith("lab")) s = "txt_y_1";
else if (Regex.IsMatch(n, @"^Sprite( \(\d+\))?$")) s = "icon_bg2";
if (s != null && m.ContainsKey(s)) return m[s];
return m.ContainsKey("icon_bg2") ? m["icon_bg2"] : null; // 兜底
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6a3beb0f1bea6743becefb7271d8b07
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+139
View File
@@ -0,0 +1,139 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// 场景加载方式配置工具(HybridCLR 配套):
///
/// 规则:**凡是挂了热更脚本(Game.Hotfix)的场景,都不能随主包(Build Settings)加载**,
/// 必须通过 AssetBundle/Addressables 加载,否则运行时会变成 missing script
/// (典型现象:场景里的 MonoBehaviour 不工作,比如背景不动、UI 不响应)。
/// - UIScene:挂了 GameManager/TiShi/LoadingPanel/HuoDe 等 → 走 Addressables
/// - XJMH/FLZY/HHWK/YYSC:挂了 BGManager/BgFit 等 → 走 Addressables
/// - BootScene:纯 AOT 空场景(唯一随主包的场景),只负责启动
/// </summary>
public static class BootSceneCreator
{
private const string BootScenePath = "Assets/MyGame/Scenes/BootScene.unity";
private const string UIScenePath = "Assets/MyGame/Scenes/UIScene.unity";
/// <summary>游戏场景(内部挂热更脚本,必须走 Addressables 加载)</summary>
private static readonly string[] GameSceneNames = { "XJMH", "FLZY", "HHWK", "YYSC" };
private static string GameScenePath(string name) => $"Assets/MyGame/Scenes/{name}.unity";
[MenuItem("Tools/Create BootScene")]
public static void CreateBootScene()
{
// 1. 新建空场景
// 引导场景必须保持“全空”:不挂任何热更 MonoBehaviour(否则构建时会被判为 missing script),
// 也不需要挂 AOT 单例——Launcher 是 static 类,靠 RuntimeInitializeOnLoadMethod 自动执行;
// BeijingTimeManager 是 MonoSingleton(Awake 里 DontDestroyOnLoad),它已经挂在 UIScene 上,
// 在这里再挂一份反而会产生两个实例(两份定时同步循环)。
Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
// 2. 保存场景
bool saved = EditorSceneManager.SaveScene(scene, BootScenePath);
if (!saved)
{
Debug.LogError("[BootSceneCreator] 引导场景保存失败");
return;
}
Debug.Log($"[BootSceneCreator] 引导场景已创建: {BootScenePath}");
// 3. 把需要走 Addressables 的场景都标记好、并统一整理 Build Settings
SetupGameScenesToAddressablesInternal();
MarkSceneAddressable(UIScenePath, "UIScene");
// 4. 打开引导场景
EditorSceneManager.OpenScene(BootScenePath);
Debug.Log("[BootSceneCreator] 完成!请运行 HybridCLR > Generate > All 并重新构建。");
}
/// <summary>
/// 把游戏场景(XJMH/FLZY/HHWK/YYSC)标记为 Addressable(address = 场景名),
/// 并把它们从 Build Settings 移除(改由 Addressables 加载)。
/// </summary>
[MenuItem("Tools/Setup Game Scenes To Addressables")]
public static void SetupGameScenesToAddressables()
{
SetupGameScenesToAddressablesInternal();
Debug.Log("[BootSceneCreator] 游戏场景已标记为 Addressable(address = 场景名)并从 Build Settings 移除。请重新构建。");
}
private static void SetupGameScenesToAddressablesInternal()
{
foreach (var name in GameSceneNames)
MarkSceneAddressable(GameScenePath(name), name);
ApplyBuildSettings();
AssetDatabase.SaveAssets();
}
/// <summary>
/// 统一整理 Build Settings:
/// - BootScene 放第一位(启动场景)
/// - UIScene 与 4 个游戏场景全部移除(它们都改走 Addressables 加载)
/// </summary>
private static void ApplyBuildSettings()
{
var scenes = new List<EditorBuildSettingsScene>(EditorBuildSettings.scenes);
// 先移除所有需要走 Addressables 的场景,避免重复
scenes.RemoveAll(s => s.path == BootScenePath);
scenes.RemoveAll(s => s.path == UIScenePath);
foreach (var name in GameSceneNames)
{
string path = GameScenePath(name);
scenes.RemoveAll(s => s.path == path);
}
// BootScene 插到最前面
scenes.Insert(0, new EditorBuildSettingsScene(BootScenePath, true));
EditorBuildSettings.scenes = scenes.ToArray();
Debug.Log("[BootSceneCreator] Build Settings 已更新:BootScene 为启动场景,UIScene/XJMH/FLZY/HHWK/YYSC 已移除(改走 Addressables)。");
}
/// <summary>
/// 把一个场景标记为 Addressable,address key 设为指定值(Launcher / 游戏内加载按这个 key 找)。
/// </summary>
private static void MarkSceneAddressable(string scenePath, string key)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
Debug.LogError("[BootSceneCreator] AddressableAssetSettings 不存在,请先初始化 Addressables。");
return;
}
string guid = AssetDatabase.AssetPathToGUID(scenePath);
if (string.IsNullOrEmpty(guid))
{
Debug.LogError($"[BootSceneCreator] 找不到场景: {scenePath}");
return;
}
// 用默认组(Default Local Group)。如果项目有专门的场景组,可手动改。
AddressableAssetGroup group = settings.DefaultGroup;
if (group == null)
{
Debug.LogError("[BootSceneCreator] Addressables 默认组不存在。");
return;
}
AddressableAssetEntry entry = settings.CreateOrMoveEntry(guid, group);
entry.address = key;
EditorUtility.SetDirty(settings);
Debug.Log($"[BootSceneCreator] 场景已标记为 Addressable: {scenePath}(address='{key}',组='{group.Name}')。");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b13da061d7e52364c901da53c4b0bf20
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+285
View File
@@ -0,0 +1,285 @@
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// ChouJiangItem 批量绑定工具(编辑器专用)。
/// 把预制体拖进窗口(或 Project 里选中预制体/文件夹后点「从当前选中添加」),再点「开始绑定」:
/// 工具会找出所有名字以 ItemGrid 开头的格子,给每个格子挂上 ChouJiangItem,并按子物体名自动赋值:
///
/// Item → img_Icon(该节点上的 Image 组件)
/// Label → tmp_Count(该节点上是 TMP 时)/ tmp_Count_Old(该节点上是 UGUI Text 时)
/// 道具名字节点 → tmp_Name / tmp_Name_Old(节点名在窗口里配,留空则不处理)
/// GridMask → obj_Drawn
/// eff_xuan → obj_Selecting
/// eff_xuanzhong → obj_Landed
///
/// 节点名先在**直接子物体**里精确匹配(ItemGrid 下还有同名的 GridMask 子节点,
/// 所以必须优先取直接子物体),找不到再退到子树里找第一个同名节点。
/// itemId 不动,交给 ChouJiangToggle 按列表顺序自动编号。
/// </summary>
public class ChouJiangItemBinder : EditorWindow
{
const string CellPrefix = "ItemGrid"; // 格子节点名前缀
const string NodeIcon = "Item"; // 奖品图片
const string NodeLabel = "Label"; // 数量文本
const string NodeDrawn = "GridMask"; // 已抽中遮罩
const string NodeSelecting = "eff_xuan"; // 选中高亮
const string NodeLanded = "eff_xuanzhong"; // 抽中停留
[SerializeField] List<GameObject> targets = new List<GameObject>();
[SerializeField] string nameNode = ""; // 道具名字节点名,留空=不处理
[SerializeField] bool keepExisting = false; // 已有引用不覆盖
Vector2 scroll;
[MenuItem("Tools/抽奖/批量绑定 ChouJiangItem")]
static void Open()
{
ChouJiangItemBinder w = GetWindow<ChouJiangItemBinder>("抽奖 Item 绑定");
w.minSize = new Vector2(440f, 360f);
}
void OnGUI()
{
EditorGUILayout.LabelField("把预制体拖进来(或 Project 里选中后点下面的按钮)", EditorStyles.boldLabel);
Rect dropArea = GUILayoutUtility.GetRect(0f, 46f, GUILayout.ExpandWidth(true));
GUI.Box(dropArea, "拖预制体 / 文件夹到这里");
HandleDrag(dropArea);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("从当前选中添加")) AddFromSelection();
if (GUILayout.Button("清空列表")) targets.Clear();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
nameNode = EditorGUILayout.TextField("道具名字节点名(留空=不处理)", nameNode);
keepExisting = EditorGUILayout.Toggle("保留已有引用(不覆盖)", keepExisting);
EditorGUILayout.LabelField($"待绑定预制体:{targets.Count} 个");
scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.Height(110f));
for (int i = targets.Count - 1; i >= 0; i--)
{
EditorGUILayout.BeginHorizontal();
targets[i] = (GameObject)EditorGUILayout.ObjectField(targets[i], typeof(GameObject), false);
if (GUILayout.Button("×", GUILayout.Width(24f))) targets.RemoveAt(i);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(targets.Count == 0))
{
if (GUILayout.Button("开始绑定", GUILayout.Height(30f))) Run();
}
}
// ---------------- 目标收集 ----------------
void HandleDrag(Rect area)
{
Event e = Event.current;
if (e == null || !area.Contains(e.mousePosition)) return;
if (e.type != EventType.DragUpdated && e.type != EventType.DragPerform) return;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (e.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (Object o in DragAndDrop.objectReferences) AddTarget(o);
e.Use();
}
}
void AddFromSelection()
{
int before = targets.Count;
foreach (Object o in Selection.objects) AddTarget(o);
if (targets.Count == before) Debug.LogWarning("[抽奖绑定] 当前选中里没有预制体或含预制体的文件夹");
}
void AddTarget(Object o)
{
// 文件夹:递归收集里面的预制体
string path = AssetDatabase.GetAssetPath(o);
if (!string.IsNullOrEmpty(path) && AssetDatabase.IsValidFolder(path))
{
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { path });
foreach (string g in guids) AddTarget(AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(g)));
return;
}
GameObject go = o as GameObject;
if (go == null)
{
Debug.LogWarning("[抽奖绑定] 只支持预制体(GameObject),已跳过:" + o);
return;
}
if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab"))
{
Debug.LogWarning("[抽奖绑定] 不是预制体文件,已跳过:" + go.name);
return;
}
if (!targets.Contains(go)) targets.Add(go);
}
// ---------------- 执行绑定 ----------------
void Run()
{
List<string> paths = new List<string>();
foreach (GameObject go in targets)
{
string p = AssetDatabase.GetAssetPath(go);
if (!string.IsNullOrEmpty(p) && !paths.Contains(p)) paths.Add(p);
}
StringBuilder sb = new StringBuilder();
int cellAll = 0, missAll = 0;
for (int i = 0; i < paths.Count; i++)
{
string path = paths[i];
EditorUtility.DisplayProgressBar("绑定 ChouJiangItem", path, (float)i / paths.Count);
try
{
int cells, miss;
BindOne(path, out cells, out miss);
cellAll += cells;
missAll += miss;
sb.AppendLine($"{System.IO.Path.GetFileName(path)}:格子 {cells} 个" +
(miss > 0 ? $",{miss} 个有缺失节点(见 Console 警告)" : ",全部绑定完成"));
}
catch (System.Exception ex)
{
sb.AppendLine($"{System.IO.Path.GetFileName(path)}:失败 {ex.Message}");
Debug.LogError($"[抽奖绑定] {path} 处理失败:{ex}");
}
}
EditorUtility.ClearProgressBar();
targets.Clear();
Debug.Log($"[抽奖绑定] 完成,共 {cellAll} 个格子,{missAll} 个有缺失节点\n{sb}");
}
void BindOne(string path, out int cellCount, out int missingCount)
{
cellCount = 0;
missingCount = 0;
GameObject root = PrefabUtility.LoadPrefabContents(path);
try
{
Transform[] all = root.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < all.Length; i++)
{
Transform t = all[i];
if (!t.name.StartsWith(CellPrefix)) continue;
// 防止把同名前缀的容器误当格子:必须带 Item 或 GridMask 直接子物体
if (t.Find(NodeIcon) == null && t.Find(NodeDrawn) == null) continue;
cellCount++;
ChouJiangItem item = t.GetComponent<ChouJiangItem>();
if (item == null) item = t.gameObject.AddComponent<ChouJiangItem>();
int missing = 0;
// 奖品图片
Image icon = FindComp<Image>(t, NodeIcon);
if (icon != null) { if (!keepExisting || item.img_Icon == null) item.img_Icon = icon; }
else missing++;
// 数量文本:TMP 优先,没有 TMP 再用 UGUI Text(两版互斥)
Transform label = FindNode(t, NodeLabel);
TextMeshProUGUI tmp = label != null ? label.GetComponent<TextMeshProUGUI>() : null;
Text txt = label != null ? label.GetComponent<Text>() : null;
if (tmp != null)
{
if (!keepExisting || item.tmp_Count == null) { item.tmp_Count = tmp; item.tmp_Count_Old = null; }
}
else if (txt != null)
{
if (!keepExisting || item.tmp_Count_Old == null) { item.tmp_Count_Old = txt; item.tmp_Count = null; }
}
else missing++;
// 道具名字(节点名可配,留空不处理)
if (!string.IsNullOrEmpty(nameNode))
{
Transform nameT = FindNode(t, nameNode);
TextMeshProUGUI nameTmp = nameT != null ? nameT.GetComponent<TextMeshProUGUI>() : null;
Text nameTxt = nameT != null ? nameT.GetComponent<Text>() : null;
if (nameTmp != null)
{
if (!keepExisting || item.tmp_Name == null) { item.tmp_Name = nameTmp; item.tmp_Name_Old = null; }
}
else if (nameTxt != null)
{
if (!keepExisting || item.tmp_Name_Old == null) { item.tmp_Name_Old = nameTxt; item.tmp_Name = null; }
}
}
// 三个状态 GameObject
Transform drawn = FindNode(t, NodeDrawn);
if (drawn != null) { if (!keepExisting || item.obj_Drawn == null) item.obj_Drawn = drawn.gameObject; }
else missing++;
Transform selecting = FindNode(t, NodeSelecting);
if (selecting != null) { if (!keepExisting || item.obj_Selecting == null) item.obj_Selecting = selecting.gameObject; }
else missing++;
Transform landed = FindNode(t, NodeLanded);
if (landed != null) { if (!keepExisting || item.obj_Landed == null) item.obj_Landed = landed.gameObject; }
else missing++;
if (missing > 0)
{
missingCount++;
Debug.LogWarning($"[抽奖绑定] {System.IO.Path.GetFileName(path)} → 格子 {t.name} 缺 {missing} 个节点" +
"(Item / Label / GridMask / eff_xuan / eff_xuanzhong)");
}
}
if (cellCount == 0)
{
Debug.LogWarning($"[抽奖绑定] {path} 里没找到名字以 {CellPrefix} 开头、且带 Item 或 GridMask 的格子");
}
else
{
PrefabUtility.SaveAsPrefabAsset(root, path);
}
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
// ---------------- 查找 ----------------
/// <summary>先找直接子物体(精确同名),找不到再在整个子树里找第一个同名节点</summary>
static Transform FindNode(Transform cell, string name)
{
Transform direct = cell.Find(name);
if (direct != null) return direct;
Transform[] all = cell.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < all.Length; i++)
{
if (all[i] != cell && all[i].name == name) return all[i];
}
return null;
}
static T FindComp<T>(Transform cell, string name) where T : Component
{
Transform n = FindNode(cell, name);
return n != null ? n.GetComponent<T>() : null;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c7eb0452470ed54ca72730328199fbb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,694 @@
#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
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 78f5c1faa544d2744847c98f0a61cc94
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
{
"name": "Game.Hotfix.Editor.Game",
"rootNamespace": "",
"references": [
"GameFramework",
"Game.Hotfix",
"CameraShake",
"spine-unity",
"Coffee.UIParticle",
"Unity.Addressables",
"Unity.Addressables.Editor",
"Unity.ResourceManager",
"Unity.TextMeshPro",
"UnityEngine.UI",
"Unity.InputSystem",
"Unity.VisualScripting.Core"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 155e266f83e00c14580ae66df254d09b
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
/// <summary>
/// 机械蝎八连抽配置生成器(编辑器工具)。
///
/// 用法:菜单 Tools → 机械蝎 → 生成八连抽配置资产
/// 1. 自动查找项目中 PW_Effect_FreeDropChip / PW_Effect_Settle_Scorpion 预制体
/// 2. 自动查找 PW_FreeItem_DropGemstone* / DropCard* / DropProp* Sprite,按文件名数字排序填入
/// 3. 生成 JiXieXieConfig.asset 到 JiXieXie 目录(已存在则覆盖并刷新)
/// 4. 选中生成的资产,拖到爆奖预制体的 JiXieXieEvent.config 字段即可
/// </summary>
public static class JiXieXieConfigAutoFill
{
private const string ConfigDir = "Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie";
private const string ConfigPath = ConfigDir + "/JiXieXieConfig.asset";
[MenuItem("Tools/机械蝎/生成八连抽配置资产")]
public static void GenerateConfig()
{
// 1. 查找预制体
GameObject chipPrefab = FindAsset<GameObject>("PW_Effect_FreeDropChip");
GameObject settlePrefab = FindAsset<GameObject>("PW_Effect_Settle_Scorpion");
// 2. 查找图片(按名字数字排序)
Sprite[] gemstones = FindSpritesSorted("PW_FreeItem_DropGemstone");
Sprite[] cards = FindSpritesSorted("PW_FreeItem_DropCard");
Sprite[] goods = FindSpritesSorted("PW_FreeItem_DropProp");
if (chipPrefab == null)
Debug.LogWarning("[JiXieXie] 未找到 PW_Effect_FreeDropChip 预制体,config.freeDropChipPrefab 为空");
if (settlePrefab == null)
Debug.LogWarning("[JiXieXie] 未找到 PW_Effect_Settle_Scorpion 预制体,config.settleScorpionPrefab 为空");
if (gemstones.Length == 0 || cards.Length == 0 || goods.Length == 0)
Debug.LogWarning($"[JiXieXie] 部分图片没找到:宝石={gemstones.Length} 卡牌={cards.Length} 道具={goods.Length}");
// 3. 生成/覆盖配置资产
JiXieXieConfig cfg = AssetDatabase.LoadAssetAtPath<JiXieXieConfig>(ConfigPath);
bool isNew = cfg == null;
if (isNew)
{
cfg = ScriptableObject.CreateInstance<JiXieXieConfig>();
EnsureDir();
AssetDatabase.CreateAsset(cfg, ConfigPath);
}
cfg.freeDropChipPrefab = chipPrefab;
cfg.settleScorpionPrefab = settlePrefab;
cfg.gemstoneSprites = gemstones;
cfg.cardSprites = cards;
cfg.goodsSprites = goods;
cfg.propNames = BuildPropNames(goods);
EditorUtility.SetDirty(cfg);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 4. 选中并提示
Selection.activeObject = cfg;
EditorGUIUtility.PingObject(cfg);
Debug.Log($"[JiXieXie] 配置资产{(isNew ? "已生成" : "已更新")}:{ConfigPath}\n" +
$" 宝石={cfg.gemstoneSprites.Length} 卡牌={cfg.cardSprites.Length} 道具={cfg.goodsSprites.Length}\n" +
" 请把该资产拖到爆奖预制体 JiXieXieEvent 的 config 字段");
}
// ==================== 工具方法 ====================
private static void EnsureDir()
{
if (!AssetDatabase.IsValidFolder(ConfigDir))
{
string parent = "Assets/MyGame/aa/Game/XJMH/Boss";
if (!AssetDatabase.IsValidFolder(parent))
{
Debug.LogError($"[JiXieXie] 目录不存在:{parent}");
return;
}
AssetDatabase.CreateFolder(parent, "JiXieXie");
}
}
/// <summary>按名称模糊查找第一个指定类型资产</summary>
private static T FindAsset<T>(string nameFilter) where T : UnityEngine.Object
{
string[] guids = AssetDatabase.FindAssets(nameFilter + " t:" + typeof(T).Name);
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
T asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset != null) return asset;
}
return null;
}
/// <summary>查找名称带前缀的所有 Sprite,并按文件名末尾数字排序</summary>
private static Sprite[] FindSpritesSorted(string namePrefix)
{
string[] guids = AssetDatabase.FindAssets(namePrefix + " t:Sprite");
List<Sprite> list = new List<Sprite>();
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
if (sprite != null)
list.Add(sprite);
}
// 按文件名末尾数字排序:DropGemstone1 < DropGemstone2 ... < DropGemstone10
list.Sort((a, b) => ExtractTrailingNumber(a.name).CompareTo(ExtractTrailingNumber(b.name)));
return list.ToArray();
}
/// <summary>提取文件名末尾的数字(如 "PW_FreeItem_DropProp1000" → 1000)</summary>
private static int ExtractTrailingNumber(string name)
{
Match m = Regex.Match(name, @"(\d+)$");
return m.Success ? int.Parse(m.Value) : 0;
}
/// <summary>按 goods 数量生成默认道具名(1000+ 猜测为火箭炮碎片,其余叫 道具{ID})</summary>
private static string[] BuildPropNames(Sprite[] goods)
{
if (goods == null || goods.Length == 0) return new string[0];
string[] names = new string[goods.Length];
for (int i = 0; i < goods.Length; i++)
{
if (goods[i] == null) { names[i] = ""; continue; }
int id = ExtractTrailingNumber(goods[i].name);
if (id >= 1000 && id <= 1003)
names[i] = "火箭炮碎片";
else if (id == 0)
names[i] = "金币";
else if (id == 1)
names[i] = "钻石";
else
names[i] = $"道具{id}";
}
return names;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 48db2b478c92fa540800e4b020aae0a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Reflection;
/// <summary>
/// 纯视觉 Prefab 生成器:剥掉所有逻辑脚本,只保留表现组件。
/// 用法:Project 窗口选中一个/多个 prefab → Tools → 生成纯视觉Prefab
/// 输出:同目录 &lt;原名&gt;_Visual.prefab(不覆盖原文件)
///
/// 保留白名单:Unity 内置表现组件、UGUI、TMP、Spine、Animancer、DOTween、URP Volume
/// 删除:其余所有 MonoBehaviour(BY_* 游戏逻辑、LuaBehaviour、FMOD、NGUI 等)
/// Missing 脚本(原逻辑脚本移除后的残留)同时清理。
/// </summary>
public static class StripVisualPrefabTool
{
// 表现类程序集/命名空间白名单
static readonly string[] KeepNamespaces = {
"UnityEngine.UI", "TMPro", "Spine", "Spine.Unity", "Animancer",
"DG.Tweening", "UnityEngine.Rendering", "UnityEngine.U2D",
"UnityEngine", "UnityEngine.Experimental",
};
// 表现类程序集名(.dll)
static readonly string[] KeepAssemblies = {
"UnityEngine.UI", "Unity.TextMeshPro", "Unity.Timeline",
"Spine", "Spine.Unity", "Animancer", "DOTween", "DOTweenPro",
"Unity.RenderPipelines.Core.Runtime", "Unity.RenderPipelines.Universal.Runtime",
"Unity.2D.Animation.Runtime", "Unity.Animator2D",
"Assembly-CSharp", // 视觉辅助:UIGradient/TextSpacing/Outline8/Z1_EffectFollow 等
};
[MenuItem("Tools/生成纯视觉Prefab(剥离逻辑脚本)")]
public static void StripSelected()
{
// 收集目标:选中的 prefab + 选中文件夹下递归的所有 prefab
var prefabPaths = new HashSet<string>();
foreach (var obj in Selection.objects)
{
var path = AssetDatabase.GetAssetPath(obj);
if (string.IsNullOrEmpty(path)) continue;
if (path.EndsWith(".prefab")) prefabPaths.Add(path);
else if (System.IO.Directory.Exists(path))
{
foreach (var f in System.IO.Directory.GetFiles(path, "*.prefab", System.IO.SearchOption.AllDirectories))
prefabPaths.Add(f.Replace('\\', '/'));
}
}
if (prefabPaths.Count == 0) { Debug.LogError("[纯视觉] 未找到 prefab——请选中 prefab 或包含 prefab 的文件夹"); return; }
Debug.Log($"[纯视觉] 共 {prefabPaths.Count} 个 prefab 待处理...");
int done = 0;
foreach (var path in prefabPaths) { StripOne(path); done++; }
Debug.Log($"[纯视觉] 处理完成 {done} 个 prefab(输出为 *_Visual.prefab)");
}
[MenuItem("Tools/生成纯视觉Prefab(剥离逻辑脚本)", true)]
static bool Validate() { return Selection.objects != null && Selection.objects.Length > 0; }
static void StripOne(string path)
{
var root = PrefabUtility.LoadPrefabContents(path);
int removed = 0;
try
{
// 1. 删除非白名单 MonoBehaviour
var mbs = root.GetComponentsInChildren<MonoBehaviour>(true);
var toRemove = new List<Component>();
foreach (var mb in mbs)
{
if (mb == null) continue;
if (!IsVisual(mb)) toRemove.Add(mb);
}
foreach (var c in toRemove) { Object.DestroyImmediate(c, true); removed++; }
// 2. 清理 Missing 脚本组件(serialized 里的 m_Script 已失效的)
foreach (var t in root.GetComponentsInChildren<Transform>(true))
RemoveMissingScripts(t.gameObject);
// 3. 另存
string dir = System.IO.Path.GetDirectoryName(path).Replace('\\', '/');
string name = System.IO.Path.GetFileNameWithoutExtension(path);
string newPath = $"{dir}/{name}_Visual.prefab";
PrefabUtility.SaveAsPrefabAsset(root, newPath);
Debug.Log($"[纯视觉] {name} → {newPath} (剥离 {removed} 个逻辑组件)");
}
finally { PrefabUtility.UnloadPrefabContents(root); }
}
static bool IsVisual(Component c)
{
var t = c.GetType();
// 程序集白名单(精确优先)
var asm = t.Assembly.GetName().Name;
foreach (var k in KeepAssemblies)
if (asm == k) return true;
// 命名空间白名单
var ns = t.Namespace ?? "";
foreach (var k in KeepNamespaces)
if (ns == k || ns.StartsWith(k + ".")) return true;
return false;
}
static void RemoveMissingScripts(GameObject go)
{
var so = new SerializedObject(go);
var comps = so.FindProperty("m_Component");
for (int i = comps.arraySize - 1; i >= 0; i--)
{
var comp = comps.GetArrayElementAtIndex(i).FindPropertyRelative("component");
if (comp != null && comp.objectReferenceValue == null)
comps.DeleteArrayElementAtIndex(i);
}
so.ApplyModifiedPropertiesWithoutUndo();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 08151b804de85fe4095e31e99127a036
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,106 @@
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using System.IO;
using System.Collections.Generic;
/// <summary>
/// 将炮台 Prefab(Turrets/paotai*.prefab)添加到 Addressables,
/// 标签:PT + Default(与 Sprite 一致)。
/// 菜单:Tools → 炮台 → 配置Prefab AA标签
/// </summary>
public class TurretPrefabAALabeler : EditorWindow
{
private const string PREFABS_DIR = "Assets/MyGame/aa/Game/Turrets";
private const string GROUP_NAME = "Default Local Group";
private const string LABEL_PT = "PT";
private const string LABEL_DEFAULT = "Default";
// ============================ 日志开关 ============================
// 启用: Player Settings → Scripting Define Symbols 添加 ENABLE_EDITOR_LOG
// 关闭: 删除该符号。未定义时所有 Log/LogError 调用被编译器自动移除,零开销
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void Log(string msg) { Debug.Log(msg); }
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void LogError(string msg) { Debug.LogError(msg); }
[MenuItem("Tools/炮台/配置Prefab AA标签(PT+Default)", false, 102)]
public static void SetupAddressableLabels()
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
EditorUtility.DisplayDialog("错误", "Addressables 未初始化。", "确定");
return;
}
AddressableAssetGroup defaultGroup = settings.FindGroup(GROUP_NAME)
?? settings.DefaultGroup;
if (defaultGroup == null)
{
EditorUtility.DisplayDialog("错误", $"找不到 Group: {GROUP_NAME}", "确定");
return;
}
EnsureLabel(settings, LABEL_PT);
EnsureLabel(settings, LABEL_DEFAULT);
string absoluteDir = Path.Combine(Application.dataPath, "MyGame/aa/Game/Turrets");
string[] prefabFiles = Directory.GetFiles(absoluteDir, "paotai*.prefab");
Log($"[TurretPrefabAA] 扫描到 {prefabFiles.Length} 个炮台 Prefab");
AssetDatabase.Refresh();
int count = 0;
var errors = new List<string>();
foreach (string absolutePath in prefabFiles)
{
string fileName = Path.GetFileNameWithoutExtension(absolutePath);
string assetPath = absolutePath.Replace(Application.dataPath, "Assets").Replace("\\", "/");
string guid = AssetDatabase.AssetPathToGUID(assetPath);
if (string.IsNullOrEmpty(guid))
{
errors.Add($"{fileName}: GUID 为空");
continue;
}
var entry = settings.FindAssetEntry(guid);
if (entry == null)
entry = settings.CreateOrMoveEntry(guid, defaultGroup);
if (entry == null)
{
errors.Add($"{fileName}: 无法创建 AA 条目");
continue;
}
entry.address = fileName; // paotai11001
entry.SetLabel(LABEL_PT, true, true);
entry.SetLabel(LABEL_DEFAULT, true, true);
count++;
Log($"[TurretPrefabAA] ✓ {fileName}");
}
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, null, true);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
string msg = $"成功: {count} 个";
if (errors.Count > 0) msg += $"\n\n失败 {errors.Count} 个:\n" + string.Join("\n", errors);
EditorUtility.DisplayDialog("Prefab AA标签完成", msg, "确定");
}
private static void EnsureLabel(AddressableAssetSettings settings, string label)
{
if (!settings.GetLabels().Contains(label))
{
settings.AddLabel(label);
Log($"[TurretPrefabAA] 新增标签: {label}");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b034bfcadf0c0946a581ce92b9d1473
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,152 @@
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using System.IO;
using System.Collections.Generic;
/// <summary>
/// 将炮台 Sprite(Sprites/paotai*.png)添加到 Addressables 系统,
/// 标签:PT + Default,地址与 prefabTurretResourcesPath 一致。
/// 菜单:Tools → 炮台 → 配置AA标签(PT+Default)
/// </summary>
public class TurretSpriteAALabeler : EditorWindow
{
private const string SPRITES_DIR = "Assets/MyGame/aa/Game/Turrets/Sprites";
private const string GROUP_NAME = "Default Local Group";
private const string LABEL_PT = "PT";
private const string LABEL_DEFAULT = "Default";
// ============================ 日志开关 ============================
// 启用: Player Settings → Scripting Define Symbols 添加 ENABLE_EDITOR_LOG
// 关闭: 删除该符号。未定义时所有 Log/LogError 调用被编译器自动移除,零开销
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void Log(string msg) { Debug.Log(msg); }
[System.Diagnostics.Conditional("ENABLE_EDITOR_LOG")]
private static void LogError(string msg) { Debug.LogError(msg); }
[MenuItem("Tools/炮台/配置AA标签(PT+Default)", false, 101)]
public static void SetupAddressableLabels()
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
if (settings == null)
{
EditorUtility.DisplayDialog("错误", "Addressables 未初始化,请先配置 Addressable Asset Settings。", "确定");
return;
}
// 找到 Default Local Group
AddressableAssetGroup defaultGroup = settings.FindGroup(GROUP_NAME);
if (defaultGroup == null)
{
defaultGroup = settings.DefaultGroup;
if (defaultGroup == null)
{
EditorUtility.DisplayDialog("错误", $"找不到 Addressable Group: {GROUP_NAME}", "确定");
return;
}
}
// 确保标签存在
EnsureLabel(settings, LABEL_PT);
EnsureLabel(settings, LABEL_DEFAULT);
// ===== 直接扫文件系统,不依赖 AssetDatabase.FindAssets =====
string absoluteSpritesDir = Path.Combine(Application.dataPath, "MyGame/aa/Game/Turrets/Sprites");
if (!Directory.Exists(absoluteSpritesDir))
{
EditorUtility.DisplayDialog("错误", $"目录不存在:\n{absoluteSpritesDir}", "确定");
return;
}
string[] pngFiles = Directory.GetFiles(absoluteSpritesDir, "paotai*.png");
Log($"[TurretSpriteAA] 扫描到 {pngFiles.Length} 个炮台 PNG 文件");
// 强制刷新 AssetDatabase,确保所有 PNG 都被识别
AssetDatabase.Refresh();
int count = 0;
var errors = new List<string>();
foreach (string absolutePath in pngFiles)
{
string fileName = Path.GetFileNameWithoutExtension(absolutePath); // paotai11001
// 绝对路径 → Assets 相对路径
string assetPath = absolutePath.Replace(Application.dataPath, "Assets").Replace("\\", "/");
// 确保 TextureImporter 是 Sprite
var importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
if (importer == null)
{
// 文件可能还没导入,强制导入
Log($"[TurretSpriteAA] 强制导入: {assetPath}");
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);
importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
}
if (importer == null)
{
errors.Add($"{fileName}: TextureImporter 仍为空,跳过");
continue;
}
if (importer.textureType != TextureImporterType.Sprite)
{
importer.textureType = TextureImporterType.Sprite;
importer.spriteImportMode = SpriteImportMode.Single;
importer.SaveAndReimport();
}
// 获取 GUID(刷新后)
string guid = AssetDatabase.AssetPathToGUID(assetPath);
if (string.IsNullOrEmpty(guid))
{
errors.Add($"{fileName}: 无法获取 GUID");
continue;
}
// 创建或获取 Addressable 条目
var entry = settings.FindAssetEntry(guid);
if (entry == null)
{
entry = settings.CreateOrMoveEntry(guid, defaultGroup);
}
if (entry == null)
{
errors.Add($"{fileName}: 无法创建 AA 条目");
continue;
}
// 设置地址 = 文件名(paotai11001),与 prefabTurretResourcesPath 匹配
entry.address = fileName;
// 设置标签:PT + Default
entry.SetLabel(LABEL_PT, true, true);
entry.SetLabel(LABEL_DEFAULT, true, true);
count++;
Log($"[TurretSpriteAA] ✓ {fileName}: address={entry.address}, labels=PT+Default");
}
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, null, true);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
string message = $"成功: {count} 个";
if (errors.Count > 0)
message += $"\n\n失败 {errors.Count} 个:\n" + string.Join("\n", errors);
EditorUtility.DisplayDialog("AA标签配置完成", message, "确定");
}
private static void EnsureLabel(AddressableAssetSettings settings, string label)
{
if (!settings.GetLabels().Contains(label))
{
settings.AddLabel(label);
Log($"[TurretSpriteAA] 新增标签: {label}");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c0b89e620b15a99479add400cc335a45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: