diff --git a/develop/example.lua b/develop/example.lua index 8fdbefa..deb727e 100644 --- a/develop/example.lua +++ b/develop/example.lua @@ -17,4 +17,12 @@ do local query = registry:query( fragments.position, fragments.velocity) + + for chunk in query:chunks() do + local ps = chunk.components[fragments.position] + local vs = chunk.components[fragments.position] + + for i = 1, #chunk.entities do + end + end end diff --git a/develop/iterators.lua b/develop/iterators.lua new file mode 100644 index 0000000..04a6a8d --- /dev/null +++ b/develop/iterators.lua @@ -0,0 +1,78 @@ +local iterators = {} + +---@generic K +---@param t table +---@return fun(): K +function iterators.keys(t) + if #t > 0 then + local i = 0 + return function() + i = i + 1 + if i <= #t then + return i + end + end + else + local k = nil + return function() + local v + k, v = next(t, k) + if k ~= nil then + return k + end + end + end +end + +---@generic V +---@param t table +---@return fun(): V? +function iterators.values(t) + if #t > 0 then + local i = 0 + return function() + i = i + 1 + if i <= #t then + return t[i] + end + end + else + local k = nil + return function() + local v + k, v = next(t, k) + if k ~= nil then + return v + end + end + end +end + +---@generic V +---@param iter fun(): V? +---@return integer +function iterators.count(iter) + local count = 0 + for _ in iter do count = count + 1 end + return count +end + +---@generic V +---@param iter fun(): V? +---@param func fun(v: V): boolean +---@return fun(): V? +function iterators.filter(iter, func) + return function() + while true do + local v = iter() + if v == nil then + return + end + if func(v) then + return v + end + end + end +end + +return iterators diff --git a/develop/untests.lua b/develop/untests.lua index 48fe230..9cbb2c0 100644 --- a/develop/untests.lua +++ b/develop/untests.lua @@ -1,4 +1,6 @@ local evolved = require 'evolved' + +local iterators = require 'develop.iterators' local utilities = require 'develop.utilities' ---@param name string @@ -25,36 +27,8 @@ local function describe(name, func, ...) collectgarbage('restart') end -describe('random entity:insert', function() - for _ = 1, 1000 do - local registry = evolved.registry() - - ---@type evolved.entity[] - local all_fragments = {} - local all_fragment_count = math.random(1, 10) - for i = 1, all_fragment_count do all_fragments[i] = registry:entity() end - - for _ = 1, 100 do - local e1, e2 = registry:entity(), registry:entity() - - ---@type evolved.entity[] - local insert_fragments = {} - local insert_fragment_count = math.random(1, 10) - for i = 1, insert_fragment_count do insert_fragments[i] = all_fragments[math.random(1, all_fragment_count)] end - - utilities.shuffle_array(insert_fragments) - for _, fragment in ipairs(insert_fragments) do e1:insert(fragment) end - - utilities.shuffle_array(insert_fragments) - for _, fragment in ipairs(insert_fragments) do e2:insert(fragment) end - - assert(e1.chunk == e2.chunk) - end - end -end) - -describe('random entity:remove', function() - for _ = 1, 1000 do +describe('entity:chunk', function() + for _ = 1, 500 do local registry = evolved.registry() ---@type evolved.entity[] @@ -93,3 +67,61 @@ describe('random entity:remove', function() end end end) + +describe('query:chunks', function() + for _ = 1, 500 do + local registry = evolved.registry() + + ---@type evolved.entity[] + local all_fragments = {} + local all_fragment_count = math.random(1, 10) + for i = 1, all_fragment_count do all_fragments[i] = registry:entity() end + + local function check_all_queries() + for _, q in ipairs(registry.queries) do + local query_chunk_count = iterators.count(q:chunks()) + local registry_chunk_count = iterators.count(iterators.filter( + iterators.values(registry.chunks), + function(chunk) return chunk:has_all_fragments(unpack(q.fragments)) end)) + assert(query_chunk_count == registry_chunk_count) + end + end + + ---@param n integer + local function create_queries(n) + for _ = 1, n do + ---@type evolved.entity[] + local query_fragments = {} + local query_fragment_count = math.random(1, 5) + for i = 1, query_fragment_count do + query_fragments[i] = all_fragments[math.random(1, all_fragment_count)] + end + + registry:query(unpack(query_fragments)) + end + end + + ---@param n integer + local function create_entities(n) + for _ = 1, n do + ---@type evolved.entity[] + local insert_fragments = {} + local insert_fragment_count = math.random(1, 10) + for i = 1, insert_fragment_count do + insert_fragments[i] = all_fragments[math.random(1, all_fragment_count)] + end + + local e = registry:entity() + for _, fragment in ipairs(insert_fragments) do e:insert(fragment) end + end + end + + create_entities(50) + create_queries(10) + check_all_queries() + + create_entities(50) + create_queries(10) + check_all_queries() + end +end) diff --git a/evolved.lua b/evolved.lua index 7041580..d7db386 100644 --- a/evolved.lua +++ b/evolved.lua @@ -11,6 +11,7 @@ local evolved = {} ---@field owner evolved.registry ---@field parent? evolved.chunk ---@field fragment? evolved.entity +---@field children evolved.chunk[] ---@field entities evolved.entity[] ---@field components table ---@field with_cache table @@ -21,6 +22,8 @@ evolved_chunk_mt.__index = evolved_chunk_mt ---@class evolved.query ---@field owner evolved.registry ---@field id integer +---@field roots evolved.chunk[] +---@field fragments evolved.entity[] local evolved_query_mt = {} evolved_query_mt.__index = evolved_query_mt @@ -37,109 +40,11 @@ evolved_entity_mt.__index = evolved_entity_mt ---@field chunks evolved.chunk[] ---@field queries evolved.query[] ---@field entities evolved.entity[] +---@field chunks_by_fragment table +---@field queries_by_fragment table local evolved_registry_mt = {} evolved_registry_mt.__index = evolved_registry_mt ---- ---- ---- ---- ---- - ----@param owner evolved.registry ----@param chunk evolved.chunk -local function on_new_chunk(owner, chunk) - owner.chunks[#owner.chunks + 1] = chunk -end - ----@param owner evolved.registry ----@param query evolved.query -local function on_new_query(owner, query) - owner.queries[#owner.queries + 1] = query -end - ----@param owner evolved.registry ----@param entity evolved.entity -local function on_new_entity(owner, entity) - owner.entities[#owner.entities + 1] = entity -end - ---- ---- ---- ---- ---- - ----@param owner evolved.registry ----@param parent? evolved.chunk ----@param fragment? evolved.entity ----@return evolved.chunk -local function create_chunk(owner, parent, fragment) - ---@type evolved.chunk - local chunk = { - owner = owner, - parent = parent, - fragment = fragment, - entities = {}, - components = {}, - with_cache = {}, - without_cache = {}, - } - - do - local iter = chunk - while iter and iter.fragment do - chunk.components[iter.fragment] = {} - iter = iter.parent - end - end - - return setmetatable(chunk, evolved_chunk_mt) -end - ----@param owner evolved.registry ----@param id integer ----@return evolved.query -local function create_query(owner, id) - ---@type evolved.query - local query = { - owner = owner, - id = id, - } - return setmetatable(query, evolved_query_mt) -end - ----@param owner evolved.registry ----@param id integer ----@return evolved.entity -local function create_entity(owner, id) - ---@type evolved.entity - local entity = { - owner = owner, - id = id, - } - - owner.chunks[1]:insert(entity) - - return setmetatable(entity, evolved_entity_mt) -end - ----@return evolved.registry -local function create_registry() - ---@type evolved.registry - local registry = { - nextid = 1, - chunks = {}, - queries = {}, - entities = {}, - } - - local chunk = create_chunk(registry) - on_new_chunk(registry, chunk) - - return setmetatable(registry, evolved_registry_mt) -end - --- --- --- CHUNK API @@ -166,17 +71,7 @@ function evolved_chunk_mt:with(fragment) return self end - if not self.fragment or self.fragment.id < fragment.id then - local new_chunk = create_chunk(self.owner, self, fragment) - on_new_chunk(self.owner, new_chunk) - - self.with_cache[fragment] = new_chunk - new_chunk.without_cache[fragment] = self - - return new_chunk - end - - do + if self.fragment and self.fragment.id > fragment.id then local sibling_chunk = self.parent :with(fragment) :with(self.fragment) @@ -185,6 +80,52 @@ function evolved_chunk_mt:with(fragment) return sibling_chunk end + + ---@type evolved.chunk + local new_chunk = { + owner = self.owner, + parent = self, + fragment = fragment, + children = {}, + entities = {}, + components = {}, + with_cache = {}, + without_cache = {}, + } + setmetatable(new_chunk, evolved_chunk_mt) + self.owner.chunks[#self.owner.chunks + 1] = new_chunk + + do + local iter = new_chunk + while iter and iter.fragment do + new_chunk.components[iter.fragment] = {} + iter = iter.parent + end + end + + do + self.children[#self.children + 1] = new_chunk + end + + do + local chunks = self.owner.chunks_by_fragment[fragment] or {} + chunks[#chunks + 1] = new_chunk + self.owner.chunks_by_fragment[fragment] = chunks + end + + do + local queries = self.owner.queries_by_fragment[fragment] or {} + for _, query in ipairs(queries) do + if new_chunk:has_all_fragments(unpack(query.fragments)) then + query.roots[#query.roots + 1] = new_chunk + end + end + end + + self.with_cache[fragment] = new_chunk + new_chunk.without_cache[fragment] = self + + return new_chunk end ---@param fragment evolved.entity @@ -199,15 +140,13 @@ function evolved_chunk_mt:without(fragment) return self end - do - local sibling_chunk = self.parent - :without(fragment) - :with(self.fragment) + local sibling_chunk = self.parent + :without(fragment) + :with(self.fragment) - self.without_cache[fragment] = sibling_chunk + self.without_cache[fragment] = sibling_chunk - return sibling_chunk - end + return sibling_chunk end ---@param entity evolved.entity @@ -268,6 +207,37 @@ function evolved_chunk_mt:has_any_fragment(...) return false end +--- +--- +--- QUERY API +--- +--- + +function evolved_query_mt:__tostring() + return string.format('evolved.query(%d)', self.id) +end + +---@return fun(): evolved.chunk? +function evolved_query_mt:chunks() + return coroutine.wrap(function() + local queue = {} + + for i = #self.roots, 1, -1 do + queue[#queue + 1] = self.roots[i] + end + + while #queue > 0 do + local chunk = table.remove(queue) + + coroutine.yield(chunk) + + for i = #chunk.children, 1, -1 do + queue[#queue + 1] = chunk.children[i] + end + end + end) +end + --- --- --- ENTITY API @@ -306,18 +276,45 @@ end --- --- ----@return evolved.chunk -function evolved_registry_mt:root() - return self.chunks[1] -end - ---@param ... evolved.entity ---@return evolved.query function evolved_registry_mt:query(...) local id = self.nextid self.nextid = self.nextid + 1 - local query = create_query(self, id) - on_new_query(self, query) + + ---@type evolved.query + local query = { + owner = self, + id = id, + roots = {}, + fragments = {}, + } + setmetatable(query, evolved_query_mt) + self.queries[#self.queries + 1] = query + + do + local fragments = { ... } + table.sort(fragments, function(a, b) return a.id < b.id end) + query.fragments = fragments + end + + if #query.fragments > 0 then + local fragment = query.fragments[#query.fragments] + local chunks = self.chunks_by_fragment[fragment] or {} + for _, chunk in ipairs(chunks) do + if chunk:has_all_fragments(...) then + query.roots[#query.roots + 1] = chunk + end + end + end + + if #query.fragments > 0 then + local fragment = query.fragments[#query.fragments] + local queries = self.queries_by_fragment[fragment] or {} + queries[#queries + 1] = query + self.queries_by_fragment[fragment] = queries + end + return query end @@ -325,8 +322,21 @@ end function evolved_registry_mt:entity() local id = self.nextid self.nextid = self.nextid + 1 - local entity = create_entity(self, id) - on_new_entity(self, entity) + + ---@type evolved.entity + local entity = { + owner = self, + id = id, + chunk = nil, + index_in_chunk = nil, + } + setmetatable(entity, evolved_entity_mt) + self.entities[#self.entities + 1] = entity + + do + self.chunks[1]:insert(entity) + end + return entity end @@ -338,7 +348,33 @@ end ---@return evolved.registry function evolved.registry() - return create_registry() + ---@type evolved.registry + local registry = { + nextid = 1, + chunks = {}, + queries = {}, + entities = {}, + chunks_by_fragment = {}, + queries_by_fragment = {}, + } + setmetatable(registry, evolved_registry_mt) + + ---@type evolved.chunk + local root_chunk = { + owner = registry, + parent = nil, + fragment = nil, + children = {}, + entities = {}, + components = {}, + with_cache = {}, + without_cache = {}, + } + setmetatable(root_chunk, evolved_chunk_mt) + + registry.chunks[1] = root_chunk + + return registry end ---