Skip to main content
Glama
RoslynRuntimeCompiler.cs46.8 kB
// RoslynRuntimeCompiler.cs // Single-file Unity tool for Editor+PlayMode dynamic C# compilation using Roslyn. // Features: // - EditorWindow GUI with a large text area for LLM-generated code // - Compile button (compiles in-memory using Roslyn) // - Run button (invokes a well-known entry point in the compiled assembly) // - Shows compile errors and runtime exceptions // - Safe: Does NOT write .cs files to Assets (no Domain Reload) // // Requirements: // 1) Add Microsoft.CodeAnalysis.CSharp.dll and Microsoft.CodeAnalysis.dll to your Unity project // (place under Assets/Plugins or Packages and target the Editor). These come from the Roslyn nuget package. // 2) This tool is designed to run in the Unity Editor (Play Mode or Edit Mode). It uses Assembly.Load(byte[]). // 3) Generated code should expose a public type and a public static entry method matching one of the supported signatures: // - public static void Run(UnityEngine.GameObject host) // - public static void Run(UnityEngine.MonoBehaviour host) // - public static System.Collections.IEnumerator RunCoroutine(UnityEngine.MonoBehaviour host) // if you want a coroutine // By convention this demo looks for a type name you specify in the window (default: "AIGenerated"). // // Usage: // - Window -> Roslyn Runtime Compiler // - Paste code into the big text area (or use LLM output pasted there) // - Optionally set Entry Type (default AIGenerated) and Entry Method (default Run) // - Press "Compile". Compiler diagnostics appear below. // - In Play Mode, press "Run" to invoke the entry method. In Edit Mode it will attempt to run if valid. // // Security note: Any dynamically compiled code runs with the same permissions as the editor. Be careful when running untrusted code. #if UNITY_EDITOR using UnityEditor; #endif using System; using System.IO; using System.Linq; using System.Reflection; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; #endif public class RoslynRuntimeCompiler : MonoBehaviour { [TextArea(8, 20)] [Tooltip("Code to compile at runtime. Example class name: AIGenerated with public static void Run(GameObject host)")] public string code = "using UnityEngine;\npublic class AIGenerated {\n public static void Run(GameObject host) {\n Debug.Log($\"Hello from AI - {host.name}\");\n host.transform.Rotate(Vector3.up * 45f * Time.deltaTime);\n }\n}"; [Tooltip("Fully qualified type name to invoke (default: AIGenerated)")] public string entryTypeName = "AIGenerated"; [Tooltip("Method name to call on entry type (default: Run)")] public string entryMethodName = "Run"; [Header("MonoBehaviour Support")] [Tooltip("If true, attempts to attach generated MonoBehaviour to target GameObject")] public bool attachAsComponent = false; [Tooltip("Target GameObject to attach component to (if null, uses this.gameObject)")] public GameObject targetGameObject; [Header("History & Tracing")] [Tooltip("Enable automatic history tracking of compiled scripts")] public bool enableHistory = true; [Tooltip("Maximum number of history entries to keep")] public int maxHistoryEntries = 20; // compiled assembly & method cache private Assembly compiledAssembly; private MethodInfo entryMethod; private Type entryType; private Component attachedComponent; // Track dynamically attached component public bool HasCompiledAssembly => compiledAssembly != null; public bool HasEntryMethod => entryMethod != null; public bool HasEntryType => entryType != null; public Type EntryType => entryType; // Public accessor for editor // compile result diagnostics (string-friendly) public string lastCompileDiagnostics = ""; // History tracking - SHARED across all instances [System.Serializable] public class CompilationHistoryEntry { public string timestamp; public string sourceCode; public string typeName; public string methodName; public bool success; public string diagnostics; public string executionTarget; } // Static shared history private static System.Collections.Generic.List<CompilationHistoryEntry> _sharedHistory = new System.Collections.Generic.List<CompilationHistoryEntry>(); public System.Collections.Generic.List<CompilationHistoryEntry> CompilationHistory => _sharedHistory; // public wrapper so EditorWindow or other runtime UI can call compile/run public bool CompileInMemory(out string diagnostics) { #if UNITY_EDITOR diagnostics = string.Empty; lastCompileDiagnostics = string.Empty; try { var syntaxTree = CSharpSyntaxTree.ParseText(code ?? string.Empty); // collect references from loaded assemblies (Editor-safe) var refs = new List<MetadataReference>(); // Always include mscorlib / system.runtime refs.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); // Add all currently loaded assemblies' locations that are not dynamic and have a location var assemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) .Distinct(); foreach (var a in assemblies) { try { refs.Add(MetadataReference.CreateFromFile(a.Location)); } catch { } } var compilation = CSharpCompilation.Create( assemblyName: "RoslynRuntimeAssembly_" + Guid.NewGuid().ToString("N"), syntaxTrees: new[] { syntaxTree }, references: refs, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) ); using (var ms = new MemoryStream()) { var result = compilation.Emit(ms); if (!result.Success) { var diagText = string.Join("\n", result.Diagnostics.Select(d => d.ToString())); lastCompileDiagnostics = diagText; diagnostics = diagText; Debug.LogError("Roslyn compile failed:\n" + diagText); return false; } ms.Seek(0, SeekOrigin.Begin); var assemblyData = ms.ToArray(); compiledAssembly = Assembly.Load(assemblyData); // find entry type var type = compiledAssembly.GetType(entryTypeName); if (type == null) { lastCompileDiagnostics = $"Type '{entryTypeName}' not found in compiled assembly."; diagnostics = lastCompileDiagnostics; return false; } entryType = type; // Check if it's a MonoBehaviour if (typeof(MonoBehaviour).IsAssignableFrom(type)) { lastCompileDiagnostics = $"Compilation OK. Type '{entryTypeName}' is a MonoBehaviour and can be attached as a component."; diagnostics = lastCompileDiagnostics; Debug.Log(diagnostics); return true; } // try various method signatures for non-MonoBehaviour types entryMethod = type.GetMethod(entryMethodName, BindingFlags.Public | BindingFlags.Static); if (entryMethod == null) { lastCompileDiagnostics = $"Static method '{entryMethodName}' not found on type '{entryTypeName}'.\n" + $"For MonoBehaviour types, set 'attachAsComponent' to true instead."; diagnostics = lastCompileDiagnostics; return false; } lastCompileDiagnostics = "Compilation OK."; diagnostics = lastCompileDiagnostics; Debug.Log("Roslyn compilation successful."); return true; } } catch (Exception ex) { diagnostics = ex.ToString(); lastCompileDiagnostics = diagnostics; Debug.LogError("Roslyn compile exception: " + diagnostics); return false; } #else diagnostics = "Roslyn compilation is only supported in the Unity Editor when referencing Roslyn assemblies."; lastCompileDiagnostics = diagnostics; Debug.LogError(diagnostics); return false; #endif } public bool InvokeEntry(GameObject host, out string runtimeError) { runtimeError = null; if (compiledAssembly == null || entryType == null) { runtimeError = "No compiled assembly / entry type. Call CompileInMemory first."; return false; } // Handle MonoBehaviour types if (typeof(MonoBehaviour).IsAssignableFrom(entryType)) { return AttachMonoBehaviour(host, out runtimeError); } // Handle static method invocation if (entryMethod == null) { runtimeError = "No entry method found. For MonoBehaviour types, use attachAsComponent=true."; return false; } try { var parameters = entryMethod.GetParameters(); if (parameters.Length == 0) { entryMethod.Invoke(null, null); return true; } else if (parameters.Length == 1) { var pType = parameters[0].ParameterType; if (pType == typeof(GameObject)) entryMethod.Invoke(null, new object[] { host }); else if (typeof(MonoBehaviour).IsAssignableFrom(pType)) { var component = host.GetComponent(pType); entryMethod.Invoke(null, new object[] { component != null ? component : (object)host }); } else if (pType == typeof(Transform)) entryMethod.Invoke(null, new object[] { host.transform }); else if (pType == typeof(object)) entryMethod.Invoke(null, new object[] { host }); else entryMethod.Invoke(null, new object[] { host }); // best effort return true; } else { runtimeError = "Entry method has unsupported parameter signature."; return false; } } catch (TargetInvocationException tie) { runtimeError = tie.InnerException?.ToString() ?? tie.ToString(); Debug.LogError("Runtime invocation error: " + runtimeError); return false; } catch (Exception ex) { runtimeError = ex.ToString(); Debug.LogError("Runtime invocation error: " + runtimeError); return false; } } /// <summary> /// Attaches a dynamically compiled MonoBehaviour to a GameObject /// </summary> public bool AttachMonoBehaviour(GameObject host, out string runtimeError) { runtimeError = null; if (host == null) { runtimeError = "Target GameObject is null."; return false; } if (entryType == null || !typeof(MonoBehaviour).IsAssignableFrom(entryType)) { runtimeError = $"Type '{entryTypeName}' is not a MonoBehaviour."; return false; } try { // Check if component already exists var existing = host.GetComponent(entryType); if (existing != null) { Debug.LogWarning($"Component '{entryType.Name}' already exists on '{host.name}'. Removing old instance."); if (Application.isPlaying) Destroy(existing); else DestroyImmediate(existing); } // Add the component attachedComponent = host.AddComponent(entryType); if (attachedComponent == null) { runtimeError = "Failed to add component to GameObject."; return false; } Debug.Log($"Successfully attached '{entryType.Name}' to '{host.name}'"); return true; } catch (Exception ex) { runtimeError = ex.ToString(); Debug.LogError("Failed to attach MonoBehaviour: " + runtimeError); return false; } } /// <summary> /// Invokes a coroutine on the compiled type if it returns IEnumerator /// </summary> public bool InvokeCoroutine(MonoBehaviour host, out string runtimeError) { runtimeError = null; if (entryMethod == null) { runtimeError = "No entry method found."; return false; } if (!typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType)) { runtimeError = $"Method '{entryMethodName}' does not return IEnumerator."; return false; } try { var parameters = entryMethod.GetParameters(); object result = null; if (parameters.Length == 0) { result = entryMethod.Invoke(null, null); } else if (parameters.Length == 1) { var pType = parameters[0].ParameterType; if (pType == typeof(GameObject)) result = entryMethod.Invoke(null, new object[] { host.gameObject }); else if (typeof(MonoBehaviour).IsAssignableFrom(pType)) result = entryMethod.Invoke(null, new object[] { host }); else result = entryMethod.Invoke(null, new object[] { host }); } if (result is System.Collections.IEnumerator coroutine) { host.StartCoroutine(coroutine); Debug.Log($"Started coroutine '{entryMethodName}' on '{host.name}'"); return true; } else { runtimeError = "Method did not return a valid IEnumerator."; return false; } } catch (Exception ex) { runtimeError = ex.ToString(); Debug.LogError("Failed to start coroutine: " + runtimeError); return false; } } /// <summary> /// MCP-callable function: Compiles code and optionally attaches to a GameObject /// </summary> /// <param name="sourceCode">C# source code to compile</param> /// <param name="typeName">Type name to instantiate/invoke</param> /// <param name="methodName">Method name to invoke (for static methods)</param> /// <param name="targetObject">Target GameObject (null = this.gameObject)</param> /// <param name="shouldAttachComponent">If true and type is MonoBehaviour, attach as component</param> /// <param name="errorMessage">Output error message if operation fails</param> /// <returns>True if successful, false otherwise</returns> public bool CompileAndExecute( string sourceCode, string typeName, string methodName, GameObject targetObject, bool shouldAttachComponent, out string errorMessage) { errorMessage = null; // Validate inputs if (string.IsNullOrWhiteSpace(sourceCode)) { errorMessage = "Source code cannot be empty."; return false; } if (string.IsNullOrWhiteSpace(typeName)) { errorMessage = "Type name cannot be empty."; return false; } // Set properties code = sourceCode; entryTypeName = typeName; entryMethodName = string.IsNullOrWhiteSpace(methodName) ? "Run" : methodName; attachAsComponent = shouldAttachComponent; targetGameObject = targetObject; // Determine target GameObject first GameObject target = targetGameObject != null ? targetGameObject : this.gameObject; string targetName = target != null ? target.name : "null"; // Compile if (!CompileInMemory(out string compileError)) { errorMessage = $"Compilation failed:\n{compileError}"; AddHistoryEntry(sourceCode, typeName, entryMethodName, false, compileError, targetName); return false; } if (target == null) { errorMessage = "No target GameObject available."; AddHistoryEntry(sourceCode, typeName, entryMethodName, false, "No target GameObject", "null"); return false; } // Execute based on type try { // MonoBehaviour attachment if (shouldAttachComponent && entryType != null && typeof(MonoBehaviour).IsAssignableFrom(entryType)) { if (!AttachMonoBehaviour(target, out string attachError)) { errorMessage = $"Failed to attach MonoBehaviour:\n{attachError}"; AddHistoryEntry(sourceCode, typeName, entryMethodName, false, attachError, target.name); return false; } Debug.Log($"[MCP] MonoBehaviour '{typeName}' successfully attached to '{target.name}'"); AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Component attached successfully", target.name); return true; } // Coroutine invocation if (entryMethod != null && typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType)) { var host = target.GetComponent<MonoBehaviour>() ?? this; if (!InvokeCoroutine(host, out string coroutineError)) { errorMessage = $"Failed to start coroutine:\n{coroutineError}"; AddHistoryEntry(sourceCode, typeName, entryMethodName, false, coroutineError, target.name); return false; } Debug.Log($"[MCP] Coroutine '{methodName}' started on '{target.name}'"); AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Coroutine started successfully", target.name); return true; } // Static method invocation if (!InvokeEntry(target, out string invokeError)) { errorMessage = $"Failed to invoke method:\n{invokeError}"; AddHistoryEntry(sourceCode, typeName, entryMethodName, false, invokeError, target.name); return false; } Debug.Log($"[MCP] Method '{methodName}' executed successfully on '{target.name}'"); AddHistoryEntry(sourceCode, typeName, entryMethodName, true, "Method executed successfully", target.name); return true; } catch (Exception ex) { errorMessage = $"Execution error:\n{ex.Message}\n{ex.StackTrace}"; return false; } } /// <summary> /// Simplified MCP-callable function with default parameters /// </summary> public bool CompileAndExecute(string sourceCode, string typeName, GameObject targetObject, out string errorMessage) { // Auto-detect if it's a MonoBehaviour by checking the source bool shouldAttach = sourceCode.Contains(": MonoBehaviour") || sourceCode.Contains(":MonoBehaviour"); return CompileAndExecute(sourceCode, typeName, "Run", targetObject, shouldAttach, out errorMessage); } /// <summary> /// MCP-callable: Compile and attach to current GameObject /// </summary> public bool CompileAndAttachToSelf(string sourceCode, string typeName, out string errorMessage) { return CompileAndExecute(sourceCode, typeName, "Run", this.gameObject, true, out errorMessage); } // helper: convenience method to compile + run on this.gameObject public void CompileAndRunOnSelf() { if (CompileInMemory(out var diag)) { if (!Application.isPlaying) Debug.LogWarning("Running compiled code in Edit Mode. Some UnityEngine APIs may not behave as expected."); GameObject target = targetGameObject != null ? targetGameObject : this.gameObject; // Check if we should attach as component if (attachAsComponent && entryType != null && typeof(MonoBehaviour).IsAssignableFrom(entryType)) { if (AttachMonoBehaviour(target, out var attachErr)) { Debug.Log($"MonoBehaviour '{entryTypeName}' attached successfully to '{target.name}'."); } else { Debug.LogError("Failed to attach MonoBehaviour: " + attachErr); } } // Check if it's a coroutine else if (entryMethod != null && typeof(System.Collections.IEnumerator).IsAssignableFrom(entryMethod.ReturnType)) { var host = target.GetComponent<MonoBehaviour>() ?? this; if (InvokeCoroutine(host, out var coroutineErr)) { Debug.Log("Coroutine started successfully."); } else { Debug.LogError("Failed to start coroutine: " + coroutineErr); } } // Regular static method invocation else if (InvokeEntry(target, out var runtimeErr)) { Debug.Log("Entry invoked successfully."); } else { Debug.LogError("Failed to invoke entry: " + runtimeErr); } } else { Debug.LogError("Compile failed: " + lastCompileDiagnostics); } } /// <summary> /// Adds an entry to the compilation history /// </summary> private void AddHistoryEntry(string sourceCode, string typeName, string methodName, bool success, string diagnostics, string target) { if (!enableHistory) return; var entry = new CompilationHistoryEntry { timestamp = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), sourceCode = sourceCode, typeName = typeName, methodName = methodName, success = success, diagnostics = diagnostics, executionTarget = target }; _sharedHistory.Add(entry); // Trim if exceeded max while (_sharedHistory.Count > maxHistoryEntries) { _sharedHistory.RemoveAt(0); } } /// <summary> /// Saves the compilation history to a JSON file outside Assets /// </summary> public bool SaveHistoryToFile(out string savedPath, out string error) { error = ""; savedPath = ""; try { string projectRoot = Application.dataPath.Replace("/Assets", "").Replace("\\Assets", ""); string historyDir = System.IO.Path.Combine(projectRoot, "RoslynHistory"); if (!System.IO.Directory.Exists(historyDir)) { System.IO.Directory.CreateDirectory(historyDir); } string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss"); string filename = $"RoslynHistory_{timestamp}.json"; savedPath = System.IO.Path.Combine(historyDir, filename); string json = JsonUtility.ToJson(new HistoryWrapper { entries = _sharedHistory }, true); System.IO.File.WriteAllText(savedPath, json); Debug.Log($"[RuntimeRoslynDemo] Saved {_sharedHistory.Count} history entries to: {savedPath}"); return true; } catch (System.Exception ex) { error = ex.Message; Debug.LogError($"[RuntimeRoslynDemo] Failed to save history: {error}"); return false; } } /// <summary> /// Saves a specific history entry as a standalone .cs file outside Assets /// </summary> public bool SaveHistoryEntryAsScript(int index, out string savedPath, out string error) { error = ""; savedPath = ""; if (index < 0 || index >= _sharedHistory.Count) { error = "Invalid history index"; return false; } try { var entry = _sharedHistory[index]; string projectRoot = Application.dataPath.Replace("/Assets", "").Replace("\\Assets", ""); string scriptsDir = System.IO.Path.Combine(projectRoot, "RoslynHistory", "Scripts"); if (!System.IO.Directory.Exists(scriptsDir)) { System.IO.Directory.CreateDirectory(scriptsDir); } string timestamp = System.DateTime.Parse(entry.timestamp).ToString("yyyyMMdd_HHmmss"); string filename = $"{entry.typeName}_{timestamp}.cs"; savedPath = System.IO.Path.Combine(scriptsDir, filename); // Add header comment string header = $"// Roslyn Runtime Compiled Script\n// Original Timestamp: {entry.timestamp}\n// Type: {entry.typeName}\n// Method: {entry.methodName}\n// Success: {entry.success}\n// Target: {entry.executionTarget}\n\n"; System.IO.File.WriteAllText(savedPath, header + entry.sourceCode); Debug.Log($"[RuntimeRoslynDemo] Saved script to: {savedPath}"); return true; } catch (System.Exception ex) { error = ex.Message; Debug.LogError($"[RuntimeRoslynDemo] Failed to save script: {error}"); return false; } } /// <summary> /// Clears the compilation history /// </summary> public void ClearHistory() { _sharedHistory.Clear(); Debug.Log("[RuntimeRoslynDemo] Compilation history cleared"); } [System.Serializable] private class HistoryWrapper { public System.Collections.Generic.List<CompilationHistoryEntry> entries; } } /// <summary> /// Static helper class for MCP tools to compile and execute C# code at runtime /// </summary> public static class RoslynMCPHelper { private static RoslynRuntimeCompiler _compiler; /// <summary> /// Get or create the runtime compiler instance /// </summary> private static RoslynRuntimeCompiler GetOrCreateCompiler() { if (_compiler == null || _compiler.gameObject == null) { var existing = UnityEngine.Object.FindFirstObjectByType<RoslynRuntimeCompiler>(); if (existing != null) { _compiler = existing; } else { var go = new GameObject("MCPRoslynCompiler"); _compiler = go.AddComponent<RoslynRuntimeCompiler>(); if (!Application.isPlaying) { go.hideFlags = HideFlags.HideAndDontSave; } } } return _compiler; } /// <summary> /// MCP Entry Point: Compile C# code and attach to a GameObject /// </summary> /// <param name="sourceCode">Complete C# source code</param> /// <param name="className">Name of the class to instantiate</param> /// <param name="targetGameObjectName">Name of GameObject to attach to (null = create new)</param> /// <param name="result">Output result message</param> /// <returns>True if successful</returns> public static bool CompileAndAttach(string sourceCode, string className, string targetGameObjectName, out string result) { try { var compiler = GetOrCreateCompiler(); // Find or create target GameObject GameObject target = null; if (!string.IsNullOrEmpty(targetGameObjectName)) { target = GameObject.Find(targetGameObjectName); if (target == null) { result = $"GameObject '{targetGameObjectName}' not found."; return false; } } else { // Create a new GameObject for the script target = new GameObject($"Generated_{className}"); UnityEngine.Debug.Log($"[MCP] Created new GameObject: {target.name}"); } // Compile and execute bool success = compiler.CompileAndExecute(sourceCode, className, target, out string error); if (success) { result = $"Successfully compiled and attached '{className}' to '{target.name}'"; UnityEngine.Debug.Log($"[MCP] {result}"); return true; } else { result = $"Failed: {error}"; UnityEngine.Debug.LogError($"[MCP] {result}"); return false; } } catch (Exception ex) { result = $"Exception: {ex.Message}"; UnityEngine.Debug.LogError($"[MCP] {result}\n{ex.StackTrace}"); return false; } } /// <summary> /// MCP Entry Point: Compile and execute static method /// </summary> /// <param name="sourceCode">Complete C# source code</param> /// <param name="className">Name of the class containing the method</param> /// <param name="methodName">Name of the static method to invoke</param> /// <param name="targetGameObjectName">GameObject to pass as parameter (optional)</param> /// <param name="result">Output result message</param> /// <returns>True if successful</returns> public static bool CompileAndExecuteStatic(string sourceCode, string className, string methodName, string targetGameObjectName, out string result) { try { var compiler = GetOrCreateCompiler(); GameObject target = compiler.gameObject; if (!string.IsNullOrEmpty(targetGameObjectName)) { var found = GameObject.Find(targetGameObjectName); if (found != null) { target = found; } } bool success = compiler.CompileAndExecute(sourceCode, className, methodName, target, false, out string error); if (success) { result = $"Successfully compiled and executed '{className}.{methodName}'"; UnityEngine.Debug.Log($"[MCP] {result}"); return true; } else { result = $"Failed: {error}"; UnityEngine.Debug.LogError($"[MCP] {result}"); return false; } } catch (Exception ex) { result = $"Exception: {ex.Message}"; UnityEngine.Debug.LogError($"[MCP] {result}\n{ex.StackTrace}"); return false; } } /// <summary> /// MCP Entry Point: Quick compile and attach MonoBehaviour /// </summary> /// <param name="sourceCode">MonoBehaviour source code</param> /// <param name="className">MonoBehaviour class name</param> /// <param name="gameObjectName">Target GameObject name (creates if null)</param> /// <returns>Success status message</returns> public static string QuickAttachScript(string sourceCode, string className, string gameObjectName = null) { bool success = CompileAndAttach(sourceCode, className, gameObjectName, out string result); return result; } /// <summary> /// MCP Entry Point: Execute code snippet with minimal parameters /// </summary> public static string ExecuteCode(string sourceCode, string className = "AIGenerated") { bool success = CompileAndExecuteStatic(sourceCode, className, "Run", null, out string result); return result; } } #if UNITY_EDITOR // Editor window public class RoslynRuntimeCompilerWindow : EditorWindow { private RoslynRuntimeCompiler helperInScene; private Vector2 scrollPos; private Vector2 diagScroll; private Vector2 historyScroll; private int selectedTab = 0; private string[] tabNames = { "Compiler", "History" }; private int selectedHistoryIndex = -1; private Vector2 historyCodeScroll; // Editor UI state private string codeText = string.Empty; private string typeName = "AIGenerated"; private string methodName = "Run"; private bool attachAsComponent = false; private GameObject targetGameObject = null; [MenuItem("Window/Roslyn Runtime Compiler")] public static void ShowWindow() { var w = GetWindow<RoslynRuntimeCompilerWindow>("Roslyn Runtime Compiler"); w.minSize = new Vector2(600, 400); } void OnEnable() { // try to find an existing helper in scene helperInScene = FindFirstObjectByType<RoslynRuntimeCompiler>(FindObjectsInactive.Include); if (helperInScene == null) { var go = new GameObject("RoslynRuntimeHelper"); helperInScene = go.AddComponent<RoslynRuntimeCompiler>(); // Don't save this helper into scene assets go.hideFlags = HideFlags.HideAndDontSave; } if (helperInScene != null) { codeText = helperInScene.code; typeName = helperInScene.entryTypeName; methodName = helperInScene.entryMethodName; attachAsComponent = helperInScene.attachAsComponent; targetGameObject = helperInScene.targetGameObject; } } void OnDisable() { // keep editor text back to helper if it still exists if (helperInScene != null && helperInScene.gameObject != null) { helperInScene.code = codeText; helperInScene.entryTypeName = typeName; helperInScene.entryMethodName = methodName; helperInScene.attachAsComponent = attachAsComponent; helperInScene.targetGameObject = targetGameObject; } } void OnDestroy() { // Clean up helper object when window is destroyed if (helperInScene != null && helperInScene.gameObject != null) { DestroyImmediate(helperInScene.gameObject); helperInScene = null; } } void OnGUI() { // Ensure helper exists before drawing GUI - recreate if needed if (helperInScene == null || helperInScene.gameObject == null) { // Try to find existing helper first helperInScene = FindFirstObjectByType<RoslynRuntimeCompiler>(FindObjectsInactive.Include); // If still not found, create a new one if (helperInScene == null) { var go = new GameObject("RoslynRuntimeHelper"); helperInScene = go.AddComponent<RoslynRuntimeCompiler>(); go.hideFlags = HideFlags.HideAndDontSave; // Initialize with default values helperInScene.code = codeText; helperInScene.entryTypeName = typeName; helperInScene.entryMethodName = methodName; helperInScene.attachAsComponent = attachAsComponent; helperInScene.targetGameObject = targetGameObject; } else { // Load state from found helper codeText = helperInScene.code; typeName = helperInScene.entryTypeName; methodName = helperInScene.entryMethodName; attachAsComponent = helperInScene.attachAsComponent; targetGameObject = helperInScene.targetGameObject; } } EditorGUILayout.LabelField("Roslyn Runtime Compiler (Editor)", EditorStyles.boldLabel); EditorGUILayout.Space(); // Tab selector selectedTab = GUILayout.Toolbar(selectedTab, tabNames); EditorGUILayout.Space(); if (selectedTab == 0) { DrawCompilerTab(); } else if (selectedTab == 1) { DrawHistoryTab(); } } void DrawCompilerTab() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Entry Type:", GUILayout.Width(70)); typeName = EditorGUILayout.TextField(typeName); EditorGUILayout.LabelField("Method:", GUILayout.Width(50)); methodName = EditorGUILayout.TextField(methodName, GUILayout.Width(120)); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); attachAsComponent = EditorGUILayout.Toggle("Attach as Component", attachAsComponent, GUILayout.Width(200)); if (attachAsComponent) { EditorGUILayout.LabelField("Target:", GUILayout.Width(45)); targetGameObject = (GameObject)EditorGUILayout.ObjectField(targetGameObject, typeof(GameObject), true); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.LabelField("Code (paste LLM output here):"); scrollPos = EditorGUILayout.BeginScrollView(scrollPos, GUILayout.Height(position.height * 0.55f)); codeText = EditorGUILayout.TextArea(codeText, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Compile")) { ApplyToHelper(); if (helperInScene != null) { var ok = helperInScene.CompileInMemory(out var diag); Debug.Log(ok ? "Compile OK" : "Compile Failed\n" + diag); } } bool canRun = helperInScene != null && helperInScene.HasCompiledAssembly && (helperInScene.HasEntryMethod || (helperInScene.HasEntryType && typeof(MonoBehaviour).IsAssignableFrom(helperInScene.EntryType))); GUI.enabled = canRun; if (GUILayout.Button("Run (invoke on selected)")) { ApplyToHelper(); var sel = Selection.activeGameObject; if (sel == null && helperInScene != null && helperInScene.gameObject != null) sel = helperInScene.gameObject; if (sel != null && helperInScene != null) { if (helperInScene.InvokeEntry(sel, out var runtimeErr)) Debug.Log("Invocation OK on: " + sel.name); else Debug.LogError("Invocation failed: " + runtimeErr); } } GUI.enabled = true; if (GUILayout.Button("Compile & Run on helper")) { ApplyToHelper(); if (helperInScene != null) { helperInScene.CompileAndRunOnSelf(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.LabelField("Diagnostics:"); diagScroll = EditorGUILayout.BeginScrollView(diagScroll, GUILayout.Height(120)); string diagnosticsText = (helperInScene != null && helperInScene.lastCompileDiagnostics != null) ? helperInScene.lastCompileDiagnostics : "No diagnostics available."; EditorGUILayout.HelpBox(diagnosticsText, MessageType.Info); EditorGUILayout.EndScrollView(); EditorGUILayout.Space(); EditorGUILayout.LabelField("Notes:"); EditorGUILayout.HelpBox("This compiles code in-memory using Roslyn. Do not write .cs files into Assets while running. Generated code runs with editor permissions.\n\n" + "Supported patterns:\n" + "1. Static method: public static void Run(GameObject host)\n" + "2. MonoBehaviour: Enable 'Attach as Component' for classes inheriting MonoBehaviour\n" + "3. Coroutine: public static IEnumerator RunCoroutine(MonoBehaviour host)\n" + "4. Parameterless: public static void Run()", MessageType.None); } void DrawHistoryTab() { if (helperInScene == null) return; var history = helperInScene.CompilationHistory; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField($"Compilation History ({history.Count} entries)", EditorStyles.boldLabel); if (GUILayout.Button("Save History JSON", GUILayout.Width(140))) { if (helperInScene.SaveHistoryToFile(out string path, out string error)) { EditorUtility.DisplayDialog("Success", $"History saved to:\n{path}", "OK"); } else { EditorUtility.DisplayDialog("Error", $"Failed to save history:\n{error}", "OK"); } } if (GUILayout.Button("Clear History", GUILayout.Width(100))) { if (EditorUtility.DisplayDialog("Clear History", "Are you sure you want to clear all compilation history?", "Yes", "No")) { helperInScene.ClearHistory(); selectedHistoryIndex = -1; } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); if (history.Count == 0) { EditorGUILayout.HelpBox("No compilation history yet. Compile and run scripts to see them here.", MessageType.Info); return; } EditorGUILayout.BeginHorizontal(); // Left panel - history list EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.4f)); EditorGUILayout.LabelField("History Entries:", EditorStyles.boldLabel); historyScroll = EditorGUILayout.BeginScrollView(historyScroll); for (int i = history.Count - 1; i >= 0; i--) // Reverse order (newest first) { var entry = history[i]; GUIStyle entryStyle = new GUIStyle(GUI.skin.button); entryStyle.alignment = TextAnchor.MiddleLeft; entryStyle.normal.textColor = entry.success ? Color.green : Color.red; if (selectedHistoryIndex == i) { entryStyle.normal.background = Texture2D.grayTexture; } string label = $"[{i}] {entry.timestamp} - {entry.typeName}.{entry.methodName}"; if (entry.success) label += " ✓"; else label += " ✗"; if (GUILayout.Button(label, entryStyle, GUILayout.Height(30))) { selectedHistoryIndex = i; } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); // Right panel - selected entry details EditorGUILayout.BeginVertical(); if (selectedHistoryIndex >= 0 && selectedHistoryIndex < history.Count) { var entry = history[selectedHistoryIndex]; EditorGUILayout.LabelField("Entry Details:", EditorStyles.boldLabel); EditorGUILayout.LabelField("Timestamp:", entry.timestamp); EditorGUILayout.LabelField("Type:", entry.typeName); EditorGUILayout.LabelField("Method:", entry.methodName); EditorGUILayout.LabelField("Target:", entry.executionTarget); EditorGUILayout.LabelField("Success:", entry.success ? "Yes" : "No"); EditorGUILayout.Space(); if (!string.IsNullOrEmpty(entry.diagnostics)) { EditorGUILayout.LabelField("Diagnostics:"); EditorGUILayout.HelpBox(entry.diagnostics, entry.success ? MessageType.Info : MessageType.Error); } EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Load to Compiler", GUILayout.Height(25))) { codeText = entry.sourceCode; typeName = entry.typeName; methodName = entry.methodName; selectedTab = 0; // Switch to compiler tab } if (GUILayout.Button("Save as .cs File", GUILayout.Height(25))) { if (helperInScene.SaveHistoryEntryAsScript(selectedHistoryIndex, out string path, out string error)) { EditorUtility.DisplayDialog("Success", $"Script saved to:\n{path}", "OK"); EditorUtility.RevealInFinder(path); } else { EditorUtility.DisplayDialog("Error", $"Failed to save script:\n{error}", "OK"); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); EditorGUILayout.LabelField("Source Code:"); historyCodeScroll = EditorGUILayout.BeginScrollView(historyCodeScroll, GUILayout.ExpandHeight(true)); EditorGUILayout.TextArea(entry.sourceCode, GUILayout.ExpandHeight(true)); EditorGUILayout.EndScrollView(); } else { EditorGUILayout.HelpBox("Select a history entry to view details.", MessageType.Info); } EditorGUILayout.EndVertical(); EditorGUILayout.EndHorizontal(); } void ApplyToHelper() { if (helperInScene == null || helperInScene.gameObject == null) { Debug.LogError("Helper object is missing or destroyed. Cannot apply settings."); return; } helperInScene.code = codeText; helperInScene.entryTypeName = typeName; helperInScene.entryMethodName = methodName; helperInScene.attachAsComponent = attachAsComponent; helperInScene.targetGameObject = targetGameObject; } } #endif

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CoplayDev/unity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server