using UnityEngine; using UnityEditor; using UnityEditor.Animations; using System.IO; using System.Collections.Generic; using System.Reflection; using System.Linq; using System.Text; public class ImprovedFishFbxReplacer : EditorWindow { private string sourceFolderPath = @"D:\鱼素材"; private string unityTargetFolder = "Assets/AssetsPackage/art/fishsetting/mesh"; private string animatorControllerFolder = "Assets/AssetsPackage/art/fishsetting/animations"; private string[] prefabSearchPaths = new string[] { "Assets/ABPackage/pkgDownloads/Fish", "Assets/ABPackage/pkgFirstPackage/Fish" }; // 需要设置Loop的动画名称 private string[] loopAnimations = new string[] { "Show01", "Swim01", "Idle01" }; // 字段名称映射规则 private Dictionary fieldNameMappings = new Dictionary { {"Head", new string[] {"Head", "Controller_Head", "head", "HEAD"}}, {"LeftEye", new string[] {"LeftEye", "Left_Eye", "L_Eye", "Eye_L", "left_eye"}}, {"RightEye", new string[] {"RightEye", "Right_Eye", "R_Eye", "Eye_R", "right_eye"}}, {"Fin", new string[] {"Fin", "Controller_fin", "fin", "FIN"}}, {"Tail", new string[] {"Tail", "tail", "TAIL"}}, {"Neck", new string[] {"Neck", "neck", "NECK"}}, {"MouthPos", new string[] {"MouthPos", "Mouth_Pos", "mouth", "Mouth"}}, {"mouthPos", new string[] {"mouthPos", "MouthPos", "Mouth_Pos", "mouth", "Mouth"}}, {"Root", new string[] {"Root", "root", "ROOT"}} }; [MenuItem("Tools/FishTools/FishReplace")] public static void ShowWindow() { GetWindow("改进版鱼FBX替换工具"); } private void OnGUI() { EditorGUILayout.LabelField("🐟 改进版鱼 FBX 替换 + 挂点修复工具", EditorStyles.boldLabel); EditorGUILayout.Space(); EditorGUILayout.LabelField("配置路径", EditorStyles.boldLabel); sourceFolderPath = EditorGUILayout.TextField("外部模型路径:", sourceFolderPath); unityTargetFolder = EditorGUILayout.TextField("Unity 模型路径:", unityTargetFolder); animatorControllerFolder = EditorGUILayout.TextField("Animator Controller路径:", animatorControllerFolder); EditorGUILayout.Space(); EditorGUILayout.LabelField("操作选项", EditorStyles.boldLabel); if (GUILayout.Button("仅修复挂点(不替换模型)", GUILayout.Height(30))) { FixAllFishPrefabs(); } if (GUILayout.Button("替换模型并修复挂点", GUILayout.Height(30))) { ReplaceFishModels(); } if (GUILayout.Button("自动挂载动画,手动设置loop", GUILayout.Height(30))) { AutoMountAnimationsAndSetLoop(); } EditorGUILayout.Space(); EditorGUILayout.HelpBox("新功能:\n• 自动检测FBX中的动画\n• 挂载到Animator Controller\n• 自动设置Loop属性\n• 只处理外部路径下的鱼", MessageType.Info); } private void AutoMountAnimationsAndSetLoop() { if (!Directory.Exists(sourceFolderPath)) { Debug.LogError("❌ 外部路径不存在: " + sourceFolderPath); return; } string[] sourceSubFolders = Directory.GetDirectories(sourceFolderPath); Dictionary mountResults = new Dictionary(); foreach (string fishFolder in sourceSubFolders) { string folderName = Path.GetFileName(fishFolder); Debug.Log($"🎬 开始处理动画: {folderName}"); AnimationMountResult result = ProcessFishAnimations(fishFolder, folderName); if (result.TotalAnimations > 0) { mountResults[folderName] = result; } } AssetDatabase.Refresh(); GenerateAnimationReport(mountResults, "AnimationMountReport.txt"); Debug.Log("🎉 动画挂载完成!"); } private AnimationMountResult ProcessFishAnimations(string fishFolder, string fishName) { AnimationMountResult result = new AnimationMountResult(); // 1. 找到FBX文件 string[] fbxFiles = Directory.GetFiles(fishFolder, "*.fbx"); if (fbxFiles.Length == 0) { Debug.LogWarning($"⚠️ 未找到FBX文件: {fishName}"); return result; } // 2. 找到对应的Animator Controller string controllerPath = GetAnimatorControllerPath(fishName); if (!File.Exists(controllerPath)) { Debug.LogWarning($"⚠️ 未找到Animator Controller: {controllerPath}"); return result; } // 3. 处理每个FBX文件 foreach (string fbxPath in fbxFiles) { string unityFbxPath = GetUnityFbxPath(fbxPath, fishName); if (!string.IsNullOrEmpty(unityFbxPath)) { ProcessSingleFbxAnimations(unityFbxPath, controllerPath, fishName, result); } } return result; } private void ProcessSingleFbxAnimations(string fbxPath, string controllerPath, string fishName, AnimationMountResult result) { // 1. 获取FBX中的所有动画 List fbxAnimations = GetAnimationsFromFbx(fbxPath); result.TotalAnimations += fbxAnimations.Count; if (fbxAnimations.Count == 0) { Debug.Log($"📭 {fishName}: FBX中没有找到动画"); return; } Debug.Log($"🎬 {fishName}: 找到 {fbxAnimations.Count} 个动画"); // 2. 加载Animator Override Controller AnimatorOverrideController overrideController = AssetDatabase.LoadAssetAtPath(controllerPath); if (overrideController == null) { Debug.LogError($"❌ 无法加载Animator Override Controller: {controllerPath}"); return; } // 3. 获取当前的Override设置 var currentOverrides = new List>(); overrideController.GetOverrides(currentOverrides); // 4. 创建新的Override映射 var newOverrides = new Dictionary(); foreach (var pair in currentOverrides) { newOverrides[pair.Key] = pair.Value; // 保持现有的映射 } // 5. 挂载缺失的动画 // 5. 挂载缺失的动画 foreach (var fbxClip in fbxAnimations) { bool mounted = TryMountAnimation(fbxClip, newOverrides, result); if (mounted) { Debug.Log($"✅ 挂载动画: {fbxClip.name}"); } // 6. 记录需要设置Loop的动画,但不立即设置 if (loopAnimations.Contains(fbxClip.name)) { result.LoopSetAnimations.Add(fbxClip.name); Debug.Log($"📝 标记需要设置Loop: {fbxClip.name}"); } } // 7. 先应用Override设置 if (result.MountedAnimations.Count > 0) { overrideController.ApplyOverrides(newOverrides.ToList()); EditorUtility.SetDirty(overrideController); AssetDatabase.SaveAssets(); Debug.Log($"💾 保存Animator Controller: {fishName}"); } // 8. 最后统一设置Loop(在所有挂载完成后) if (result.LoopSetAnimations.Count > 0) { SetFbxAnimationsLoop(fbxPath, result.LoopSetAnimations); } } private bool TryMountAnimation(AnimationClip fbxClip, Dictionary overrides, AnimationMountResult result) { foreach (var kvp in overrides.ToList()) { // 查找名称匹配的槽位(无论是否为空) if (kvp.Key.name == fbxClip.name) { if (kvp.Value == null) { Debug.Log($"🎯 挂载到空槽位: {fbxClip.name}"); } else { Debug.Log($"🔄 替换现有动画: {kvp.Value.name} -> {fbxClip.name}"); } overrides[kvp.Key] = fbxClip; result.MountedAnimations.Add(fbxClip.name); return true; } } Debug.LogWarning($"⚠️ 未找到匹配的槽位: {fbxClip.name}"); return false; } private List GetAnimationsFromFbx(string fbxPath) { List animations = new List(); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(fbxPath); foreach (Object asset in assets) { if (asset is AnimationClip clip && !clip.name.Contains("__preview__")) { animations.Add(clip); } } return animations; } private void SetAnimationClipLoop(AnimationClip clip, bool loop) { try { string assetPath = AssetDatabase.GetAssetPath(clip); ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter; if (importer != null) { Debug.Log($"🔍 处理FBX: {assetPath}"); // 获取现有的动画设置 ModelImporterClipAnimation[] clipAnimations = importer.clipAnimations; Debug.Log($"🔍 当前clipAnimations数量: {clipAnimations?.Length ?? 0}"); // 如果没有clipAnimations,需要先创建 if (clipAnimations == null || clipAnimations.Length == 0) { Debug.Log("🔧 创建新的clipAnimations数组"); clipAnimations = new ModelImporterClipAnimation[1]; clipAnimations[0] = new ModelImporterClipAnimation(); clipAnimations[0].name = clip.name; clipAnimations[0].takeName = clip.name; clipAnimations[0].loopTime = loop; } else { // 查找对应的动画设置 bool found = false; for (int i = 0; i < clipAnimations.Length; i++) { Debug.Log($"🔍 检查动画: {clipAnimations[i].name} vs {clip.name}"); if (clipAnimations[i].name == clip.name) { clipAnimations[i].loopTime = loop; found = true; Debug.Log($"✅ 找到并设置 {clip.name} Loop: {loop}"); break; } } if (!found) { Debug.LogWarning($"⚠️ 未找到匹配的动画设置: {clip.name}"); } } // 应用设置 importer.clipAnimations = clipAnimations; importer.SaveAndReimport(); Debug.Log($"💾 重新导入完成: {assetPath}"); } else { Debug.LogError($"❌ 无法获取ModelImporter: {AssetDatabase.GetAssetPath(clip)}"); } } catch (System.Exception e) { Debug.LogError($"❌ 设置Loop失败 {clip.name}: {e.Message}"); } } private string GetAnimatorControllerPath(string fishName) { // 如果fishName已经以F_开头,就不再添加F_前缀 string controllerName = fishName.StartsWith("F_") ? $"{fishName}.controller" : $"F_{fishName}.controller"; return Path.Combine(animatorControllerFolder, $"{fishName}.overrideController").Replace("\\", "/"); } private string GetUnityFbxPath(string externalFbxPath, string fishName) { string fileName = Path.GetFileName(externalFbxPath); string unityPath = Path.Combine(unityTargetFolder, fishName, fileName).Replace("\\", "/"); if (File.Exists(unityPath)) { return unityPath; } Debug.LogWarning($"⚠️ Unity中未找到FBX文件: {unityPath}"); return null; } private void GenerateAnimationReport(Dictionary results, string fileName) { if (results.Count > 0) { string logPath = Application.dataPath + "/../" + fileName; using (StreamWriter writer = new StreamWriter(logPath)) { writer.WriteLine($"鱼动画挂载报告 - {System.DateTime.Now}"); writer.WriteLine("=" + new string('=', 50)); writer.WriteLine(); foreach (var kv in results) { writer.WriteLine($"【{kv.Key}】"); writer.WriteLine($"总动画数量: {kv.Value.TotalAnimations}"); writer.WriteLine($"挂载动画数量: {kv.Value.MountedAnimations.Count}"); if (kv.Value.MountedAnimations.Count > 0) { writer.WriteLine($"挂载的动画: {string.Join(", ", kv.Value.MountedAnimations)}"); } if (kv.Value.LoopSetAnimations.Count > 0) { writer.WriteLine($"设置Loop的动画: {string.Join(", ", kv.Value.LoopSetAnimations)}"); } writer.WriteLine(); } writer.WriteLine($"总计处理了 {results.Count} 条鱼"); } Debug.Log($"📄 动画挂载报告已生成: {logPath}"); } } // 原有的其他方法保持不变... private void FixAllFishPrefabs() { List allPrefabs = new List(); foreach (string searchPath in prefabSearchPaths) { if (AssetDatabase.IsValidFolder(searchPath)) { string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new string[] { searchPath }); foreach (string guid in prefabGuids) { string path = AssetDatabase.GUIDToAssetPath(guid); allPrefabs.Add(path); } } } Debug.Log($"🔍 找到 {allPrefabs.Count} 个Prefab,开始修复挂点..."); Dictionary> fixResults = new Dictionary>(); foreach (string prefabPath in allPrefabs) { string prefabName = Path.GetFileNameWithoutExtension(prefabPath); GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); if (prefabAsset != null) { GameObject instance = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; if (instance != null) { Fish fishComp = instance.GetComponent(); if (fishComp != null) { List fixedFields = FixFishComponentImproved(instance.transform, fishComp); if (fixedFields.Count > 0) { fixResults[prefabName] = fixedFields; Debug.Log($"🔧 修复挂点:{prefabName} -> {string.Join(", ", fixedFields)}"); // 强制标记为脏数据 EditorUtility.SetDirty(fishComp); EditorUtility.SetDirty(instance); } else { Debug.Log($"✅ 所有挂点完整:{prefabName}"); } PrefabUtility.SaveAsPrefabAsset(instance, prefabPath); } GameObject.DestroyImmediate(instance); } } } GenerateFixReport(fixResults, "FishPrefabFixReport.txt"); AssetDatabase.Refresh(); Debug.Log("🎉 批量修复完成!"); } private void ReplaceFishModels() { if (!Directory.Exists(sourceFolderPath)) { Debug.LogError("❌ 外部路径不存在: " + sourceFolderPath); return; } string[] sourceSubFolders = Directory.GetDirectories(sourceFolderPath); List replacedFish = new List(); List notFoundFish = new List(); Dictionary> prefabMissingLog = new Dictionary>(); foreach (string fishFolder in sourceSubFolders) { string folderName = Path.GetFileName(fishFolder); string unityFolderPath = Path.Combine(unityTargetFolder, folderName).Replace("\\", "/"); if (!AssetDatabase.IsValidFolder(unityFolderPath)) { Debug.LogWarning($"⚠️ 未找到 Unity 中对应目录: {unityFolderPath}"); notFoundFish.Add(folderName); continue; } Debug.Log($"✅ 正在处理: {folderName}"); replacedFish.Add(folderName); // 删除原模型资源 string fullUnityPath = Path.Combine(Application.dataPath, "../", unityFolderPath); if (Directory.Exists(fullUnityPath)) { DirectoryInfo dirInfo = new DirectoryInfo(fullUnityPath); foreach (FileInfo file in dirInfo.GetFiles()) file.Delete(); foreach (DirectoryInfo subDir in dirInfo.GetDirectories()) subDir.Delete(true); } // 拷贝 .fbx 文件 string[] fbxFiles = Directory.GetFiles(fishFolder, "*.fbx"); foreach (string fbxPath in fbxFiles) { string fileName = Path.GetFileName(fbxPath); string destPath = Path.Combine(fullUnityPath, fileName); File.Copy(fbxPath, destPath, true); Debug.Log($"📁 复制 {fileName} 到 {unityFolderPath}"); } // 修复挂点 string prefabPath = FindPrefabByName(folderName); if (!string.IsNullOrEmpty(prefabPath)) { GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); GameObject instance = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; if (instance != null) { Fish fishComp = instance.GetComponent(); if (fishComp != null) { List fixedFields = FixFishComponentImproved(instance.transform, fishComp); if (fixedFields.Count > 0) { if (prefabMissingLog == null) prefabMissingLog = new Dictionary>(); prefabMissingLog[folderName] = fixedFields; Debug.Log($"🔧 修复挂点:{folderName} -> {string.Join(", ", fixedFields)}"); } else { Debug.Log($"✅ 所有挂点完整:{folderName}"); } PrefabUtility.SaveAsPrefabAsset(instance, prefabPath); GameObject.DestroyImmediate(instance); } else { Debug.LogWarning($"❗Prefab {folderName} 没有挂 Fish 脚本!"); } } } else { Debug.LogWarning($"❌ 未找到 prefab: {folderName}"); } } AssetDatabase.Refresh(); GenerateFixReport(prefabMissingLog, "FishReplacementReport.txt"); Debug.Log("🎉 替换完成!"); if (replacedFish.Count > 0) Debug.Log($"🟢 已成功替换:{string.Join(", ", replacedFish)}"); if (notFoundFish.Count > 0) Debug.LogWarning($"🔴 未找到目录:{string.Join(", ", notFoundFish)}"); } private string FindPrefabByName(string folderName) { foreach (string path in prefabSearchPaths) { string potentialPath = Path.Combine(path, folderName + ".prefab").Replace("\\", "/"); if (File.Exists(potentialPath)) return potentialPath; } return null; } private Transform FindChildRecursiveImproved(Transform parent, string[] possibleNames) { foreach (string name in possibleNames) { Transform exactMatch = FindExactMatch(parent, name); if (exactMatch != null) return exactMatch; } foreach (string name in possibleNames) { Transform containsMatch = FindContainsMatch(parent, name); if (containsMatch != null) return containsMatch; } return null; } private Transform FindExactMatch(Transform parent, string name) { if (parent.name.Equals(name, System.StringComparison.OrdinalIgnoreCase)) return parent; foreach (Transform child in parent) { Transform result = FindExactMatch(child, name); if (result != null) return result; } return null; } private Transform FindContainsMatch(Transform parent, string name) { if (parent.name.ToLower().Contains(name.ToLower())) return parent; foreach (Transform child in parent) { Transform result = FindContainsMatch(child, name); if (result != null) return result; } return null; } private bool IsValidReference(Object obj) { return obj != null && obj; } private List FixFishComponentImproved(Transform root, Fish fish) { List fixedFields = new List(); var type = typeof(Fish); var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); string rootName = (root != null) ? root.name : "Unknown"; Debug.Log($"🔍 开始修复 {rootName} 的挂点..."); foreach (var field in fields) { if (field.FieldType == typeof(Transform)) { var value = field.GetValue(fish); Transform transformValue = value as Transform; if (!IsValidReference(transformValue)) { string fieldName = field.Name; Debug.Log($"🔧 尝试修复字段: {fieldName} (Missing Reference)"); string[] possibleNames = GetPossibleNames(fieldName); Transform found = FindChildRecursiveImproved(root, possibleNames); if (found != null) { field.SetValue(fish, found); fixedFields.Add(fieldName); Debug.Log($"✅ 成功修复 {fieldName} -> {found.name}"); } else { Debug.LogWarning($"❌ 无法找到字段 {fieldName} 对应的节点,尝试的名称: {string.Join(", ", possibleNames)}"); LogAllChildNames(root, fieldName); } } else { Debug.Log($"✅ 字段 {field.Name} 引用正常: {transformValue.name}"); } } else if (field.FieldType == typeof(Animator)) { var value = field.GetValue(fish); Animator animatorValue = value as Animator; if (!IsValidReference(animatorValue)) { string fieldName = field.Name; Debug.Log($"🔧 尝试修复Animator字段: {fieldName} (Missing Reference)"); string[] possibleNames = GetPossibleNames(fieldName); Transform found = FindChildRecursiveImproved(root, possibleNames); if (found != null) { Animator animator = found.GetComponent(); if (animator != null) { field.SetValue(fish, animator); fixedFields.Add(fieldName); Debug.Log($"✅ 成功修复Animator {fieldName} -> {found.name}"); } else { Debug.LogWarning($"❌ 找到节点 {found.name} 但没有Animator组件"); } } else { Debug.LogWarning($"❌ 无法找到Animator字段 {fieldName} 对应的节点"); } } else { Debug.Log($"✅ Animator字段 {field.Name} 引用正常: {animatorValue.gameObject.name}"); } } } return fixedFields; } private string[] GetPossibleNames(string fieldName) { if (fieldNameMappings.ContainsKey(fieldName)) { return fieldNameMappings[fieldName]; } List variants = new List(); variants.Add(fieldName); variants.Add(fieldName.ToLower()); variants.Add(fieldName.ToUpper()); variants.Add("Controller_" + fieldName); variants.Add("B_" + fieldName); variants.Add(fieldName + "_Bone"); return variants.ToArray(); } private void LogAllChildNames(Transform root, string searchingFor) { if (root == null) { Debug.LogWarning($"🔍 无法为字段 {searchingFor} 搜索节点:root为null"); return; } List allNames = new List(); CollectAllChildNames(root, allNames); Debug.Log($"🔍 为字段 {searchingFor} 搜索时,发现以下所有子节点:"); foreach (string name in allNames.Take(20)) { Debug.Log($" - {name}"); } if (allNames.Count > 20) { Debug.Log($" ... 还有 {allNames.Count - 20} 个节点"); } } private void CollectAllChildNames(Transform parent, List names) { if (parent == null) return; names.Add(parent.name); foreach (Transform child in parent) { CollectAllChildNames(child, names); } } private void GenerateFixReport(Dictionary> results, string fileName) { if (results.Count > 0) { string logPath = Application.dataPath + "/../" + fileName; using (StreamWriter writer = new StreamWriter(logPath)) { writer.WriteLine($"鱼Prefab挂点修复报告 - {System.DateTime.Now}"); writer.WriteLine("=" + new string('=', 50)); writer.WriteLine(); foreach (var kv in results) { writer.WriteLine($"【{kv.Key}】"); writer.WriteLine($"修复字段数量: {kv.Value.Count}"); writer.WriteLine($"修复字段: {string.Join(", ", kv.Value)}"); writer.WriteLine(); } writer.WriteLine($"总计修复了 {results.Count} 个Prefab"); } Debug.Log($"📄 修复报告已生成: {logPath}"); } } private void SetFbxAnimationsLoop(string fbxPath, List animationNames) { Debug.Log($"📝 需要手动设置Loop的动画: {string.Join(", ", animationNames)}"); Debug.Log($"📁 FBX文件路径: {fbxPath}"); Debug.Log($"💡 请手动在FBX的Import Settings -> Animations中勾选这些动画的Loop Time"); // 不进行任何自动设置,避免破坏FBX } } // 这是类的结束括号 // 动画挂载结果数据结构 public class AnimationMountResult { public int TotalAnimations = 0; public List MountedAnimations = new List(); public List LoopSetAnimations = new List(); }