Components#
Components hold entity data. A system binds component columns from each matching archetype, reads through get, and takes writable columns through getMut:
(tecs.ecs.Component)
local struct Velocity
x: number = 0
y: number = 0
end
local Transform2D = tecs.ecs.Transform2D
local movers = world:newQuery({
include = {Transform2D, Velocity},
})
world:addSystem({
name = "game.Move",
phase = tecs.ecs.phases.Update,
run = function(dt: number)
for archetype, length in movers:iter() do
local transforms = assert(archetype:getMut(Transform2D))
local velocities = assert(archetype:get(Velocity))
unsafe do
for row = 1, length as integer do
transforms[row].x = transforms[row].x
+ velocities[row].x * dt
transforms[row].y = transforms[row].y
+ velocities[row].y * dt
end
end
end
end,
})getMut marks the Transform2D column dirty. Incremental consumers such as hierarchy composition use that mark to skip unchanged work.
Component values belong to callers. Callers may replace them with world:set or mutate their fields through getMut; Tecs owns the storage and dirty marks around those values.
Storage choices#
Derive tecs.ecs.Component on the value declaration. A struct selects native columns and a record selects managed columns. Pass new Velocity(...) to mutations and Velocity itself to queries, reads, removals and bulk defaults. The component name defaults to the module-qualified declaration name. Pin a persisted identity with @component(name = "game.Velocity"); moving or renaming an unpinned declaration changes its snapshot identity.
tecs.ecs.newComponent(Velocity, options) configures requirements, codecs or custom factories, or gives the same value layout a distinct component identity. It does not choose a different physical representation. Configure the default identity before using the declaration in a world.
Generic helpers accept Type<T> with T is tecs.ecs.ComponentValue. Helpers that inspect edges can use T is tecs.ecs.RelationshipPayload. These bounds keep the stored value type intact without importing an internal module.
| Kind | Use |
|---|---|
| Native struct | Fixed-layout numeric data in contiguous native columns |
| Record | Structured values, strings, nested records and handles |
| Scalar | One number, boolean or string per entity |
| Tag | Presence with no per-entity value |
The engine uses the same storage selection. Transform, tint, shape material, camera, lighting, animation, audio state, physics values, and numeric UI components use native structs with storage selected by their declarations. Renderable2D is a tag. Text, styles, Tiled metadata and other components with strings or managed collections remain records.
Native component columns are one-based T[?] arrays, indexed inside unsafe do using the query's row count. Reads return live row references; reacquire them after publication, compaction, clear or restore. Native assignment copies a value into the row rather than sharing a managed object.
Nupp owns struct methods and derives. Built-in native values derive nupp.derive.Debug and nupp.derive.Serde. A prepared Serde codec can encode a borrowed row directly. That generic encoding describes the struct's own fields; world snapshots still use Tecs's component codecs, including name-based asset resolution and transient-state handling.
Entity access#
world:get(entity, Component) returns the component or nil. A scalar component returns its raw value:
local transform = world:get(entity, tecs.ecs.Transform2D)
local name = world:get(entity, tecs.ecs.Name)Call world:getMut before an in-place write:
local transform = world:getMut(entity, tecs.ecs.Transform2D)
if transform then
transform.x = transform.x + 10
endDo not call getMut at a site that might only read. It declares mutation intent and defeats dirty-gated work even when no value changes.
A reference obtained through world:get does not mark its column dirty. If code writes through that reference, it must call world:markComponentDirty(entity, Component) explicitly.
A spawn reserves an ID but does not place the entity until the next pipeline barrier. world:get and world:getMut return nil for that staged entity. Pass its initial values to world:spawn instead.
Adding and removing components#
Pass instances to world:spawn and world:set:
local entity = world:spawn(
tecs.ecs.Name("Frank"),
tecs.ecs.Transform2D(100, 200)
)
world:set(entity, tecs.ecs.Name("Grace"))
world:remove(entity, tecs.ecs.Name)world:has(entity, Component) tests presence, including a relationship definition. Read a relationship value to inspect its target; see Relationships.
Adding or removing a component changes the entity's archetype, so those calls always stage until a pipeline barrier. Structural transactions covers the visibility rules.
Component dependencies#
requires declares components that must accompany another component. Tecs adds the full transitive closure in one archetype transition:
local Moving = tecs.ecs.newTagComponent({
name = "Moving",
requires = {tecs.ecs.Transform2D},
})
local entity = world:spawn(Moving)
world:commit()
assert(world:has(entity, tecs.ecs.Transform2D))A requirement names a component definition. Automatic addition uses that component's default constructor. RelativeTransform2D requires Transform2D, so a relative transform and the world transform it feeds enter together.
Transient state#
Set transient = true on components and relationships that hold runtime projections such as native handles or caches. Snapshots omit those columns but keep their entities. Rebuild the omitted values from durable components after load. Save games covers custom save and load callbacks.