On this page
  1. Getting started
  2. Install the CLI
  3. Build this repository
  4. Entry file
  5. First plugin
  6. Optional transparent meshes
  7. Game modules
  8. Next pages

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 run

The 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 gltf3d

cargo 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/package

Entry 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 get and mark written columns with getMut.
  • Run query:iter() to exhaustion. Use query: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.

Next pages

  • tecs.ecs introduces worlds, components, queries, and systems.
  • Modules lists every public engine module.
  • Tecs CLI covers project commands and the offline reference.