Unity:如何在自定义编辑器中动态附加未知脚本到游戏对象?

3

我目前正在为Unity编辑器制作一个系统(自定义检查器和自定义窗口),旨在自动化并方便制作我们正在制作的游戏的艺术家们,但我遇到了瓶颈。

我试图找到一种方法,通过编辑器中的文本框输入和GUI按钮来动态添加未知脚本到场景中的gameobject中。艺术家/程序员将在文本框中键入脚本的名称,然后它会搜索并添加到gameobject中,但我不知道如何继续进行,特别是因为Unity 5.3的gameObject.AddComponent()的某些函数已过时。

这是我尝试做的:

public string scriptname;
GameObject obj = null;
scriptname = EditorGUILayout.TextField("Script name:", scriptname, GUILayout.MaxHeight(25));
if (GUILayout.Button("Attach script"))
{
    //search for the script to check if it exists, using DirectoryInfo
    DirectoryInfo dir = new DirectoryInfo(Application.dataPath);
    FileInfo[] info = dir.GetFiles("*.*", SearchOption.AllDirectories);
    foreach (FileInfo f in info) // cycles through all the files
    {
        if(f.Name == scriptname)
        {
            //attaches to the gameobject (NOT WORKING)
            System.Type MyScriptType = System.Type.GetType(scriptname + ",Assembly-CSharp"); 
            obj.AddComponent(MyScriptType);
        }
    }
}

当然,这只是一个总结版本,我从脚本的不同部分复制了相关行。

但它不起作用。 有任何想法吗?


什么确切地不起作用?是找到脚本、将其附加到游戏对象还是两者都有问题?当您执行代码时会发生什么? - Nahuel Ianni
在这里,问题是将其附加到游戏对象中,在GUILayout.Button内的两行代码。为了找到它我正在使用DirectoryInfo(实际上是为了检查它是否存在)。我也将上面的搜索系统添加到代码中。 - Nathan Danzmann
要查找类型是否存在,只需执行 assembly.GetTypes().Any(t => t.Name == scriptname); - mrogal.ski
你在控制台上得到任何输出吗?例如警告或错误? - Johan Lindkvist
@NathanDanzmann 你试过我添加的解决方案了吗? - Programmer
6个回答

6
经过广泛的实验,我成功得到了这个。它也涵盖了所有的Unity组件。只是将其制作为扩展方法,让生活更加简单。
public static class ExtensionMethod
{
    public static Component AddComponentExt(this GameObject obj, string scriptName)
    {
        Component cmpnt = null;


        for (int i = 0; i < 10; i++)
        {
            //If call is null, make another call
            cmpnt = _AddComponentExt(obj, scriptName, i);

            //Exit if we are successful
            if (cmpnt != null)
            {
                break;
            }
        }


        //If still null then let user know an exception
        if (cmpnt == null)
        {
            Debug.LogError("Failed to Add Component");
            return null;
        }
        return cmpnt;
    }

    private static Component _AddComponentExt(GameObject obj, string className, int trials)
    {
        //Any script created by user(you)
        const string userMadeScript = "Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
        //Any script/component that comes with Unity such as "Rigidbody"
        const string builtInScript = "UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "Image"
        const string builtInScriptUI = "UnityEngine.UI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "Networking"
        const string builtInScriptNetwork = "UnityEngine.Networking, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "AnalyticsTracker"
        const string builtInScriptAnalytics = "UnityEngine.Analytics, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "AnalyticsTracker"
        const string builtInScriptHoloLens = "UnityEngine.HoloLens, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";

        Assembly asm = null;

        try
        {
            //Decide if to get user script or built-in component
            switch (trials)
            {
                case 0:

                    asm = Assembly.Load(userMadeScript);
                    break;

                case 1:
                    //Get UnityEngine.Component Typical component format
                    className = "UnityEngine." + className;
                    asm = Assembly.Load(builtInScript);
                    break;
                case 2:
                    //Get UnityEngine.Component UI format
                    className = "UnityEngine.UI." + className;
                    asm = Assembly.Load(builtInScriptUI);
                    break;

                case 3:
                    //Get UnityEngine.Component Video format
                    className = "UnityEngine.Video." + className;
                    asm = Assembly.Load(builtInScript);
                    break;

                case 4:
                    //Get UnityEngine.Component Networking format
                    className = "UnityEngine.Networking." + className;
                    asm = Assembly.Load(builtInScriptNetwork);
                    break;
                case 5:
                    //Get UnityEngine.Component Analytics format
                    className = "UnityEngine.Analytics." + className;
                    asm = Assembly.Load(builtInScriptAnalytics);
                    break;

                case 6:
                    //Get UnityEngine.Component EventSystems format
                    className = "UnityEngine.EventSystems." + className;
                    asm = Assembly.Load(builtInScriptUI);
                    break;

                case 7:
                    //Get UnityEngine.Component Audio format
                    className = "UnityEngine.Audio." + className;
                    asm = Assembly.Load(builtInScriptHoloLens);
                    break;

                case 8:
                    //Get UnityEngine.Component SpatialMapping format
                    className = "UnityEngine.VR.WSA." + className;
                    asm = Assembly.Load(builtInScriptHoloLens);
                    break;

                case 9:
                    //Get UnityEngine.Component AI format
                    className = "UnityEngine.AI." + className;
                    asm = Assembly.Load(builtInScript);
                    break;
            }
        }
        catch (Exception e)
        {
            //Debug.Log("Failed to Load Assembly" + e.Message);
        }

        //Return if Assembly is null
        if (asm == null)
        {
            return null;
        }

        //Get type then return if it is null
        Type type = asm.GetType(className);
        if (type == null)
            return null;

        //Finally Add component since nothing is null
        Component cmpnt = obj.AddComponent(type);
        return cmpnt;
    }
}

用法:
gameObject.AddComponentExt("YourScriptOrComponentName");

重要的是要理解我是如何做到的,这样您就可以在任何未来的Unity更新中为新组件添加支持。

对于任何由用户创建的脚本

1。弄清楚Assembly.Load函数中???需要什么。

Assembly asm = Assembly.Load("???");

你可以通过将这段代码放入你的脚本中来实现这个。
Debug.Log("Info: " + this.GetType().Assembly);

我得到了:Assembly-CSharp,Version=0.0.0.0,Culture=neutral,PublicKeyToken=null 现在我们应该用这个替换掉???
Assembly asm = Assembly.Load("Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");

找出在 asm.GetType 函数中需要放置什么内容。
Assembly asm = Assembly.Load("Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
Type type = asm.GetType(???); 

在这种情况下,它只是你想要添加到游戏对象的脚本名称。
假设你的脚本名字是NathanScript:
Assembly asm = Assembly.Load("Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
Type type = asm.GetType("NathanScript"); 
gameObject.AddComponent(type);

对于由Unity内置的脚本/组件脚本而非用户创建的脚本::

其中一个例子是 RigidbodyLinerendererImage 这些组件,任何非用户创建的组件都属于这一类。

1.查找在 Assembly.Load 函数中的 ??? 部分需要包含什么内容。

Assembly asm = Assembly.Load("???");

你可以通过将以下内容放入你的脚本中来实现这个功能:

ParticleSystem pt = gameObject.AddComponent<ParticleSystem>();
Debug.Log("Info11: " + pt.GetType().Assembly);

我得到了:

我得到了:UnityEngine,Version = 0.0.0.0,Culture = neutral,PublicKeyToken = null

我们现在应该用这个替换 ???

Assembly asm = Assembly.Load("UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");

找出在`asm.GetType`函数中的`???`需要填入什么内容。
Assembly asm = Assembly.Load("UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
Type type = asm.GetType(???); 

你可以通过将以下内容放入你的脚本中来实现这一点:
ParticleSystem pt = gameObject.AddComponent<ParticleSystem>();
Debug.Log("Info: " + pt.GetType());

我得到的是:UnityEngine.ParticleSystem 请记住,这里使用ParticleSystem作为示例。因此,将传递给asm.GetType函数的最终字符串将如下计算:
string typeString = "UnityEngine." + componentName;

假设您想要添加的组件是LineRenderer:
Assembly asm = Assembly.Load("UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
string typeString = "UnityEngine." + "LineRenderer";
Type type = asm.GetType(typeString); 
gameObject.AddComponent(type);

将其整合为扩展方法:

正如您所看到的,添加您创建的脚本和Unity提供的脚本/组件需要完全不同的过程。您可以通过检查类型是否为null来解决此问题。如果类型为null,则执行另一步骤。如果另一步骤也是null,那么该脚本将简单地不存在


@m.rogalski “这是按设计制作的”。当我编写该函数时,我问自己“当您有两个具有相同名称但位于不同命名空间中的类时会发生什么?”那里就会出现冲突,所以所有要做的就是键入完全限定名称。它会找到它。因此,“gameObject.AddComponentExt(“Namespace1.Namespace2.ClassDefinition”);”将起作用,并且它将仅查找该脚本。 - Programmer
@程序员 碰巧的是,我刚刚尝试过,它仍然无法加载并引发您创建的“添加组件失败”异常。在捕获中,它不断引发 “UnityEngine.Analytics” 和“UnityEngine.HoloLens” 模块的“FileNotFoundException” 异常。 - Nathan Danzmann
这段代码是通过Unity 5.6 beta进行测试的。您可能使用的是较低版本,因此缺少HoloLens和Analytics以及其他未启用的程序集。这就是为什么我注释了“Debug.Log”语句。不要取消注释。这段代码应该适用于任何脚本或组件。如果不行,请告诉我。像我的答案一样将“Debug.Log”注释掉,异常就能正常处理了。 - Programmer
使用 gameObject.AddComponentExt("ParticleSystem"); 进行简单的测试,并告诉我您的 GameObject 是否已附加 ParticleSystem - Programmer
是的,它添加了粒子系统、视频播放器、网格、音频、画布等。我尝试过的任何标准组件都可以工作,除了自定义脚本(对于延迟再次道歉...)。 - Nathan Danzmann
显示剩余8条评论

1
我建议这样做:

if(GUILayout.Button("Attach script"))
{
    // check if type is contained in your assembly:
    Type type = typeof(MeAssemblyType).Assembly.GetTypes().FirstOrDefault(t => t.Name == scriptname);
    if(type != null)
    {
        // script exists in the same assembly that MeAssemblyType is
        obj.AddComponent(type); // add the component
    }
    else
    { 
        // display some error message
    }
}

当然,如果您使用一些包含其他组件的插件(依赖项),这种方法可能会失败,但是您可以通过检查程序集的依赖项来解决这个问题:
typeof(MeAssemblyType) // your type from Assembly-CSharp 
    .Assembly // Assembly-CSharp assembly
    .GetReferencedAssemblies() // get referenced assemblies
    .FirstOrDefault(m => 
        m.Assembly // from this assembly
        .GetTypes() // get all types
        .FirstOrDefault(t => 
            t.Name == scriptname // select first one that matches the name
        )
    )

备注:

GetReferencedAssemblies方法只会返回已经被你的程序集“使用”(加载)的程序集。为了更好地理解,可以假设你正在引用这些程序集:

  1. System.Xml,
  2. NewtonsoftJson

这段代码:

static void Main()
{
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(<some_xml_input>);
}

然后,GetReferencedAssemblies 的输出将类似于以下内容:

>>> System.Xml, Version=<version>, Culture=neutral, PublicKeyToken=<key>

意思是它不会加载NewtonsoftJson,因为它没有在该程序集内使用。
更好的建议:
我建议您混合@Programmer答案中的方法,但不要加载程序集,因为它们已经在Unity的编辑器启动时与您的项目一起加载。相反,使用GetReferencedAssemblies方法,从那里调用GetTypes方法检索该程序集中的所有可能类型(这将很慢,但将保证您获得所需的结果)。之后,您只需使用FirstOrDefault或自己遍历Type[]来找到所需的类型。

0

这仍然是可能的。使用这个。

UnityEngineInternal.APIUpdaterRuntimeServices.AddComponent(GameObject go, "", string componentName);

希望这有所帮助。

已弃用意味着它很快将被移除。它在内部使用 AddComponent(string) - Programmer
问题涉及当前的Unity版本,而不是未来的版本。这个解决方案目前可以在Unity 5.5中使用。 - Greg Lukosek
你猜你还没有尝试过这个是否有效?@程序员 - Greg Lukosek
上次我尝试做这个的时候,失败得很惨,因为我找到的每个API都已经过时了。我会再试一次并让你知道结果,但在答案中使用已弃用的东西不是一个好主意。 - Programmer
你可以检查我的答案。 - Programmer
显示剩余3条评论

0

这对我起作用了 obj.AddComponent(System.Type.GetType(ScriptToAdd.name + ",Assembly-CSharp"));

  • ScriptToAdd 是 UnityEngine.Object

0

反编译Unity的AddComponentWindow。同时添加链接:
AddComponentAdjusted

然后将窗口命名为类似于这样的名称:

  ws.winx.editor.windows.AddComponentWindow.Show(rect);

            ws.winx.editor.windows.AddComponentWindow.OnClose += OnCloseComponentSelectedFromPopUpMenu;
            ws.winx.editor.windows.AddComponentWindow.ComponentSelected += (menuPath) => ComponentSelectedFromPopUpMenu(positionData.Item1, menuPath);

处理返回值

    private void ComponentSelectedFromPopUpMenu(Vector2 position, string menuPath) {
        
        
                    MonoScript monoScript;
        
                    char[] kPathSepChars = new char[]
                    {
                        '/',
                        '\\'
                    };
        
                    menuPath = menuPath.Replace(" ", "");
                    string[] pathElements = menuPath.Split(kPathSepChars);
        
                    string fileName = pathElements[pathElements.Length - 1].Replace(".cs", "");
        
        
        
        
                    if (pathElements[0] == "Assets") {
        
                        Debug.LogWarning("Unity need to compile new added file so can be included");
        
        
                    } else if (pathElements.Length == 2) {
        
//use fileName
                        //do something
        
                        
                    } else if (pathElements[1] == "Scripts") {//Component/Scripts/MyScript.cs
                        
        
                        string[] guids = AssetDatabase.FindAssets("t:Script " + fileName.Replace(".cs", ""));
        
                        if (guids.Length > 0) {
        
                            for (int i = 0; i < guids.Length; i++) {
                                monoScript = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[i]), typeof(MonoScript)) as MonoScript;
                                Type typet = monoScript.GetClass();
        
                                if (typet == null) continue;
        
        
                               
        
                    } else {//Component/Physics/Rigidbody
                        //try to find by type, cos probably Unity type
                        Type unityType = ReflectionUtility.GetType("UnityEngine." + fileName);
        
                        if (unityType != null) {
        
    //do something
        
                            return;
        
                        }
        
        
        
        
        
        //Based on attribute  [AddComponentMenu("Logic/MyComponent")] 
                        //Component/Logics/MyComponent
                        string[] guids = AssetDatabase.FindAssets("t:Script " + fileName.Replace(".cs", ""));
        
                        if (guids.Length > 0) {
        
                            for (int i = 0; i < guids.Length; i++) {
                                monoScript = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[i]), typeof(MonoScript)) as MonoScript;
                                Type typet = monoScript.GetClass();
        
                                if (typet == null) continue;
        
                                object[] addComponentMenuAttributes = typet.GetCustomAttributes(typeof(AddComponentMenu), true);
        
        
        
                                if (addComponentMenuAttributes != null && addComponentMenuAttributes.Length > 0 && "Component/" + ((AddComponentMenu)addComponentMenuAttributes[0]).componentMenu == menuPath)
                                {
        
                                    //do somethings
        
                                }
                            }
        
        
                        }
        
        
                    }
                }

0

您可以在应用程序域中迭代程序集。 然后在该程序集的GetTypes()中搜索该名称(例如@mrogal.ski的答案)

static Type FindType(string typeName, string namespaceString = "") 
{
    foreach(var asm in AppDomain.CurrentDomain.GetAssemblies()) 
    {
        var type = asm.GetTypes().FirstOrDefault(t => 
            t.Name == typeName && (string.IsNullOrEmpty(namespaceString) || namespaceString == t.Namespace));

        if (type != null) { return type; }
    }
    return null;
}

使用方法如下:

var someGameObject = new GameObject("FindTypeDemoObject");
var rbType = FindType("Rigidbody");
someGameObject.AddComponent(rbType);

var someMonoBType = FindType("SomeMonoB", "Fake.Example.Namespace");
someGameObject.AddComponent(someMonoBType);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接