From c768cf3655f45434ede0f1e1c2acc6169823d034 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 5 May 2024 02:45:52 +0200 Subject: [PATCH] Remove matter from main --- benches/query.lua | 195 +----- newMatter.lua | 1499 ------------------------------------------- oldMatter.lua | 1567 --------------------------------------------- 3 files changed, 1 insertion(+), 3260 deletions(-) delete mode 100644 newMatter.lua delete mode 100644 oldMatter.lua diff --git a/benches/query.lua b/benches/query.lua index 2c4cd55..8f54596 100644 --- a/benches/query.lua +++ b/benches/query.lua @@ -10,9 +10,6 @@ end local jecs = require("../mirror/init") -local oldMatter = require("../oldMatter") - -local newMatter = require("../newMatter") type i53 = number do @@ -106,194 +103,4 @@ do view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) end -end - -do - TITLE(testkit.color.white_underline("OldMatter query")) - - local ecs = oldMatter.World.new() - local component = oldMatter.component - - do - TITLE("one component in common") - local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) - BENCH("1 component", function() - for _ in world:query(A) do - end - end) - - BENCH("2 component", function() - for _ in world:query(A, B) do - end - end) - - BENCH("4 component", function() - for _ in world:query(A, B, C, D) do - end - end) - - BENCH("8 component", function() - for _ in world:query(A, B, C, D, E, F, G, H) do - end - end) - end - - local D1 = component() - local D2 = component() - local D3 = component() - local D4 = component() - local D5 = component() - local D6 = component() - local D7 = component() - local D8 = component() - - local function flip() - return math.random() >= 0.15 - end - - local added = 0 - local archetypes = {} - for i = 1, 2 ^ 16 - 2 do - local entity = ecs:spawn() - - local combination = "" - - if flip() then - combination ..= "B" - ecs:insert(entity, D2({value = true})) - end - if flip() then - combination ..= "C" - ecs:insert(entity, D3({value = true})) - end - if flip() then - combination ..= "D" - ecs:insert(entity, D4({value = true})) - end - if flip() then - combination ..= "E" - ecs:insert(entity, D5({value = true})) - end - if flip() then - combination ..= "F" - ecs:insert(entity, D6({value = true})) - end - if flip() then - combination ..= "G" - ecs:insert(entity, D7({value = true})) - end - if flip() then - combination ..= "H" - ecs:insert(entity, D8({value = true})) - end - - if #combination == 7 then - added += 1 - ecs:insert(entity, D1({value = true})) - end - archetypes[combination] = true - end - - local a = 0 - for _ in archetypes do - a += 1 - end - - view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) - end -end - -do - TITLE(testkit.color.white_underline("NewMatter query")) - - local ecs = newMatter.World.new() - local component = newMatter.component - - do - TITLE("one component in common") - local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) - BENCH("1 component", function() - for _ in world:query(A) do - end - end) - - BENCH("2 component", function() - for _ in world:query(A, B) do - end - end) - - BENCH("4 component", function() - for _ in world:query(A, B, C, D) do - end - end) - - BENCH("8 component", function() - for _ in world:query(A, B, C, D, E, F, G, H) do - end - end) - end - - local D1 = component() - local D2 = component() - local D3 = component() - local D4 = component() - local D5 = component() - local D6 = component() - local D7 = component() - local D8 = component() - - local function flip() - return math.random() >= 0.15 - end - - local added = 0 - local archetypes = {} - for i = 1, 2 ^ 16 - 2 do - local entity = ecs:spawn() - - local combination = "" - - if flip() then - combination ..= "B" - ecs:insert(entity, D2({value = true})) - end - if flip() then - combination ..= "C" - ecs:insert(entity, D3({value = true})) - end - if flip() then - combination ..= "D" - ecs:insert(entity, D4({value = true})) - end - if flip() then - combination ..= "E" - ecs:insert(entity, D5({value = true})) - end - if flip() then - combination ..= "F" - ecs:insert(entity, D6({value = true})) - end - if flip() then - combination ..= "G" - ecs:insert(entity, D7({value = true})) - end - if flip() then - combination ..= "H" - ecs:insert(entity, D8({value = true})) - end - - if #combination == 7 then - added += 1 - ecs:insert(entity, D1({value = true})) - end - archetypes[combination] = true - end - - local a = 0 - for _ in archetypes do - a += 1 - end - - view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) - end -end +end \ No newline at end of file diff --git a/newMatter.lua b/newMatter.lua deleted file mode 100644 index 4a50791..0000000 --- a/newMatter.lua +++ /dev/null @@ -1,1499 +0,0 @@ ---!optimize 2 ---!native ---!strict - -local None = {} - -local function merge(one, two) - local new = table.clone(one) - - for key, value in two do - if value == None then - new[key] = nil - else - new[key] = value - end - end - - return new -end - --- https://github.com/freddylist/llama/blob/master/src/List/toSet.lua -local function toSet(list) - local set = {} - - for _, v in ipairs(list) do - set[v] = true - end - - return set -end - --- https://github.com/freddylist/llama/blob/master/src/Dictionary/values.lua -local function values(dictionary) - local valuesList = {} - - local index = 1 - - for _, value in pairs(dictionary) do - valuesList[index] = value - index = index + 1 - end - - return valuesList -end - -local stack = {} - -local function newStackFrame(node) - return { - node = node, - accessedKeys = {}, - } -end - -local function cleanup() - local currentFrame = stack[#stack] - - for baseKey, state in pairs(currentFrame.node.system) do - for key, value in pairs(state.storage) do - if not currentFrame.accessedKeys[baseKey] or not currentFrame.accessedKeys[baseKey][key] then - local cleanupCallback = state.cleanupCallback - - if cleanupCallback then - local shouldAbortCleanup = cleanupCallback(value) - - if shouldAbortCleanup then - continue - end - end - - state.storage[key] = nil - end - end - end -end - -local function start(node, fn) - table.insert(stack, newStackFrame(node)) - fn() - cleanup() - table.remove(stack, #stack) -end - -local function withinTopoContext() - return #stack ~= 0 -end - -local function useFrameState() - return stack[#stack].node.frame -end - -local function useCurrentSystem() - if #stack == 0 then - return - end - - return stack[#stack].node.currentSystem -end - - ---[=[ - @within Matter - - :::tip - **Don't use this function directly in your systems.** - - This function is used for implementing your own topologically-aware functions. It should not be used in your - systems directly. You should use this function to implement your own utilities, similar to `useEvent` and - `useThrottle`. - ::: - - `useHookState` does one thing: it returns a table. An empty, pristine table. Here's the cool thing though: - it always returns the *same* table, based on the script and line where *your function* (the function calling - `useHookState`) was called. - - ### Uniqueness - - If your function is called multiple times from the same line, perhaps within a loop, the default behavior of - `useHookState` is to uniquely identify these by call count, and will return a unique table for each call. - - However, you can override this behavior: you can choose to key by any other value. This means that in addition to - script and line number, the storage will also only return the same table if the unique value (otherwise known as the - "discriminator") is the same. - - ### Cleaning up - As a second optional parameter, you can pass a function that is automatically invoked when your storage is about - to be cleaned up. This happens when your function (and by extension, `useHookState`) ceases to be called again - next frame (keyed by script, line number, and discriminator). - - Your cleanup callback is passed the storage table that's about to be cleaned up. You can then perform cleanup work, - like disconnecting events. - - *Or*, you could return `true`, and abort cleaning up altogether. If you abort cleanup, your storage will stick - around another frame (even if your function wasn't called again). This can be used when you know that the user will - (or might) eventually call your function again, even if they didn't this frame. (For example, caching a value for - a number of seconds). - - If cleanup is aborted, your cleanup function will continue to be called every frame, until you don't abort cleanup, - or the user actually calls your function again. - - ### Example: useThrottle - - This is the entire implementation of the built-in `useThrottle` function: - - ```lua - local function cleanup(storage) - return os.clock() < storage.expiry - end - - local function useThrottle(seconds, discriminator) - local storage = useHookState(discriminator, cleanup) - - if storage.time == nil or os.clock() - storage.time >= seconds then - storage.time = os.clock() - storage.expiry = os.clock() + seconds - return true - end - - return false - end - ``` - - A lot of talk for something so simple, right? - - @param discriminator? any -- A unique value to additionally key by - @param cleanupCallback (storage: {}) -> boolean? -- A function to run when the storage for this hook is cleaned up -]=] -local function useHookState(discriminator, cleanupCallback): {} - local file, line = debug.info(3, "sl") - local fn = debug.info(2, "f") - - local baseKey = string.format("%s:%s:%d", tostring(fn), file, line) - - local currentFrame = stack[#stack] - - if currentFrame == nil then - error("Attempt to access topologically-aware storage outside of a Loop-system context.", 3) - end - - if not currentFrame.accessedKeys[baseKey] then - currentFrame.accessedKeys[baseKey] = {} - end - - local accessedKeys = currentFrame.accessedKeys[baseKey] - - local key = #accessedKeys - - if discriminator ~= nil then - if type(discriminator) == "number" then - discriminator = tostring(discriminator) - end - - key = discriminator - end - - accessedKeys[key] = true - - if not currentFrame.node.system[baseKey] then - currentFrame.node.system[baseKey] = { - storage = {}, - cleanupCallback = cleanupCallback, - } - end - - local storage = currentFrame.node.system[baseKey].storage - - if not storage[key] then - storage[key] = {} - end - - return storage[key] -end - -local topoRuntime = { - start = start, - useHookState = useHookState, - useFrameState = useFrameState, - useCurrentSystem = useCurrentSystem, - withinTopoContext = withinTopoContext, -} - - ---[=[ - @class Component - - A component is a named piece of data that exists on an entity. - Components are created and removed in the [World](/api/World). - - In the docs, the terms "Component" and "ComponentInstance" are used: - - **"Component"** refers to the base class of a specific type of component you've created. - This is what [`Matter.component`](/api/Matter#component) returns. - - **"Component Instance"** refers to an actual piece of data that can exist on an entity. - The metatable of a component instance table is its respective Component table. - - Component instances are *plain-old data*: they do not contain behaviors or methods. - - Since component instances are immutable, one helper function exists on all component instances, `patch`, - which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations. -]=] - ---[=[ - @within Component - @type ComponentInstance {} - - The `ComponentInstance` type refers to an actual piece of data that can exist on an entity. - The metatable of the component instance table is set to its particular Component table. - - A component instance can be created by calling the Component table: - - ```lua - -- Component: - local MyComponent = Matter.component("My component") - - -- component instance: - local myComponentInstance = MyComponent({ - some = "data" - }) - - print(getmetatable(myComponentInstance) == MyComponent) --> true - ``` -]=] - --- This is a special value we set inside the component's metatable that will allow us to detect when --- a Component is accidentally inserted as a Component Instance. --- It should not be accessible through indexing into a component instance directly. -local DIAGNOSTIC_COMPONENT_MARKER = {} - -local nextId = 0 -local function newComponent(name, defaultData) - name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") - assert( - defaultData == nil or type(defaultData) == "table", - "if component default data is specified, it must be a table" - ) - - local component = {} - component.__index = component - - function component.new(data) - data = data or {} - - if defaultData then - data = merge(defaultData, data) - end - - return table.freeze(setmetatable(data, component)) - end - - --[=[ - @within Component - - ```lua - for id, target in world:query(Target) do - if shouldChangeTarget(target) then - world:insert(id, target:patch({ -- modify the existing component - currentTarget = getNewTarget() - })) - end - end - ``` - - A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table - will override those of the existing component instance. - - As all components are immutable and frozen, it is not possible to modify the existing component directly. - - You can use the `Matter.None` constant to remove a value from the component instance: - - ```lua - target:patch({ - currentTarget = Matter.None -- sets currentTarget to nil - }) - ``` - - @param partialNewData {} -- The table to be merged with the existing component data. - @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. - ]=] - function component:patch(partialNewData) - local patch = getmetatable(self).new(merge(self, partialNewData)) - return patch - end - - nextId += 1 - local id = nextId - - setmetatable(component, { - __call = function(_, ...) - return component.new(...) - end, - __tostring = function() - return name - end, - __len = function() - return id - end, - [DIAGNOSTIC_COMPONENT_MARKER] = true, - }) - - return component -end - -local function assertValidType(value, position) - if typeof(value) ~= "table" then - error(string.format("Component #%d is invalid: not a table", position), 3) - end - - local metatable = getmetatable(value) - - if metatable == nil then - error(string.format("Component #%d is invalid: has no metatable", position), 3) - end -end - -local function assertValidComponent(value, position) - assertValidType(value, position) - - local metatable = getmetatable(value) - - if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then - error( - string.format( - "Component #%d is invalid: Component Instance %s was passed instead of the Component itself!", - position, - tostring(metatable) - ), - 3 - ) - end -end - -local function assertValidComponentInstance(value, position) - assertValidType(value, position) - - if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then - error( - string.format( - "Component #%d is invalid: passed a Component instead of a Component instance; " - .. "did you forget to call it as a function?", - position - ), - 3 - ) - end -end - -local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" -local ERROR_DUPLICATE_ENTITY = - "The world already contains an entity with ID %d. Use World:replace instead if this is intentional." -local ERROR_NO_COMPONENTS = "Missing components" - -type i53 = number -type i24 = number - -type Component = { [any]: any } -type ComponentInstance = Component - -type Ty = { i53 } -type ArchetypeId = number - -type Column = { any } - -type Archetype = { - -- Unique identifier of this archetype - id: number, - edges: { - [i24]: { - add: Archetype, - remove: Archetype, - }, - }, - types: Ty, - type: string | number, - entities: { number }, - columns: { Column }, - records: {}, -} - -type Record = { - archetype: Archetype, - row: number, -} - -type EntityIndex = { [i24]: Record } -type ComponentIndex = { [i24]: ArchetypeMap } - -type ArchetypeRecord = number -type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord }, size: number } -type Archetypes = { [ArchetypeId]: Archetype } - -local function transitionArchetype( - entityIndex: EntityIndex, - to: Archetype, - destinationRow: i24, - from: Archetype, - sourceRow: i24 -) - local columns = from.columns - local sourceEntities = from.entities - local destinationEntities = to.entities - local destinationColumns = to.columns - local tr = to.records - local types = from.types - - for componentId, column in columns do - local targetColumn = destinationColumns[tr[types[componentId]]] - if targetColumn then - targetColumn[destinationRow] = column[sourceRow] - end - - if sourceRow ~= #column then - column[sourceRow] = column[#column] - column[#column] = nil - end - end - - destinationEntities[destinationRow] = sourceEntities[sourceRow] - entityIndex[sourceEntities[sourceRow]].row = destinationRow - - local movedAway = #sourceEntities - if sourceRow ~= movedAway then - sourceEntities[sourceRow] = sourceEntities[movedAway] - entityIndex[sourceEntities[movedAway]].row = sourceRow - end - - sourceEntities[movedAway] = nil -end - -local function archetypeAppend(entity: i53, archetype: Archetype): i24 - local entities = archetype.entities - table.insert(entities, entity) - return #entities -end - -local function newEntity(entityId: i53, record: Record, archetype: Archetype) - local row = archetypeAppend(entityId, archetype) - record.archetype = archetype - record.row = row - return record -end - -local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) - local sourceRow = record.row - local from = record.archetype - local destinationRow = archetypeAppend(entityId, to) - transitionArchetype(entityIndex, to, destinationRow, from, sourceRow) - record.archetype = to - record.row = destinationRow -end - -local function hash(arr): string | number - return table.concat(arr, "_") -end - -local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype) - local destinationCount = #to.types - local destinationIds = to.types - - for i = 1, destinationCount do - local destinationId = destinationIds[i] - - if not componentIndex[destinationId] then - componentIndex[destinationId] = { sparse = {}, size = 0 } - end - componentIndex[destinationId].sparse[to.id] = i - to.records[destinationId] = i - end -end - -local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype - local ty = hash(types) - - world.nextArchetypeId = (world.nextArchetypeId :: number) + 1 - local id = world.nextArchetypeId - - local columns = {} :: { any } - - for _ in types do - table.insert(columns, {}) - end - - local archetype = { - id = id, - types = types, - type = ty, - columns = columns, - entities = {}, - edges = {}, - records = {}, - } - - world.archetypeIndex[ty] = archetype - world.archetypes[id] = archetype - - if #types > 0 then - createArchetypeRecords(world.componentIndex, archetype, prev) - end - - return archetype -end - -local World = {} -World.__index = World - -function World.new() - local self = setmetatable({ - entityIndex = {}, - componentIndex = {}, - componentIdToComponent = {}, - archetypes = {}, - archetypeIndex = {}, - nextId = 0, - nextArchetypeId = 0, - _size = 0, - _changedStorage = {}, - }, World) - - self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil) - return self -end - -type World = typeof(World.new()) - -local function ensureArchetype(world: World, types, prev) - if #types < 1 then - return world.ROOT_ARCHETYPE - end - - local ty = hash(types) - local archetype = world.archetypeIndex[ty] - if archetype then - return archetype - end - - return archetypeOf(world, types, prev) -end - -local function findInsert(types: { i53 }, toAdd: i53) - local count = #types - for i = 1, count do - local id = types[i] - if id == toAdd then - return -1 - end - if id > toAdd then - return i - end - end - return count + 1 -end - -local function findArchetypeWith(world: World, node: Archetype, componentId: i53) - local types = node.types - local at = findInsert(types, componentId) - if at == -1 then - return node - end - - local destinationType = table.clone(node.types) - table.insert(destinationType, at, componentId) - return ensureArchetype(world, destinationType, node) -end - -local function ensureEdge(archetype: Archetype, componentId: i53) - if not archetype.edges[componentId] then - archetype.edges[componentId] = {} :: any - end - return archetype.edges[componentId] -end - -local function archetypeTraverseAdd(world: World, componentId: i53, archetype: Archetype?): Archetype - local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype - local edge = ensureEdge(from, componentId) - - if not edge.add then - edge.add = findArchetypeWith(world, from, componentId) - end - - return edge.add -end - -local function componentAdd(world: World, entityId: i53, componentInstance) - local component = getmetatable(componentInstance) - local componentId = #component - - -- TODO: - -- This never gets cleaned up - world.componentIdToComponent[componentId] = component - - local record = world:ensureRecord(entityId) - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype) - - if sourceArchetype == destinationArchetype then - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = componentInstance - return - end - - if sourceArchetype then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) - else - -- if it has any components, then it wont be the root archetype - if #destinationArchetype.types > 0 then - newEntity(entityId, record, destinationArchetype) - end - end - - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = componentInstance -end - -function World.ensureRecord(world: World, entityId: i53) - local entityIndex = world.entityIndex - local id = entityId - if not entityIndex[id] then - entityIndex[id] = {} :: Record - end - return entityIndex[id] -end - -local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype - local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype - local edge = ensureEdge(from, componentId) - - if not edge.remove then - local to = table.clone(from.types) - table.remove(to, table.find(to, componentId)) - edge.remove = ensureArchetype(world, to, from) - end - - return edge.remove -end - -local function get(componentIndex: ComponentIndex, record: Record, componentId: i24): ComponentInstance? - local archetype = record.archetype - if archetype == nil then - return nil - end - - local archetypeRecord = archetype.records[componentId] - if not archetypeRecord then - return nil - end - - return archetype.columns[archetypeRecord][record.row] -end - -local function componentRemove(world: World, entityId: i53, component: Component) - local componentId = #component - local record = world:ensureRecord(entityId) - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) - - -- TODO: - -- There is a better way to get the component for returning - local componentInstance = get(world.componentIndex, record, componentId) - if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) - end - - return componentInstance -end - ---[=[ - Removes a component (or set of components) from an existing entity. - - ```lua - local removedA, removedB = world:remove(entityId, ComponentA, ComponentB) - ``` - - @param entityId number -- The entity ID - @param ... Component -- The components to remove - @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed. -]=] -function World.remove(world: World, entityId: i53, ...) - if not world:contains(entityId) then - error(ERROR_NO_ENTITY, 2) - end - - local length = select("#", ...) - local removed = {} - for i = 1, length do - table.insert(removed, componentRemove(world, entityId, select(i, ...))) - end - - return unpack(removed, 1, length) -end - -function World.get( - world: World, - entityId: i53, - a: Component, - b: Component?, - c: Component?, - d: Component?, - e: Component? -): any - local componentIndex = world.componentIndex - local record = world.entityIndex[entityId] - if not record then - return nil - end - - local va = get(componentIndex, record, #a) - - if b == nil then - return va - elseif c == nil then - return va, get(componentIndex, record, #b) - elseif d == nil then - return va, get(componentIndex, record, #b), get(componentIndex, record, #c) - elseif e == nil then - return va, get(componentIndex, record, #b), get(componentIndex, record, #c), get(componentIndex, record, #d) - else - error("args exceeded") - end -end - -function World.insert(world: World, entityId: i53, ...) - if not world:contains(entityId) then - error(ERROR_NO_ENTITY, 2) - end - - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - local oldComponent = world:get(entityId, metatable) - componentAdd(world, entityId, newComponent) - - world:_trackChanged(metatable, entityId, oldComponent, newComponent) - end -end - -function World.replace(world: World, entityId: i53, ...: ComponentInstance) - error("Replace is unimplemented") - - if not world:contains(entityId) then - error(ERROR_NO_ENTITY, 2) - end - - --moveEntity(entityId, record, world.ROOT_ARCHETYPE) - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - assertValidComponentInstance(newComponent, i) - end -end - -function World.entity(world: World) - world.nextId += 1 - return world.nextId -end - -function World:__iter() - local previous = nil - return function() - local entityId, data = next(self.entityIndex, previous) - previous = entityId - - if entityId == nil then - return nil - end - - local archetype = data.archetype - if not archetype then - return entityId, {} - end - - local columns = archetype.columns - local components = {} - for i, map in columns do - local componentId = archetype.types[i] - components[self.componentIdToComponent[componentId]] = map[data.row] - end - - return entityId, components - end -end - -function World._trackChanged(world: World, metatable, id, old, new) - if not world._changedStorage[metatable] then - return - end - - if old == new then - return - end - - local record = table.freeze({ - old = old, - new = new, - }) - - for _, storage in ipairs(world._changedStorage[metatable]) do - -- If this entity has changed since the last time this system read it, - -- we ensure that the "old" value is whatever the system saw it as last, instead of the - -- "old" value we have here. - if storage[id] then - storage[id] = table.freeze({ old = storage[id].old, new = new }) - else - storage[id] = record - end - end -end - ---[=[ - Spawns a new entity in the world with a specific entity ID and given components. - - The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID. - - @param entityId number -- The entity ID to spawn with - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The same entity ID that was passed in -]=] -function World.spawnAt(world: World, entityId: i53, ...: ComponentInstance) - if world:contains(entityId) then - error(string.format(ERROR_DUPLICATE_ENTITY, entityId), 2) - end - - if entityId >= world.nextId then - world.nextId = entityId + 1 - end - - world._size += 1 - world:ensureRecord(entityId) - - local components = {} - for i = 1, select("#", ...) do - local component = select(i, ...) - assertValidComponentInstance(component, i) - - local metatable = getmetatable(component) - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) - end - - world:_trackChanged(metatable, entityId, nil, component) - - components[metatable] = component - componentAdd(world, entityId, component) - end - - return entityId -end - ---[=[ - Spawns a new entity in the world with the given components. - - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The new entity ID. -]=] -function World.spawn(world: World, ...: ComponentInstance) - return world:spawnAt(world.nextId, ...) -end - -function World.despawn(world: World, entityId: i53) - local entityIndex = world.entityIndex - local record = entityIndex[entityId] - moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) - world.ROOT_ARCHETYPE.entities[record.row] = nil - entityIndex[entityId] = nil - world._size -= 1 -end - -function World.clear(world: World) - world.entityIndex = {} - world.componentIndex = {} - world.archetypes = {} - world.archetypeIndex = {} - world._size = 0 - world.ROOT_ARCHETYPE = archetypeOf(world, {}, nil) -end - -function World.size(world: World) - return world._size -end - -function World.contains(world: World, entityId: i53) - return world.entityIndex[entityId] ~= nil -end - -local function noop(): any - return function() end -end - -local emptyQueryResult = setmetatable({ - next = function() end, - snapshot = function() - return {} - end, - without = function(self) - return self - end, - view = function() - return { - get = function() end, - contains = function() end, - } - end, -}, { - __iter = noop, - __call = noop, -}) - -local function queryResult(compatibleArchetypes, components: { number }, queryLength, ...): any - local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ... - local lastArchetype, archetype = next(compatibleArchetypes) - if not lastArchetype then - return emptyQueryResult - end - - local lastRow - local queryOutput = {} - local function iterate() - local row = next(archetype.entities, lastRow) - while row == nil do - lastArchetype, archetype = next(compatibleArchetypes, lastArchetype) - if lastArchetype == nil then - return - end - row = next(archetype.entities, row) - end - - lastRow = row - - local columns = archetype.columns - local entityId = archetype.entities[row :: number] - local archetypeRecords = archetype.records - - if queryLength == 1 then - return entityId, columns[archetypeRecords[a]][row] - elseif queryLength == 2 then - return entityId, columns[archetypeRecords[a]][row], columns[archetypeRecords[b]][row] - elseif queryLength == 3 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row] - elseif queryLength == 4 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row] - elseif queryLength == 5 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row] - elseif queryLength == 6 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row], - columns[archetypeRecords[f]][row] - elseif queryLength == 7 then - return columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row], - columns[archetypeRecords[f]][row], - columns[archetypeRecords[g]][row] - elseif queryLength == 8 then - return columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row], - columns[archetypeRecords[f]][row], - columns[archetypeRecords[g]][row], - columns[archetypeRecords[h]][row] - end - - for i, componentId in components do - queryOutput[i] = columns[archetypeRecords[componentId]][row] - end - - return entityId, unpack(queryOutput, 1, queryLength) - 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 - - -- TODO: - -- remove in matter 1.0 - function QueryResult:__call() - return iterate() - end - - function QueryResult:__iter() - return function() - return iterate() - end - end - - --[=[ - Returns an iterator that will skip any entities that also have the given components. - - @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 components = { ... } - for i, component in components do - components[i] = #component - end - - local compatibleArchetypes = compatibleArchetypes - for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i] - local shouldRemove = false - for _, componentId in components do - if archetype.records[componentId] then - shouldRemove = true - break - end - end - - if shouldRemove then - table.remove(compatibleArchetypes, i) - end - end - - lastArchetype, archetype = next(compatibleArchetypes) - if not lastArchetype then - return emptyQueryResult - end - - return 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 iterate() - end - - local function drain() - local entry = table.pack(iterate()) - return if entry.n > 0 then entry else nil - end - - local Snapshot = { - __iter = function(self): any - local i = 0 - return function() - i += 1 - - local data = self[i] :: any - - if data then - return unpack(data, 1, data.n) - end - - return - end - end, - } - - function QueryResult:snapshot() - local list = setmetatable({}, Snapshot) :: any - for entry in drain do - table.insert(list, entry) - end - - return list - 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 - local inflicting = world:query(Damage, Hitting, Player):view() - for _, source in world:query(DamagedBy) do - local damage = inflicting:get(source.from) - end - - for _ in world:query(Damage):view() do end -- You can still iterate views if you want! - ``` - - @return View See [View](/api/View) docs. - ]=] - function QueryResult:view() - local fetches = {} - local list = {} :: any - - local View = {} - View.__index = View - - function View:__iter() - local current = list.head - return function() - if not current then - return - end - local entity = current.entity - local fetch = fetches[entity] - current = current.next - - return entity, unpack(fetch, 1, fetch.n) - end - end - - --[=[ - @within View - 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 = fetches[entity] - local queryLength = fetch.n - - if queryLength == 1 then - return fetch[1] - elseif queryLength == 2 then - return fetch[1], fetch[2] - elseif queryLength == 3 then - return fetch[1], fetch[2], fetch[3] - elseif queryLength == 4 then - return fetch[1], fetch[2], fetch[3], fetch[4] - elseif queryLength == 5 then - return fetch[1], fetch[2], fetch[3], fetch[4], fetch[5] - end - - return unpack(fetch, 1, fetch.n) - end - - --[=[ - @within View - Equivalent to `world:contains()` - @param entity number - the entity ID - @return boolean - ]=] - function View:contains(entity) - return fetches[entity] ~= nil - end - - for entry in drain do - local entityId = entry[1] - local fetch = table.pack(select(2, unpack(entry))) - local node = { entity = entityId, next = nil } - fetches[entityId] = fetch - - if not list.head then - list.head = node - else - local current = list.head - while current.next do - current = current.next - end - current.next = node - end - end - - return setmetatable({}, View) - end - - return setmetatable({}, QueryResult) -end - ---[=[ - Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over - the results of the query. - - Order of iteration is not guaranteed. - - ```lua - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - - for id in world:query(Target):without(Model) do - -- Again, with feeling - 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. -]=] -function World.query(world: World, ...: Component): any - local compatibleArchetypes = {} - local components = { ... } - local archetypes = world.archetypes - local queryLength = select("#", ...) - local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ... - - if queryLength == 0 then - return emptyQueryResult - end - - if queryLength == 1 then - a = #a - components = { a } - -- local archetypesMap = world.componentIndex[a] - -- components = { a } - -- local function single() - -- local id = next(archetypesMap) - -- local archetype = archetypes[id :: number] - -- local lastRow - - -- return function(): any - -- local row, entity = next(archetype.entities, lastRow) - -- while row == nil do - -- id = next(archetypesMap, id) - -- if id == nil then - -- return - -- end - -- archetype = archetypes[id] - -- row = next(archetype.entities, row) - -- end - -- lastRow = row - - -- return entity, archetype.columns[archetype.records[a]] - -- end - -- end - -- return single() - elseif queryLength == 2 then - --print("iter double") - a = #a - b = #b - components = { a, b } - - -- --print(a, b, world.componentIndex) - -- --[[local archetypesMap = world.componentIndex[a] - -- for id in archetypesMap do - -- local archetype = archetypes[id] - -- if archetype.records[b] then - -- table.insert(compatibleArchetypes, archetype) - -- end - -- end - - -- local function double(): () -> (number, any, any) - -- local lastArchetype, archetype = next(compatibleArchetypes) - -- local lastRow - - -- return function() - -- local row = next(archetype.entities, lastRow) - -- while row == nil do - -- lastArchetype, archetype = next(compatibleArchetypes, lastArchetype) - -- if lastArchetype == nil then - -- return - -- end - - -- row = next(archetype.entities, row) - -- end - -- lastRow = row - - -- local entity = archetype.entities[row :: number] - -- local columns = archetype.columns - -- local archetypeRecords = archetype.records - -- return entity, columns[archetypeRecords[a]], columns[archetypeRecords[b]] - -- end - -- end - -- return double() - elseif queryLength == 3 then - a = #a - b = #b - c = #c - components = { a, b, c } - elseif queryLength == 4 then - a = #a - b = #b - c = #c - d = #d - - components = { a, b, c, d } - elseif queryLength == 5 then - a = #a - b = #b - c = #c - d = #d - e = #e - - components = { a, b, c, d, e } - elseif queryLength == 6 then - a = #a - b = #b - c = #c - d = #d - e = #e - f = #f - - components = { a, b, c, d, e, f } - elseif queryLength == 7 then - a = #a - b = #b - c = #c - d = #d - e = #e - f = #f - g = #g - - components = { a, b, c, d, e, f, g } - elseif queryLength == 8 then - a = #a - b = #b - c = #c - d = #d - e = #e - f = #f - g = #g - h = #h - - components = { a, b, c, d, e, f, g, h } - else - for i, component in components do - components[i] = (#component) :: any - end - end - - local firstArchetypeMap - local componentIndex = world.componentIndex - for _, componentId in (components :: any) :: { number } do - local map = componentIndex[componentId] - if not map then - return emptyQueryResult - end - - if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then - firstArchetypeMap = map - end - end - - for id in firstArchetypeMap.sparse do - local archetype = archetypes[id] - local archetypeRecords = archetype.records - local matched = true - for _, componentId in components do - if not archetypeRecords[componentId] then - matched = false - break - end - end - - if matched then - table.insert(compatibleArchetypes, archetype) - end - end - - return queryResult(compatibleArchetypes, components :: any, queryLength, a, b, c, d, e, f, g, h) -end - -local function cleanupQueryChanged(hookState) - local world = hookState.world - local componentToTrack = hookState.componentToTrack - - for index, object in world._changedStorage[componentToTrack] do - if object == hookState.storage then - table.remove(world._changedStorage[componentToTrack], index) - break - end - end - - if next(world._changedStorage[componentToTrack]) == nil then - world._changedStorage[componentToTrack] = nil - end -end - -function World.queryChanged(world: World, componentToTrack, ...: nil) - if ... then - error("World:queryChanged does not take any additional parameters", 2) - end - - local hookState = topoRuntime.useHookState(componentToTrack, cleanupQueryChanged) :: any - if hookState.storage then - return function(): any - local entityId, record = next(hookState.storage) - - if entityId then - hookState.storage[entityId] = nil - - return entityId, record - end - return - end - end - - if not world._changedStorage[componentToTrack] then - world._changedStorage[componentToTrack] = {} - end - - local storage = {} - hookState.storage = storage - hookState.world = world - hookState.componentToTrack = componentToTrack - - table.insert(world._changedStorage[componentToTrack], storage) - - local queryResult = world:query(componentToTrack) - - return function(): any - local entityId, component = queryResult:next() - - if entityId then - return entityId, table.freeze({ new = component }) - end - return - end -end - -return { - World = World, - component = newComponent -} diff --git a/oldMatter.lua b/oldMatter.lua deleted file mode 100644 index 0baf7a7..0000000 --- a/oldMatter.lua +++ /dev/null @@ -1,1567 +0,0 @@ - -local None = {} - -local function merge(one, two) - local new = table.clone(one) - - for key, value in two do - if value == None then - new[key] = nil - else - new[key] = value - end - end - - return new -end - --- https://github.com/freddylist/llama/blob/master/src/List/toSet.lua -local function toSet(list) - local set = {} - - for _, v in ipairs(list) do - set[v] = true - end - - return set -end - --- https://github.com/freddylist/llama/blob/master/src/Dictionary/values.lua -local function values(dictionary) - local valuesList = {} - - local index = 1 - - for _, value in pairs(dictionary) do - valuesList[index] = value - index = index + 1 - end - - return valuesList -end - -local valueIds = {} -local nextValueId = 0 -local compatibilityCache = {} -local archetypeCache = {} - -local function getValueId(value) - local valueId = valueIds[value] - if valueId == nil then - valueIds[value] = nextValueId - valueId = nextValueId - nextValueId += 1 - end - - return valueId -end - -function archetypeOf(...) - local length = select("#", ...) - - local currentNode = archetypeCache - - for i = 1, length do - local nextNode = currentNode[select(i, ...)] - - if not nextNode then - nextNode = {} - currentNode[select(i, ...)] = nextNode - end - - currentNode = nextNode - end - - if currentNode._archetype then - return currentNode._archetype - end - - local list = table.create(length) - - for i = 1, length do - list[i] = getValueId(select(i, ...)) - end - - table.sort(list) - - local archetype = table.concat(list, "_") - - currentNode._archetype = archetype - - return archetype -end - -function negateArchetypeOf(...) - return string.gsub(archetypeOf(...), "_", "x") -end - -function areArchetypesCompatible(queryArchetype, targetArchetype) - local archetypes = string.split(queryArchetype, "x") - local baseArchetype = table.remove(archetypes, 1) - - local cachedCompatibility = compatibilityCache[queryArchetype .. "-" .. targetArchetype] - if cachedCompatibility ~= nil then - return cachedCompatibility - end - - local queryIds = string.split(baseArchetype, "_") - local targetIds = toSet(string.split(targetArchetype, "_")) - local excludeIds = toSet(archetypes) - - for _, queryId in ipairs(queryIds) do - if targetIds[queryId] == nil then - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false - return false - end - end - - for excludeId in excludeIds do - if targetIds[excludeId] then - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false - return false - end - end - - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = true - - return true - -end - -local stack = {} - -local function newStackFrame(node) - return { - node = node, - accessedKeys = {}, - } -end - -local function cleanup() - local currentFrame = stack[#stack] - - for baseKey, state in pairs(currentFrame.node.system) do - for key, value in pairs(state.storage) do - if not currentFrame.accessedKeys[baseKey] or not currentFrame.accessedKeys[baseKey][key] then - local cleanupCallback = state.cleanupCallback - - if cleanupCallback then - local shouldAbortCleanup = cleanupCallback(value) - - if shouldAbortCleanup then - continue - end - end - - state.storage[key] = nil - end - end - end -end - -local function start(node, fn) - table.insert(stack, newStackFrame(node)) - fn() - cleanup() - table.remove(stack, #stack) -end - -local function withinTopoContext() - return #stack ~= 0 -end - -local function useFrameState() - return stack[#stack].node.frame -end - -local function useCurrentSystem() - if #stack == 0 then - return - end - - return stack[#stack].node.currentSystem -end - - ---[=[ - @within Matter - - :::tip - **Don't use this function directly in your systems.** - - This function is used for implementing your own topologically-aware functions. It should not be used in your - systems directly. You should use this function to implement your own utilities, similar to `useEvent` and - `useThrottle`. - ::: - - `useHookState` does one thing: it returns a table. An empty, pristine table. Here's the cool thing though: - it always returns the *same* table, based on the script and line where *your function* (the function calling - `useHookState`) was called. - - ### Uniqueness - - If your function is called multiple times from the same line, perhaps within a loop, the default behavior of - `useHookState` is to uniquely identify these by call count, and will return a unique table for each call. - - However, you can override this behavior: you can choose to key by any other value. This means that in addition to - script and line number, the storage will also only return the same table if the unique value (otherwise known as the - "discriminator") is the same. - - ### Cleaning up - As a second optional parameter, you can pass a function that is automatically invoked when your storage is about - to be cleaned up. This happens when your function (and by extension, `useHookState`) ceases to be called again - next frame (keyed by script, line number, and discriminator). - - Your cleanup callback is passed the storage table that's about to be cleaned up. You can then perform cleanup work, - like disconnecting events. - - *Or*, you could return `true`, and abort cleaning up altogether. If you abort cleanup, your storage will stick - around another frame (even if your function wasn't called again). This can be used when you know that the user will - (or might) eventually call your function again, even if they didn't this frame. (For example, caching a value for - a number of seconds). - - If cleanup is aborted, your cleanup function will continue to be called every frame, until you don't abort cleanup, - or the user actually calls your function again. - - ### Example: useThrottle - - This is the entire implementation of the built-in `useThrottle` function: - - ```lua - local function cleanup(storage) - return os.clock() < storage.expiry - end - - local function useThrottle(seconds, discriminator) - local storage = useHookState(discriminator, cleanup) - - if storage.time == nil or os.clock() - storage.time >= seconds then - storage.time = os.clock() - storage.expiry = os.clock() + seconds - return true - end - - return false - end - ``` - - A lot of talk for something so simple, right? - - @param discriminator? any -- A unique value to additionally key by - @param cleanupCallback (storage: {}) -> boolean? -- A function to run when the storage for this hook is cleaned up -]=] -local function useHookState(discriminator, cleanupCallback): {} - local file, line = debug.info(3, "sl") - local fn = debug.info(2, "f") - - local baseKey = string.format("%s:%s:%d", tostring(fn), file, line) - - local currentFrame = stack[#stack] - - if currentFrame == nil then - error("Attempt to access topologically-aware storage outside of a Loop-system context.", 3) - end - - if not currentFrame.accessedKeys[baseKey] then - currentFrame.accessedKeys[baseKey] = {} - end - - local accessedKeys = currentFrame.accessedKeys[baseKey] - - local key = #accessedKeys - - if discriminator ~= nil then - if type(discriminator) == "number" then - discriminator = tostring(discriminator) - end - - key = discriminator - end - - accessedKeys[key] = true - - if not currentFrame.node.system[baseKey] then - currentFrame.node.system[baseKey] = { - storage = {}, - cleanupCallback = cleanupCallback, - } - end - - local storage = currentFrame.node.system[baseKey].storage - - if not storage[key] then - storage[key] = {} - end - - return storage[key] -end - -local topoRuntime = { - start = start, - useHookState = useHookState, - useFrameState = useFrameState, - useCurrentSystem = useCurrentSystem, - withinTopoContext = withinTopoContext, -} - - - -local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" - ---[=[ - @class World - - A World contains entities which have components. - The World is queryable and can be used to get entities with a specific set of components. - Entities are simply ever-increasing integers. -]=] -local World = {} -World.__index = World - ---[=[ - Creates a new World. -]=] -function World.new() - local firstStorage = {} - - return setmetatable({ - -- List of maps from archetype string --> entity ID --> entity data - _storages = { firstStorage }, - -- The most recent storage that has not been dirtied by an iterator - _pristineStorage = firstStorage, - - -- Map from entity ID -> archetype string - _entityArchetypes = {}, - - -- Cache of the component metatables on each entity. Used for generating archetype. - -- Map of entity ID -> array - _entityMetatablesCache = {}, - - -- Cache of what query archetypes are compatible with what component archetypes - _queryCache = {}, - - -- Cache of what entity archetypes have ever existed in the game. This is used for knowing - -- when to update the queryCache. - _entityArchetypeCache = {}, - - -- The next ID that will be assigned with World:spawn - _nextId = 1, - - -- The total number of active entities in the world - _size = 0, - - -- Storage for `queryChanged` - _changedStorage = {}, - }, World) -end - --- Searches all archetype storages for the entity with the given archetype --- Returns the storage that the entity is in if it exists, otherwise nil -function World:_getStorageWithEntity(archetype, id) - for _, storage in self._storages do - local archetypeStorage = storage[archetype] - if archetypeStorage then - if archetypeStorage[id] then - return storage - end - end - end - return nil -end - -function World:_markStorageDirty() - local newStorage = {} - table.insert(self._storages, newStorage) - self._pristineStorage = newStorage - - if topoRuntime.withinTopoContext() then - local frameState = topoRuntime.useFrameState() - - frameState.dirtyWorlds[self] = true - end -end - -function World:_getEntity(id) - local archetype = self._entityArchetypes[id] - local storage = self:_getStorageWithEntity(archetype, id) - - return storage[archetype][id] -end - -function World:_next(last) - local entityId, archetype = next(self._entityArchetypes, last) - - if entityId == nil then - return nil - end - - local storage = self:_getStorageWithEntity(archetype, entityId) - - return entityId, storage[archetype][entityId] -end - ---[=[ - Iterates over all entities in this World. Iteration returns entity ID followed by a dictionary mapping - Component to Component Instance. - - **Usage:** - - ```lua - for entityId, entityData in world do - print(entityId, entityData[Components.Example]) - end - ``` - - @return number - @return {[Component]: ComponentInstance} -]=] -function World:__iter() - return World._next, self -end - ---[=[ - Spawns a new entity in the world with the given components. - - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The new entity ID. -]=] -function World:spawn(...) - return self:spawnAt(self._nextId, ...) -end - ---[=[ - @class Component - - A component is a named piece of data that exists on an entity. - Components are created and removed in the [World](/api/World). - - In the docs, the terms "Component" and "ComponentInstance" are used: - - **"Component"** refers to the base class of a specific type of component you've created. - This is what [`Matter.component`](/api/Matter#component) returns. - - **"Component Instance"** refers to an actual piece of data that can exist on an entity. - The metatable of a component instance table is its respective Component table. - - Component instances are *plain-old data*: they do not contain behaviors or methods. - - Since component instances are immutable, one helper function exists on all component instances, `patch`, - which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations. -]=] - ---[=[ - @within Component - @type ComponentInstance {} - - The `ComponentInstance` type refers to an actual piece of data that can exist on an entity. - The metatable of the component instance table is set to its particular Component table. - - A component instance can be created by calling the Component table: - - ```lua - -- Component: - local MyComponent = Matter.component("My component") - - -- component instance: - local myComponentInstance = MyComponent({ - some = "data" - }) - - print(getmetatable(myComponentInstance) == MyComponent) --> true - ``` -]=] - --- This is a special value we set inside the component's metatable that will allow us to detect when --- a Component is accidentally inserted as a Component Instance. --- It should not be accessible through indexing into a component instance directly. -local DIAGNOSTIC_COMPONENT_MARKER = {} - -local function newComponent(name, defaultData) - name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") - - assert( - defaultData == nil or type(defaultData) == "table", - "if component default data is specified, it must be a table" - ) - - local component = {} - component.__index = component - - function component.new(data) - data = data or {} - - if defaultData then - data = merge(defaultData, data) - end - - return table.freeze(setmetatable(data, component)) - end - - --[=[ - @within Component - - ```lua - for id, target in world:query(Target) do - if shouldChangeTarget(target) then - world:insert(id, target:patch({ -- modify the existing component - currentTarget = getNewTarget() - })) - end - end - ``` - - A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table - will override those of the existing component instance. - - As all components are immutable and frozen, it is not possible to modify the existing component directly. - - You can use the `Matter.None` constant to remove a value from the component instance: - - ```lua - target:patch({ - currentTarget = Matter.None -- sets currentTarget to nil - }) - ``` - - @param partialNewData {} -- The table to be merged with the existing component data. - @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. - ]=] - function component:patch(partialNewData) - local patch = getmetatable(self).new(merge(self, partialNewData)) - return patch - end - - setmetatable(component, { - __call = function(_, ...) - return component.new(...) - end, - __tostring = function() - return name - end, - [DIAGNOSTIC_COMPONENT_MARKER] = true, - }) - - return component -end - -local function assertValidType(value, position) - if typeof(value) ~= "table" then - error(string.format("Component #%d is invalid: not a table", position), 3) - end - - local metatable = getmetatable(value) - - if metatable == nil then - error(string.format("Component #%d is invalid: has no metatable", position), 3) - end -end - -local function assertValidComponent(value, position) - assertValidType(value, position) - - local metatable = getmetatable(value) - - if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then - error( - string.format( - "Component #%d is invalid: Component Instance %s was passed instead of the Component itself!", - position, - tostring(metatable) - ), - 3 - ) - end -end - -local function assertValidComponentInstance(value, position) - assertValidType(value, position) - - if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then - error( - string.format( - "Component #%d is invalid: passed a Component instead of a Component instance; " - .. "did you forget to call it as a function?", - position - ), - 3 - ) - end -end - ---[=[ - Spawns a new entity in the world with a specific entity ID and given components. - - The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID. - - @param id number -- The entity ID to spawn with - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The same entity ID that was passed in -]=] -function World:spawnAt(id, ...) - if self:contains(id) then - error( - string.format( - "The world already contains an entity with ID %d. Use World:replace instead if this is intentional.", - id - ), - 2 - ) - end - - self._size += 1 - - if id >= self._nextId then - self._nextId = id + 1 - end - - local components = {} - local metatables = {} - - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) - end - - self:_trackChanged(metatable, id, nil, newComponent) - - components[metatable] = newComponent - table.insert(metatables, metatable) - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, components) - - return id -end - -function World:_newQueryArchetype(queryArchetype) - if self._queryCache[queryArchetype] == nil then - self._queryCache[queryArchetype] = {} - else - return -- Archetype isn't actually new - end - - for _, storage in self._storages do - for entityArchetype in storage do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - self._queryCache[queryArchetype][entityArchetype] = true - end - end - end -end - -function World:_updateQueryCache(entityArchetype) - for queryArchetype, compatibleArchetypes in pairs(self._queryCache) do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - compatibleArchetypes[entityArchetype] = true - end - end -end - -function World:_transitionArchetype(id, components) - local newArchetype = nil - local oldArchetype = self._entityArchetypes[id] - local oldStorage - - if oldArchetype then - oldStorage = self:_getStorageWithEntity(oldArchetype, id) - - if not components then - oldStorage[oldArchetype][id] = nil - end - end - - if components then - newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id])) - - if oldArchetype ~= newArchetype then - if oldStorage then - oldStorage[oldArchetype][id] = nil - end - - if self._pristineStorage[newArchetype] == nil then - self._pristineStorage[newArchetype] = {} - end - - if self._entityArchetypeCache[newArchetype] == nil then - self._entityArchetypeCache[newArchetype] = true - self:_updateQueryCache(newArchetype) - end - self._pristineStorage[newArchetype][id] = components - else - oldStorage[newArchetype][id] = components - end - end - - self._entityArchetypes[id] = newArchetype -end - ---[=[ - Replaces a given entity by ID with an entirely new set of components. - Equivalent to removing all components from an entity, and then adding these ones. - - @param id number -- The entity ID - @param ... ComponentInstance -- The component values to spawn the entity with. -]=] -function World:replace(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local components = {} - local metatables = {} - local entity = self:_getEntity(id) - - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) - end - - self:_trackChanged(metatable, id, entity[metatable], newComponent) - - components[metatable] = newComponent - table.insert(metatables, metatable) - end - - for metatable, component in pairs(entity) do - if not components[metatable] then - self:_trackChanged(metatable, id, component, nil) - end - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, components) -end - ---[=[ - Despawns a given entity by ID, removing it and all its components from the world entirely. - - @param id number -- The entity ID -]=] -function World:despawn(id) - local entity = self:_getEntity(id) - - for metatable, component in pairs(entity) do - self:_trackChanged(metatable, id, component, nil) - end - - self._entityMetatablesCache[id] = nil - self:_transitionArchetype(id, nil) - - self._size -= 1 -end - ---[=[ - Removes all entities from the world. - - :::caution - Removing entities in this way is not reported by `queryChanged`. - ::: -]=] -function World:clear() - local firstStorage = {} - self._storages = { firstStorage } - self._pristineStorage = firstStorage - self._entityArchetypes = {} - self._entityMetatablesCache = {} - self._size = 0 - self._changedStorage = {} -end - ---[=[ - Checks if the given entity ID is currently spawned in this world. - - @param id number -- The entity ID - @return bool -- `true` if the entity exists -]=] -function World:contains(id) - return self._entityArchetypes[id] ~= nil -end - ---[=[ - Gets a specific component (or set of components) from a specific entity in this world. - - @param id number -- The entity ID - @param ... Component -- The components to fetch - @return ... -- Returns the component values in the same order they were passed in -]=] -function World:get(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local length = select("#", ...) - - if length == 1 then - assertValidComponent((...), 1) - return entity[...] - end - - local components = {} - for i = 1, length do - local metatable = select(i, ...) - assertValidComponent(metatable, i) - components[i] = entity[metatable] - end - - return unpack(components, 1, length) -end - -local function noop() end - -local noopQuery = setmetatable({ - next = noop, - snapshot = noop, - without = function(self) - return self - end, - 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 = negateArchetypeOf(...) - - 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 - ---[=[ - @class View - - Provides random access to the results of a query. - - Calling the View is equivalent to iterating a query. - - ```lua - for id, player, health, poison in world:query(Player, Health, Poison):view() do - -- Do something - 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 - local inflicting = world:query(Damage, Hitting, Player):view() - for _, source in world:query(DamagedBy) do - local damage = inflicting:get(source.from) - end - - for _ in world:query(Damage):view() do end -- You can still iterate views if you want! - ``` - - @return View See [View](/api/View) docs. -]=] - -function QueryResult:view() - local function iter() - return nextItem(self) - end - - local fetches = {} - local list = {} :: any - - local View = {} - View.__index = View - - function View:__iter() - local current = list.head - return function() - if not current then - return - end - local entity = current.entity - local fetch = fetches[entity] - current = current.next - - return entity, unpack(fetch, 1, fetch.n) - end - end - - --[=[ - @within View - 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 = fetches[entity] - local queryLength = fetch.n - - if queryLength == 1 then - return fetch[1] - elseif queryLength == 2 then - return fetch[1], fetch[2] - elseif queryLength == 3 then - return fetch[1], fetch[2], fetch[3] - elseif queryLength == 4 then - return fetch[1], fetch[2], fetch[3], fetch[4] - elseif queryLength == 5 then - return fetch[1], fetch[2], fetch[3], fetch[4], fetch[5] - end - - return unpack(fetch, 1, fetch.n) - end - - --[=[ - @within View - Equivalent to `world:contains()` - @param entity number - the entity ID - @return boolean - ]=] - - function View:contains(entity) - return fetches[entity] ~= nil - end - - 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 } - - fetches[entityId] = fetch - - if not list.head then - list.head = node - else - local current = list.head - while current.next do - current = current.next - end - current.next = node - end - end - end - - return setmetatable({}, View) -end - ---[=[ - Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over - the results of the query. - - Order of iteration is not guaranteed. - - ```lua - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - - for id in world:query(Target):without(Model) do - -- Again, with feeling - 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. -]=] - -function World:query(...) - assertValidComponent((...), 1) - - local metatables = { ... } - local queryLength = select("#", ...) - - local archetype = archetypeOf(...) - - if self._queryCache[archetype] == nil then - self:_newQueryArchetype(archetype) - end - - local compatibleArchetypes = self._queryCache[archetype] - - if next(compatibleArchetypes) == nil then - -- If there are no compatible storages avoid creating our complicated iterator - return noopQuery - end - - local queryOutput = table.create(queryLength) - - local function expand(entityId, entityData) - if not entityId then - return - end - - if queryLength == 1 then - return entityId, entityData[metatables[1]] - elseif queryLength == 2 then - return entityId, entityData[metatables[1]], entityData[metatables[2]] - elseif queryLength == 3 then - return entityId, entityData[metatables[1]], entityData[metatables[2]], entityData[metatables[3]] - elseif queryLength == 4 then - return entityId, - entityData[metatables[1]], - entityData[metatables[2]], - entityData[metatables[3]], - entityData[metatables[4]] - elseif queryLength == 5 then - return entityId, - entityData[metatables[1]], - entityData[metatables[2]], - entityData[metatables[3]], - entityData[metatables[4]], - entityData[metatables[5]] - end - - for i, metatable in ipairs(metatables) do - queryOutput[i] = entityData[metatable] - end - - return entityId, unpack(queryOutput, 1, queryLength) - end - - if self._pristineStorage == self._storages[1] then - self:_markStorageDirty() - end - - return QueryResult.new(self, expand, archetype, compatibleArchetypes) -end - -local function cleanupQueryChanged(hookState) - local world = hookState.world - local componentToTrack = hookState.componentToTrack - - for index, object in world._changedStorage[componentToTrack] do - if object == hookState.storage then - table.remove(world._changedStorage[componentToTrack], index) - break - end - end - - if next(world._changedStorage[componentToTrack]) == nil then - world._changedStorage[componentToTrack] = nil - end -end - ---[=[ - @interface ChangeRecord - @within World - .new? ComponentInstance -- The new value of the component. Nil if just removed. - .old? ComponentInstance -- The former value of the component. Nil if just added. -]=] - ---[=[ - :::info Topologically-aware function - This function is only usable if called within the context of [`Loop:begin`](/api/Loop#begin). - ::: - - Queries for components that have changed **since the last time your system ran `queryChanged`**. - - Only one changed record is returned per entity, even if the same entity changed multiple times. The order - in which changed records are returned is not guaranteed to be the order that the changes occurred in. - - It should be noted that `queryChanged` does not have the same iterator invalidation concerns as `World:query`. - - :::tip - The first time your system runs (i.e., on the first frame), all existing entities in the world that match your query - are returned as "new" change records. - ::: - - :::info - Calling this function from your system creates storage internally for your system. Then, changes meeting your - criteria are pushed into your storage. Calling `queryChanged` again each frame drains this storage. - - If your system isn't called every frame, the storage will continually fill up and does not empty unless you drain - it. - - If you stop calling `queryChanged` in your system, changes will stop being tracked. - ::: - - ### Returns - `queryChanged` returns an iterator function, so you call it in a for loop just like `World:query`. - - The iterator returns the entity ID, followed by a [`ChangeRecord`](#ChangeRecord). - - The `ChangeRecord` type is a table that contains two fields, `new` and `old`, respectively containing the new - component instance, and the old component instance. `new` and `old` will never be the same value. - - `new` will be nil if the component was removed (or the entity was despawned), and `old` will be nil if the - component was just added. - - The `old` field will be the value of the component the last time this system observed it, not - necessarily the value it changed from most recently. - - The `ChangeRecord` table is potentially shared with multiple systems tracking changes for this component, so it - cannot be modified. - - ```lua - for id, record in world:queryChanged(Model) do - if record.new == nil then - -- Model was removed - - if enemy.type == "this is a made up example" then - world:remove(id, Enemy) - end - end - end - ``` - - @param componentToTrack Component -- The component you want to listen to changes for. - @return () -> (id, ChangeRecord) -- Iterator of entity ID and change record -]=] -function World:queryChanged(componentToTrack, ...: nil) - if ... then - error("World:queryChanged does not take any additional parameters", 2) - end - - local hookState = topoRuntime.useHookState(componentToTrack, cleanupQueryChanged) - - if hookState.storage then - return function(): any - local entityId, record = next(hookState.storage) - - if entityId then - hookState.storage[entityId] = nil - - return entityId, record - end - return - end - end - - if not self._changedStorage[componentToTrack] then - self._changedStorage[componentToTrack] = {} - end - - local storage = {} - hookState.storage = storage - hookState.world = self - hookState.componentToTrack = componentToTrack - - table.insert(self._changedStorage[componentToTrack], storage) - - local queryResult = self:query(componentToTrack) - - return function(): any - local entityId, component = queryResult:next() - - if entityId then - return entityId, table.freeze({ new = component }) - end - return - end -end - -function World:_trackChanged(metatable, id, old, new) - if not self._changedStorage[metatable] then - return - end - - if old == new then - return - end - - local record = table.freeze({ - old = old, - new = new, - }) - - for _, storage in ipairs(self._changedStorage[metatable]) do - -- If this entity has changed since the last time this system read it, - -- we ensure that the "old" value is whatever the system saw it as last, instead of the - -- "old" value we have here. - if storage[id] then - storage[id] = table.freeze({ old = storage[id].old, new = new }) - else - storage[id] = record - end - end -end - ---[=[ - Inserts a component (or set of components) into an existing entity. - - If another instance of a given component already exists on this entity, it is replaced. - - ```lua - world:insert( - entityId, - ComponentA({ - foo = "bar" - }), - ComponentB({ - baz = "qux" - }) - ) - ``` - - @param id number -- The entity ID - @param ... ComponentInstance -- The component values to insert -]=] -function World:insert(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local wasNew = false - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - local oldComponent = entity[metatable] - - if not oldComponent then - wasNew = true - - table.insert(self._entityMetatablesCache[id], metatable) - end - - self:_trackChanged(metatable, id, oldComponent, newComponent) - - entity[metatable] = newComponent - end - - if wasNew then -- wasNew - self:_transitionArchetype(id, entity) - end -end - ---[=[ - Removes a component (or set of components) from an existing entity. - - ```lua - local removedA, removedB = world:remove(entityId, ComponentA, ComponentB) - ``` - - @param id number -- The entity ID - @param ... Component -- The components to remove - @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed. -]=] -function World:remove(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local length = select("#", ...) - local removed = {} - - for i = 1, length do - local metatable = select(i, ...) - - assertValidComponent(metatable, i) - - local oldComponent = entity[metatable] - - removed[i] = oldComponent - - self:_trackChanged(metatable, id, oldComponent, nil) - - entity[metatable] = nil - end - - -- Rebuild entity metatable cache - local metatables = {} - - for metatable in pairs(entity) do - table.insert(metatables, metatable) - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, entity) - - return unpack(removed, 1, length) -end - ---[=[ - Returns the number of entities currently spawned in the world. -]=] -function World:size() - return self._size -end - ---[=[ - :::tip - [Loop] automatically calls this function on your World(s), so there is no need to call it yourself if you're using - a Loop. - ::: - - If you are not using a Loop, you should call this function at a regular interval (i.e., once per frame) to optimize - the internal storage for queries. - - This is part of a strategy to eliminate iterator invalidation when modifying the World while inside a query from - [World:query]. While inside a query, any changes to the World are stored in a separate location from the rest of - the World. Calling this function combines the separate storage back into the main storage, which speeds things up - again. -]=] -function World:optimizeQueries() - if #self._storages == 1 then - return - end - - local firstStorage = self._storages[1] - - for i = 2, #self._storages do - local storage = self._storages[i] - - for archetype, entities in storage do - if firstStorage[archetype] == nil then - firstStorage[archetype] = entities - else - for entityId, entityData in entities do - if firstStorage[archetype][entityId] then - error("Entity ID already exists in first storage...") - end - firstStorage[archetype][entityId] = entityData - end - end - end - end - - table.clear(self._storages) - - self._storages[1] = firstStorage - self._pristineStorage = firstStorage -end - -return { - World = World, - component = newComponent -}