Files
MinFt/Client/Assets/Editor/FishiTool/FishFbxReplace.cs
Liubing\LB 3d8d4d18f3 first commit
先修复一下,错误的场景

删除不必要的钓场资产

修复into场景

update:更新meta文件,修复报错

update:修复资源

修复第一章节建造

修复建造第三章

update:删除多余内容

update:更新README.md

update:还原图标

update:更新README

update:更新配置
2026-04-28 02:17:22 +08:00

786 lines
28 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, string[]> fieldNameMappings = new Dictionary<string, string[]>
{
{"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<ImprovedFishFbxReplacer>("改进版鱼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<string, AnimationMountResult> mountResults = new Dictionary<string, AnimationMountResult>();
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<AnimationClip> 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<AnimatorOverrideController>(controllerPath);
if (overrideController == null)
{
Debug.LogError($"❌ 无法加载Animator Override Controller: {controllerPath}");
return;
}
// 3. 获取当前的Override设置
var currentOverrides = new List<KeyValuePair<AnimationClip, AnimationClip>>();
overrideController.GetOverrides(currentOverrides);
// 4. 创建新的Override映射
var newOverrides = new Dictionary<AnimationClip, AnimationClip>();
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<AnimationClip, AnimationClip> 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<AnimationClip> GetAnimationsFromFbx(string fbxPath)
{
List<AnimationClip> animations = new List<AnimationClip>();
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<string, AnimationMountResult> 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<string> allPrefabs = new List<string>();
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<string, List<string>> fixResults = new Dictionary<string, List<string>>();
foreach (string prefabPath in allPrefabs)
{
string prefabName = Path.GetFileNameWithoutExtension(prefabPath);
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
if (prefabAsset != null)
{
GameObject instance = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
if (instance != null)
{
Fish fishComp = instance.GetComponent<Fish>();
if (fishComp != null)
{
List<string> 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<string> replacedFish = new List<string>();
List<string> notFoundFish = new List<string>();
Dictionary<string, List<string>> prefabMissingLog = new Dictionary<string, List<string>>();
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<GameObject>(prefabPath);
GameObject instance = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
if (instance != null)
{
Fish fishComp = instance.GetComponent<Fish>();
if (fishComp != null)
{
List<string> fixedFields = FixFishComponentImproved(instance.transform, fishComp);
if (fixedFields.Count > 0)
{
if (prefabMissingLog == null)
prefabMissingLog = new Dictionary<string, List<string>>();
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<string> FixFishComponentImproved(Transform root, Fish fish)
{
List<string> fixedFields = new List<string>();
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<Animator>();
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<string> variants = new List<string>();
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<string> allNames = new List<string>();
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<string> names)
{
if (parent == null) return;
names.Add(parent.name);
foreach (Transform child in parent)
{
CollectAllChildNames(child, names);
}
}
private void GenerateFixReport(Dictionary<string, List<string>> 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<string> 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<string> MountedAnimations = new List<string>();
public List<string> LoopSetAnimations = new List<string>();
}