From bee92f489ccf9348e32a049000941034d8e74fdd Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 27 Jun 2025 17:42:54 +0200 Subject: [PATCH 1/6] fetch b --- jecs.luau | 4 ++-- package.json | 2 +- wally.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jecs.luau b/jecs.luau index d3a81cb..3dacd69 100755 --- a/jecs.luau +++ b/jecs.luau @@ -563,7 +563,7 @@ local function world_get(world: World, entity: Entity, if not b then return va elseif not c then - return va, fetch(a, columns_map, row) + return va, fetch(b, columns_map, row) elseif not d then return va, fetch(b, columns_map, row), fetch(c, columns_map, row) elseif not e then @@ -2217,7 +2217,7 @@ local function world_new() if not b then return va elseif not c then - return va, fetch(a, columns_map, row) + return va, fetch(b, columns_map, row) elseif not d then return va, fetch(b, columns_map, row), fetch(c, columns_map, row) elseif not e then diff --git a/package.json b/package.json index 7025974..5e4af50 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rbxts/jecs", - "version": "0.7.1", + "version": "0.7.2", "description": "Stupidly fast Entity Component System", "main": "jecs.luau", "repository": { diff --git a/wally.toml b/wally.toml index d66636f..a551265 100755 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.7.1" +version = "0.7.2" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" license = "MIT" From 3c7f3b4eb3f23720409872cd08aba37b9ce4b009 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 30 Jun 2025 00:40:03 +0200 Subject: [PATCH 2/6] 0.7.3 (#247) * 0.7.3 * Remove print * fix jecs.meta for adding values --- jecs.d.ts | 4 +++- jecs.luau | 33 ++++++++++++++++---------- package.json | 2 +- test/tests.luau | 63 ++++++++++++++++++++++++++++++++++++++++++++++--- wally.toml | 2 +- 5 files changed, 85 insertions(+), 19 deletions(-) diff --git a/jecs.d.ts b/jecs.d.ts index cccf6d5..ea36bf8 100755 --- a/jecs.d.ts +++ b/jecs.d.ts @@ -105,7 +105,7 @@ export class World { /** * Creates a new World. */ - constructor(); + private constructor(); /** * Enforces a check for entities to be created within a desired range. @@ -249,6 +249,8 @@ export class World { query(...components: T): Query>; } +export function world(): World; + export function component(): Entity; export function tag(): Tag; diff --git a/jecs.luau b/jecs.luau index 3dacd69..214b066 100755 --- a/jecs.luau +++ b/jecs.luau @@ -52,6 +52,8 @@ export type Query = typeof(setmetatable( } )) +type QueryArm = () -> () + export type Observer = { callback: (archetype: Archetype) -> (), query: QueryInner, @@ -658,6 +660,9 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord local relation = id local target = 0 local is_pair = ECS_IS_PAIR(id :: number) + + local has_delete = false + if is_pair then relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53 ecs_assert(relation and entity_index_is_alive( @@ -665,15 +670,18 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id :: number)) :: i53 ecs_assert(target and entity_index_is_alive( entity_index, target), ECS_INTERNAL_ERROR) - end - local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) - local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) + local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0) - local has_delete = false + if cleanup_policy_target == EcsDelete then + has_delete = true + end + else + local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) - if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then - has_delete = true + if cleanup_policy == EcsDelete then + has_delete = true + end end local on_add, on_change, on_remove = world_get(world, @@ -2026,18 +2034,16 @@ local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, va local value = values[i] :: any local on_add = idr.hooks.on_add - local on_change = idr.hooks.on_change - if value then + if value ~= nil then columns_map[id][row] = value + local on_change = idr.hooks.on_change local hook = if set then on_change else on_add if hook then hook(entity, id, value :: any) end - else - if on_add then - on_add(entity, id, value) - end + elseif on_add then + on_add(entity, id) end end end @@ -2533,6 +2539,7 @@ local function world_new() end end end + end local function inner_world_delete(world: World, entity: Entity) @@ -2803,7 +2810,7 @@ local function world_new() if value == NULL then inner_world_add(world, i, ty) else - inner_world_add(world, i, ty, value) + inner_world_set(world, i, ty, value) end end end diff --git a/package.json b/package.json index 5e4af50..568bcbf 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rbxts/jecs", - "version": "0.7.2", + "version": "0.7.3", "description": "Stupidly fast Entity Component System", "main": "jecs.luau", "repository": { diff --git a/test/tests.luau b/test/tests.luau index 74a5cab..dd53258 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -25,6 +25,57 @@ local entity_visualiser = require("@tools/entity_visualiser") local lifetime_tracker_add = require("@tools/lifetime_tracker") local dwi = entity_visualiser.stringify +TEST("repro#", function() + do CASE "pair(OnDelete, Delete)" + local world = jecs.world() + local ct = world:component() + world:add(ct, jecs.pair(jecs.OnDelete, jecs.Delete)) + + local e1 = world:entity() + local e2 = world:entity() + + local dummy = world:entity() + + world:add(e1, ct) + world:add(e2, jecs.pair(ct, dummy)) + + world:delete(dummy) + + CHECK(world:contains(e2)) + + world:delete(ct) + + CHECK(not world:contains(e1)) + end + + do CASE "pair(OnDeleteTarget, Delete)" + print("start") + local world = jecs.world() + local ct = world:component() + world:add(ct, jecs.pair(jecs.OnDeleteTarget, jecs.Delete)) + + local e1 = world:entity() + local e2 = world:entity() + + -- local dummy = world:entity() + + print("flags") + world:add(e1, ct) + + print(world.component_index[ct].flags) + -- world:add(e2, jecs.pair(ct, dummy)) + + -- world:delete(dummy) + + -- CHECK(not world:contains(e2)) + + world:delete(ct) + + CHECK(world:contains(e1)) + end + +end) + TEST("bulk", function() local world = jecs.world() local A = world:component() @@ -42,7 +93,10 @@ TEST("bulk", function() CHECK(world:get(e, B) == 2) CHECK(world:get(e, C) == 3) - jecs.bulk_insert(world, e, { D, E, F }, { 4, nil, 5 }) + jecs.bulk_insert(world, e, + { D, E, F }, + { 4, nil, 5 } + ) CHECK(world:get(e, A) == 1) CHECK(world:get(e, B) == 2) CHECK(world:get(e, C) == 3) @@ -51,7 +105,10 @@ TEST("bulk", function() CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, F) == 5) - jecs.bulk_insert(world, e, { A, D, E, F, C }, { 10, 40, nil, 50, 30 }) + jecs.bulk_insert(world, e, + { A, D, E, F, C }, + { 10, 40, nil, 50, 30 } + ) CHECK(world:get(e, A) == 10) CHECK(world:get(e, B) == 2) @@ -441,7 +498,7 @@ TEST("world:delete()", function() local A = world:entity() local B = world:entity() - world:add(Relation, pair(jecs.OnDelete, jecs.Delete)) + world:add(Relation, pair(jecs.OnDeleteTarget, jecs.Delete)) local entity = world:entity() diff --git a/wally.toml b/wally.toml index a551265..19f4969 100755 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.7.2" +version = "0.7.3" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" license = "MIT" From d6e720f2008222ded4b64a006e1c9fb154d9b2b9 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 30 Jun 2025 01:06:31 +0200 Subject: [PATCH 3/6] Optimize removal path (#248) * Optimize removal path * Replace eindex_get implementation --- jecs.luau | 65 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/jecs.luau b/jecs.luau index 214b066..fbddb26 100755 --- a/jecs.luau +++ b/jecs.luau @@ -2146,31 +2146,20 @@ local function world_new() return r end - -- local function entity_index_try_get_safe(entity: number): Record? - -- local r = entity_index_try_get_any_fast(entity_index, entity) - -- if r then - -- local r_dense = r.dense - -- if r_dense > entity_index.alive_count then - -- return nil - -- end - -- if entity_index.dense_array[r_dense] ~= entity then - -- return nil - -- end - -- end - -- return r - -- end - local function inner_entity_index_try_get(entity: number): Record? - local r = eindex_sparse_array[ECS_ENTITY_T_LO(entity)] + local r = inner_entity_index_try_get_any(entity) if r then - if eindex_dense_array[r.dense] ~= entity then + local r_dense = r.dense + if r_dense > entity_index.alive_count then + return nil + end + if eindex_dense_array[r_dense] ~= entity then return nil end end return r end - local function inner_world_add( world: World, entity: Entity, @@ -2582,15 +2571,41 @@ local function world_new() archetype_destroy(world, idr_archetype) end else - for archetype_id in idr.records do - local idr_archetype = archetypes[archetype_id] - local entities = idr_archetype.entities - local n = #entities - for i = n, 1, -1 do - inner_world_remove(world, entities[i], entity) - end + local on_remove = idr.hooks.on_remove + if on_remove then + for archetype_id in idr.records do + local idr_archetype = archetypes[archetype_id] + local to = archetype_traverse_remove(world, entity, idr_archetype) + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + local e = entities[i] + on_remove(e, entity) + local r = eindex_sparse_array[ECS_ID(e :: number)] + local from = r.archetype + if from ~= idr_archetype then + -- unfortunately the on_remove hook allows a window where `e` can have changed archetype + -- this is hypothetically not that expensive of an operation anyways + to = archetype_traverse_remove(world, entity, from) + end + entity_move(entity_index, e, r, to) + end - archetype_destroy(world, idr_archetype) + archetype_destroy(world, idr_archetype) + end + else + for archetype_id in idr.records do + local idr_archetype = archetypes[archetype_id] + local to = archetype_traverse_remove(world, entity, idr_archetype) + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + local e = entities[i] + entity_move(entity_index, e, eindex_sparse_array[ECS_ID(e :: number)], to) + end + + archetype_destroy(world, idr_archetype) + end end end end From 7c8358656a3e20091c90ba839e41d2f614d67cdd Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 30 Jun 2025 01:35:55 +0200 Subject: [PATCH 4/6] unsafe get --- jecs.luau | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/jecs.luau b/jecs.luau index fbddb26..ff6d917 100755 --- a/jecs.luau +++ b/jecs.luau @@ -2146,13 +2146,27 @@ local function world_new() return r end - local function inner_entity_index_try_get(entity: number): Record? + -- local function inner_entity_index_try_get(entity: number): Record? + -- local r = inner_entity_index_try_get_any(entity) + -- if r then + -- local r_dense = r.dense + -- if r_dense > entity_index.alive_count then + -- return nil + -- end + -- if eindex_dense_array[r_dense] ~= entity then + -- return nil + -- end + -- end + -- return r + -- end + + local function inner_entity_index_try_get_unsafe(entity: number): Record? local r = inner_entity_index_try_get_any(entity) if r then local r_dense = r.dense - if r_dense > entity_index.alive_count then - return nil - end + -- if r_dense > entity_index.alive_count then + -- return nil + -- end if eindex_dense_array[r_dense] ~= entity then return nil end @@ -2166,7 +2180,7 @@ local function world_new() id: Id ): () local entity_index = world.entity_index - local record = inner_entity_index_try_get(entity :: number) + local record = inner_entity_index_try_get_unsafe(entity :: number) if not record then return end @@ -2194,7 +2208,7 @@ local function world_new() local function inner_world_get(world: World, entity: Entity, a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any - local record = inner_entity_index_try_get(entity::number) + local record = inner_entity_index_try_get_unsafe(entity::number) if not record then return nil end @@ -2225,7 +2239,7 @@ local function world_new() local function inner_world_has(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean - local record = inner_entity_index_try_get(entity) + local record = inner_entity_index_try_get_unsafe(entity) if not record then return false end @@ -2245,7 +2259,7 @@ local function world_new() end local function inner_world_target(world: World, entity: Entity, relation: Id, index: number?): Entity? - local record = inner_entity_index_try_get(entity :: number) + local record = inner_entity_index_try_get_unsafe(entity :: number) if not record then return nil end @@ -2307,7 +2321,7 @@ local function world_new() end local function inner_world_set(world: World, entity: Entity, id: Id, data: a): () - local record = inner_entity_index_try_get(entity :: number) + local record = inner_entity_index_try_get_unsafe(entity :: number) if not record then return end @@ -2408,7 +2422,7 @@ local function world_new() end local function inner_world_remove(world: World, entity: Entity, id: Id) - local record = inner_entity_index_try_get(entity :: number) + local record = inner_entity_index_try_get_unsafe(entity :: number) if not record then return end @@ -2533,7 +2547,7 @@ local function world_new() local function inner_world_delete(world: World, entity: Entity) local entity_index = world.entity_index - local record = inner_entity_index_try_get(entity::number) + local record = inner_entity_index_try_get_unsafe(entity::number) if not record then return end From 4ff492ceaf8a76e60e88efd28e732ce906cf151f Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 30 Jun 2025 22:37:20 +0200 Subject: [PATCH 5/6] Optimize moving archetype --- jecs.luau | 81 ++++++++++++++++++++++++------------------ test/tests.luau | 94 +++++++++++++++++++++++-------------------------- 2 files changed, 91 insertions(+), 84 deletions(-) diff --git a/jecs.luau b/jecs.luau index ff6d917..ef2a02d 100755 --- a/jecs.luau +++ b/jecs.luau @@ -439,6 +439,7 @@ end local function archetype_move( entity_index: EntityIndex, + entity: Entity, to: Archetype, dst_row: i24, from: Archetype, @@ -452,48 +453,58 @@ local function archetype_move( local id_types = from.types local columns_map = to.columns_map - for i, column in src_columns do - if column == NULL_ARRAY then - continue - end - -- Retrieves the new column index from the source archetype's record from each component - -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. - local dst_column = columns_map[id_types[i]] - - -- Sometimes target column may not exist, e.g. when you remove a component. - if dst_column then - dst_column[dst_row] = column[src_row] - end - + if src_row ~= last then -- If the entity is the last row in the archetype then swapping it would be meaningless. - if src_row ~= last then + + for i, column in src_columns do + if column == NULL_ARRAY then + continue + end + -- Retrieves the new column index from the source archetype's record from each component + -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. + local dst_column = columns_map[id_types[i]] + + -- Sometimes target column may not exist, e.g. when you remove a component. + if dst_column then + dst_column[dst_row] = column[src_row] + end + -- Swap rempves columns to ensure there are no holes in the archetype. column[src_row] = column[last] + column[last] = nil end - column[last] = nil - end - local moved = #src_entities - -- Move the entity from the source to the destination archetype. - -- Because we have swapped columns we now have to update the records - -- corresponding to the entities' rows that were swapped. - local e1 = src_entities[src_row] - local e2 = src_entities[moved] + -- Move the entity from the source to the destination archetype. + -- Because we have swapped columns we now have to update the records + -- corresponding to the entities' rows that were swapped. - if src_row ~= moved then + local e2 = src_entities[last] src_entities[src_row] = e2 + + local sparse_array = entity_index.sparse_array + local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)] + record2.row = src_row + else + for i, column in src_columns do + if column == NULL_ARRAY then + continue + end + -- Retrieves the new column index from the source archetype's record from each component + -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. + local dst_column = columns_map[id_types[i]] + + -- Sometimes target column may not exist, e.g. when you remove a component. + if dst_column then + dst_column[dst_row] = column[src_row] + end + + column[last] = nil + end end - src_entities[moved] = nil :: any - dst_entities[dst_row] = e1 - - local sparse_array = entity_index.sparse_array - - local record1 = sparse_array[ECS_ENTITY_T_LO(e1 :: number)] - local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)] - record1.row = dst_row - record2.row = src_row + src_entities[last] = nil :: any + dst_entities[dst_row] = entity end local function archetype_append( @@ -526,7 +537,7 @@ local function entity_move( local sourceRow = record.row local from = record.archetype local dst_row = archetype_append(entity, to) - archetype_move(entity_index, to, dst_row, from, sourceRow) + archetype_move(entity_index, entity, to, dst_row, from, sourceRow) record.archetype = to record.row = dst_row end @@ -2394,7 +2405,7 @@ local function world_new() return entity else for i = eindex_max_id + 1, index do - eindex_sparse_array[i] = { dense = i } :: Record + eindex_sparse_array[i]= { dense = i } :: Record eindex_dense_array[i] = i end entity_index.max_id = index @@ -2459,8 +2470,8 @@ local function world_new() local idr_archetype = archetypes[archetype_id] local entities = idr_archetype.entities local n = #entities + table.move(entities, 1, n, count + 1, queue) count += n - table.move(entities, 1, n, #queue + 1, queue) end for _, e in queue do inner_world_remove(world, e, entity) diff --git a/test/tests.luau b/test/tests.luau index dd53258..6387d03 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -22,57 +22,10 @@ type Entity = jecs.Entity type Id = jecs.Id local entity_visualiser = require("@tools/entity_visualiser") -local lifetime_tracker_add = require("@tools/lifetime_tracker") local dwi = entity_visualiser.stringify -TEST("repro#", function() - do CASE "pair(OnDelete, Delete)" - local world = jecs.world() - local ct = world:component() - world:add(ct, jecs.pair(jecs.OnDelete, jecs.Delete)) +TEST("repro", function() - local e1 = world:entity() - local e2 = world:entity() - - local dummy = world:entity() - - world:add(e1, ct) - world:add(e2, jecs.pair(ct, dummy)) - - world:delete(dummy) - - CHECK(world:contains(e2)) - - world:delete(ct) - - CHECK(not world:contains(e1)) - end - - do CASE "pair(OnDeleteTarget, Delete)" - print("start") - local world = jecs.world() - local ct = world:component() - world:add(ct, jecs.pair(jecs.OnDeleteTarget, jecs.Delete)) - - local e1 = world:entity() - local e2 = world:entity() - - -- local dummy = world:entity() - - print("flags") - world:add(e1, ct) - - print(world.component_index[ct].flags) - -- world:add(e2, jecs.pair(ct, dummy)) - - -- world:delete(dummy) - - -- CHECK(not world:contains(e2)) - - world:delete(ct) - - CHECK(world:contains(e1)) - end end) @@ -433,8 +386,51 @@ TEST("world:contains()", function() end) TEST("world:delete()", function() + do CASE "pair(OnDelete, Delete)" + local world = jecs.world() + local ct = world:component() + world:add(ct, jecs.pair(jecs.OnDelete, jecs.Delete)) + + local e1 = world:entity() + local e2 = world:entity() + + local dummy = world:entity() + + world:add(e1, ct) + world:add(e2, jecs.pair(ct, dummy)) + + world:delete(dummy) + + CHECK(world:contains(e2)) + + world:delete(ct) + + CHECK(not world:contains(e1)) + end + + do CASE "pair(OnDeleteTarget, Delete)" + local world = jecs.world() + local ct = world:component() + world:add(ct, jecs.pair(jecs.OnDeleteTarget, jecs.Delete)) + + local e1 = world:entity() + local e2 = world:entity() + + local dummy = world:entity() + + world:add(e1, ct) + + world:add(e2, jecs.pair(ct, dummy)) + + world:delete(dummy) + + CHECK(not world:contains(e2)) + + world:delete(ct) + + CHECK(world:contains(e1)) + end do CASE "remove (*, R) pairs when relationship is invalidated" - print("-------") local world = jecs.world() local e1 = world:entity() local e2 = world:entity() From 9b57189c3add7e303a6a4ec64ff70134c6db94df Mon Sep 17 00:00:00 2001 From: renyang19910211 <842759495@qq.com> Date: Tue, 1 Jul 2025 04:41:29 +0800 Subject: [PATCH 6/6] Fix receive_replication.luau removed issue (#243) --- .../src/ReplicatedStorage/systems/receive_replication.luau | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/demo/src/ReplicatedStorage/systems/receive_replication.luau b/demo/src/ReplicatedStorage/systems/receive_replication.luau index b4be32f..a28e0b7 100755 --- a/demo/src/ReplicatedStorage/systems/receive_replication.luau +++ b/demo/src/ReplicatedStorage/systems/receive_replication.luau @@ -74,11 +74,12 @@ return function(world: types.World) local removed = map.removed if removed then - for i, e in removed do - if not world:contains(e) then + for _, entity in removed do + entity = ecs_map_get(world, entity) + if not world:contains(entity) then continue end - world:remove(e, id) + world:remove(entity, id) end end end