Lisp based scripting in Unity 3D

A practical example

by Valeriya Pudova, June 2013

This article describes the addition of a Scheme interpreter to the Unity3D game engine resulting in faster, more flexible and more reliable development than using the existing Unity3D C#/Unityscript environment. For games in particular, which have a lot of simultaneous behaviors at runtime and complex data structures to interpret, the high-level Scheme “data as code” duality and functional programming style have proven to be valuable.

Introduction

Unity3D’s GUI-based scripting and configuration is very useful for simple scene construction and elementary character behaviors. However, it does not scale conveniently to larger projects. Creating, editing and otherwise managing large numbers of objects and scripts with correspondingly large numbers of connections and relationships among them with a GUI interface is a daunting proposal.

As other Unity developers have noted [1], a text-based configuration system can allow easier automation, configuration and resource management. Text files are preferable for shared development where multiple developers maintain the current state of a project using source code management. The system described in this article goes one step beyond ordinary JSON or XML text-based configuration by adding script and behavior information to the text files with a LISP style syntax. By using LISP for these text files, it is possible to include code with the data, allowing objects to implement custom behavior without having to create complex type hierarchies in a more static language.  

LISP allows for high-level programming and faster development than C# or Javascript in the Unity environment. Text files that define game objects can also include event-processing routines, state machines, threads and other game objects to be instantiated. This allows game levels to be read in and their game play to be configured with much less ceremony than with C# or Javascript based scene management.

The LISP runtime REPL interface allows debugging, monitoring, scripting and configuration directly from a text-based console. This makes for faster development by avoiding many iterations of the edit-compile-run loop while tuning game parameters or establishing scene configurations.

One well known pattern in game development is the use of data driven application design [2,3,4,7]. Other good examples of using Lisp in game development presented in [4,5,6,7]. The Lisp approach combines code and data into the same S-expression based structures.

Embedding Lisp interpreter in Unity 3d

The system described in this article uses the Scheme dialect of Lisp. The Scheme interpreter is based on the C# TAME Scheme library by Andrew Hunter. It translates source code to instruction bytecode. The source code can be precooked for production, with instruction lists saved to disk files. Also source code can be translated to the CLR, but I do not use this feature.  

Data Types

For performance reasons, most of the data in the system is represented with C# types and formats. The Lisp implementation does not require Lisp representations for application data, preferring instead to add new Lisp primitives. The TAME library makes adding these C#-coded primitives very easy. As result there are not any run-time conversions between C# and Lisp data formats. A few Lisp-determined data types must be used in the application C# code, such as: Symbol, Cons, Function, Continuation, etc., but these are defined as C# classes.

Garbage Collector

The Lisp system does not have its own garbage collector, relying on the native C# one instead. In most cases Lisp code uses object names or opaque handles to reference to objects, so the garbage collector can move objects around in memory as needed. Ordinary C# pointers are used as temporary values in a few places where it is certain that the garbage collector won’t run.

Naming Convention

To help keep track of which “world” a symbol belongs to, a naming convention is used:

  • lower case dashed names for lisp code: elapse-time
  • lower case underscored for C# code: elapse_time
  • camel cased for C# types and classes and for Lisp’s reflection code: PlayerController 

Reflection

The Lisp virtual machine uses reflection to access Unity data. Lisp code for basic operations on objects looks like this:

;;; Get field or property, or call function without arguments

(-> object fieldname) ;; for object

(-> type fieldname)   ;; static member

;;; Set field or property, or call function with arguments

(-> object (fieldname value)) ;; for object

(-> type (fieldname value))   ;; for static

A chain of dot operators like foo.bar(1).baz(2,3)is written as:

(-> object foo (bar 1) (baz 2 3))

Constructing a new object:

(new vector3 1 2 3)

Constructing an object with default constructor and selected fields by key-value pairs

(new* vector3 :x 1 :y 2)

There are a number of other reflection methods: find and cast type, namespaces, enum values, delegates, generic classes, inspect objects and classes. But reflection is slow. That is why we use another approach when performance is needed, called the entity Billboard. The billboard is a dynamic list of typed attributes attached to each interactive object, represented as a table of <key,value> pairs. The cost of a billboard access is similar to a small hash table lookup, which can be much faster than reflection. References to billboard slots can be saved after an initial lookup, allowing access at speeds similar to those of C++ smart pointers.

Game Object & Entity

As the GameObject class cannot be extended with additional attributes or methods, we instead separate these items to a single Entity component class. Every interactive game object must have this component (or a component derived from the Entity component). Entity component instances contain the billboard and bindings to the Lisp code.

Figure 1. Type Hierarchy

Custom Entity types are rarely actually needed because the Entity class can be extended with Lisp script and billboard entries, but they sometimes useful for avoiding otherwise repetitious code. Entity behavior and data can be additionally defined and customized by adding Custom Components.

The spawner is special case. It is a lightweight proxy object for some Entity that will be created at runtime, which exposes to the Unity editor the billboard of the spawnable object. Because the spawner does not have its own renderable mesh, it renders in the editor the shape of spawnable object.

Figure 2. Spawner does not have it’s own mesh filter or renderer but it renders spawnable mesh in editor window. Previewed object is selectable as well.

Component System

The Unity component system usable as-is for C# and Javascript, but could not easily be accessed from Lisp. The basic problem was how to communicate between components in an Entity. The C# way would be to use messages or to define interfaces that different types of components would provide. This is a problem for Lisp because with messages, the Lisp and C# definitions of messages would have to be kept in sync and time would be lost formatting and interpreting messages. If interfaces were used instead, the Lisp code would have to use reflection at runtime which also would be sloy. Instead of messages or interfaces, we use the Entity billboard for communicating and sharing values between components and with the Lisp code.

The billboard is a container for named, typed attributes. Attribute names are unique for each game object. Every component has a declaration of its attributes. When an attribute declaration is executed during initialization of any component in an Entity, the system checks whether the Entity’s billboard already has that attribute. If it does, the attribute declaration returns a reference to the existing billboard slot. Otherwise, a new slot for the attribute is added to the billboard and a reference to that slot is returned.

Figure 3. Components and billboard

Whenever any attribute is updated, a callback to the component is scheduled to allow the component to react to the changes. The callback mechanism is smart enough to prevent extra callbacks when an attribute is changed multiple times in a single frame or if multiple attributes were updated. Declaration and use of a billboard attribute in C# looks like:

[BillboardFloat("angle-min",0f)]

private PropFloat angle_min;

void Awake ()

{

        // look up the attribute once by name and save a reference

        angle_min = FindProperty ("angle-min") as PropFloat;

}

void Update()

{

        angle = Mathf.Max(angle + angle_velocity, angle_min.GetFloat());

}

In this particular case attribute has float type, but many other attribute types are available. Billboard attribute definitions and values are exposed in the editor.

Figure 4. Billboard Editor.  Properties are grouped by component.

With the billboard technique the Lisp can work fast. Access to any attribute almost same fast as to local variable. There is an example:

(define (change-direction new-direction)

(set! old-direction (get-vector3 :direction))

(set-vector3 :direction new-direction))

There is another benefit of the billboard. Imagine object spawner which should spawn a character when it receives a message. The spawner has a reference to the character’s prefab, so it can add the character prefab’s billboard contents to the spawner’s billboard. The spawner can then customize the resulting character’s billboard by overriding selected values from the prefab or by adding new values.

Messaging system

Unity’s messaging system is good for sending messages between C# behaviors, but it presents a problem for communicating between Lisp and C#. Because every SendMessage eventually calls some C# method, there must be a C# method for each message. But for some messages, the intended recipient could be Lisp code. That is why entities use a custom message system. Sending a message to an entity looks like:

Message msg;

msg = new Message (sender-entity, Symbols.s_fire);

msg.Send (recipient-entity);

Any required parameters are simply added to the message:

Message msg;

msg = new Message (sender-entity, Symbol.s_damage);

msg[Symbol.s_force] = damageForce;

msg[Symbol.s_position] = hitPosition;

msg.Send (recipient-entity);

The entity could receive this message with C# code or with Lisp code.

public override bool OnMessage (Message msg)

{

        if (msg.name == Symbol.s_animate) {

                string animation = msg[Symbol.s_name] as string;

                Animate(animation);

                return true;

        }

        return base.OnMessage(msg);

}

The message will be delivered this message to the entity’s state process Lisp code if the C#  OnMessage() does not return true. A Lisp message handler looks like:

(on (event go-to-level)

    (destroy player)

    (set! level-name (msg-get :level-name))

    (spawn player)

    (go :start))

As you can see access to the message properties same as to the billboard. Sending message from Lisp code is simple too.

(send elapse-time-name :show)

Additional parameters can be specified with a key-value pair list, same as in the C# case.

(send "player-l" :go-to-level :level-name “deadzone” :elapse-time 100)

Because of their self-describing <key,value> format, messages can easily be edited and redirected

; send current message

(msg-send "player-l")

; send current message but override message name

(msg-send "player-l" :shoot)

; send current message but override or add parameters

(msg-send "player-l" null :speed 100 :direction (go-direction-to “exit-dor”))

As a design convention, one entity cannot access the fields of another entity directly. Messages must be used instead to transfer values. However, components within the same entity can communicate through the entity’s billboard.

Messages can be delivered immediately or after some delay. Message processing  inside a single object follows a sequence:

  • Entity C# code which can quickly decide which components, if any, should receive the message.
  • Entity Lisp code may alter or process the message and may return it to the entity’s components.
  • Entity components C# code can take any action based on the message contents.

This allows the Lisp code to override the initial behaviour of the components, because Lisp can receive the message and throw it away, replace it with another, or change some message data. Another convention is not to use the same message name for a command to different components of any one entity. For example if two different components can move the Entity to some target position but by different methods, then the messages must be different too, e.g., move-to-with-ai and move-to-straight.

State Process

Every entity may refer to a Lisp script called the entity state process. In this case the state process extends the entity. A state process definition looks like:

(define state-process-name

  (new-state-object (state-process-name)

       ; Initial state definition

       :initial-state :idle

       ; List of parameters and methods shared between states

       ; State process local scope

       :properties (prop-list

                     ; shared variables

                     (field-name field-initial-value)

                     ; other variables here

                     ; shared methods

(define (method-name arguments)

   ; Code here: method body

   )

; other methods here

))

       ; List of states

       :states (state-list

                ; Single state definition

                (define-state (idle)

                  ; Define C# functions which will be executed

                  ; every frame while state process in this state

                  (on (proc) :c_function_name)

                  ; State events

                  (on (enter)

                      ; when enter to this state

                      ; Code here)

                  (on (exit)

                      ; when exit from this state

                      ; Code here)

                  (on (update)

                      ; every frame

                      ; code here)

                  ; custom events

                  (on (event event-name)

                      ; when receive message 'event-name'

                      ; code here)

                  ; other custom events here

               )

               ; other states here

       )))

Every state process has a unique named list of states and additional attributes. One attribute is the name of the default state. Other attributes comprise a list of properties which may contain variable and method declarations. After that follows the states list.

Every state has list of methods. There are three important methods which every state can define: enter, exit, update. When state process switches from state A to state B, it will call A.exit then B.enter. After that, it will call B.update every frame as long as B remains the current state. Other message processing methods can be added with the (on (event event-name) ...) syntax.

The Lisp interpreter is fast but it is not as fast as C#, and it’s often better to code per-frame processing for an entity in C#. A state process can call C# code directly by using the special  proc keyword.  For example, assume that the custom entity class has C# methods: move_forward, rotate_to_enemy.

(define-state (idle)

  ; Define C# functions which will be executed every frame while

 ; state process in this state

 (on (proc) :move_forward)

 (on (proc) :rotate_to_enemy)

Additionally there are two other primitives proc- and proc+ to control the process list in a more dynamic manner. These primitives can be used inside any Lisp function.

(define-state (idle)

 (on (proc) :move_forward)

 (on (event :reach_goal)    

    ; disable C# function

    (proc- :move_forward)

    ; enable C# function

    (proc+ :rotate_to_enemy))

Message receivers added to a state named “default” will be called for messages received while in any other state, unless that other state declares its own receiver. This saves some code duplication for messages that the entity must always be ready to process regardless of the current state.

Entire state processes or and individual single states can be inherited from state processes. After inheritance, individual states and methods could be overridden.  

(new-state-object (monkey npc) ; monkey inherits from npc

   ; Override state idle

   (define-state (idle)

     ; State code)

   ; Override state walking by state from biped’s sleeping

   (define-state (walking (biped walking))))

In any state process code the self keyword refers to the entity that owns the state process.

(animate :self "die")

Any other object can be referred to by entity, game object or its name:

(animate current-camera "look-at" sun-position)

(animate "player" "die")

(animate (go-find "player") "die")

Switching  to a different state can be achieved with the go primitive.

Thread Control

Any lisp function is a coroutine and can  yield and later be resumed:

; Wait seconds

(wait-time 10)

; Sleep current continuation

(sleep)

; Resume continuation

(resume continuation)

To make new continuation we can get the current continuation with cc primitive or create continuation with continuation-new primitive.

There are a lot more primitives for thread control, but the main benefit of all of them is less ceremony that C# equivalent.

Object Spawning in Lisp

One common task for the Lisp code is instantiating prefabs in the scene. That is accomplished with this syntax:

(spawn resource-name parameter-list)

The parameter-list is a key-value pair list. The following parameters apply directly to the prefab:

name         instance name

position        instance position

rotation        instance rotation

parent        name of parent object

Any other parameters (in this example health) will be used to update the billboard and state process scope (list of parameters shared between states). So when we spawn any object we can in the same function call override a bunch of values. Example:

(spawn "player"

   :name "player-left"

   :parent "player-left"

   :position (go-get-position “start-position”)

   :rotation (go-get-rotation “start-position”)

   :health 100)

Behaviour tree

We looked at using behavior trees [8,9,10] but decided against them for the current project. Instead, state processes are used to implement character reasoning, reactions and sequencing. Just using Lisp carefully can give similar notational convenience for programmers, and there are no non-programmer scripters (the main beneficiaries of behavior tree’s declarative style) on this game project.

REPL

The REPL runs as an entity in the game level. The state script bound to this entity is the main level logic. Also, the REPL executes before any other object in the level.

We have two implementations of the REPL window, one for the Unity Editor and another for Application. Both variants have a history buffer and navigation inside history.

Figure 4. Unity Lisp console. This window shows switching to a selected game level and starting interactive development mode.

The Unity Editor version of the REPL sets the dynamic scope for evaluation to the scope of the currently selected object.  The prompt display the selected state script name. In this case the self keyword will refer to selected state process.

Script source files are located in the StreamingAssets folder.

Figure 5. Emacs window with state script

Performance

TAME scheme speed  is decent.  It uses efficient instruction lists for functions and a very small VM main loop. There are faster implementations of Lisp but TAME was preferable due to its easy extensibility, friendliness to C# code and embeddable architecture. We considered making our own Scheme which could compile directly to machine code and interface to other languages, and also allow more of the game to be coded in Lisp, but the project schedule did not allow it. Maybe next time.

In general there was not any performance issue because the Lisp code runs per event, and not per frame in most cases. Per-frame Lisp code is limited to short functions or temporary conditions,  and only for a few active processes at a time.

Conclusion

The LISP based component and behavior tree system has proven to be useful, increasing productivity and making more complex game logic easier to implement by avoiding Unity’s high-ceremony C# API’s. This provides a direct benefit for the current game development: with a more powerful language and component model, more things are possible. The overhead from adding this extra level of interpretation above Unity has not been a significant problem.

Problems which I met while doing this job:

  • Unity does not permit customized naming convention for fields names. Editor works correctly only with camel-case.
  • Custom editor can be made only for classes. Better approach will be if custom editor can be defined per field type.
  • Unity serialization in the editor is awkward and hard to extend with new field types.
  • The object loading and initialization with methods Awake, Start, OnEnable is not flexible enough.
  • Order of script computation can’t be controlled with C# script. There is only pure GUI tool for.
  • Reflection is slow and awkward
  • Unity log window slow and not extensible enough. I would like to print errors from Lisp and open Lisp document when double click on error message.
  • GameObject.Find ignores disabled objects, and plenty of others small issues like that.
  • Some Unity bugs
  • TAME did not have debugging features built in. We added source file, line and character position information to bytecode instructions in debug mode, so the REPL could show the current position in Lisp source code and also print useful stack traces. This worked except for code produced by macro expansion. TAME’s macro expander is coded in C# and translates directly to bytecode, losing any debugging information. There is no equivalent to macroexpand, so it was sometimes hard to figure out the sources of problems with macros.

References

  1. 50 Tips for Working with Unity (Best Practices) by Herman Tulleken, 2012
  2. A Data-Driven Game Object System, Scott Bilas, Gas Powered Games, GDC 2002
  3. Adventures-In-Data-Compilation, Naughty Dog, GDC2008
  4. State-Based Scripting in Uncharted 2, Jason Gregory, Naughty Dog, GDC2009
  5. Making Crash Bandicoot, Andy Gavin
  6. Postmortem: Naughty Dog's Jak and Daxter: the Precursor Legacy, Stephen White
  7. Game Engine Architecture, Jason Gregory
  8. Behaviour Trees, Simon Colton & Alison Pease
  9. Behavior Trees and Reactive Planning, Peter Mawhorter, October 8, 2010
  10. Evolving Behaviour Trees for the Commercial Game DEFCON, Chong-U Lim, Robin Baumgarten and Simon Colton

●     Valeriya P,  June 2013     ●