This commit is contained in:
Marcus 2024-12-24 08:24:57 +01:00 committed by GitHub
commit ee3486f170
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 173 additions and 128 deletions

247
jecs.luau
View file

@ -89,7 +89,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 EcsTableCreate = HI_COMPONENT_ID + 12
local EcsRest = HI_COMPONENT_ID + 13
local ECS_PAIR_FLAG = 0x8 local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10 local ECS_ID_FLAGS_MASK = 0x10
@ -250,6 +251,57 @@ 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(query, archetype)
local records = archetype.records
for _, id in query.ids do
if not records[id] then
return false
end
end
local filters = query.filters
if filters then
local without = filters.without
if without then
for _, id in filters.without do
if records[id] then
return false
end
end
end
local with = filters.with
if with then
for _, id in filters.without do
if not records[id] then
return false
end
end
end
end
return true
end
local function observer_invoke(observer, event)
table.insert(observer.query.compatible_archetypes, event.archetype)
end
local function emit(world: World, event)
local map = world.observerable[event.id]
if not map then
return
end
local observer_list: {[string]: any} = map[event.component]
if not observer_list then
return
end
for _, observer in observer_list do
if query_match(observer.query, event.archetype) then
observer_invoke(observer, event)
end
end
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
@ -535,15 +587,49 @@ local function archetype_append_to_records(
end end
end end
local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): Archetype local function create_observer_uni(world: World, component: number, event)
local map = world.observerable[event]
if not map then
map = {}
world.observerable[event] = map
end
local observer_list = map[component]
if not observer_list then
observer_list = {}
map[component] = observer_list
end
local observer = {}
table.insert(observer_list, observer)
return observer
end
local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype
local archetype_id = (world.nextArchetypeId :: number) + 1 local archetype_id = (world.nextArchetypeId :: number) + 1
world.nextArchetypeId = archetype_id world.nextArchetypeId = archetype_id
local length = #types local length = #id_types
local columns = (table.create(length) :: any) :: { Column } local columns = (table.create(length) :: any) :: { Column }
local records: { ArchetypeRecord } = {} local records: { ArchetypeRecord } = {}
for i, componentId in types do
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
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)
@ -564,20 +650,12 @@ local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): A
else else
columns[i] = NULL_ARRAY columns[i] = NULL_ARRAY
end end
end end
local archetype: Archetype = { for _, id in id_types do
columns = columns, emit(world, { id = EcsTableCreate, component = id, archetype = archetype})
entities = {}, end
id = archetype_id,
records = records,
type = ty,
types = types,
add = {},
remove = {},
refs = {} :: GraphEdge,
}
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[archetype_id] = archetype world.archetypes[archetype_id] = archetype
@ -1409,32 +1487,35 @@ local function query_iter(query): () -> (number, ...any)
end end
local function query_without(query: { compatible_archetypes: { Archetype } }, ...) local function query_without(query: { compatible_archetypes: { Archetype } }, ...)
local filters = query.filters
local without = { ... }
if not filters then
filters = {}
query.filters = filters
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
@ -1442,31 +1523,36 @@ end
local function query_with(query: { compatible_archetypes: { Archetype } }, ...) local function query_with(query: { compatible_archetypes: { Archetype } }, ...)
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query.compatible_archetypes
local N = select("#", ...) local filters = query.filters
local with = { ... }
if not filters then
filters = {}
query.filters = filters
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
@ -1477,6 +1563,12 @@ local function query_archetypes(query)
return query.compatible_archetypes return query.compatible_archetypes
end end
local function query_cached(query)
local observer = create_observer_uni(query.world, query.ids[1], EcsTableCreate)
observer.query = query
return query
end
local Query = {} local Query = {}
Query.__index = Query Query.__index = Query
Query.__iter = query_iter Query.__iter = query_iter
@ -1484,6 +1576,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 = {}
@ -1496,10 +1589,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
@ -1508,7 +1607,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
@ -1536,15 +1635,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
@ -1730,6 +1820,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, {}, "")
@ -1793,13 +1884,14 @@ 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...>
} }
export type World = { export type World = {
@ -1812,6 +1904,8 @@ export type World = {
nextComponentId: number, nextComponentId: number,
nextEntityId: number, nextEntityId: number,
nextArchetypeId: number, nextArchetypeId: number,
observerable: { [string]: { [Id]: { query: Query<i53> } } }
} & { } & {
--- Creates a new entity --- Creates a new entity
entity: (self: World) -> Entity, entity: (self: World) -> Entity,
@ -1854,42 +1948,7 @@ export type World = {
children: (self: World, id: Id) -> () -> Entity, children: (self: World, id: Id) -> () -> Entity,
--- Searches the world for entities that match a given query --- Searches the world for entities that match a given query
query: (<A>(self: World, Id<A>) -> Query<A>) query: (<A>(self: World, a: { __T: A }) -> Query<A>)
& (<A, B>(self: World, Id<A>, Id<B>) -> Query<A, B>)
& (<A, B, C>(self: World, Id<A>, Id<B>, Id<C>) -> Query<A, B, C>)
& (<A, B, C, D>(self: World, Id<A>, Id<B>, Id<C>, Id<D>) -> Query<A, B, C, D>)
& (<A, B, C, D, E>(self: World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>) -> Query<A, B, C, D, E>)
& (<A, B, C, D, E, F>(
self: 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>(
self: 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>(
self: 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>),
} }
return { return {

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
@ -353,6 +357,22 @@ 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}, "_")]))
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()
@ -808,40 +828,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