Developing the Scripting API

With the vertical slice complete and the 3D renderer in a good spot, this week I decided to shift focus to the scripting API of the engine.

Scripting is a very desirable feature for any engine. It allows adding (and modifying) logic on the fly, without having to recompile or relink any parts of the program. It makes iteration times super fast, enabling creativity.

In Vortex, we chose Lua for the scripting backend. We added initial support about a year ago. At that time, we decided to build a custom binding from scratch and we succeeded, but the work done was mostly proof of concept. This weekend, the objective was to expand this foundation so scripts could perform more useful tasks, such as inspecting and manipulating the world.

In order to achieve this, a number of changes were needed, both at the scripting level and at the editor level. In particular, we needed:

  • A way to wrap and expose entity transforms to Lua scripts.
  • A way to mutate these transforms.
  • A way for scripts to add themselves to the runloop and run logic every frame.
  • A way for the engine and editor to run (and “step”) scripts.
  • A way to hot reload scripts and rebooting VM when things went south.

The video above shows all these concepts coming together to allow creating a simple simulation of a ball bouncing inside a 3D box. The ball has green a point light inside that moves around with it. This is mostly to show that this simulation is still running on the engine’s modern deferred renderer ;)

The Scripting Model

Key to the scripting model is the ability to talk to the engine from a loaded script and find objects in the scene. This allows the user to visually create worlds in the Vortex Editor and then find the important entities from scripts.

Scripts can also create their own entities of course, but for this example, we just wanted to pre-build the world visually.

For the bouncy ball example in the video above, we started off by creating the containing box, the ball object, and the lights in the scene. We used the Editor tools to create all materials and define the look of the entities and lighting.

But once we have our visual scene, how do we script it?

The entry point for scripts running in Vortex is the vtx namespace. Scripts hosted by Vortex automatically get access to a global table with entry points to the engine.

Functions in the vtx namespace are serviced directly from C++. This is a powerful abstraction that allows exposing virtually all engine functionality to a script.

This is exactly what we did. Through the vtx namespace, the bouncy_ball.lua script easily finds the ball, the walls, and the light. Once we have these objects we can get their transforms and register a function that will update them every frame.

Running Scripts

Once our script is ready, we can bring it into the scene directly from within the Editor.

Currently, loading any script will execute it. This runs all code at the file scope inside it. It’s important that scripts that want to respond to engine events register their callbacks at this point.

In order to run every frame, we are interested in the on_frame event inside the vtx.callbacks table. This table behaves essentially like a list. Once every frame, the engine will walk this list and call all functions registered there.

Pausing and Testing

Since the runloop is controlled directly by the engine, this gives the Editor enormous control over script execution. In particular, we can use the Editor to pause and even step scripts!

Coupled with the Editor’s REPL Lua console, this gives the user a lot of control. Through the Editor UI, the user can stop the scripts and inspect and change any Lua objects in realtime. No need to recompile the Editor or reload the scene or scripts.

Show me the Code!

Ok, we covered a lot of ground above. To help the concepts settle in, here’s the complete bouncy_ball.lua script used to build the simulation shown above. The main points of interest are the main and on_frame functions.

-- A ball bouncing inside a 3D box in Vortex Engine
-- This script looks for the following entities in the scene:
-- 1. box (the container)
-- 2. ball (the bouncing ball)
-- 3. ball_light (a light that is placed inside the ball)

move_speed = 5.0 -- ball move speed

function main()
    -- Find entities that we need and cache important
    -- transforms. 
    ball = vtx.find_first_entity_by_name( "ball" )
    ball_xform = ball:get_transform()
    ball_xform:set_position( 0, 3, 0, 1 )
    ball_radius = ball_xform:get_scale() * 0.5

    ball_light = vtx.find_first_entity_by_name( "ball_light" )
    ball_light_xform = ball_light:get_transform()
    ball_light_xform:set_position( 0, 3, 0, 1 )

    box = vtx.find_first_entity_by_name( "box" )
    box_xform = box:get_transform()
    bx,by,bz = box_xform:get_position()
    bsx, bsy, bsz = box_xform:get_scale()
    box_scale = { bsx, bsy, bsz }

    move_dir = { 1.0, 0.75, 0.5 } -- could be randomized with a seed

    -- Add ourselves to the engine's scripting runloop:
    table.insert( vtx.callbacks.on_frame, on_frame )
end

function on_frame( deltat )
    -- Called every frame by the engine

    x,y,z,w = ball_xform:get_position()

    -- Arrays in Lua are 1-based:
    x = x + move_dir[1] * deltat * move_speed
    y = y + move_dir[2] * deltat * move_speed
    z = z + move_dir[3] * deltat * move_speed

    if x + ball_radius >= bx + box_scale[1] * 0.5 or x - ball_radius <= bx - box_scale[1] * 0.5 then
        move_dir[1] = -move_dir[1]
    end

    if y + ball_radius >= by + box_scale[2] * 0.5 or y - ball_radius <= by - box_scale[2] * 0.5 then
        move_dir[2] = -move_dir[2]
    end

    if z + ball_radius >= bz + box_scale[3] * 0.5 or z - ball_radius <= bz - box_scale[3] * 0.5 then
        move_dir[3] = -move_dir[3]
    end

    ball_xform:set_position( x, y, z, w )
    ball_light_xform:set_position( x, y, z, w)

end

main()

The main function is responsible for finding all important entities in the scene and initializing the simulation. As mentioned before, it is run as soon as the script is loaded into the engine. Notice how the main function adds the on_frame function to the runloop.

The on_frame function runs every frame. It receives a time scale that can be used to implement a framerate-independent simulation.

It is worth noting that nothing in the on_frame function allocates memory. In particular, position components are passed into and pulled out of the engine in the Lua stack, with no heap allocations. This is important, as Lua has a Garbage-Collected runtime and we want to avoid collection pauses during the simulation.

Conclusion

It's been a lot of fun exploring hosting a scripting language inside the engine and manually building the binding between it and C++.

I think the ability of defining the visual appearance of the scene from the Editor and then allowing scripts to find entities at runtime was the right decision at this time. It's a simple model that solves the problem elegantly and can be performant if you cache things you need access often.

I am going to continue working on the binding further and seeing how far it can go. It's a good break from just working on the renderer all the time ;)

I'm definitely interested in your thoughts! Please share below and, as usual, stay tuned for more!

2 thoughts on “Developing the Scripting API

  1. I have a question about “It is worth noting that nothing in the on_frame function allocates memory.” For this to be true, shoudn’t the x,y,z,w be local, i.e.
    local x,y,z,w = ball_xform:get_position()
    ?

Comments are closed.