使用asmdef生成的程序集中的单行为

时间:2018-11-26 15:46:00

标签: c# unity3d

我们想使用汇编文件而不是.cs脚本来分发我们的项目。 我们认为这要归功于程序集定义文件,因为统一会为它们所引用的脚本创建程序集文件。 原来,在删除.cs文件并放入程序集时,我们遇到了一个问题: 程序集中定义的monobehaviors(因此以前在我们的脚本中)不能手动添加到场景中:

  

“由于找不到脚本类而无法添加脚本组件xxx”

如果我们通过脚本(即AddComponent)添加组件,则它会起作用。

我正在使用Unity 2017.3.f1生成程序集文件

有一个技巧可以使这项工作吗?还是我应该尝试使用另一种方法来生成程序集?

4 个答案:

答案 0 :(得分:2)

在这里

简单的答案是:不要同时保留asmdef和程序集文件。如果用生成的程序集替换脚本,请删除asmdef文件

我最终要做的事情大致如下(这是出于CI目的): 首先,我们需要确保Unity编译了程序集文件。因此,我在“编辑器”文件夹中有一个GenerateAssemblies.cs文件,该文件可以从命令行执行:

GenerateAssemblies.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;


[InitializeOnLoad]
public static class GenerateAssemblies
{
    private static string BATCH_MODE_PARAM = "-batchmode";
    private const string REPLACE_ASSEMBLY_PARAM = "-replaceassembly";

    static GenerateAssemblies()
    {
        List<String> args = Environment.GetCommandLineArgs().ToList();

        if (args.Any(arg => arg.ToLower().Equals(BATCH_MODE_PARAM)))
        {
            Debug.LogFormat("GenerateAssemblies will try to parse the command line to replace assemblies.\n" +
               "\t Use {0} \"assemblyname\" for every assembly you wish to replace"
               , REPLACE_ASSEMBLY_PARAM);
        }

        if (args.Any(arg => arg.ToLower().Equals(REPLACE_ASSEMBLY_PARAM))) // is a replacement requested ?
        {
            int lastIndex = 0;
            while (lastIndex != -1)
            {
                lastIndex = args.FindIndex(lastIndex, arg => arg.ToLower().Equals(REPLACE_ASSEMBLY_PARAM));
                if (lastIndex >= 0 && lastIndex + 1 < args.Count)
                {
                    string assemblyToReplace = args[lastIndex + 1];
                    if (!assemblyToReplace.EndsWith(ReplaceAssemblies.ASSEMBLY_EXTENSION))
                        assemblyToReplace = assemblyToReplace + ReplaceAssemblies.ASSEMBLY_EXTENSION;
                    ReplaceAssemblies.instance.AddAssemblyFileToReplace(assemblyToReplace);
                    Debug.LogFormat("Added assembly {0} to the list of assemblies to replace.", assemblyToReplace);
                    lastIndex++;
                }
            }
            CompilationPipeline.assemblyCompilationFinished += ReplaceAssemblies.instance.ReplaceAssembly; /* This serves as callback after Unity as compiled an assembly */

            Debug.Log("Forcing recompilation of all scripts");
            // to force recompilation
            PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, PlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone) + ";DUMMY_SYMBOL");
            AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
        }
    }

}

然后我在编辑器文件夹中有一个ReplaceAssemblies.cs文件:

  1. 找到与asmdef文件对应的程序集文件
  2. 保存脚本文件的guid / classes对应
  3. 将脚本文件移动到临时文件夹中
  4. 将程序集与asmdef文件移动到同一文件夹中
  5. 将asmdef移至临时文件夹
  6. 替换装配中每个脚本的Guid和File ID值(以避免破坏场景和预制件中的引用)

ReplaceAssemblies.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;

public class ReplaceAssemblies : ScriptableSingleton<ReplaceAssemblies>
{

    public static string ASSEMBLY_EXTENSION = ".dll";
    public static string ASSEMBLY_DEFINITION_EXTENSION = ".asmdef";

    [SerializeField]
    private List<String> assembliesFilesToReplace = new List<string>();

    [SerializeField]
    private List<string> pathsOfAssemblyFilesInAssetFolder = new List<string>();
    [SerializeField]
    private List<string> pathsOfAssemblyFilesCreatedByUnity = new List<string>();

    [SerializeField]
    private string tempSourceFilePath;

    private static readonly string[] fileListPath = { "*.prefab", "*.unity", "*.asset" };


    public string TempSourceFilePath
    {
        get
        {
            if (String.IsNullOrEmpty(tempSourceFilePath))
            {
                tempSourceFilePath = FileUtil.GetUniqueTempPathInProject();
            }

            return tempSourceFilePath;
        }
    }

    void OnEnable()
    {
        Debug.Log("temp dir : " + TempSourceFilePath);
    }

    public void ReplaceAssembly(string assemblyPath, CompilerMessage[] messages)
    {
        string assemblyFileName = assembliesFilesToReplace.Find(assembly => assemblyPath.EndsWith(assembly));
        // is this one of the assemblies we want to replace ?
        if (!String.IsNullOrEmpty(assemblyFileName))
        {
            string[] assemblyDefinitionFilePaths = Directory.GetFiles(".", Path.GetFileNameWithoutExtension(assemblyFileName) + ASSEMBLY_DEFINITION_EXTENSION, SearchOption.AllDirectories);
            if (assemblyDefinitionFilePaths.Length > 0)
            {
                string assemblyDefinitionFilePath = assemblyDefinitionFilePaths[0];
                ReplaceAssembly(assemblyDefinitionFilePath);
            }
        }
    }

    public void AddAssemblyFileToReplace(string assemblyFile)
    {
        assembliesFilesToReplace.Add(assemblyFile);
    }

    private void ReplaceAssembly(string assemblyDefinitionFilePath)
    {
        Debug.LogFormat("Replacing scripts for assembly definition file {0}", assemblyDefinitionFilePath);
        string asmdefDirectory = Path.GetDirectoryName(assemblyDefinitionFilePath);
        string assemblyName = Path.GetFileNameWithoutExtension(assemblyDefinitionFilePath);
        Assembly assemblyToReplace = CompilationPipeline.GetAssemblies().ToList().Find(assembly => assembly.name.ToLower().Equals(assemblyName.ToLower()));
        string assemblyPath = assemblyToReplace.outputPath;
        string assemblyFileName = Path.GetFileName(assemblyPath);
        string[] assemblyFilePathInAssets = Directory.GetFiles("./Assets", assemblyFileName, SearchOption.AllDirectories);

        // save the guid/classname correspondance of the scripts that we will remove
        Dictionary<string, string> oldGUIDToClassNameMap = new Dictionary<string, string>();
        if (assemblyFilePathInAssets.Length <= 0)
        {
            // Move all script files outside the asset folder
            foreach (string sourceFile in assemblyToReplace.sourceFiles)
            {
                string tempScriptPath = Path.Combine(TempSourceFilePath, sourceFile);
                Directory.CreateDirectory(Path.GetDirectoryName(tempScriptPath));
                if (!File.Exists(sourceFile))
                    Debug.LogErrorFormat("File {0} does not exist while the assembly {1} references it.", sourceFile, assemblyToReplace.name);
                Debug.Log("will move " + sourceFile + " to " + tempScriptPath);
                // save the guid of the file because we may need to replace it later
                MonoScript monoScript = AssetDatabase.LoadAssetAtPath<MonoScript>(sourceFile);
                if (monoScript != null && monoScript.GetClass() != null)
                    oldGUIDToClassNameMap.Add(AssetDatabase.AssetPathToGUID(sourceFile), monoScript.GetClass().FullName);
                FileUtil.MoveFileOrDirectory(sourceFile, tempScriptPath);
            }

            Debug.Log("Map of GUID/Class : \n" + String.Join("\n", oldGUIDToClassNameMap.Select(pair => pair.Key + " : " + pair.Value).ToArray()));

            string finalAssemblyPath = Path.Combine(asmdefDirectory, assemblyFileName);
            Debug.Log("will move " + assemblyPath + " to " + finalAssemblyPath);
            FileUtil.MoveFileOrDirectory(assemblyPath, finalAssemblyPath);
            string tempAsmdefPath = Path.Combine(TempSourceFilePath, Path.GetFileName(assemblyDefinitionFilePath));
            Debug.Log("will move " + assemblyDefinitionFilePath + " to " + tempAsmdefPath);
            FileUtil.MoveFileOrDirectory(assemblyDefinitionFilePath, tempAsmdefPath);
            // Rename the asmdef meta file to the dll meta file so that the dll guid stays the same
            FileUtil.MoveFileOrDirectory(assemblyDefinitionFilePath + ".meta", finalAssemblyPath + ".meta");
            pathsOfAssemblyFilesInAssetFolder.Add(finalAssemblyPath);
            pathsOfAssemblyFilesCreatedByUnity.Add(assemblyPath);


            // We need to refresh before accessing the assets in the new assembly
            AssetDatabase.Refresh();


            // We need to remove .\ when using LoadAsslAssetsAtPath
            string cleanFinalAssemblyPath = finalAssemblyPath.Replace(".\\", "");
            var assetsInAssembly = AssetDatabase.LoadAllAssetsAtPath(cleanFinalAssemblyPath);

            // list all components in the assembly file. 
            var assemblyObjects = assetsInAssembly.OfType<MonoScript>().ToArray();

            // save the new GUID and file ID for the MonoScript in the new assembly
            Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap = new Dictionary<string, KeyValuePair<string, long>>();
            // for each component, replace the guid and fileID file
            for (var i = 0; i < assemblyObjects.Length; i++)
            {
                long dllFileId;
                string dllGuid = null;
                if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(assemblyObjects[i], out dllGuid, out dllFileId))
                {
                    string fullClassName = assemblyObjects[i].GetClass().FullName;
                    newMonoScriptToIDsMap.Add(fullClassName, new KeyValuePair<string, long>(dllGuid, dllFileId));
                }
            }

            Debug.Log("Map of Class/GUID:FILEID : \n" + String.Join("\n", newMonoScriptToIDsMap.Select(pair => pair.Key + " : " + pair.Value.Key + " - " + pair.Value.Value).ToArray()));

            ReplaceIdsInAssets(oldGUIDToClassNameMap, newMonoScriptToIDsMap);
        }


        else
        {
            Debug.Log("Already found an assembly file named " + assemblyFileName + " in asset folder");
        }
    }

    /// <summary>
    /// Replace ids in all asset files using the given maps
    /// </summary>
    /// <param name="oldGUIDToClassNameMap">Maps GUID to be replaced => FullClassName</param>
    /// <param name="newMonoScriptToIDsMap">Maps FullClassName => new GUID, new FileID</param>
    private static void ReplaceIdsInAssets(Dictionary<string, string> oldGUIDToClassNameMap, Dictionary<string, KeyValuePair<string, long>> newMonoScriptToIDsMap)
    {
        StringBuilder output = new StringBuilder("Report of replaced ids : \n");
        // list all the potential files that might need guid and fileID update
        List<string> fileList = new List<string>();
        foreach (string extension in fileListPath)
        {
            fileList.AddRange(Directory.GetFiles(Application.dataPath, extension, SearchOption.AllDirectories));
        }
        foreach (string file in fileList)
        {
            string[] fileLines = File.ReadAllLines(file);

            for (int line = 0; line < fileLines.Length; line++)
            {
                //find all instances of the string "guid: " and grab the next 32 characters as the old GUID
                if (fileLines[line].Contains("guid: "))
                {
                    int index = fileLines[line].IndexOf("guid: ") + 6;
                    string oldGUID = fileLines[line].Substring(index, 32); // GUID has 32 characters.
                    if (oldGUIDToClassNameMap.ContainsKey(oldGUID) && newMonoScriptToIDsMap.ContainsKey(oldGUIDToClassNameMap[oldGUID]))
                    {
                        fileLines[line] = fileLines[line].Replace(oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
                        output.AppendFormat("File {0} : Found GUID {1} of class {2}. Replaced with new GUID {3}.", file, oldGUID, oldGUIDToClassNameMap[oldGUID], newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Key);
                        if (fileLines[line].Contains("fileID: "))
                        {
                            index = fileLines[line].IndexOf("fileID: ") + 8;
                            int index2 = fileLines[line].IndexOf(",", index);
                            string oldFileID = fileLines[line].Substring(index, index2 - index); // GUID has 32 characters.
                            fileLines[line] = fileLines[line].Replace(oldFileID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
                            output.AppendFormat("Replaced fileID {0} with {1}", oldGUID, newMonoScriptToIDsMap[oldGUIDToClassNameMap[oldGUID]].Value.ToString());
                        }
                        output.Append("\n");
                    }
                }
            }
            //Write the lines back to the file
            File.WriteAllLines(file, fileLines);
        }
        Debug.Log(output.ToString());
    }

    [MenuItem("Tools/Replace Assembly")]
    public static void ReplaceAssemblyMenu()
    {
        string assemblyDefinitionFilePath = EditorUtility.OpenFilePanel(
            title: "Select Assembly Definition File",
            directory: Application.dataPath,
            extension: ASSEMBLY_DEFINITION_EXTENSION.Substring(1));
        if (assemblyDefinitionFilePath.Length == 0)
            return;

        instance.ReplaceAssembly(assemblyDefinitionFilePath);

    }
}

答案 1 :(得分:1)

我遇到了这个问题,就像您一样,我使用了asmdef文件提供的信息来提供构建程序集所需的所有信息(哪些.cs文件,哪些引用,定义等)。

我发现问题是我正在创建的DLL与我用来提供信息的asmdef文件同名。即使不再编译asmdef文件(因为已删除所有脚本来构建DLL),它仍在干扰项目。

所以对我来说,从编辑器内部和内部脚本访问脚本之间的不一致是因为在项目中存在一个具有相同名称的DLL和asmdef文件。

为编译后的DLL提供一个不同的名称或删除asmdef文件是我的解决方案。

答案 2 :(得分:0)

只需使用Unity 2019.3.0b1进行测试。

测试课程的内容:

using System.Reflection;
using UnityEngine;

namespace Assets.Test
{
    public class TestBehaviour : MonoBehaviour
    {
        void Start()
        {
            Debug.Log(Assembly.GetAssembly(GetType()));
        }
    }
}

第一个包含源代码和程序集定义文件的项目

First project with source code and assembly definition file

带有生成的DLL的第二个项目,按预期工作

Second project with the generated DLL, working as expected

答案 3 :(得分:0)

就我而言,使用asmdef仅迫使unity3d将您的脚本编译成单独的程序集,然后由您的项目引用该程序集。 实际上,它会在您的统一解决方案中创建包含.cs文件的项目,并且每个项目都被编译到自己的输出程序集中。

您看到的错误可能与程序集缓存有关。 几个月前,我遇到了该错误,这是由于过时的程序集仍在缓存中。 结果,unity3d editor有点在加载项目时出现问题,因此无法加载特定的程序集。 我通过删除目录LibraryobjTemp来修复它,然后重新加载了 unity3d项目

要彻底摆脱这种情况,我们一劳永逸地放弃了我们统一项目中的asmdef.cs文件。 我们所有的脚本都已提取到单独的项目中,而unity3d从未涉及过这些项目。 每个项目引用UnityEngine.dll和/或UnityEditor.dll(对于Editor程序集)取决于它可能需要哪个unity3d types。 这些项目是使用Visual Studio或我们的CI pipeline中的服务器端在本地构建的。 将输出手动复制到一个统一项目的Assets目录中,然后从unity3d editor内将其加载。 诚然,最后一步是痛苦的,但我还没有找到时间来简化此过程。

这种方法有一些好处

  • 我们在一个存储库中控制我们的代码。 只有一个事实真相,每个开发人员都将更改提交到相同的代码库中。 在使用我们的类型的任何数量的统一项目中,没有.cs文件的副本。
  • 在存在删除的地方更新unitypackage并不需要找出合并冲突。
  • 可以在服务器端(CI pipeline)进行单元测试,而无需在顶部带有unity3d的某些Docker映像(当然,在不运行整个unity3d环境的情况下,您可以测试的数量受到限制 em>)。
  • 我们创建了自己的NuGet软件包,可以在项目(vcproj不是统一项目!)中引用。
  • 可以通过代码或通过MonoBehaviour将源自GameObjects的类型添加到unity3d editor中。 您还可以通过点击程序集的箭头来展开unity3d editor项目视图中的已加载程序集,该箭头将展开以显示包含的相关类型的列表。

让我们谈谈不利之处

一个例子是我们使用SteamVR与控件进行交互。 用于unity3d的SteamVR插件是通过unity's asset store发布的,它令人讨厌地包含脚本文件和资源,但没有程序集。 顺便说一下,这几乎适用于商店中的所有资产。 由于我们无法针对代码进行构建,因此我们必须解决一次编译SteamVR的麻烦,然后将输出程序集复制到其他位置。

这不仅是一项任务所需要的乏味,而且还有其自身的一些局限性,我将在后面介绍。 无论如何,这使我们能够使用自己的代码引用已编译资产,因此我们可以在代码中使用资产SteamVR_Action之类的资产特定类型,而不必在统一项目中使用unity3d editor和脚本文件(或更糟)。

这样的编译资产限制有两个方面。 第一次,到达那里是极其低效的。 另一方面,对于资产的每个版本,您只需这样做一次。 完成此操作后,将其设为私有NuGet包,您会很高兴。

另一个限制是unity3d如何进行依赖注入。 其实我不完全确定他们真正想做些什么,但是可以。 Unity3d希望您仅引用../UnityInstallDirectory/Editor/Data/Managed/中的程序集。

在理想环境中,您自己的程序集引用了此目录中的笨拙UnityEngine.dll,一旦被unity3d editor加载,一切都会按预期进行。 但是,当您从unity3d editor内编译一个统一项目时,生成的程序集引用了../UnityInstallDirectory/Editor/Data/Managed/UnityEngine/内的所有程序集,其中包含一个很小的UnityEngine.dll版本,而该版本又充当了类型转发器。所有其他子模块。

现在不是一个如此完美的世界吗? 您先前编译的资产要求类型MonoBehaviour位于称为UnityEngine.CoreModule.dll的程序集中。 但是,您自己的项目希望它位于UnityEngine.dll中,因为您是一个好人,并且遵守规则。

这只是自找麻烦,为了解决这个问题,我们现在直接从../UnityInstallDirectory/Editor/Data/Managed/UnityEngine/中引用所有托管子模块。 我们也忽略了unity3d editor抱怨我们做错了事的事情。

tl; dr

通过从上面进行所有操作,将asmdef.cs文件排除在等式之外,我们就可以构建,单元测试并将逻辑和类型打包为程序集。 我们还能够保持干净的代码库,可以轻松维护和扩展这些代码库,而无需在多个位置和/或存储库中复制大量相同的代码。

为什么unity3d以他们的方式做事,我永远不会理解。 我确实知道有一个叫做Building from HEAD的东西,但是由于整个.net生态系统都使用二进制格式以可引用程序集的形式共享内容,所以为什么要以不同的方式做事? 不过,这是另一天的话题。

如果您在这篇文章中一路走来,我衷心希望它能帮助您解决当前的问题。 万一我误解了你的问题...对不起:-)

Unity3d很奇怪...