
Live inspection
ECS built for LuaJIT
Batteries included
Static typing
Install
The tecs command carries the compiler, engine, type definitions, and project template. It needs no separate Lua, LuaRocks, Teal, or C compiler installation.
brew install tecs-dev/tap/tecsscoop bucket add tecs https://github.com/tecs-dev/scoop-bucket
scoop install tecsbrew install tecs-dev/tap/tecsCreate a game and run it:
tecs new my-game && cd my-game && tecs runThe Tecs CLI carries the project toolchain in one file. Contributors can build this repository through Cargo; getting started covers that workflow.
Entities are the interface
A drawn quad, a light, a sound, a physics body: each one is an entity carrying components, and the subsystem that cares for it finds it by query. A game holds no draw list, no voice handle and no body pointer, so what works on one of them works on all of them. A snapshot saves the scene, the profiler reports where the frame went, and the debug server inspects and edits any of it while the game runs.
The host owns the loop. An entry file returns an application, and the plugin it carries registers work instead of driving frames.
local Transform2D <const> = tecs.Transform2D
local gfx <const> = tecs.gfx
return tecs.newApplication({
window = {title = "Spin", width = 1280, height = 720},
ambientLight = {0.05, 0.05, 0.08},
plugin = function(world: tecs.World, app: tecs.Application)
-- Something on screen is an entity. Nothing issues a draw call for it.
world:spawn(
Transform2D.new({
x = 640, y = 360, scaleX = 64, scaleY = 64
}),
gfx.Tint(0.85, 0.4, 0.3, 1.0),
gfx.Renderable2D()
)
-- So is the light falling on it, placed by the same Transform2D.
world:spawn(
Transform2D.new({x = 520, y = 300}),
gfx.PointLight2D(120, 600, 1.0, 0.9, 0.7, 3.0)
)
local movers <const> = world:newQuery({
include = {Transform2D, gfx.Renderable2D},
})
world:addSystem({
name = "game.Spin",
phase = tecs.ecs.phases.Update,
run = function(dt: number)
for archetype, length in movers:iter() do
local transforms <const> = archetype:getMut(
Transform2D
)
for row = 1, length do
transforms[row].rotation = transforms[row].rotation + dt
end
end
end,
})
end,
})ECS examples
local world = tecs.ecs.newWorld()
-- Define typed components with Teal
local record Position is tecs.ecs.Component
x: number
y: number
__call: function(self, x?: number, y?: number): Position
end
tecs.ecs.newFFIComponent({
name = "Position",
container = Position,
fields = {{"x", "float"}, {"y", "float"}},
})
local record Velocity is tecs.ecs.Component
x: number
y: number
__call: function(self, x?: number, y?: number): Velocity
end
tecs.ecs.newFFIComponent({
name = "Velocity",
container = Velocity,
fields = {{"x", "float"}, {"y", "float"}},
})
-- Query and update entities
local query = world:newQuery({include = {Position, Velocity}})
world:addSystem({
phase = tecs.ecs.phases.Update,
run = function(dt: number, _world: tecs.World)
for archetype, len in query:iter() do
local positions = archetype:getMut(Position)
local velocities = archetype:get(Velocity)
for row = 1, len do
positions[row].x = positions[row].x + velocities[row].x * dt
end
end
end,
})
world:spawn(Position(100, 100), Velocity(10, 0))local world = tecs.ecs.newWorld()
local ChildOf = tecs.ecs.ChildOf
-- Create a parent entity
local parent: integer = world:spawn(
tecs.ecs.Name("parent"), tecs.Transform2D(100, 100)
)
-- ChildOf has cascadeDelete; despawning parent despawns children too.
local child1: integer = world:spawn(
ChildOf(parent), tecs.ecs.RelativeTransform2D(20, 0)
)
local child2: integer = world:spawn(
ChildOf(parent), tecs.ecs.RelativeTransform2D(-20, 0)
)
-- Walk children of a specific parent
world:targets(
parent,
ChildOf,
function(childId: integer) print("child:", childId) end
)
-- Despawning parent cascades to child1 and child2
world:despawn(parent)local world = tecs.ecs.newWorld()
-- Define a custom event
local record DamageEvent is tecs.events.Event
target: integer
amount: number
__call: function(
self, target: integer, amount: number
): DamageEvent
end
-- Wire up the event init hook (it mutates a pre-allocated instance)
DamageEvent.init = function(
e: DamageEvent, target: integer, amount: number
)
e.target = target
e.amount = amount
end
tecs.events.newEvent(DamageEvent)
-- Observe events anywhere in your game (0 = world-level)
world:observe(
0,
DamageEvent,
function(e: DamageEvent)
local health: Health = world:get(e.target, Health)
if health then
health.current = health.current - e.amount
if health.current <= 0 then
world:despawn(e.target)
end
end
end
)
-- Emit events from systems
world:emit(0, DamageEvent, enemyId, 25)Reference
The host loads tecs before the entry file. A game can use these names without a require:
tecs.assets- loading bytes, images and sounds off the main threadtecs.audio- voices, groups, keyed limits, fades, pitch, loop points, streaming, devicestecs.data- typed stores, JSON, byte encodings, DEFLATE, transcoding, UTF-8, UUIDs, hashes and checksumstecs.ecs- worlds, components, queries, systems, events and resourcestecs.events- typed events and address-based message busestecs.gfx- the camera, the components, the renderer, text, and the vocabularies belowtecs.input- gameplay input, gamepads and standalone sensorstecs.io- binary I/O, nonblocking sockets, HTTP, and external toolstecs.log- named, leveled platform loggingtecs.math- angle math and two-dimensional geometrytecs.physics- Rapier 2D, solved across a shared thread pooltecs.platform- platform events, operating-system services, time, and windowstecs.regex- compiled regular expressions over Lua byte stringstecs.runtime- process-wide polling for asynchronous worktecs.sequence- timelines with the tween runtime inside themtecs.ui- retained layout, scrolling, clipping, and interaction over existing drawing componentstecs.workers- typed background jobs
Inside one of those, one level and no deeper:
tecs.data.utf8- UTF-8 codepoint decoding, encoding, validation and truncationtecs.ecs.random- seeded named streams and standalone generatorstecs.gfx.animation- sprite sheets, and the playback that reads themtecs.gfx.layers- z-ordering and per-layer behaviortecs.gfx.materials- one fragment shader, compiled from the material settecs.gfx.particles- emitterstecs.io.files- where a game may read and write, and what to do with a pathtecs.io.http- fetching over HTTP without stopping the frametecs.io.mcp- the debug server agents and humans drive a running game throughtecs.io.Path- immutable UTF-8 paths with platform-native component rulestecs.io.Process- streaming child processes with backpressured standard I/Otecs.io.URI- immutable general-purpose URIs with component-aware modificationtecs.io.watcher- watching files for changetecs.math.noise- native procedural scalar fields and bulk gridstecs.math.vec2- allocation-free two-dimensional vector and point mathtecs.platform.events- typed platform events routed through the worldtecs.platform.os- capabilities, process signals, the clipboard, and desktop servicestecs.platform.time- clocks, calendar time, delays and frame timingtecs.platform.window- the window, its size, its display and its mode
On tecs itself, because no one module owns them:
tecs.Application- the object an entry file returns, and what the host drivestecs.Future- a value that settles oncetecs.newApplication- builds the application an entry file returnstecs.scoped- closes explicitly owned resources when one callback endstecs.Transform2D- where an entity is, and the one component every subsystem movestecs.Transform3D- a right-handed 3D position, orientation, and scaletecs.version- the version of this build, as a string
tecs.ecs
Worlds, components, queries, systems, events and resources. One table with two ways in: a game reads it off tecs like any other module, and an engine module writes require("tecs.ecs"), because tecs is the aggregator that pulls every engine module in and a module tecs exports cannot also depend on tecs. These are the concepts behind the names.
- Overview - the model, in one page
- Archetypes - cache-friendly storage for millions of entities
- Builtins - names, transforms, hierarchy, TTL, pause, disable, state events
- Bundles - reusable entity templates and batch spawning
- Components - table, tag, scalar and FFI data containers
- Dirty tracking - change-gated systems and GPU synchronization
- Events - type-safe pub/sub and entity lifecycle events
- Mutation model - the normative rules for reads, writes and dirty bits
- Phases - ordered phase scheduling
- Plugins - modular, shareable game mechanics
- Profiling - where a frame went
- Queries - reusable filters with archetype iteration, callbacks and grouping
- Relationships - links, hierarchies, relative transforms, cascade deletion
- Save games - snapshots, component codecs, migrations, resource handlers
- States - stack-based game states with transition events
- Systems - phase scheduling, dependencies and run conditions
- World - entities, resources and the state stack