Skip to content

Commit

Permalink
Merge to main branch
Browse files Browse the repository at this point in the history
  • Loading branch information
Ukendio committed Oct 12, 2024
2 parents a42c948 + c5e20aa commit f82318c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 59 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@

[![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

```lua
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()

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rbxts/jecs",
"version": "0.3.2",
"version": "0.3.3",
"description": "Stupidly fast Entity Component System",
"main": "src",
"repository": {
Expand Down
105 changes: 55 additions & 50 deletions src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

local world_delete: (world: World, entity: i53, destruct: boolean?) -> ()
do
function world_delete(world: World, entity: i53, destruct: boolean?)
local entityIndex = world.entityIndex
local sparse_array = entityIndex.sparse
Expand Down
42 changes: 42 additions & 0 deletions test/tests.luau
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit f82318c

Please sign in to comment.