Scalar Components
Scalar components store a single primitive value per entity (number, boolean, or string) in a direct column. Use them when you want tight SoA iteration for simple values without per-row table allocation.
If you need the shared constructor model first, see Component Construction. For the bigger picture of when scalar components fit, start from the Components overview and compare them with table components, FFI components, and tag components.
Creating a scalar component
local tecs = require("tecs")
local Health = tecs.newScalarComponent({
name = "Health",
kind = "number",
default = 100,
})Options
| Option | Type | Required | Description |
|---|---|---|---|
name | string | yes | Component name. |
kind | "number" | "boolean" | "string" | yes | Lua type of the value the column holds. |
default | T | no | Value used when the component is added without a value. If omitted, defaults to the zero value for the kind: 0, false, or "". |
requires | {Component} | no | Declarative auto-dependencies. When this component is added to an entity, any listed components not already present are added in the same archetype transition. Entries may be component TYPES (constructed with no args) or INSTANCES. Transitive. |
transient | boolean | no | If true, omit this scalar component from snapshots. |
Typed exports
To export a scalar from a module with a precise Teal type, declare the field type on the module record and assign the registration result to it:
local tecs <const> = require("tecs")
local record mymodule
Health: tecs.ScalarComponent<number>
end
mymodule.Health = tecs.newScalarComponent({
name = "Health",
kind = "number",
default = 100,
})
return mymoduleSet and get scalar values
The 3-arg world:set(entity, componentType, value) form is the fast path for scalar writes:
local id = world:spawn()
world:set(id, Health, 75)
local hp = world:get(id, Health) -- 75You can also set a scalar without passing a value to write its default:
world:set(id, Health)
local hp = world:get(id, Health) -- 100world:get always returns the raw scalar value, never a wrapper:
local hp = world:get(id, Health)
print(type(hp)) -- "number"
print(hp == 75) -- trueSpawn with scalar components
Health(75) produces a scalar instance you can pass directly to world:spawn alongside other components. The instance is unwrapped at the spawn boundary, so the column still stores the raw value.
local id = world:spawn(
Transform(0, 0),
Health(75),
Mana(10)
)The 2-arg world:set(entity, instance) form works the same way:
world:set(id, Health(50))Scalar instances are wrappers, not raw values
Health(75) returns a small wrapper carrying both the component type and the value, so the spawn dispatcher can route it. That means Health(75) == 75 is false. Treat scalar instances as opaque tokens for world:spawn / world:set; for everything else, read the raw value back via world:get.
For string-kind scalars, repeated calls with the same value reuse a cached wrapper, so Name("Frank") does not allocate after the first call with "Frank".
Query scalar columns
Scalar columns are regular archetype columns, so query iteration is the same pattern as other components.
local query = world:query({ include = {Health} })
for archetype, len in query:iter() do
local health = archetype:getMut(Health)
for row = 1, len do
health[row] = health[row] - 1
end
endSerialization
Scalar components serialize as an object with a single value field:
{ "value": 75 }On deserialize, missing value falls back to the scalar default.
When to use scalar vs table/FFI
- scalar components: use when the component is exactly one primitive value and you want simple, fast SoA column access.
- table components: use when you need structured Lua fields and flexible shape. See Table Components.
- FFI components: use when you need packed structs and FFI interop. See FFI Components.
- tag components: use when presence alone is the signal and there is no payload at all. See Tag Components.
Don't overdo scalar components
Scalar components are usually faster than table or FFI components for their narrow use case. But don't over-apply them and lose abstraction. For example, in most code, a single Position component with x and y is preferable to splitting into separate PositionX and PositionY components.