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 +"
";
   		}
   		
   	}
   	
   }

Very cool implications here. Have you experimented with sandboxing to render this a safe game modding system?

That is awesome! I can not wait to try the lua implementation. This would be just fine for my application. Thank you Bunny83

LUA is something different and I love C#. I would be happy if it was possible to add an addon system to my game depending on C# language. Have you found a solution for including the mono compiler into the game build ?

Hi there, i've got the same problem, I'm tryng to use your solution but aI think I'm a bit too noob for it! Can you please post a step by step tutorial? Thanks a lot!

FYI to anyone looking at shipping the mono compiler with your project. You'll definitely need to obtain a commercial license from Xamarin: http://www.mono-project.com/docs/faq/licensing/

3 Answers

3

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.

Bunny83 Thank you so much for you reply! Very encouraging! - I tried installing two different versions of Mono. - I tried placing the files I thought it was looking for (indicated in output_log.txt) in "...mono\\mono\\mini\\" etc. No luck so far. How would you recommend I include the "Mono compiler" in my game build? Is there a "mono.dll" I can place in my assets folder? What are your thought?

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.

Another solution is to take the MCS compiler source code which is written in C# and compile it for .NET 3.5 You can try referencing the mcs.exe it self as it is now, but it is compiled with .NET 4.0 so it doesn't work with Unity.

Here is the definitive answer to this cause: https://github.com/aeroson/mcs-ICodeCompiler

How can I use only the default solutions (provided only by Microsoft and/or Mono at all) to be able to compile c# codes at runtime? I don't want to use ICodeCompiler you made at all, to understand better the whole thing. So I get the "Windows mono path not found: F:/mono/mono/mini/mono.exe" error message, if I want to compile in build. You said you have copied the installed Mono folder near your game.exe. But where can you find CSharpCodeCompiler? And how can you create an assembly with that?

It seems they took down the source Mono code. Is there anywhere I can view it?

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.

This worked, thanks much. I think this is actually a cleaner solution. Than the recompiled MCS (which doesnt work with Visual Studio Integration tools because of conflicts)

@Lee7 there are workarounds for that in issues