特效处理
This commit is contained in:
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/// 输出:同目录 <原名>_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:
|
||||
Reference in New Issue
Block a user