Compare commits

...

5 commits

Author SHA1 Message Date
PepeElToro41
c72b6ec682
Merge f912866fcb into ac4441eb84 2025-08-10 18:13:17 +02:00
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
PepeElToro41
f912866fcb add way to check preregistered id creations after world creation 2025-07-29 14:18:15 -06:00
6 changed files with 221 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

@ -126,6 +126,8 @@ type world = {
max_component_id: number,
max_archetype_id: number,
start_component_id: number,
start_tag_id: number,
observable: Map<i53, Map<i53, { Observer }>>,
@ -163,6 +165,8 @@ export type World = {
max_component_id: number,
max_archetype_id: number,
start_component_id: number,
start_tag_id: number,
observable: Map<Id, Map<Id, { Observer }>>,
@ -760,6 +764,12 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean
return first == EcsWildcard or second == EcsWildcard
end
local function get_max_ids_difference(world: World): (number, number)
local diff_components = world.start_component_id - ecs_max_component_id
local diff_tags = world.start_tag_id - ecs_max_tag_id
return diff_components, diff_tags
end
local function id_record_get(world: World, id: Entity): ComponentRecord?
local component_index = world.component_index
local idr: ComponentRecord = component_index[id]
@ -2264,6 +2274,9 @@ local function world_new()
max_archetype_id = 0,
max_component_id = ecs_max_component_id,
start_component_id = ecs_max_component_id,
start_tag_id = ecs_max_tag_id,
observable = observable,
signals = signals,
} :: world
@ -2443,10 +2456,9 @@ local function world_new()
if not idr then
idr = component_index[wc]
end
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
end
else
if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local on_remove = idr.on_remove
@ -2544,10 +2556,9 @@ local function world_new()
if not idr then
idr = component_index[wc]
end
end
edge[id] = to
archetype_edges[(to :: Archetype).id][id] = src
end
else
if bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local on_remove = idr.on_remove
@ -3368,6 +3379,7 @@ return {
pair_first = ecs_pair_first :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>,
pair_second = ecs_pair_second :: <P, O>(world: World, pair: Pair<P, O>) -> Id<O>,
entity_index_get_alive = entity_index_get_alive,
get_max_ids_difference = get_max_ids_difference,
archetype_append_to_records = archetype_append_to_records,
id_record_ensure = id_record_ensure :: (World, Id) -> ComponentRecord,

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"