In-Game scripting works in Editor (Try it!) - But Not Build

The Idea

I want players to be able to interact with the game through C# scripting.
Everything works in the editor (go ahead an try it), but not when I “build” the game.
This is the error (exception message) I see:


The Error Message

An exception was thrown by the type initializer for Mono.CSharp.CSharpCodeCompiler System
at Microsoft.CSharp.CSharpCodeProvider.CreateCompiler ()
[0x00000] in :0
at System.CodeDom.Compiler.CodeDomProvider.CompileAssemblyFromSource (System.CodeDom.Compiler.CompilerParameters options, System.String fileNames) [0x00000] in :0
at ScriptEditor.Compile () [0x00000] in :0


The Code

To Run this script in your game:

  1. Create a C# script file "ScriptEditor.cs "
  2. Paste this code.
  3. Attach to any game object
  4. In Build Settings->Player Settings, change “.NET 2.0 Subset” to “.NET 2.0”

using UnityEngine;
using System;
using System.Text;
using System.Collections;
using System.Reflection;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.CSharp;

/// <summary>
/// ScriptEditor.cs (Attach this to any game object)
/// Useage Notes:
/// 	For this to run, you MUST:
/// 	1. Go to File-->Build Settings...
/// 	2. Click "Player Settings..."
/// 	3. "Other Settings"
/// 	4. "Optimization"-->"API Compatability Level"
/// 	5. Change from ".NET 2.0 Subset" --> ".NET 2.0"
/// </summary>

public class ScriptEditor : MonoBehaviour
{
	
	void Update()
	{
		// Dont run our script until it has been COMPILED successfully.
		if(myScript_Type == null || myScript_Instance == null) return;  	
		
		// Run the script's Update() function
		myScript_Type.InvokeMember("Update",BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public, null, myScript_Instance, null);
		
		// Read the scripts "readme" variable.
		PropertyInfo readmePropertyInfo = myScript_Type.GetProperty("readme"); 	   // Source: http://www.csharp-examples.net/reflection-examples/
		readme = (string)readmePropertyInfo.GetValue(myScript_Instance, null);
	}
	
    private string scriptText = 
@"using System;
using System.Collections.Generic;

public class MyScript
{
	public string readme {get;set;}  // Our unity program will read this variable.
	private int counter = 0;	

	public MyScript() // Constructor
	{
		readme = """";
	}

	public void Update()  // Our unity game will run this function.
	{
		counter++;		
		readme = ""Hi! Script is Running! "" + counter + ""

“”;
}

}
";
	
	private string readme = "";			// Displayed on the GUI
	private string compilerErrorMessages = "";		// Displayed on the GUI

	void OnGUI() 
	{
		// ********** Display the script in Unity
		scriptText = GUI.TextArea(new Rect(10, 10, 700, 500), scriptText);
		
		// ********** Compile your script
		if (GUI.Button(new Rect(720, 10, 300, 30), "Compile and Run"))
		{							
			Compile ();  	// Compile the script. Write errors to "compilerErrorMessages", if any.
		}
		
		// ********** Display any compiler errors
		GUI.TextArea(new Rect(10, 510, 700, 50), compilerErrorMessages);  // The console for displaying errors.
		
		// ********** Display the Script's Output.
		GUI.TextArea (new Rect(720, 50, 300, 30), readme); 	
    }
	
	private Assembly generatedAssembly;					// Compiled code is called an "Assembly"
	private Type myScript_Type = null;					// These two variables are used run the compiled code.
	private object myScript_Instance = null;			// These two variables are used run the compiled code.
	
	private void Compile()
	{
		try
		{
			compilerErrorMessages = "";  // clear any previous messages
			
			// ********** Create an instance of the C# compiler   
	        CSharpCodeProvider codeProvider = new CSharpCodeProvider();
			
	        // ********** add compiler parameters
	        CompilerParameters compilerParams = new CompilerParameters();
	        compilerParams.CompilerOptions = "/target:library /optimize /warn:0"; 
	        compilerParams.GenerateExecutable = false;
	        compilerParams.GenerateInMemory = true;
	        compilerParams.IncludeDebugInformation = false;
	        compilerParams.ReferencedAssemblies.Add("System.dll");
			compilerParams.ReferencedAssemblies.Add("System.Core.dll");
			
	        // ********** actually compile the code  ??????? THIS LINE WORKS IN UNITY EDITOR --- BUT NOT IN BUILD ??????????
			CompilerResults results = codeProvider.CompileAssemblyFromSource(compilerParams,scriptText);
			
	        // ********** Do we have any compiler errors
	        if (results.Errors.Count > 0)
	        {
	            foreach (CompilerError error in results.Errors)
	                compilerErrorMessages = compilerErrorMessages + error.ErrorText + '

';
}
else
{
// ********** get a hold of the actual assembly that was generated
generatedAssembly = results.CompiledAssembly;

				if(generatedAssembly != null)
				{
					// get type of class Calculator from just loaded assembly
					myScript_Type = generatedAssembly.GetType("MyScript");
					
					// create instance of class MyScript
					myScript_Instance = Activator.CreateInstance(myScript_Type);
					
					// Say success!
					compilerErrorMessages = "Success!";
				}
			}
		}
		catch(Exception o)
		{
			compilerErrorMessages = ""+o.Message +"

“+ o.Source +”
“+ o.StackTrace +”
";
}

	}
	
}

The answer is quite simple. If you use ILSpy or any other .NET reflector you will see that the Mono.CSharp.CSharpCodeCompiler has a static constructor which searches for the Mono SDK.

I’m not sure why so many implementetions catch exceptions to rethrow them in a cryptical manner. When you remove your try-catch block and run your code in a standalone build, you will see something like that in your output_log.txt:

FileNotFoundException: Windows mono path not found: D:\Unity35\Projects\mono\mono\mini\mono.exe
  at Mono.CSharp.CSharpCodeCompiler..cctor () [0x00000] in <filename unknown>:0 
Rethrow as TypeInitializationException: An exception was thrown by the type initializer for Mono.CSharp.CSharpCodeCompiler
  at Microsoft.CSharp.CSharpCodeProvider.CreateCompiler () [0x00000] in <filename unknown>:0 

  at System.CodeDom.Compiler.CodeDomProvider.CompileAssemblyFromSource (System.CodeDom.Compiler.CompilerParameters options, System.String[] fileNames) [0x00000] in <filename unknown>:0 

  at ScriptEditor.Compile () [0x00000] in <filename unknown>:0 

  at ScriptEditor.OnGUI () [0x00000] in <filename unknown>:0

This:

D:\Unity35\Projects\

is actually not my project path. It’s actually:

D:\Unity35\Projects\TestProject\build

TestProject is the project folder with the asset folder. The build folder contains the whole build.

It’s a bit strange how Unity’s mono version searches for the path. this is the static constructor:

static CSharpCodeCompiler()
{
	if (Path.DirectorySeparatorChar == '\\')
	{
		PropertyInfo property = typeof(Environment).GetProperty("GacPath", BindingFlags.Static | BindingFlags.NonPublic);
		MethodInfo getMethod = property.GetGetMethod(true);
		string directoryName = Path.GetDirectoryName((string)getMethod.Invoke(null, null));
		CSharpCodeCompiler.windowsMonoPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(directoryName)), "bin\\mono.bat");
		if (!File.Exists(CSharpCodeCompiler.windowsMonoPath))
		{
			CSharpCodeCompiler.windowsMonoPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(directoryName)), "bin\\mono.exe");
		}
		if (!File.Exists(CSharpCodeCompiler.windowsMonoPath))
		{
			CSharpCodeCompiler.windowsMonoPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(directoryName))), "mono\\mono\\mini\\mono.exe");
		}
		if (!File.Exists(CSharpCodeCompiler.windowsMonoPath))
		{
			throw new FileNotFoundException("Windows mono path not found: " + CSharpCodeCompiler.windowsMonoPath);
		}
		CSharpCodeCompiler.windowsMcsPath = Path.Combine(directoryName, "2.0\\gmcs.exe");
		if (!File.Exists(CSharpCodeCompiler.windowsMcsPath))
		{
			CSharpCodeCompiler.windowsMcsPath = Path.Combine(Path.GetDirectoryName(directoryName), "lib\

et_2_0\gmcs.exe");
}
if (!File.Exists(CSharpCodeCompiler.windowsMcsPath))
{
throw new FileNotFoundException("Windows mcs path not found: " + CSharpCodeCompiler.windowsMcsPath);
}
}
}

I guess since Unity brings it’s own Mono compiler it’s not installed in your system. When you run your game inside the editor it uses different assemblies which are shared with the editor.

Solved it.

Installed Mono in default path: C:\Program Files (x86)\Mono (used for editor), then copied the Mono folder next to my standalone .exe as well (used for standalone).

alt text

Now i know where Mono is located so i just needed to edit the paths of mono.exe and mcs.exe. So i modified the CSharpCodeCompiler (link provided by Alizarin). And used this modified class to create the assembly.

Here source to the modification and it works both in editor and standalone. Thank you all.

Building on aeroson’s answer I managed to get Unity to build C# in standalone in a much simpler way, though not as clean and space-efficient as his improved answer that comes with a patched version of mcs.
This version is probably more suitable to bigger projects, for which including a 400 MB Mono runtime isn’t an issue, if you want to have DLLs generated (to reuse them afterwards for example), or do not want to cope with all the little quirks of using Reflection.Emit instead of a good old compiled assembly. This version also fully works with CodeDOM objects.

I reused aeroson’s CSharpCodeCompiler, which I copied into a CustomCSharpCodeCompiler.cs file inside my project.
Then I added a slightly modified CSharpCodeGenerator using the Mono source code, which I copied into a CustomCSharpCodeGenerator.cs file inside my project. The main modification to this file was moving it to the Modified.Mono.CSharp package where the CSharpCodeCompiler is, which enabled the generator to use the custom compiler.

And here’s the bit of script that uses those classes, it’s a simple function that takes a CodeDOM object, a filename (used to determine the name of the assembly to compile), compiles it into a DLL file and loads this assembly in memory so you can use it straight away.