Skip to content

Component Construction

All component kinds in Tecs share the same high-level construction model:

  • Positional __call(...) is the hot path.
  • .new(data) is the named/table form.
  • fields define positional order.
  • defaults fill omitted positional values in field order.
  • init(instance, ...) runs after the base instance has been allocated and populated.
  • optional config __call(instance, ...) replaces the default positional field-mapping path.

The storage backend changes how the instance is stored; it does not change the basic construction rules.

This page documents the shared component model. For backend and category-specific details, see Table Components, FFI Components, Scalar Components, Tag Components, Relationships, and FFI Relationships.

Positional construction

Call the component container directly to construct an instance:

teal
local record Health is tecs.Component
    current: integer
    maximum: integer
    metamethod __call: function(self, current?: integer, maximum?: integer): Health
end

tecs.newComponent({
    name = "Health",
    container = Health,
    fields = {"current", "maximum"},
    defaults = {100, 100},
})

local a: Health = Health()
local b: Health = Health(80, 120)

When fields are present, positional arguments map to those fields in order. For the concrete table-backed and FFI-backed forms of this pattern, see Table Components and FFI Components.

Table construction

Every component also exposes .new(data):

teal
local h: Health = Health.new({
    current = 80,
    maximum = 120,
})

When fields are present, Tecs generates .new(data) automatically by unpacking field names in order and routing through __call.

Use an explicit new = function(data) ... end only when the table form should not map directly to the positional form.

This named construction path matters most for table components, FFI components, and default component serialization.

fields

fields define the positional argument order and the generated base shape.

Table components use:

teal
fields = {"x", "y", "z"}

FFI components use:

teal
fields = {
    {"x", "float"},
    {"y", "float"},
    {"z", "float"},
}

Relationships follow the same rule, except the relationship target is always the first positional argument and is not included in the public fields list. For relationship-specific construction and target semantics, see Relationships and FFI Relationships.

defaults

defaults are positional and line up with fields.

teal
defaults = {0, 0, 1}

That means:

  • field 1 defaults to 0
  • field 2 defaults to 0
  • field 3 defaults to 1

Use nil for "no default".

teal
defaults = {nil, nil, 1}

For FFI-backed components and relationships, omitted fields that still have no default remain zero-initialized by the allocator.

For examples of defaults on plain Lua payloads versus FFI-backed payloads, see Table Components, FFI Components, and Relationships plus FFI Relationships.

init(instance, ...)

init is a post-allocation hook.

The split is:

  • generated/base construction owns allocation, field mapping, and defaults
  • init owns validation, normalization, and derived state

Example:

teal
tecs.newFFIComponent({
    name = "Transform",
    container = Transform,
    fields = {
        {"x", "float"},
        {"y", "float"},
        {"layer", "int32_t"},
    },
    defaults = {0, 0, 1},
    init = function(instance: Transform)
        if instance.layer < 1 then
            error("Transform layer must be greater than 0")
        end
    end,
})

By the time init runs, instance.layer already reflects the positional args plus any defaults.

If you provide init, you must also provide fields or new. Otherwise Tecs would have no clear way to implement .new(data).

init is most relevant for structured payload components and relationships. For concrete usage patterns, compare Table Components, FFI Components, Relationships, and FFI Relationships. Scalar and tag components have narrower creation APIs; see Scalar Components and Tag Components.

Custom constructor __call(instance, ...)

Supply config __call when the public constructor arguments are not the same as the stored fields.

On this path, Tecs:

  • allocates the base instance
  • applies declarative defaults
  • calls your custom __call(instance, ...)
  • does not auto-run init

That last point is intentional: if you want to share logic, call Component.init(instance, ...) explicitly from the custom __call.

teal
tecs.newFFIComponent({
    name = "Text",
    container = Text,
    fields = {
        {"slabOffset", "uint32_t"},
        {"charCount", "uint16_t"},
        {"fontId", "uint16_t"},
    },
    __call = function(instance: Text, fontPath: string, value: string)
        Text.init(instance, fontPath, value)
    end,
    init = function(instance: Text, fontPath: string, value: string)
        -- custom semantic construction here
    end,
})

Use this only when the default positional field mapping is the wrong model. If your constructor arguments already line up with fields, prefer the normal generated path and init.