diff --git a/lib/World.lua b/lib/World.lua index 91f5f33..8bf1177 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -533,6 +533,112 @@ 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 + +function View.new() + return setmetatable({ + fetches = {}, + }, View) +end + +function View:__iter() + local current = self.head + return function() + if current then + local entity = current.entity + local fetch = self.fetches[entity] + current = current.next + + return entity, unpack(fetch, 1, fetch.n) + end + 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 + end + + local fetch = self.fetches[entity] + + return unpack(fetch, 1, fetch.n) +end + +--[=[ + Equivalent to `world:contains()` + @param entity number - the entity ID + @return boolean +]=] + +function View:contains(entity) + return self.fetches[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() + local function iter() + return self._next() + end + + local view = View.new() + + for entityId, entityData in iter do + if entityId then + -- We start at 2 on Select since we don't need want to pack the entity id. + 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 + + return view +end + --[=[ Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over the results of the query. diff --git a/lib/World.spec.lua b/lib/World.spec.lua index d4f9a4a..53c4ba0 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,76 @@ return function() end end) + 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) }), Root()) + local _otherRoot = world:spawn(Transform({ pos = Vector2.new(1, 2) }), Root()) + + 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 = Vector2.new(-1, 0) }), + }), + Transform.new({ pos = Vector2.zero }) + ) + + local parents = world:query(Parent):view() + local roots = world:query(Transform, Root):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 + + 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() local world = World.new() local A = component()