🌝 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)
- The Lua 5.1 Reference Manual
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
- LuaJIT Overview
- LuaJIT Benchmarks
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 tableUsing 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.