🌝 Lua Scripting#

IOLITE uses Lua to provide its scripting functionality. Lua is an excellent language for scripting due to its minimal footprint, efficiency, and ease of use.

If you have not worked with Lua before, make sure to check out the following resources to learn the language:

Programming in Lua (First Edition)

https://www.lua.org/pil/contents.html

The Lua 5.1 Reference Manual

https://www.lua.org/manual/5.1/

Important

This section serves as an introduction to the Lua scripting integration in IOLITE. For further details, make sure to check out the Lua API documentation to quickly locate the functionality you are looking for.

Lua runtime and libraries#

IOLITE uses LuaJIT-2.1.0-beta3 for maximum performance, which supports the language features of Lua 5.1. The following standard Lua libraries are available:

  • base, coroutine, string, and table

Check out the following resources if you’re interested in the features and implications induced by using the LuaJIT runtime:

LuaJIT Homepage

https://luajit.org

LuaJIT Overview

https://luajit.org/luajit.html

LuaJIT Benchmarks

https://luajit.org/performance_x86.html

The basic structure of scripts#

When using scripts, IOLITE expects you to provide a particular set of callback functions with the correct naming and parameters, which it can call in different scenarios.

Important

Please note that you do not need to provide all callback functions, even not providing any callback function is fine. IOLITE checks for the availability of each upfront.

The minimal.lua script available in the default data source contains stubs for all the available callback functions and can serve as a template for creating new scripts.

Let’s have a closer look at each of the available callback functions.

Available callback functions#

function OnActivate(entity)
end

Called precisely once during a script’s lifetime.

This function is called once when the script becomes active. Scripts become active when, e.g., a world is loaded or if an entity with an attached script component gets spawned. This is the right place to set up additional resources and variables for your script.

function OnDeactivate(entity)
end

Called precisely once during a script’s lifetime.

This is the counterpart to onActivate and is called once when the script becomes active, either by unloading a world or by destroying a script component during runtime. Use this to tear down additional resources created by your script.

function Tick(entity, delta_t)
end

Called precisely once each rendered frame.

Use this function for functionality that has a visual effect, like updating the final position of a character or a projectile, for example. It’s also the right spot to react to the user’s input as quickly as possible.

In general, it’s wise to keep the workload in this function to a minimum and, e.g., implement actual gameplay and AI logic in the OnUpdate callback function at a lower frequency. The results computed at the lower frequency can then be interpolated in this function to achieve visually pleasing results.

function Update(entity, delta_t)
end

Called exactly once at the interval specified in the script component.

Use this callback for implementing logic that has no imminent visual effect. This is the perfect spot for implementing AI and gameplay logic.

Important

Don’t use this function for reacting on input or for updating data that has a visual effect!

function OnEvent(entity, events)
end

Called as soon as one or multiple events are available.

All the different types of available events are described in a later section. But the grasp the general concept, here’s an example of handling contact events that occur when voxel shapes, and their rigid bodies, interact with each other:

function OnEvent(entity, events)
    -- Iterate over all the available events
    for i = 1, #events do
      local e = events[i]
      -- Handle contact events
      if e.type == "Contact" then
        -- Provides the position of the contact
        -- "e.data.pos", the resulting impulse "e.data.impulse",
        -- and the interacting entities "e.data.entity0"
        -- and "e.data.entity1"
      end
    end

Last but not least, a variation of the Tick callback function:

function TickAsync(entity, delta_t)
end

Called precisely once during each rendered frame but executed asynchronously till the next call to this function.

Use this function to optimize scripts that need to do some complex and costly calculations. Check out the heightmap sample in our GitHub repository, which uses this functionality.

Important

It’s only safe to do some basic calculations here and to modify the internal state of the current script. Accessing entities and components via the scripting API will most certainly lead to crashes or very hard to reproduce bugs. Use with absolute caution!

Loading API interfaces#

IOLITE provides a lot of different API interfaces for all the available subsystems. To ensure that scripts have a minimal footprint, you have to explicitly state which interfaces you want to use at the beginning of your script.

As an example, if you want to work with nodes and print some text to the log/console, you’ll have to load the Log and Node interface tables like this:

Node.load()
Log.load()

In this example, the calls to load() populate the functions provided by the interfaces Node and Log via the according global tables.

Please note that not loading the API interfaces will lead to errors stating that the requested function is unavailable.

Hot reloading and error logging#

Scripts are hot-reloaded on every change you make. Potential errors and your log calls end up in IOLITE’s console and log file. To toggle the console, press [F2].

If executing the script throws an error, go ahead and adjust the faulty line of code, save the file, and directly check back in IOLITE if the error is gone. It’s as easy as that.

Date structures and refs#

When interacting with IOLITE via the scripting interface, you’ll encounter three different types of data structures:

PODs (Plain Old Data)

Vectors provided by the math interface, etc.

Refs

Used to reference entities, components, and resources on engine-side

Handles

Like refs, but specific to certain subsystems, like, e.g., the particle or sound system

Refs, compared to handles, are agnostic of the underlying subsystems. A ref can reference any component, entity, or resource, providing interfaces for checking the underlying type and whether the referenced resource is still alive.

Let’s look at some examples of how refs can be utilized in detail. Here we’re searching for a specific entity in the scene and checking whether it’s available:

Entity.load()

-- Try to find the "goose" entity in the world
local goose = Entity.find_first_entity_with_name("goose")
if Ref.is_valid(goose) then
  -- Do something to the goose...
end

Now we’re dealing with a ref of unknown origin, and we want to make sure it is (A) a node and (B) still alive:

Node.load()

-- Check if a given ref is referencing a node component
-- and whether the component is still alive
if Node.get_type_id() == Ref.get_type_id(my_potential_node)
    and Node.is_alive(my_potential_node) then
  -- Retrieve the position when we're safe
  local pos = Node.get_world_position(my_potential_node)
  -- Do something with the position...
end

Error handling and scripts#

IOLITE strives for a good mixture of error handling and performance.

While a lot of user errors won’t make the engine crash, like, e.g., passing the wrong amount of parameters to a function, there are certain cases where this behavior is expected, mostly related to interacting with resources and refs:

  • Using the ref on an entity, component, or resource which is no longer alive. Make sure to only interact with alive resources using the is_alive function of the corresponding interface table

  • Using an invalid ref to execute functions. Ensure you’re always using valid refs using Ref.is_valid(ref_in_requestion)

Going further#

Our GitHub repository houses a couple of Lua-based samples which serve as an excellent reference and starting point. Otherwise, header over to the Lua API documentation to quickly locate the functionality you are looking for.