On this page
Getting started
Tecs combines a typed entity component system with a game engine for Teal and LuaJIT. Game state, rendering, audio, physics, and tools share the same world.
Install the CLI
The Tecs CLI carries the engine, project toolchain, template, and offline reference:
tecs new my-game
cd my-game
tecs runThe generated tecs.lua marks the project root and names its entry file. Project commands work from any directory below it.
Build this repository
Contributors build the engine through Cargo:
git clone https://github.com/tecs-dev/tecs.git
cd tecs
cargo xtask deps
cargo xtask build
cargo xtask test
cargo xtask example ui-demo
cargo xtask example gltf3dcargo xtask deps installs system development dependencies. The repository pins Teal, the formatter, and tealdoc revisions for every checkout.
--preset selects a target. Development presets use system libraries. Package presets build pinned dependencies from source:
cargo xtask presets
cargo xtask test # run Rust, ABI, and Lua/Teal tests
cargo xtask package --preset macos-arm64
cargo xtask check-package out/packageEntry file
The host owns the loop. The entry file returns an application:
return tecs.newApplication({
window = {
title = "Hello",
width = 1280,
height = 960,
},
plugin = function(world: tecs.World, app: tecs.Application)
-- Register the game here.
end,
})The host loads tecs before the entry file, so game code uses it as a global. A headless script or spec loads the same table explicitly:
local tecs <const> = require("tecs")See tecs.Application for lifecycle and configuration.
First plugin
The entry plugin registers systems, observers, resources, and entities:
local Transform2D <const> = tecs.Transform2D
return tecs.newApplication({
plugin = function(world: tecs.World, app: tecs.Application)
local movers <const> = world:newQuery({
include = {Transform2D},
})
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,
})
world:spawn(Transform2D(100, 100))
end,
})Keep three rules visible when writing systems:
- Create persistent queries during plugin setup.
- Read columns with
getand mark written columns withgetMut. - Run
query:iter()to exhaustion. Usequery:newCursor()and close it when a loop may stop early.
The mutation model covers deferred changes and dirty tracking.
Optional transparent meshes
Opaque mesh rendering keeps its original three-pass cull chain. Enable the separate transparent resources only when a game needs glTF BLEND materials or registers an ALPHA_BLEND material itself:
return tecs.newApplication({
sprites = false,
meshes = {
transparency = true,
},
plugin = function(world: tecs.World, app: tecs.Application)
tecs.assets.loadGLTF(
tecs.io.files.assetPath("models/glass.gltf")
):onSettle(function(loaded: tecs.Future<tecs.assets.Model>)
if loaded.status ~= "ready" then
error(loaded.error, 0)
end
for _, primitive in ipairs(
app.renderer.meshes:registerModel(loaded.value)
) do
world:spawn(
primitive.transform,
primitive.mesh,
primitive.bounds,
primitive.material,
tecs.gfx.Tint(),
tecs.gfx.Renderable3D()
)
end
end)
end,
})The mesh domain frustum-culls and depth-sorts complete indexed commands on the GPU. It draws transparent meshes before the sprite forward lane, so sprites retain deterministic overlay ordering in a renderer that enables both domains.
Game modules
Split a game into plugins and install them from the entry plugin:
local enemies <const> = require("game.enemies")
local movement <const> = require("game.movement")
return tecs.newApplication({
plugin = function(world: tecs.World, app: tecs.Application)
world:addPlugin(movement.plugin)
world:addPlugin(enemies.plugin)
end,
})The host adds the project content root to package.path, so require("game.enemies") loads game/enemies.lua.