Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Ukendio/jecs/llms.txt
Use this file to discover all available pages before exploring further.
Quickstart Guide
This guide will walk you through the basics of jecs: creating a world, defining components, creating entities, and querying them.
Creating a World
The world is the foundation of your ECS. It stores entities and their components, and manages all ECS operations.
local jecs = require("@jecs")
local world = jecs.world()
A world is required for every ECS operation. You’ll reference it throughout your application.
Defining Components
Components are typed data containers that you attach to entities. Think of them as columns in a database table.
Create component IDs
Use world:component() to register component types:local Position = world:component() :: jecs.Id<vector>
local Health = world:component() :: jecs.Id<number>
The type annotation :: jecs.Id<Type> provides type safety, ensuring you can only assign values of the correct type to entities. Understand component IDs
Components are keys to ECS storage columns. When you create a component, you’re creating a key that indexes into the storage:
- Rows are entities
- Columns are components
- Cells are the component data for that entity
print(`Position component ID: {Position}`)
print(`Health component ID: {Health}`)
Creating Entities
Entities represent things in your game - characters, buildings, projectiles, etc. By themselves, entities are just unique identifiers.
-- Create an empty entity
local entity = world:entity()
print(entity)
-- Create an entity with a specific ID (bypasses the entity range)
world:entity(42)
Entity IDs contain both an index and a generation counter. The generation increments when an entity is recycled, preventing stale references.
Working with Component Data
Now let’s add data to entities and retrieve it.
Setting Component Values
Use world:set() to attach data to entities:
local entity = world:entity()
-- Set initial values
world:set(entity, Position, vector.create(10, 20, 30))
world:set(entity, Health, 100)
-- Update values (set works for both adding and updating)
world:set(entity, Position, vector.create(40, 50, 60))
world:set(entity, Health, 50)
The type system enforces that the value matches the component’s type:
-- This works:
world:set(entity, Position, vector.create(1, 2, 3))
-- This would cause a type error:
-- world:set(entity, Position, "invalid") -- Error: string is not assignable to vector
Getting Component Values
Use world:get() to retrieve component data:
local pos = world:get(entity, Position)
local health = world:get(entity, Health)
print(`Entity position: {pos}`)
print(`Entity health: {health}`)
world:get() returns T? (the type or nil), not just T. Always handle the nil case or use assert() if you know the component exists.
Handling Optional Components
local maybe_pos = world:get(entity, Position)
if maybe_pos then
print(`Position exists: {maybe_pos}`)
else
print("Entity doesn't have Position")
end
Removing Components
Use world:remove() to remove a component from an entity:
-- Remove a component
world:remove(entity, Health)
print(`Health after remove: {world:get(entity, Health)}`) -- nil
print(`Position still exists: {world:get(entity, Position)}`) -- still has value
-- Removing a component that doesn't exist does nothing (idempotent)
world:remove(entity, Health) -- Safe to call again
world:remove() only removes the component, not the entire entity. Use world:delete(entity) to delete the entity completely.
Complete Example
Here’s a complete example putting it all together:
local jecs = require("@jecs")
local world = jecs.world()
-- Define components
local Position = world:component() :: jecs.Id<vector>
-- Create an entity
local entity = world:entity()
-- Set component data
world:set(entity, Position, vector.create(1, 2, 3))
-- Get component data
local position = world:get(entity, Position)
print(`Entity's Position is {position}`)
Understanding Entity IDs
Entity IDs use a 48-bit format that encodes both an index and a generation:
[ 47 ........ 24 ][ 23 ........ 0 ]
| generation | index |
| (24 bits) | (24 bits) |
You can access these values using the public API:
local ECS_ID = jecs.ECS_ID
local ECS_GENERATION = jecs.ECS_GENERATION
local index = ECS_ID(entity)
local generation = ECS_GENERATION(entity)
print(`Entity's index and generation are {index} and {generation} respectively`)
Entity Recycling
When an entity is deleted, its index is recycled but the generation increments:
world:delete(entity)
local recycled_entity = world:entity()
print(`This is a huge number: {recycled_entity}`)
print(`Generation incremented to {ECS_GENERATION(recycled_entity)}`)
print(`Retains the old index at {ECS_ID(recycled_entity)}`)
This prevents stale entity references from accidentally accessing the wrong entity.
Entity Records
Every entity has a corresponding record that keeps indices into arrays of data:
local record = jecs.record(world, entity)
print(record)
The record format:
Under the hood, the ECS uses a dense array and sparse array to manage entities efficiently, but this is an implementation detail you typically don’t need to worry about.
Next Steps
Now that you understand the basics, explore more advanced topics:
- Learn about querying entities with specific components
- Discover entity relationships and how to build complex game logic
- Explore the example projects in the GitHub repository
- Check out the modules folder for ready-to-use helpers
For a deeper understanding of ECS design patterns, check out the how_to folder in the jecs repository for step-by-step tutorials.