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 ## Unreleased
## 0.7.0
### Added ### Added
- `jecs.component_record` for retrieving the component_record of a component. - `jecs.component_record` for retrieving the component_record of a component.
- `Column<T>` and `ColumnsMap<T>` types for typescript. - `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 ### 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. - 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.

172
jecs.luau
View file

@ -35,7 +35,7 @@ export type QueryInner = {
export type Entity<T = any> = number | { __T: T } export type Entity<T = any> = number | { __T: T }
export type Id<T = any> = number | { __T: T } export type Id<T = any> = number | { __T: T }
export type Pair<P, O> = Id<P> export type Pair<P, O> = Id<P>
type ecs_id_t<T=unknown> = Id<T> | Pair<T, "Tag"> | Pair<"Tag", T> type ecs_id_t<T = unknown> = Id<T> | Pair<T, "Tag"> | Pair<"Tag", T>
export type Item<T...> = (self: Query<T...>) -> (Entity, T...) export type Item<T...> = (self: Query<T...>) -> (Entity, T...)
export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...) export type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
@ -48,13 +48,13 @@ export type Query<T...> = typeof(setmetatable(
cached: (self: Query<T...>) -> Query<T...>, cached: (self: Query<T...>) -> Query<T...>,
}, },
{} :: { {} :: {
__iter: Iter<T...> __iter: Iter<T...>,
} }
)) ))
export type Observer = { export type Observer = {
callback: (archetype: Archetype) -> (), callback: (archetype: Archetype) -> (),
query: QueryInner, query: QueryInner,
} }
export type World = { export type World = {
@ -102,7 +102,7 @@ export type World = {
--- Returns whether the entity has the ID. --- Returns whether the entity has the ID.
has: (<T, a>(World, Entity<T>, Id<a>) -> boolean) has: (<T, a>(World, Entity<T>, Id<a>) -> boolean)
& (<T, a, b >(World, Entity<T>, Id<a>, Id<a>) -> boolean) & (<T, a, b>(World, Entity<T>, Id<a>, Id<a>) -> boolean)
& (<T, a, b, c>(World, Entity<T>, Id<a>, Id<b>, Id<c>) -> boolean) & (<T, a, b, c>(World, Entity<T>, Id<a>, Id<b>, Id<c>) -> boolean)
& <T, a, b, c, d>(World, Entity<T>, Id<a>, Id<b>, Id<c>, Id<d>) -> boolean, & <T, a, b, c, d>(World, Entity<T>, Id<a>, Id<b>, Id<c>, Id<d>) -> boolean,
@ -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>(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>(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>(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>(
& (<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>) 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 = { export type Record = {
@ -155,7 +175,7 @@ export type EntityIndex = {
alive_count: number, alive_count: number,
max_id: number, max_id: number,
range_begin: number?, range_begin: number?,
range_end: number? range_end: number?,
} }
-- stylua: ignore start -- stylua: ignore start
@ -822,7 +842,6 @@ end
local function find_insert(id_types: { i53 }, toAdd: i53): number local function find_insert(id_types: { i53 }, toAdd: i53): number
for i, id in id_types do for i, id in id_types do
if id == toAdd then if id == toAdd then
error("Duplicate component id")
return -1 return -1
end end
if id > toAdd then 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)) return world_each(world, ECS_PAIR(EcsChildOf, parent::number))
end 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 function world_new()
local eindex_dense_array = {} :: { Entity } local eindex_dense_array = {} :: { Entity }
local eindex_sparse_array = {} :: { Record } local eindex_sparse_array = {} :: { Record }
@ -2193,10 +2340,7 @@ local function world_new()
-- If there was a previous archetype, then the entity needs to move the archetype -- If there was a previous archetype, then the entity needs to move the archetype
entity_move(entity_index, entity, record, to) entity_move(entity_index, entity, record, to)
else else
if #to.types > 0 then new_entity(entity, record, to)
-- When there is no previous archetype it should create the archetype
new_entity(entity, record, to)
end
end end
local column = to.columns_map[id] local column = to.columns_map[id]
column[record.row] = data column[record.row] = data
@ -2742,6 +2886,8 @@ return {
create_edge_for_remove = create_edge_for_remove, create_edge_for_remove = create_edge_for_remove,
archetype_traverse_add = archetype_traverse_add, archetype_traverse_add = archetype_traverse_add,
archetype_traverse_remove = archetype_traverse_remove, archetype_traverse_remove = archetype_traverse_remove,
bulk_insert = ecs_bulk_insert,
bulk_remove = ecs_bulk_remove,
entity_move = entity_move, entity_move = entity_move,

View file

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

View file

@ -25,6 +25,93 @@ local entity_visualiser = require("@tools/entity_visualiser")
local lifetime_tracker_add = require("@tools/lifetime_tracker") local lifetime_tracker_add = require("@tools/lifetime_tracker")
local dwi = entity_visualiser.stringify 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() TEST("repro", function()
local Model = jecs.component() local Model = jecs.component()
local Relation = jecs.component() local Relation = jecs.component()

View file

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