diff --git a/.vscode/launch.json b/.vscode/launch.json index d8d9e70..255d999 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,15 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Launch Evolved All", + "type": "lua-local", + "request": "launch", + "program": { + "lua": "luajit", + "file": "${workspaceFolder}/develop/all.lua" + } + }, { "name": "Launch Evolved Example", "type": "lua-local", diff --git a/develop/all.lua b/develop/all.lua new file mode 100644 index 0000000..e386ff5 --- /dev/null +++ b/develop/all.lua @@ -0,0 +1,3 @@ +require 'develop.example' +require 'develop.unbench' +require 'develop.untests' diff --git a/develop/example.lua b/develop/example.lua index 59ece27..ab3c48f 100644 --- a/develop/example.lua +++ b/develop/example.lua @@ -1 +1,14 @@ local evolved = require 'evolved' + +local registry = evolved.registry() + +local fragments = { + position = registry:entity(), + velocity = registry:entity(), +} + +do + local entity = registry:entity() + entity:insert(fragments.position) + entity:insert(fragments.velocity) +end diff --git a/develop/unbench.lua b/develop/unbench.lua index 75c959b..24ca578 100644 --- a/develop/unbench.lua +++ b/develop/unbench.lua @@ -1,4 +1,5 @@ local evolved = require 'evolved' +local utilities = require 'develop.utilities' ---@param name string ---@param func fun(...):... @@ -6,7 +7,7 @@ local evolved = require 'evolved' local function describe(name, func, ...) collectgarbage('stop') - print(string.format('- %s ...', name)) + print(string.format('| unbench | %s ...', name)) local start_s = os.clock() local start_kb = collectgarbage('count') @@ -23,3 +24,18 @@ local function describe(name, func, ...) collectgarbage('restart') end + +describe('memory footprint of 1k entities', function() + local registry = evolved.registry() + for _ = 1, 1000 do registry:entity() end +end) + +describe('memory footprint of 10k entities', function() + local registry = evolved.registry() + for _ = 1, 10000 do registry:entity() end +end) + +describe('memory footprint of 100k entities', function() + local registry = evolved.registry() + for _ = 1, 100000 do registry:entity() end +end) diff --git a/develop/untests.lua b/develop/untests.lua index 75c959b..48fe230 100644 --- a/develop/untests.lua +++ b/develop/untests.lua @@ -1,4 +1,5 @@ local evolved = require 'evolved' +local utilities = require 'develop.utilities' ---@param name string ---@param func fun(...):... @@ -6,7 +7,7 @@ local evolved = require 'evolved' local function describe(name, func, ...) collectgarbage('stop') - print(string.format('- %s ...', name)) + print(string.format('| untests | %s ...', name)) local start_s = os.clock() local start_kb = collectgarbage('count') @@ -23,3 +24,72 @@ 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 + 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 + + ---@type evolved.entity[] + local remove_fragments = {} + local remove_fragment_count = math.random(1, 10) + for i = 1, remove_fragment_count do remove_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) + + utilities.shuffle_array(remove_fragments) + for _, fragment in ipairs(remove_fragments) do e1:remove(fragment) end + + utilities.shuffle_array(remove_fragments) + for _, fragment in ipairs(remove_fragments) do e2:remove(fragment) end + + assert(e1.chunk == e2.chunk) + end + end +end) diff --git a/develop/utilities.lua b/develop/utilities.lua new file mode 100644 index 0000000..33cc5d5 --- /dev/null +++ b/develop/utilities.lua @@ -0,0 +1,11 @@ +local utilities = {} + +---@param vs any[] +function utilities.shuffle_array(vs) + for i = 1, #vs do + local j = math.random(i, #vs) + vs[i], vs[j] = vs[j], vs[i] + end +end + +return utilities diff --git a/evolved.lua b/evolved.lua index 5c5d2c6..ee981e8 100644 --- a/evolved.lua +++ b/evolved.lua @@ -1,4 +1,238 @@ ---@class evolved local evolved = {} +--- +--- +--- +--- +--- + +---@class evolved.chunk +---@field owner evolved.registry +---@field parent? evolved.chunk +---@field fragment? evolved.entity +---@field entities evolved.entity[] +---@field with_cache table +---@field without_cache table +local evolved_chunk_mt = {} +evolved_chunk_mt.__index = evolved_chunk_mt + +---@class evolved.entity +---@field owner evolved.registry +---@field id integer +---@field chunk? evolved.chunk +---@field index_in_chunk? integer +local evolved_entity_mt = {} +evolved_entity_mt.__index = evolved_entity_mt + +---@class evolved.registry +---@field nextid integer +---@field chunks evolved.chunk[] +local evolved_registry_mt = {} +evolved_registry_mt.__index = evolved_registry_mt + +--- +--- +--- +--- +--- + +---@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 = {}, + with_cache = {}, + without_cache = {}, + } + return setmetatable(chunk, evolved_chunk_mt) +end + +---@param owner evolved.registry +---@param id integer +local function create_entity(owner, id) + ---@type evolved.entity + local entity = { + owner = owner, + id = id, + } + return setmetatable(entity, evolved_entity_mt) +end + +local function create_registry() + ---@type evolved.registry + local registry = { + nextid = 1, + chunks = {}, + } + + registry.chunks[1] = create_chunk(registry) + + return setmetatable(registry, evolved_registry_mt) +end + +--- +--- +--- CHUNK API +--- +--- + +function evolved_chunk_mt:__tostring() + local id, iter = '', self + while iter and iter.parent and iter.fragment do + id, iter = iter.fragment.id .. (id == '' and '' or ',') .. id, iter.parent + end + return string.format('evolved.chunk(%s)', id) +end + +---@param fragment evolved.entity +---@return evolved.chunk +function evolved_chunk_mt:with(fragment) + do + local with_chunk = self.with_cache[fragment] + if with_chunk then return with_chunk end + end + + if self.fragment and self.fragment.id == fragment.id then + return self + end + + if not self.fragment or self.fragment.id < fragment.id then + local new_chunk = create_chunk(self.owner, self, fragment) + self.owner.chunks[#self.owner.chunks + 1] = new_chunk + + self.with_cache[fragment] = new_chunk + new_chunk.without_cache[fragment] = self + + return new_chunk + end + + do + local sibling_chunk = self.parent + :with(fragment) + :with(self.fragment) + + self.with_cache[fragment] = sibling_chunk + + return sibling_chunk + end +end + +---@param fragment evolved.entity +---@return evolved.chunk +function evolved_chunk_mt:without(fragment) + do + local without_chunk = self.without_cache[fragment] + if without_chunk then return without_chunk end + end + + if not self.fragment or self.fragment.id < fragment.id then + return self + end + + do + local sibling_chunk = self.parent + :without(fragment) + :with(self.fragment) + + self.without_cache[fragment] = sibling_chunk + + return sibling_chunk + end +end + +---@param entity evolved.entity +function evolved_chunk_mt:insert(entity) + self.entities[#self.entities + 1] = entity + entity.chunk, entity.index_in_chunk = self, #self.entities +end + +---@param entity evolved.entity +function evolved_chunk_mt:remove(entity) + local last_entity = self.entities[#self.entities] + + if entity ~= last_entity then + self.entities[entity.index_in_chunk] = last_entity + last_entity.index_in_chunk = entity.index_in_chunk + end + + self.entities[#self.entities] = nil + entity.chunk, entity.index_in_chunk = nil, 0 +end + +--- +--- +--- ENTITY API +--- +--- + +function evolved_entity_mt:__tostring() + return string.format('evolved.entity(%d)', self.id) +end + +function evolved_entity_mt:destroy() + self.chunk:remove(self) +end + +---@param fragment evolved.entity +function evolved_entity_mt:insert(fragment) + local old_chunk = assert(self.chunk) + local new_chunk = old_chunk:with(fragment) + + old_chunk:remove(self) + new_chunk:insert(self) +end + +---@param fragment evolved.entity +function evolved_entity_mt:remove(fragment) + local old_chunk = assert(self.chunk) + local new_chunk = old_chunk:without(fragment) + + old_chunk:remove(self) + new_chunk:insert(self) +end + +--- +--- +--- REGISTRY API +--- +--- + +---@return evolved.chunk +function evolved_registry_mt:root() + return self.chunks[1] +end + +---@return evolved.entity +function evolved_registry_mt:entity() + local id = self.nextid + self.nextid = self.nextid + 1 + local entity = create_entity(self, id) + self.chunks[1]:insert(entity) + return entity +end + +--- +--- +--- MODULE API +--- +--- + +---@return evolved.registry +function evolved.registry() + return create_registry() +end + +--- +--- +--- +--- +--- + return evolved