Components
A component is a plain data object attached to an entity. Components describe traits like position, velocity, or health, and are the building blocks of game state.
Component types
Tecs provides several component kinds for different use cases.
- Table component: backed by a Lua table. Use this when the data can't fit a fixed C struct: strings, nested tables, Love2D handles, or any value needing Lua reference semantics.
- Tag component: carries no data; presence is the whole signal.
- FFI component: backed by an FFI struct. Use this for numeric and primitive data that maps cleanly to fixed-size C fields.
- Scalar component: a single string, number, or boolean value. Use this when a component is really just one value (e.g.,
Health).
World methods
These methods are available on every World.
| Method | Description |
|---|---|
world:get | Return one component from an entity. |
world:getMut | Return a component for in-place mutation and mark its column dirty. |
world:getFirstRelationship | Return the first relationship instance for a relationship container. |
world:has | Check whether an entity has a component or relationship target. |
world:set | Attach or replace a component on an entity. |
world:remove | Remove a component from an entity. |
world:markComponentDirty | Mark a component column dirty for one entity's archetype. |
world:get
Retrieves a component from an entity.
function World:get<T is Component>(entity: integer, component: T): TParameters:
entity: Entity ID.component: Component type or relationship instance to retrieve.
Returns:
- The component instance, or
nilif not found.
world:getMut
Mutable counterpart to world:get. It returns the component and marks that component dirty on the entity's archetype. Use this whenever you intend to mutate the returned reference in place.
function World:getMut<T is Component>(entity: integer, component: T): TParameters:
entity: Entity ID.component: Component type or relationship instance to get and mark dirty.
Returns:
- The component instance, or
nilif not found.
See Dirty tracking for when dirty marks are needed.
world:getFirstRelationship
Returns the first relationship instance for a relationship container on an entity. For exclusive relationships, this is the single instance.
function World:getFirstRelationship<T is Relationship>(entity: integer, relationship: T): TParameters:
entity: Entity ID.relationship: Relationship container type.
Returns:
- The relationship instance, or
nilif not found.
world:has
Checks whether an entity currently has a component.
function World:has(entity: integer, component: Component): booleanFor sparse relationships, passing the relationship container checks whether the entity has any target for that relationship; passing a relationship instance checks for that specific target.
world:has(entity, Health)
world:has(entity, ChildOf)
world:has(entity, ChildOf(specificParent))world:set
Attaches or replaces a component on an entity.
function World:set(entity: integer, component: Component, value?: any)Parameters:
entity: Entity ID.component: Component instance to attach, or a component type when using the optional scalar value form.value: Optional raw value for scalar component writes.
This is a deferred operation inside query iteration, callbacks, explicit defer scopes, and batch callbacks.
world:remove
Removes a component from an entity.
function World:remove(entity: integer, component: Component)Parameters:
entity: Entity ID.component: Component type or relationship instance to remove.
This is a deferred operation inside query iteration, callbacks, explicit defer scopes, and batch callbacks.
world:markComponentDirty
Marks a component dirty on the entity's archetype. Prefer world:getMut(entity, component) when you are fetching and then mutating the component; use this when you already have a reference from another path.
function World:markComponentDirty(entity: integer, component: Component)Parameters:
entity: Entity ID.component: Component type whose column was mutated.
See Dirty tracking for the full dirty-bit model.
Getting components
Access an entity's components with world:get.
local name = world:get(entityId, tecs.builtins.Name)Component access is typed
Tecs is built from the ground-up to be strongly typed with Teal; the get method is generic over the provided component type. So in the above example, the return value of get is an instance of tecs.builtins.Name or nil if not found.
Setting components
Set components on entities with world:set.
world:set(entityId, tecs.builtins.Name("Frank"))You can also set components when spawning an entity.
world:spawn(
tecs.builtins.Name("Frank"),
tecs.builtins.Position(100, 200)
)Removing components
Remove components from entities with world:remove.
world:remove(entityId, tecs.builtins.Name)Getting components from archetypes
When iterating entities in a system, the query gives you the archetype directly. You can bind the component's column once and then index by row, avoiding per-entity lookups:
local query: tecs.Query = world:query({include = {Position, Velocity}})
world:addSystem({
name = "Movement",
phase = tecs.phases.Update,
run = function(dt: number, _world: tecs.World)
for archetype, length in query:iter() do
local positions = archetype:getMut(Position) -- bind column, mark dirty
local velocities = archetype:get(Velocity) -- read-only column
for row = 1, length do
positions[row].x = positions[row].x + velocities[row].x * dt
positions[row].y = positions[row].y + velocities[row].y * dt
end
end
end
})This is significantly faster than calling world:get per entity because the archetype and column are already known: each access is just an array index.
Auto-dependencies with requires
Declare components that must accompany another component using requires. When a component with requires is added to an entity (via world:set, world:spawn, or any other path), every listed component that is not already present is added in the same archetype transition, so the entity skips intermediate archetypes.
Entries may be either component types (the container is called with no args to produce a default instance) or instance values (shared by every entity that auto-adds the dependency). The closure is transitive: if a required component itself declares requires, those are pulled in too.
local record Position is tecs.Component
x: number
y: number
metamethod __call: function(self, x?: number, y?: number): Position
end
local record Velocity is tecs.Component
vx: number
vy: number
metamethod __call: function(self, vx?: number, vy?: number): Velocity
end
tecs.newComponent({
name = "Position",
container = Position,
fields = {"x", "y"},
defaults = {0, 0},
})
tecs.newComponent({
name = "Velocity",
container = Velocity,
fields = {"vx", "vy"},
defaults = {0, 0},
requires = {Position}, -- Velocity implies Position
})
-- Spawning Velocity auto-adds a default Position in the same transition.
local entity: integer = world:spawn(Velocity(10, 20))
assert(world:get(entity, Position) ~= nil)For lifecycle reactions ("run code when an entity gains or loses this component"), use query callbacks: onEntitiesAdded and onEntitiesRemoved on world:query(...).
Transient components
Set transient = true on any component or relationship whose value is runtime projection state rather than durable world state (e.g., renderer caches, GPU/bucket routing tags, per-frame scratch). Snapshot saves skips transient component columns while keeping the entity itself.
tecs.newFFIComponent({
name = "SpriteData",
container = SpriteData,
transient = true,
fields = {
{"width", "float"},
{"height", "float"},
}
})transient = true is mutually exclusive with serialize. The option is accepted by newComponent, newFFIComponent, newTagComponent, newScalarComponent, newRelationship, and newFFIRelationship.
See Serialization for how components round-trip through save games, networking, and the MCP server.