diff --git a/README.md b/README.md index 49fccb0..0b1694b 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,15 @@ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache-blue.svg?style=for-the-badge)](LICENSE-APACHE) [![Wally](https://img.shields.io/github/v/tag/ukendio/jecs?&style=for-the-badge)](https://wally.run/package/ukendio/jecs) -jecs is Just a stupidly fast Entity Component System +Just a stupidly fast Entity Component System -- Entity Relationships as first class citizens -- Iterate 800,000 entities at 60 frames per second -- Type-safe [Luau](https://luau-lang.org/) API -- Zero-dependency package -- Optimized for column-major operations -- Cache friendly archetype/SoA storage -- Unit tested for stability +* [Entity Relationships](https://ajmmertens.medium.com/building-games-in-ecs-with-entity-relationships-657275ba2c6c) as first class citizens +* Iterate 800,000 entities at 60 frames per second +* Type-safe [Luau](https://luau-lang.org/) API +* Zero-dependency package +* Optimized for column-major operations +* Cache friendly [archetype/SoA](https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9) storage +* Rigorously [unit tested](https://github.com/Ukendio/jecs/actions/workflows/ci.yaml) for stability ### Example @@ -21,6 +21,8 @@ jecs is Just a stupidly fast Entity Component System local world = jecs.World.new() local pair = jecs.pair +-- These components and functions are actually already builtin +-- but have been illustrated for demonstration purposes local ChildOf = world:component() local Name = world:component() diff --git a/package.json b/package.json index de18f30..a99752f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rbxts/jecs", - "version": "0.3.2", + "version": "0.3.3", "description": "Stupidly fast Entity Component System", "main": "src", "repository": { diff --git a/src/init.luau b/src/init.luau index 8f2b7b0..9ee392c 100644 --- a/src/init.luau +++ b/src/init.luau @@ -808,23 +808,6 @@ local function world_remove(world: World, entity: i53, id: i53) end end -local function world_clear(world: World, entity: i53) - --TODO: use sparse_get (stashed) - local record = world.entityIndex.sparse[entity] - if not record then - return - end - - local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE - local archetype = record.archetype - - if archetype == nil or archetype == ROOT_ARCHETYPE then - return - end - - entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE) -end - local function archetype_fast_delete_last(columns: { Column }, column_count: number, types: { i53 }, entity: i53) for i, column in columns do if column ~= NULL_ARRAY then @@ -842,6 +825,59 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end end +local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) + local entityIndex = world.entityIndex + local columns = archetype.columns + local types = archetype.types + local entities = archetype.entities + local column_count = #entities + local last = #entities + local move = entities[last] + local delete = entities[row] + entities[row] = move + entities[last] = nil + + if row ~= last then + -- TODO: should be "entity_index_sparse_get(entityIndex, move)" + local record_to_move = entityIndex.sparse[move] + if record_to_move then + record_to_move.row = row + end + end + + -- TODO: if last == 0 then deactivate table + + for _, id in types do + invoke_hook(world, EcsOnRemove, id, delete) + end + + if row == last then + archetype_fast_delete_last(columns, column_count, types, delete) + else + archetype_fast_delete(columns, column_count, row, types, delete) + end +end + +local function world_clear(world: World, entity: i53) + --TODO: use sparse_get (stashed) + local record = world.entityIndex.sparse[entity] + if not record then + return + end + + local archetype = record.archetype + local row = record.row + + if archetype then + -- In the future should have a destruct mode for + -- deleting archetypes themselves. Maybe requires recycling + archetype_delete(world, archetype, row) + end + + record.archetype = nil + record.row = nil +end + local function archetype_disconnect_edge(edge: GraphEdge) local edge_next = edge.next local edge_prev = edge.prev @@ -936,41 +972,10 @@ local function world_cleanup(world) world.archetypeIndex = new_archetype_map end + + local world_delete: (world: World, entity: i53, destruct: boolean?) -> () do - local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) - local entityIndex = world.entityIndex - local columns = archetype.columns - local types = archetype.types - local entities = archetype.entities - local column_count = #entities - local last = #entities - local move = entities[last] - local delete = entities[row] - entities[row] = move - entities[last] = nil - - if row ~= last then - -- TODO: should be "entity_index_sparse_get(entityIndex, move)" - local record_to_move = entityIndex.sparse[move] - if record_to_move then - record_to_move.row = row - end - end - - -- TODO: if last == 0 then deactivate table - - for _, id in types do - invoke_hook(world, EcsOnRemove, id, delete) - end - - if row == last then - archetype_fast_delete_last(columns, column_count, types, delete) - else - archetype_fast_delete(columns, column_count, row, types, delete) - end - end - function world_delete(world: World, entity: i53, destruct: boolean?) local entityIndex = world.entityIndex local sparse_array = entityIndex.sparse diff --git a/test/tests.luau b/test/tests.luau index fe7e39e..4b6fef1 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -827,6 +827,48 @@ TEST("world:clear()", function() CHECK(world:get(e, A) == nil) CHECK(world:get(e, B) == nil) end + + do CASE "should move last record" + local world = world_new() + local A = world:component() + + local e = world:entity() + local e1 = world:entity() + + world:add(e, A) + world:add(e1, A) + + local archetype = world.archetypeIndex["1"] + local archetype_entities = archetype.entities + + local _e = e :: number + local _e1 = e1 :: number + + CHECK(archetype_entities[1] == _e) + CHECK(archetype_entities[2] == _e1) + + local sparse_array = world.entityIndex.sparse + local e_record = sparse_array[e] + local e1_record = sparse_array[e1] + CHECK(e_record.archetype == archetype) + CHECK(e1_record.archetype == archetype) + CHECK(e1_record.row == 2) + + world:clear(e) + + CHECK(e_record.archetype == nil) + CHECK(e_record.row == nil) + CHECK(e1_record.archetype == archetype) + CHECK(e1_record.row == 1) + + CHECK(archetype_entities[1] == _e1) + CHECK(archetype_entities[2] == nil) + + CHECK(world:contains(e) == true) + CHECK(world:has(e, A) == false) + CHECK(world:contains(e1) == true) + CHECK(world:has(e1, A) == true) + end end) TEST("world:has()", function()