From 76d1ff91c0f7a5b17f393d86782695a7a6500066 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 23 Jul 2024 04:44:56 +0200 Subject: [PATCH 1/7] Add has function (#85) --- src/init.luau | 27 +++++++++++++++++++++++++++ test/tests.luau | 28 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/init.luau b/src/init.luau index 4ccb761..521a551 100644 --- a/src/init.luau +++ b/src/init.luau @@ -669,6 +669,32 @@ do end end +local world_has: () -> boolean +do + function world_has(world, entity_id, ...) + local id = entity_id + local record = world.entityIndex.sparse[id] + if not record then + return false + end + + local archetype = record.archetype + if not archetype then + return false + end + + local tr = archetype.records + + for i = 1, select("#", ...) do + if not tr[select(i, ...)] then + return false + end + end + + return true + end +end + type Item = () -> (number, ...any) export type Query = typeof({ __iter = function(): Item @@ -1077,6 +1103,7 @@ World.component = world_component World.add = world_add World.set = world_set World.get = world_get +World.has = world_has World.target = world_target World.parent = world_parent diff --git a/test/tests.luau b/test/tests.luau index 67559cb..508648c 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -530,6 +530,34 @@ TEST("world", function() CHECK(withoutCount == 0) end + + do CASE "should find Tag on entity" + local world = jecs.World.new() + + local Tag = world:component() + + local e = world:entity() + world:add(e, Tag) + + CHECK(world:has(e, Tag)) + end + + do CASE "should return false when missing one tag" + local world = jecs.World.new() + + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + + local e = world:entity() + world:add(e, A) + world:add(e, C) + world:add(e, D) + + CHECK(world:has(e, A, B, C, D) == false) + end + end) From 0f8f9348f527ba6e5f67f237fc13e201cc40256c Mon Sep 17 00:00:00 2001 From: Marcus Date: Fri, 26 Jul 2024 02:55:36 +0200 Subject: [PATCH 2/7] Fix column pointers invalidated by upvalues (#88) * Initial commit * Add types to world:has() --- .gitignore | 4 +- src/init.luau | 329 +++++++++++++++++++++++++----------------------- test/tests.luau | 6 +- 3 files changed, 180 insertions(+), 159 deletions(-) diff --git a/.gitignore b/.gitignore index c3a366f..2f97ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,7 @@ WallyPatches # Misc roblox.toml sourcemap.json -drafts/*.lua +drafts/ # Cached Vitepress (docs) @@ -61,4 +61,4 @@ drafts/*.lua /docs/.vitepress/dist .vitepress/cache -.vitepress/dist \ No newline at end of file +.vitepress/dist diff --git a/src/init.luau b/src/init.luau index 521a551..303dcde 100644 --- a/src/init.luau +++ b/src/init.luau @@ -1,4 +1,3 @@ - --!optimize 2 --!native --!strict @@ -17,6 +16,7 @@ type ArchetypeEdge = { remove: Archetype, } + type Archetype = { id: number, edges: { [i53]: ArchetypeEdge }, @@ -669,7 +669,8 @@ do end end -local world_has: () -> boolean +local world_has: (world: World, entityId: number, ...i53) -> boolean + do function world_has(world, entity_id, ...) local id = entity_id @@ -704,8 +705,8 @@ export type Query = typeof({ end, }) & { next: Item, - replace: (Query, ...any) -> (), - without: (Query) -> Query + without: (Query) -> Query, + replace: (Query, (...any) -> (...any)) -> () } type CompatibleArchetype = { archetype: Archetype, indices: { number } } @@ -714,135 +715,145 @@ local world_query: (World, ...i53) -> Query do local noop: Item = function() - return nil :: any + return nil :: any end local EmptyQuery: Query = { - __iter = function(): Item - return noop - end, - next = noop :: Item, - replace = noop :: (Query, ...any) -> (), - without = function(self: Query, ...) - return self - end + __iter = function(): Item + return noop + end, + next = noop :: Item, + replace = noop :: (Query, ...any) -> (), + without = function(self: Query, ...) + return self + end } setmetatable(EmptyQuery, EmptyQuery) - local indices: { { number } } - local compatibleArchetypes: { Archetype } - local length - local components: { number } - local queryLength: number local lastArchetype: number local archetype: Archetype + local queryOutput: { any } + local queryLength: number + local entities: { number } + local i: number - local queryOutput: { any } + local compatible_archetypes: { Archetype } + local column_indices: { { number} } + local ids: { number } - local entities: {} - local i: number - - local function world_query_next() + local function world_query_next(): any local entityId = entities[i] - while entityId == nil do + while entityId == nil do lastArchetype += 1 - archetype = compatibleArchetypes[lastArchetype] - + archetype = compatible_archetypes[lastArchetype] if not archetype then - return - end - - entities = archetype.entities + return nil + end + entities = archetype.entities i = #entities - entityId = entities[i] - end + entityId = entities[i] + end - local row = i - i-=1 + local row = i + i-=1 - local columns = archetype.columns - local tr = indices[lastArchetype] + local columns = archetype.columns + local tr = column_indices[lastArchetype] - if queryLength == 1 then - return entityId, columns[tr[1]][row] - elseif queryLength == 2 then - return entityId, columns[tr[1]][row], columns[tr[2]][row] - elseif queryLength == 3 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] - elseif queryLength == 4 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] - elseif queryLength == 5 then - return entityId,columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row] - elseif queryLength == 6 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row] - elseif queryLength == 7 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row] - elseif queryLength == 8 then - return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], - columns[tr[5]][row], - columns[tr[6]][row], - columns[tr[7]][row], - columns[tr[8]][row] - end + if queryLength == 1 then + return entityId, columns[tr[1]][row] + elseif queryLength == 2 then + return entityId, columns[tr[1]][row], columns[tr[2]][row] + elseif queryLength == 3 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] + elseif queryLength == 4 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] + elseif queryLength == 5 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row] + elseif queryLength == 6 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row] + elseif queryLength == 7 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row] + elseif queryLength == 8 then + return entityId, + columns[tr[1]][row], + columns[tr[2]][row], + columns[tr[3]][row], + columns[tr[4]][row], + columns[tr[5]][row], + columns[tr[6]][row], + columns[tr[7]][row], + columns[tr[8]][row] + end - for i in components do - queryOutput[i] = columns[tr[i]][row] - end + for j in ids do + queryOutput[j] = columns[tr[j]][row] + end - return entityId, unpack(queryOutput, 1, queryLength) + return entityId, unpack(queryOutput, 1, queryLength) end - local function world_query_without(self, ...): Query + local function world_query_iter() + return world_query_next + end + + local function world_query_without(self, ...) local withoutComponents = { ... } - for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i] + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] local records = archetype.records local shouldRemove = false for _, componentId in withoutComponents do - if records[componentId] then - shouldRemove = true - break - end + if records[componentId] then + shouldRemove = true + break + end end if shouldRemove then - table.remove(compatibleArchetypes, i) + table.remove(compatible_archetypes, i) end - end + end - if #compatibleArchetypes == 0 then - return EmptyQuery - end - - return self - end - - local function world_query_iter() lastArchetype = 1 - archetype = compatibleArchetypes[1] - entities = archetype.entities - i = #entities + archetype = compatible_archetypes[lastArchetype] - return world_query_next + if not archetype then + return EmptyQuery + end + + return self end local function world_query_replace_values(row, columns, ...) - for i, column in columns do - column[row] = select(i, ...) - end + for i, column in columns do + column[row] = select(i, ...) + end end - local function world_query_replace(_, fn: any) - for i, archetype in compatibleArchetypes do - local tr = indices[i] + local function world_query_replace(_, fn: (...any) -> (...any)) + for i, archetype in compatible_archetypes do + local tr = column_indices[i] local columns = archetype.columns for row in archetype.entities do @@ -881,80 +892,88 @@ do end end - function world_query(world: World, ...: number): Query - -- breaking? - if (...) == nil then - error("Missing components") - end - indices = {} - compatibleArchetypes = {} - length = 0 - components = { ... } + function world_query(world: World, ...: any): Query + -- breaking? + if (...) == nil then + error("Missing components") + end - local archetypes: { Archetype } = world.archetypes :: any - local firstArchetypeMap: ArchetypeMap - local componentIndex = world.componentIndex + local indices = {} + local compatibleArchetypes = {} + local length = 0 - for _, componentId in components do - local map: ArchetypeMap = componentIndex[componentId] :: any - if not map then - return EmptyQuery - end + local components = { ... } :: any + local archetypes = world.archetypes - if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size > map.size then - firstArchetypeMap = map - end - end + local firstArchetypeMap: ArchetypeMap + local componentIndex = world.componentIndex - for id in firstArchetypeMap.cache do - local compatibleArchetype = archetypes[id] - local archetypeRecords = compatibleArchetype.records + for _, componentId in components do + local map = componentIndex[componentId] + if not map then + return EmptyQuery + end - local records: { number } = {} - local skip = false + if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then + firstArchetypeMap = map + end + end - for i, componentId in components do - local index = archetypeRecords[componentId] - if not index then - skip = true - break - end - -- index should be index.offset - records[i] = index - end + for id in firstArchetypeMap.cache do + local compatibleArchetype = archetypes[id] + local archetypeRecords = compatibleArchetype.records - if skip then - continue - end + local records = {} + local skip = false - length += 1 - compatibleArchetypes[length] = compatibleArchetype - indices[length] = records - end + for i, componentId in components do + local index = archetypeRecords[componentId] + if not index then + skip = true + break + end + -- index should be index.offset + records[i] = index + end - lastArchetype = 1 - archetype = compatibleArchetypes[lastArchetype] + if skip then + continue + end - if not archetype then - return EmptyQuery - end + length += 1 + compatibleArchetypes[length] = compatibleArchetype + indices[length] = records + end - queryOutput = {} - queryLength = #components + compatible_archetypes = compatibleArchetypes + column_indices = indices + ids = components - entities = archetype.entities - i = #entities + lastArchetype = 1 + archetype = compatible_archetypes[lastArchetype] - local it = { - __iter = world_query_iter, - next = world_query_next, - without = world_query_without, - replace = world_query_replace - } + if not archetype then + return EmptyQuery + end - return setmetatable(it, it) :: any - end + queryOutput = {} + queryLength = #ids + + entities = archetype.entities + i = #entities + + local it = { + __iter = world_query_iter, + next = world_query_next, + without = world_query_without, + replace = world_query_replace + } :: any + + setmetatable(it, it) + + return it + end end type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) @@ -997,11 +1016,13 @@ export type WorldShim = typeof(setmetatable( --- Removes a component from the given entity remove: (WorldShim, id: Entity, component: Entity) -> (), --- Retrieves the value of up to 4 components. These values may be nil. - get: ((WorldShim, id: any, Entity) -> A) + get: ((WorldShim, id: Entity, Entity) -> A) & ((WorldShim, id: Entity, Entity, Entity) -> (A, B)) & ((WorldShim, id: Entity, Entity, Entity, Entity) -> (A, B, C)) & (WorldShim, id: Entity, Entity, Entity, Entity, Entity) -> (A, B, C, D), + has: (WorldShim, Entity, ...Entity) -> boolean, + --- Searches the world for entities that match a given query query: ((WorldShim, Entity) -> QueryShim) & ((WorldShim, Entity, Entity) -> QueryShim) diff --git a/test/tests.luau b/test/tests.luau index 508648c..6110be0 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -60,10 +60,9 @@ TEST("world", function() world:clear(e) CHECK(world:get(e, A) == nil) CHECK(world:get(e, B) == nil) - end - do CASE("iterator should not drain the query") + do CASE("should drain query while iterating") local world = jecs.World.new() :: World local A = world:component() local B = world:component() @@ -85,7 +84,8 @@ TEST("world", function() for _ in q do j+=1 end - CHECK(i == j) + CHECK(i == 2) + CHECK(j == 0) end do CASE("should be able to get next results") From 65984b6c65103de603b0372ead2e0ec00214d923 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 26 Jul 2024 03:03:00 +0200 Subject: [PATCH 3/7] Remove indices --- src/init.luau | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.luau b/src/init.luau index 303dcde..c7e5c74 100644 --- a/src/init.luau +++ b/src/init.luau @@ -670,7 +670,6 @@ do end local world_has: (world: World, entityId: number, ...i53) -> boolean - do function world_has(world, entity_id, ...) local id = entity_id @@ -832,6 +831,7 @@ do if shouldRemove then table.remove(compatible_archetypes, i) + table.remove(column_indices, i) end end From 059a3f9bed70faa8798d6074cf8bbc1ae524a9a5 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 26 Jul 2024 04:36:30 +0200 Subject: [PATCH 4/7] Fix world:remove --- docs/api.md | 59 ----------------- docs/api/query.md | 132 ++++++++++++++++++++++++++++++++++++++ docs/api/world.md | 70 ++++++++++++++++++++ docs/concepts/entities.md | 2 - src/init.luau | 1 + test/tests.luau | 7 ++ 6 files changed, 210 insertions(+), 61 deletions(-) delete mode 100644 docs/api.md create mode 100644 docs/api/query.md create mode 100644 docs/api/world.md diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index d11eb98..0000000 --- a/docs/api.md +++ /dev/null @@ -1,59 +0,0 @@ -# API - -## World - -### World.new() -> `World` -Creates a new world. - -Example: -::: code-group - -```luau [luau] -local world = jecs.World.new() -``` - -```ts [typescript] -import { World } from "@rbxts/jecs"; - -const world = new World(); -``` - -::: - -### world:entity() -> `Entity` -Creates a new entity. - -Example: -::: code-group - -```luau [luau] -local entity = world:entity() -``` - -```ts [typescript] -const entity = world.entity(); -``` - -::: - -### world:component() -> `Entity` -Creates a new static component. Keep in mind that components are also entities. - -Example: -::: code-group - -```luau [luau] -local Health = world:component() -``` - -```ts [typescript] -const Health = world.component(); -``` - -::: - -::: info -You should use this when creating static components. - -For example, a generic Health entity should be created using this. -::: \ No newline at end of file diff --git a/docs/api/query.md b/docs/api/query.md new file mode 100644 index 0000000..e7702fa --- /dev/null +++ b/docs/api/query.md @@ -0,0 +1,132 @@ +# Query + +A World contains entities which have components. The World is queryable and can be used to get entities with a specific set of components. + +## Functions + +### new() +```luau +function World.new(): World +``` +Creates a new world. + +Example: +::: code-group + +```luau [luau] +local world = jecs.World.new() +``` + +```ts [typescript] +import { World } from "@rbxts/jecs"; + +const world = new World(); +``` + +::: + +## entity() +```luau +function World:entity(): Entity -- The new entit. +``` +Creates a new entity. + +Example: +::: code-group + +```luau [luau] +local entity = world:entity() +``` + +```ts [typescript] +const entity = world.entity(); +``` + +:: +: + +### component() +```luau +function World:component(): Entity -- The new componen. +``` +Creates a new component. + +Example: +::: code-group + +```luau [luau] +local Health = world:component() :: jecs.Entity +``` + +```ts [typescript] +const Health = world.component(); +``` +::: + +::: info +You should use this when creating components. + +For example, a Health type should be created using this. +::: + +### get() +```luau +function World:get( + entity: Entity, -- The entity + ...: Entity -- The types to fetch +): ... -- Returns the component data in the same order they were passed in +``` +Returns the data for each provided type for the corresponding entity. + +::: + +### add() +```luau +function World:add( + entity: Entity, -- The entity + id: Entity -- The component ID to add +): () +``` +Adds a component ID to the entity. + +This operation adds a single (component) id to an entity. + +::: info +This function is idempotent, meaning if the entity already has the id, this operation will have no side effects. +::: + + +### set() +```luau +function World:set( + entity: Entity, -- The entity + id: Entity, -- The component ID to set + data: T -- The data of the component's type +): () +``` +Adds or changes the entity's component. + +### query() +```luau +function World:query( + ...: Entity -- The component IDs to query with. Entities that satifies the conditions will be returned +): Query<...Entity> -- Returns the Query which gets the entity and their corresponding data when iterated +``` +Creates a [`query`](query) with the given component IDs. + +Example: +::: code-group + +```luau [luau] +for id, position, velocity in world:query(Position, Velocity) do + -- Do something +end +``` + +```ts [typescript] +for (const [id, position, velocity] of world.query(Position, Velocity) { + // Do something +} +``` + +::: diff --git a/docs/api/world.md b/docs/api/world.md new file mode 100644 index 0000000..96f7fb3 --- /dev/null +++ b/docs/api/world.md @@ -0,0 +1,70 @@ +# World + +A World contains entities which have components. The World is queryable and can be used to get entities with a specific set of components. + +## Functions + +### new() +```lua +function World.new(): World +``` +Creates a new world. + +Example: +::: code-group + +```luau [luau] +local world = jecs.World.new() +``` + +```ts [typescript] +import { World } from "@rbxts/jecs"; + +const world = new World(); +``` + +::: + +## entity() +```luau +function World:entity(): Entity +``` +Creates a new entity. + +Example: +::: code-group + +```luau [luau] +local entity = world:entity() +``` + +```ts [typescript] +const entity = world.entity(); +``` + +::: + +### component()` +```luau +function World:component(): Entity +``` +Creates a new component. + +Example: +::: code-group + +```luau [luau] +local Health = world:component() :: jecs.Entity +``` + +```ts [typescript] +const Health = world.component(); +``` + +::: + +::: info +You should use this when creating components. + +For example, a Health type should be created using this. +::: diff --git a/docs/concepts/entities.md b/docs/concepts/entities.md index 2e03428..d3f5a12 100644 --- a/docs/concepts/entities.md +++ b/docs/concepts/entities.md @@ -1,3 +1 @@ -## TODO -This is a TODO stub. \ No newline at end of file diff --git a/src/init.luau b/src/init.luau index c7e5c74..b2f4b5d 100644 --- a/src/init.luau +++ b/src/init.luau @@ -512,6 +512,7 @@ end local function archetype_traverse_remove(world: World, componentId: i53, from: Archetype): Archetype + from = from or world.ROOT_ARCHETYPE local edge = edge_ensure(from, componentId) local remove = edge.remove diff --git a/test/tests.luau b/test/tests.luau index 6110be0..f7807d3 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -25,6 +25,13 @@ local N = 10 type World = jecs.WorldShim TEST("world", function() + do CASE "should not error when removing a component that entity doesn't have" + local world = jecs.World.new() :: World + local A = world:component() + local e = world:entity() + world:remove(e, A) + CHECK(true) + end do CASE("should find every component id") local world = jecs.World.new() :: World local A = world:component() From 1844b0cfd87ccdbb85b5c2a003407ef248cd0ef5 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 26 Jul 2024 04:45:07 +0200 Subject: [PATCH 5/7] Amend test --- test/tests.luau | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/test/tests.luau b/test/tests.luau index f7807d3..adee56e 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -25,13 +25,24 @@ local N = 10 type World = jecs.WorldShim TEST("world", function() - do CASE "should not error when removing a component that entity doesn't have" - local world = jecs.World.new() :: World - local A = world:component() - local e = world:entity() - world:remove(e, A) - CHECK(true) - end + do CASE "should allow remove a component that doesn't exist on entity" + local world = jecs.World.new() + + local Health = world:entity() + local Poison = world:component() + + local id = world:entity() + do + world:remove(id, Poison) + CHECK(true) -- Didn't error + end + + world:set(id, Health, 50) + world:remove(id, Poison) + + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == 50) + end do CASE("should find every component id") local world = jecs.World.new() :: World local A = world:component() @@ -231,20 +242,6 @@ TEST("world", function() CHECK(world:get(id1, Health) == 50) end - do CASE("should allow remove that doesn't exist on entity") - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Health, 50) - world:remove(id, Poison) - - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == 50) - end - do CASE("should increment generation") local world = jecs.World.new() local e = world:entity() From f9e0aa010cc68204ae15bb13e5a9d5d2e96c0dd6 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 26 Jul 2024 04:50:08 +0200 Subject: [PATCH 6/7] Fix api pages --- docs/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 6c4d72c..953fa01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,8 +13,8 @@ hero: text: Get Started link: /overview/get-started.md - theme: alt - text: API Examples - link: /api.md + text: API References + link: /api/ features: - title: Stupidly Fast @@ -26,4 +26,4 @@ features: - title: Zero-Dependencies icon: 📦 details: Jecs doesn't rely on anything other than itself. ---- \ No newline at end of file +--- From 2f0f817b0835d07d0f03daf9e0b5ab9474c93d46 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 26 Jul 2024 16:15:12 +0200 Subject: [PATCH 7/7] Optimize remove --- src/init.luau | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/init.luau b/src/init.luau index b2f4b5d..c4231d8 100644 --- a/src/init.luau +++ b/src/init.luau @@ -512,7 +512,6 @@ end local function archetype_traverse_remove(world: World, componentId: i53, from: Archetype): Archetype - from = from or world.ROOT_ARCHETYPE local edge = edge_ensure(from, componentId) local remove = edge.remove @@ -534,6 +533,9 @@ local function world_remove(world: World, entityId: i53, componentId: i53) local entityIndex = world.entityIndex local record = entityIndex.sparse[entityId] local sourceArchetype = record.archetype + if not sourceArchetype then + return + end local destinationArchetype = archetype_traverse_remove(world, componentId, sourceArchetype) if sourceArchetype and not (sourceArchetype == destinationArchetype) then