This commit is contained in:
Marcus 2024-12-25 00:07:48 +00:00 committed by GitHub
commit de2bb07dd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 275 additions and 127 deletions

346
jecs.luau
View file

@ -78,30 +78,32 @@ type EntityIndex = {
local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
-- stylua: ignore start -- stylua: ignore start
local EcsOnAdd = HI_COMPONENT_ID + 1 local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2 local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnSet = HI_COMPONENT_ID + 3 local EcsOnSet = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4 local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5 local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6 local EcsComponent = HI_COMPONENT_ID + 6
local EcsOnDelete = HI_COMPONENT_ID + 7 local EcsOnDelete = HI_COMPONENT_ID + 7
local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9 local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10 local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11 local EcsName = HI_COMPONENT_ID + 11
local EcsRest = HI_COMPONENT_ID + 12 local EcsArchetypeCreate = HI_COMPONENT_ID + 12
local EcsArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14
local ECS_PAIR_FLAG = 0x8 local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10 local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_ID_DELETE = 0b0000_0001 local ECS_ID_DELETE = 0b0000_0001
local ECS_ID_IS_TAG = 0b0000_0010 local ECS_ID_IS_TAG = 0b0000_0010
local ECS_ID_HAS_ON_ADD = 0b0000_0100 local ECS_ID_HAS_ON_ADD = 0b0000_0100
local ECS_ID_HAS_ON_SET = 0b0000_1000 local ECS_ID_HAS_ON_SET = 0b0000_1000
local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 local ECS_ID_HAS_ON_REMOVE = 0b0001_0000
local ECS_ID_MASK = 0b0000_0000 local ECS_ID_MASK = 0b0000_0000
-- stylua: ignore end -- stylua: ignore end
local NULL_ARRAY = table.freeze({}) :: Column local NULL_ARRAY = table.freeze({}) :: Column
@ -250,6 +252,29 @@ local function ecs_pair_second(world, e)
return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e))
end end
local function query_match(terms: { {i53 }}, archetype: Archetype)
local records = archetype.records
for _, term in terms do
local id = term[1]
local out = term[2]
local has = records[id] ~= nil
if has ~= not out then
return false
end
end
return true
end
local function find_observers(world: World, event, component): { Observer }?
local cache = world.observerable[event]
if not cache then
return nil
end
return cache[component] :: any
end
local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24)
local src_columns = from.columns local src_columns = from.columns
local dst_columns = to.columns local dst_columns = to.columns
@ -549,6 +574,20 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
local columns = (table.create(length) :: any) :: { Column } local columns = (table.create(length) :: any) :: { Column }
local records: { ArchetypeRecord } = {} local records: { ArchetypeRecord } = {}
local archetype: Archetype = {
columns = columns,
entities = {},
id = archetype_id,
records = records,
type = ty,
types = id_types,
add = {},
remove = {},
refs = {} :: GraphEdge,
}
for i, componentId in id_types do for i, componentId in id_types do
local idr = id_record_ensure(world, componentId) local idr = id_record_ensure(world, componentId)
archetype_append_to_records(idr, archetype_id, records, componentId, i) archetype_append_to_records(idr, archetype_id, records, componentId, i)
@ -572,18 +611,17 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
end end
end end
local archetype: Archetype = { for _, id in id_types do
columns = columns, local observer_list = find_observers(world, EcsArchetypeCreate, id)
entities = {}, if not observer_list then
id = archetype_id, continue
records = records, end
type = ty, for _, observer in observer_list do
types = id_types, if query_match(observer.terms, archetype) then
observer.callback(archetype)
add = {}, end
remove = {}, end
refs = {} :: GraphEdge, end
}
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[archetype_id] = archetype world.archetypes[archetype_id] = archetype
@ -626,13 +664,13 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number
end end
local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype
local types = node.types local id_types = node.types
-- Component IDs are added incrementally, so inserting and sorting -- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion -- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array. -- point in the types array.
local dst = table.clone(node.types) :: { i53 } local dst = table.clone(node.types) :: { i53 }
local at = find_insert(types, id) local at = find_insert(id_types, id)
if at == -1 then if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it -- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype. -- directly instead of needing to hash types for a lookup to the archetype.
@ -644,13 +682,13 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch
end end
local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype
local types = node.types local id_types = node.types
local at = table.find(types, id) local at = table.find(id_types, id)
if at == nil then if at == nil then
return node return node
end end
local dst = table.clone(types) local dst = table.clone(id_types)
table.remove(dst, at) table.remove(dst, at)
return archetype_ensure(world, dst) return archetype_ensure(world, dst)
@ -1006,6 +1044,18 @@ local function archetype_destroy(world: World, archetype: Archetype)
world.archetypeIndex[archetype.type] = nil :: any world.archetypeIndex[archetype.type] = nil :: any
local records = archetype.records local records = archetype.records
for id in records do
local observer_list = find_observers(world, EcsArchetypeDelete, id)
if not observer_list then
continue
end
for _, observer in observer_list do
if query_match(observer.terms, archetype) then
observer.callback(archetype)
end
end
end
for id in records do for id in records do
local idr = component_index[id] local idr = component_index[id]
idr.cache[archetype_id] = nil :: any idr.cache[archetype_id] = nil :: any
@ -1064,23 +1114,30 @@ do
local idr = component_index[delete] local idr = component_index[delete]
if idr then if idr then
local children = {}
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
for i, child in idr_archetype.entities do
table.insert(children, child)
end
end
local flags = idr.flags local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do for archetype_id in idr.cache do
-- Cascade deletion to children local idr_archetype = archetypes[archetype_id]
world_delete(world, child)
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
world_delete(world, entities[i])
end
end end
else else
for _, child in children do for archetype_id in idr.cache do
world_remove(world, child, delete) local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities
local n = #entities
for i = n, 1, -1 do
world_remove(world, entities[i], delete)
end
end
for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id]
archetype_destroy(world, idr_archetype)
end end
end end
end end
@ -1414,65 +1471,73 @@ local function query_iter(query): () -> (number, ...any)
return query_next return query_next
end end
local function query_without(query: { compatible_archetypes: { Archetype } }, ...) local function query_without(query: QueryInner, ...: i53)
local filters: { without: { i53 } } = query.filters :: any
local without = { ... }
if not filters then
filters = {}
query.filters = filters :: any
end
filters.without = without
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...)
for i = #compatible_archetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local shouldRemove = false local matches = true
for j = 1, N do for _, id in without do
local id = select(j, ...)
if records[id] then if records[id] then
shouldRemove = true matches = false
break break
end end
end end
if shouldRemove then if matches then
local last = #compatible_archetypes continue
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end end
end
if #compatible_archetypes == 0 then local last = #compatible_archetypes
return EMPTY_QUERY if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end end
return query :: any return query :: any
end end
local function query_with(query: { compatible_archetypes: { Archetype } }, ...) local function query_with(query: QueryInner, ...)
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...) local filters: { with: { i53 } } = query.filters :: any
local with = { ... }
if not filters then
filters = {}
query.filters = filters :: any
end
filters.with = with
for i = #compatible_archetypes, 1, -1 do for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i] local archetype = compatible_archetypes[i]
local records = archetype.records local records = archetype.records
local shouldRemove = false local matches = true
for j = 1, N do for _, id in with do
local id = select(j, ...)
if not records[id] then if not records[id] then
shouldRemove = true matches = false
break break
end end
end end
if shouldRemove then if matches then
local last = #compatible_archetypes continue
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end end
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end end
if #compatible_archetypes == 0 then
return EMPTY_QUERY
end
return query :: any return query :: any
end end
@ -1483,6 +1548,76 @@ local function query_archetypes(query)
return query.compatible_archetypes return query.compatible_archetypes
end end
local function query_cached(query: QueryInner)
local archetypes = query.compatible_archetypes
local world = query.world :: World
-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
-- because the event will be emitted for all components of that Archetype.
local first = query.ids[1]
local observerable = world.observerable
local on_create_action = observerable[EcsArchetypeCreate]
if not on_create_action then
on_create_action = {}
observerable[EcsArchetypeCreate] = on_create_action
end
local query_cache_on_create = on_create_action[first]
if not query_cache_on_create then
query_cache_on_create = {}
on_create_action[first] = query_cache_on_create
end
local on_delete_action = observerable[EcsArchetypeDelete]
if not on_delete_action then
on_delete_action = {}
observerable[EcsArchetypeDelete] = on_delete_action
end
local query_cache_on_delete = on_delete_action[first]
if not query_cache_on_delete then
query_cache_on_delete = {}
on_delete_action[first] = query_cache_on_delete
end
local function on_create_callback(archetype)
table.insert(archetypes, archetype)
end
local function on_delete_callback(archetype)
local i = table.find(archetypes, archetype) :: number
local n = #archetypes
archetypes[i] = archetypes[n]
archetypes[n] = nil
end
local terms = {}
for _, id in query.ids do
table.insert(terms, { id })
end
local filters = query.filters
if filters then
local with = filters.with
if with then
for _, id in with do
table.insert(terms, { id })
end
end
local without = filters.without
if without then
for _, id in without do
table.insert(terms, { id, true })
end
end
end
local observer_for_create = { query = query, callback = on_create_callback, terms = terms }
local observer_for_delete = { query = query, callback = on_delete_callback, terms = terms }
table.insert(query_cache_on_create, observer_for_create)
table.insert(query_cache_on_delete, observer_for_delete)
return query
end
local Query = {} local Query = {}
Query.__index = Query Query.__index = Query
Query.__iter = query_iter Query.__iter = query_iter
@ -1490,6 +1625,7 @@ Query.iter = query_iter_init
Query.without = query_without Query.without = query_without
Query.with = query_with Query.with = query_with
Query.archetypes = query_archetypes Query.archetypes = query_archetypes
Query.cached = query_cached
local function world_query(world: World, ...) local function world_query(world: World, ...)
local compatible_archetypes = {} local compatible_archetypes = {}
@ -1502,10 +1638,16 @@ local function world_query(world: World, ...)
local idr: IdRecord? local idr: IdRecord?
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
local q = setmetatable({
ids = ids,
compatible_archetypes = compatible_archetypes,
world = world,
}, Query)
for _, id in ids do for _, id in ids do
local map = componentIndex[id] local map = componentIndex[id]
if not map then if not map then
return EMPTY_QUERY return q
end end
if idr == nil or map.size < idr.size then if idr == nil or map.size < idr.size then
@ -1514,7 +1656,7 @@ local function world_query(world: World, ...)
end end
if not idr then if not idr then
return EMPTY_QUERY return q
end end
for archetype_id in idr.cache do for archetype_id in idr.cache do
@ -1542,15 +1684,6 @@ local function world_query(world: World, ...)
compatible_archetypes[length] = compatibleArchetype compatible_archetypes[length] = compatibleArchetype
end end
if length == 0 then
return EMPTY_QUERY
end
local q = setmetatable({
compatible_archetypes = compatible_archetypes,
ids = ids,
}, Query) :: any
return q return q
end end
@ -1736,6 +1869,7 @@ function World.new()
nextComponentId = 0 :: number, nextComponentId = 0 :: number,
nextEntityId = 0 :: number, nextEntityId = 0 :: number,
ROOT_ARCHETYPE = (nil :: any) :: Archetype, ROOT_ARCHETYPE = (nil :: any) :: Archetype,
observerable = {},
}, World) :: any }, World) :: any
self.ROOT_ARCHETYPE = archetype_create(self, {}, "") self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
@ -1781,7 +1915,7 @@ type function ecs_entity_t(entity)
return entity:components()[2]:readproperty(types.singleton("__T")) return entity:components()[2]:readproperty(types.singleton("__T"))
end end
export type function Pair(first, second) type function Pair(first, second)
local thing = first:components()[2] local thing = first:components()[2]
if thing:readproperty(types.singleton("__T")):is("nil") then if thing:readproperty(types.singleton("__T")):is("nil") then
@ -1797,15 +1931,39 @@ export type Entity<T = unknown> = number & { __T: T }
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...) type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
type Query<T...> = typeof(setmetatable({}, { export type Query<T...> = typeof(setmetatable({}, {
__iter = (nil :: any) :: Iter<T...>, __iter = (nil :: any) :: Iter<T...>,
})) & { })) & {
iter: Iter<T...>, iter: Iter<T...>,
with: (self: Query<T...>, ...i53) -> Query<T...>, with: (self: Query<T...>, ...Id) -> Query<T...>,
without: (self: Query<T...>, ...i53) -> Query<T...>, without: (self: Query<T...>, ...Id) -> Query<T...>,
archetypes: (self: Query<T...>) -> { Archetype }, archetypes: (self: Query<T...>) -> { Archetype },
cached: (self: Query<T...>) -> Query<T...>,
} }
type QueryInner = {
compatible_archetypes: { Archetype },
filters: {
without: { i53 }?,
with: { i53 }?,
}?,
ids: { i53 },
world: {} -- Downcasted to be serializable by the analyzer
}
type Observer = {
callback: (archetype: Archetype) -> (),
query: QueryInner,
}
type function ecs_partial_t(ty)
local output = types.newtable()
for k, v in ty:properties() do
output:setproperty(k, types.unionof(v.write, types.singleton(nil)))
end
return output
end
export type World = { export type World = {
archetypeIndex: { [string]: Archetype }, archetypeIndex: { [string]: Archetype },
archetypes: Archetypes, archetypes: Archetypes,
@ -1816,6 +1974,8 @@ export type World = {
nextComponentId: number, nextComponentId: number,
nextEntityId: number, nextEntityId: number,
nextArchetypeId: number, nextArchetypeId: number,
observerable: { [i53]: { [i53]: { { query: Query<i53> } } } },
} & { } & {
--- Creates a new entity --- Creates a new entity
entity: (self: World) -> Entity, entity: (self: World) -> Entity,

View file

@ -62,6 +62,10 @@ local function debug_world_inspect(world: World)
} }
end end
local function name(world, e)
return world:get(e, jecs.Name)
end
TEST("archetype", function() TEST("archetype", function()
local archetype_append_to_records = jecs.archetype_append_to_records local archetype_append_to_records = jecs.archetype_append_to_records
local id_record_ensure = jecs.id_record_ensure local id_record_ensure = jecs.id_record_ensure
@ -358,6 +362,24 @@ TEST("world:add()", function()
end) end)
TEST("world:query()", function() TEST("world:query()", function()
do CASE "cached"
local world = world_new()
local Foo = world:component()
local Bar = world:component()
local Baz = world:component()
local e = world:entity()
local q = world:query(Foo, Bar):without(Baz):cached()
world:set(e, Foo, true)
world:set(e, Bar, false)
world:set(e, Baz, true)
for _, e in q do
CHECK(true)
end
CHECK(#q:archetypes() == 1)
CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")]))
world:delete(Foo)
CHECK(#q:archetypes() == 0)
end
do CASE("multiple iter") do CASE("multiple iter")
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
@ -813,40 +835,6 @@ TEST("world:query()", function()
CHECK(withoutCount == 0) CHECK(withoutCount == 0)
end end
do
CASE("Empty Query")
do
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:add(e1, A)
local query = world:query(B)
CHECK(query:without() == query)
CHECK(query:with() == query)
-- They always return the same EMPTY_LIST
CHECK(query:archetypes() == world:query(B):archetypes())
end
do
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:add(e1, A)
local count = 0
for id in world:query(B) do
count += 1
end
CHECK(count == 0)
end
end
do do
CASE("without") CASE("without")
do do