Unity编辑器动态生成C#类、获取Type并创建ScriptableObject资产
问题描述
- 核心需求:动态生成物理存储的C#类
.cs文件,加载后获取对应Type,基于Type创建实例/泛型对象,实际用于Unity编辑器下状态机模块的ScriptableObject子类及对应资产一键生成工具 - 已实现逻辑:模板读取、类文件内容替换、
.cs文件写入、AssetDatabase保存刷新 - 现存异常:刷新后立即在当前AppDomain查找新生成类的
Type返回null,提前存在的类可正常获取Type - 初始逻辑示例:
void CreateNewClass(string name) { File file = CreateFile(name); Type type = GetTypeOfFile(file); List<type> someList = new List<type>(); } void CreateFile(string name) { //获取模板文件 //复制文件 //修改文件内的类名等内容 //返回文件 } Type GetTypeOfFile(File file) { //获取该文件对应的类(Type) //返回类型 }
- 业务场景说明:状态机基类为
public abstract class State : ScriptableObject{},工具逻辑为输入状态名后自动生成public class Idle : State{}这类子类文件,再调用资产创建接口生成对应.asset文件,现有问题代码如下:
using System; using System.IO; using System.Linq; using System.Text; using AI.StateMachine.BaseClasses; using UnityEditor; using UnityEngine; namespace AI.StateMachine.Editor { public class ActionWizard : EditorWindow { [MenuItem("Tools/AI/StateMachine/ActionWizard")] static void Init() { // 获取已打开的窗口,若不存在则新建 ActionWizard window = (ActionWizard) EditorWindow.GetWindow(typeof(ActionWizard)); window.Show(); } private string actionName; void OnGUI() { actionName = GUILayout.TextField(actionName); if (GUILayout.Button("Create Action")) { //读取基类文件内容 string baseClassContent = File.ReadAllText($"{Application.dataPath}/Scripts/AI/StateMachine/BaseClasses/SM_Action.cs"); //修改基类内容生成派生类代码 string newClassContent = baseClassContent .Replace("SM_Action", actionName) .Replace("ScriptableObject", "SM_Action") .Replace("abstract ", "") .Replace("namespace AI.StateMachine.BaseClasses", "namespace AI.StateMachine.Assets.Actions.Scripts") .Replace("//", ""); //创建派生类文件 using (FileStream fs = File.Create($"{Application.dataPath}/Scripts/AI/StateMachine/Assets/Actions/Scripts/{actionName}.cs")) { byte[] info = new UTF8Encoding(true).GetBytes(newClassContent); fs.Write(info, 0, info.Length); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); //获取创建的类的Type var allTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes()).ToList(); var inputType = allTypes.Find(type => typeof(SM_Action).IsAssignableFrom(type) && type.Name == actionName); //此处返回null.... //如果类是之前创建好的,则可以正常找到目标类型 Debug.Log(inputType); //创建对应类型的ScriptableObject实例 // var so = ScriptableObject.CreateInstance(inputType); //保存ScriptableObject资产 // AssetDatabase.CreateAsset(so,"Assets/Scripts/AI/StateMachine/Actions/"+actionName + ".asset"); // AssetDatabase.Refresh(); } } } }
问题根因
AssetDatabase.Refresh()默认是异步执行的,调用该方法后Unity不会同步完成脚本解析、编译、程序集重载的全流程。在同一段同步代码中立即遍历AppDomain的所有程序集时,新生成的.cs文件还未被编译加载到运行域中,自然无法找到对应Type。
实现方案
1. 延迟执行类型查找逻辑,等待脚本编译重载完成
不要在调用Refresh的同个同步上下文中查找Type,利用Unity提供的脚本重载完成回调,等新脚本编译完成、加载进AppDomain后再执行后续逻辑。为了避免脚本重载导致编辑器窗口状态丢失,可将待创建的类名临时存在EditorPrefs中。
2. 使用非泛型反射API创建实例,无需构造泛型类型
不需要通过反射构造List<具体类型>这类泛型定义,Unity已经提供了适配的非泛型重载:
ScriptableObject.CreateInstance(Type type)可直接传入Type创建对应ScriptableObject实例AssetDatabase.CreateAsset(Object asset, string path)支持直接传入创建好的实例,不需要调用泛型版本- 如果确实需要构造对应类型的泛型集合(比如
List<T>),可通过typeof(List<>).MakeGenericType(type)获取泛型类型,再用Activator.CreateInstance创建实例即可。
修正后的完整代码
using System; using System.IO; using System.Linq; using System.Text; using AI.StateMachine.BaseClasses; using UnityEditor; using UnityEngine; namespace AI.StateMachine.Editor { public class ActionWizard : EditorWindow { private const string PENDING_CREATE_KEY = "StateMachine_PendingCreateAction"; private string actionName; [MenuItem("Tools/AI/StateMachine/ActionWizard")] static void Init() { ActionWizard window = (ActionWizard)EditorWindow.GetWindow(typeof(ActionWizard)); window.Show(); } // 脚本重载完成后自动执行待处理的创建任务 [UnityEditor.Callbacks.DidReloadScripts] private static void OnScriptsReloaded() { string pendingAction = EditorPrefs.GetString(PENDING_CREATE_KEY, string.Empty); if (string.IsNullOrEmpty(pendingAction)) return; EditorPrefs.DeleteKey(PENDING_CREATE_KEY); CreateActionAsset(pendingAction); } private static void CreateActionAsset(string actionName) { // 查找目标Type,加异常处理避免部分程序集反射报错中断流程 var allTypes = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(assembly => { try { return assembly.GetTypes(); } catch { return Array.Empty<Type>(); } }) .ToList(); var targetType = allTypes.Find(type => typeof(SM_Action).IsAssignableFrom(type) && !type.IsAbstract && type.Name == actionName); if (targetType == null) { Debug.LogError($"创建失败,未找到类型{actionName}"); return; } // 创建实例与资产,路径需匹配自身项目目录结构 var so = ScriptableObject.CreateInstance(targetType); string savePath = $"Assets/Scripts/AI/StateMachine/Assets/Actions/{actionName}.asset"; // 确保目录存在 Directory.CreateDirectory(Path.GetDirectoryName(savePath)); AssetDatabase.CreateAsset(so, savePath); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"成功创建Action资产:{savePath}", so); } void OnGUI() { actionName = GUILayout.TextField(actionName); if (GUILayout.Button("Create Action")) { if (string.IsNullOrWhiteSpace(actionName)) { Debug.LogError("Action名称不能为空"); return; } // 校验类名合法性 if (!char.IsLetter(actionName[0]) && actionName[0] != '_') { Debug.LogError("类名必须以字母或下划线开头"); return; } string scriptSaveDir = $"{Application.dataPath}/Scripts/AI/StateMachine/Assets/Actions/Scripts"; Directory.CreateDirectory(scriptSaveDir); string scriptPath = $"{scriptSaveDir}/{actionName}.cs"; // 避免重复创建同名脚本 if (File.Exists(scriptPath)) { Debug.LogError($"已存在同名脚本文件:{scriptPath}"); return; } // 读取模板生成类代码 string baseClassContent = File.ReadAllText($"{Application.dataPath}/Scripts/AI/StateMachine/BaseClasses/SM_Action.cs"); string newClassContent = baseClassContent .Replace("SM_Action", actionName) .Replace("ScriptableObject", "SM_Action") .Replace("abstract ", "") .Replace("namespace AI.StateMachine.BaseClasses", "namespace AI.StateMachine.Assets.Actions.Scripts") .Replace("//", ""); // 写入脚本文件 File.WriteAllText(scriptPath, newClassContent, new UTF8Encoding(true)); // 存储待创建的类名,等待脚本重载完成后执行创建 EditorPrefs.SetString(PENDING_CREATE_KEY, actionName); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); } } } }
补充说明
如果需要强制同步刷新不等待,可以给AssetDatabase.Refresh传入ImportAssetOptions.ForceSynchronousImport参数,但该方式在大项目中可能触发长时间卡顿,优先使用脚本重载回调的方案。
如果需要创建对应类型的List泛型集合,参考如下代码:
Type listType = typeof(List<>).MakeGenericType(targetType); object listInstance = Activator.CreateInstance(listType);
内容的提问来源于stack exchange,提问作者Pascal
相关产品推荐
相关产品推荐

