From 0fe23e151c39359c1762072cb6aae8a551ea14d9 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 7 Jul 2024 04:53:17 +0200 Subject: [PATCH] Add replace method to query (#46) * Add replace function * Add next method * Remove tostring * Fix EmptyQuery * add replace method * merge conflicts * return self in without * Make aliases relative * Add test * add to changelog --- .luaurc | 4 +- CHANGELOG.md | 26 +++++----- benches/query.luau | 1 + src/init.luau | 121 ++++++++++++++++++++++++++++++++------------- tests/world.luau | 21 +++++++- 5 files changed, 120 insertions(+), 53 deletions(-) diff --git a/.luaurc b/.luaurc index ca68025..c0ecdb0 100644 --- a/.luaurc +++ b/.luaurc @@ -1,6 +1,6 @@ { "aliases": { - "jecs": "C:/Users/Marcus/Documents/packages/jecs/src", - "testkit": "C:/Users/Marcus/Documents/packages/jecs/testkit" + "jecs": "src", + "testkit": "testkit" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fd615a..5a3a52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [Unreleased] +### Added + +- Added `query:replace(function(...T) return ...U end)` for replacing components in place + - Method is fast pathed to replacing the data to the components for each corresponding entity + ### Changed - Iterator now goes backwards instead to prevent common cases of iterator invalidation @@ -25,13 +30,13 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ### Added -- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships. +- Added `world:parent(entity)` and `jecs.ChildOf` respectively as first class citizen for building parent-child relationships. - Give a parent to an entity with `world:add($source, pair(ChildOf, $target))` - Use `world:parent(entity)` to find the target of the relationship - Added user-facing Luau types ### Changed -- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream: +- Improved iteration speeds 20-40% by manually indexing rather than using `next()` :scream: ## [0.1.1] - 2024-05-19 @@ -48,19 +53,19 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [0.1.0-rc.6] - 2024-05-13 -### Added +### Added - Added a `jecs.Wildcard` term - it lets you query any partially matched pairs ## [0.1.0-rc.5] - 2024-05-10 -### Added +### Added - Added Entity relationships for creating logical connections between entities - Added `world:__iter method` which allows for iteration over the whole world to get every entity - used for reconciling whole worlds such as via replication, saving/loading, etc -- Added `world:add(entity, component)` which adds a component to the entity +- Added `world:add(entity, component)` which adds a component to the entity - it is an idempotent function, so calling it twice and in any order should be fine ### Fixed @@ -89,7 +94,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to ## [0.0.0-prototype.rc.2] - 2024-04-26 ### Changed -- Optimized the creation of the query +- Optimized the creation of the query - It will now finds the smallest archetype map to iterate over - Optimized the query iterator - It will now populates iterator with columns for faster indexing @@ -109,12 +114,3 @@ The format is based on [Keep a Changelog][kac], and this project adheres to [0.0.0-prototype-rc.3]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.3 [0.0.0-prototype.rc.2]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.2 [0.0.0-prototype-rc.1]: https://github.com/ukendio/jecs/releases/tag/v0.0.0-prototype.rc.1 - - - - - - - - - diff --git a/benches/query.luau b/benches/query.luau index a2df698..92b6c1c 100644 --- a/benches/query.luau +++ b/benches/query.luau @@ -7,6 +7,7 @@ local function TITLE(title: string) print() print(testkit.color.white(title)) end + local jecs = require("@jecs") local mirror = require("../mirror/init") diff --git a/src/init.luau b/src/init.luau index d89e406..8356b9b 100644 --- a/src/init.luau +++ b/src/init.luau @@ -42,7 +42,7 @@ TODO: index: number, count: number, column: number -} +} ]] @@ -131,7 +131,7 @@ local function ECS_ENTITY_T_LO(e: i53): i24 return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e end -local function STRIP_GENERATION(e: i53): i24 +local function STRIP_GENERATION(e: i53): i24 return ECS_ENTITY_T_LO(e) end @@ -149,7 +149,7 @@ local function getAlive(index: EntityIndex, e: i24): i53 if id then local currentGeneration = ECS_GENERATION(id) local gen = ECS_GENERATION(e) - if gen == currentGeneration then + if gen == currentGeneration then return id end @@ -159,7 +159,7 @@ local function getAlive(index: EntityIndex, e: i24): i53 error(ERROR_ENTITY_NOT_ALIVE) end -local function sparseGet(entityIndex, id) +local function sparseGet(entityIndex, id) return entityIndex.sparse[getAlive(entityIndex, id)] end @@ -353,7 +353,7 @@ end -- TODO: -- should have an additional `nth` parameter which selects the nth target -- this is important when an entity can have multiple relationships with the same target -local function target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24? +local function target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24? local entityIndex = world.entityIndex local record = entityIndex.sparse[entity] local archetype = record.archetype @@ -374,7 +374,7 @@ local function target(world: World, entity: i53, relation: i24--[[, nth: number] return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord]) end -local function parent(world: World, entity: i53) +local function parent(world: World, entity: i53) return target(world, entity, EcsChildOf) end @@ -462,7 +462,7 @@ local function add(world: World, entityId: i53, componentId: i53) end -- Symmetric like `World.add` but idempotent -local function set(world: World, entityId: i53, componentId: i53, data: unknown) +local function set(world: World, entityId: i53, componentId: i53, data: unknown) local record = world.entityIndex.sparse[entityId] local from = record.archetype local to = archetypeTraverseAdd(world, componentId, from) @@ -600,17 +600,17 @@ local function delete(world: World, entityId: i53) end -local function clear(world: World, entityId: i53) +local function clear(world: World, entityId: i53) --TODO: use sparse_get (stashed) local record = world.entityIndex.sparse[entityId] - if not record then + if not record then return end local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE local archetype = record.archetype - if archetype == nil or archetype == ROOT_ARCHETYPE then + if archetype == nil or archetype == ROOT_ARCHETYPE then return end @@ -662,21 +662,29 @@ local function noop(_self: Query, ...): () -> () end local EmptyQuery = { - __iter = noop, - without = noop, + __iter = iterNoop, + next = noop, + replace = noop, + without = function(self) + return self + end } -EmptyQuery.__index = EmptyQuery -setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) type CompatibleArchetype = { archetype: Archetype, indices: { number } } -local function preparedQuery(compatibleArchetypes: { Archetype }, - components: { i53? }, indices: { { number } }) +local function replaceMult(row, columns, ...) + for i, column in columns do + column[row] = select(i, ...) + end +end + +local function preparedQuery(compatibleArchetypes: { Archetype }, + components: { i53? }, indices: { { number } }) local queryLength = #components - + local lastArchetype = 1 local archetype: Archetype = compatibleArchetypes[lastArchetype] @@ -686,16 +694,16 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, local queryOutput = {} - local entities = archetype.entities + local entities = archetype.entities local i = #entities local function queryNext(): ...any local entityId = entities[i] - while entityId == nil do + while entityId == nil do lastArchetype += 1 archetype = compatibleArchetypes[lastArchetype] - - if not archetype then + + if not archetype then return end @@ -782,34 +790,77 @@ local function preparedQuery(compatibleArchetypes: { Archetype }, return self end - - local it = { - __iter = function() - lastArchetype = 1 - archetype = compatibleArchetypes[1] - entities = archetype.entities - i = #entities - return queryNext - end, + local function iter() + lastArchetype = 1 + archetype = compatibleArchetypes[1] + entities = archetype.entities + i = #entities + + return queryNext + end + + local function replace(_, fn) + for i, archetype in compatibleArchetypes do + local tr = indices[i] + local columns = archetype.columns + + for row in archetype.entities do + if queryLength == 1 then + local a = columns[tr[1]] + local pa = fn(a[row]) + + a[row] = pa + elseif queryLength == 2 then + local a = columns[tr[1]] + local b = columns[tr[2]] + + a[row], b[row] = fn(a[row], b[row]) + elseif queryLength == 3 then + local a = columns[tr[1]] + local b = columns[tr[2]] + local c = columns[tr[3]] + + a[row], b[row], c[row] = fn(a[row], b[row], c[row]) + elseif queryLength == 4 then + local a = columns[tr[1]] + local b = columns[tr[2]] + local c = columns[tr[3]] + local d = columns[tr[4]] + + a[row], b[row], c[row], d[row] = fn( + a[row], b[row], c[row], d[row]) + else + for i = 1, queryLength do + queryOutput[i] = columns[tr[i]][row] + end + replaceMult(row, columns, fn(unpack(queryOutput))) + end + end + end + end + + local it = { + __iter = iter, next = queryNext, - without = without + without = without, + replace = replace } return setmetatable(it, it) :: any end -local function query(world: World, ...: number): Query +local function query(world: World, ...: number): Query -- breaking? if (...) == nil then error("Missing components") end local indices: { { number } } = {} - local compatibleArchetypes: { Archetype } = {} + local compatibleArchetypes: { Archetype } = {} local length = 0 - local components: { number } = { ... } + local components: { number } = { ... } local archetypes: { Archetype } = world.archetypes :: any local firstArchetypeMap: ArchetypeMap @@ -989,7 +1040,7 @@ export type WorldShim = typeof(setmetatable( local World = {} World.__index = World -function World.new() +function World.new() local self = setmetatable({ archetypeIndex = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, diff --git a/tests/world.luau b/tests/world.luau index 8f62349..178650a 100644 --- a/tests/world.luau +++ b/tests/world.luau @@ -449,9 +449,28 @@ TEST("world", function() count += 1 end - print(count) CHECK(count == 2) end + + do CASE "should replace component data" + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() + + local e = world:entity() + world:set(e, A, 1) + world:set(e, B, true) + world:set(e, C, "hello ") + + world:query(A, B, C):replace(function(a, b, c) + return a * 2, not b, c.."world" + end) + + CHECK(world:get(e, A) == 2) + CHECK(world:get(e, B) == false) + CHECK(world:get(e, C) == "hello world") + end end) FINISH()