mirror of
https://github.com/Ukendio/jecs.git
synced 2025-08-04 19:29:18 +00:00
Compare commits
3 commits
825a8248a7
...
f887ecb2a7
Author | SHA1 | Date | |
---|---|---|---|
|
f887ecb2a7 | ||
|
8058182d59 | ||
|
ed5277391d |
5 changed files with 249 additions and 15 deletions
|
@ -5,6 +5,7 @@
|
|||
### 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.
|
||||
|
|
172
jecs.luau
172
jecs.luau
|
@ -35,7 +35,7 @@ export type QueryInner = {
|
|||
export type Entity<T = any> = number | { __T: T }
|
||||
export type Id<T = any> = number | { __T: T }
|
||||
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 Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
|
||||
|
||||
|
@ -48,13 +48,13 @@ export type Query<T...> = typeof(setmetatable(
|
|||
cached: (self: Query<T...>) -> Query<T...>,
|
||||
},
|
||||
{} :: {
|
||||
__iter: Iter<T...>
|
||||
__iter: Iter<T...>,
|
||||
}
|
||||
))
|
||||
|
||||
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: (<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, 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, 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,10 +2340,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 +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,
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue