7
\$\begingroup\$

The idea here is to let users program the behaviour of an agent in a way that reminds of the "Screeps" game, where you get to develop code which later runs and dictates the behaviour of your units.

My idea is to let users develop the behaviour or their units with C# code, compile that code at runtime and attach it as a MonoBehavior to an existing game object.

How can I achieve this?

\$\endgroup\$
  • 2
    \$\begingroup\$ Yes. It is possible. Does this really answer your question though? \$\endgroup\$ – Kromster Sep 21 '16 at 10:22
  • 2
    \$\begingroup\$ No need to delete, just edit! :) \$\endgroup\$ – Tyyppi_77 Sep 21 '16 at 10:26
  • 1
    \$\begingroup\$ Who has said anything about implementing a custom language? You yourself brought that up. \$\endgroup\$ – Tyyppi_77 Sep 21 '16 at 10:44
  • 3
    \$\begingroup\$ You could also use an existing scripting language. For example, there is JavaScript.net which allows you to interpret and execute JavaScripts from a C# program. Pro: minimal implementation effort, you can refer users to existing JS learning material. Con: Might be too powerful and too confusing for your limited use-case. \$\endgroup\$ – Philipp Sep 21 '16 at 10:45
  • 1
    \$\begingroup\$ Another option would be to create a visual editor where users drag and drop around blocks of instructions. Pro: Looks and feels more like a game, easier to teach, easier to learn for non-programmers. Contra: Takes time to develop, might be too limiting for experienced programmers (and those who become it by playing the game). \$\endgroup\$ – Philipp Sep 21 '16 at 10:51
12
\$\begingroup\$

I haven't done this myself, so this answer is based on an article by exodrifter (I just Googled "unity runtime compile" and found 4 answers right at the top, so don't neglect to try searching yourself!)

What you're describing is possible, but not without pitfalls:

  • Use .Net 2.0 compatibility level, otherwise some namespaces you need may be absent.

  • Not every platform will allow you to compile new C# from strings at runtime - specifically Android and iOS disallow this.

  • Out of the box this works on Windows, but not on Macs/Linux unless the user has the Mono SDK installed. You can include aeroson's mcs-ICodeCompiler to build this functionality into your game as a plugin.

exodrifter's example code (modified to add a component)

(refer to the article for full details - I just want to include enough here that this answer is a useful signpost even if the linked article goes down)

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

public class RuntimeCompileTest : MonoBehaviour
{
    void Start()
    {
        var assembly = Compile(@"
        using UnityEngine;

        public class RuntimeCompiled : MonoBehaviour
        {
            public static RuntimeCompiled AddYourselfTo(GameObject host)
            {
                return host.AddComponent<RuntimeCompiled>();
            }

            void Start()
            {
                Debug.Log(""The runtime compiled component was successfully attached to"" + gameObject.name);
            }
        }");    

        var runtimeType = assembly.GetType("RuntimeCompiled");
        var method = runtimeType.GetMethod("AddYourselfTo");
        var del = (Func<GameObject, MonoBehaviour>)
                      Delegate.CreateDelegate(
                          typeof(Func<GameObject, MonoBehaviour>), 
                          method
                  );

        // We ask the compiled method to add its component to this.gameObject
        var addedComponent = del.Invoke(gameObject);

        // The delegate pre-bakes the reflection, so repeated calls don't
        // cost us every time, as long as we keep re-using the delegate.
    }

    public static Assembly Compile(string source)
    {
        // Replace this Compiler.CSharpCodeProvider wth aeroson's version
        // if you're targeting non-Windows platforms:
        var provider = new CSharpCodeProvider();
        var param = new CompilerParameters();

        // Add ALL of the assembly references
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            param.ReferencedAssemblies.Add(assembly.Location);
        }

        // Or, uncomment just the assemblies you need...

        // System namespace for common types like collections.
        //param.ReferencedAssemblies.Add("System.dll");

        // This contains methods from the Unity namespaces:
        //param.ReferencedAssemblies.Add("UnityEngines.dll");

        // This assembly contains runtime C# code from your Assets folders:
        // (If you're using editor scripts, they may be in another assembly)
        //param.ReferencedAssemblies.Add("CSharp.dll");


        // Generate a dll in memory
        param.GenerateExecutable = false;
        param.GenerateInMemory = true;

        // Compile the source
        var result = provider.CompileAssemblyFromSource(param, source);

        if (result.Errors.Count > 0) {
            var msg = new StringBuilder();
            foreach (CompilerError error in result.Errors) {
                msg.AppendFormat("Error ({0}): {1}\n",
                    error.ErrorNumber, error.ErrorText);
            }
            throw new Exception(msg.ToString());
        }

        // Return the assembly
        return result.CompiledAssembly;
    }
}

Warning

Be very careful when letting your game compile & run scripts you didn't write yourself. These scripts could be made to do just about anything the player's user account has privilege to do - delete or corrupt user files, download and run malware, snoop personal information, all of that.

As much as we might try to block & detect these cases, I'd never be especially confident we'd caught them all - there's always the possibility some C# language feature I didn't know about lets them access features I thought were cut off.

If the script is only created by the local player, then they can break the game in arbitrary ways, but can't do anything to their computer they lacked permission to do anyway. But if players can share scripts, download them from a network resource, or can be tricked into copying-and-pasting a script they didn't write, then this becomes an attack vector that can be exploited.

If you want to allow programming by the player without opening up this kind of vulnerability, consider writing your own virtual machine to parse/interpret/execute the player's scripts in a more limited environment that you control completely.

\$\endgroup\$
  • \$\begingroup\$ This answers half the question, but not yet what to do with the Assembly object to attach it as a component to a game object. The problem is that GameObject.AddComponent requires a string, so I doubt that it is going to find the new class when it's not in the right assembly. As a possible workaround one could create a wrapper-behavior which calls methods from the new assembly through delegates. \$\endgroup\$ – Philipp Sep 27 '16 at 14:11
  • 1
    \$\begingroup\$ @Philipp: I found a workaround by asking the compiled script to add itself to a provided GameObject. ;) Tested and it works. \$\endgroup\$ – DMGregory Sep 27 '16 at 14:44
  • \$\begingroup\$ Thanks for the input, especially regarding the warnings you gave me, I had an idea of them but not so concise as you stated \$\endgroup\$ – Oscar Guillamon Sep 28 '16 at 10:31
  • \$\begingroup\$ @DMGregory - let's see the workaround how you added the compiled script to the GameObject! \$\endgroup\$ – eric frazer Dec 24 '17 at 17:22
  • \$\begingroup\$ @eric It's in the code above — the AddYourselfTo method invoked through reflection. \$\endgroup\$ – DMGregory Dec 24 '17 at 17:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.