Typed Inspector fields, defaults, overrides, and schemas.
Script properties
Properties are the fields a script exposes to designers: jump height, walk speed, a projectile count. You declare them next to the fields in Lua, the editor Inspector picks them up, and scene or prefab documents override them per entity. Defaults install on the instance before your hooks run.
Why properties exist
Hardcoding numbers inside Lua means every balance tweak needs a programmer. Properties move those values out to the entity, where a designer can change 8 to 16 and press play. The script still reads them as ordinary fields on self.
Declaring with annotations
Comment lines starting with ---@demi_component and ---@demi_property describe the component and each property. They sit directly above the thing they describe: the component annotation above the module table, each property annotation above its assignment. They are read by the engine and editor, not executed.
---@demi_component
---@display_name Hover Rune
---@category Gameplay
---@description A hovering collectible rune with a soft bob.
local SkyforgeHoverRune = {}
---@demi_property number
---@range 0 1
SkyforgeHoverRune.amplitude = 0.15
---@demi_property string
SkyforgeHoverRune.label = "Rune"
---@demi_property boolean
SkyforgeHoverRune.glow = true
---@demi_property
---@options soft, bold, ancient
SkyforgeHoverRune.style = "soft"
function SkyforgeHoverRune:on_start()
-- self.amplitude, self.label, self.glow, self.style are ready here
end
return SkyforgeHoverRune
How to read this:
---@demi_componentopts the file into the editor’s Add Component selector.---@display_name,---@category, and---@descriptionare optional; the display name defaults to the humanized filename and the category toScripting.---@demi_propertyexposes the assignment directly below it. The type after the tag is optional —boolean,number,string, and numeric tables are inferred from the assigned value. Write the type explicitly (integer,asset,entity,enum,array,object,vec2,vec3,color) when inference would be wrong or ambiguous, for exampleintegerto reject fractional overrides orassetfor anasset://string.- Extra lines tune the Inspector:
---@label Friendly Name,---@description Tooltip text,---@range minimum maximum,---@options a, b, cfor enums. A LuaLS---@typeline may follow---@demi_property. - Defaults install before the first hook, so read them; do not assign them again inside
on_createoron_start— that would replace the authored value.
A real-world example, from the performance lab’s tower script:
---@demi_component
local Tower = {}
---@demi_property integer
---@range 2 8
Tower.columns = 8
---@demi_property integer
---@range 2 64
Tower.levels = 16
---@demi_property
---@range 1 200
Tower.projectile_mass = 60
columns and levels are typed integer with slider ranges, so the Inspector renders constrained number boxes and rejects fractional overrides.
An entity supports a single Lua script, so the editor disables further Lua choices with an explanation. The component added to the scene is the standard LuaScript; the annotations only describe its module.
The editor also omits native components already attached to the selected entity from Add Component. Adding a script or native component does not duplicate one already present.
Property types
| Type | Holds | Default shape |
|---|---|---|
boolean | True/false flag | true or false |
number / integer | Float or whole number | A Lua number; integers reject fractional overrides |
string | Text | Quoted string |
asset | Reference to an asset | URI string such as "asset://models/barrel" |
entity | Reference to a scene entity | Entity ID string |
enum | One of a fixed set | One of the options values |
array | List of values | Lua array table |
object | Named fields | Lua table |
vec2 / vec3 | Vector | {x, y} or {x, y, z} numeric array (index 1, 2, 3) |
color | RGBA color | 4-number array {r, g, b, a} 0…1 |
Vectors and colors are embedded numeric arrays, exactly as in API conventions: index 1 is the first component. An enum default must be one of its options; anything else is invalid.
Assigning objects in the Inspector
Declare an entity property to reference another object. Select the entity that owns the script, then drag the target object from the Hierarchy onto the property in the Inspector. You can also choose it from the property's picker, or choose None to clear it. Assignment uses the normal Undo/Redo and Save workflow.
local Entity = require("demi.entity")
local Transform3D = require("demi.transform3d")
---@demi_component
local Follower = {}
---@demi_property entity
Follower.target = ""
function Follower:on_update(dt)
if self.target == "" or not Entity.exists(self.target) then
return
end
local x, y, z = Transform3D.get_world_position(self.target)
-- Use the target's position for your gameplay behavior.
end
return Follower
The property holds the object's stable entity ID, not a copy of its components or a native pointer. Pass it to engine services such as Entity.exists and Transform3D.get_world_position. An empty string means unassigned. Check availability before using a reference: the target may have been destroyed or its scene unloaded. The picker uses objects in the active scene or prefab document, including resolved prefab members; editing an instance records an override for that instance.
The property_schema table
Annotations are the friendly form. The same declarations can be a returned table named property_schema when you need to build the description in Lua, or when a module should validate properties without opting into the component selector:
local Player = {}
Player.property_schema = {
speed = {
type = "number",
default = 6.0,
minimum = 0.0,
maximum = 20.0,
},
team = {
type = "enum",
values = { "red", "blue" },
default = "red",
},
model = { type = "asset", required = true },
}
return Player
type— one of the property types above (string).default— value installed when the scene does not override it.minimum/maximum— numeric bounds fornumberandinteger.required— boolean; when true and no default is declared, an authored value must be supplied.values— allowed values for anenum, as an array of strings.
Both forms describe the same contract: unknown properties, wrong types, invalid enum values, and out-of-range numbers reject script activation.
Setting properties from a scene
The owning scene or prefab supplies only overrides:
{
"id": "skyforge_rune",
"components": {
"LuaScript": {
"module": "script://scripts/hover_rune.lua",
"properties": {
"amplitude": 0.3,
"label": "Sky Rune",
"style": "ancient"
}
}
}
}
module— thescript://URI of the script file.properties— overrides merged onto the declared defaults. Omitted keys keep their defaults; there is no need to repeat every field.
Overrides are validated against the declaration at load time. A violation rejects activation — the scene load fails with an error naming the property, instead of starting with a half-configured script. During development, a hot reload that fails validation keeps the previous instance running so a typo does not erase live state.
Next steps
- Script lifecycle — when each hook sees the installed properties.
- Data schemas — the same idea for data documents.
- Entity API — reading and writing fields at runtime.