Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Implementing Views #80

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
106 changes: 106 additions & 0 deletions lib/World.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its unclear what "entity" is meant to be in this example to me. Is this an entityId?

```

@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.
Expand Down
75 changes: 73 additions & 2 deletions lib/World.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()
Expand Down