Compare commits

..

7 commits

Author SHA1 Message Date
Laptev Stanislav
b0065aa2ce
Merge ca10a8a8f3 into 9b57189c3a 2025-06-30 20:46:24 +00:00
Laptev Stanislav
ca10a8a8f3
Merge branch 'Ukendio:main' into bugfix/readme 2025-06-30 23:46:22 +03:00
renyang19910211
9b57189c3a
Fix receive_replication.luau removed issue (#243)
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-06-30 22:41:29 +02:00
Ukendio
4ff492ceaf Optimize moving archetype 2025-06-30 22:37:30 +02:00
Ukendio
7c8358656a unsafe get
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-06-30 01:44:25 +02:00
Marcus
d6e720f200
Optimize removal path (#248)
* Optimize removal path

* Replace eindex_get implementation
2025-06-30 01:06:31 +02:00
Marcus
3c7f3b4eb3
0.7.3 (#247)
* 0.7.3

* Remove print

* fix jecs.meta for adding values
2025-06-30 00:40:03 +02:00
6 changed files with 184 additions and 81 deletions

View file

@ -74,11 +74,12 @@ return function(world: types.World)
local removed = map.removed local removed = map.removed
if removed then if removed then
for i, e in removed do for _, entity in removed do
if not world:contains(e) then entity = ecs_map_get(world, entity)
if not world:contains(entity) then
continue continue
end end
world:remove(e, id) world:remove(entity, id)
end end
end end
end end

4
jecs.d.ts vendored
View file

@ -105,7 +105,7 @@ export class World {
/** /**
* Creates a new World. * Creates a new World.
*/ */
constructor(); private constructor();
/** /**
* Enforces a check for entities to be created within a desired range. * Enforces a check for entities to be created within a desired range.
@ -249,6 +249,8 @@ export class World {
query<T extends Id[]>(...components: T): Query<InferComponents<T>>; query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
} }
export function world(): World;
export function component<T>(): Entity<T>; export function component<T>(): Entity<T>;
export function tag(): Tag; export function tag(): Tag;

135
jecs.luau
View file

@ -52,6 +52,8 @@ export type Query<T...> = typeof(setmetatable(
} }
)) ))
type QueryArm<T...> = () -> ()
export type Observer = { export type Observer = {
callback: (archetype: Archetype) -> (), callback: (archetype: Archetype) -> (),
query: QueryInner, query: QueryInner,
@ -437,6 +439,7 @@ end
local function archetype_move( local function archetype_move(
entity_index: EntityIndex, entity_index: EntityIndex,
entity: Entity,
to: Archetype, to: Archetype,
dst_row: i24, dst_row: i24,
from: Archetype, from: Archetype,
@ -450,6 +453,9 @@ local function archetype_move(
local id_types = from.types local id_types = from.types
local columns_map = to.columns_map local columns_map = to.columns_map
if src_row ~= last then
-- If the entity is the last row in the archetype then swapping it would be meaningless.
for i, column in src_columns do for i, column in src_columns do
if column == NULL_ARRAY then if column == NULL_ARRAY then
continue continue
@ -463,35 +469,42 @@ local function archetype_move(
dst_column[dst_row] = column[src_row] dst_column[dst_row] = column[src_row]
end end
-- If the entity is the last row in the archetype then swapping it would be meaningless.
if src_row ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype. -- Swap rempves columns to ensure there are no holes in the archetype.
column[src_row] = column[last] column[src_row] = column[last]
end
column[last] = nil column[last] = nil
end end
local moved = #src_entities
-- Move the entity from the source to the destination archetype. -- Move the entity from the source to the destination archetype.
-- Because we have swapped columns we now have to update the records -- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped. -- corresponding to the entities' rows that were swapped.
local e1 = src_entities[src_row]
local e2 = src_entities[moved]
if src_row ~= moved then local e2 = src_entities[last]
src_entities[src_row] = e2 src_entities[src_row] = e2
end
src_entities[moved] = nil :: any
dst_entities[dst_row] = e1
local sparse_array = entity_index.sparse_array 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)] local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)]
record1.row = dst_row
record2.row = src_row 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[last] = nil :: any
dst_entities[dst_row] = entity
end end
local function archetype_append( local function archetype_append(
@ -524,7 +537,7 @@ local function entity_move(
local sourceRow = record.row local sourceRow = record.row
local from = record.archetype local from = record.archetype
local dst_row = archetype_append(entity, to) 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.archetype = to
record.row = dst_row record.row = dst_row
end end
@ -658,6 +671,9 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
local relation = id local relation = id
local target = 0 local target = 0
local is_pair = ECS_IS_PAIR(id :: number) local is_pair = ECS_IS_PAIR(id :: number)
local has_delete = false
if is_pair then if is_pair then
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53 relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53
ecs_assert(relation and entity_index_is_alive( ecs_assert(relation and entity_index_is_alive(
@ -665,16 +681,19 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id :: number)) :: i53 target = entity_index_get_alive(entity_index, ECS_PAIR_SECOND(id :: number)) :: i53
ecs_assert(target and entity_index_is_alive( ecs_assert(target and entity_index_is_alive(
entity_index, target), ECS_INTERNAL_ERROR) 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
if cleanup_policy == EcsDelete or cleanup_policy_target == EcsDelete then
has_delete = true has_delete = true
end end
else
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
if cleanup_policy == EcsDelete then
has_delete = true
end
end
local on_add, on_change, on_remove = world_get(world, local on_add, on_change, on_remove = world_get(world,
relation, EcsOnAdd, EcsOnChange, EcsOnRemove) relation, EcsOnAdd, EcsOnChange, EcsOnRemove)
@ -2026,18 +2045,16 @@ local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, va
local value = values[i] :: any local value = values[i] :: any
local on_add = idr.hooks.on_add 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 columns_map[id][row] = value
local on_change = idr.hooks.on_change
local hook = if set then on_change else on_add local hook = if set then on_change else on_add
if hook then if hook then
hook(entity, id, value :: any) hook(entity, id, value :: any)
end end
else elseif on_add then
if on_add then on_add(entity, id)
on_add(entity, id, value)
end
end end
end end
end end
@ -2140,38 +2157,41 @@ local function world_new()
return r return r
end end
-- local function entity_index_try_get_safe(entity: number): Record? -- local function inner_entity_index_try_get(entity: number): Record?
-- local r = entity_index_try_get_any_fast(entity_index, entity) -- local r = inner_entity_index_try_get_any(entity)
-- if r then -- if r then
-- local r_dense = r.dense -- local r_dense = r.dense
-- if r_dense > entity_index.alive_count then -- if r_dense > entity_index.alive_count then
-- return nil -- return nil
-- end -- end
-- if entity_index.dense_array[r_dense] ~= entity then -- if eindex_dense_array[r_dense] ~= entity then
-- return nil -- return nil
-- end -- end
-- end -- end
-- return r -- return r
-- end -- end
local function inner_entity_index_try_get(entity: number): Record? local function inner_entity_index_try_get_unsafe(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 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 return nil
end end
end end
return r return r
end end
local function inner_world_add<T, a>( local function inner_world_add<T, a>(
world: World, world: World,
entity: Entity<T>, entity: Entity<T>,
id: Id<a> id: Id<a>
): () ): ()
local entity_index = world.entity_index 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 if not record then
return return
end end
@ -2199,7 +2219,7 @@ local function world_new()
local function inner_world_get(world: World, entity: Entity, local function inner_world_get(world: World, entity: Entity,
a: Id, b: Id?, c: Id?, d: Id?, e: Id?): ...any 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 if not record then
return nil return nil
end end
@ -2230,7 +2250,7 @@ local function world_new()
local function inner_world_has(world: World, entity: i53, local function inner_world_has(world: World, entity: i53,
a: i53, b: i53?, c: i53?, d: i53?, e: i53?): boolean 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 if not record then
return false return false
end end
@ -2250,7 +2270,7 @@ local function world_new()
end end
local function inner_world_target<T, a>(world: World, entity: Entity<T>, relation: Id<a>, index: number?): Entity? local function inner_world_target<T, a>(world: World, entity: Entity<T>, relation: Id<a>, 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 if not record then
return nil return nil
end end
@ -2312,7 +2332,7 @@ local function world_new()
end end
local function inner_world_set<T, a>(world: World, entity: Entity<T>, id: Id<a>, data: a): () local function inner_world_set<T, a>(world: World, entity: Entity<T>, id: Id<a>, 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 if not record then
return return
end end
@ -2385,7 +2405,7 @@ local function world_new()
return entity return entity
else else
for i = eindex_max_id + 1, index do 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 eindex_dense_array[i] = i
end end
entity_index.max_id = index entity_index.max_id = index
@ -2413,7 +2433,7 @@ local function world_new()
end end
local function inner_world_remove<T, a>(world: World, entity: Entity<T>, id: Id<a>) local function inner_world_remove<T, a>(world: World, entity: Entity<T>, id: Id<a>)
local record = inner_entity_index_try_get(entity :: number) local record = inner_entity_index_try_get_unsafe(entity :: number)
if not record then if not record then
return return
end end
@ -2450,8 +2470,8 @@ local function world_new()
local idr_archetype = archetypes[archetype_id] local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities local entities = idr_archetype.entities
local n = #entities local n = #entities
table.move(entities, 1, n, count + 1, queue)
count += n count += n
table.move(entities, 1, n, #queue + 1, queue)
end end
for _, e in queue do for _, e in queue do
inner_world_remove(world, e, entity) inner_world_remove(world, e, entity)
@ -2533,11 +2553,12 @@ local function world_new()
end end
end end
end end
end end
local function inner_world_delete<T>(world: World, entity: Entity<T>) local function inner_world_delete<T>(world: World, entity: Entity<T>)
local entity_index = world.entity_index 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 if not record then
return return
end end
@ -2575,16 +2596,42 @@ local function world_new()
archetype_destroy(world, idr_archetype) archetype_destroy(world, idr_archetype)
end end
else else
local on_remove = idr.hooks.on_remove
if on_remove then
for archetype_id in idr.records do for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id] local idr_archetype = archetypes[archetype_id]
local to = archetype_traverse_remove(world, entity, idr_archetype)
local entities = idr_archetype.entities local entities = idr_archetype.entities
local n = #entities local n = #entities
for i = n, 1, -1 do for i = n, 1, -1 do
inner_world_remove(world, entities[i], entity) 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 end
archetype_destroy(world, idr_archetype) archetype_destroy(world, idr_archetype)
end 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
end end
@ -2803,7 +2850,7 @@ local function world_new()
if value == NULL then if value == NULL then
inner_world_add(world, i, ty) inner_world_add(world, i, ty)
else else
inner_world_add(world, i, ty, value) inner_world_set(world, i, ty, value)
end end
end end
end end

View file

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

View file

@ -22,9 +22,13 @@ type Entity<T=nil> = jecs.Entity<T>
type Id<T=unknown> = jecs.Id<T> type Id<T=unknown> = jecs.Id<T>
local entity_visualiser = require("@tools/entity_visualiser") local entity_visualiser = require("@tools/entity_visualiser")
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local dwi = entity_visualiser.stringify local dwi = entity_visualiser.stringify
TEST("repro", function()
end)
TEST("bulk", function() TEST("bulk", function()
local world = jecs.world() local world = jecs.world()
local A = world:component() local A = world:component()
@ -42,7 +46,10 @@ TEST("bulk", function()
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 3) 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, A) == 1)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 3) CHECK(world:get(e, C) == 3)
@ -51,7 +58,10 @@ TEST("bulk", function()
CHECK(world:get(e, E) == nil and world:has(e, E)) CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 5) 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, A) == 10)
CHECK(world:get(e, B) == 2) CHECK(world:get(e, B) == 2)
@ -376,8 +386,51 @@ TEST("world:contains()", function()
end) end)
TEST("world:delete()", function() 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" do CASE "remove (*, R) pairs when relationship is invalidated"
print("-------")
local world = jecs.world() local world = jecs.world()
local e1 = world:entity() local e1 = world:entity()
local e2 = world:entity() local e2 = world:entity()
@ -441,7 +494,7 @@ TEST("world:delete()", function()
local A = world:entity() local A = world:entity()
local B = 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() local entity = world:entity()

View file

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