Compare commits

...

3 commits

Author SHA1 Message Date
Six
ac4441eb84
roblox-ts: Fix columns_map for easier lookups (#263)
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
* feat: retype archetype columns_map based on query

* feat: move InferComponents into Iter
2025-08-10 18:13:13 +02:00
Laptev Stanislav
f031dcee8d
docs(api): consolidate and clarify contains method documentation (#264)
Remove duplicate contains method section and update description to be more precise about checking both entities and components. Also fix example code references to use contains instead of has for consistency.
2025-08-10 18:12:50 +02:00
Ukendio
1d650d12e9 Fix backwards edge traversal for exclusive relationships 2025-08-10 16:52:08 +02:00
6 changed files with 207 additions and 70 deletions

View file

@ -355,46 +355,9 @@ This operation is the same as calling:
world:target(entity, jecs.ChildOf, 0)
```
## contains
Checks if an entity or component (id) exists in the world.
```luau
function World:contains(
entity: Entity,
): boolean
```
Example:
::: code-group
```luau [luau]
local entity = world:entity()
print(world:contains(entity))
print(world:contains(1))
print(world:contains(2))
-- Outputs:
-- true
-- true
-- false
```
```ts [typescript]
const entity = world.entity();
print(world.contains(entity));
print(world.contains(1));
print(world.contains(2));
// Outputs:
// true
// true
// false
```
:::
## remove
Removes a component (ID) from an entity
@ -460,11 +423,11 @@ Example:
```luau [luau]
local entity = world:entity()
print(world:has(entity))
print(world:contains(entity))
world:delete(entity)
print(world:has(entity))
print(world:contains(entity))
-- Outputs:
-- true
@ -601,7 +564,7 @@ print(retrievedParent === parent) // true
## contains
Checks if an entity exists and is alive in the world.
Checks if an entity or component (id) exists and is alive in the world.
```luau
function World:contains(

45
jecs.d.ts vendored
View file

@ -43,18 +43,18 @@ type InferComponents<A extends Id[]> = { [K in keyof A]: InferComponent<A[K]> };
type ArchetypeId = number;
export type Column<T> = T[];
export type Archetype<T extends unknown[]> = {
export type Archetype<T extends Id[]> = {
id: number;
types: number[];
type: string;
entities: number[];
columns: Column<unknown>[];
columns_map: Record<Id, Column<T[number]>>
columns_map: { [K in T[number]]: Column<InferComponent<K>> };
};
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
type Iter<T extends Id[]> = IterableFunction<LuaTuple<[Entity, ...InferComponents<T>]>>;
export type CachedQuery<T extends unknown[]> = {
export type CachedQuery<T extends Id[]> = {
/**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/
@ -67,7 +67,7 @@ export type CachedQuery<T extends unknown[]> = {
archetypes(): Archetype<T>[];
} & Iter<T>;
export type Query<T extends unknown[]> = {
export type Query<T extends Id[]> = {
/**
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
*/
@ -246,11 +246,11 @@ export class World {
* @param components The list of components to query.
* @returns A Query object to iterate over results.
*/
query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
query<T extends Id[]>(...components: T): Query<T>;
added<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void
changed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void
removed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>) => void): () => void
added<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void;
changed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>, value: T) => void): () => void;
removed<T>(component: Entity<T>, listener: (e: Entity, id: Id<T>) => void): () => void;
}
export function world(): World;
@ -297,11 +297,11 @@ export function ECS_PAIR_FIRST(pair: Pair): number;
export function ECS_PAIR_SECOND(pair: Pair): number;
type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & {
readonly __nominal_StatefulHook: unique symbol,
}
readonly __nominal_StatefulHook: unique symbol;
};
type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & {
readonly __nominal_StatelessHook: unique symbol,
}
readonly __nominal_StatelessHook: unique symbol;
};
export declare const OnAdd: StatefulHook;
export declare const OnRemove: StatelessHook;
@ -319,12 +319,17 @@ export declare const Exclusive: Tag;
export declare const Rest: Entity;
export type ComponentRecord = {
records: Map<Id, number>,
counts: Map<Id, number>,
size: number,
}
records: Map<Id, number>;
counts: Map<Id, number>;
size: number;
};
export function component_record(world: World, id: Id): ComponentRecord
export function component_record(world: World, id: Id): ComponentRecord;
export function bulk_insert<const C extends Id[]>(world: World, entity: Entity, ids: C, values: InferComponents<C>): void
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void
export function bulk_insert<const C extends Id[]>(
world: World,
entity: Entity,
ids: C,
values: InferComponents<C>,
): void;
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void;

View file

@ -2443,10 +2443,9 @@ local function world_new()
if not idr then
idr = component_index[wc]
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
else
if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local on_remove = idr.on_remove
@ -2544,10 +2543,9 @@ local function world_new()
if not idr then
idr = component_index[wc]
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
else
if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local on_remove = idr.on_remove

View file

@ -1,6 +1,6 @@
{
"name": "@rbxts/jecs",
"version": "0.9.0-rc.9",
"version": "0.9.0-rc.10",
"description": "Stupidly fast Entity Component System",
"main": "jecs.luau",
"repository": {

View file

@ -24,6 +24,25 @@ type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser")
local dwi = entity_visualiser.stringify
TEST("", function()
local world = jecs.world()
local a = world:entity()
local b = world:entity()
local c = world:entity()
world:add(a, pair(ChildOf, b))
world:add(a, pair(ChildOf, c))
CHECK(not world:has(a, pair(ChildOf, b)))
CHECK(world:has(a, pair(ChildOf, c)))
world:remove(a, pair(ChildOf, c))
CHECK(not world:has(a, pair(ChildOf, b)))
CHECK(not world:has(a, pair(ChildOf, c)))
end)
TEST("ardi", function()
local world = jecs.world()
local r = world:entity()
@ -319,7 +338,25 @@ TEST("repro", function()
end)
TEST("world:add()", function()
do CASE "exclusive relations"
do CASE "Removing exclusive pair should traverse backwards on edge"
local world = jecs.world()
local a = world:entity()
local b = world:entity()
local c = world:entity()
world:add(a, pair(ChildOf, b))
world:add(a, pair(ChildOf, c))
CHECK(not world:has(a, pair(ChildOf, b)))
CHECK(world:has(a, pair(ChildOf, c)))
world:remove(a, pair(ChildOf, c))
CHECK(not world:has(a, pair(ChildOf, b)))
CHECK(not world:has(a, pair(ChildOf, c)))
CHECK(not world:target(a, ChildOf))
end
do CASE "Exclusive relations"
local world = jecs.world()
local A = world:component()
world:add(A, jecs.Exclusive)
@ -2044,6 +2081,140 @@ TEST("world:remove()", function()
end)
TEST("world:set()", function()
do CASE "Removing exclusive pair should traverse backwards on edge"
local world = jecs.world()
local a = world:entity()
local b = world:entity()
local c = world:entity()
local BattleLink = world:component()
world:add(BattleLink, jecs.Exclusive)
world:set(a, pair(BattleLink, b), {
timestamp = 1,
transform = vector.create(1, 2, 3)
})
world:set(a, pair(BattleLink, c), {
timestamp = 2,
transform = vector.create(1, 2, 3)
})
CHECK(not world:has(a, pair(BattleLink, b)))
CHECK(world:has(a, pair(BattleLink, c)))
world:remove(a, pair(BattleLink, c))
CHECK(not world:has(a, pair(BattleLink, b)))
CHECK(not world:has(a, pair(BattleLink, c)))
CHECK(not world:target(a, BattleLink))
end
do CASE "Exclusive relations"
local world = jecs.world()
local A = world:component()
world:add(A, jecs.Exclusive)
local B = world:component()
local C = world:component()
local e = world:entity()
world:set(e, pair(A, B), true)
world:set(e, pair(A, C), true)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
-- We have to test the path that checks the uncached method
local e1 = world:entity()
world:set(e1, pair(A, B), true)
world:set(e1, pair(A, C), true)
CHECK(world:has(e1, pair(A, B)) == false)
CHECK(world:has(e1, pair(A, C)) == true)
end
do CASE "exclusive relations invoke hooks"
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
local e_ptr: jecs.Entity = (jecs.Rest :: any) + 1
world:add(A, jecs.Exclusive)
local on_remove_call = false
world:set(A, jecs.OnRemove, function(e, id)
on_remove_call = true
end)
local on_add_call_count = 0
world:set(A, jecs.OnAdd, function(e, id)
on_add_call_count += 1
end)
local e = world:entity()
CHECK(e == e_ptr)
world:set(e, pair(A, B))
CHECK(on_add_call_count == 1)
world:set(e, pair(A, C))
CHECK(on_add_call_count == 2)
CHECK(on_remove_call)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
-- We have to ensure that it actually invokes hooks everytime it
-- traverses the archetype
e = world:entity()
world:add(e, pair(A, B))
CHECK(on_add_call_count == 3)
world:add(e, pair(A, C))
CHECK(on_add_call_count == 4)
CHECK(on_remove_call)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
end
do CASE "exclusive relations invoke on_remove hooks that should allow side effects"
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
world:add(A, jecs.Exclusive)
local call_count = 0
world:set(A, jecs.OnRemove, function(e, id)
call_count += 1
if call_count == 1 then
world:set(e, C, true)
else
world:set(e, D, true)
end
end)
local e = world:entity()
world:set(e, pair(A, B), true)
world:set(e, pair(A, C), true)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
CHECK(world:has(e, C))
-- We have to ensure that it actually invokes hooks everytime it
-- traverses the archetype
e = world:entity()
world:set(e, pair(A, B), true)
world:set(e, pair(A, C), true)
CHECK(world:has(e, pair(A, B)) == false)
CHECK(world:has(e, pair(A, C)) == true)
CHECK(world:has(e, D))
end
do CASE "archetype move"
local world = jecs.world()

View file

@ -1,6 +1,6 @@
[package]
name = "ukendio/jecs"
version = "0.9.0-rc.9"
version = "0.9.0-rc.10"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
license = "MIT"