You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 01:57:22