diff --git a/concord/component.lua b/concord/component.lua index bf862a0..eece914 100644 --- a/concord/component.lua +++ b/concord/component.lua @@ -18,6 +18,7 @@ function Component.new(populate) local componentClass = setmetatable({ __populate = populate, + __name = nil, __isComponentClass = true, }, Component.__mt) @@ -32,22 +33,44 @@ end function Component:__populate() -- luacheck: ignore end +function Component:serialize() -- luacheck: ignore +end + +function Component:deserialize(data) -- luacheck: ignore +end + +--- Internal: Creates a new Component. +-- @return A new Component +function Component:__new() + local component = setmetatable({ + __componentClass = self, + + __isComponent = true, + __isComponentClass = false, + }, self.__mt) + + return component +end + --- Internal: Creates and populates a new Component. -- @param ... Varargs passed to the populate function -- @return A new populated Component function Component:__initialize(...) - local component = setmetatable({ - __componentClass = self, - - __isComponent = true, - __isComponentClass = false, - }, self) + local component = self:__new() self.__populate(component, ...) return component end +function Component:hasName() + return self.__name and true or false +end + +function Component:getName() + return self.__name +end + return setmetatable(Component, { __call = function(_, ...) return Component.new(...) diff --git a/concord/components.lua b/concord/components.lua index ba88c7e..522cf78 100644 --- a/concord/components.lua +++ b/concord/components.lua @@ -20,12 +20,22 @@ function Components.register(name, componentClass) end if (rawget(Components, name)) then - error("bad argument #2 to 'Components.register' (ComponentClass with name '"..name.."' was already registerd)", 3) + error("bad argument #2 to 'Components.register' (ComponentClass with name '"..name.."' was already registerd)", 3) -- luacheck: ignore end Components[name] = componentClass + componentClass.__name = name end +function Components.has(name) + return Components[name] and true or false +end + +function Components.get(name) + return Components[name] +end + + return setmetatable(Components, { __index = function(_, name) error("Attempt to index ComponentClass '"..tostring(name).."' that does not exist / was not registered", 2) diff --git a/concord/entity.lua b/concord/entity.lua index cce5213..8359fd8 100644 --- a/concord/entity.lua +++ b/concord/entity.lua @@ -5,7 +5,8 @@ local PATH = (...):gsub('%.[^%.]+$', '') -local Type = require(PATH..".type") +local Components = require(PATH..".components") +local Type = require(PATH..".type") local Entity = {} Entity.__mt = { @@ -173,6 +174,41 @@ function Entity:getWorld() return self.__world end +function Entity:serialize() + local data = {} + + for _, component in pairs(self.__components) do + if component.__name then + local componentData = component:serialize() + componentData.__name = component.__name + + data[#data + 1] = componentData + end + end + + return data +end + +function Entity:deserialize(data) + for i = 1, #data do + local componentData = data[i] + + if (not Components[componentData.__name]) then + error("bad argument #1 to 'Entity:deserialize' (ComponentClass "..type(componentData.__name).." wasn't yet loaded)") -- luacheck: ignore + end + + local componentClass = Components[componentData.__name] + + local component = componentClass:__new() + component:deserialize(componentData) + + self[componentClass] = component + self.__components[componentClass] = component + + self:__dirty() + end +end + return setmetatable(Entity, { __call = function(_, ...) return Entity.new(...) diff --git a/concord/world.lua b/concord/world.lua index 4fbcc9a..18520cd 100644 --- a/concord/world.lua +++ b/concord/world.lua @@ -6,9 +6,10 @@ local PATH = (...):gsub('%.[^%.]+$', '') -local Type = require(PATH..".type") -local List = require(PATH..".list") -local Utils = require(PATH..".utils") +local Entity = require(PATH..".entity") +local Type = require(PATH..".type") +local List = require(PATH..".list") +local Utils = require(PATH..".utils") local World = { ENABLE_OPTIMIZATION = true, @@ -21,10 +22,10 @@ World.__mt = { -- @return The new World function World.new() local world = setmetatable({ - entities = List(), - systems = List(), + __entities = List(), + __systems = List(), - events = {}, + __events = {}, __added = List(), __backAdded = List(), __removed = List(), __backRemoved = List(), @@ -104,10 +105,10 @@ function World:__flush() for i = 1, self.__backAdded.size do e = self.__backAdded[i] - self.entities:__add(e) + self.__entities:__add(e) - for j = 1, self.systems.size do - self.systems[j]:__evaluate(e) + for j = 1, self.__systems.size do + self.__systems[j]:__evaluate(e) end self:onEntityAdded(e) @@ -119,10 +120,10 @@ function World:__flush() e = self.__backRemoved[i] e.__world = nil - self.entities:__remove(e) + self.__entities:__remove(e) - for j = 1, self.systems.size do - self.systems[j]:__remove(e) + for j = 1, self.__systems.size do + self.__systems[j]:__remove(e) end self:onEntityRemoved(e) @@ -133,8 +134,8 @@ function World:__flush() for i = 1, self.__backDirty.size do e = self.__backDirty[i] - for j = 1, self.systems.size do - self.systems[j]:__evaluate(e) + for j = 1, self.__systems.size do + self.__systems[j]:__evaluate(e) end end self.__backDirty:__clear() @@ -168,18 +169,18 @@ function World:addSystem(systemClass) local system = systemClass(self) self.__systemLookup[systemClass] = system - self.systems:__add(system) + self.__systems:__add(system) for callbackName, callback in pairs(systemClass) do -- Skip callback if its blacklisted if (not blacklistedSystemFunctions[callbackName]) then -- Make container for all listeners of the callback if it does not exist yet - if (not self.events[callbackName]) then - self.events[callbackName] = {} + if (not self.__events[callbackName]) then + self.__events[callbackName] = {} end -- Add callback to listeners - local listeners = self.events[callbackName] + local listeners = self.__events[callbackName] listeners[#listeners + 1] = { system = system, callback = callback, @@ -188,8 +189,8 @@ function World:addSystem(systemClass) end -- Evaluate all existing entities - for j = 1, self.entities.size do - system:__evaluate(self.entities[j]) + for j = 1, self.__entities.size do + system:__evaluate(self.__entities[j]) end return self @@ -243,7 +244,7 @@ function World:emit(functionName, ...) error("bad argument #1 to 'World:emit' (String expected, got "..type(functionName)..")") end - local listeners = self.events[functionName] + local listeners = self.__events[functionName] if listeners then for i = 1, #listeners do @@ -263,17 +264,58 @@ end --- Removes all entities from the World -- @return self function World:clear() - for i = 1, self.entities.size do - self:removeEntity(self.entities[i]) + for i = 1, self.__entities.size do + self:removeEntity(self.__entities[i]) end - for i = 1, self.systems.size do - self.systems[i]:__clear() + for i = 1, self.__systems.size do + self.__systems[i]:__clear() end return self end +function World:getEntities() + return self.__entities +end + +function World:getSystems() + return self.__systems +end + +function World:serialize() + self:__flush() + + local data = {} + + for i = 1, self.__entities.size do + local entity = self.__entities[i] + + local entityData = entity:serialize() + + data[i] = entityData + end + + return data +end + +function World:deserialize(data, append) + if (not append) then + self:clear() + end + + for i = 1, #data do + local entityData = data[i] + + local entity = Entity() + entity:deserialize(entityData) + + self:addEntity(entity) + end + + self:__flush() +end + --- Callback for when an Entity is added to the World. -- @param e The Entity that was added function World:onEntityAdded(e) -- luacheck: ignore diff --git a/examples/serialization/init.lua b/examples/serialization/init.lua new file mode 100644 index 0000000..2b5e05d --- /dev/null +++ b/examples/serialization/init.lua @@ -0,0 +1,69 @@ +local Concord = require("concord") + +local function display(t) + print("Table: " ..tostring(t)) + for key, value in pairs(t) do + if type(value) == "table" then + display(value) + else + print(key, value) + end + end +end + +local test_component_1 = Concord.component(function(e, x, y) + e.x = x or 0 + e.y = y or 0 +end) +Concord.components.register("test_component_1", test_component_1) + +function test_component_1:serialize() + return { + x = self.x, + y = self.y, + } +end + +function test_component_1:deserialize(data) + self.x = data.x or 0 + self.y = data.y or 0 +end + +local test_component_2 = Concord.component(function(e, foo) + e.foo = foo +end) +Concord.components.register("test_component_2", test_component_2) + +function test_component_2:serialize() + return { + foo = self.foo + } +end + +function test_component_2:deserialize(data) + self.foo = data.foo +end + +-- Test worlds +local world_1 = Concord.world() +local world_2 = Concord.world() + +-- Test Entity +Concord.entity(world_1) +:give(test_component_1, 100, 50) +:give(test_component_2, "Hello World!") + +-- Serialize world +local data = world_1:serialize() + +-- Deserialize world +world_2:deserialize(data) + +-- Check result +local test_entity_copy = world_2:getEntities()[1] + +local test_comp_1 = test_entity_copy[test_component_1] +local test_comp_2 = test_entity_copy[test_component_2] + +print(test_comp_1.x, test_comp_1.y) +print(test_comp_2.foo) \ No newline at end of file diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..7cfd88a --- /dev/null +++ b/main.lua @@ -0,0 +1 @@ +require("examples.serialization") \ No newline at end of file