On this page
Table components
Table components hold values that do not fit a fixed C struct: strings, nested tables, opaque handles, and data that needs Lua reference semantics.
local record Health is tecs.ecs.Component
value: number
max: number
__call: function(
self, value?: number, max?: number
): Health
end
tecs.ecs.newComponent({
name = "Health",
container = Health,
fields = {"value", "max"},
defaults = {100, 100},
})
local full <const> = Health()
local hurt <const> = Health(40, 100)
local named <const> = Health.new({value = 40, max = 100})Each instance owns a Lua table. Its metatable resolves componentType, methods, and other container fields without copying them onto every instance. Callers own the instance fields and may mutate them through getMut. Tecs owns the metatable and component metadata; callers should treat those as read-only.
Use FFI components for fixed-size primitive fields that hot loops or native code read from contiguous memory.
Field construction
fields controls positional order and generates the table-form .new. defaults fills omitted values in the same order; nil leaves a field without a default.
Registration requires only name and container. A table component may act as a presence marker with no instance fields:
local record Renderable2D is tecs.ecs.Component
end
tecs.ecs.newComponent({
name = "Renderable2D",
container = Renderable2D,
})For field-by-field checking on .new, declare a config record and narrow the inherited signature:
local record Health is tecs.ecs.Component
value: number
max: number
record Config
value: number
max: number
end
__call: function(
self, value?: number, max?: number
): Health
new: function(config: Config): Health
endThe narrower declaration changes Teal checking only. The generated .new still maps the table through fields.
Validation and derived fields
Add init when direct field mapping needs validation or refinement:
local record Inventory is tecs.ecs.Component
slots: {string}
capacity: integer
__call: function(
self, slots: {string}, capacity?: integer
): Inventory
end
tecs.ecs.newComponent({
name = "Inventory",
container = Inventory,
fields = {"slots", "capacity"},
defaults = {nil, 10},
init = function(inventory: Inventory)
if inventory.slots == nil then
error("Inventory requires slots")
end
if #inventory.slots > inventory.capacity then
error("Inventory exceeds capacity")
end
end,
})
local inventory <const> = Inventory({"sword"})Tecs fills fields and defaults before init runs. Both the positional constructor and generated .new run the hook.
An init hook requires fields or an explicit new. Without one of those, Tecs cannot map the table form to positional arguments.
Semantic constructors
Use a custom __call when arguments describe an operation rather than a field list. Tecs allocates the instance and applies defaults before it invokes the hook. A custom __call replaces the generated path and does not invoke init; call shared initialization explicitly.
Pair it with a custom new when the table form needs its own mapping:
tecs.ecs.newComponent({
name = "ParticleEmitter",
container = ParticleEmitter,
requires = {tecs.Transform2D},
__call = function(emitter: ParticleEmitter, options: EmitterOptions)
initEmitter(emitter, options)
end,
new = function(data: {string: any}): ParticleEmitter
local emitter <const> = {} as ParticleEmitter
initEmitter(emitter, data as EmitterOptions)
return emitter
end,
})
local sparks <const> = ParticleEmitter({effect = "sparks"})Tecs applies the component metatable to the table returned by new.
Component construction covers the rules shared with FFI components and relationships.
Lifecycle reactions
Table components support in-place writes, so a value change does not pass through a setter. Use dirty tracking when a consumer needs to find changed columns.
Use query callbacks when code must react to a component entering or leaving a matching signature. Query callbacks operate on contiguous row ranges and can match several components at once.
Snapshots serialize declared fields by default. Use customserialize/deserialize functions for process-local values, or set transient = true for state that should not enter a snapshot. See Component serialization.