diff --git a/lib/World.lua b/lib/World.lua index 8e1f15dd..9135f58c 100644 --- a/lib/World.lua +++ b/lib/World.lua @@ -7,8 +7,6 @@ local assertValidComponent = Component.assertValidComponent local archetypeOf = archetypeModule.archetypeOf local areArchetypesCompatible = archetypeModule.areArchetypesCompatible -local QueryResult = require(script.Parent.query) - local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" --[=[ @@ -194,9 +192,27 @@ function World:_newQueryArchetype(queryArchetype) for _, storage in self._storages do for entityArchetype in storage do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - self._queryCache[queryArchetype][entityArchetype] = true + local archetypes = string.split(queryArchetype, "x") + local baseArchetype = table.remove(archetypes, 1) + + if not areArchetypesCompatible(baseArchetype, entityArchetype) then + continue end + + local skip = false + + for _, exclude in archetypes do + if areArchetypesCompatible(exclude, entityArchetype) then + skip = true + break + end + end + + if skip then + continue + end + + self._queryCache[queryArchetype][entityArchetype] = true end end end @@ -380,13 +396,235 @@ local noopQuery = setmetatable({ without = function(self) return self end, - view = noop, + view = { + get = noop, + contains = noop, + }, }, { __iter = function() return noop end, }) +--[=[ + @class QueryResult + + A result from the [`World:query`](/api/World#query) function. + + Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the + QueryResult is exhausted and is no longer useful. + + ```lua + for id, enemy, charge, model in world:query(Enemy, Charge, Model) do + -- Do something + end + ``` +]=] + +local QueryResult = {} +QueryResult.__index = QueryResult + +function QueryResult.new(world, expand, queryArchetype, compatibleArchetypes) + return setmetatable({ + world = world, + seenEntities = {}, + currentCompatibleArchetype = next(compatibleArchetypes), + compatibleArchetypes = compatibleArchetypes, + storageIndex = 1, + _expand = expand, + _queryArchetype = queryArchetype, + }, QueryResult) +end + +local function nextItem(query) + local world = query.world + local currentCompatibleArchetype = query.currentCompatibleArchetype + local seenEntities = query.seenEntities + local compatibleArchetypes = query.compatibleArchetypes + + local entityId, entityData + + local storages = world._storages + repeat + local nextStorage = storages[query.storageIndex] + local currently = nextStorage[currentCompatibleArchetype] + if currently then + entityId, entityData = next(currently, query.lastEntityId) + end + + while entityId == nil do + currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype) + + if currentCompatibleArchetype == nil then + query.storageIndex += 1 + + nextStorage = storages[query.storageIndex] + + if nextStorage == nil or next(nextStorage) == nil then + return + end + + currentCompatibleArchetype = nil + + if world._pristineStorage == nextStorage then + world:_markStorageDirty() + end + + continue + elseif nextStorage[currentCompatibleArchetype] == nil then + continue + end + + entityId, entityData = next(nextStorage[currentCompatibleArchetype]) + end + + query.lastEntityId = entityId + + until seenEntities[entityId] == nil + + query.currentCompatibleArchetype = currentCompatibleArchetype + + seenEntities[entityId] = true + + return entityId, entityData +end + +function QueryResult:__iter() + return function() + return self._expand(nextItem(self)) + end +end + +function QueryResult:__call() + return self._expand(nextItem(self)) +end + +--[=[ + Returns the next set of values from the query result. Once all results have been returned, the + QueryResult is exhausted and is no longer useful. + + :::info + This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly + done by the language itself. + ::: + + ```lua + -- Using world:query in this position will make Lua invoke the table as a function. This is conventional. + for id, enemy, charge, model in world:query(Enemy, Charge, Model) do + -- Do something + end + ``` + + If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly + instead of calling the QueryResult as a function. + ```lua + local id, enemy, charge, model = world:query(Enemy, Charge, Model):next() + local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional + ``` + + @return id -- Entity ID + @return ...ComponentInstance -- The requested component values +]=] +function QueryResult:next() + return self._expand(nextItem(self)) +end + +local snapshot = { + __iter = function(self): any + local i = 0 + return function() + i += 1 + + local data = self[i] + + if data then + return unpack(data, 1, data.n) + end + return + end + end, +} + +--[=[ + Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results. + + By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so + changes that occur during the iteration will affect future results. + + By contrast, `QueryResult:snapshot()` creates a list of all of the results of this query at the moment it is called, + so changes made while iterating over the result of `QueryResult:snapshot` do not affect future results of the + iteration. + + Of course, this comes with a cost: we must allocate a new list and iterate over everything returned from the + QueryResult in advance, so using this method is slower than iterating over a QueryResult directly. + + The table returned from this method has a custom `__iter` method, which lets you use it as you would use QueryResult + directly: + + ```lua + for entityId, health, player in world:query(Health, Player):snapshot() do + + end + ``` + + However, the table itself is just a list of sub-tables structured like `{entityId, component1, component2, ...etc}`. + + @return {{entityId: number, component: ComponentInstance, component: ComponentInstance, component: ComponentInstance, ...}} +]=] +function QueryResult:snapshot() + local list = setmetatable({}, snapshot) + + local function iter() + return nextItem(self) + end + + for entityId, entityData in iter do + if entityId then + table.insert(list, table.pack(self._expand(entityId, entityData))) + end + end + + return list +end + +--[=[ + Returns an iterator that will skip any entities that also have the given components. + + :::tip + This is essentially equivalent to querying normally, using `World:get` to check if a component is present, + and using Lua's `continue` keyword to skip this iteration (though, using `:without` is faster). + + This means that you should avoid queries that return a very large amount of results only to filter them down + to a few with `:without`. If you can, always prefer adding components and making your query more specific. + ::: + + @param ... Component -- The component types to filter against. + @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values + + ```lua + for id in world:query(Target):without(Model) do + -- Do something + end + ``` +]=] + +function QueryResult:without(...) + local world = self.world + local filter = string.gsub(archetypeOf(...), "_", "x") + + local negativeArchetype = `{self._queryArchetype}x{filter}` + + if world._queryCache[negativeArchetype] == nil then + world:_newQueryArchetype(negativeArchetype) + end + + local compatibleArchetypes = world._queryCache[negativeArchetype] + + self.compatibleArchetypes = compatibleArchetypes + self.currentCompatibleArchetype = next(compatibleArchetypes) + return self +end + --[=[ Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over the results of the query. @@ -464,63 +702,11 @@ function World:query(...) return entityId, unpack(queryOutput, 1, queryLength) end - local currentCompatibleArchetype = next(compatibleArchetypes) - local lastEntityId - local storageIndex = 1 - if self._pristineStorage == self._storages[1] then self:_markStorageDirty() end - local seenEntities = {} - - local function nextItem() - local entityId, entityData - - local storages = self._storages - repeat - local nextStorage = storages[storageIndex] - local currently = nextStorage[currentCompatibleArchetype] - if nextStorage[currentCompatibleArchetype] then - entityId, entityData = next(currently, lastEntityId) - end - - while entityId == nil do - currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype) - - if currentCompatibleArchetype == nil then - storageIndex += 1 - - nextStorage = storages[storageIndex] - - if nextStorage == nil or next(nextStorage) == nil then - return - end - - currentCompatibleArchetype = nil - - if self._pristineStorage == nextStorage then - self:_markStorageDirty() - end - - continue - elseif nextStorage[currentCompatibleArchetype] == nil then - continue - end - - entityId, entityData = next(nextStorage[currentCompatibleArchetype]) - end - - lastEntityId = entityId - - until seenEntities[entityId] == nil - - seenEntities[entityId] = true - - return entityId, entityData - end - - return QueryResult.new(expand, nextItem) + return QueryResult.new(self, expand, archetype, compatibleArchetypes) end local function cleanupQueryChanged(hookState) diff --git a/lib/World.spec.lua b/lib/World.spec.lua index b1d6dfd8..d2c598ce 100644 --- a/lib/World.spec.lua +++ b/lib/World.spec.lua @@ -138,6 +138,24 @@ return function() expect(world:size()).to.equal(1) end) + it("should not find any entities", function() + local world = World.new() + + local Hello = component() + local Bob = component() + local Shirley = component() + + local _helloBob = world:spawn(Hello(), Bob()) + local _helloShirley = world:spawn(Hello(), Shirley()) + + local withoutCount = 0 + for _ in world:query(Hello):without(Bob, Shirley) do + withoutCount += 1 + end + + expect(withoutCount).to.equal(0) + end) + it("should be queryable", function() local world = World.new() diff --git a/lib/query.lua b/lib/query.lua deleted file mode 100644 index df78452a..00000000 --- a/lib/query.lua +++ /dev/null @@ -1,172 +0,0 @@ ---[=[ - @class QueryResult - - A result from the [`World:query`](/api/World#query) function. - - Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the - QueryResult is exhausted and is no longer useful. - - ```lua - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - ``` -]=] -local QueryResult = {} -QueryResult.__index = QueryResult - -function QueryResult.new(expand, nextItem) - return setmetatable({ - _filter = {}, - _next = nextItem, - _expand = expand, - }, QueryResult) -end - -function QueryResult:__iter() - return function() - while true do - local entityId, entityData = self._next() - - if not entityId then - break - end - - local skip = false - for _, metatable in ipairs(self._filter) do - if entityData[metatable] then - skip = true - break - end - end - - if skip then - continue - end - - return self._expand(entityId, entityData) - end - return - end -end - -function QueryResult:__call() - return self._expand(self._next()) -end - ---[=[ - Returns the next set of values from the query result. Once all results have been returned, the - QueryResult is exhausted and is no longer useful. - - :::info - This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly - done by the language itself. - ::: - - ```lua - -- Using world:query in this position will make Lua invoke the table as a function. This is conventional. - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - ``` - - If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly - instead of calling the QueryResult as a function. - ```lua - local id, enemy, charge, model = world:query(Enemy, Charge, Model):next() - local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional - ``` - - @return id -- Entity ID - @return ...ComponentInstance -- The requested component values -]=] -function QueryResult:next() - return self._expand(self._next()) -end - -local snapshot = { - __iter = function(self): any - local i = 0 - return function() - i += 1 - - local data = self[i] - - if data then - return unpack(data, 1, data.n) - end - return - end - end, -} - ---[=[ - Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results. - - By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so - changes that occur during the iteration will affect future results. - - By contrast, `QueryResult:snapshot()` creates a list of all of the results of this query at the moment it is called, - so changes made while iterating over the result of `QueryResult:snapshot` do not affect future results of the - iteration. - - Of course, this comes with a cost: we must allocate a new list and iterate over everything returned from the - QueryResult in advance, so using this method is slower than iterating over a QueryResult directly. - - The table returned from this method has a custom `__iter` method, which lets you use it as you would use QueryResult - directly: - - ```lua - for entityId, health, player in world:query(Health, Player):snapshot() do - - end - ``` - - However, the table itself is just a list of sub-tables structured like `{entityId, component1, component2, ...etc}`. - - @return {{entityId: number, component: ComponentInstance, component: ComponentInstance, component: ComponentInstance, ...}} -]=] -function QueryResult:snapshot() - local list = setmetatable({}, snapshot) - - local function iter() - return self._next() - end - - for entityId, entityData in iter do - if entityId then - table.insert(list, table.pack(self._expand(entityId, entityData))) - end - end - - return list -end - ---[=[ - Returns an iterator that will skip any entities that also have the given components. - - :::tip - This is essentially equivalent to querying normally, using `World:get` to check if a component is present, - and using Lua's `continue` keyword to skip this iteration (though, using `:without` is faster). - - This means that you should avoid queries that return a very large amount of results only to filter them down - to a few with `:without`. If you can, always prefer adding components and making your query more specific. - ::: - - @param ... Component -- The component types to filter against. - @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values - - ```lua - for id in world:query(Target):without(Model) do - -- Do something - end - ``` -]=] - -function QueryResult:without(...) - self._filter = { ... } - - return self -end - -return QueryResult