mirror of
https://github.com/Ukendio/jecs.git
synced 2025-08-04 19:29:18 +00:00
Compare commits
3 commits
825a8248a7
...
8194a03304
Author | SHA1 | Date | |
---|---|---|---|
|
8194a03304 | ||
|
8058182d59 | ||
|
ed5277391d |
5 changed files with 251 additions and 15 deletions
|
@ -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.
|
||||||
|
|
166
jecs.luau
166
jecs.luau
|
@ -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,7 +48,7 @@ export type Query<T...> = typeof(setmetatable(
|
||||||
cached: (self: Query<T...>) -> Query<T...>,
|
cached: (self: Query<T...>) -> Query<T...>,
|
||||||
},
|
},
|
||||||
{} :: {
|
{} :: {
|
||||||
__iter: Iter<T...>
|
__iter: Iter<T...>,
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -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,11 +2340,8 @@ 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
|
|
||||||
-- When there is no previous archetype it should create the archetype
|
|
||||||
new_entity(entity, record, to)
|
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,
|
||||||
|
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue