1. 编辑器运行代码的核心原理
在Unity3D开发中,让编辑器直接运行代码是一个强大的功能,它允许开发者在编辑模式下就能看到脚本效果,而不需要每次都进入Play模式。这个功能的核心在于Unity的编辑器扩展系统,特别是ExecuteInEditMode和EditorWindow这两个关键特性。
ExecuteInEditMode属性可以让MonoBehaviour脚本在编辑模式下执行,而EditorWindow则允许创建自定义的编辑器界面。当这两个特性结合使用时,就能实现编辑器直接运行代码的效果。
重要提示:在编辑模式下运行的代码要特别注意性能问题,避免执行过于频繁的更新操作,否则会导致编辑器卡顿。
2. 实现编辑器运行代码的完整步骤
2.1 基础脚本设置
首先创建一个基础的C#脚本,添加ExecuteInEditMode属性:
using UnityEngine; [ExecuteInEditMode] public class EditorCodeRunner : MonoBehaviour { public string message = "Hello Editor!"; void Update() { if (!Application.isPlaying) { Debug.Log(message + " at " + Time.time); } } }这个脚本会在编辑模式下每帧输出一条日志信息。Application.isPlaying检查确保代码只在编辑模式下运行,而不在游戏运行时重复执行。
2.2 创建自定义编辑器窗口
为了更方便地控制代码执行,我们需要创建一个自定义的编辑器窗口:
using UnityEditor; using UnityEngine; public class CodeRunnerWindow : EditorWindow { private string codeToRun = "Debug.Log(\"Code executed in editor!\");"; private bool autoRefresh = false; private float lastExecutionTime; [MenuItem("Window/Code Runner")] public static void ShowWindow() { GetWindow<CodeRunnerWindow>("Code Runner"); } void OnGUI() { GUILayout.Label("Code Execution", EditorStyles.boldLabel); codeToRun = EditorGUILayout.TextArea(codeToRun, GUILayout.Height(100)); autoRefresh = EditorGUILayout.Toggle("Auto Refresh", autoRefresh); if (GUILayout.Button("Execute") || (autoRefresh && Time.realtimeSinceStartup > lastExecutionTime + 1f)) { ExecuteCode(); lastExecutionTime = Time.realtimeSinceStartup; } } void ExecuteCode() { // 这里可以添加更复杂的代码执行逻辑 Debug.Log("Executing code in editor..."); // 实际应用中,这里可以集成编译器来执行输入的代码 } }3. 高级功能实现
3.1 动态编译执行代码
要让编辑器能够真正执行任意代码,需要用到C#的编译功能。以下是实现动态编译的关键代码:
using System.CodeDom.Compiler; using Microsoft.CSharp; public static bool CompileAndExecute(string code) { CSharpCodeProvider provider = new CSharpCodeProvider(); CompilerParameters parameters = new CompilerParameters(); // 添加必要的程序集引用 parameters.ReferencedAssemblies.Add("System.dll"); parameters.ReferencedAssemblies.Add("UnityEngine.dll"); parameters.ReferencedAssemblies.Add("UnityEditor.dll"); // 编译代码 CompilerResults results = provider.CompileAssemblyFromSource(parameters, code); if (results.Errors.HasErrors) { foreach (CompilerError error in results.Errors) { Debug.LogError(error.ErrorText); } return false; } // 执行编译后的代码 System.Reflection.Assembly assembly = results.CompiledAssembly; // 这里可以添加调用特定方法的逻辑 return true; }3.2 编辑器工具集成
为了提升使用体验,可以将代码执行功能集成到Unity的Inspector面板中:
using UnityEditor; using UnityEngine; [CustomEditor(typeof(EditorCodeRunner))] public class EditorCodeRunnerEditor : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); EditorCodeRunner runner = (EditorCodeRunner)target; if (GUILayout.Button("Execute Now")) { runner.Update(); } EditorGUILayout.HelpBox("This script runs in edit mode. Use it for editor utilities.", MessageType.Info); } }4. 实际应用场景与优化技巧
4.1 常见应用场景
- 快速原型验证:在编辑器模式下测试算法或逻辑,无需进入Play模式
- 批量处理资源:编写编辑器脚本批量修改场景中的对象
- 自定义工具开发:为团队创建专用的开发工具
- 数据可视化:在编辑器中实时显示游戏数据
4.2 性能优化建议
- 限制执行频率:避免在Update中执行耗时操作,使用EditorApplication.update事件替代
- 缓存计算结果:对于不变的数据,只计算一次并缓存
- 提供手动刷新选项:让用户决定何时执行代码
- 使用协程处理长任务:防止编辑器卡死
IEnumerator LongRunningTaskInEditor() { for (int i = 0; i < 100; i++) { // 执行任务的一部分 yield return null; // 让编辑器有机会更新 } }4.3 调试技巧
- 使用
Debug.Log输出信息到控制台 - 在复杂编辑器脚本中添加
try-catch块捕获异常 - 使用
EditorUtility.DisplayProgressBar显示长时间操作的进度 - 通过
AssetDatabase.Refresh()确保资源更改被正确识别
5. 安全注意事项与最佳实践
在编辑器模式下运行代码虽然强大,但也需要注意以下安全事项:
- 代码注入风险:如果允许执行任意代码,要确保输入经过验证
- 数据备份:执行可能修改场景或资源的脚本前,先备份项目
- 权限控制:团队项目中,限制关键操作的执行权限
- 错误处理:提供清晰的错误信息,帮助用户理解问题
专业建议:对于生产环境,考虑将编辑器工具打包为单独的DLL,限制可执行的操作范围,提高安全性。
6. 扩展思路与进阶功能
6.1 集成外部编译器
对于更复杂的场景,可以集成Roslyn编译器提供更强大的代码分析功能:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; public static SyntaxTree ParseCode(string code) { return CSharpSyntaxTree.ParseText(code); }6.2 创建交互式REPL环境
实现一个读取-求值-输出循环(REPL)可以让代码测试更加高效:
public class EditorREPL { private List<string> history = new List<string>(); private StringBuilder currentInput = new StringBuilder(); public void ProcessInput(string input) { history.Add(input); currentInput.AppendLine(input); if (input.EndsWith(";") || input.EndsWith("}")) { string code = currentInput.ToString(); currentInput.Clear(); CompileAndExecute(code); } } }6.3 可视化脚本编辑
结合Unity的UI系统,可以创建节点式的可视化脚本编辑器:
public class VisualScriptNode { public Rect rect; public string title; public List<VisualScriptNode> connections = new List<VisualScriptNode>(); public void Draw() { GUI.Box(rect, title); // 绘制连接线等 } }7. 常见问题解决方案
7.1 编辑器卡顿问题
问题现象:编辑器在运行代码时变得卡顿或无响应
解决方案:
- 检查代码中是否有死循环
- 减少每帧执行的操作量
- 使用
EditorApplication.delayCall将任务分散到多帧
void LongOperation() { // 第一阶段工作 EditorApplication.delayCall += () => { // 第二阶段工作 }; }7.2 脚本编译错误
问题现象:动态编译的代码报错
解决方案:
- 提供清晰的错误信息展示
- 实现语法高亮和实时检查
- 限制可用的API范围
7.3 跨平台兼容性问题
问题现象:编辑器脚本在不同操作系统上表现不一致
解决方案:
- 避免使用平台特定的路径分隔符
- 使用Unity提供的API处理文件路径
- 在关键操作前检查平台条件
if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows特定处理 }8. 性能监控与优化
为了确保编辑器扩展不会影响开发效率,建议添加性能监控功能:
using System.Diagnostics; public class EditorProfiler { private static Stopwatch sw = new Stopwatch(); public static void StartMeasure() { sw.Restart(); } public static void EndMeasure(string operationName) { sw.Stop(); Debug.Log($"{operationName} took {sw.ElapsedMilliseconds}ms"); } }使用示例:
EditorProfiler.StartMeasure(); // 执行需要监控的代码 EditorProfiler.EndMeasure("Code Execution");9. 实际案例:批量重命名工具
下面是一个完整的编辑器工具示例,展示如何批量重命名场景中的对象:
using UnityEditor; using UnityEngine; using System.Text.RegularExpressions; public class BatchRenamer : EditorWindow { private string searchPattern = ""; private string replacePattern = ""; private bool includeInactive = false; [MenuItem("Tools/Batch Renamer")] public static void ShowWindow() { GetWindow<BatchRenamer>("Batch Renamer"); } void OnGUI() { GUILayout.Label("Batch Rename Settings", EditorStyles.boldLabel); searchPattern = EditorGUILayout.TextField("Search Pattern", searchPattern); replacePattern = EditorGUILayout.TextField("Replace With", replacePattern); includeInactive = EditorGUILayout.Toggle("Include Inactive", includeInactive); if (GUILayout.Button("Rename Selected Objects")) { RenameObjects(); } } void RenameObjects() { if (Selection.gameObjects.Length == 0) { EditorUtility.DisplayDialog("No Selection", "Please select some objects first.", "OK"); return; } Undo.RecordObjects(Selection.gameObjects, "Batch Rename"); foreach (GameObject go in Selection.gameObjects) { if (!includeInactive && !go.activeInHierarchy) continue; string newName = Regex.Replace(go.name, searchPattern, replacePattern); if (newName != go.name) { go.name = newName; EditorUtility.SetDirty(go); } } AssetDatabase.SaveAssets(); } }10. 编辑器脚本调试技巧
调试编辑器脚本有时比普通游戏脚本更具挑战性,以下是一些实用技巧:
使用Visual Studio附加调试:
- 在VS中选择"附加到Unity"时,确保勾选"Editor"实例
- 设置断点后,在Unity编辑器中触发相应操作
日志输出策略:
- 使用
Debug.Log输出关键变量值 - 为不同类型的日志使用不同的颜色:
Debug.Log("<color=green>Success:</color> Operation completed");
- 使用
编辑器暂停功能:
- 在关键位置插入
Debug.Break()暂停编辑器执行 - 使用
EditorApplication.isPaused以编程方式控制暂停状态
- 在关键位置插入
自定义调试面板:
public class DebugPanel : EditorWindow { private Vector2 scrollPos; [MenuItem("Window/Debug Panel")] static void Init() { GetWindow<DebugPanel>("Debug"); } void OnGUI() { scrollPos = EditorGUILayout.BeginScrollView(scrollPos); // 显示调试信息 EditorGUILayout.EndScrollView(); } }性能分析标记:
using Unity.Profiling; static readonly ProfilerMarker marker = new ProfilerMarker("MyEditorTool"); void PerformOperation() { using (marker.Auto()) { // 被分析的代码 } }
11. 编辑器UI最佳实践
创建用户友好的编辑器界面需要注意以下要点:
布局组织:
- 使用
EditorGUILayout.BeginVertical()和EndVertical()创建逻辑分组 - 合理使用空格和分隔线提高可读性
- 使用
响应式设计:
EditorGUIUtility.labelWidth = position.width * 0.3f;自定义样式:
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, normal = { textColor = Color.blue } };交互反馈:
- 操作成功后显示确认对话框
- 长时间操作时显示进度条
- 输入无效时高亮显示问题字段
预设保存:
private const string PREFS_KEY = "MyEditorToolSettings"; void SaveSettings() { string json = JsonUtility.ToJson(this); EditorPrefs.SetString(PREFS_KEY, json); } void LoadSettings() { if (EditorPrefs.HasKey(PREFS_KEY)) { JsonUtility.FromJsonOverwrite(EditorPrefs.GetString(PREFS_KEY), this); } }
12. 资源管理与清理
编辑器脚本经常需要处理项目资源,需要注意:
资源加载:
Texture2D icon = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Editor/Icons/icon.png");资源创建:
Material newMat = new Material(Shader.Find("Standard")); AssetDatabase.CreateAsset(newMat, "Assets/Materials/NewMaterial.mat");资源删除:
if (EditorUtility.DisplayDialog("Confirm Delete", "Are you sure?", "Yes", "No")) { AssetDatabase.DeleteAsset("Assets/ToDelete.asset"); }引用管理:
string[] dependencies = AssetDatabase.GetDependencies("Assets/Prefabs/MyPrefab.prefab");内存管理:
- 及时释放不需要的引用
- 对于大型数据集,考虑分块处理
13. 多语言与本地化支持
为编辑器工具添加多语言支持可以提升国际团队的体验:
public static class Localization { private static Dictionary<string, string> strings = new Dictionary<string, string>() { {"en|rename", "Rename"}, {"ja|rename", "名前を変更"}, {"zh|rename", "重命名"} }; public static string Get(string key) { string lang = "en"; // 可以从设置中获取 string fullKey = $"{lang}|{key}"; return strings.ContainsKey(fullKey) ? strings[fullKey] : key; } } // 使用示例 GUILayout.Button(Localization.Get("rename"));14. 版本兼容性处理
确保编辑器扩展在不同Unity版本中都能正常工作:
#if UNITY_2019_1_OR_NEWER // 使用新API EditorUtility.OpenWithDefaultApp(path); #else // 旧版本备用方案 Application.OpenURL(path); #endif15. 用户设置与偏好
提供用户设置可以大大提高工具的可用性:
public class EditorToolSettings : ScriptableObject { public bool autoSave = true; public Color highlightColor = Color.yellow; private static EditorToolSettings instance; public static EditorToolSettings Instance { get { if (instance == null) { instance = AssetDatabase.LoadAssetAtPath<EditorToolSettings>("Assets/Editor/EditorToolSettings.asset"); if (instance == null) { instance = CreateInstance<EditorToolSettings>(); AssetDatabase.CreateAsset(instance, "Assets/Editor/EditorToolSettings.asset"); } } return instance; } } }16. 自动化测试集成
为编辑器脚本添加测试可以确保长期维护的可靠性:
using NUnit.Framework; using UnityEditor; using UnityEngine; public class EditorToolTests { [Test] public void TestBatchRenamer() { // 创建测试对象 var go = new GameObject("TestObject"); // 执行重命名 string newName = BatchRenamer.RenameSingle(go.name, "Test", "New"); // 验证结果 Assert.AreEqual("NewObject", newName); // 清理 Object.DestroyImmediate(go); } }17. 插件架构设计
对于大型编辑器扩展,考虑使用插件架构:
public interface IEditorToolPlugin { string Name { get; } void OnGUI(); void OnEnable(); void OnDisable(); } public class PluginManager { private List<IEditorToolPlugin> plugins = new List<IEditorToolPlugin>(); public void RegisterPlugin(IEditorToolPlugin plugin) { plugins.Add(plugin); plugin.OnEnable(); } public void DrawAllPlugins() { foreach (var plugin in plugins) { EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField(plugin.Name, EditorStyles.boldLabel); plugin.OnGUI(); EditorGUILayout.EndVertical(); } } }18. 项目模板与快速启动
创建自定义的项目模板可以加速新项目开发:
public static class ProjectTemplateCreator { [MenuItem("Tools/Create Project Template")] public static void CreateTemplate() { // 创建标准文件夹结构 Directory.CreateDirectory("Assets/Art"); Directory.CreateDirectory("Assets/Scripts"); Directory.CreateDirectory("Assets/Prefabs"); // 创建基本场景 Scene newScene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects); EditorSceneManager.SaveScene(newScene, "Assets/Scenes/Main.unity"); // 设置默认渲染管线 GraphicsSettings.renderPipelineAsset = AssetDatabase.LoadAssetAtPath<RenderPipelineAsset>("Assets/Settings/URP-HighFidelity-Renderer.asset"); AssetDatabase.Refresh(); } }19. 编辑器脚本打包与分发
将编辑器工具打包为.unitypackage便于分享:
public static class PackageExporter { [MenuItem("Tools/Export Editor Tools")] public static void Export() { string[] paths = { "Assets/Editor/Tools", "Assets/Editor/Icons", "Assets/Editor/Resources" }; AssetDatabase.ExportPackage(paths, "EditorTools.unitypackage", ExportPackageOptions.Recurse); EditorUtility.RevealInFinder("EditorTools.unitypackage"); } }20. 持续集成与自动化
将编辑器脚本集成到CI/CD流程中:
public class BuildAutomation { public static void PerformBuild() { // 设置构建选项 BuildPlayerOptions options = new BuildPlayerOptions(); options.scenes = EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray(); options.locationPathName = "Builds/Game.exe"; options.target = BuildTarget.StandaloneWindows64; options.options = BuildOptions.None; // 执行构建 BuildPipeline.BuildPlayer(options); // 上传构建结果等后续操作 } }在实际项目中,这些技术可以组合使用,根据具体需求创建强大的编辑器扩展工具。记住,好的编辑器工具应该:
- 解决具体问题
- 提供清晰的用户界面
- 有良好的错误处理
- 不影响编辑器性能
- 易于维护和扩展