Appearance
编辑器扩展
Editor 脚本放在哪个目录?
一句话答案: Unity 的 Editor 脚本要放在任意名为 Editor 的目录下,比如 Assets/Editor、Assets/Scripts/Editor、Assets/Tools/Editor。这些脚本只在 Unity 编辑器里编译,不会打进最终游戏包。
常见目录写法:
c
Assets/Editor
Assets/Scripts/Editor
Assets/Tools/Editor
Assets/UI/Editor
Assets/Gameplay/Editor只要目录名叫 Editor,它下面的脚本就会被 Unity 当成编辑器脚本。
哪些脚本应该放进 Editor 目录?
使用了 UnityEditor 命名空间的脚本。 自定义 Inspector。 自定义 EditorWindow。 自定义菜单工具。 资源导入处理脚本。 Prefab 扫描工具、配置表导入工具、批量改名工具等。
例子:自定义菜单工具
c
using UnityEditor; // 引入 Unity 编辑器 API,只能在 Editor 脚本中使用
using UnityEngine; // 引入 Unity 常用类型
public static class MyEditorTool // 定义一个编辑器工具类
{ // 类开始
[MenuItem("Tools/Print Hello")] // 在 Unity 顶部菜单 Tools 下添加一个菜单项
public static void PrintHello() // 定义菜单点击后执行的方法
{ // 方法开始
Debug.Log("Hello Editor"); // 在 Console 中输出编辑器工具日志
} // 方法结束
} // 类结束这个脚本应该放在:
c
Assets/Editor/MyEditorTool.cs或者:
c
Assets/Tools/Editor/MyEditorTool.cs如果放错会怎样? 如果用了 using UnityEditor;,但脚本放在普通运行时代码目录,比如:
c
Assets/Scripts/MyEditorTool.cs打包时可能会报错,因为最终游戏包里没有 UnityEditor 这个程序集。
asmdef 情况下注意: 如果项目用了 Assembly Definition,Editor 脚本最好单独放一个 Editor asmdef,并设置:
c
Include Platforms: Editor这样它只会在编辑器平台编译,不会进入 Player。
NOTE
面试高分回答: Unity 的 Editor 脚本应该放在名为 Editor 的文件夹下,这个文件夹可以在 Assets 下,也可以在某个功能模块目录下。只要路径中有 Editor 文件夹,Unity 就会把里面的脚本编译到编辑器程序集里,而不会打进最终 Player 包。凡是用了 UnityEditor API 的脚本,比如自定义 Inspector、EditorWindow、菜单工具、资源导入工具,都应该放到 Editor 编译范围中。如果项目使用 asmdef,还要把编辑器程序集限制为 Editor 平台。
CustomEditor 是什么?
一句话答案:CustomEditor 是 Unity 编辑器扩展里的一个特性,用来给某个 MonoBehaviour 或 ScriptableObject 定制 Inspector 面板。它只影响编辑器显示和编辑方式,不影响游戏运行逻辑。
普通 Inspector 是什么样? 默认情况下,Unity 会按字段顺序把可序列化字段显示出来。
比如:
c
using UnityEngine; // 引入 Unity 常用类型
public class PlayerStats : MonoBehaviour // 定义玩家属性组件
{ // 类开始
public int hp = 100; // 定义血量字段
public int attack = 20; // 定义攻击力字段
public float moveSpeed = 5f; // 定义移动速度字段
} // 类结束Unity 默认 Inspector 会显示:
c
Hp
Attack
Move Speed但是如果你想加按钮、提示、校验逻辑、自定义布局,就可以用 CustomEditor。
CustomEditor 示例:
这个脚本要放在 Editor 文件夹里,比如:
c
Assets/Editor/PlayerStatsEditor.cs代码如下:
c
using UnityEditor; // 引入 Unity 编辑器 API,只能在 Editor 脚本里使用
using UnityEngine; // 引入 Unity 常用类型
[CustomEditor(typeof(PlayerStats))] // 指定这个自定义 Inspector 用来编辑 PlayerStats
public class PlayerStatsEditor : Editor // 定义自定义编辑器类,必须继承 Editor
{ // 类开始
public override void OnInspectorGUI() // 重写 Inspector 绘制方法
{ // 方法开始
DrawDefaultInspector(); // 先绘制 Unity 默认的 Inspector 字段
PlayerStats playerStats = (PlayerStats)target; // 获取当前正在被编辑的 PlayerStats 对象
EditorGUILayout.Space(); // 在 Inspector 中添加一点空白间距
if (GUILayout.Button("重置属性")) // 绘制一个按钮,并判断按钮是否被点击
{ // if 开始
Undo.RecordObject(playerStats, "Reset Player Stats"); // 记录撤销操作,让 Ctrl+Z 可以恢复
playerStats.hp = 100; // 把血量重置为 100
playerStats.attack = 20; // 把攻击力重置为 20
playerStats.moveSpeed = 5f; // 把移动速度重置为 5
EditorUtility.SetDirty(playerStats); // 标记对象已修改,让 Unity 保存变化
} // if 结束
if (playerStats.hp <= 0) // 判断血量是否不合法
{ // if 开始
EditorGUILayout.HelpBox("血量必须大于 0", MessageType.Warning); // 在 Inspector 中显示警告提示
} // if 结束
} // 方法结束
} // 类结束它能做什么?
自定义字段显示顺序。 加按钮,比如“一键生成配置”“一键刷新”“一键查错”。 显示提示框,比如配置错误时显示红色警告。 限制编辑方式,比如滑条、下拉框、折叠面板。 做配置检查,比如技能伤害不能小于 0。 做资源工具,比如批量收集引用、自动生成 ID。
注意点:
CustomEditor 脚本属于编辑器代码。 用了 using UnityEditor; 的脚本必须放在 Editor 目录,或者放进只包含 Editor 平台的 asmdef。 不要把真正的游戏运行逻辑写在 CustomEditor 里。 如果编辑器脚本修改对象字段,最好配合 Undo.RecordObject 和 EditorUtility.SetDirty。 如果只是想保留默认 Inspector,再加一点按钮,可以先调用 DrawDefaultInspector()。
TIP
面试高分回答:CustomEditor 是 Unity 的编辑器扩展机制,用来为指定类型定制 Inspector 面板。它通过 [CustomEditor(typeof(SomeType))] 绑定一个目标类型,然后继承 Editor 并重写 OnInspectorGUI() 来控制 Inspector 的绘制。它常用于配置检查、工具按钮、自定义布局和提升编辑效率。它属于编辑器代码,所以需要放在 Editor 文件夹或 Editor-only 程序集中,不会进入最终游戏包。实际项目里我会用它给技能配置、怪物配置、关卡配置做可视化编辑和合法性校验。
EditorWindow 可以做什么?
一句话答案:EditorWindow 可以用来做 Unity 编辑器里的独立工具窗口,比如资源批处理、配置表导入、Prefab 检查、Missing Reference 扫描、关卡编辑工具、打包工具等。
它和 CustomEditor 的区别:
CustomEditor 是给某个对象定制 Inspector。 比如选中 PlayerStats,右边 Inspector 怎么显示。
EditorWindow 是做一个独立窗口。 比如顶部菜单打开一个“配置表导入工具”“资源检查工具”“批量改名工具”。
常见用途:
资源批量处理:批量改名、批量设置图片压缩格式、批量修改 AssetBundle 名称。
配置工具:导入 Excel、JSON、CSV,生成 ScriptableObject 或二进制配置。
Prefab 检查:扫描 Missing Reference、Missing Script、空引用、命名不规范。
关卡工具:编辑刷怪点、路径点、触发区域、关卡波次。
打包工具:一键构建、选择平台、切换环境、生成版本号。
性能辅助:扫描大图、重复资源、未压缩音频、超大 Prefab。
简单示例:批量改名窗口
这个脚本要放在:
c
Assets/Editor/BatchRenameWindow.cs代码如下:
c
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
public class BatchRenameWindow : EditorWindow // 定义一个编辑器窗口类,必须继承 EditorWindow
{ // 类开始
private string prefix = "Item_"; // 定义批量改名使用的前缀
[MenuItem("Tools/Batch Rename Window")] // 把窗口入口添加到 Unity 顶部菜单 Tools 中
public static void Open() // 定义打开窗口的方法
{ // 方法开始
GetWindow<BatchRenameWindow>("批量改名"); // 打开或获取一个 BatchRenameWindow 窗口,并设置标题
} // 方法结束
private void OnGUI() // 绘制窗口 UI,每次窗口刷新时调用
{ // 方法开始
EditorGUILayout.LabelField("批量改名工具", EditorStyles.boldLabel); // 绘制一个加粗标题
prefix = EditorGUILayout.TextField("前缀", prefix); // 绘制一个文本输入框,用来修改前缀
if (GUILayout.Button("给选中对象改名")) // 绘制按钮,并判断按钮是否被点击
{ // if 开始
RenameSelectedObjects(); // 点击按钮后调用批量改名方法
} // if 结束
} // 方法结束
private void RenameSelectedObjects() // 定义批量改名方法
{ // 方法开始
GameObject[] selectedObjects = Selection.gameObjects; // 获取当前在 Hierarchy 中选中的所有 GameObject
for (int i = 0; i < selectedObjects.Length; i++) // 遍历所有选中的对象
{ // for 开始
Undo.RecordObject(selectedObjects[i], "Batch Rename"); // 记录撤销操作,支持 Ctrl+Z
selectedObjects[i].name = prefix + i; // 给当前对象设置新名字
EditorUtility.SetDirty(selectedObjects[i]); // 标记对象已修改,让 Unity 保存变化
} // for 结束
} // 方法结束
} // 类结束注意点:
EditorWindow 属于编辑器代码。 用了 UnityEditor,所以脚本必须放在 Editor 文件夹,或者 Editor-only asmdef 里。 它不会进入最终游戏包。 不要把游戏运行时逻辑写在 EditorWindow 里。 修改场景对象或资源时,最好支持 Undo,并正确标记脏数据。
WARNING
面试高分回答:EditorWindow 是 Unity 编辑器扩展中用来创建独立工具窗口的类。它适合做和具体 Inspector 无关的全局工具,比如资源批处理、配置导入、Prefab 检查、关卡编辑、打包面板等。通常通过 [MenuItem] 提供菜单入口,用 GetWindow<T>() 打开窗口,在 OnGUI() 里绘制按钮、输入框和列表。它属于编辑器代码,必须放在 Editor 编译范围内,不会打进最终包。实际项目里我会用它把重复的人工操作做成一键工具,提高团队编辑效率并减少配置错误。
PropertyDrawer 是什么?
一句话答案:PropertyDrawer 是 Unity 的编辑器扩展,用来定制某个字段类型或某个 Attribute 标记的字段在 Inspector 里的显示方式。它是字段级别的自定义 Inspector。
它解决什么问题? 比如你有一个范围数据:
c
min
max默认 Inspector 可能会把它展开成两行。 但你想让它显示成一行:
c
Damage Range Min: 10 Max: 30这时就可以用 PropertyDrawer。
运行时代码:定义一个可序列化类型
这个脚本可以放在普通目录,比如:
c
Assets/Scripts/StatRange.cs
using System; // 引入 Serializable 特性
[Serializable] // 让 Unity 可以序列化这个自定义数据类型
public class StatRange // 定义一个属性范围类
{ // 类开始
public int min; // 定义最小值字段
public int max; // 定义最大值字段
} // 类结束运行时代码:在组件中使用它
c
using UnityEngine; // 引入 Unity 常用类型
public class EnemyConfig : MonoBehaviour // 定义怪物配置组件
{ // 类开始
public StatRange damageRange; // 使用 StatRange 字段,Inspector 会应用对应的 PropertyDrawer
} // 类结束编辑器代码:自定义字段显示
这个脚本必须放在 Editor 文件夹,比如:
c
Assets/Editor/StatRangeDrawer.cs
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
[CustomPropertyDrawer(typeof(StatRange))] // 指定这个 Drawer 用来绘制 StatRange 类型
public class StatRangeDrawer : PropertyDrawer // 定义自定义属性绘制器,继承 PropertyDrawer
{ // 类开始
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) // 重写字段绘制方法
{ // 方法开始
EditorGUI.BeginProperty(position, label, property); // 开始绘制属性,保证 Prefab Override 等功能正常
Rect labelRect = new Rect(position.x, position.y, 90, position.height); // 定义标签区域
Rect minRect = new Rect(position.x + 95, position.y, 80, position.height); // 定义最小值输入区域
Rect maxRect = new Rect(position.x + 180, position.y, 80, position.height); // 定义最大值输入区域
SerializedProperty minProperty = property.FindPropertyRelative("min"); // 找到 min 子字段
SerializedProperty maxProperty = property.FindPropertyRelative("max"); // 找到 max 子字段
EditorGUI.LabelField(labelRect, label); // 绘制字段标签
EditorGUI.PropertyField(minRect, minProperty, GUIContent.none); // 绘制 min 输入框
EditorGUI.PropertyField(maxRect, maxProperty, GUIContent.none); // 绘制 max 输入框
EditorGUI.EndProperty(); // 结束属性绘制
} // 方法结束
} // 类结束和 CustomEditor 的区别:
CustomEditor 是定制整个组件或资源的 Inspector。 PropertyDrawer 是定制某个字段怎么显示。
比如:
CustomEditor:改整个 EnemyConfig 面板。 PropertyDrawer:只改 damageRange 这种字段的显示方式。
两种常见用法:
第一种:给某个类型做 Drawer。 比如所有 StatRange 都显示成一行。
第二种:给某个 Attribute 做 Drawer。 比如 [ReadOnly]、[SceneName]、[Dropdown] 这种自定义特性。
IMPORTANT
面试高分回答:PropertyDrawer 是 Unity 的字段级 Inspector 扩展。它可以通过 [CustomPropertyDrawer] 绑定一个可序列化类型,或者绑定一个继承自 PropertyAttribute 的自定义特性,然后重写 OnGUI 来控制字段在 Inspector 中如何绘制。它适合做可复用的字段显示逻辑,比如范围输入、只读字段、下拉框、配置校验等。和 CustomEditor 相比,CustomEditor 是改整个对象的 Inspector,而 PropertyDrawer 是改某个字段的显示方式。因为它使用 UnityEditor,所以必须放在 Editor 目录或 Editor-only 程序集中。
MenuItem 怎么添加菜单?
一句话答案: 用 [MenuItem("菜单路径")] 标记一个 static 方法,就可以把这个方法添加到 Unity 菜单里。这个脚本要放在 Editor 文件夹下,因为它使用的是 UnityEditor API。
最简单例子:添加顶部菜单
脚本放在:
c
Assets/Editor/MyMenuTools.cs
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
public static class MyMenuTools // 定义一个静态编辑器工具类
{ // 类开始
[MenuItem("Tools/Say Hello")] // 在 Unity 顶部菜单 Tools 下添加 Say Hello 菜单项
public static void SayHello() // 定义菜单点击后执行的静态方法
{ // 方法开始
Debug.Log("Hello MenuItem"); // 在 Console 输出日志
} // 方法结束
} // 类结束Unity 顶部菜单会出现:
c
Tools -> Say Hello菜单路径怎么写?
c
[MenuItem("Tools/My Tools/Check Prefabs")] // 创建 Tools/My Tools/Check Prefabs 三级菜单斜杠 / 表示菜单层级。 Tools 是顶部菜单。 My Tools 是子菜单。 Check Prefabs 是具体菜单项。
添加 Project 右键菜单:
c
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
public static class AssetMenuTools // 定义资源菜单工具类
{ // 类开始
[MenuItem("Assets/Print Selected Asset Name")] // 在 Project 面板右键菜单 Assets 下添加菜单项
public static void PrintSelectedAssetName() // 定义菜单执行方法
{ // 方法开始
Object selectedObject = Selection.activeObject; // 获取当前 Project 面板中选中的资源
Debug.Log(selectedObject.name); // 输出选中资源的名字
} // 方法结束
} // 类结束添加校验菜单:让菜单在不满足条件时置灰
c
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
public static class ValidateMenuTools // 定义带校验的菜单工具类
{ // 类开始
[MenuItem("GameObject/Print Selected GameObject")] // 添加 GameObject 菜单项
public static void PrintSelectedGameObject() // 定义真正执行菜单逻辑的方法
{ // 方法开始
Debug.Log(Selection.activeGameObject.name); // 输出当前选中 GameObject 的名字
} // 方法结束
[MenuItem("GameObject/Print Selected GameObject", true)] // 添加同路径的校验方法,true 表示这是 validate 函数
public static bool ValidatePrintSelectedGameObject() // 定义菜单是否可用的校验方法
{ // 方法开始
return Selection.activeGameObject != null; // 只有选中了 GameObject,菜单才可以点击
} // 方法结束
} // 类结束注意点:
MenuItem 标记的方法必须是 static。 脚本必须放在 Editor 文件夹,或者 Editor-only asmdef。 用了 using UnityEditor; 的代码不能进入最终游戏包。 MenuItem("xxx", true) 是校验方法,不是真正执行方法。 校验方法返回 true,菜单可点击;返回 false,菜单置灰。 可以用 priority 控制菜单排序,比如 [MenuItem("Tools/Test", false, 100)]。
CAUTION
面试高分回答:MenuItem 是 Unity 编辑器扩展里用来添加菜单入口的特性。它可以把一个静态方法注册到 Unity 的顶部菜单、Project 右键菜单、Hierarchy 菜单或组件上下文菜单中。菜单路径用字符串表示,斜杠表示层级。脚本需要放在 Editor 目录或 Editor-only 程序集中,因为它依赖 UnityEditor。实际项目中我会用 MenuItem 做工具入口,比如打开 EditorWindow、批量处理资源、扫描 Missing Reference、生成配置表或执行打包流程。
如何做一个批量改资源工具?
一句话答案: 批量改资源工具一般用 EditorWindow + Selection + AssetDatabase + AssetImporter 实现:先选中资源,拿到资源路径,获取对应 Importer,修改设置,然后 SaveAndReimport() 重新导入。
实现思路:
- 脚本放到
Assets/Editor目录。 - 用
[MenuItem]在 Unity 顶部菜单加入口。 - 用
EditorWindow做一个工具窗口。 - 用
Selection.objects获取 Project 面板选中的资源。 - 用
AssetDatabase.GetAssetPath()获取资源路径。 - 用
AssetImporter.GetAtPath()获取导入器。 - 判断是不是你要处理的类型,比如
TextureImporter。 - 修改导入设置。
- 调用
SaveAndReimport()重新导入资源。 - 输出日志,告诉用户处理了多少个资源。
示例:批量修改选中图片导入设置
脚本路径:
c
Assets/Editor/BatchTextureTool.cs
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
public class BatchTextureTool : EditorWindow // 定义批量图片工具窗口类,继承 EditorWindow
{ // 类开始
private int maxTextureSize = 1024; // 定义图片最大尺寸默认值
private bool mipmapEnabled = false; // 定义是否开启 MipMap
private TextureImporterCompression compression = TextureImporterCompression.Compressed; // 定义图片压缩方式
[MenuItem("Tools/Batch Texture Tool")] // 在 Unity 顶部菜单 Tools 下添加工具入口
public static void OpenWindow() // 定义打开窗口的方法
{ // 方法开始
GetWindow<BatchTextureTool>("批量图片工具"); // 打开批量图片工具窗口
} // 方法结束
private void OnGUI() // 绘制编辑器窗口界面
{ // 方法开始
EditorGUILayout.LabelField("批量修改选中图片", EditorStyles.boldLabel); // 绘制窗口标题
maxTextureSize = EditorGUILayout.IntField("最大尺寸", maxTextureSize); // 绘制最大尺寸输入框
mipmapEnabled = EditorGUILayout.Toggle("开启 MipMap", mipmapEnabled); // 绘制 MipMap 开关
compression = (TextureImporterCompression)EditorGUILayout.EnumPopup("压缩方式", compression); // 绘制压缩方式下拉框
EditorGUILayout.Space(); // 绘制一段空白间距
if (GUILayout.Button("应用到选中图片")) // 绘制按钮并判断是否点击
{ // if 开始
ApplyToSelectedTextures(); // 点击按钮后执行批量修改
} // if 结束
} // 方法结束
private void ApplyToSelectedTextures() // 定义批量修改选中图片的方法
{ // 方法开始
Object[] selectedObjects = Selection.objects; // 获取 Project 面板当前选中的资源对象
int changedCount = 0; // 记录成功修改的资源数量
for (int i = 0; i < selectedObjects.Length; i++) // 遍历所有选中的资源
{ // for 开始
Object selectedObject = selectedObjects[i]; // 取出当前选中的资源对象
string assetPath = AssetDatabase.GetAssetPath(selectedObject); // 获取当前资源在 Assets 下的路径
if (string.IsNullOrEmpty(assetPath)) // 判断资源路径是否为空
{ // if 开始
continue; // 路径为空说明不是有效资源,跳过
} // if 结束
AssetImporter importer = AssetImporter.GetAtPath(assetPath); // 根据资源路径获取对应的导入器
TextureImporter textureImporter = importer as TextureImporter; // 尝试把导入器转换成 TextureImporter
if (textureImporter == null) // 判断当前资源是否不是图片资源
{ // if 开始
continue; // 不是图片资源就跳过
} // if 结束
Undo.RecordObject(textureImporter, "Batch Modify Texture Importer"); // 记录撤销操作,方便 Ctrl+Z
textureImporter.maxTextureSize = maxTextureSize; // 设置图片最大尺寸
textureImporter.mipmapEnabled = mipmapEnabled; // 设置是否开启 MipMap
textureImporter.textureCompression = compression; // 设置图片压缩方式
EditorUtility.SetDirty(textureImporter); // 标记导入器已修改
textureImporter.SaveAndReimport(); // 保存设置并重新导入图片资源
changedCount++; // 成功修改数量加一
} // for 结束
AssetDatabase.SaveAssets(); // 保存所有资源修改
AssetDatabase.Refresh(); // 刷新 Unity 资源数据库
Debug.Log("批量修改完成,处理图片数量:" + changedCount); // 输出处理结果日志
} // 方法结束
} // 类结束工具设计时要注意:
批量工具最好先处理“选中资源”,不要一开始就全项目扫描。 修改前可以弹确认框,防止误操作。 要过滤资源类型,不要把 Prefab、材质、脚本误处理。 修改资源对象时尽量支持 Undo。 处理大量资源时要输出日志,方便知道改了哪些。 修改 Importer 会触发重新导入,资源多时可能会卡一会儿。 正式跑全量前,先复制一小批资源测试。
NOTE
面试高分回答: 我会把批量改资源工具做成 EditorWindow,通过 MenuItem 提供入口。工具窗口里配置参数,比如图片最大尺寸、压缩格式、是否开启 MipMap。执行时从 Selection.objects 获取选中的资源,用 AssetDatabase.GetAssetPath 得到路径,再用 AssetImporter.GetAtPath 拿到对应 Importer。比如图片资源就转成 TextureImporter,修改导入设置后调用 SaveAndReimport。为了安全性,我会做类型过滤、确认提示、日志输出和 Undo 支持,避免误改资源。核心思路就是:编辑器选资源,AssetDatabase 找路径,Importer 改设置,保存并重新导入。
AssetPostprocessor 有什么用?
一句话答案:AssetPostprocessor 是 Unity 的资源导入钩子。它可以在图片、模型、音频等资源导入前后自动执行代码,用来统一资源导入设置、检查资源规范、自动生成辅助数据。
它解决什么问题?
如果没有 AssetPostprocessor,每张 UI 图片都要人工设置:
Texture Type = Sprite关闭 MipMap设置压缩格式设置最大尺寸
人一多就容易漏。AssetPostprocessor 可以把这些规则写成代码,资源一导入就自动处理。
简单示例:自动设置 UI 图片导入规则
脚本放在:
c
Assets/Editor/UITexturePostprocessor.cs
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
public class UITexturePostprocessor : AssetPostprocessor // 定义资源后处理类,继承 AssetPostprocessor
{ // 类开始
private void OnPreprocessTexture() // 在图片资源导入前调用
{ // 方法开始
if (!assetPath.Contains("/UI/")) // 判断资源路径是否不在 UI 目录下
{ // if 开始
return; // 不是 UI 图片就直接返回,不做处理
} // if 结束
TextureImporter textureImporter = assetImporter as TextureImporter; // 把当前资源导入器转换成 TextureImporter
if (textureImporter == null) // 判断导入器是否转换失败
{ // if 开始
return; // 如果不是图片导入器,就直接返回
} // if 结束
textureImporter.textureType = TextureImporterType.Sprite; // 把图片类型设置为 Sprite
textureImporter.mipmapEnabled = false; // UI 图片通常不需要 MipMap
textureImporter.alphaIsTransparency = true; // 让透明区域按透明处理
textureImporter.maxTextureSize = 1024; // 设置最大图片尺寸
textureImporter.textureCompression = TextureImporterCompression.Compressed; // 设置图片压缩方式
} // 方法结束
private void OnPostprocessTexture(Texture2D texture) // 在图片资源导入完成后调用
{ // 方法开始
if (!assetPath.Contains("/UI/")) // 判断资源路径是否不在 UI 目录下
{ // if 开始
return; // 不是 UI 图片就不检查
} // if 结束
if (texture.width > 1024 || texture.height > 1024) // 判断图片宽高是否超过限制
{ // if 开始
Debug.LogWarning("UI 图片尺寸过大:" + assetPath); // 输出警告,提示资源尺寸过大
} // if 结束
} // 方法结束
} // 类结束常见用途:
自动设置图片导入规则。
自动设置模型导入规则。
自动设置音频压缩格式。
自动检查资源命名规范。
自动检查图片尺寸是否超标。
自动给资源设置 AssetBundle 名称。
自动生成配置、缩略图、辅助资源。
导入表格后自动生成 ScriptableObject 或代码。常见回调:
OnPreprocessTexture:图片导入前。 OnPostprocessTexture:图片导入后。 OnPreprocessModel:模型导入前。 OnPostprocessModel:模型导入后。 OnPreprocessAudio:音频导入前。 OnPostprocessAllAssets:一批资源导入、删除、移动后统一回调。
和 MenuItem 的区别:
MenuItem 是手动点菜单执行。 AssetPostprocessor 是资源导入时自动执行。
所以:
批量手动修资源,可以用 MenuItem 或 EditorWindow。 想让团队以后导入资源都自动符合规范,用 AssetPostprocessor。
注意点:
它属于编辑器代码,要放在 Editor 目录或 Editor-only asmdef。 不要写太重的逻辑,否则导入资源会变慢。 路径判断要严格,避免误处理全项目资源。 不要在回调里反复修改资源导致循环导入。 团队规则要稳定,否则每次导入结果都变,版本管理会很乱。
NOTE
面试高分回答:AssetPostprocessor 是 Unity 资源导入管线的扩展点,可以在资源导入前后自动执行代码。它常用来统一图片、模型、音频等资源的 Import Settings,比如 UI 图片自动设置成 Sprite、关闭 MipMap,模型自动关闭不需要的导入选项,音频自动设置压缩格式。它也可以做资源规范检查,比如命名、尺寸、路径、标签、AssetBundle 名。相比 MenuItem 这种手动工具,AssetPostprocessor 是自动触发的,更适合做团队级资源规范管线。但要注意逻辑不能太重,条件要写清楚,避免拖慢导入或误处理资源。
OnValidate 适合做什么?
一句话答案:OnValidate 适合做编辑器阶段的字段校验、自动修正、轻量预览刷新。它不是运行时初始化函数,不应该拿来替代 Awake 或 Start。
什么时候会调用?
OnValidate 会在编辑器中触发,常见时机是:
脚本加载时。 Inspector 里的字段值发生变化时。 对象反序列化后。 Prefab、ScriptableObject、MonoBehaviour 配置被 Unity 刷新时。
适合做什么?
适合做数值范围修正:
血量不能小于 1
速度不能小于 0
概率限制在 0 到 1
min 不能大于 max适合做配置检查:
技能图标不能为空
特效 Prefab 不能为空
怪物掉落表不能重复
技能 ID 不能重复适合做轻量自动同步:
根据名字自动生成显示名
根据半径刷新 Gizmos 预览
根据配置自动更新描述文本代码示例:
c
using UnityEngine; // 引入 Unity 常用类型
public class SkillConfig : MonoBehaviour // 定义技能配置组件
{ // 类开始
[SerializeField] private int damage = 10; // 定义技能伤害字段
[SerializeField] private float cooldown = 1f; // 定义技能冷却字段
[SerializeField] private float range = 5f; // 定义技能范围字段
[SerializeField] private GameObject effectPrefab; // 定义技能特效预制体引用
private void OnValidate() // 当 Inspector 字段变化或脚本加载时调用
{ // 方法开始
damage = Mathf.Max(1, damage); // 保证伤害至少为 1
cooldown = Mathf.Max(0f, cooldown); // 保证冷却时间不能小于 0
range = Mathf.Max(0f, range); // 保证技能范围不能小于 0
if (effectPrefab == null) // 判断特效引用是否为空
{ // if 开始
Debug.LogWarning("技能特效 Prefab 没有配置:" + name, this); // 在编辑器中提示配置缺失
} // if 结束
} // 方法结束
} // 类结束不适合做什么?
不要在 OnValidate 里做复杂运行时初始化。 不要把它当成 Awake 或 Start。 不要频繁 Instantiate 或 Destroy。 不要做大量文件 IO。 不要每次字段变化都扫描全项目。 不要写依赖运行时对象顺序的逻辑。
因为 OnValidate 可能被频繁调用,而且是在编辑器状态下调用,调用时机不适合承载真正的游戏流程。
和 Awake / Start 的区别:
OnValidate:编辑器校验,用来保证配置填得合理。 Awake:运行时对象初始化,用来准备游戏逻辑。 Start:运行时首帧前初始化,用来处理依赖其他对象的初始化。
IMPORTANT
面试高分回答:OnValidate 是 Unity 在编辑器中提供的校验回调,通常在脚本加载或 Inspector 字段发生变化时调用。我会用它做轻量的配置校验和自动修正,比如限制数值范围、检查空引用、保证最小值不大于最大值、刷新 Gizmos 预览参数等。它的价值是让错误在编辑阶段就暴露出来,而不是等运行时才报错。但我不会在里面写运行时初始化、创建销毁对象、大量扫描或复杂逻辑,因为它可能频繁调用,且调用时机不等同于游戏运行生命周期。
编辑器工具如何避免污染运行时代码?
一句话答案: 编辑器工具要避免污染运行时代码,核心就是:运行时代码不引用 UnityEditor,编辑器代码放到 Editor 编译范围,依赖方向只能是 Editor 依赖 Runtime,不能反过来。
最常用做法:
把运行时代码放这里:
c
Assets/Scripts/Runtime把编辑器工具放这里:
c
Assets/Scripts/Editor只要脚本用了:
c
using UnityEditor; // 引入 Unity 编辑器 API它就应该放到 Editor 文件夹,或者放进 Editor-only 的 asmdef 程序集。
错误写法:运行时代码直接引用 UnityEditor
c
using UnityEngine; // 引入 Unity 运行时 API
using UnityEditor; // 错误:运行时代码不应该直接引用 UnityEditor
public class Player : MonoBehaviour // 定义玩家组件
{ // 类开始
private void Start() // 游戏运行时调用
{ // 方法开始
string path = AssetDatabase.GetAssetPath(gameObject); // 错误:AssetDatabase 是编辑器 API,打包后不存在
} // 方法结束
} // 类结束这种代码在编辑器里可能能跑,但打包 Player 时会报错,因为 UnityEditor 不会进入最终游戏包。
正确做法一:把工具代码放到 Editor 脚本里
c
using UnityEditor; // 引入 Unity 编辑器 API
using UnityEngine; // 引入 Unity 常用类型
public static class PlayerEditorTool // 定义编辑器工具类
{ // 类开始
[MenuItem("Tools/Print Selected Asset Path")] // 在 Unity 顶部菜单添加工具入口
public static void PrintSelectedAssetPath() // 定义菜单点击后执行的方法
{ // 方法开始
Object selectedObject = Selection.activeObject; // 获取当前选中的资源或对象
string path = AssetDatabase.GetAssetPath(selectedObject); // 获取选中对象的资源路径
Debug.Log(path); // 输出资源路径
} // 方法结束
} // 类结束这个脚本应该放在:
c
Assets/Editor/PlayerEditorTool.cs正确做法二:少量编辑器代码用条件编译包住
c
using UnityEngine; // 引入 Unity 运行时 API
public class DebugPathExample : MonoBehaviour // 定义调试路径示例组件
{ // 类开始
public void PrintDebugInfo() // 定义打印调试信息的方法
{ // 方法开始
#if UNITY_EDITOR // 只有在 Unity 编辑器环境下才编译下面这段代码
string path = UnityEditor.AssetDatabase.GetAssetPath(this); // 使用 UnityEditor API 获取当前对象路径
Debug.Log(path); // 输出路径信息
#endif // 结束 Unity 编辑器条件编译
} // 方法结束
} // 类结束这个方式适合少量调试辅助,不适合复杂工具。复杂工具还是应该拆到 Editor 目录。
正确做法三:用 asmdef 做程序集隔离
推荐结构:
c
Assets/Game/Runtime/Game.Runtime.asmdef
Assets/Game/Runtime/Player.cs
Assets/Game/Editor/Game.Editor.asmdef
Assets/Game/Editor/PlayerEditorTool.csGame.Runtime.asmdef:给游戏运行时代码用。 Game.Editor.asmdef:给编辑器工具用,设置 Include Platforms = Editor。 Game.Editor 可以引用 Game.Runtime。 Game.Runtime 不要引用 Game.Editor。
设计原则:
运行时只放游戏真正需要的逻辑和数据。 编辑器工具放资源扫描、批处理、Inspector 扩展、导入工具。 公共数据结构可以放 Runtime,比如 SkillConfig、ItemData。 工具逻辑放 Editor,比如配置校验工具、批量生成工具。 不要让运行时脚本知道 AssetDatabase、EditorWindow、MenuItem、CustomEditor 这些东西。
WARNING
面试高分回答: 我会从编译边界上避免编辑器代码污染运行时。凡是用了 UnityEditor 的代码,都放到 Editor 文件夹或者 Editor-only asmdef 里,让它只在编辑器程序集编译,不进入 Player。项目使用 asmdef 时,我会拆成 Runtime 和 Editor 两个程序集,Editor 可以引用 Runtime,因为工具需要读取游戏配置和数据结构,但 Runtime 不能引用 Editor。少量调试代码可以用 #if UNITY_EDITOR 包住,但复杂工具不能塞进运行时文件里。这样可以避免打包时报 UnityEditor 不存在,也能让游戏逻辑和编辑器工具职责清晰。
大厂为什么重视工具链开发?
一句话答案: 大厂重视工具链开发,是因为项目规模一大,不能靠人肉操作、口头规范和个人经验来保证效率和质量。工具链能把重复流程自动化,把团队规范固化,把错误提前暴露,让项目稳定交付。
为什么大厂特别重视?
小团队里,一个资源导错了,喊一声可能就解决了。
大团队里,几十个程序、几十个美术、几十个策划同时工作,如果没有工具链,问题会变成:
资源格式不统一。
配置表有人填错。
Prefab 引用丢失。
图片尺寸超标。
热更包漏资源。
打包流程依赖某个人。
线上问题没有日志和追踪。
这些问题靠“大家注意一点”是靠不住的,所以要靠工具。工具链具体能做什么?
资源导入自动化: 比如 UI 图片自动设为 Sprite,关闭 MipMap,限制最大尺寸。
配置表自动化: 比如 Excel 导出 JSON、二进制、C# 类,顺便检查字段类型和重复 ID。
资源检查自动化: 比如扫描 Missing Reference、Missing Script、超大贴图、重复资源、未压缩音频。
打包发布自动化: 比如一键构建 Android、iOS、Windows 包,自动生成版本号和构建报告。
热更新工具链: 比如资源版本对比、差异包生成、MD5 校验、回滚策略。
性能分析工具: 比如自动采集帧率、内存、GC、加载耗时、卡顿日志。
编辑器工具: 比如怪物刷点编辑器、技能编辑器、关卡配置工具、剧情对话编辑器。
为什么这能提升质量?
因为工具可以把错误挡在上线前。
比如策划填了一个不存在的技能 ID,如果没有工具,可能运行时才报错。 如果有配置校验工具,导表时就能报错。 这样问题在开发阶段就解决了,不会流到测试、上线、玩家手里。
为什么这能提升效率?
因为很多工作不该让人重复做。
人适合做设计、判断、创造。 工具适合做检查、转换、批量处理、生成、记录。
比如 500 张图片要改压缩格式,如果人工点 Inspector,很慢也容易漏。 工具一键处理,结果更快、更一致、更可追踪。
WARNING
面试高分回答: 大厂重视工具链,是因为大项目的核心问题不是“能不能做出来”,而是“能不能稳定、高效、可控地持续交付”。当团队规模和资源量上来以后,靠个人经验和手动操作很容易出错,所以需要工具链把流程自动化,把规范固化,把错误提前暴露。比如资源导入规范、配置表生成、Prefab 检查、热更打包、自动构建、性能采集和日志分析,这些都能减少重复劳动和人为失误。对游戏项目来说,工具链本质上是在提高生产效率、降低质量风险、支撑多人协作和长期维护能力。大厂看重工具链开发,其实是在看一个人有没有工程化意识。