DEMIENGINEDOCUMENTATION Everything is free
Lua scripting · Create

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_component opts the file into the editor’s Add Component selector. ---@display_name, ---@category, and ---@description are optional; the display name defaults to the humanized filename and the category to Scripting.
  • ---@demi_property exposes 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 example integer to reject fractional overrides or asset for an asset:// string.
  • Extra lines tune the Inspector: ---@label Friendly Name, ---@description Tooltip text, ---@range minimum maximum, ---@options a, b, c for enums. A LuaLS ---@type line may follow ---@demi_property.
  • Defaults install before the first hook, so read them; do not assign them again inside on_create or on_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.

One script per entity

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

TypeHoldsDefault shape
booleanTrue/false flagtrue or false
number / integerFloat or whole numberA Lua number; integers reject fractional overrides
stringTextQuoted string
assetReference to an assetURI string such as "asset://models/barrel"
entityReference to a scene entityEntity ID string
enumOne of a fixed setOne of the options values
arrayList of valuesLua array table
objectNamed fieldsLua table
vec2 / vec3Vector{x, y} or {x, y, z} numeric array (index 1, 2, 3)
colorRGBA color4-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 for number and integer.
  • required — boolean; when true and no default is declared, an authored value must be supplied.
  • values — allowed values for an enum, 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 — the script:// 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