diff --git a/Assets/AddressableAssetsData/Windows/addressables_content_state.bin b/Assets/AddressableAssetsData/Windows/addressables_content_state.bin index 94b090bc3..80693c8f9 100644 Binary files a/Assets/AddressableAssetsData/Windows/addressables_content_state.bin and b/Assets/AddressableAssetsData/Windows/addressables_content_state.bin differ diff --git a/Assets/Art/FengLei/ResLY/dataresources/new_effect/timeline/6004/AnimationClipMismatchChecker.cs b/Assets/Art/FengLei/ResLY/dataresources/new_effect/timeline/6004/AnimationClipMismatchChecker.cs deleted file mode 100644 index 85d39ebfb..000000000 --- a/Assets/Art/FengLei/ResLY/dataresources/new_effect/timeline/6004/AnimationClipMismatchChecker.cs +++ /dev/null @@ -1,2186 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using UnityEditor; -using UnityEngine; - -public class HumanoidBoneMismatchChecker : EditorWindow -{ - // 参考对象(可以是Prefab或Hierarchy中的对象) - private UnityEngine.Object referenceObject; - - // 从参考对象读取的实际骨骼结构(带完整路径) - private List actualBoneFullPaths = new List(); - private List actualBoneNames = new List(); - private Dictionary bonePathToName = new Dictionary(); - - private List clips = new List(); - - private Dictionary> mismatchDetails = new Dictionary>(); - private Dictionary mismatchCountMap = new Dictionary(); - private Dictionary matchPercentageMap = new Dictionary(); - private Dictionary> animationBonesCache = new Dictionary>(); - - // 多条件检测设置 - private bool useMismatchCount = true; - private bool useMatchPercentage = true; - private bool useRequiredBonesList = false; - private bool useMinBoneCount = false; - - private int mismatchCountThreshold = 3; - private float matchPercentageRequired = 80f; - private int minBoneCountRequired = 10; - private List requiredBonesList = new List(); - private string newRequiredBone = ""; - private Vector2 requiredBonesScrollPos; - - // 匹配模式 - private bool useFullPathMatch = true; - - // 判断逻辑 - private bool useAndLogic = true; - - private Vector2 clipListScrollPos; - private Vector2 resultScrollPos; - private Vector2 boneListScrollPos; - - private string rootFolder = "Assets/Animations"; - private string goodSubFolder = "good"; - private string badSubFolder = "bad"; - private bool exportBadClips = true; - - private bool hasRunCheck = false; - private bool showAdvancedInfo = false; - private bool showBoneList = true; - private bool showConditionSettings = true; - private bool showBgSettings = false; - - // 检测结果列表缓存(避免每帧 OnGUI 重复排序构建) - private List<(AnimationClip clip, int totalBones, int mismatch, int matched, float percent, bool isCompatible, bool forcedFailed, string reason)> cachedResultList; - private int cachedTotalClips = -1; - private int cachedCompatibleCount = 0; - - // 大背景图片(可选,拉伸填充整个窗口) - private Texture2D bgImageGlobal; // 静态图片 - private const string BgImagePrefKey = "BoneMismatchChecker_BgImagePath"; - - // GIF动画背景 - private Texture2D[] gifFrames; - private int[] gifDelays; // 每帧延迟(ms) - private int gifTotalDuration; - private bool isGif; - private double gifStartTime; - - // 异步处理 - private bool isProcessing = false; - private float progress = 0f; - private string currentProgressText = ""; - private Task currentTask; - - // 缓存数据用于异步处理 - private struct ClipData - { - public string clipPath; - public string clipName; - public List bonePaths; // 预缓存的骨骼路径 - } - - private List cachedClipData = new List(); - - [MenuItem("Tools/动画骨骼检测工具")] - public static void ShowWindow() - { - var win = GetWindow("动画骨骼检测工具"); - win.LoadBgImage(); - } - Vector2 scrollPos; - private double lastGifRepaintTime; - - private void OnDestroy() - { - EditorApplication.update -= OnEditorUpdate; - ClearGif(); - } - - private void OnEnable() - { - EditorApplication.update -= OnEditorUpdate; - EditorApplication.update += OnEditorUpdate; - } - - private void OnEditorUpdate() - { - if (isGif && gifFrames != null && gifFrames.Length > 1) - { - if (EditorApplication.timeSinceStartup - lastGifRepaintTime > 0.033) // ~30fps - { - Repaint(); - lastGifRepaintTime = EditorApplication.timeSinceStartup; - } - } - } - - private void OnGUI() - { - // 配色方案 - Color headerBg = new Color(0.3f, 0.5f, 0.8f, 1f); // 蓝色板块标题 - Color section1Bg = new Color(0.35f, 0.55f, 0.82f, 0.6f); // 蓝 - 参考对象 - Color section2Bg = new Color(0.3f, 0.65f, 0.35f, 0.6f); // 绿 - 动画列表 - Color section3Bg = new Color(0.75f, 0.65f, 0.25f, 0.6f); // 黄 - 条件设置 - Color section4Bg = new Color(0.65f, 0.35f, 0.7f, 0.6f); // 紫 - 检测按钮/进度 - Color section5Bg = new Color(0.2f, 0.6f, 0.55f, 0.6f); // 青 - 检测结果 - Color section6Bg = new Color(0.7f, 0.45f, 0.2f, 0.6f); // 橙 - 导出设置 - - // 绘制大背景图片(拉伸填满整个窗口) - Texture2D bgToDraw = null; - if (isGif && gifFrames != null && gifFrames.Length > 0) - { - int elapsed = (int)((EditorApplication.timeSinceStartup - gifStartTime) * 1000) % gifTotalDuration; - int accum = 0; - for (int i = 0; i < gifFrames.Length; i++) - { - accum += gifDelays[i]; - if (elapsed < accum) { bgToDraw = gifFrames[i]; break; } - } - if (bgToDraw == null) bgToDraw = gifFrames[0]; - } - else if (bgImageGlobal != null) - { - bgToDraw = bgImageGlobal; - } - if (bgToDraw != null) - { - Rect fullRect = new Rect(0, 0, position.width, position.height); - GUI.DrawTexture(fullRect, bgToDraw, ScaleMode.StretchToFill); - } - - // 整体滚动区域 - scrollPos = EditorGUILayout.BeginScrollView(scrollPos); - - // 大标题 - EditorGUILayout.Space(5); - EditorGUILayout.LabelField("动画骨骼检测工具", new GUIStyle(EditorStyles.boldLabel) - { - fontSize = 18, - alignment = TextAnchor.MiddleCenter, - fontStyle = FontStyle.Bold - }); - GUILayout.Space(6); - - // 界面背景设置(折叠) - showBgSettings = EditorGUILayout.Foldout(showBgSettings, "🎨 界面背景设置", true); - if (showBgSettings) - { - EditorGUILayout.BeginVertical("box"); - EditorGUILayout.HelpBox("拖入背景图片(Texture2D)或选择GIF文件,拉伸填充整个工具窗口。\nGIF支持逐帧动画播放。板块背景色仍保持半透明叠加。", MessageType.Info); - GUILayout.Space(4); - - // 静态图片 - Texture2D newBg = (Texture2D)EditorGUILayout.ObjectField("背景图片", bgImageGlobal, typeof(Texture2D), false); - if (newBg != bgImageGlobal) - { - bgImageGlobal = newBg; - ClearGif(); - SaveBgImage(); - } - - // GIF文件路径 - string gifPath = EditorGUILayout.TextField("GIF文件路径", isGif ? gifSourcePath : ""); - if (GUILayout.Button("选择GIF文件")) - { - string selected = EditorUtility.OpenFilePanel("选择GIF图片", "", "gif"); - if (!string.IsNullOrEmpty(selected)) - { - LoadGifFromFile(selected); - gifPath = selected; - } - } - if (GUILayout.Button("清除GIF")) - { - ClearGif(); - gifPath = ""; - } - - GUILayout.Space(4); - EditorGUILayout.EndVertical(); - GUILayout.Space(6); - } - - // 1. 参考对象 - DrawSectionHeader("1. 参考对象(骨骼来源)", headerBg); - DrawSectionContent(DrawReferenceObjectSectionInner, section1Bg); - GUILayout.Space(6); - - // 2. 动画列表 - DrawSectionHeader("2. 动画列表", headerBg); - DrawSectionContent(DrawClipListInner, section2Bg); - GUILayout.Space(6); - - // 3. 条件设置 - DrawSectionHeader("3. 多条件检测设置", headerBg); - DrawSectionContent(DrawConditionSettingsInner, section3Bg); - GUILayout.Space(6); - - // 4. 检测按钮 / 进度 - DrawSectionHeader("4. 执行检测", headerBg); - DrawSectionContent(DrawCheckSection, section4Bg); - GUILayout.Space(6); - - // 5. 检测结果 - if (hasRunCheck) - { - DrawSectionHeader("5. 检测结果", headerBg); - DrawSectionContent(DrawResultListInner, section5Bg); - } - GUILayout.Space(6); - - EditorGUILayout.EndScrollView(); - - // 6. 导出区域(固定在底部,不随滚动) - GUILayout.Space(8); - DrawSectionHeader("6. 导出设置", headerBg); - DrawSectionContent(DrawExportSection, new Color(0.7f, 0.45f, 0.2f, 0.6f)); - } - - - private void DrawProgressBar() - { - GUILayout.Label("正在检测动画...", EditorStyles.boldLabel); - GUILayout.Space(10); - - EditorGUILayout.HelpBox(currentProgressText, MessageType.Info); - - Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField"); - EditorGUI.ProgressBar(rect, progress, $"{progress * 100:F1}%"); - - GUILayout.Space(10); - - if (GUILayout.Button("取消", GUILayout.Height(30))) - { - isProcessing = false; - if (currentTask != null) - { - currentTask = null; - } - Repaint(); - } - } - - // ----------------------------- - // 刷新动画缓存(修复:刷新界面 + 清空旧结果) - // ----------------------------- - private void RefreshAnimationCache() - { - if (isProcessing) return; - - Debug.Log("开始刷新动画缓存..."); - PreCacheAnimationBones(); - Debug.Log($"动画缓存刷新完成!共缓存 {cachedClipData.Count} 个动画"); - - // 清空旧检测结果 - ClearResults(); - - // 强制刷新界面 - Repaint(); - - EditorUtility.DisplayDialog( - "刷新完成", - $"动画缓存刷新完成!\n共缓存 {cachedClipData.Count} 个动画", - "确定" - ); - } - - // ----------------------------- - // 通用板块 UI 辅助方法 - // ----------------------------- - private void DrawSectionHeader(string title, Color bgColor) - { - EditorGUILayout.Space(3); - var prevBg = GUI.backgroundColor; - GUI.backgroundColor = bgColor; - EditorGUILayout.BeginHorizontal("box"); - GUI.backgroundColor = prevBg; - GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) - { - fontSize = 13, - fontStyle = FontStyle.Bold, - alignment = TextAnchor.MiddleLeft, - padding = new RectOffset(4, 4, 2, 2) - }; - GUIContent iconContent = EditorGUIUtility.IconContent("d_SettingsIcon"); - GUILayout.Label(iconContent, GUILayout.Width(20), GUILayout.Height(20)); - GUILayout.Label(title, headerStyle); - GUILayout.FlexibleSpace(); - EditorGUILayout.EndHorizontal(); - } - - // 缓存生成的背景纹理(纯色) - private static readonly Dictionary bgTextures = new Dictionary(); - - private Texture2D GetOrCreateBgTexture(string key, Color color) - { - if (!bgTextures.TryGetValue(key, out var tex)) - { - tex = new Texture2D(1, 1); - tex.SetPixel(0, 0, color); - tex.Apply(); - bgTextures[key] = tex; - } - return tex; - } - - private void DrawSectionContent(GUIAction drawAction, Color bgColor) - { - GUIStyle bgStyle = new GUIStyle(GUI.skin.box); - bgStyle.normal.background = GetOrCreateBgTexture(bgColor.GetHashCode().ToString(), bgColor); - - EditorGUILayout.BeginVertical(bgStyle); - drawAction(); - EditorGUILayout.EndVertical(); - } - - // GIF源路径(用于持久化) - private string gifSourcePath = ""; - - // 大背景图片持久化 - private void SaveBgImage() - { - if (isGif && !string.IsNullOrEmpty(gifSourcePath)) - { - EditorPrefs.SetString(BgImagePrefKey, "GIF:" + gifSourcePath); - } - else if (bgImageGlobal != null) - { - string path = AssetDatabase.GetAssetPath(bgImageGlobal); - EditorPrefs.SetString(BgImagePrefKey, path); - } - else - { - EditorPrefs.DeleteKey(BgImagePrefKey); - } - } - - private void LoadBgImage() - { - string raw = EditorPrefs.GetString(BgImagePrefKey, ""); - if (string.IsNullOrEmpty(raw)) return; - - if (raw.StartsWith("GIF:")) - { - string gifPath = raw.Substring(4); - if (File.Exists(gifPath)) - LoadGifFromFile(gifPath); - else - EditorPrefs.DeleteKey(BgImagePrefKey); - } - else - { - bgImageGlobal = AssetDatabase.LoadAssetAtPath(raw); - if (bgImageGlobal == null) - EditorPrefs.DeleteKey(BgImagePrefKey); - } - } - - private void ClearGif() - { - if (gifFrames != null) - { - for (int i = 0; i < gifFrames.Length; i++) - { - if (gifFrames[i] != null) DestroyImmediate(gifFrames[i]); - } - gifFrames = null; - gifDelays = null; - } - isGif = false; - gifSourcePath = ""; - gifTotalDuration = 0; - } - - private void LoadGifFromFile(string filePath) - { - ClearGif(); - if (!File.Exists(filePath)) return; - - try - { - byte[] data = File.ReadAllBytes(filePath); - if (data.Length < 13 || data[0] != 'G' || data[1] != 'I' || data[2] != 'F') - { - Debug.LogWarning("不是GIF格式文件"); - return; - } - - int pos = 6; // 跳过 "GIF89a" - int screenWidth = data[pos] | (data[pos + 1] << 8); - int screenHeight = data[pos + 2] | (data[pos + 3] << 8); - pos += 4; - byte packed = data[pos++]; // packed byte - int bgColorIdx = data[pos++]; // background color index - pos++; // pixel aspect ratio - - // 全局颜色表 - Color32[] gct = null; - if ((packed & 0x80) != 0) - { - int gctCount = 1 << ((packed & 0x07) + 1); - gct = ReadColorTable(data, ref pos, gctCount); - } - - var frames = new List(); - var delays = new List(); - - // 画布,初始化为背景色 - Color32[] canvas = new Color32[screenWidth * screenHeight]; - Color32 bgColor = (gct != null && bgColorIdx < gct.Length) ? gct[bgColorIdx] : new Color32(0, 0, 0, 0); - for (int i = 0; i < canvas.Length; i++) canvas[i] = bgColor; - - // GCE状态(紧跟在Image之前) - int gceDisposal = 0; - int gceTransIdx = -1; - int gceDelay = 100; - - // disposal=3 用的前一帧画布副本 - Color32[] savedCanvas = null; - int savedDisposal = 0; - int fLeft = 0, fTop = 0, fWidth = 0, fHeight = 0; - - while (pos < data.Length) - { - int sentinel = data[pos++]; - - if (sentinel == 0x3B) break; // Trailer - - if (sentinel == 0x21) // Extension - { - int label = data[pos++]; - if (label == 0xF9) // GCE - { - pos++; // block size = 4 - byte gcPacked = data[pos++]; - gceDisposal = (gcPacked >> 2) & 0x07; - int delay = data[pos] | (data[pos + 1] << 8); - gceDelay = delay > 0 ? delay * 10 : 100; - gceTransIdx = (gcPacked & 0x01) != 0 ? data[pos + 2] : -1; - pos += 3; - pos++; // block terminator - } - else - { - // 跳过其他扩展(Application Extension, Comment等) - while (pos < data.Length) - { - int sz = data[pos++]; - if (sz == 0) break; - pos += sz; - } - } - } - else if (sentinel == 0x2C) // Image Descriptor - { - delays.Add(gceDelay); - - // 应用上一帧的disposal - if (savedDisposal == 2) // restore to background - { - for (int y = 0; y < fHeight; y++) - for (int x = 0; x < fWidth; x++) - canvas[(fTop + y) * screenWidth + fLeft + x] = bgColor; - } - else if (savedDisposal == 3 && savedCanvas != null) // restore to previous - { - for (int i = 0; i < canvas.Length; i++) - canvas[i] = savedCanvas[i]; - } - - fLeft = data[pos] | (data[pos + 1] << 8); - fTop = data[pos + 2] | (data[pos + 3] << 8); - fWidth = data[pos + 4] | (data[pos + 5] << 8); - fHeight = data[pos + 6] | (data[pos + 7] << 8); - pos += 8; - - byte imgPacked = data[pos++]; - bool hasLct = (imgPacked & 0x80) != 0; - bool interlaced = (imgPacked & 0x40) != 0; - - Color32[] lct = null; - if (hasLct) - { - int lctCount = 1 << ((imgPacked & 0x07) + 1); - lct = ReadColorTable(data, ref pos, lctCount); - } - - Color32[] colorTable = lct ?? gct; - if (colorTable == null) - { - colorTable = new Color32[256]; - for (int i = 0; i < 256; i++) colorTable[i] = new Color32((byte)i, (byte)i, (byte)i, 255); - } - - int minCodeSize = data[pos++]; - int[] indices = DecodeLzw(data, ref pos, minCodeSize, fWidth * fHeight); - - // 渲染到画布 - if (interlaced) - { - int idx = 0; - int[] offsets = { 0, 4, 2, 1 }; - int[] steps = { 8, 8, 4, 2 }; - for (int pass = 0; pass < 4; pass++) - { - for (int y = offsets[pass]; y < fHeight; y += steps[pass]) - { - for (int x = 0; x < fWidth; x++) - { - if (idx < indices.Length) - { - int c = indices[idx++]; - if (c != gceTransIdx && c >= 0 && c < colorTable.Length) - canvas[(fTop + y) * screenWidth + fLeft + x] = colorTable[c]; - } - } - } - } - } - else - { - int idx = 0; - for (int y = 0; y < fHeight; y++) - { - for (int x = 0; x < fWidth; x++) - { - if (idx < indices.Length) - { - int c = indices[idx++]; - if (c != gceTransIdx && c >= 0 && c < colorTable.Length) - canvas[(fTop + y) * screenWidth + fLeft + x] = colorTable[c]; - } - } - } - } - - // 保存画布状态(供下一帧disposal=3使用) - if (gceDisposal == 3) - savedCanvas = (Color32[])canvas.Clone(); - else - savedCanvas = null; - - savedDisposal = gceDisposal; - - var tex = new Texture2D(screenWidth, screenHeight, TextureFormat.RGBA32, false); - tex.SetPixels32(canvas); - tex.Apply(); - frames.Add(tex); - } - else - { - // 未知块,尝试跳过子块 - while (pos < data.Length) - { - int sz = data[pos++]; - if (sz == 0) break; - pos += sz; - } - } - } - - if (frames.Count == 0) - { - Debug.LogWarning("GIF未找到有效帧"); - return; - } - - while (delays.Count < frames.Count) delays.Add(100); - - gifFrames = frames.ToArray(); - gifDelays = delays.ToArray(); - gifTotalDuration = gifDelays.Sum(); - isGif = true; - gifSourcePath = filePath; - gifStartTime = EditorApplication.timeSinceStartup; - SaveBgImage(); - Debug.Log("GIF加载成功: " + frames.Count + "帧, " + screenWidth + "x" + screenHeight); - } - catch (System.Exception e) - { - Debug.LogError("GIF加载失败: " + e.Message + "\n" + e.StackTrace); - ClearGif(); - } - } - - private static Color32[] ReadColorTable(byte[] data, ref int pos, int count) - { - Color32[] table = new Color32[count]; - for (int i = 0; i < count; i++) - { - table[i] = new Color32(data[pos], data[pos + 1], data[pos + 2], 255); - pos += 3; - } - return table; - } - - private static int[] DecodeLzw(byte[] data, ref int pos, int minCodeSize, int pixelCount) - { - int clearCode = 1 << minCodeSize; - int eoiCode = clearCode + 1; - int codeSize = minCodeSize + 1; - int codeMask = (1 << codeSize) - 1; - int nextCode = eoiCode + 1; - - // 读取所有LZW子块到byte数组(不使用StringBuilder) - var bytes = new List(); - while (pos < data.Length) - { - int blockSize = data[pos++]; - if (blockSize == 0) break; - for (int i = 0; i < blockSize && pos < data.Length; i++) - bytes.Add(data[pos++]); - } - - var output = new List(pixelCount); - int bitBuf = 0, bitsLeft = 0, byteIdx = 0; - int prevCode = -1; - - // 初始化字典 - var dict = new System.Collections.Generic.Dictionary(); - for (int i = 0; i < clearCode; i++) - dict[i] = new int[] { i }; - - while (output.Count < pixelCount && byteIdx < bytes.Count) - { - // 填充bit buffer - while (bitsLeft < codeSize && byteIdx < bytes.Count) - { - bitBuf |= bytes[byteIdx++] << bitsLeft; - bitsLeft += 8; - } - if (bitsLeft < codeSize) break; - - int code = bitBuf & codeMask; - bitBuf >>= codeSize; - bitsLeft -= codeSize; - - if (code == clearCode) - { - codeSize = minCodeSize + 1; - codeMask = (1 << codeSize) - 1; - nextCode = eoiCode + 1; - dict.Clear(); - for (int i = 0; i < clearCode; i++) - dict[i] = new int[] { i }; - prevCode = -1; - continue; - } - if (code == eoiCode) break; - - int[] entry; - if (dict.ContainsKey(code)) - entry = dict[code]; - else if (code == nextCode && prevCode >= 0) - { - var prev = dict[prevCode]; - entry = new int[prev.Length + 1]; - prev.CopyTo(entry, 0); - entry[prev.Length] = prev[0]; - } - else - break; // 数据异常,停止 - - output.AddRange(entry); - - if (prevCode >= 0 && nextCode < 4096) - { - var prev = dict[prevCode]; - var newEntry = new int[prev.Length + 1]; - prev.CopyTo(newEntry, 0); - newEntry[prev.Length] = entry[0]; - dict[nextCode] = newEntry; - nextCode++; - if (nextCode > codeMask + 1 && codeSize < 12) - { - codeSize++; - codeMask = (1 << codeSize) - 1; - } - } - prevCode = code; - } - - // 不足像素补0 - while (output.Count < pixelCount) output.Add(0); - return output.ToArray(); - } - - private delegate void GUIAction(); - - // ----------------------------- - // 参考对象选择区域(支持 GameObject + Avatar) - // ----------------------------- - private void DrawReferenceObjectSectionInner() - { - Rect dropArea = GUILayoutUtility.GetRect(0, 50, GUILayout.ExpandWidth(true)); - GUI.Box(dropArea, "拖拽 GameObject 或 Avatar 到这里\n(可从Hierarchy或Project拖入)"); - - HandleReferenceObjectDragDrop(dropArea); - - EditorGUI.BeginChangeCheck(); - UnityEngine.Object newRef = EditorGUILayout.ObjectField("参考对象", referenceObject, typeof(UnityEngine.Object), true); - - if (EditorGUI.EndChangeCheck()) - { - referenceObject = newRef; - - if (referenceObject is GameObject go) - { - LoadGameObjectBones(go); - } - else if (referenceObject is Avatar avatar) - { - LoadAvatarBones(avatar); - } - - ClearResults(); - } - - // 显示加载信息 - if (referenceObject is GameObject go2 && actualBoneFullPaths.Count > 0) - { - string sourceType = PrefabUtility.IsPartOfAnyPrefab(go2) ? "Prefab" : "场景对象"; - EditorGUILayout.HelpBox($"✓ 已加载: {go2.name} ({sourceType})\n骨骼数量: {actualBoneFullPaths.Count}", MessageType.Info); - } - else if (referenceObject is Avatar avatar2) - { - EditorGUILayout.HelpBox($"✓ 已加载 Avatar 骨骼映射\n骨骼数量: {actualBoneFullPaths.Count}", MessageType.Info); - } - else if (referenceObject != null) - { - EditorGUILayout.HelpBox("⚠️ 未能从该对象读取到骨骼结构", MessageType.Warning); - } - else - { - EditorGUILayout.HelpBox("请拖入一个带骨骼结构的 GameObject 或 Avatar", MessageType.Warning); - } - } - - // ----------------------------- - // 拖拽支持(GameObject + Avatar) - // ----------------------------- - private void HandleReferenceObjectDragDrop(Rect dropArea) - { - Event evt = Event.current; - if (!dropArea.Contains(evt.mousePosition)) - return; - - switch (evt.type) - { - case EventType.DragUpdated: - DragAndDrop.visualMode = DragAndDropVisualMode.Copy; - evt.Use(); - break; - - case EventType.DragPerform: - DragAndDrop.AcceptDrag(); - foreach (Object obj in DragAndDrop.objectReferences) - { - if (obj is GameObject go) - { - referenceObject = go; - LoadGameObjectBones(go); - ClearResults(); - break; - } - else if (obj is Avatar avatar) - { - referenceObject = avatar; - LoadAvatarBones(avatar); - ClearResults(); - break; - } - } - evt.Use(); - break; - } - } - - // ----------------------------- - // 清空检测结果 - // ----------------------------- - private void ClearResults() - { - mismatchDetails.Clear(); - mismatchCountMap.Clear(); - matchPercentageMap.Clear(); - animationBonesCache.Clear(); - hasRunCheck = false; - } - // ----------------------------- - // 加载 GameObject 骨骼结构 - // ----------------------------- - private void LoadGameObjectBones(GameObject obj) - { - actualBoneFullPaths.Clear(); - actualBoneNames.Clear(); - bonePathToName.Clear(); - - if (obj == null) return; - - GameObject sourceObject = obj; - GameObject tempInstance = null; - - if (PrefabUtility.IsPartOfPrefabAsset(obj) && !PrefabUtility.IsPartOfNonAssetPrefabInstance(obj)) - { - tempInstance = (GameObject)PrefabUtility.InstantiatePrefab(obj); - sourceObject = tempInstance; - } - - try - { - GetAllBonesWithPath(sourceObject.transform, ""); - Debug.Log($"成功从 GameObject 加载 {actualBoneFullPaths.Count} 个骨骼"); - } - finally - { - if (tempInstance != null) - DestroyImmediate(tempInstance); - } - } - - // ----------------------------- - // 递归读取骨骼路径 - // ----------------------------- - private void GetAllBonesWithPath(Transform trans, string parentPath) - { - string currentPath = string.IsNullOrEmpty(parentPath) ? trans.name : parentPath + "/" + trans.name; - - actualBoneFullPaths.Add(currentPath); - actualBoneNames.Add(trans.name); - bonePathToName[currentPath] = trans.name; - - foreach (Transform child in trans) - GetAllBonesWithPath(child, currentPath); - } - // ----------------------------- - // 显示骨骼层级 - // ----------------------------- - private void ShowBoneHierarchy() - { - string message = "骨骼层级结构:\n\n"; - foreach (string path in actualBoneFullPaths) - { - int depth = path.Count(c => c == '/'); - string indent = new string(' ', depth * 2); - string boneName = path.Split('/').Last(); - message += $"{indent}└─ {boneName}\n"; - } - - EditorUtility.DisplayDialog("骨骼层级结构", message, "确定"); - } - - // ----------------------------- - // 显示骨骼列表 - // ----------------------------- - private void DrawBoneList() - { - showBoneList = EditorGUILayout.Foldout(showBoneList, $"骨骼结构 ({actualBoneFullPaths.Count} 个骨骼,带完整路径)", true); - - if (showBoneList) - { - EditorGUILayout.BeginVertical("box"); - boneListScrollPos = EditorGUILayout.BeginScrollView(boneListScrollPos, GUILayout.Height(150)); - - string search = EditorGUILayout.TextField("搜索:", ""); - - var bonesToShow = actualBoneFullPaths; - if (!string.IsNullOrEmpty(search)) - { - bonesToShow = actualBoneFullPaths.Where(b => b.ToLower().Contains(search.ToLower())).ToList(); - } - - EditorGUILayout.LabelField($"共 {bonesToShow.Count} 个骨骼", EditorStyles.miniLabel); - - foreach (string bonePath in bonesToShow.Take(80)) - { - GUILayout.Label($"• {bonePath}", GUILayout.Width(350)); - } - - if (bonesToShow.Count > 80) - { - GUILayout.Label($"... 还有 {bonesToShow.Count - 80} 个骨骼", EditorStyles.miniLabel); - } - - EditorGUILayout.EndScrollView(); - EditorGUILayout.EndVertical(); - } - } - - // ----------------------------- - // 动画列表 - // ----------------------------- - private void DrawClipListInner() - { - Rect dropArea = GUILayoutUtility.GetRect(0, 60, GUILayout.ExpandWidth(true)); - GUI.Box(dropArea, "拖拽 AnimationClip 到这里\n支持多选拖拽"); - - HandleClipDragDrop(dropArea); - - GUILayout.BeginHorizontal(); - GUILayout.Label($"当前动画列表 ({clips.Count} 个)", EditorStyles.boldLabel); - if (GUILayout.Button("清空列表", GUILayout.Width(80))) - { - clips.Clear(); - ClearResults(); - cachedClipData.Clear(); - } - GUILayout.EndHorizontal(); - - if (clips.Count > 0) - { - clipListScrollPos = EditorGUILayout.BeginScrollView(clipListScrollPos, GUILayout.Height(100)); - - for (int i = 0; i < clips.Count; i++) - { - GUILayout.BeginHorizontal(); - clips[i] = (AnimationClip)EditorGUILayout.ObjectField(clips[i], typeof(AnimationClip), false); - if (GUILayout.Button("✖", GUILayout.Width(25))) - { - var clipToRemove = clips[i]; - clips.RemoveAt(i); - mismatchDetails.Remove(clipToRemove); - mismatchCountMap.Remove(clipToRemove); - matchPercentageMap.Remove(clipToRemove); - animationBonesCache.Remove(clipToRemove); - // 同时从缓存中移除 - cachedClipData.RemoveAll(c => c.clipPath == AssetDatabase.GetAssetPath(clipToRemove)); - i--; - continue; - } - GUILayout.EndHorizontal(); - } - - EditorGUILayout.EndScrollView(); - } - else - { - EditorGUILayout.HelpBox("暂无动画,请拖入 AnimationClip", MessageType.Info); - } - - GUILayout.BeginHorizontal(); - if (GUILayout.Button("从文件夹添加")) - { - string folder = EditorUtility.OpenFolderPanel("选择动画文件夹", "Assets", ""); - if (!string.IsNullOrEmpty(folder)) - { - string relativePath = GetRelativePath(folder); - AddClipsFromFolder(relativePath); - ClearResults(); - } - } - if (GUILayout.Button("添加选中的动画")) - { - foreach (var obj in Selection.objects) - { - if (obj is AnimationClip clip && !clips.Contains(clip)) - clips.Add(clip); - } - ClearResults(); - } - GUILayout.EndHorizontal(); - } - - private void HandleClipDragDrop(Rect dropArea) - { - Event evt = Event.current; - if (!dropArea.Contains(evt.mousePosition)) - return; - - switch (evt.type) - { - case EventType.DragUpdated: - DragAndDrop.visualMode = DragAndDropVisualMode.Copy; - evt.Use(); - break; - - case EventType.DragPerform: - DragAndDrop.AcceptDrag(); - foreach (Object obj in DragAndDrop.objectReferences) - { - if (obj is AnimationClip clip && !clips.Contains(clip)) - clips.Add(clip); - } - ClearResults(); - evt.Use(); - break; - } - } - - private void AddClipsFromFolder(string folderPath) - { - string[] guids = AssetDatabase.FindAssets("t:AnimationClip", new[] { folderPath }); - foreach (string guid in guids) - { - string path = AssetDatabase.GUIDToAssetPath(guid); - AnimationClip clip = AssetDatabase.LoadAssetAtPath(path); - if (clip != null && !clips.Contains(clip)) - clips.Add(clip); - } - Debug.Log($"从 {folderPath} 添加了 {guids.Length} 个动画"); - } - - private string GetRelativePath(string absolutePath) - { - string dataPath = Application.dataPath; - if (absolutePath.StartsWith(dataPath)) - { - return "Assets" + absolutePath.Substring(dataPath.Length); - } - return absolutePath; - } - - // ----------------------------- - // 多条件检测设置 - // ----------------------------- - private void DrawConditionSettingsInner() - { - // 匹配模式选择 - EditorGUILayout.BeginHorizontal(); - GUILayout.Label("骨骼匹配模式:", GUILayout.Width(100)); - if (GUILayout.Toggle(useFullPathMatch, "完整路径匹配(推荐)", GUILayout.Width(150))) - useFullPathMatch = true; - if (GUILayout.Toggle(!useFullPathMatch, "仅名称匹配(宽松)", GUILayout.Width(130))) - useFullPathMatch = false; - EditorGUILayout.EndHorizontal(); - - if (useFullPathMatch) - { - EditorGUILayout.HelpBox("完整路径匹配:动画中的 \"Hips/Spine/Chest\" 必须与模型中的路径完全一致", MessageType.Info); - } - else - { - EditorGUILayout.HelpBox("仅名称匹配:只比较骨骼名称,不比较层级路径(可能误匹配)", MessageType.Warning); - } - - GUILayout.Space(10); - - // 判断逻辑选择 - EditorGUILayout.BeginHorizontal(); - GUILayout.Label("可选条件判断逻辑:", GUILayout.Width(120)); - if (GUILayout.Toggle(useAndLogic, "AND(全部满足才算适配)", GUILayout.Width(180))) - useAndLogic = true; - if (GUILayout.Toggle(!useAndLogic, "OR(任一满足即适配)", GUILayout.Width(150))) - useAndLogic = false; - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.HelpBox( - "⚠️ 注意:必要骨骼和最小骨骼数是强制条件,不满足直接标记为不适配!", - MessageType.Warning); - - GUILayout.Space(10); - - // ========== 强制条件区域 ========== - GUILayout.Label("【强制条件】不满足直接标记不适配", EditorStyles.boldLabel); - - // 条件3:最小骨骼数(强制) - EditorGUILayout.BeginHorizontal(); - useMinBoneCount = EditorGUILayout.Toggle(useMinBoneCount, GUILayout.Width(20)); - GUILayout.Label("强制条件A:最小骨骼数量", GUILayout.Width(150)); - if (useMinBoneCount) - { - minBoneCountRequired = EditorGUILayout.IntSlider(minBoneCountRequired, 1, 100); - GUILayout.Label($"≥ {minBoneCountRequired}", GUILayout.Width(40)); - } - EditorGUILayout.EndHorizontal(); - - if (useMinBoneCount) - { - EditorGUILayout.HelpBox("动画使用的骨骼数量必须达到最小值,防止单根骨骼动画被误判为适配", MessageType.None); - } - - // 条件4:必要骨骼列表(强制) - EditorGUILayout.BeginHorizontal(); - useRequiredBonesList = EditorGUILayout.Toggle(useRequiredBonesList, GUILayout.Width(20)); - GUILayout.Label("强制条件B:必要骨骼列表", GUILayout.Width(150)); - EditorGUILayout.EndHorizontal(); - - if (useRequiredBonesList) - { - EditorGUILayout.BeginVertical("box"); - - EditorGUILayout.HelpBox( - "⚠️ 强制条件:缺失任何必要骨骼,动画将直接标记为不适配!", - MessageType.Error); - - GUILayout.BeginHorizontal(); - newRequiredBone = EditorGUILayout.TextField("添加必要骨骼路径:", newRequiredBone); - if (GUILayout.Button("添加", GUILayout.Width(60)) && !string.IsNullOrEmpty(newRequiredBone)) - { - if (!requiredBonesList.Contains(newRequiredBone)) - requiredBonesList.Add(newRequiredBone); - newRequiredBone = ""; - } - GUILayout.EndHorizontal(); - - // 快捷按钮行 - GUILayout.BeginHorizontal(); - if (GUILayout.Button("从当前骨骼列表选择", GUILayout.Width(150))) - { - ShowBoneSelectionDialog(); - } - if (GUILayout.Button("添加根骨骼作为必要", GUILayout.Width(150))) - { - if (actualBoneFullPaths.Count > 0) - { - string rootBone = actualBoneFullPaths[0]; - if (!requiredBonesList.Contains(rootBone)) - requiredBonesList.Add(rootBone); - Debug.Log($"添加根骨骼: {rootBone}"); - } - } - if (GUILayout.Button("清空列表", GUILayout.Width(100))) - { - requiredBonesList.Clear(); - } - GUILayout.EndHorizontal(); - - if (requiredBonesList.Count > 0) - { - requiredBonesScrollPos = EditorGUILayout.BeginScrollView(requiredBonesScrollPos, GUILayout.Height(100)); - for (int i = 0; i < requiredBonesList.Count; i++) - { - GUILayout.BeginHorizontal(); - GUILayout.Label($"• {requiredBonesList[i]}", GUILayout.Width(350)); - if (GUILayout.Button("删除", GUILayout.Width(50))) - { - requiredBonesList.RemoveAt(i); - i--; - } - GUILayout.EndHorizontal(); - } - EditorGUILayout.EndScrollView(); - } - - EditorGUILayout.EndVertical(); - } - - GUILayout.Space(10); - - // ========== 可选条件区域 ========== - GUILayout.Label("【可选条件】根据AND/OR逻辑判断", EditorStyles.boldLabel); - - // 条件1:缺失数量 - EditorGUILayout.BeginHorizontal(); - useMismatchCount = EditorGUILayout.Toggle(useMismatchCount, GUILayout.Width(20)); - GUILayout.Label("可选条件A:缺失骨骼数量", GUILayout.Width(130)); - if (useMismatchCount) - { - mismatchCountThreshold = EditorGUILayout.IntSlider(mismatchCountThreshold, 0, 50); - GUILayout.Label($"< {mismatchCountThreshold}", GUILayout.Width(40)); - } - EditorGUILayout.EndHorizontal(); - - // 条件2:匹配百分比 - EditorGUILayout.BeginHorizontal(); - useMatchPercentage = EditorGUILayout.Toggle(useMatchPercentage, GUILayout.Width(20)); - GUILayout.Label("可选条件B:骨骼匹配百分比", GUILayout.Width(130)); - if (useMatchPercentage) - { - matchPercentageRequired = EditorGUILayout.Slider(matchPercentageRequired, 0f, 100f); - GUILayout.Label($"≥ {matchPercentageRequired}%", GUILayout.Width(60)); - } - EditorGUILayout.EndHorizontal(); - } - // ----------------------------- - // 加载 Avatar 骨骼结构(Humanoid) - // ----------------------------- - private void LoadAvatarBones(Avatar avatar) - { - actualBoneFullPaths.Clear(); - actualBoneNames.Clear(); - bonePathToName.Clear(); - - if (avatar == null) - return; - - SerializedObject so = new SerializedObject(avatar); - SerializedProperty tosProp = so.FindProperty("m_TOS"); - - if (tosProp == null) - { - Debug.LogError("Avatar 中未找到 m_TOS(完整骨骼路径表)"); - return; - } - - for (int i = 0; i < tosProp.arraySize; i++) - { - SerializedProperty pair = tosProp.GetArrayElementAtIndex(i); - - SerializedProperty keyProp = pair.FindPropertyRelative("first"); - SerializedProperty valueProp = pair.FindPropertyRelative("second"); - - string path = valueProp.stringValue; - - if (!string.IsNullOrEmpty(path)) - { - actualBoneFullPaths.Add(path); - string name = path.Split('/').Last(); - actualBoneNames.Add(name); - bonePathToName[path] = name; - } - } - - Debug.Log($"成功从 Avatar.m_TOS 读取完整骨骼路径,共 {actualBoneFullPaths.Count} 个节点"); - } - - - private void ShowBoneSelectionDialog() - { - GenericMenu menu = new GenericMenu(); - - foreach (string bonePath in actualBoneFullPaths) - { - string path = bonePath; - string cleanPath = RemoveCommonPrefixes(path); - string displayName = $"{path}"; - - if (cleanPath != path.ToLower()) - displayName += $" (匹配名: {cleanPath})"; - - menu.AddItem(new GUIContent(displayName), requiredBonesList.Contains(path), () => { - if (!requiredBonesList.Contains(path)) - requiredBonesList.Add(path); - }); - } - - menu.ShowAsContext(); - } - - // ----------------------------- - // 移除常见的前缀 - // ----------------------------- - private string RemoveCommonPrefixes(string path) - { - string[] prefixes = { "mixamorig:", "uma:", "character:", "armature|", "root|" }; - string result = path.ToLower(); - - foreach (string prefix in prefixes) - { - if (result.StartsWith(prefix)) - { - result = result.Substring(prefix.Length); - break; - } - } - - return result; - } - - // ----------------------------- - // 从动画中提取骨骼路径(必须在主线程调用) - // ----------------------------- - private List GetAnimationBonePaths(AnimationClip clip) - { - if (animationBonesCache.ContainsKey(clip)) - return animationBonesCache[clip]; - - HashSet animationBones = new HashSet(); - - var bindings = AnimationUtility.GetCurveBindings(clip); - - foreach (var binding in bindings) - { - if (binding.type == typeof(Transform) || binding.type == typeof(Animator)) - { - string path = binding.path; - if (string.IsNullOrEmpty(path)) continue; - animationBones.Add(path); - } - } - - var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip); - foreach (var binding in objectBindings) - { - if (binding.type == typeof(Transform) || binding.type == typeof(Animator)) - { - string path = binding.path; - if (string.IsNullOrEmpty(path)) continue; - animationBones.Add(path); - } - } - - var result = animationBones.ToList(); - animationBonesCache[clip] = result; - return result; - } - - // ----------------------------- - // 分析动画骨骼(完整修复版) - // ----------------------------- - private (List mismatchedBones, List matchedBones, int matchedCount, int totalAnimBones, float matchPercentage) - AnalyzeAnimationBonesData( - List animationBonePaths, - List modelBoneFullPaths, - bool useFullPathMatchLocal) - { - List mismatchedBones = new List(); - List matchedBones = new List(); - - // 模型没有骨骼 - if (modelBoneFullPaths.Count == 0) - { - mismatchedBones.Add("错误:参考对象没有骨骼结构"); - return (mismatchedBones, matchedBones, 0, 0, 0f); - } - - // 动画没有骨骼 - if (animationBonePaths.Count == 0) - { - mismatchedBones.Add("警告:动画中没有检测到任何骨骼曲线"); - return (mismatchedBones, matchedBones, 0, 0, 0f); - } - - int totalAnimBones = animationBonePaths.Count; - - // 预处理模型骨骼路径 - HashSet modelPathSet = new HashSet(modelBoneFullPaths.Select(p => p.ToLower())); - Dictionary modelCleanMap = new Dictionary(); - - foreach (string modelPath in modelBoneFullPaths) - { - modelCleanMap[modelPath.ToLower()] = RemoveCommonPrefixes(modelPath.ToLower()); - } - - bool[] matchResults = new bool[totalAnimBones]; - - // ----------------------------- - // 并行匹配动画骨骼 - // ----------------------------- - Parallel.For(0, totalAnimBones, idx => - { - string animPath = animationBonePaths[idx]; - string lowerAnimPath = animPath.ToLower(); - string cleanAnimPath = RemoveCommonPrefixes(lowerAnimPath); - - bool found = false; - - foreach (var kvp in modelCleanMap) - { - string modelFull = kvp.Key; - string modelClean = kvp.Value; - - if (useFullPathMatchLocal) - { - // 完整路径匹配 - if (lowerAnimPath == modelFull || cleanAnimPath == modelClean) - { - found = true; - break; - } - - // 末尾匹配(防止前缀不同) - if (cleanAnimPath.EndsWith("/" + modelClean) || - cleanAnimPath == modelClean || - modelClean.EndsWith("/" + cleanAnimPath)) - { - found = true; - break; - } - } - else - { - // 名称匹配 - string animName = lowerAnimPath.Split('/').Last(); - string modelName = modelFull.Split('/').Last(); - - if (animName == modelName) - { - found = true; - break; - } - } - } - - matchResults[idx] = found; - }); - - // ----------------------------- - // 收集结果 - // ----------------------------- - for (int i = 0; i < totalAnimBones; i++) - { - if (matchResults[i]) - matchedBones.Add(animationBonePaths[i]); - else - mismatchedBones.Add(animationBonePaths[i]); - } - - int matchedCount = matchedBones.Count; - float matchPercentage = totalAnimBones > 0 ? (matchedCount * 100f / totalAnimBones) : 0f; - - return (mismatchedBones, matchedBones, matchedCount, totalAnimBones, matchPercentage); - } - - - // ----------------------------- - // 检查必要骨骼条件(纯数据计算)- 强制条件 - // ----------------------------- - private (bool passed, List missingBones) CheckRequiredBonesConditionData( - List animationBonePaths, - List requiredBones, - bool useFullPathMatchLocal) - { - if (requiredBones.Count == 0) - return (true, new List()); - - // 获取动画中的骨骼信息 - HashSet animPathSet = new HashSet(animationBonePaths.Select(p => p.ToLower())); - HashSet animCleanSet = new HashSet(animationBonePaths.Select(p => RemoveCommonPrefixes(p))); - HashSet animNameSet = new HashSet(animationBonePaths.Select(p => p.Split('/').Last().ToLower())); - - List missingBones = new List(); - - foreach (string required in requiredBones) - { - string lowerRequired = required.ToLower(); - string cleanRequired = RemoveCommonPrefixes(lowerRequired); - string requiredName = lowerRequired.Split('/').Last(); - - bool found = false; - - if (useFullPathMatchLocal) - { - // 1. 完整路径匹配 - if (animPathSet.Contains(lowerRequired)) - { - found = true; - } - // 2. 清理前缀后匹配 - else if (animCleanSet.Contains(cleanRequired)) - { - found = true; - } - // 3. 骨骼名称匹配 - else if (animNameSet.Contains(requiredName)) - { - found = true; - } - // 4. 后缀匹配 - else - { - foreach (string animPath in animationBonePaths) - { - string cleanAnim = RemoveCommonPrefixes(animPath); - if (cleanAnim.EndsWith("/" + cleanRequired) || cleanAnim == cleanRequired) - { - found = true; - break; - } - } - } - } - else - { - found = animNameSet.Contains(requiredName); - } - - if (!found) - { - missingBones.Add(required); - } - } - - return (missingBones.Count == 0, missingBones); - } - - // ----------------------------- - // 动画适配判定(完整修复版) - // ----------------------------- - private (bool isCompatible, string forceReason, string optionalReason) IsClipCompatibleData( - int mismatchCount, - int totalAnimBones, - float matchPercentage, - List animPaths, - List requiredBones, - bool useRequiredBonesList, - bool useMinBoneCount, - bool useMismatchCount, - bool useMatchPercentage, - int mismatchThreshold, - float matchPercentageRequired, - int minBoneCountRequired, - bool useAndLogic, - bool useFullPathMatch) - { - string forceReason = ""; - string optionalReason = ""; - - // ----------------------------- - // 1. 强制条件(不满足直接不适配) - // ----------------------------- - - // A. 最小骨骼数 - if (useMinBoneCount) - { - if (totalAnimBones < minBoneCountRequired) - { - forceReason += $"[强制条件] 动画骨骼数 {totalAnimBones} < 最小要求 {minBoneCountRequired}\n"; - return (false, forceReason, optionalReason); - } - } - - // B. 必要骨骼列表 - if (useRequiredBonesList && requiredBones.Count > 0) - { - foreach (var req in requiredBones) - { - bool found = false; - - foreach (var animBone in animPaths) - { - if (useFullPathMatch) - { - // 完整路径匹配 - if (animBone.ToLower() == req.ToLower()) - { - found = true; - break; - } - } - else - { - // 名称匹配 - string animName = animBone.Split('/').Last().ToLower(); - string reqName = req.Split('/').Last().ToLower(); - if (animName == reqName) - { - found = true; - break; - } - } - } - - if (!found) - { - forceReason += $"[强制条件] 缺少必要骨骼:{req}\n"; - return (false, forceReason, optionalReason); - } - } - } - - // ----------------------------- - // 2. 可选条件(根据 AND / OR 判断) - // ----------------------------- - List optionalResults = new List(); - - // A. 缺失数量 - if (useMismatchCount) - { - bool ok = mismatchCount < mismatchThreshold; - optionalResults.Add(ok); - - if (!ok) - optionalReason += $"[可选条件] 缺失骨骼数 {mismatchCount} ≥ 阈值 {mismatchThreshold}\n"; - } - - // B. 匹配百分比 - if (useMatchPercentage) - { - bool ok = matchPercentage >= matchPercentageRequired; - optionalResults.Add(ok); - - if (!ok) - optionalReason += $"[可选条件] 匹配率 {matchPercentage:F1}% < 要求 {matchPercentageRequired}%\n"; - } - - // 如果没有启用任何可选条件 → 默认通过 - if (optionalResults.Count == 0) - return (true, forceReason, optionalReason); - - // AND 逻辑:全部满足才通过 - if (useAndLogic) - { - bool allOK = optionalResults.All(r => r); - return (allOK, forceReason, optionalReason); - } - else - { - // OR 逻辑:任意满足即可 - bool anyOK = optionalResults.Any(r => r); - return (anyOK, forceReason, optionalReason); - } - } - - - private string GetConditionDescription() - { - List forcedConditions = new List(); - List optionalConditions = new List(); - - if (useMinBoneCount) - forcedConditions.Add($"最小骨骼 ≥ {minBoneCountRequired}"); - if (useRequiredBonesList && requiredBonesList.Count > 0) - forcedConditions.Add($"必要骨骼({requiredBonesList.Count}个)"); - - if (useMismatchCount) - optionalConditions.Add($"缺失 < {mismatchCountThreshold}"); - if (useMatchPercentage) - optionalConditions.Add($"匹配 ≥ {matchPercentageRequired}%"); - - string result = ""; - if (forcedConditions.Count > 0) - result += $"【强制】{string.Join("、", forcedConditions)}"; - - if (optionalConditions.Count > 0) - { - if (result.Length > 0) result += " + "; - result += $"【可选】{string.Join(useAndLogic ? " 且 " : " 或 ", optionalConditions)}"; - } - - if (result.Length == 0) - return "无条件"; - - return result; - } - - private void PreCacheAnimationBones() - { - cachedClipData.Clear(); - - foreach (var clip in clips) - { - if (clip == null) continue; - - string path = AssetDatabase.GetAssetPath(clip); - string name = clip.name; - List bones = GetAnimationBonePaths(clip); // 这里必须在主线程 - - cachedClipData.Add(new ClipData - { - clipPath = path, - clipName = name, - bonePaths = bones - }); - } - } - - - // ----------------------------- - // 异步执行检测 - // ----------------------------- - // 添加临时存储 - private ConcurrentDictionary mismatched, int count, float percentage)> tempResults; - - // ... 中间的代码保持不变 ... - - // 异步检测入口(主线程) - private async void RunCheckAsync() - { - if (isProcessing || clips.Count == 0 || referenceObject == null) - { - EditorUtility.DisplayDialog("提示", "请先选择参考对象并添加动画!", "确定"); - return; - } - - // 确保缓存已构建(主线程中构建) - if (cachedClipData == null || cachedClipData.Count == 0) - { - PreCacheAnimationBones(); // 这里面要在主线程里填好 clipName / bonePaths - } - - isProcessing = true; - hasRunCheck = false; - progress = 0f; - currentProgressText = "准备开始检测..."; - Repaint(); - - mismatchDetails.Clear(); - mismatchCountMap.Clear(); - matchPercentageMap.Clear(); - - // 拷贝一份本地数据,避免并发修改 - var clipDataListLocal = new List(cachedClipData); - var modelBones = new List(actualBoneFullPaths); - var requiredBones = new List(requiredBonesList); - - // 把当前设置也拍一份,避免子线程访问 Editor 状态 - bool useFullPathMatchLocal = useFullPathMatch; - bool useRequiredBonesListLocal = useRequiredBonesList; - bool useMinBoneCountLocal = useMinBoneCount; - bool useMismatchCountLocal = useMismatchCount; - bool useMatchPercentageLocal = useMatchPercentage; - int mismatchCountThresholdLocal = mismatchCountThreshold; - float matchPercentageRequiredLocal = matchPercentageRequired; - int minBoneCountRequiredLocal = minBoneCountRequired; - bool useAndLogicLocal = useAndLogic; - - // 子线程执行纯数据计算 - await Task.Run(() => - { - RunCheckInternal( - clipDataListLocal, - modelBones, - requiredBones, - useFullPathMatchLocal, - useRequiredBonesListLocal, - useMinBoneCountLocal, - useMismatchCountLocal, - useMatchPercentageLocal, - mismatchCountThresholdLocal, - matchPercentageRequiredLocal, - minBoneCountRequiredLocal, - useAndLogicLocal - ); - }); - - // 子线程结束后,把结果应用回主线程 - if (tempResults != null) - { - foreach (var clip in clips) - { - string path = AssetDatabase.GetAssetPath(clip); - if (tempResults.TryGetValue(path, out var data)) - { - var (mismatched, count, percentage) = data; - - mismatchDetails[clip] = mismatched; - mismatchCountMap[clip] = count; - matchPercentageMap[clip] = percentage; - } - } - } - - isProcessing = false; - hasRunCheck = true; - currentProgressText = "检测完成!"; - progress = 1f; - cachedResultList = null; // 清空结果列表缓存,下次 Draw 时重建 - Repaint(); - - EditorUtility.DisplayDialog("完成", "动画检测已完成!", "确定"); - } - - private void RunCheckInternal( - List clipDataListLocal, - List modelBones, - List requiredBones, - bool useFullPathMatchLocal, - bool useRequiredBonesListLocal, - bool useMinBoneCountLocal, - bool useMismatchCountLocal, - bool useMatchPercentageLocal, - int mismatchCountThresholdLocal, - float matchPercentageRequiredLocal, - int minBoneCountRequiredLocal, - bool useAndLogicLocal) - { - int total = clipDataListLocal.Count; - int badCount = 0; - int perfectCount = 0; - int goodCount = 0; - - var results = new ConcurrentDictionary mismatched, int count, float percentage)>(); - - Parallel.For(0, total, i => - { - var clipData = clipDataListLocal[i]; - - List animationBones = clipData.bonePaths; - if (animationBones == null || animationBones.Count == 0) - return; - - // 分析动画骨骼(纯数据) - var (mismatchedBones, matchedBones, matchedCount, totalAnimBones, matchPercentage) = - AnalyzeAnimationBonesData(animationBones, modelBones, useFullPathMatchLocal); - - // 先判定适配,不适配直接跳过,不写入结果字典 - var (isCompatible, forceReason, optionalReason) = IsClipCompatibleData( - mismatchedBones.Count, - totalAnimBones, - matchPercentage, - animationBones, - requiredBones, - useRequiredBonesListLocal, - useMinBoneCountLocal, - useMismatchCountLocal, - useMatchPercentageLocal, - mismatchCountThresholdLocal, - matchPercentageRequiredLocal, - minBoneCountRequiredLocal, - useAndLogicLocal, - useFullPathMatchLocal); - - bool forcedFailed = !string.IsNullOrEmpty(forceReason); - string reason = string.IsNullOrEmpty(forceReason) - ? optionalReason - : forceReason + optionalReason; - - if (!isCompatible) - { - // 不适配:只计数,不写 results,不输出 Debug.Log(避免大量 clip 时卡顿) - Interlocked.Increment(ref badCount); - return; - } - - // 适配的才写入结果字典,不输出逐条日志 - results[clipData.clipPath] = (mismatchedBones, mismatchedBones.Count, matchPercentage); - - if (mismatchedBones.Count == 0 && matchPercentage >= 99.9f) - Interlocked.Increment(ref perfectCount); - else - Interlocked.Increment(ref goodCount); - }); - - tempResults = results; - - currentProgressText = $"检测完成! 完美:{perfectCount} 适配:{goodCount} 不适配:{badCount}"; - // 只输出一条汇总日志,避免大量 Debug.Log 导致卡顿 - Debug.Log($"[骨骼检测] 完成:完美适配 {perfectCount} 个,适配 {goodCount} 个,不适配 {badCount} 个(共 {total} 个)"); -} - - // ----------------------------- - // 执行检测板块 - // ----------------------------- - private void DrawCheckSection() - { - if (!isProcessing) - { - var prevBg = GUI.backgroundColor; - GUI.backgroundColor = new Color(0.4f, 0.7f, 0.4f, 1f); - if (GUILayout.Button("▶ 开始检测动画", GUILayout.Height(36))) - RunCheckAsync(); - GUI.backgroundColor = prevBg; - } - else - { - DrawProgressBar(); - } - } - - // ----------------------------- - // 导出设置板块 - // ----------------------------- - private void DrawExportSection() - { - rootFolder = EditorGUILayout.TextField("导出根目录", rootFolder); - goodSubFolder = EditorGUILayout.TextField("适配子目录", goodSubFolder); - badSubFolder = EditorGUILayout.TextField("不适配子目录", badSubFolder); - exportBadClips = EditorGUILayout.Toggle("导出不适配动画", exportBadClips); - - GUILayout.Space(8); - var prevBg = GUI.backgroundColor; - GUI.backgroundColor = new Color(0.85f, 0.55f, 0.3f, 1f); - if (GUILayout.Button("📦 导出动画", GUILayout.Height(32))) - ExportResults(); - GUI.backgroundColor = prevBg; - } - - private void DrawResultListInner() - { - if (clips.Count == 0) - { - EditorGUILayout.HelpBox("没有动画可显示", MessageType.Warning); - return; - } - - // 只有缓存为空(首次或检测后)才重建结果列表,避免每帧 OnGUI 重复排序 - if (cachedResultList == null || cachedTotalClips != clips.Count) - { - cachedResultList = new List<(AnimationClip, int, int, int, float, bool, bool, string)>(); - cachedTotalClips = clips.Count; - cachedCompatibleCount = 0; - - foreach (var clip in clips) - { - if (!mismatchDetails.ContainsKey(clip)) - continue; - - List animPaths = animationBonesCache.TryGetValue(clip, out var cached) ? cached : new List(); - int totalAnimBones = animPaths.Count; - int mismatch = mismatchCountMap.TryGetValue(clip, out var mis) ? mis : 0; - float percentage = matchPercentageMap.TryGetValue(clip, out var p) ? p : 0f; - int matchedCount = Mathf.Max(0, totalAnimBones - mismatch); - - cachedResultList.Add((clip, totalAnimBones, mismatch, matchedCount, percentage, true, false, "")); - cachedCompatibleCount++; - } - - // 按匹配率降序排序(只排一次) - cachedResultList = cachedResultList.OrderByDescending(r => r.percent).ToList(); - } - - var resultList = cachedResultList; - int totalClips = cachedTotalClips; - int compatibleCount = cachedCompatibleCount; - - // 统计摘要:适配数 / 总数,不适配的已过滤 - int badFiltered = totalClips - compatibleCount; - string summaryMsg = $"显示适配动画:{compatibleCount} 个"; - if (badFiltered > 0) - summaryMsg += $"(已过滤不适配:{badFiltered} 个,可通过Console日志查看详情)"; - EditorGUILayout.HelpBox(summaryMsg, MessageType.Info); - - // 开始画 UI - resultScrollPos = EditorGUILayout.BeginScrollView(resultScrollPos, GUILayout.Height(260)); - - // 表头 - EditorGUILayout.BeginHorizontal(); - GUILayout.Label("状态", GUILayout.Width(40)); - GUILayout.Label("动画名", GUILayout.Width(200)); - GUILayout.Label("总骨骼", GUILayout.Width(60)); - GUILayout.Label("匹配数", GUILayout.Width(70)); - GUILayout.Label("缺失数", GUILayout.Width(60)); - GUILayout.Label("匹配率", GUILayout.Width(70)); - GUILayout.Label("判定结果", GUILayout.Width(120)); - GUILayout.Label("操作", GUILayout.Width(120)); - EditorGUILayout.EndHorizontal(); - - GUILayout.Space(4); - - foreach (var r in resultList) - { - var clip = r.clip; - - // 状态颜色 - string statusIcon; - string statusText; - Color rowColor; - - // 结果列表中全部为适配,只区分"完美适配"和"适配" - if (r.mismatch == 0 && r.percent >= 99.9f) - { - statusIcon = "✓"; - statusText = "完美适配"; - rowColor = new Color(0.8f, 1f, 0.8f); - } - else - { - statusIcon = "•"; - statusText = "适配"; - rowColor = new Color(1f, 1f, 0.9f); - } - - // 横向一行平铺 - var oldColor = GUI.backgroundColor; - GUI.backgroundColor = rowColor; - EditorGUILayout.BeginHorizontal("box"); - GUI.backgroundColor = oldColor; - - GUILayout.Label(statusIcon, GUILayout.Width(40)); - GUILayout.Label(clip.name, GUILayout.Width(200)); - GUILayout.Label(r.totalBones.ToString(), GUILayout.Width(60)); - GUILayout.Label(r.matched.ToString(), GUILayout.Width(70)); - GUILayout.Label(r.mismatch.ToString(), GUILayout.Width(60)); - GUILayout.Label($"{r.percent:F1}%", GUILayout.Width(70)); - GUILayout.Label(statusText, GUILayout.Width(120)); - - // ------------------------- - // 位置按钮(你要的功能) - // ------------------------- - if (GUILayout.Button("定位", GUILayout.Width(50))) - { - Selection.activeObject = clip; - EditorGUIUtility.PingObject(clip); - } - - // ------------------------- - // 详情按钮 - // ------------------------- - if (GUILayout.Button("详情", GUILayout.Width(50))) - { - string msg = $"动画:{clip.name}\n" + - $"总骨骼:{r.totalBones}\n" + - $"匹配骨骼:{r.matched}\n" + - $"缺失骨骼:{r.mismatch}\n" + - $"匹配率:{r.percent:F1}%\n\n"; - - if (!string.IsNullOrEmpty(r.reason)) - msg += $"判定原因:\n{r.reason}\n\n"; - - if (mismatchDetails.TryGetValue(clip, out var list) && list.Count > 0) - { - msg += "缺失骨骼列表(最多显示 50 个):\n"; - foreach (var bone in list.Take(50)) - msg += $" • {bone}\n"; - - if (list.Count > 50) - msg += $" ... 还有 {list.Count - 50} 个未显示\n"; - } - - EditorUtility.DisplayDialog("检测详情", msg, "确定"); - } - - EditorGUILayout.EndHorizontal(); - GUILayout.Space(2); - } - - EditorGUILayout.EndScrollView(); - } - - - - - - private void ShowDetailsPopup(AnimationClip clip, List mismatchedBones, float percentage, int matchedCount, int totalAnimBones, bool isCompatible, string reason, bool forcedFailed) - { - string message = $"━━━━━━━━━━━━━━━━━━━━\n"; - message += $"🎬 动画: {clip.name}\n"; - message += $"━━━━━━━━━━━━━━━━━━━━\n\n"; - - message += $"📦 参考对象: {referenceObject.name}\n"; - message += $"🦴 参考骨骼数: {actualBoneFullPaths.Count}\n"; - message += $"🔍 匹配模式: {(useFullPathMatch ? "完整路径匹配" : "仅名称匹配")}\n\n"; - - message += $"━━━━━━━━━━━━━━━━━━━━\n"; - message += $"📊 检测结果:\n"; - message += $" 动画使用骨骼: {totalAnimBones} 个\n"; - message += $" 匹配骨骼: {matchedCount} 个\n"; - message += $" 匹配度: {percentage:F1}%\n\n"; - - message += $"━━━━━━━━━━━━━━━━━━━━\n"; - message += $"⚙️ 判定条件:\n"; - message += $" {GetConditionDescription()}\n"; - - // 显示各条件状态 - List animPaths = GetAnimationBonePaths(clip); - - // 强制条件状态 - message += $"\n【强制条件】\n"; - if (useMinBoneCount) - { - bool minOk = totalAnimBones >= minBoneCountRequired; - message += $" 最小骨骼数({minBoneCountRequired}): {(minOk ? "✓ " + totalAnimBones : "✗ " + totalAnimBones)}\n"; - } - - if (useRequiredBonesList && requiredBonesList.Count > 0) - { - var (requiredOk, missing) = CheckRequiredBonesConditionData(animPaths, requiredBonesList, useFullPathMatch); - message += $" 必要骨骼: {(requiredOk ? "✓ 满足" : "✗ 不满足")}\n"; - if (!requiredOk && missing.Count > 0) - { - foreach (string m in missing) - { - message += $" • 缺失: {m}\n"; - } - } - } - - // 可选条件状态 - message += $"\n【可选条件】\n"; - if (useMismatchCount) - { - bool mismatchOk = mismatchedBones.Count < mismatchCountThreshold; - message += $" 缺失数量({mismatchedBones.Count}): {(mismatchOk ? "✓ <" + mismatchCountThreshold : "✗ ≥" + mismatchCountThreshold)}\n"; - } - - if (useMatchPercentage) - { - bool percentOk = percentage >= matchPercentageRequired; - message += $" 匹配百分比({percentage:F1}%): {(percentOk ? "✓ ≥" + matchPercentageRequired : "✗ <" + matchPercentageRequired)}\n"; - } - - message += $"\n━━━━━━━━━━━━━━━━━━━━\n"; - message += $"📋 最终判定: {(isCompatible ? "✓ 适配" : "✗ 不适配")}\n"; - - if (!isCompatible && !string.IsNullOrEmpty(reason)) - { - message += $"📌 原因: {reason}\n"; - } - - message += $"\n━━━━━━━━━━━━━━━━━━━━\n"; - - if (mismatchedBones != null && mismatchedBones.Count > 0) - { - var realMissing = mismatchedBones.Where(b => !b.Contains("警告") && !b.Contains("请先")).ToList(); - - if (realMissing.Count > 0) - { - message += $"\n⚠️ 不匹配的骨骼路径 ({realMissing.Count} 个):\n\n"; - message += string.Join("\n", realMissing.Select(b => "• " + b)); - } - } - else - { - message += $"\n✓ 所有动画骨骼路径都匹配!"; - } - - EditorUtility.DisplayDialog("骨骼匹配详情", message, "确定"); - } - - - private void ExportResults() - { - if (!hasRunCheck) - { - EditorUtility.DisplayDialog("提示", "请先执行检测!", "确定"); - return; - } - - string root = rootFolder; - if (!Directory.Exists(root)) - Directory.CreateDirectory(root); - - string goodPath = Path.Combine(root, goodSubFolder); - string badPath = Path.Combine(root, badSubFolder); - - if (!Directory.Exists(goodPath)) - Directory.CreateDirectory(goodPath); - - if (exportBadClips && !Directory.Exists(badPath)) - Directory.CreateDirectory(badPath); - - int goodCount = 0; - int badCount = 0; - - foreach (var clip in clips) - { - if (!mismatchDetails.ContainsKey(clip)) - continue; - - List animPaths = GetAnimationBonePaths(clip); - int totalAnimBones = animPaths.Count; - int mismatch = mismatchCountMap[clip]; - float percentage = matchPercentageMap[clip]; - - // 使用最终判定逻辑 - var (isCompatible, forceReason, optionalReason) = IsClipCompatibleData( - mismatch, - totalAnimBones, - percentage, - animPaths, - requiredBonesList, - useRequiredBonesList, - useMinBoneCount, - useMismatchCount, - useMatchPercentage, - mismatchCountThreshold, - matchPercentageRequired, - minBoneCountRequired, - useAndLogic, - useFullPathMatch - ); - - string src = AssetDatabase.GetAssetPath(clip); - string dst = Path.Combine(isCompatible ? goodPath : badPath, clip.name + ".anim"); - - if (isCompatible) - { - File.Copy(src, dst, true); - goodCount++; - } - else if (exportBadClips) - { - File.Copy(src, dst, true); - badCount++; - } - } - - AssetDatabase.Refresh(); - - EditorUtility.DisplayDialog( - "导出完成", - $"适配动画:{goodCount}\n不适配动画:{badCount}", - "确定" - ); - } - -} \ No newline at end of file diff --git a/Assets/Art/FengLei/ResLY/dataresources/new_effect/timeline/6004/AnimationClipMismatchChecker.cs.meta b/Assets/Art/FengLei/ResLY/dataresources/new_effect/timeline/6004/AnimationClipMismatchChecker.cs.meta deleted file mode 100644 index 164429e03..000000000 --- a/Assets/Art/FengLei/ResLY/dataresources/new_effect/timeline/6004/AnimationClipMismatchChecker.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 65c7b29898458444fb24885d13077424 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs index 503c55994..f946015b4 100644 --- a/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs +++ b/Assets/MyGame/Scripts/Net/WebSocketReponseList.cs @@ -204,6 +204,10 @@ namespace Assets.MyGame.Scripts.Net break; case FrameDataType.freezeFish: EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_FreezeFish, syncGameData); + break; + case FrameDataType.changeTurretState: + EventCenter.Instance.EventTrigger(ConstEventString.SyncGameData_TurretState, syncGameData); + break; default: break; diff --git a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs index ee68f7e7b..64d7d6593 100644 --- a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs +++ b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs @@ -306,7 +306,10 @@ public class GameMainPanel_FishBase : GameMainPanel_Base { // 本地清理锁定 if (currSitData != null) + { currSitData.ClearLock(); + SyncTurretState(TurretState.Idle); + } // 同步给服务器:取消锁定 SyncGameData sgd = new SyncGameData @@ -1083,6 +1086,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base else { currSitData.ClearLock(); + SyncTurretState(TurretState.Idle); SyncLock(null); } } @@ -1136,6 +1140,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base if (monster == null || !monster.gameObject.activeInHierarchy) { currSitData.ClearLock(); // 清除锁定 + SyncTurretState(TurretState.Idle); SyncLock(null); // 同步服务器 // ⭐⭐⭐ 关键:怪死了 → 动画回到 Idle ⭐⭐⭐ @@ -1181,6 +1186,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base else { currSitData.ClearLock(); + SyncTurretState(TurretState.Idle); SyncLock(null); } }