587 lines
21 KiB
C#
587 lines
21 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEngine;
|
||
using UnityEditor;
|
||
using Object = UnityEngine.Object;
|
||
|
||
namespace MindPowerSdk.EditorWindow
|
||
{
|
||
/// <summary>
|
||
/// Unity对象替换工具
|
||
/// 用于搜索并替换场景中包含指定名称的游戏对象
|
||
/// </summary>
|
||
public class ObjectReplacerWindow : UnityEditor.EditorWindow
|
||
{
|
||
[Serializable]
|
||
public class ReplacementConfig
|
||
{
|
||
[Header("搜索设置")]
|
||
public string searchName = "";
|
||
public bool useExactMatch = false;
|
||
|
||
[Header("替换设置")]
|
||
public GameObject replacementPrefab;
|
||
|
||
[Header("保留选项")]
|
||
public bool preservePosition = true;
|
||
public bool preserveRotation = true;
|
||
public bool preserveScale = true;
|
||
public bool preserveComponents = false;
|
||
|
||
[Header("组件过滤")]
|
||
public List<string> componentsToPreserve = new List<string>();
|
||
}
|
||
|
||
#region 私有字段
|
||
private ReplacementConfig config = new ReplacementConfig();
|
||
private Vector2 scrollPosition;
|
||
private List<GameObject> foundObjects = new List<GameObject>();
|
||
private bool showFoundObjects = false;
|
||
private bool showComponentOptions = false;
|
||
|
||
// 配置管理
|
||
private string configName = "新配置";
|
||
private List<ReplacementConfig> savedConfigs = new List<ReplacementConfig>();
|
||
private int selectedConfigIndex = 0;
|
||
private string[] configNames = new string[0];
|
||
#endregion
|
||
|
||
#region Unity生命周期
|
||
[MenuItem("MindPowerSdk/工具/对象替换器")]
|
||
public static void ShowWindow()
|
||
{
|
||
var window = GetWindow<ObjectReplacerWindow>("对象替换工具");
|
||
window.minSize = new Vector2(420, 600);
|
||
window.Show();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
LoadConfigs();
|
||
UpdateConfigNames();
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
SaveConfigs();
|
||
}
|
||
#endregion
|
||
|
||
#region GUI绘制
|
||
private void OnGUI()
|
||
{
|
||
EditorGUILayout.BeginVertical();
|
||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||
|
||
DrawHeader();
|
||
DrawSearchSection();
|
||
DrawReplacementSection();
|
||
DrawOptionsSection();
|
||
DrawActionSection();
|
||
DrawResultsSection();
|
||
DrawConfigSection();
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
private void DrawHeader()
|
||
{
|
||
EditorGUILayout.Space(5);
|
||
var headerStyle = new GUIStyle(EditorStyles.largeLabel)
|
||
{
|
||
fontSize = 18,
|
||
fontStyle = FontStyle.Bold,
|
||
alignment = TextAnchor.MiddleCenter
|
||
};
|
||
EditorGUILayout.LabelField("🔄 Unity对象替换工具", headerStyle);
|
||
EditorGUILayout.LabelField("快速替换场景中的游戏对象", EditorStyles.centeredGreyMiniLabel);
|
||
EditorGUILayout.Space(10);
|
||
}
|
||
|
||
private void DrawSearchSection()
|
||
{
|
||
EditorGUILayout.LabelField("🔍 搜索设置", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
config.searchName = EditorGUILayout.TextField("搜索名称", config.searchName);
|
||
config.useExactMatch = EditorGUILayout.Toggle("精确匹配", config.useExactMatch);
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("🔍 搜索对象"))
|
||
{
|
||
SearchObjects();
|
||
}
|
||
if (GUILayout.Button("🧹 清空结果"))
|
||
{
|
||
ClearResults();
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.EndVertical();
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
private void DrawReplacementSection()
|
||
{
|
||
EditorGUILayout.LabelField("🎯 替换设置", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
config.replacementPrefab = (GameObject)EditorGUILayout.ObjectField(
|
||
"替换预制体", config.replacementPrefab, typeof(GameObject), false);
|
||
|
||
if (config.replacementPrefab == null)
|
||
{
|
||
EditorGUILayout.HelpBox("请选择用于替换的预制体或模型", MessageType.Warning);
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
private void DrawOptionsSection()
|
||
{
|
||
EditorGUILayout.LabelField("⚙️ 保留选项", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
config.preservePosition = EditorGUILayout.Toggle("保留位置", config.preservePosition);
|
||
config.preserveRotation = EditorGUILayout.Toggle("保留旋转", config.preserveRotation);
|
||
config.preserveScale = EditorGUILayout.Toggle("保留缩放", config.preserveScale);
|
||
config.preserveComponents = EditorGUILayout.Toggle("保留组件", config.preserveComponents);
|
||
|
||
if (config.preserveComponents)
|
||
{
|
||
EditorGUILayout.Space(3);
|
||
showComponentOptions = EditorGUILayout.Foldout(showComponentOptions, "组件类型过滤");
|
||
if (showComponentOptions)
|
||
{
|
||
DrawComponentOptions();
|
||
}
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
private void DrawComponentOptions()
|
||
{
|
||
EditorGUILayout.BeginVertical("helpbox");
|
||
EditorGUILayout.LabelField("要保留的组件类型名称 (为空时保留所有组件):", EditorStyles.miniLabel);
|
||
|
||
for (int i = 0; i < config.componentsToPreserve.Count; i++)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
config.componentsToPreserve[i] = EditorGUILayout.TextField(config.componentsToPreserve[i]);
|
||
if (GUILayout.Button("✖", GUILayout.Width(25)))
|
||
{
|
||
config.componentsToPreserve.RemoveAt(i);
|
||
i--;
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
if (GUILayout.Button("➕ 添加组件类型"))
|
||
{
|
||
config.componentsToPreserve.Add("");
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
private void DrawActionSection()
|
||
{
|
||
EditorGUILayout.LabelField("🚀 执行操作", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
bool canReplace = foundObjects.Count > 0 && config.replacementPrefab != null;
|
||
|
||
EditorGUI.BeginDisabledGroup(!canReplace);
|
||
var buttonStyle = new GUIStyle(GUI.skin.button) { fixedHeight = 35 };
|
||
if (GUILayout.Button($"🔄 替换 {foundObjects.Count} 个对象", buttonStyle))
|
||
{
|
||
if (ShowReplaceConfirmation())
|
||
{
|
||
ReplaceObjects();
|
||
}
|
||
}
|
||
EditorGUI.EndDisabledGroup();
|
||
|
||
if (!canReplace)
|
||
{
|
||
string message = foundObjects.Count == 0 ? "请先搜索要替换的对象" : "请选择替换用的预制体";
|
||
EditorGUILayout.HelpBox(message, MessageType.Info);
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
private void DrawResultsSection()
|
||
{
|
||
if (foundObjects.Count > 0)
|
||
{
|
||
showFoundObjects = EditorGUILayout.Foldout(showFoundObjects,
|
||
$"📋 搜索结果 ({foundObjects.Count} 个对象)");
|
||
|
||
if (showFoundObjects)
|
||
{
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
int displayCount = Mathf.Min(foundObjects.Count, 15);
|
||
for (int i = 0; i < displayCount; i++)
|
||
{
|
||
var obj = foundObjects[i];
|
||
if (obj == null) continue;
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
EditorGUILayout.ObjectField(obj, typeof(GameObject), true);
|
||
if (GUILayout.Button("定位", GUILayout.Width(40)))
|
||
{
|
||
Selection.activeGameObject = obj;
|
||
EditorGUIUtility.PingObject(obj);
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
if (foundObjects.Count > displayCount)
|
||
{
|
||
EditorGUILayout.HelpBox($"显示前 {displayCount} 个对象,总共 {foundObjects.Count} 个",
|
||
MessageType.Info);
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
}
|
||
|
||
private void DrawConfigSection()
|
||
{
|
||
EditorGUILayout.LabelField("💾 配置管理", EditorStyles.boldLabel);
|
||
EditorGUILayout.BeginVertical("box");
|
||
|
||
// 保存配置
|
||
EditorGUILayout.BeginHorizontal();
|
||
configName = EditorGUILayout.TextField("配置名称", configName);
|
||
if (GUILayout.Button("保存", GUILayout.Width(50)))
|
||
{
|
||
SaveCurrentConfig();
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
// 加载配置
|
||
if (savedConfigs.Count > 0)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
selectedConfigIndex = EditorGUILayout.Popup("已保存配置", selectedConfigIndex, configNames);
|
||
if (GUILayout.Button("加载", GUILayout.Width(50)))
|
||
{
|
||
LoadConfig(selectedConfigIndex);
|
||
}
|
||
if (GUILayout.Button("删除", GUILayout.Width(50)))
|
||
{
|
||
DeleteConfig(selectedConfigIndex);
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
#endregion
|
||
|
||
#region 核心功能
|
||
private void SearchObjects()
|
||
{
|
||
foundObjects.Clear();
|
||
|
||
if (string.IsNullOrEmpty(config.searchName.Trim()))
|
||
{
|
||
EditorUtility.DisplayDialog("输入错误", "请输入要搜索的对象名称!", "确定");
|
||
return;
|
||
}
|
||
|
||
var allObjects = FindObjectsOfType<GameObject>();
|
||
string searchTerm = config.searchName.Trim();
|
||
|
||
foreach (var obj in allObjects)
|
||
{
|
||
bool isMatch = config.useExactMatch
|
||
? obj.name.Equals(searchTerm, StringComparison.OrdinalIgnoreCase)
|
||
: obj.name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0;
|
||
|
||
if (isMatch)
|
||
{
|
||
foundObjects.Add(obj);
|
||
}
|
||
}
|
||
|
||
showFoundObjects = foundObjects.Count > 0;
|
||
Debug.Log($"搜索完成: 找到 {foundObjects.Count} 个匹配的对象");
|
||
|
||
if (foundObjects.Count == 0)
|
||
{
|
||
EditorUtility.DisplayDialog("搜索结果", "未找到匹配的对象", "确定");
|
||
}
|
||
}
|
||
|
||
private void ClearResults()
|
||
{
|
||
foundObjects.Clear();
|
||
showFoundObjects = false;
|
||
Debug.Log("已清空搜索结果");
|
||
}
|
||
|
||
private bool ShowReplaceConfirmation()
|
||
{
|
||
return EditorUtility.DisplayDialog("确认替换",
|
||
$"确定要替换 {foundObjects.Count} 个对象吗?\n\n⚠️ 此操作支持撤销(Ctrl+Z)",
|
||
"确认替换", "取消");
|
||
}
|
||
|
||
private void ReplaceObjects()
|
||
{
|
||
if (config.replacementPrefab == null || foundObjects.Count == 0) return;
|
||
|
||
Undo.SetCurrentGroupName("批量替换对象");
|
||
int undoGroup = Undo.GetCurrentGroup();
|
||
|
||
int successCount = 0;
|
||
var objectsToReplace = foundObjects.ToArray(); // 复制数组避免迭代中修改
|
||
|
||
foreach (var originalObj in objectsToReplace)
|
||
{
|
||
if (originalObj == null) continue;
|
||
|
||
try
|
||
{
|
||
if (ReplaceObject(originalObj))
|
||
{
|
||
successCount++;
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"替换对象 '{originalObj.name}' 时出错: {e.Message}");
|
||
}
|
||
}
|
||
|
||
Undo.CollapseUndoOperations(undoGroup);
|
||
|
||
Debug.Log($"替换完成: 成功替换 {successCount}/{foundObjects.Count} 个对象");
|
||
ClearResults();
|
||
|
||
EditorUtility.DisplayDialog("替换完成",
|
||
$"成功替换 {successCount} 个对象\n\n💡 可使用 Ctrl+Z 撤销操作", "确定");
|
||
}
|
||
|
||
private bool ReplaceObject(GameObject original)
|
||
{
|
||
// 记录原始变换信息
|
||
Transform originalTransform = original.transform;
|
||
Vector3 position = originalTransform.position;
|
||
Quaternion rotation = originalTransform.rotation;
|
||
Vector3 scale = originalTransform.localScale;
|
||
Transform parent = originalTransform.parent;
|
||
int siblingIndex = originalTransform.GetSiblingIndex();
|
||
|
||
// 获取要保留的组件
|
||
Component[] componentsToKeep = null;
|
||
if (config.preserveComponents)
|
||
{
|
||
componentsToKeep = GetComponentsToPreserve(original);
|
||
}
|
||
|
||
// 创建新对象
|
||
GameObject newObj = PrefabUtility.InstantiatePrefab(config.replacementPrefab) as GameObject;
|
||
if (newObj == null)
|
||
{
|
||
newObj = Instantiate(config.replacementPrefab);
|
||
}
|
||
|
||
// 注册撤销操作
|
||
Undo.RegisterCreatedObjectUndo(newObj, "创建替换对象");
|
||
|
||
// 设置层级和位置
|
||
newObj.transform.SetParent(parent);
|
||
newObj.transform.SetSiblingIndex(siblingIndex);
|
||
|
||
// 应用变换
|
||
if (config.preservePosition) newObj.transform.position = position;
|
||
if (config.preserveRotation) newObj.transform.rotation = rotation;
|
||
if (config.preserveScale) newObj.transform.localScale = scale;
|
||
|
||
// 复制组件
|
||
if (config.preserveComponents && componentsToKeep != null)
|
||
{
|
||
CopyComponents(componentsToKeep, newObj);
|
||
}
|
||
|
||
// 删除原对象
|
||
Undo.DestroyObjectImmediate(original);
|
||
|
||
return true;
|
||
}
|
||
|
||
private Component[] GetComponentsToPreserve(GameObject obj)
|
||
{
|
||
var allComponents = obj.GetComponents<Component>();
|
||
|
||
if (config.componentsToPreserve.Count == 0)
|
||
{
|
||
// 保留所有组件 (除了Transform)
|
||
return allComponents.Where(c => c != null && !(c is Transform)).ToArray();
|
||
}
|
||
|
||
// 只保留指定类型的组件
|
||
var components = new List<Component>();
|
||
foreach (var component in allComponents)
|
||
{
|
||
if (component == null || component is Transform) continue;
|
||
|
||
string componentTypeName = component.GetType().Name;
|
||
bool shouldPreserve = config.componentsToPreserve.Any(typeName =>
|
||
!string.IsNullOrEmpty(typeName) &&
|
||
componentTypeName.IndexOf(typeName.Trim(), StringComparison.OrdinalIgnoreCase) >= 0);
|
||
|
||
if (shouldPreserve)
|
||
{
|
||
components.Add(component);
|
||
}
|
||
}
|
||
|
||
return components.ToArray();
|
||
}
|
||
|
||
private void CopyComponents(Component[] components, GameObject target)
|
||
{
|
||
foreach (var component in components)
|
||
{
|
||
if (component == null) continue;
|
||
|
||
try
|
||
{
|
||
UnityEditorInternal.ComponentUtility.CopyComponent(component);
|
||
UnityEditorInternal.ComponentUtility.PasteComponentAsNew(target);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"复制组件 {component.GetType().Name} 失败: {e.Message}");
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region 配置管理
|
||
private void SaveCurrentConfig()
|
||
{
|
||
if (string.IsNullOrEmpty(configName.Trim()))
|
||
{
|
||
EditorUtility.DisplayDialog("保存失败", "请输入配置名称!", "确定");
|
||
return;
|
||
}
|
||
|
||
var newConfig = JsonUtility.FromJson<ReplacementConfig>(JsonUtility.ToJson(config));
|
||
|
||
// 检查是否已存在
|
||
int existingIndex = savedConfigs.FindIndex(c => c.searchName == configName);
|
||
if (existingIndex >= 0)
|
||
{
|
||
if (EditorUtility.DisplayDialog("配置已存在", "是否覆盖现有配置?", "覆盖", "取消"))
|
||
{
|
||
savedConfigs[existingIndex] = newConfig;
|
||
savedConfigs[existingIndex].searchName = configName;
|
||
}
|
||
else
|
||
{
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
newConfig.searchName = configName;
|
||
savedConfigs.Add(newConfig);
|
||
}
|
||
|
||
SaveConfigs();
|
||
UpdateConfigNames();
|
||
Debug.Log($"配置 '{configName}' 已保存");
|
||
}
|
||
|
||
private void LoadConfig(int index)
|
||
{
|
||
if (index >= 0 && index < savedConfigs.Count)
|
||
{
|
||
var configToLoad = savedConfigs[index];
|
||
config = JsonUtility.FromJson<ReplacementConfig>(JsonUtility.ToJson(configToLoad));
|
||
configName = configToLoad.searchName;
|
||
Debug.Log($"配置 '{configToLoad.searchName}' 已加载");
|
||
}
|
||
}
|
||
|
||
private void DeleteConfig(int index)
|
||
{
|
||
if (index >= 0 && index < savedConfigs.Count)
|
||
{
|
||
string configToDelete = savedConfigs[index].searchName;
|
||
if (EditorUtility.DisplayDialog("确认删除", $"确定要删除配置 '{configToDelete}' 吗?", "删除", "取消"))
|
||
{
|
||
savedConfigs.RemoveAt(index);
|
||
selectedConfigIndex = Mathf.Max(0, selectedConfigIndex - 1);
|
||
SaveConfigs();
|
||
UpdateConfigNames();
|
||
Debug.Log($"配置 '{configToDelete}' 已删除");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateConfigNames()
|
||
{
|
||
configNames = savedConfigs.Select(c => c.searchName ?? "未命名").ToArray();
|
||
selectedConfigIndex = Mathf.Clamp(selectedConfigIndex, 0, Mathf.Max(0, configNames.Length - 1));
|
||
}
|
||
|
||
private void SaveConfigs()
|
||
{
|
||
try
|
||
{
|
||
string json = JsonUtility.ToJson(new ConfigList { configs = savedConfigs }, true);
|
||
EditorPrefs.SetString("ObjectReplacer_SavedConfigs", json);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"保存配置失败: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private void LoadConfigs()
|
||
{
|
||
try
|
||
{
|
||
string json = EditorPrefs.GetString("ObjectReplacer_SavedConfigs", "");
|
||
if (!string.IsNullOrEmpty(json))
|
||
{
|
||
var configList = JsonUtility.FromJson<ConfigList>(json);
|
||
savedConfigs = configList?.configs ?? new List<ReplacementConfig>();
|
||
}
|
||
else
|
||
{
|
||
savedConfigs = new List<ReplacementConfig>();
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogWarning($"加载配置失败: {e.Message}");
|
||
savedConfigs = new List<ReplacementConfig>();
|
||
}
|
||
}
|
||
|
||
[Serializable]
|
||
private class ConfigList
|
||
{
|
||
public List<ReplacementConfig> configs;
|
||
}
|
||
#endregion
|
||
}
|
||
} |