From 9a845772a9fc636e439f607e372a21f8dc1c03cf Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 20 Dec 2023 06:15:40 +0100 Subject: [PATCH 01/12] Add check for config --- lib/debugger/hookWidgets.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/debugger/hookWidgets.lua b/lib/debugger/hookWidgets.lua index 2f425e4..b3eb498 100644 --- a/lib/debugger/hookWidgets.lua +++ b/lib/debugger/hookWidgets.lua @@ -34,7 +34,10 @@ local dummyHandles = { }, slider = function(config) - return config.initial or 0 + if type(config) == "table" then + config = config.initial + end + return config end, window = { From 89c3f2c1aedae900a8bd01b10e153c33bf106eb0 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 20 Dec 2023 18:30:20 +0100 Subject: [PATCH 02/12] Prototyped implementation --- lib/World.lua | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/lib/World.lua b/lib/World.lua index 91f5f33..2a69da6 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -533,6 +533,55 @@ function QueryResult:without(...) end end +local viewHandle = { + + __iter = function() + local i = 0 + return function() + i += 1 + + local data = self[i] + + if data then + return unpack(data, 1, data.n) + end + return + end + end, +} + +function QueryResult:view(entity) + local viewHandle = {} + viewHandle.__index = viewHandle + + local function iter() + return self._next() + end + + local view = {} + + for entityId, entityData in iter do + if entityId then + -- We start at 2 since we don't need to return the eentity id. Might be a better + view[entityId] = table.pack(select(2, self._expand(entityId, entityData))) + end + end + + function view:get(entity) + if not self:contains(entity) then + return + end + + return unpack(self[entity], 1, self[entity].n) + end + + function view:contains(entity) + return self[entity] ~= nil + end + + return setmetatable(view, viewwHandle) +end + --[=[ Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over the results of the query. From 3b8d5d775655287b2d02038ac52b72bd7bdbc93c Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 20 Dec 2023 18:33:23 +0100 Subject: [PATCH 03/12] Remove whitspace --- lib/World.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/World.lua b/lib/World.lua index 2a69da6..d807a26 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -534,7 +534,6 @@ function QueryResult:without(...) end local viewHandle = { - __iter = function() local i = 0 return function() From 28e1c9e2e68432469d07478a32b63910168d5b61 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 00:54:58 +0100 Subject: [PATCH 04/12] Added unit tests --- lib/World.spec.lua | 56 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/World.spec.lua b/lib/World.spec.lua index d4f9a4a..e84cb75 100644 --- a/lib/World.spec.lua +++ b/lib/World.spec.lua @@ -505,9 +505,10 @@ return function() }) ) - local snapshot = world:query(Health, Player):snapshot() + local query = world:query(Health, Player) + local snapshot = query:snapshot() - for entityId, health, player in world:query(Health, Player):snapshot() do + for entityId, health, player in snapshot do expect(type(entityId)).to.equal("number") expect(type(player.name)).to.equal("string") expect(type(health.value)).to.equal("number") @@ -523,6 +524,57 @@ return function() end end) + it("should allow viewing a query", function() + local Parent = component("Parent") + local Transform = component("Transform") + + local world = World.new() + + local root = world:spawn(Transform({ pos = Vector2.new(3, 4) })) + local otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) })) + + local child = world:spawn( + Parent({ + entity = root, + fromChild = Transform({ pos = Vector2.one }), + }), + Transform.new({ pos = Vector2.zero }) + ) + + local otherChild = world:spawn( + Parent({ + entity = root, + fromChild = Transform({ pos = Vector2.new(0, 0) }), + }), + Transform.new({ pos = Vector2.zero }) + ) + + local grandChild = world:spawn( + Parent({ + entity = child, + fromChild = Transform({ pos = Vector3.new(-1, 0) }), + }), + Transform.new({ pos = Vector2.zero }) + ) + + local parents = world:query(Transform, Parent):view() + + expect(parents:contains(root)).to.equal(false) + + local orderOfIteration = {} + + for id in world:query(Transform, Parent) do + table.insert(orderOfIteration, id) + end + + local view = world:query(Transform, Parent):view() + local i = 0 + for id in view do + i += 1 + expect(orderOfIteration[i]).to.equal(id) + end + end) + it("should not invalidate iterators", function() local world = World.new() local A = component() From 7a93fd71afdea419d3d17f5ede47ff0eab39a57d Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 00:55:09 +0100 Subject: [PATCH 05/12] Preserve order of query traversal --- lib/World.lua | 68 +++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/lib/World.lua b/lib/World.lua index d807a26..cff19c2 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -533,52 +533,56 @@ function QueryResult:without(...) end end -local viewHandle = { - __iter = function() - local i = 0 - return function() - i += 1 +local View = {} +View.__index = View - local data = self[i] +function View.new() + return setmetatable({ + entities = {}, + items = {}, + }, View) +end - if data then - return unpack(data, 1, data.n) - end - return - end - end, -} +function View:__iter() + local i = 0 + return function() + i += 1 -function QueryResult:view(entity) - local viewHandle = {} - viewHandle.__index = viewHandle + local entity = self.entities[i] + return entity, self:get(entity) + end +end +function View:get(entity) + if not self:contains(entity) then + return + end + + local item = self.items[entity] + + return unpack(item, 1, item.n) +end + +function View:contains(entity) + return self.items[entity] ~= nil +end + +function QueryResult:view(entity) local function iter() return self._next() end - local view = {} + local view = View.new() for entityId, entityData in iter do if entityId then - -- We start at 2 since we don't need to return the eentity id. Might be a better - view[entityId] = table.pack(select(2, self._expand(entityId, entityData))) - end - end - - function view:get(entity) - if not self:contains(entity) then - return + table.insert(view.entities, entityId) + -- We start 2 on Select since we don't need to return the eentity id. Might be a better + view.items[entityId] = table.pack(select(2, self._expand(entityId, entityData))) end - - return unpack(self[entity], 1, self[entity].n) - end - - function view:contains(entity) - return self[entity] ~= nil end - return setmetatable(view, viewwHandle) + return view end --[=[ From aa3d0f4b4888bc08816223dff13f2f738670f7ae Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 03:23:00 +0100 Subject: [PATCH 06/12] Adding comments --- lib/World.lua | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/World.lua b/lib/World.lua index cff19c2..87ece91 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -533,6 +533,20 @@ function QueryResult:without(...) end end +--[=[ + @class View + + Provides random access to the results of a query. + + Calling the table is equivalent iterating a query. + + ```lua + for id, player, health, poison in world:query(Player, Health, Poison):view() do + -- Do something + end + ``` +]=] + local View = {} View.__index = View @@ -553,6 +567,11 @@ function View:__iter() end end +--[=[ + Retrieve the query results to corresponding `entity` + @param entity number - the entity ID + @return ...ComponentInstance +]=] function View:get(entity) if not self:contains(entity) then return @@ -563,10 +582,33 @@ function View:get(entity) return unpack(item, 1, item.n) end +--[=[ + Equivalent to `world:contains()` + @param entity number - the entity ID + @return boolean +]=] + function View:contains(entity) return self.items[entity] ~= nil end +--[=[ + Creates a View of the query and does all of the iterator tasks at once at an amortized cost. + This is used for many repeated random access to an entity. If you only need to iterate, just use a query. + + ```lua + for id, player, health, poison in world:query(Player, Health, Poison):view() do + -- Do something + end + + local dyingPeople = world:query(Player, Health, Poison):view() + local remainingHealth = dyingPeople:get(entity) + ``` + + @param ... Component - The component types to query. Only entities with *all* of these components will be returned. + @return View See [View](/api/View) docs. +]=] + function QueryResult:view(entity) local function iter() return self._next() @@ -577,7 +619,7 @@ function QueryResult:view(entity) for entityId, entityData in iter do if entityId then table.insert(view.entities, entityId) - -- We start 2 on Select since we don't need to return the eentity id. Might be a better + -- We start at 2 on Select since we don't need want to pack the entity id. view.items[entityId] = table.pack(select(2, self._expand(entityId, entityData))) end end From 4868ba6ade1d1b7dc9b1ba182689529264d563e1 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 03:41:50 +0100 Subject: [PATCH 07/12] Remove entity param from view --- lib/World.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/World.lua b/lib/World.lua index 87ece91..d5e13d5 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -609,7 +609,7 @@ end @return View See [View](/api/View) docs. ]=] -function QueryResult:view(entity) +function QueryResult:view() local function iter() return self._next() end @@ -620,7 +620,7 @@ function QueryResult:view(entity) if entityId then table.insert(view.entities, entityId) -- We start at 2 on Select since we don't need want to pack the entity id. - view.items[entityId] = table.pack(select(2, self._expand(entityId, entityData))) + view.items[entityId] = table.pack(select(2, s._expand(entityId, entityData))) end end From 7f67d1d021d2ce60b0f97ca49f56fb17867f099c Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 03:42:25 +0100 Subject: [PATCH 08/12] Oops. --- lib/World.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/World.lua b/lib/World.lua index d5e13d5..feabec1 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -620,7 +620,7 @@ function QueryResult:view() if entityId then table.insert(view.entities, entityId) -- We start at 2 on Select since we don't need want to pack the entity id. - view.items[entityId] = table.pack(select(2, s._expand(entityId, entityData))) + view.items[entityId] = table.pack(select(2, self._expand(entityId, entityData))) end end From e70a590761ca8866c48d7fb40900475ae0ca0a56 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 03:43:11 +0100 Subject: [PATCH 09/12] Add "_" to unused variables --- lib/World.spec.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/World.spec.lua b/lib/World.spec.lua index e84cb75..601d8aa 100644 --- a/lib/World.spec.lua +++ b/lib/World.spec.lua @@ -541,7 +541,7 @@ return function() Transform.new({ pos = Vector2.zero }) ) - local otherChild = world:spawn( + local _otherChild = world:spawn( Parent({ entity = root, fromChild = Transform({ pos = Vector2.new(0, 0) }), @@ -549,7 +549,7 @@ return function() Transform.new({ pos = Vector2.zero }) ) - local grandChild = world:spawn( + local _grandChild = world:spawn( Parent({ entity = child, fromChild = Transform({ pos = Vector3.new(-1, 0) }), From 84e1c36d125e9b6d46ddec35c527a94746f874da Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 03:43:37 +0100 Subject: [PATCH 10/12] Change otherRoot name in tests --- lib/World.spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/World.spec.lua b/lib/World.spec.lua index 601d8aa..03107bb 100644 --- a/lib/World.spec.lua +++ b/lib/World.spec.lua @@ -531,7 +531,7 @@ return function() local world = World.new() local root = world:spawn(Transform({ pos = Vector2.new(3, 4) })) - local otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) })) + local _otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) })) local child = world:spawn( Parent({ From 817d1dfc6d92e167cf22251089663db6195679cd Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 12:17:11 +0100 Subject: [PATCH 11/12] Put keys in a linked list --- lib/World.lua | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/World.lua b/lib/World.lua index feabec1..8bf1177 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -552,18 +552,20 @@ View.__index = View function View.new() return setmetatable({ - entities = {}, - items = {}, + fetches = {}, }, View) end function View:__iter() - local i = 0 + local current = self.head return function() - i += 1 + if current then + local entity = current.entity + local fetch = self.fetches[entity] + current = current.next - local entity = self.entities[i] - return entity, self:get(entity) + return entity, unpack(fetch, 1, fetch.n) + end end end @@ -577,9 +579,9 @@ function View:get(entity) return end - local item = self.items[entity] + local fetch = self.fetches[entity] - return unpack(item, 1, item.n) + return unpack(fetch, 1, fetch.n) end --[=[ @@ -589,7 +591,7 @@ end ]=] function View:contains(entity) - return self.items[entity] ~= nil + return self.fetches[entity] ~= nil end --[=[ @@ -618,9 +620,19 @@ function QueryResult:view() for entityId, entityData in iter do if entityId then - table.insert(view.entities, entityId) -- We start at 2 on Select since we don't need want to pack the entity id. - view.items[entityId] = table.pack(select(2, self._expand(entityId, entityData))) + local fetch = table.pack(select(2, self._expand(entityId, entityData))) + local node = { entity = entityId, next = nil } + view.fetches[entityId] = fetch + if not view.head then + view.head = node + else + local current = view.head + while current.next do + current = current.next + end + current.next = node + end end end From e7889b965a578b614a9cdb8e309bd54f12711ac0 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 21 Dec 2023 12:49:35 +0100 Subject: [PATCH 12/12] Add checks for views:get() --- lib/World.spec.lua | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/World.spec.lua b/lib/World.spec.lua index 03107bb..53c4ba0 100644 --- a/lib/World.spec.lua +++ b/lib/World.spec.lua @@ -527,11 +527,12 @@ return function() it("should allow viewing a query", function() local Parent = component("Parent") local Transform = component("Transform") + local Root = component("Root") local world = World.new() - local root = world:spawn(Transform({ pos = Vector2.new(3, 4) })) - local _otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) })) + local root = world:spawn(Transform({ pos = Vector2.new(3, 4) }), Root()) + local _otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) }), Root()) local child = world:spawn( Parent({ @@ -552,12 +553,13 @@ return function() local _grandChild = world:spawn( Parent({ entity = child, - fromChild = Transform({ pos = Vector3.new(-1, 0) }), + fromChild = Transform({ pos = Vector2.new(-1, 0) }), }), Transform.new({ pos = Vector2.zero }) ) - local parents = world:query(Transform, Parent):view() + local parents = world:query(Parent):view() + local roots = world:query(Transform, Root):view() expect(parents:contains(root)).to.equal(false) @@ -573,6 +575,23 @@ return function() i += 1 expect(orderOfIteration[i]).to.equal(id) end + + for id, absolute, parent in world:query(Transform, Parent) do + local relative = parent.fromChild.pos + local ancestor = parent.entity + local current = parents:get(ancestor) + while current do + relative = current.fromChild.pos * relative + ancestor = current.entity + current = parents:get(ancestor) + end + + local pos = roots:get(ancestor).pos + + world:insert(id, absolute:patch({ pos = Vector2.new(pos.x + relative.x, pos.y + relative.y) })) + end + + expect(world:get(child, Transform).pos).to.equal(Vector2.new(4, 5)) end) it("should not invalidate iterators", function()