Compare commits

..

5 commits

Author SHA1 Message Date
Ukendio
3e46b723e9 Clarify exists usage
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-07-26 02:50:24 +02:00
Ukendio
3ab1d970e2 Remove dead links 2025-07-26 02:45:13 +02:00
Ukendio
3e2d40e706 Make a table for cleanup 2025-07-26 02:41:40 +02:00
Ukendio
1c2dee57d3 Add some examples 2025-07-26 02:41:18 +02:00
Ukendio
3777585677 Address docs issues 2025-07-26 02:09:28 +02:00
6 changed files with 550 additions and 41 deletions

View file

@ -2,7 +2,7 @@
Jecs. Just an Entity Component System. Jecs. Just an Entity Component System.
# Properties # Members
## World ## World
```luau ```luau
@ -12,27 +12,93 @@ A world is a container of all ECS data. Games can have multiple worlds but compo
## Wildcard ## Wildcard
```luau ```luau
jecs.Wildcard: Entity jecs.Wildcard: Id
``` ```
Builtin component type. This ID is used for wildcard queries. Builtin component type. This ID is used for wildcard queries.
## Component ## Component
```luau ```luau
jecs.Component: Entity jecs.Component: Id
``` ```
Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID. Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID.
## ChildOf ## ChildOf
```luau ```luau
jecs.ChildOf: Entity jecs.ChildOf: Id
``` ```
Builtin component type. This ID is for creating parent-child hierarchies. Builtin component type. This ID is for creating parent-child hierarchies.
## OnAdd
```luau
jecs.OnAdd: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is added.
## OnRemove
```luau
jecs.OnRemove: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is removed.
## OnChange
```luau
jecs.OnChange: Id
```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is changed.
## Exclusive
```lua
jecs.Exclusive: Id
```
Builtin component type. This ID is for encoding that an ID is Exclusive meaning that an entity can never have more than one target for that exclusive relation.
:::code-group
```luau [luau]
local ChildOf = world:entity()
world:add(ChildOf, jecs.Exclusive)
local pop = world:entity()
local dad = world:entity()
local kid = world:entity()
world:add(kid, pair(ChildOf, dad))
print(world:target(kid, ChildOf, 0) == dad)
world:add(kid, pair(ChildOf, pop))
print(world:target(kid, ChildOf, 1) == dad) -- If ChildOf was not exclusive this would have been true
print(world:target(kid, ChildOf, 0) == pop)
-- Output:
-- true
-- false
-- true
```
:::info
By default, jecs.ChildOf is already an exclusive relationship and this is just a demonstration of how to use it.
In some cases you can use Exclusive relationships as a performance optimization as you can guarantee there will only be one target, therefore
retrieving the data from a wildcard pair with that exclusive relationship can be deterministic.
:::
## Name
```luau
jecs.Name: Id
```
Builtin component type. This ID is for naming components, but realistically you could use any component to do that.
## Rest ## Rest
```luau ```luau
jecs.Rest: Entity jecs.Rest: Id
``` ```
Builtin component type. This ID is for setting up a callback that is invoked when an instance of a component is changed.
# Functions # Functions
## pair() ## pair()
@ -48,3 +114,30 @@ function jecs.pair(
While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding. While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
::: :::
## pair_first()
```luau
function jecs.pair_first(
pair: Id, -- A full pair ID encoded using a relation-target pair.
): Entity -- The ID of the first element. Returns 0 if the ID is not alive.
```
Returns the first element (the relation part) of a pair ID.
**Example:**
```luau
local Likes = world:component()
local alice = world:entity()
local bob = world:entity()
local pair_id = pair(Likes, alice)
local relation = jecs.pair_first(pair_id)
print(relation == Likes) -- true
```
## pair_second()
```luau
function jecs.pair_second(
pair: Id, -- A full pair ID encoded using a relation-target pair.
): Entity -- The ID of the second element. Returns 0 if the ID is not alive.
```
Returns the second element (the target part) of a pair ID.

196
docs/api/observers.md Executable file
View file

@ -0,0 +1,196 @@
# Observers
The observers addon extends the World with signal-based reactivity and query-based observers. This addon provides a more ergonomic way to handle component lifecycle events and query changes.
## Installation
The observers addon is included with jecs and can be imported directly:
```luau
local jecs = require(path/to/jecs)
local observers_add = require(path/to/jecs/addons/observers)
local world = observers_add(jecs.world())
```
## Methods
### added
Registers a callback that is invoked when a component is added to any entity.
```luau
function World:added<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>, value: T?) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for additions
- `callback` - Function called when component is added, receives entity, component ID, and value
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:added(Health, function(entity, id, value)
print("Health component added to entity", entity, "with value", value)
end)
-- Later, to stop listening:
unsubscribe()
```
### removed
Registers a callback that is invoked when a component is removed from any entity.
```luau
function World:removed<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for removals
- `callback` - Function called when component is removed, receives entity and component ID
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:removed(Health, function(entity, id)
print("Health component removed from entity", entity)
end)
```
### changed
Registers a callback that is invoked when a component's value is changed on any entity.
```luau
function World:changed<T>(
component: Id<T>,
callback: (entity: Entity, id: Id<T>, value: T) -> ()
): () -> () -- Returns an unsubscribe function
```
**Parameters:**
- `component` - The component ID to listen for changes
- `callback` - Function called when component value changes, receives entity, component ID, and new value
**Returns:** An unsubscribe function that removes the listener when called
**Example:**
```luau
local Health = world:component() :: jecs.Entity<number>
local unsubscribe = world:changed(Health, function(entity, id, value)
print("Health changed to", value, "for entity", entity)
end)
```
### observer
Creates a query-based observer that triggers when entities match or stop matching a query.
```luau
function World:observer<T...>(
query: Query<T...>,
callback: ((entity: Entity, id: Id, value: any?) -> ())?
): () -> () -> Entity -- Returns an iterator function
```
**Parameters:**
- `query` - The query to observe for changes
- `callback` - Optional function called when entities match the query
**Returns:** An iterator function that returns entities that matched the query since last iteration
**Example:**
```luau
local Position = world:component() :: jecs.Id<Vector3>
local Velocity = world:component() :: jecs.Id<Vector3>
local moving_entities = world:observer(
world:query(Position, Velocity),
function(entity, id, value)
print("Entity", entity, "started moving")
end
)
-- In your game loop:
for entity in moving_entities() do
-- Process newly moving entities
end
```
### monitor
Creates a query-based monitor that triggers when entities are added to or removed from a query.
```luau
function World:monitor<T...>(
query: Query<T...>,
callback: ((entity: Entity, id: Id, value: any?) -> ())?
): () -> () -> Entity -- Returns an iterator function
```
**Parameters:**
- `query` - The query to monitor for additions/removals
- `callback` - Optional function called when entities are added or removed from the query
**Returns:** An iterator function that returns entities that were added or removed since last iteration
**Example:**
```luau
local Health = world:component() :: jecs.Id<number>
local health_changes = world:monitor(
world:query(Health),
function(entity, id, value)
print("Health component changed for entity", entity)
end
)
-- In your game loop:
for entity in health_changes() do
-- Process entities with health changes
end
```
## Usage Patterns
### Component Lifecycle Tracking
```luau
local Player = world:component()
local Health = world:component() :: jecs.Id<number>
-- Track when players are created
world:added(Player, function(entity, id, instance)
instance:SetAttribute("entityid", entity)
end)
world:removed(Player, function(entity, id)
world:add(entity, Destroy) -- process its deletion later!
end)
```
## Performance Considerations
- **Signal listeners** are called immediately when components are added/removed/changed
- **Query observers** cache the query for better performance
- **Multiple listeners** for the same component are supported and called in registration order
- **Unsubscribe functions** should be called when listeners are no longer needed to prevent memory leaks
- **Observer iterators** should be called regularly to clear the internal buffer
## Integration with Built-in Hooks
The observers addon integrates with the built-in component hooks (`OnAdd`, `OnRemove`, `OnChange`). If a component already has these hooks configured, the observers addon will preserve them and call both the original hook and any registered signal listeners.

View file

@ -4,13 +4,33 @@ A World contains entities which have components. The World is queryable and can
# Methods # Methods
## iter ## cached
Returns an iterator that can be used to iterate over the query. Returns a cached version of the query. This is useful if you want to create a query that you can iterate multiple times.
```luau ```luau
function Query:iter(): () -> (Entity, ...) function Query:cached(): Query -- Returns the cached Query
``` ```
Example:
```luau [luau]
local lerps = world:query(Lerp):cached() -- Ensure that you cache this outside a system so you do not create a new cache for a query every frame
local function system(dt)
for entity, lerp in lerps do
-- Do something
end
end
```
```ts [typescript]
const lerps = world.query(Lerp).cached()
function system(dt) {
for (const [entity, lerp] of lerps) {
// Do something
}
}
## with ## with
@ -83,15 +103,13 @@ Example:
```luau [luau] ```luau [luau]
for i, archetype in world:query(Position, Velocity):archetypes() do for i, archetype in world:query(Position, Velocity):archetypes() do
local columns = archetype.columns local field = archetype.columns_map
local field = archetype.records local positions = field[Position]
local velocities = field[Velocity]
local P = field[Position]
local V = field[Velocity]
for row, entity in archetype.entities do for row, entity in archetype.entities do
local position = columns[P][row] local position = positions[row]
local velocity = columns[V][row] local velocity = velocities[row]
-- Do something -- Do something
end end
end end
@ -101,10 +119,27 @@ end
This function is meant for people who want to really customize their query behaviour at the archetype-level This function is meant for people who want to really customize their query behaviour at the archetype-level
::: :::
## cached ## iter
In most cases, you can iterate over queries directly using `for entity, ... in query do`. The `:iter()` method is mainly useful if you are on the old solver, to get types for the returned values.
Returns a cached version of the query. This is useful if you want to iterate over the same query multiple times.
```luau ```luau
function Query:cached(): Query -- Returns the cached Query function Query:iter(): () -> (Entity, ...)
```
Example:
::: code-group
```luau [luau]
local query = world:query(Position, Velocity)
-- Direct iteration (recommended)
for entity, position, velocity in query do
-- Process entity
end
-- Using explicit iterator (when needed for the old solver)
local iterator = query:iter()
for entity, position, velocity in iterator do
-- Process entity
end
``` ```

View file

@ -6,7 +6,7 @@ A World contains entities which have components. The World is queryable and can
## new ## new
`World` utilizes a class, meaning JECS allows you to create multiple worlds. `World` utilizes a class, meaning jecs allows you to create multiple worlds.
```luau ```luau
function World.new(): World function World.new(): World
@ -55,7 +55,7 @@ const entity = world.entity();
## component ## component
Creates a new component. Do note components are entities as well, meaning JECS allows you to add other components onto them. Creates a new component. Do note components are entities as well, meaning jecs allows you to add other components onto them.
These are meant to be added onto other entities through `add` and `set` These are meant to be added onto other entities through `add` and `set`
@ -241,7 +241,10 @@ print(world.get(Entity, Health));
// 100 // 100
// 50 // 50
``` ```
:::
:::info
`world:set(entity, component, value)` propagates that a change has happened for thes component on this entity, while mutating a value directly would not.
::: :::
## query ## query
@ -289,10 +292,52 @@ If the index is larger than the total number of instances the entity has for the
```luau ```luau
function World:target( function World:target(
entity: Entity, -- The entity entity: Entity, -- The entity
relation: Entity, -- The relationship between the entity and the target relation: Id, -- The relationship between the entity and the target
nth: number, -- The index nth: number, -- The index
): Entity? -- The target for the relationship at the specified index. ): Id? -- The target for the relationship at the specified index.
``` ```
Example:
::: code-group
```luau [luau]
local function timers_count(world: types.World)
local timers = world
:query(jecs.pair(ct.Timer, jecs.w))
:without(ct.Destroy)
:cached()
return function(_, dt: number)
for entity in timers do
local index = 0
local nth = world:target(entity, ct.Timer, index)
while nth do
local timer = world:get(entity, jecs.pair(ct.Timer, nth))
local elapsed = timer.elapsed + dt
if elapsed >= timer.duration then
world:add(entity, ct.Destroy)
end
timer.elapsed = elapsed
end
end
end
end
```
```ts [typescript]
const entity = world.entity();
print(world.contains(entity));
print(world.contains(1));
print(world.contains(2));
// Outputs:
// true
// true
// false
```
:::
## parent ## parent
@ -355,9 +400,9 @@ print(world.contains(2));
Removes a component (ID) from an entity Removes a component (ID) from an entity
```luau ```luau
function World:remove( function World:remove<T>(
entity: Entity, entity: Entity,
component: Entity<T> component: Id<T>
): void ): void
``` ```
@ -458,20 +503,20 @@ Useful when you only need the entity for a specific ID and you want to avoid cre
```luau ```luau
function World:each( function World:each(
id: Entity -- The component ID component: Id -- The component ID
): () -> Entity ): () -> Entity
``` ```
Example: Example:
::: code-group ::: code-group
```luau [luau] ```luau [luau]
local id = world:entity() local id = world:component()
for entity in world:each(id) do for entity in world:each(id) do
-- Do something -- Do something
end end
``` ```
```ts [typescript] ```ts [typescript]
const id = world.entity(); const id = world.component();
for (const entity of world.each(id)) { for (const entity of world.each(id)) {
// Do something // Do something
} }
@ -500,6 +545,122 @@ Enforces a check for entities to be created within a desired range.
```luau ```luau
function World:range( function World:range(
range_begin: number -- The starting point, range_begin: number -- The starting point,
range_begin: number? -- The end point (optional) range_end: number? -- The end point (optional)
) )
``` ```
Example:
::: code-group
```luau [luau]
world:range(1000, 5000) -- Entities will be created with IDs 1000-5000
local entity = world:entity()
print(entity) -- Will be >= 1000 and < 5000
```
```ts [typescript]
world.range(1000, 5000) // Entities will be created with IDs 1000-5000
const entity = world.entity()
print(entity) // Will be >= 1000 and < 5000
```
:::
## parent
Gets the parent entity of the specified entity using the built-in `ChildOf` relationship.
```luau
function World:parent(
entity: Entity
): Entity? -- Returns the parent entity or nil if no parent
```
Example:
::: code-group
```luau [luau]
local parent = world:entity()
local child = world:entity()
world:add(child, pair(jecs.ChildOf, parent))
local retrieved_parent = world:parent(child)
print(retrieved_parent == parent) -- true
```
```ts [typescript]
const parent = world.entity()
const child = world.entity()
world.add(child, pair(jecs.ChildOf, parent))
const retrievedParent = world.parent(child)
print(retrievedParent === parent) // true
```
:::
## contains
Checks if an entity exists and is alive in the world.
```luau
function World:contains(
entity: Entity
): boolean
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
print(world:contains(entity)) -- true
world:delete(entity)
print(world:contains(entity)) -- false
```
```ts [typescript]
const entity = world.entity()
print(world.contains(entity)) // true
world.delete(entity)
print(world.contains(entity)) // false
```
:::
## exists
Checks if the entity ID exists regardless of whether it is alive or not. Useful to know if the ID is occupied in the entity index.
```luau
function World:exists(
entity: Entity
): boolean
```
## cleanup
Cleans up deleted entities and their associated data. This is automatically called by jecs, but can be called manually if needed.
```luau
function World:cleanup(): void
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
world:delete(entity)
-- Cleanup is usually automatic, but can be called manually
world:cleanup()
```
```ts [typescript]
const entity = world.entity()
world.delete(entity)
// Cleanup is usually automatic, but can be called manually
world.cleanup()
```
:::

View file

@ -15,6 +15,9 @@ hero:
- theme: alt - theme: alt
text: API References text: API References
link: /api/jecs.md link: /api/jecs.md
- theme: alt
text: Observers
link: /api/observers.md
features: features:
- title: Stupidly Fast - title: Stupidly Fast

View file

@ -27,8 +27,8 @@ local jecs = require(path/to/jecs)
local world = jecs.world() local world = jecs.world()
``` ```
```typescript [typescript] ```typescript [typescript]
import { World } from "@rbxts/jecs" import * as jecs from "@rbxts/jecs"
const world = new World() const world = jecs.world()
// creates a new entity with no components and returns its identifier // creates a new entity with no components and returns its identifier
const entity = world.entity() const entity = world.entity()
@ -156,6 +156,13 @@ world.set(Transform, OnChange, (entity, id, data) => {
``` ```
::: :::
:::info
Children are cleaned up before parents
When a parent and its children are deleted, OnRemove hooks will be invoked for children first, under the condition that there are no cycles in the relationship graph of the deleted entities. This order is maintained for any relationship that has the (OnDeleteTarget, Delete) trait (see Component Traits for more details).
When an entity graph contains cycles, order is undefined. This includes cycles that can be formed using different relationships.
:::
### Cleanup Traits ### Cleanup Traits
When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references. When entities that are used as tags, components, relationships or relationship targets are deleted, cleanup traits ensure that the store does not contain any dangling references. Any cleanup policy provides this guarantee, so while they are configurable, games cannot configure traits that allows for dangling references.
@ -166,13 +173,20 @@ This is what cleanup traits are for: to specify which action needs to be execute
To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed. To configure a cleanup policy for an entity, a `(Condition, Action)` pair can be added to it. If no policy is specified, the default cleanup action (`Remove`) is performed.
There are two cleanup actions: #### Cleanup Traits Summary
| Condition | Action | Description | Use Case |
|-----------|--------|-------------|----------|
| `OnDelete` | `Remove` | Removes the component from all entities when the component is deleted | Default behavior, safe cleanup |
| `OnDelete` | `Delete` | Deletes all entities that have the component when the component is deleted | Cascading deletion, dangerous |
| `OnDeleteTarget` | `Remove` | Removes the relationship from all entities when the target is deleted | Safe relationship cleanup |
| `OnDeleteTarget` | `Delete` | Deletes all entities that have the relationship when the target is deleted | Hierarchical deletion (e.g., parent-child) |
**Cleanup Actions:**
- `Remove`: removes instances of the specified (component) id from all entities (default) - `Remove`: removes instances of the specified (component) id from all entities (default)
- `Delete`: deletes all entities with specified id - `Delete`: deletes all entities with specified id
There are two cleanup conditions: **Cleanup Conditions:**
- `OnDelete`: the component, tag or relationship is deleted - `OnDelete`: the component, tag or relationship is deleted
- `OnDeleteTarget`: a target used with the relationship is deleted - `OnDeleteTarget`: a target used with the relationship is deleted
@ -285,9 +299,10 @@ jecs.world() -- Position gets registered here
``` ```
```typescript [typescript] ```typescript [typescript]
import { world } from "@rbxts/jecs"
const Position = jecs.component<Vector3>(); const Position = jecs.component<Vector3>();
new World() // Position gets registered here world() // Position gets registered here
``` ```
::: :::
@ -301,9 +316,11 @@ jecs.world() -- Position gets registered here with its name "Position"
``` ```
```typescript [typescript] ```typescript [typescript]
import { world } from "@rbxts/jecs"
jecs.meta(Position, jecs.Name, "Position") jecs.meta(Position, jecs.Name, "Position")
new World() // Position gets registered here with its name "Position" world() // Position gets registered here with its name "Position"
``` ```
::: :::
@ -632,7 +649,7 @@ world:set(e, pair(Eats, Apples), { amount = 1 })
world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0)) world:set(e, pair(Begin, Position), Vector3.new(0, 0, 0))
world:set(e, pair(End, Position), Vector3.new(10, 20, 30)) world:set(e, pair(End, Position), Vector3.new(10, 20, 30))
world:add(e, jecs.ChildOf, Position) world:add(e, pair(jecs.ChildOf, Position))
``` ```
```typescript [typescript] ```typescript [typescript]
@ -648,7 +665,7 @@ world.set(e, pair(Eats, Apples), { amount: 1 })
world.set(e, pair(Begin, Position), new Vector3(0, 0, 0)) world.set(e, pair(Begin, Position), new Vector3(0, 0, 0))
world.set(e, pair(End, Position), new Vector3(10, 20, 30)) world.set(e, pair(End, Position), new Vector3(10, 20, 30))
world.add(e, jecs.ChildOf, Position) world.add(e, pair(jecs.ChildOf, Position))
``` ```
::: :::
@ -695,3 +712,7 @@ To improve the speed of evaluating queries, Jecs has indices that store all arch
While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships. While registering an archetype for a relationship index is not more expensive than registering an archetype for a regular index, an archetype with relationships has to also register itself with the appropriate wildcard indices for its relationships. For example, an archetype with relationship `pair(Likes, Apples)` registers itself with the `pair(Likes, Apples)`, `pair(Likes, jecs.Wildcard)` and `pair(jecs.Wildcard, Apples)` indices. For this reason, creating new archetypes with relationships has a higher overhead than an archetype without relationships.
This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html). This page takes wording and terminology directly from Flecs, the first ECS with full support for [Entity Relationships](https://www.flecs.dev/flecs/md_docs_2Relationships.html).
## Next Steps
- [API Reference](../api/jecs.md) - Complete API documentation