diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c4c7aa..b389262 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `jecs.component_record` for retrieving the component_record of a component. - `Column` and `ColumnsMap` 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. diff --git a/jecs.luau b/jecs.luau index 925308e..094d2a3 100755 --- a/jecs.luau +++ b/jecs.luau @@ -35,7 +35,7 @@ export type QueryInner = { export type Entity = number | { __T: T } export type Id = number | { __T: T } export type Pair = Id

-type ecs_id_t = Id | Pair | Pair<"Tag", T> +type ecs_id_t = Id | Pair | Pair<"Tag", T> export type Item = (self: Query) -> (Entity, T...) export type Iter = (query: Query) -> () -> (Entity, T...) @@ -48,13 +48,13 @@ export type Query = typeof(setmetatable( cached: (self: Query) -> Query, }, {} :: { - __iter: Iter + __iter: Iter, } )) export type Observer = { - callback: (archetype: Archetype) -> (), - query: QueryInner, + callback: (archetype: Archetype) -> (), + query: QueryInner, } export type World = { @@ -102,7 +102,7 @@ export type World = { --- Returns whether the entity has the ID. has: ((World, Entity, Id) -> boolean) - & ((World, Entity, Id, Id) -> boolean) + & ((World, Entity, Id, Id) -> boolean) & ((World, Entity, Id, Id, Id) -> boolean) & (World, Entity, Id, Id, Id, Id) -> boolean, @@ -126,8 +126,28 @@ export type World = { & ((World, Id, Id, Id, Id) -> Query) & ((World, Id, Id, Id, Id, Id) -> Query) & ((World, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) + & (( + World, + Id, + Id, + Id, + Id, + Id, + Id, + Id + ) -> Query) + & (( + World, + Id, + Id, + Id, + Id, + Id, + Id, + Id, + Id, + ...Id + ) -> Query), } 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 @@ -182,7 +202,8 @@ local EcsRemove = HI_COMPONENT_ID + 10 local EcsName = HI_COMPONENT_ID + 11 local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 -local EcsRest = HI_COMPONENT_ID + 14 +local EcsTag = HI_COMPONENT_ID + 14 +local EcsRest = HI_COMPONENT_ID + 15 local NULL_ARRAY = table.freeze({}) :: Column local NULL = newproxy(false) @@ -822,7 +843,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 +1963,134 @@ local function world_children(world: World, parent: Id) 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,10 +2341,7 @@ 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 + new_entity(entity, record, to) end local column = to.columns_map[id] column[record.row] = data @@ -2742,6 +2887,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, diff --git a/test/tests.luau b/test/tests.luau index 2f30264..74a5cab 100755 --- a/test/tests.luau +++ b/test/tests.luau @@ -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()