Alright so if you really want to speed your compilation you need to forget about UnityScript/JS/Boo and use C#, it’s tons of times faster.
Now here’s how you can manual compile stuff so you could measure things. First let’s start with C#, there’s two ways, first via a static method, second is via the StartCompiler
instance method. In both cases you have to provide:
- Absolute paths to your source file(s) (With their extensions like .cs .js etc)
- (Optional) Absolute paths to any non .NET reference that you used (if any) (see later) (msdn)
- (Optional) Any defines that you used (msdn)
- Absolute output file path (with the .dll extension as well)
Now create an editor file (put it in your Editor folder) called “ManualCompile.cs” (or anything)
using UnityEngine;
using UnityEditor;
using System.Linq;
using System;
using System.Reflection;
using System.Collections.Generic;
public static class ManualCompile
{
[MenuItem("Tools/Compile Via Static Method")]
public static void CompileCSStatic()
{
}
[MenuItem("Tools/Compile Via MonoIsland")]
public static void CompileCSViaIsland()
{
}
}
Now let’s write these methods:
public static void CompileCSStatic()
{
// Get a reference to the UnityEditor.dll - could be done via any UnityEditor type, not just Editor. So typeof(SerializedProperty).Assembly also works...
var editorAsm = typeof(Editor).Assembly;
// Get the type object for our mono cs compiler
// For some reason doing a editorAsm.GetType("Whatever") always returns null so I get all the types and filter the type I'm interested in
var compilerType = editorAsm.GetTypes().FirstOrDefault(t => t.Name == "MonoCSharpCompiler");
// Get a MethodInfo reference to our static Compile method
var compileMethod = compilerType.GetMethod("Compile", BindingFlags.Static | BindingFlags.Public);
// Now we set the sources, refs, defs and output
var sources = new[] { @"absolute path to a source file, ex: C:\Users\vexe\Desktop\Test\Assets\Editor\ManualCompile.cs" };
var references = new string[]
{
typeof(GameObject).Assembly.Location, // or C:\Program Files (x86)\Unity\Editor\Data\Managed\UnityEngine.dll
editorAsm.Location
//, add more references if needed - no need to add .NET references like System, etc
};
var defines = new string[] { };
var output = @"C:\Users\vexe\Desktop\Test\Assets\output.dll";
// Invoking the compile method will return a string[] containing all the compilation errors (if any)
var errors = compileMethod.Invoke(null, new object[] { sources, references, defines, output });
foreach (var e in (errors as IEnumerable<string>))
Debug.LogError(e);
// We import and refresh
AssetDatabase.ImportAsset("Assets/output.dll");
AssetDatabase.Refresh();
}
And that’s it!
Now I have to mention the other way (via MonoIsland
) because it seems that the UnityScript
compiler doesn’t have a static Compile
method so we can’t just change the compiler type and use the same code above, we have to do it the island way, so let’s see…
First, I should mention that the static compile method above, creates an island internally so it’s just a wrapper for the method that we’ll write next. Now what is an island? It’s just a struct that holds references to the source files, references, defines, output file, build target and a library profile:
internal struct MonoIsland
{
public readonly BuildTarget _target;
public readonly string _classlib_profile;
public readonly string[] _files;
public readonly string[] _references;
public readonly string[] _defines;
public readonly string _output;
public MonoIsland(BuildTarget target, string classlib_profile, string[] files, string[] references, string[] defines, string output)
{
this._target = target;
this._classlib_profile = classlib_profile;
this._files = files;
this._references = references;
this._defines = defines;
this._output = output;
}
}
Now what is BuildTarget
? - It’s just an enum in the UnityEditor namespace.
What is _classlib_prfile
? - I’m not a 100% sure, but reading Unity’s code (specifically the GetProfileDirectory
method in the internal class MonoInstallationFinder
in the UnityEditor.Utils
namespace) I found out that all the profiles are located in EditorApplication.applicationPath + @"\Data\Mono\lib\mono"
(EditorApplication.applicationPath
== (in my computer) @“C:\Program Files (x86)\Unity\Editor”) - So I used “2.0” - Judging from the number, this could be the equivalent .NET framework that Unity’s Mono uses (somebody correct me if I’m wrong)
So now basically the only difference, is that we have to create a MonoIsland, and pass it to the StartCompile
method - StartCompile
returns a Program
reference (an internal class in UnityEditor.Utils
) from which we can take the output messages (errors, etc). So here we go:
public static void CompileCSViaIsland()
{
var editorAsm = typeof(Editor).Assembly;
var editorTypes = editorAsm.GetTypes();
var target = BuildTarget.StandaloneWindows64;
var profile = "2.0";
var sources = new[]
{
@"C:\Users\vexe\Desktop\Test\Assets\Editor\ManualCompile.cs"
//, add more source files...
};
var references = new string[]
{
typeof(GameObject).Assembly.Location,
editorAsm.Location
};
var defines = new string[] { };
var output = @"C:\Users\vexe\Desktop\Test\Assets\output.dll";
var islandType = editorTypes.FirstOrDefault(t => t.Name == "MonoIsland");
var islandInstance = Activator.CreateInstance(islandType, target, profile, sources, references, defines, output);
var compilerType = editorTypes.FirstOrDefault(t => t.Name == "MonoCSharpCompiler");
var compilerInstance = Activator.CreateInstance(compilerType, islandInstance);
// For some reason I got an AmbiguousMatchException thrown when I used compilerType.GetMethod("StartCompiler, BF.Instance | BF.NonPublic);
// I don't know why, since there's only one StartCompiler method so there should be no ambiguity...
// So I used a more explicit overload of GetMethod and specified that I want the StartCompiler that takes no arguments...
var startCompiler = compilerType.GetMethod("StartCompiler", BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
// Like we said earlier, StartCompiler returns a Program instance
var program = startCompiler.Invoke(compilerInstance, null);
var errors = program.GetType().GetMethod("GetErrorOutput", BindingFlags.Instance | BindingFlags.Public).Invoke(program, null);
foreach (var e in (errors as IEnumerable<string>))
Debug.LogError(e);
AssetDatabase.ImportAsset("Assets/output.dll");
AssetDatabase.Refresh();
}
Now I haven’t tried it, but I’m willing to bet that all you have to do to compile UnityScripts, is just change the compiler type name, so instead of “MonoCSharpCompiler” you use “UnityScriptCompiler”
I will write a more nicified/friendly/easy-to-use/portable versions of the two methods above later so you can pass a whole directory to compile, much better.
Now you can measure your stuff, just write the source files paths, use System.Diagnostics.Stopwatch
and compile. - Let me know how it goes.
EDIT:
Actually you can do your compilation from the command line, reading from MonoScriptCompilerBase
(in UnityEditor.Scripting.Compilers
) StartCompiler
method you can see that the compiler is actually executed from the profile folders I mentioned above, so if you go to the “Editor/Data/Mono/lib/mono/2.0” the compilers executables are there, somewhere…
EDIT:
Here’s a better-written version - Notice I’m not detecting the references when I compile a directory - So you have to manually add the references your self. Remember, you don’t need to add .NET references like System, etc but only custom/user references, like Unity dlls or MyRuntimeExtensions.dll
or whatever… You might want to put the stopwatch in a dif place, or put more than one, up to you.
EDIT:
Forgot to mention something, the second answer here covers it.