On this page
Component construction
Structured components share one construction model:
Component(...)maps positional arguments to declared fields.Component.new({...})provides the named form.defaultsfill omitted positional values.initvalidates or derives values after field mapping.- A custom configuration
__callreplaces positional mapping.
Storage changes the allocated value, not these rules.
Positional fields
Table components list field names. FFI components list name and C type pairs:
local record Health is tecs.ecs.Component
current: integer
maximum: integer
__call: function(
self, current?: integer, maximum?: integer
): Health
end
tecs.ecs.newFFIComponent({
name = "Health",
container = Health,
fields = {
{"current", "int32_t"},
{"maximum", "int32_t"},
},
defaults = {100, 100},
})
local full <const> = Health()
local damaged <const> = Health(80, 120)Defaults line up with fields. A nil slot means no declared default, while false remains a valid default. FFI allocation supplies zero values for fields that positional arguments and defaults omit.
Relationship targets always occupy the first positional argument and do not appear in the public field list.
Named construction
The named form routes fields through the positional constructor:
local damaged <const> = Health.new({
current = 80,
maximum = 120,
})Tecs reads declared fields in order and calls Health(80, 120). The initializer receives positional values, not the input table.
Provide a custom new only when named construction cannot map to the positional form.
Validation and derived values
init(instance, ...) runs after allocation, field assignment, and defaults:
tecs.ecs.newFFIComponent({
name = "Health",
container = Health,
fields = {
{"current", "int32_t"},
{"maximum", "int32_t"},
},
defaults = {100, 100},
init = function(instance: Health)
if instance.current < 0 then
error("Health.current must not be negative")
end
if instance.maximum < instance.current then
error("Health.maximum must cover current health")
end
end,
})Use defaults for static values. Use init for validation, normalization, and derived state. An initializer requires declared fields or a custom new, so the named path stays defined.
Custom call shapes
Supply configuration __call(instance, ...) when public constructor arguments do not match stored fields:
tecs.ecs.newComponent({
name = "ParticleEmitter",
container = ParticleEmitter,
requires = {tecs.Transform2D},
__call = function(
instance: ParticleEmitter, options: EmitterOptions
)
initEmitter(instance, options)
end,
new = function(data: {string: any}): ParticleEmitter
local instance <const> = {} as ParticleEmitter
initEmitter(instance, data as EmitterOptions)
return instance
end,
})init afterwards. Call shared initialization explicitly when both paths need it.