Skip to content

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

teal
local tecs = require("tecs")

local Health = tecs.newScalarComponent({
    name = "Health",
    kind = "number",
    default = 100,
})

Options

OptionTypeRequiredDescription
namestringyesComponent name.
kind"number" | "boolean" | "string"yesLua type of the value the column holds.
defaultTnoValue used when the component is added without a value. If omitted, defaults to the zero value for the kind: 0, false, or "".
requires{Component}noDeclarative 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.
transientbooleannoIf 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:

teal
local tecs <const> = require("tecs")

local record mymodule
    Health: tecs.ScalarComponent<number>
end

mymodule.Health = tecs.newScalarComponent({
    name = "Health",
    kind = "number",
    default = 100,
})

return mymodule

Set and get scalar values

The 3-arg world:set(entity, componentType, value) form is the fast path for scalar writes:

teal
local id = world:spawn()

world:set(id, Health, 75)
local hp = world:get(id, Health) -- 75

You can also set a scalar without passing a value to write its default:

teal
world:set(id, Health)
local hp = world:get(id, Health) -- 100

world:get always returns the raw scalar value, never a wrapper:

teal
local hp = world:get(id, Health)
print(type(hp))       -- "number"
print(hp == 75)       -- true

Spawn 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.

teal
local id = world:spawn(
    Transform(0, 0),
    Health(75),
    Mana(10)
)

The 2-arg world:set(entity, instance) form works the same way:

teal
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.

teal
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
end

Serialization

Scalar components serialize as an object with a single value field:

json
{ "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.