Compare commits

..

3 commits

Author SHA1 Message Date
Ukendio
8194a03304 bump
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
2025-06-27 15:58:33 +02:00
Ukendio
8058182d59 Update Changelog 2025-06-27 15:47:27 +02:00
Ukendio
ed5277391d Add bulk operations 2025-06-27 15:43:46 +02:00
5 changed files with 251 additions and 15 deletions

View file

@ -2,9 +2,12 @@
## Unreleased
## 0.7.0
### Added
- `jecs.component_record` for retrieving the component_record of a component.
- `Column<T>` and `ColumnsMap<T>` types for typescript.
- `bulk_insert` and `bulk_remove` respectively for moving an entity to an archetype without intermediate steps.
### Changed
- The fields `archetype.records[id]` and `archetype.counts[id` have been removed from the archetype struct and been moved to the component record `component_index[id].records[archetype.id]` and `component_index[id].counts[archetype.id]` respectively.

162
jecs.luau
View file

@ -48,7 +48,7 @@ export type Query<T...> = typeof(setmetatable(
cached: (self: Query<T...>) -> Query<T...>,
},
{} :: {
__iter: Iter<T...>
__iter: Iter<T...>,
}
))
@ -126,8 +126,28 @@ export type World = {
& (<A, B, C, D>(World, Id<A>, Id<B>, Id<C>, Id<D>) -> Query<A, B, C, D>)
& (<A, B, C, D, E>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>) -> Query<A, B, C, D, E>)
& (<A, B, C, D, E, F>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>, Id<F>) -> Query<A, B, C, D, E, F>)
& (<A, B, C, D, E, F, G>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>, Id<F>, Id<G>) -> Query<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>, Id<F>, Id<G>, Id<H>, ...Id<any>) -> Query<A, B, C, D, E, F, G, H>)
& (<A, B, C, D, E, F, G>(
World,
Id<A>,
Id<B>,
Id<C>,
Id<D>,
Id<E>,
Id<F>,
Id<G>
) -> Query<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(
World,
Id<A>,
Id<B>,
Id<C>,
Id<D>,
Id<E>,
Id<F>,
Id<G>,
Id<H>,
...Id<any>
) -> Query<A, B, C, D, E, F, G, H>),
}
export type Record = {
@ -155,7 +175,7 @@ export type EntityIndex = {
alive_count: number,
max_id: number,
range_begin: number?,
range_end: number?
range_end: number?,
}
-- stylua: ignore start
@ -822,7 +842,6 @@ end
local function find_insert(id_types: { i53 }, toAdd: i53): number
for i, id in id_types do
if id == toAdd then
error("Duplicate component id")
return -1
end
if id > toAdd then
@ -1943,6 +1962,134 @@ local function world_children<a>(world: World, parent: Id<a>)
return world_each(world, ECS_PAIR(EcsChildOf, parent::number))
end
local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, values: { any })
local entity_index = world.entity_index
local r = entity_index_try_get(entity_index, entity)
if not r then
return
end
local from = r.archetype
local component_index = world.component_index
if not from then
local dst_types = ids
local to = archetype_ensure(world, dst_types)
new_entity(entity, r, to)
local row = r.row
local columns_map = to.columns_map
for i, id in ids do
local value = values[i]
local cdr = component_index[id]
local on_add = cdr.hooks.on_add
if value then
columns_map[id][row] = value
if on_add then
on_add(entity, id, value :: any)
end
else
if on_add then
on_add(entity, id)
end
end
end
return
end
local dst_types = table.clone(from.types)
local emplaced: { [number]: boolean } = {}
for i, id in ids do
local at = find_insert(dst_types :: { number }, id :: number)
if at == -1 then
emplaced[i] = true
continue
end
emplaced[i] = false
table.insert(dst_types, at, id)
end
local to = archetype_ensure(world, dst_types)
local columns_map = to.columns_map
if from ~= to then
entity_move(entity_index, entity, r, to)
end
local row = r.row
for i, set in emplaced do
local id = ids[i]
local idr = component_index[id]
local value = values[i] :: any
local on_add = idr.hooks.on_add
local on_change = idr.hooks.on_change
if value then
columns_map[id][row] = value
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
end
end
end
local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity })
local entity_index = world.entity_index
local r = entity_index_try_get(entity_index, entity)
if not r then
return
end
local from = r.archetype
local component_index = world.component_index
if not from then
return
end
local remove: { [Entity]: boolean } = {}
local columns_map = from.columns_map
for i, id in ids do
if not columns_map[id] then
continue
end
remove[id] = true
local idr = component_index[id]
local on_remove = idr.hooks.on_remove
if on_remove then
on_remove(entity, id)
end
end
local to = r.archetype
if from ~= to then
from = to
end
local dst_types = table.clone(from.types) :: { Entity }
for id in remove do
local at = table.find(dst_types, id)
table.remove(dst_types, at)
end
to = archetype_ensure(world, dst_types)
if from ~= to then
entity_move(entity_index, entity, r, to)
end
end
local function world_new()
local eindex_dense_array = {} :: { Entity }
local eindex_sparse_array = {} :: { Record }
@ -2193,11 +2340,8 @@ local function world_new()
-- If there was a previous archetype, then the entity needs to move the archetype
entity_move(entity_index, entity, record, to)
else
if #to.types > 0 then
-- When there is no previous archetype it should create the archetype
new_entity(entity, record, to)
end
end
local column = to.columns_map[id]
column[record.row] = data
@ -2742,6 +2886,8 @@ return {
create_edge_for_remove = create_edge_for_remove,
archetype_traverse_add = archetype_traverse_add,
archetype_traverse_remove = archetype_traverse_remove,
bulk_insert = ecs_bulk_insert,
bulk_remove = ecs_bulk_remove,
entity_move = entity_move,

View file

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

View file

@ -25,6 +25,93 @@ local entity_visualiser = require("@tools/entity_visualiser")
local lifetime_tracker_add = require("@tools/lifetime_tracker")
local dwi = entity_visualiser.stringify
TEST("bulk", function()
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local E = world:entity()
local F = world:component()
local e = world:entity()
local r = jecs.entity_index_try_get(world.entity_index, e)
jecs.bulk_insert(world, e, { A, B, C }, { 1, 2, 3 })
CHECK(world:get(e, A) == 1)
CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 3)
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)
CHECK(world:get(e, D) == 4)
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 })
CHECK(world:get(e, A) == 10)
CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 30)
CHECK(world:get(e, D) == 40)
CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50)
local G = world:component()
world:set(e, G, 100)
CHECK(world:get(e, A) == 10)
CHECK(world:get(e, B) == 2)
CHECK(world:get(e, C) == 30)
CHECK(world:get(e, D) == 40)
CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 100)
world:remove(e, B)
CHECK(world:get(e, A) == 10)
CHECK(world:has(e, B) == false)
CHECK(world:get(e, C) == 30)
CHECK(world:get(e, D) == 40)
CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 100)
jecs.bulk_remove(world, e, { A, B, C, D })
CHECK(world:has(e, A) == false)
CHECK(world:has(e, B) == false)
CHECK(world:has(e, C) == false)
CHECK(world:has(e, D) == false)
CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 100)
jecs.bulk_insert(world, e, { D, G }, { 999, 1 })
CHECK(world:has(e, A) == false)
CHECK(world:has(e, B) == false)
CHECK(world:has(e, C) == false)
CHECK(world:get(e, D) == 999)
CHECK(world:get(e, E) == nil and world:has(e, E))
CHECK(world:get(e, F) == 50)
CHECK(world:get(e, G) == 1)
jecs.bulk_remove(world, e, { A, B, C, D, E, F, G })
CHECK(world:has(e, A) == false)
CHECK(world:has(e, B) == false)
CHECK(world:has(e, C) == false)
CHECK(world:has(e, D) == false)
CHECK(world:has(e, E) == false)
CHECK(world:has(e, F) == false)
CHECK(world:has(e, G) == false)
end)
TEST("repro", function()
local Model = jecs.component()
local Relation = jecs.component()

View file

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