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

Archetype negation #93

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9a84577
Add check for config
Ukendio Dec 20, 2023
89c3f2c
Prototyped implementation
Ukendio Dec 20, 2023
3b8d5d7
Remove whitspace
Ukendio Dec 20, 2023
28e1c9e
Added unit tests
Ukendio Dec 20, 2023
7a93fd7
Preserve order of query traversal
Ukendio Dec 20, 2023
a46a97a
Merge branch 'main' of https://github.com/evaera/matter into views
Ukendio Dec 21, 2023
aa3d0f4
Adding comments
Ukendio Dec 21, 2023
0a2f6df
Merge branch 'main' of https://github.com/evaera/matter into views
Ukendio Dec 21, 2023
4868ba6
Remove entity param from view
Ukendio Dec 21, 2023
7f67d1d
Oops.
Ukendio Dec 21, 2023
e70a590
Add "_" to unused variables
Ukendio Dec 21, 2023
84e1c36
Change otherRoot name in tests
Ukendio Dec 21, 2023
79822a7
Merge branch 'main' of https://github.com/evaera/matter into views
Ukendio Dec 21, 2023
817d1df
Put keys in a linked list
Ukendio Dec 21, 2023
e7889b9
Add checks for views:get()
Ukendio Dec 21, 2023
09bd251
Initial Commit
Ukendio Dec 22, 2023
ad24560
iterate self._filter instead of entityData
Ukendio Dec 22, 2023
ebb1f85
Create a NOOP
Ukendio Dec 22, 2023
03a31b5
Remove unused function
Ukendio Dec 22, 2023
e0ed459
Moved functions under corresponding comments
Ukendio Dec 22, 2023
bdae1c4
Initial commit
Ukendio Dec 22, 2023
fe4ec1d
Expand should be a closure for inlining
Ukendio Dec 22, 2023
0c97ff7
Merge branch 'query-iter' of https://github.com/evaera/matter into ar…
Ukendio Dec 22, 2023
fad7489
Make :_next into a closure instead
Ukendio Dec 23, 2023
4e9eecf
Merge branch 'query-iter' of https://github.com/evaera/matter into ar…
Ukendio Dec 23, 2023
a7fe37a
remove topoRuntime import
Ukendio Dec 23, 2023
1cc5144
Make struct smaller
Ukendio Dec 23, 2023
ac1d737
Merge branch 'query-iter' of https://github.com/evaera/matter into ar…
Ukendio Dec 23, 2023
a53ce69
Change split to ||
Ukendio Dec 23, 2023
ef5d8fe
Merge branch 'views' of https://github.com/evaera/matter into archety…
Ukendio Dec 23, 2023
4e9a28b
Moving back some of the query logic
Ukendio Dec 24, 2023
c744828
Return static noopQuery
Ukendio Dec 24, 2023
90ef6db
Handle filter iteratively
Ukendio Dec 24, 2023
1304db4
Pull in upstream change from query-iter
Ukendio Dec 24, 2023
234d8d9
Merge branch 'archetype-negation' of https://github.com/evaera/matter…
Ukendio Dec 24, 2023
d65ac20
Update storage index on empty storage
Ukendio Dec 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 37 additions & 219 deletions lib/World.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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"

--[=[
Expand Down Expand Up @@ -192,7 +194,23 @@ function World:_newQueryArchetype(queryArchetype)

for _, storage in self._storages do
for entityArchetype in storage do
if areArchetypesCompatible(queryArchetype, entityArchetype) then
local archetype = string.split(queryArchetype, "||")
local negatedArchetype = archetype[1]
local filter = { unpack(archetype, 2, #archetype) }

local skip = false
for _, exclude in filter do
if areArchetypesCompatible(exclude, entityArchetype) then
skip = true
break
end
end

if skip then
continue
end

if areArchetypesCompatible(negatedArchetype, entityArchetype) then
self._queryCache[queryArchetype][entityArchetype] = true
end
end
Expand Down Expand Up @@ -370,169 +388,6 @@ function World:get(id, ...)
return unpack(components, 1, length)
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:__call()
return self._expand(self._next())
end

function QueryResult:__iter()
return function()
return self._expand(self._next())
end
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(...)
local metatables = { ... }
return function(): any
while true do
local entityId, entityData = self._next()

if not entityId then
break
end

local skip = false
for _, metatable in ipairs(metatables) do
if entityData[metatable] then
skip = true
break
end
end

if skip then
continue
end

return self._expand(entityId, entityData)
end
return
end
end

--[=[
Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over
the results of the query.
Expand All @@ -552,6 +407,22 @@ end
@param ... Component -- The component types to query. Only entities with *all* of these components will be returned.
@return QueryResult -- See [QueryResult](/api/QueryResult) docs.
]=]

local function noop() end

local noopQuery = setmetatable({
next = noop,
snapshot = noop,
without = function(self)
return self
end,
view = noop,
}, {
__iter = function()
return noop
end,
})

function World:query(...)
debug.profilebegin("World:query")
assertValidComponent((...), 1)
Expand All @@ -571,10 +442,7 @@ function World:query(...)

if next(compatibleArchetypes) == nil then
-- If there are no compatible storages avoid creating our complicated iterator
return setmetatable({
_expand = function() end,
_next = function() end,
}, QueryResult)
return noopQuery
end

local queryOutput = table.create(queryLength)
Expand All @@ -591,61 +459,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

repeat
if self._storages[storageIndex][currentCompatibleArchetype] then
entityId, entityData = next(self._storages[storageIndex][currentCompatibleArchetype], lastEntityId)
end

while entityId == nil do
currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype)

if currentCompatibleArchetype == nil then
storageIndex += 1

local nextStorage = self._storages[storageIndex]

if nextStorage == nil or next(nextStorage) == nil then
return
end

currentCompatibleArchetype = nil

if self._pristineStorage == nextStorage then
self:_markStorageDirty()
end

continue
elseif self._storages[storageIndex][currentCompatibleArchetype] == nil then
continue
end

entityId, entityData = next(self._storages[storageIndex][currentCompatibleArchetype])
end
lastEntityId = entityId

until seenEntities[entityId] == nil

seenEntities[entityId] = true
return entityId, entityData
end

return setmetatable({
_expand = expand,
_next = nextItem,
}, QueryResult)
return QueryResult.new(self, expand, archetype, compatibleArchetypes)
end

local function cleanupQueryChanged(hookState)
Expand Down
Loading