Scripting Unity

As #Script is a clean embeddable scripting language with no compilation, few dependencies and a cascading Reflection Utils for utilizing the fastest implementation capable per target platform, it's able to add scripting to most C#/.NET platforms supporting .NET Framework or .NET Standard 2.0 target frameworks (e.g. .NET Core/Mono).

One of C#'s popular target platforms that's appealing to extend via scripting is the Unity Game Engine which allows you by-pass the slow dev iteration cycles letting you to rapidly prototype a scene, or dynamically create one without needing to rebuild or redeploy your Game when downloading and evaluating scripts on-the-fly.

To showcase Lisp scripting in Unity we've added an in-game REPL and used it to create and modify Unity Game Objects:

This demo is just running a copy of Unity's Karting MyFirstGame Project Template with scripting support added. We'll go through how to add scripting support below. For reference, a copy of this project has been published to:

Add ServiceStack.Common .dll's to your project

Follow the Using .NET 4.x in Unity guide to add .NET .dll's to a unity project which requires adding the binaries from the ServiceStack.Common NuGet package and all its dependencies to your projects plugins folder and a link.xml file to preserve LINQ Expression Reflection support in Unity's bytecode stripping:

To simplify downloading NuGet packages independently, a copy of ServiceStack.Common .dll's and all its dependencies is available from:

Add REPL InputField and Text UI Objects

Add the REPL UI InputField Control for capturing Lisp script input and a Text label for displaying REPL output:

Modify InputField by increasing the height and width of the control and changing the Line Type to Multi Line Submit:

The positioning of each control can also be updated using Drag and Drop from Unity's Scene UI editor.

Add New C# Script to your GameObject

We can then implement the behavior of our REPL by adding a New Script component to our GameObject:

The entire REPL behavior is encapsulated inside the ScriptExample.cs below:

using System;
using System.IO;
using ServiceStack.Script;
using UnityEngine;
using UnityEngine.UI;

public class ScriptExample : MonoBehaviour
{
    private ScriptContext script;

    /// <summary>
    /// Typed API wrappers required for some of Unity's "special properties"
    /// </summary>
    public class UnityScripts : ScriptMethods
    {
        public string name(GameObject o, string name) => o.name = name; 
        public Transform transform(Component c) => c.transform;
        public Color color(Material m, Color color) { m.SetColor("_Color", color); return color; }
        public Vector3 position(Transform t) => t.position;
        public Vector3 position(Transform t, Vector3 position) => t.position = position;
        public Vector3 localScale(Transform t) => t.localScale;
        public Vector3 localScale(Transform t, Vector3 localScale) => t.localScale = localScale;
    }

    Lisp.Interpreter lisp;
    InputField txtRepl;
    Text textReplOut;

    // Start is called before the first frame update
    void Start()
    {
        script = new ScriptContext {
            ScriptLanguages = {
                ScriptLisp.Language  
            },
            ScriptMethods = {
                new ProtectedScripts(),  
                new UnityScripts(),
            },
            AllowScriptingOfAllTypes = true,
            ScriptNamespaces = {
                nameof(UnityEngine)
            },
            Args = {
                [nameof(gameObject)] = gameObject,
            }
        }.Init();
        
        lisp = Lisp.CreateInterpreter();

        txtRepl = gameObject.GetComponentInChildren<InputField>();
        textReplOut = gameObject.GetComponentInChildren<Text>();
    }

    private string lastScript = "";

    // Update is called once per frame
    void Update()
    {
        if ((Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl)) 
            && Input.GetKey(KeyCode.Return))
        {
            var srcLisp = txtRepl.text;
            if (srcLisp == lastScript) // prevent multiple evals
                return;
            
            lastScript = srcLisp;
            try
            {
                var output = lisp.ReplEval(script, Stream.Null, srcLisp);
                textReplOut.color = Color.white;
                textReplOut.text = output;
            }
            catch (Exception e)
            {
                textReplOut.color = Color.red;
                textReplOut.text = e.ToString();
            }
            txtRepl.Select();
            txtRepl.ActivateInputField();
        }
    }
}

In void Start() you'll want to initialize any one-off initialization tasks like initializing the ScriptContext and creating the Lisp.Interpreter that will be used to evaluate code within the Lisp REPL as well as storing references to the current gameObject, InputField and Text UI controls.

Update() is where to put your scripts runtime logic which gets called once per frame. Here we listen out for the Ctrl+Enter shortcut key combination to trigger evaluating the Lisp source code that's in the InputField text field. lastScript is maintained to prevent multiple evaluations as Update() can be called multiple times with the same keyboard combination.

The same Lisp.Interpreter instance is used for each evaluation which maintains any state defined from previous evaluations.

Unity Script Methods

Unfortunately Lisp can't access all of Unity's APIs directly where it doesn't appear you can access Unity's native API wrappers from reflection, e.g:

public class Component : Object
{
    public extern Transform transform { 
        [FreeFunction("GetTransform", HasExplicitThis = true, ThrowsException = true), 
            MethodImpl(MethodImplOptions.InternalCall)] get; }
}

In these cases we require adding our own .NET Script Method wrapper API which Scripts can access instead:

public class UnityScripts : ScriptMethods
{
    public Transform transform(Component c) => c.transform;
    public Vector3 position(Transform t) => t.position;
    public Vector3 position(Transform t, Vector3 position) => t.position = position;
}

To maximize API familiarity we use the same name for the script method getters/setters, e.g:

(transform instance)

Which just requires stripping the . prefix from normal instance member access:

(.transform instance)

Normal .NET Properties like material:

public class Renderer : Component
{
    public Material material
    {
        get
        {
            if (!this.IsPersistent())
            return this.GetMaterial();
            return (Material) null;
        }
        set
        {
            this.SetMaterial(value);
        }
    }
}

Will be able to be accessed using normal member instance notation directly, e.g:

(.material instance)

Ignoring Key Input when in REPL

The only other code changes needed was in the KartRepositionTrigger.cs and KeyboardInput.cs classes to ignore any Keyboard input whilst typing in the REPL:

void Update ()
{
    if (Selectable.allSelectables.Any(x => x is InputField))
        return;
    //...
}

Annotated Unity REPL Transcript

; test Lisp REPL
(println "Hi from #Script Lisp! " (+ 1 2 3))

; set the color of the gameObject's first Renderer Material
(color (.material (.GetComponentInChildren<Renderer> gameObject)) (Color/cyan))

; set the colors of all gameObject's Renderer's Material 
(map #(color (.material %) (Color/cyan)) (.GetComponentsInChildren<Renderer> gameObject))

; create a "Cube" GameObject and assign its Renderer to 'R'
(def R (.GetComponent<Renderer> (GameObject/CreatePrimitive "Cube")))

; modify the position of the cube
(position (transform R) (Vector3. 15 3 3))
(position (transform R) (Vector3. 13 1 5))

; change the color of the cube's Material to grey
(color (.material R) (Color/grey))

; scale the cube to 150% of its size
(localScale (transform R) (Vector3. 1.5 1.5 1.5))

; change the color of the cube's Material to blue
(color (.material R) (Color/blue))

; create 3 "Sphere" GameObject's and assign to 'spheres'
(def spheres (map #(GameObject/CreatePrimitive "Sphere") (range 3)))
; assign the spheres Renderers to 'SR'
(def SR (map #(.GetComponent<Renderer> %) spheres))

; move the position of all 3 spheres
(map-index #(position (transform %1) (Vector3. (+ (* %2 2) 13) 4 4)) SR)
(map-index #(position (transform %1) (Vector3. (+ (* %2 2) 13) 3 3)) SR)

; change the color of each sphere to a different color
(def colors [(Color/green) (Color/yellow) (Color/red)])
(map-index #(color (.material %1) (nth colors %2)) SR)

; scale all spheres to 150% of its size
(map #(localScale (transform %) (Vector3. 1.5 1.5 1.5)) SR)

; add Rigidbody to all spheeres putting it under control of Unity's physics engine and giving it gravity
(def SRB (map #(.AddComponent<Rigidbody> %) spheres))

made with by ServiceStack