World#

A world owns the complete ECS runtime: entities, archetypes, queries, systems, resources, bundles, event observers, snapshot handlers, and state.

local world = tecs.ecs.newWorld({
    timestep = 1 / 60,
})

The default world supports about one million concurrent entity slots. A configuration may raise maxEntities to the packed-ID limit of 2^22 - 1. The maximum is 4,194,303 live entities; slot zero is reserved.

Lifecycle#

Application creates and drives its world. After the entry plugin finishes, it calls startup() once, update(dt) every host iteration, and shutdown() at teardown.

Tests, tools, and benchmarks may drive those calls directly:

world:startup()
world:update(1 / 60)
world:shutdown()

Before phase dispatch, update publishes pending structural work. The pipeline then publishes after each non-empty phase, and update clears component dirty bits only after the pipeline returns. The host then extracts and encodes the completed world into a render packet.

getFixedTiming() returns the timestep, residual accumulator, and clamped interpolation alpha without allocating. fixedStepCount() returns the number of fixed steps since world construction. The scheduler advances those values even when callers disable fixed phases.

Entity IDs#

Entity IDs pack a slot and generation into an opaque number:

local old = world:spawn()
-- `old` becomes live at the next pipeline barrier.

Slot reuse changes the generation, so stale handles fail lookups. Do not inspect IDs with LuaJIT bit operations; packed values may exceed 32 bits.

Use EntityKey for the few authored entities that runtime code must rediscover:

world:spawn(
    tecs.ecs.EntityKey("player"),
    tecs.ecs.Name("Player ship")
)

world:commit()
local player = world:requireKey("player")

Callers choose keys. Tecs owns the unique index, releases entries on removal or despawn, and rebuilds it after snapshot load.

Spawning entities#

spawn accepts initial components and returns an ID immediately:

local player = world:spawn(
    tecs.ecs.Transform2D(100, 100),
    tecs.gfx.Tint(1, 1, 1, 1),
    tecs.gfx.Renderable2D,
    tecs.ecs.Name("Player")
)

The ID is reserved immediately, but the entity occupies no archetype until the next pipeline barrier. Later set, remove, or despawn calls in the same transaction may use that ID and edit its final staged result.

spawnAt places caller-chosen packed IDs. Snapshot loading uses them to preserve relationship targets and generations. The caller must ensure that each chosen slot has no live entity.

Entity clearing and storage maintenance#

clearEntities() removes entity data, pending transactions, keys, and entity-address observers. It preserves registered systems, queries, global observers, bundles, and component definitions.

Use a new world when systems and queries must also disappear. getStats() returns entity, archetype, component, and system counts, together with fixed-step overload statistics.

Structural transactions#

Structural mutations always stage. The scheduler publishes at lifecycle and phase boundaries, so systems in one phase normally share a transaction and see the same committed archetype membership.

A system that needs an extra boundary declares it in its configuration:

world:addSystem({
    name = "game.ResolveDamage",
    phase = tecs.ecs.phases.Update,
    commitBefore = true,
    run = resolveDamage,
})

commitBefore publishes work from earlier systems in the same phase before this system runs. commitAfter publishes this system's work before the next system. Prefer normal phase ordering when it expresses the dependency.

world:commit() publishes synchronously. Finish query traversal before calling it, because publication can change archetype storage. In systems, prefer phase boundaries or commitBefore / commitAfter to make dependencies explicit.

world:set stages replacements as well as additions. Writes through getMut change the committed value immediately and mark its column dirty. Query iteration owns no transaction scope, so an early break is safe. The queries guide covers traversal.

Plugins and resources#

A plugin configures one world. Games, engine features, and reusable mechanics use ordinary functions for composition:

local RATE: nupp.store.Key<number> = nupp.store.newKey("game.spinRate")

local function spinPlugin(exclusive world: tecs.ecs.World): nil
    world.resources:set(RATE, 1.5)
    -- Build queries and register systems here.
end

spinPlugin(world)

Callers own resource values and may replace them. Key identity and typed stores come directly from nupp.store. Snapshots omit world.resources; register a snapshot handler for durable resource state.

World subsystems#

The world exposes the shared entry points for:

Those pages own their interaction rules; generated Nupp reference owns individual method signatures and records.