常用编辑器工具有哪些Excel导SO/json通用导表工具。需要支持枚举、/分割的列表、/和*分割的字典。AssetDatabase资源校验工具。从根配置到每张配表到每个资源检查路径。笨方法是每张表用手写也有通用的方法批量给预制体注入id的工具批量给资源设置ab包名的工具。可以在资源校验的时候顺便做了上方菜单[MenuItem(工具栏选项名)] static void XXX(){ }弹窗下面演示了在弹窗上点击一个按钮出现文件浏览器且只显示xlsx文件很有用。public class PopWindow : EditorWindow { [MenuItem(我的工具/弹窗)] static void OpenWindow(){ GetWindowPopWindow(false,我的弹窗,true); } void OnGUI(){ EditorGUILayout.LabelField(Excel路径, ExcelPath); if (GUILayout.Button(选择Excel文件)) { ExcelPath EditorUtility.OpenFilePanel(选择Excel文件, Application.dataPath /.., xlsx); } } static string ExcelPath; }自定义检查器[CustomEditor(typeof(XXX))] public class MyXXXEditor : Editor { XXX _component; void OnEnable(){ _componenttarget as XXX; } void OnDisable(){ _componentnull; } public override void OnInspectorGUI() { base.OnInspectorGUI(); } }Hierarchy窗口增加选项[MenuItem(GameObject/选项)] static void XXX(){ }Project窗口增加选项[MenuItem(Assets/选项)] static void XXX(){ }在Add Component菜单增加选项[AddComponentMenu(选项/选项2)] public class XXX:MonoBehaviour{ }在组件右键菜单增加选项[MenuItem(CONTEXT/脚本名/选项)] static void XXX(){ }GUI的控件GUI需要传入Rect指定位置大小不会自动布局。GUILayout的控件GUILayout会自动布局。GUILayout.ButtonEditorGUILayout的控件Excel自动生成C#数据类工具生成C#数据类把Excel配表二进制序列化存储。没啥用有写Excel表的工夫不如手写C#。using Excel; using System.Data; using System.IO; using System.Text; using UnityEditor; using UnityEngine; public class AutoGenerateDataClassTool : EditorWindow { [MenuItem(我的工具/Excel自动生成数据类工具)] static void OpenWindow(){ GetWindowAutoGenerateDataClassTool(false, Excel自动生成数据类工具, true); } void OnGUI() { EditorGUILayout.LabelField(Excel路径, ExcelPath); if (GUILayout.Button(选择Excel文件)) { ExcelPath EditorUtility.OpenFilePanel(选择Excel文件, Application.dataPath /.., xlsx); } NAMESPACE EditorGUILayout.TextField(命名空间, NAMESPACE); FieldNameRow EditorGUILayout.IntField(变量名所在行, FieldNameRow); TypeRow EditorGUILayout.IntField(类型所在行, TypeRow); KeyMarkRow EditorGUILayout.IntField(key标记所在行, KeyMarkRow); ContentStartRow EditorGUILayout.IntField(内容开始行, ContentStartRow); EditorGUILayout.LabelField(数据类生成路径, OutputCsPath); if (GUILayout.Button(选择数据类生成路径)) { OutputCsPath EditorUtility.OpenFolderPanel(选择数据类生成路径, Application.dataPath /.., ); } DataClassName EditorGUILayout.TextField(数据类名, DataClassName); if (GUILayout.Button(生成C#数据类)) { GenerateDataClass(); } if (OKLastTime) { EditorGUILayout.LabelField(成功); } } static int FieldNameRow 0;//第几行是字段名 static int TypeRow 1;//第几行是类型名 static int KeyMarkRow 2;//第几行标记key static int ContentStartRow 3;//从第几行数据开始 static string ExcelPath; static string OutputCsPath ${Application.dataPath}/Scripts; static string NAMESPACE 哈哈哈; static string DataClassName; static bool OKLastTime; static void GenerateDataClass() { using (FileStream excelStream File.Open(ExcelPath, FileMode.Open, FileAccess.Read)) { IExcelDataReader reader ExcelReaderFactory.CreateOpenXmlReader(excelStream); DataSet dataSet reader.AsDataSet(); DataTable table dataSet.Tables[0]; DataRow fieldRow table.Rows[FieldNameRow]; DataRow typeRow table.Rows[TypeRow]; string className string.IsNullOrEmpty(DataClassName) ? table.TableName : DataClassName; StringBuilder builder new StringBuilder($namespace {NAMESPACE}{{\n\tpublic class {className}{{\n); for (int i 0; i table.Columns.Count; i) { builder.Append($\t\tpublic {typeRow[i]} {fieldRow[i]};\n); } builder.Append(\t}\n}); File.WriteAllText(Path.Combine(OutputCsPath,${DataClassName}.cs), builder.ToString()); excelStream.Close(); } AssetDatabase.Refresh(); } }一个枪械配表导表工具这里把Excel导出为SO比导出json更方便做后续修改。using Excel; using Mercenaria; using System; using System.Collections.Generic; using System.Data; using System.IO; using UnityEditor; using UnityEngine; /// summary /// 读取枪械Excel配表并导出为SO或json /// /summary public class MyGunConfigExcelTool : EditorWindow { [MenuItem(我的工具/Excel枪械导表工具)] static void OpenWindow(){ GetWindowMyGunConfigExcelTool(false, Excel枪械导表工具, true); } void OnGUI() { EditorGUILayout.LabelField(Excel路径, ExcelPath); if (GUILayout.Button(选择Excel文件)) { ExcelPath EditorUtility.OpenFilePanel(选择Excel文件, Application.dataPath /.., xlsx); } FieldNameRow EditorGUILayout.IntField(变量名所在行, FieldNameRow); TypeRow EditorGUILayout.IntField(类型所在行, TypeRow); ContentStartRow EditorGUILayout.IntField(内容开始行, ContentStartRow); OutputGunConfigPath EditorGUILayout.TextField(导出配置路径, OutputGunConfigPath); if (GUILayout.Button(生成枪械配置文件)) { ExcelToSO(); } EditorGUILayout.LabelField(hint); } static int FieldNameRow 0;//第几行是字段名 static int TypeRow 1;//第几行是类型名 static int ContentStartRow 3;//从第几行数据开始 static string ExcelPath; static string hint; static string OutputGunConfigPath Application.dataPath /Resources/GunsData.asset; static string OutputGunConfigInAssetsPath { get { return MyTools.GetAssetsPathFromFullPath(OutputGunConfigPath); } } static void ExcelToSO() { using (FileStream excelStream File.Open(ExcelPath, FileMode.Open, FileAccess.Read)) using(IExcelDataReader reader ExcelReaderFactory.CreateOpenXmlReader(excelStream)) { DataSet dataSet reader.AsDataSet(); bool isNewfalse; WeaponDataAsset dataAssetAssetDatabase.LoadAssetAtPath WeaponDataAsset(OutputGunConfigInAssetsPath); if (dataAssetnull) { dataAssetCreateInstanceWeaponDataAsset(); isNew true; } DataTable table dataSet.Tables[0]; Debug.Log($读取{table}它有{table.Rows.Count}行); for (int j ContentStartRow; j table.Rows.Count; j) { DataRow row; row table.Rows[j]; if (!string.IsNullOrEmpty(row[0].ToString())) { GunConfig entry new(); //这样赋值写通过报错能看出哪个字段失败了。直接初始化不行 entry.id row[0].ToString(); entry.itemName row[1].ToString(); entry.prefab AssetDatabase.LoadAssetAtPathGameObject(row[2].ToString()); entry.equipPrefab AssetDatabase.LoadAssetAtPathGameObject(row[3].ToString()); entry.displayPrefab AssetDatabase.LoadAssetAtPathGameObject(row[4].ToString()); entry.icon AssetDatabase.LoadAssetAtPath Sprite(row[5].ToString()); entry.desc row[6].ToString(); entry.magID row[7].ToString(); entry.muzzleJump float.Parse(row[8].ToString()); entry.xNoiseRange float.Parse(row[9].ToString()); entry.gunType (GunTypes)int.Parse(row[10].ToString()); entry.shellType(ShellType)int.Parse(row[11].ToString()); entry.bulletSpeed int.Parse(row[12].ToString()); entry.autoModes ParseEnumsAutoMode(row[13].ToString()); DamageData damageData new(); damageData.headDamageint.Parse(row[14].ToString()); damageData.trunkDamageint.Parse(row[15].ToString()); damageData.armDamageint.Parse(row[16].ToString()); damageData.legDamageint.Parse(row[17].ToString()); entry.damageDatadamageData; entry.fireAudio AssetDatabase.LoadAssetAtPathAudioClip(row[18].ToString()); entry.reloadAudio AssetDatabase.LoadAssetAtPathAudioClip(row[19].ToString()); entry.overrideAnims.reloadAnimClip AssetDatabase.LoadAssetAtPathAnimationClip(row[20].ToString()); dataAsset.gunConfigs.Add(entry); } } if (isNew) { AssetDatabase.CreateAsset(dataAsset, OutputGunConfigInAssetsPath); } EditorUtility.SetDirty(dataAsset); AssetDatabase.SaveAssets(); excelStream.Close(); AssetDatabase.Refresh(); Debug.Log(对话配置导出完毕); } } /// summary /// 把反斜杠分割的字符串解析成枚举T列表 /// /summary /// typeparam nameT/typeparam /// param namestr/param /// returns/returns static ListT ParseEnumsT(string str) where T : struct, Enum { string[] strs str.Split(/); ListT enums new(); for (int i 0; i strs.Length; i) { if (Enum.TryParse(strs[i], out T option)) { enums.Add(option); } } return enums; } }代码批量修改预制体这里给一个SO配表增加编辑器专用方法把里面配置的预制体身上的脚本的id改成和配置的id一致。注意SetDirty和SaveAssets两句执行这两句修改才保存否则会看见单机预制体和点进去脚本上的值不一致。[CreateAssetMenu(menuName MyFarm/道具配置表)] public class ItemConfigTable : ScriptableObject { public ListItemConfigSO items; #if UNITY_EDITOR [ContextMenu(为预制体注入id)] void InjectToPrefab() { for (int i 0; i items.Count; i) { if (items[i].prefab.TryGetComponent(out ItemBehavior itemBehavior)) { itemBehavior.id items[i].id; UnityEditor.EditorUtility.SetDirty(items[i].prefab); UnityEditor.AssetDatabase.SaveAssets(); Debug.Log($为{items[i].prefab}注入了id); } } } }编辑器为场景所有Guid组件生成GuidGuid组件public void GenerateGUID() { guidprefix System.Guid.NewGuid().ToString(); #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(this); // 标记组件脏确保字段变更被序列化 #endif }编辑器static void GenerateGUIDForAll() { GUIDBehavior[]guidBehaviors FindObjectsOfTypeGUIDBehavior(); for (int i 0; i guidBehaviors.Length; i) { guidBehaviors[i].GenerateGUID(); } Scene sceneSceneManager.GetActiveScene(); EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); Debug.Log($为{guidBehaviors.Length}个组件生成了guid); }注意两个脚本里的设置脏是让保存生效的关键。编辑器批量修改总结用编辑器做批量操作后一定要把修改过的对象设置脏然后保存。把/分割的枚举选项字符串读取为枚举列表的小工具public static ListT ParseEnumsT(string str) where T : struct, Enum { string[] strs str.Split(/); ListT enums new(); for (int i 0; i strs.Length; i) { if (Enum.TryParse(strs[i], out T option)) { enums.Add(option); } } return enums; }Editor文件夹Using UnityEditor打包不能包含三者的关系Using UnityEditor的脚本不是必须放在Editor文件夹。如果放在Editor文件夹就属于Assembly-CSharp-Editor程序集否则属于Assembly-CSharp程序集Using UnityEditor的脚本不放在Editor文件夹打包会报错除非把using UnityEditor改成所有UnityEditor命名空间的类前面加上UnityEditor.再把这些代码段用#if UNITY_EDITOR括起来打包时自动剔除。编辑器下Physics.Raycast检测不到地面经查明是layerMask参数直接给了需要层的序号没做位偏移。编辑器窗口显示EnumField显示LayerMask时本质还是层的序号。写一个编辑器里吸附到Terrain上的脚本using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace XXX{ [ExecuteInEditMode] public class AttachToTerrain:MonoBehaviour{ #if UNITY_EDITOR void Update() { if (!EditorApplication.isPlaying Selection.activeGameObject gameObject) { if (Physics.Raycast(transform.position Vector3.up * 100, Vector3.down, out RaycastHit hitInfo, 1000, GameSceneManager.layersGround)) { if (hitInfo.collider is TerrainCollider) { transform.position hitInfo.point; } } } } } #endif }AssetDatabase.LoadAssetAtPath T(path)能直接加载GameObject上的组件吗能。柏林噪声生成工具using System.IO; using System.Collections.Generic; using UnityEngine; using UnityEditor; public class PerlinNoiseTool : EditorWindow { [MenuItem(我的工具/柏林噪声工具)] static void ShowWindow() { GetWindowPerlinNoiseTool(柏林噪声工具); } static int width512; static int height512; static float scale1; static string texNamePerlinNoiseTex; static string pathApplication.dataPath; private void OnGUI() { GUILayout.Label(设置); width EditorGUILayout.IntField(宽,width); height EditorGUILayout.IntField(高, height); scale EditorGUILayout.FloatField(缩放, scale); texName EditorGUILayout.TextField(纹理名字, texName); EditorGUILayout.LabelField(路径, path); if (GUILayout.Button(选择路径)) { path EditorUtility.OpenFolderPanel(选择路径, Application.dataPath /.., ); } if (GUILayout.Button(生成)){ Texture2D tex new(width, height); for (int i 0; i height; i) { for (int j 0; j width; j) { float v Mathf.PerlinNoise(1f*j/width*scale, 1f*i/height*scale); tex.SetPixel(j, i, new Color(v, v, v)); } } tex.Apply(); File.WriteAllBytes(Path.Combine(path,texName.png),tex.EncodeToPNG()); AssetDatabase.Refresh(); } } }Assembly-CSharp不能引用Assembly-CSharp-Editor的函数怎么办让Assembly-CSharp的函数开放一个UnityAction参数Assembly-CSharp-Editor的代码调用它并把要做的操作传入。快捷打开persistent文件夹[MenuItem(我的工具/打开persistent)] static void OpenPersist() { EditorUtility.RevealInFinder(Application.persistentDataPath); }