Compare commits

...

5 commits

Author SHA1 Message Date
PepeElToro41
fb8f8bddc1
Merge f912866fcb into 5de842d144 2025-08-23 20:30:15 +03:00
Ukendio
5de842d144 query should use internal world type
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-08-23 18:33:53 +02:00
Ukendio
9d1665944e world:added should not union the parameterized type with nil 2025-08-22 18:02:58 +02:00
Ukendio
8ace046470 Optimize queries 2025-08-22 17:54:35 +02:00
PepeElToro41
f912866fcb add way to check preregistered id creations after world creation 2025-07-29 14:18:15 -06:00
5 changed files with 863 additions and 660 deletions

View file

@ -603,12 +603,26 @@ function World:exists(
## cleanup ## cleanup
Cleans up deleted entities and their associated data. This is automatically called by jecs, but can be called manually if needed. Cleans up empty archetypes.
```luau ```luau
function World:cleanup(): void function World:cleanup(): void
``` ```
:::info
It is recommended to profile the optimal interval you should cleanup because it varies completely from game to game.
Here are a couple of reasons from Sander Mertens:
- some applications are memory constrained, so any wasted memory on empty
archetypes has to get cleaned up
- many archetypes can get created during game startup but aren't used later
on, so it would be wasteful to keep them around
- empty archetypes can slow queries down, especially if there are many more
empty ones than non-empty ones
- if the total number of component permutations (/relationships) is too
high, you have no choice but to periodically cleanup empty archetypes
:::
Example: Example:
::: code-group ::: code-group

278
jecs.luau
View file

@ -74,7 +74,7 @@ type query = {
filter_with: { i53 }, filter_with: { i53 },
filter_without: { i53 }, filter_without: { i53 },
next: () -> (i53, ...any), next: () -> (i53, ...any),
world: World, world: world,
} }
export type observer = { export type observer = {
@ -126,6 +126,8 @@ type world = {
max_component_id: number, max_component_id: number,
max_archetype_id: number, max_archetype_id: number,
start_component_id: number,
start_tag_id: number,
observable: Map<i53, Map<i53, { Observer }>>, observable: Map<i53, Map<i53, { Observer }>>,
@ -163,10 +165,12 @@ export type World = {
max_component_id: number, max_component_id: number,
max_archetype_id: number, max_archetype_id: number,
start_component_id: number,
start_tag_id: number,
observable: Map<Id, Map<Id, { Observer }>>, observable: Map<Id, Map<Id, { Observer }>>,
added: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T?) -> ()) -> () -> (), added: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (),
removed: <T>(World, Entity<T>, (e: Entity, id: Id<T>) -> ()) -> () -> (), removed: <T>(World, Entity<T>, (e: Entity, id: Id<T>) -> ()) -> () -> (),
changed: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (), changed: <T>(World, Entity<T>, <e>(e: Entity<e>, id: Id<T>, value: T) -> ()) -> () -> (),
@ -760,6 +764,12 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean
return first == EcsWildcard or second == EcsWildcard return first == EcsWildcard or second == EcsWildcard
end end
local function get_max_ids_difference(world: World): (number, number)
local diff_components = world.start_component_id - ecs_max_component_id
local diff_tags = world.start_tag_id - ecs_max_tag_id
return diff_components, diff_tags
end
local function id_record_get(world: World, id: Entity): ComponentRecord? local function id_record_get(world: World, id: Entity): ComponentRecord?
local component_index = world.component_index local component_index = world.component_index
local idr: ComponentRecord = component_index[id] local idr: ComponentRecord = component_index[id]
@ -926,9 +936,9 @@ local function archetype_create(world: world, id_types: { i53 }, ty, prev: i53?)
end end
end end
world.archetype_index[archetype.type] = archetype world.archetype_index[ty] = archetype
world.archetypes[archetype_id] = archetype world.archetypes[archetype_id] = archetype
world.archetype_edges[archetype.id] = {} :: Map<i53, archetype> world.archetype_edges[archetype_id] = {} :: Map<i53, archetype>
for id in columns_map do for id in columns_map do
local observer_list = find_observers(world, EcsOnArchetypeCreate, id) local observer_list = find_observers(world, EcsOnArchetypeCreate, id)
@ -1175,11 +1185,86 @@ end
local function NOOP() end local function NOOP() end
local function query_archetypes(query: query)
local compatible_archetypes = query.compatible_archetypes
if not compatible_archetypes then
compatible_archetypes = {}
query.compatible_archetypes = compatible_archetypes
local world = query.world
local archetypes = world.archetypes
local component_index = world.component_index
local idr: componentrecord?
local with = query.filter_with
for _, id in with do
local map = component_index[id]
if not map then
continue
end
if idr == nil or (map.size :: number) < (idr.size :: number) then
idr = map
end
end
if idr == nil then
return compatible_archetypes
end
local without = query.filter_without
for archetype_id in idr.records do
local archetype = archetypes[archetype_id]
local columns_map = archetype.columns_map
local skip = false
for _, component in with do
if not columns_map[component] then
skip = true
break
end
end
if skip then
continue
end
if without then
for _, component in without do
if columns_map[component] then
skip = true
break
end
end
end
if skip then
continue
end
table.insert(compatible_archetypes, archetype)
end
end
return compatible_archetypes
end
local function query_with(query: query, ...: i53)
local ids = query.ids
local with = { ... }
table.move(ids, 1, #ids, #with + 1, with)
query.filter_with = with
return query
end
local function query_without(query: query, ...: i53)
local without = { ... }
query.filter_without = without
return query
end
local function query_iter_init(query: QueryInner): () -> (number, ...any) local function query_iter_init(query: QueryInner): () -> (number, ...any)
local world_query_iter_next local world_query_iter_next
local compatible_archetypes = query.compatible_archetypes local compatible_archetypes = query_archetypes(query::any) :: { Archetype }
local lastArchetype = 1 local lastArchetype = 1
local archetype = compatible_archetypes[1] local archetype = compatible_archetypes[1]
if not archetype then if not archetype then
@ -1252,9 +1337,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1277,9 +1359,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1303,9 +1382,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1330,9 +1406,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1358,9 +1431,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1387,9 +1457,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1417,9 +1484,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1448,9 +1512,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1482,9 +1543,6 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1520,84 +1578,8 @@ local function query_iter(query): () -> (number, ...any)
return query_next return query_next
end end
local function query_without(query: QueryInner, ...: Id)
local without = { ... }
query.filter_without = without :: any
local compatible_archetypes = query.compatible_archetypes
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local columns_map = archetype.columns_map
local matches = true
for _, id in without do
if columns_map[id] then
matches = false
break
end
end
if matches then
continue
end
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end
return query :: any
end
local function query_with(query: QueryInner, ...: Id)
local compatible_archetypes = query.compatible_archetypes
local with = { ... } :: any
query.filter_with = with
for i = #compatible_archetypes, 1, -1 do
local archetype = compatible_archetypes[i]
local columns_map = archetype.columns_map
local matches = true
for _, id in with do
if not columns_map[id] then
matches = false
break
end
end
if matches then
continue
end
local last = #compatible_archetypes
if last ~= i then
compatible_archetypes[i] = compatible_archetypes[last]
end
compatible_archetypes[last] = nil :: any
end
return query :: any
end
-- Meant for directly iterating over archetypes to minimize
-- function call overhead. Should not be used unless iterating over
-- hundreds of thousands of entities in bulk.
local function query_archetypes(query)
return query.compatible_archetypes
end
local function query_cached(query: QueryInner) local function query_cached(query: QueryInner)
local with = query.filter_with
local ids = query.ids local ids = query.ids
if with then
table.move(ids, 1, #ids, #with + 1, with)
else
query.filter_with = ids
end
local compatible_archetypes = query.compatible_archetypes
local lastArchetype = 1 local lastArchetype = 1
local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id }) local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id })
@ -1609,7 +1591,8 @@ local function query_cached(query: QueryInner)
local i: number local i: number
local archetype: Archetype local archetype: Archetype
local columns_map: { [Id]: Column } local columns_map: { [Id]: Column }
local archetypes = query.compatible_archetypes local archetypes = query_archetypes(query :: any)
local compatible_archetypes = archetypes :: { Archetype }
local world = query.world local world = query.world
-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
@ -1727,9 +1710,6 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1752,9 +1732,6 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1778,9 +1755,6 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1805,9 +1779,6 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1892,10 +1863,6 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
b = columns_map[B] b = columns_map[B]
@ -1923,9 +1890,6 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -1957,9 +1921,6 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
if i == 0 then
continue
end
entity = entities[i] entity = entities[i]
columns_map = archetype.columns_map columns_map = archetype.columns_map
a = columns_map[A] a = columns_map[A]
@ -2001,65 +1962,16 @@ Query.archetypes = query_archetypes
Query.cached = query_cached Query.cached = query_cached
local function world_query(world: World, ...) local function world_query(world: World, ...)
local compatible_archetypes = {}
local length = 0
local ids = { ... } local ids = { ... }
local archetypes = world.archetypes
local idr: ComponentRecord?
local component_index = world.component_index
local q = setmetatable({ local q = setmetatable({
ids = ids, ids = ids,
compatible_archetypes = compatible_archetypes, filter_with = ids,
world = world, world = world,
}, Query) }, Query)
for _, id in ids do
local map = component_index[id]
if not map then
return q return q
end end
if idr == nil or (map.size :: number) < (idr.size :: number) then
idr = map
end
end
if idr == nil then
return q
end
for archetype_id in idr.records do
local compatibleArchetype = archetypes[archetype_id]
if #compatibleArchetype.entities == 0 then
continue
end
local columns_map = compatibleArchetype.columns_map
local skip = false
for i, id in ids do
local column = columns_map[id]
if not column then
skip = true
break
end
end
if skip then
continue
end
length += 1
compatible_archetypes[length] = compatibleArchetype
end
return q
end
local function world_each(world: world, id: i53): () -> i53 local function world_each(world: world, id: i53): () -> i53
local idr = world.component_index[id] local idr = world.component_index[id]
if not idr then if not idr then
@ -2260,6 +2172,9 @@ local function world_new()
max_archetype_id = 0, max_archetype_id = 0,
max_component_id = ecs_max_component_id, max_component_id = ecs_max_component_id,
start_component_id = ecs_max_component_id,
start_tag_id = ecs_max_tag_id,
observable = observable, observable = observable,
signals = signals, signals = signals,
} :: world } :: world
@ -3361,6 +3276,7 @@ return {
pair_first = ecs_pair_first :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>, pair_first = ecs_pair_first :: <P, O>(world: World, pair: Pair<P, O>) -> Id<P>,
pair_second = ecs_pair_second :: <P, O>(world: World, pair: Pair<P, O>) -> Id<O>, pair_second = ecs_pair_second :: <P, O>(world: World, pair: Pair<P, O>) -> Id<O>,
entity_index_get_alive = entity_index_get_alive, entity_index_get_alive = entity_index_get_alive,
get_max_ids_difference = get_max_ids_difference,
archetype_append_to_records = archetype_append_to_records, archetype_append_to_records = archetype_append_to_records,
id_record_ensure = id_record_ensure :: (World, Id) -> ComponentRecord, id_record_ensure = id_record_ensure :: (World, Id) -> ComponentRecord,

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "@rbxts/jecs", "name": "@rbxts/jecs",
"version": "0.9.0-rc.11", "version": "0.9.0-rc.12",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "repository": {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ukendio/jecs" name = "ukendio/jecs"
version = "0.9.0-rc.11" version = "0.9.0-rc.12"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"