Meta programming¶
Trigger functionalities can be extended with Meta programming. It is possible to customize the following:
Meta-model file format:
return {
dataTypes = ..., -- table (or Lua function that returns a table) consisting of action types
events = ..., -- table (or Lua function that returns a table) consisting of action types
actions = ..., -- table (or Lua function that returns a table) consisting of action types
functions = ..., -- table (or Lua function that returns a table) consisting of function types
}
Events¶
Events invoke triggers, and are caused by various Spring callins.
They have the following fields:
- humanName (mandatory). Human readable name for display in the UI.
- name (mandatory). Unique identifier that is used to produce readable models.
- param (optional). Additional, event data sources that are available to the entire trigger.
- tags (optional). List of human-readable tags used for grouping in the UI.
Example of event programming:
{
humanName = "Unit enters area",
name = "UNIT_ENTER_AREA",
param = { "unit", "area" },
}
Actions¶
{
humanName = "Hello world",
name = "MY_HELLO_WORLD",
execute = function()
Spring.Echo("Hello world")
end,
}
The above code block defines a simple action. The name and humanName properties of the action define the machine (unique) and display name respectively. The execute property defines the function to be executed when the trigger is successfully fired. When used in the editor, this action would print a Hello World on the screen.
It is common for actions to receive input that defines its behavior. One such example would be:
{
humanName = "Print unit position",
name = "PRINT_UNIT_POSITION",
input = "unit",
execute = function(input)
local x, y, z = Spring.GetUnitPosition(input.unit)
Spring.Echo("Unit position: ", x, y, z)
end,
}
As one might guess, this action would take the specified unit as input and print out its position. The GUI editor will parse the input type and the user (level designer) will be able to specify the unit when creating an instance of this action. This is equivalent to the following Lua code:
function PRINT_UNIT_POSITION(unitID)
local x, y, z = Spring.GetUnitPosition(unitID)
Spring.Echo("Unit position: ", x, y, z)
end
Functions¶
The real power of the meta programming comes with the introduction of function types. Function types produce an output (result of the function), which often depends on the input.
Note
There’s a difference between a Lua function and a function type in the meta model. The function type represents a component in the meta model and is defined with a table.
Note
Function types should not have a side effect (they shouldn’t cause any changes to the game state), but they don’t have to be pure (they don’t need to produce the same output for the same input).
Example of a function type:
{
humanName = "Unit Health",
name = "UNIT_HEALTH",
input = "unit",
output = "number"
execute = function(input)
return Spring.GetUnitHealth(input.unit)
end,
}
This function type takes a unit as input and produce a number as output. A special class of these function types are those that return bool as output, and they represent conditions in the GUI programming.
Data types¶
Custom data types can be created as composites of builtin data types. This allows game developers to expose game-specific concepts. These data types are defined by specifying three fields: humanName (display name), name (machine name) and input (table of fields that it consists of). Example of a Person data type:
{
humanName = "Person",
name = "person",
input = {
{
name = "first_name",
humanName = "First name",
type = "string",
},
{
name = "last_name",
humanName = "Last name",
type = "string",
}
}
}
This custom data type can then be used in meta-programming as usual. Below we present a sample action that would print out person’s details.
{
humanName = "Print person",
name = "PRINT_PERSON",
input = "person" ,
execute = function(input)
local person = input.person
Spring.Echo("Hello! I am " .. person.first_name .. " " .. person.last_name)
end
}
Higher-order functions (Advanced)¶
As one of the more advanced uses meta-programming also has support for higher-order functions, i.e. fuctions that take other functions as parameters. An example of a filter higher-order function implemented in Lua is given below. This function will filter out table elements that don’t satisfy a given function. In this case, it will filter out elements that are lower or equal to five. As functions as first-class citizens in Lua, writing them is relatively simple.
function above5(x)
return x > 5
end
function filter(elements, f)
local retVal = {}
for _, el in pairs(elements) do
if f(el) then
table.insert(retVal, el)
end
end
return retVal
end
elements = {1, 12, 3, -5, 7}
filter(elements, above5)
In SpringBoard’s meta-programming however, higher-order functions need to have explicit types, as the meta-programming language is statically (and explicitly) typed. The same filter function type is given below, now in SpringBoard’s meta-programming language. The extraSources parameter defines additional scoped inputs. The function signature is defined by the output parameter. Normally the input parameter could also be specified, but that wasn’t done in this case, as the predicate function isn’t required to use the number parameter.
{
humanName = "Filter elements in number array",
name = "number_array_FILTER",
input = {
"number_array",
{
name = "filter_function",
type = "function",
extraSources = {
"number",
},
output = "bool",
},
},
output = "number_array",
tags = {"Array"},
execute = function(input)
local retVal = {}
for _, element in pairs(input.number_array) do
if input.filter_function({number = element}) then
table.insert(retVal, element)
end
end
return retVal
end,
}
Additionally, it is possible to use actions as parameters to higher-order actions types, in the same way like it is done for functions. Below we present a foreach action type that will iterate through all elements of an array and execute the specified action for them.
{
humanName = "For each number in number array",
name = "number_array_FOR_EACH",
input = {
"number_array",
{
name = "for_each_action",
type = "action",
extraSources = {
"number",
},
},
},
tags = {"Array"},
execute = function(input)
for _, element in pairs(input.number_array) do
input.for_each_action({number = element})
end
end,
}
Example¶
An example of practical meta-programming usage can be seen in the case of Gravitas.
In particular we will focus on two parts of it: the GATE_OPENED event type and the LINK_PLATE_GATE action type.
The event type is straightfoward, and signals a gate being opened. The unit parameter represents the gate being opened.
{
humanName = "Gate opened",
name = "GATE_OPENED",
param = "unit",
}
The LINK_PLATE_GATE action type takes two unit parameters, one representing a plate, and other representing a gate. It then uses game API to link the two together, causing the gate to open if the pressure plate is activated.
{
humanName = "Link Plate To Gate",
name = "LINK_PLATE_GATE",
input = {
{
name = "plate",
type = "unit",
},
{
name = "gate",
type = "unit",
},
},
execute = function(input)
GG.Plate.SimpleLink(input.plate, input.gate)
end
}
Example scenario implemented using this custom meta-programming is available at Gravitas Example. To use it, extract it and open it as a SpringBoard project.