Use monobehaviour from asmdef generated assembly

unity add assembly reference
unity assemblydefinitionreference
unity rename assembly-csharp
unity assembly load
unity asmref
unity scriptassemblies
unity assemblyinfo
assembly definition

We would like to distribute our project with assembly files instead of .cs scripts. We thought that this would be easy thanks to assembly definition files, as unity is creating assembly files for the scripts they refer to anyway. It turns out that when removing the .cs files and putting the assemblies, we ran into a problem : The monobehaviors defined in the assemblies (so previously in our scripts) can't be added manually to a scene :

"Can't add script component xxx because the script class cannot be found"

While if we add the component through script (i.e. AddComponent) it works.

I'm using Unity 2017.3.f1 to generate the assembly files

Is there a trick to make this work ? or should I try to generate the assemblies using another approach ?

OP here.

Short answer is : don't keep both asmdef and assembly files. Remove the asmdef file if you replace the scripts with the generated assembly

What I ended up doing is the roughly following (this was for CI purpose): First, we need to make sure Unity compiles the assembly file. So I have a GenerateAssemblies.cs file in an Editor folder that can be executed from command line:

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);
        }
    }

}

Then I have a ReplaceAssemblies.cs file in an editor folder that will :

  1. find the assembly file correpsonding to the asmdef file
  2. save the guid/classes correspondance of the script files
  3. move the script files in a temporary folder
  4. move the assembly in the same folder as the asmdef file
  5. move the asmdef to a temporary folder
  6. Replace the Guid and File ID values for each script in the assembly (to avoid breaking references in scenes and prefabs)

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);

    }
}

Assembly Definitions - Unity, You can use Assembly Definitions to organize the scripts in your Project into An Assembly Definition Asset is a text file, with a file extension of .asmdef, that predefined assemblies, assemblies created with Assembly Definition Assets, and​  You can use Assembly Definition Reference Assets to add additional folders to an existing Assembly Definition asset. Using Assembly Definitions. Add Assembly Definition Assets to folders in a Unity Project to define an assembly. After compilation, the assembly contains all the scripts in the folder and its subfolders (unless the subfolders have their own Assembly Definitions). Set the name of the assembly in the Inspector.

I was experiencing this issue, and like you, I was using the information provided from asmdef files to provide all the required information (which .cs files, what references, defines, etc) to build an assembly.

I found that the issue was the DLL I was creating had the same name as the asmdef file I was using to provide the information. Even though the asmdef file was no longer being compiled (because all the scripts had been removed to build the DLL), it was still interfering with the project.

So for me, the inconsistency between accessing a script from inside the editor and from inside scripts was because there was a DLL and as asmdef file with the same name in the project.

Giving the compiled DLL a different name or removing the asmdef file was the solution for me.

Use monobehaviour from asmdef generated assembly, I'm using Unity 2017.3.f1 to generate the assembly files. Is there a trick to make this work ? or should I try to generate the assemblies using  You can create an asmdef file in any folder. Any C# code in the folder and its child folders will be compiled to its own dll. This means that the source files in the folder will be skipped during recompilation if there are no changes in it or on any of its asmdef dependencies.

Just tested with Unity 2019.3.0b1.

Content of test class:

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

Second project with the generated DLL, working as expected

Assembly Definition files not working as expected., I am trying to use Assembly definition files, I create one and suddenly suddenly unity doesnt see my scripts as monobehaviours anymore. behavior that a lot of my ASMDEFs generate a second VS project file, ending on . You want your test assembly to reference anything else but it is not possible to do it with Assembly-CSharp.dll. Check mate, you delete the asmdef and think this is a waste of time. The bridge problem. you see some folder that looks self-contained and would be nice to place asmdef on.

Missing reference between 'Assembly-CSharp' and 'Tests' assembly , Use Unity to build high-quality 3D and 2D games, deploy them across mobile, Then, I created a MonoBehaviour in my "Assembly-CSharp" project. non-​existent assembly 'Assembly-CSharp' (Assets/Tests/Tests.asmdef)". In The 2.0.0 release, all of the official Mixed Reality Toolkit assembly names and their associated assembly definition (.asmdef) files have been updated to fit the following pattern. Microsoft.MixedReality.Toolkit[.<name>] In some instances, multiple assemblies have been merged to create better unity of their contents.

Reducing Compile Time in Unity Using Assembly Definition Files , When I first started using asmdef, what I did was I created these files all over the place whenever I can. When I see a compile error, I make an  You need to have an assembly definition for the code under test and add a reference from the test code to the newly created assembly definition. This must be done in the Unity editor UI. by default, unity adds your script code to an assembly called Assembly-CSharp.dll , and, for reasons unknown, this assembly isn't referenced by my edit mode

Unity3d assembly, Use monobehaviour from asmdef generated assembly. We would like to distribute our project with assembly files instead of .cs scripts. We thought that this  I have tried removing the files and altering them to remove the references to multiple files, but that generated a large amount of errors in response. These are the assembly files in question: IBM.Watson.Tests.asmdef:

How to remodel your project for asmdef and UPM, Thereafter I tried to browse the assembly using Red Gate's . Mar 18 I think you have to link it at compile/build/AOT time. asmdef”, that Without Assembly Definitions, Unity the problem can be solved by simply dropping in a new MonoBehaviour. This is the default for Unity-generated VS files, so it should work unless the  Get early access to the features now. Both Assembly Definition Files and the Transform Tool are a part of the Unity 2017.3 beta together with new improvements to the particle system, an updated Crunch texture compression library and much more.

Comments
  • Did you ever find a solution? Please update with an answer if you did.
  • @MichaelHouse how are you building said assemblies?
  • @Ron I'm using Unity's AssemblyBuilder class. Your answer doesn't show your build process.
  • I just let Unity detect code change, compile it and copy it from the output folder
  • The problem I guess is that Unity expects every MonoBehaviour component to be an individual script file with matching name...
  • Great, thanks for coming back and giving a detailed answer. I've awarded the bounty to you.
  • Yes the error in question was caused by the presence of both the assembly and asmdef file. I'v put a more detailed answer as to how we replaced asmdef files with dlls automatically in our CI pipeline