diff --git a/LICENSE b/LICENSE index 8771a88..e3ce12d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 Justin van der Leij - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +MIT License + +Copyright (c) 2018 Justin van der Leij + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e93dd8a..9343d63 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Concord +# Concord diff --git a/concord/component.lua b/concord/component.lua new file mode 100644 index 0000000..eaf4f56 --- /dev/null +++ b/concord/component.lua @@ -0,0 +1,41 @@ +local Component = {} +Component.__index = Component + +--- Creates a new Component. +-- @param populate A function that populates the Bag with values +-- @param inherit States if the Bag should inherit the Component's functions +-- @return A Component object +function Component.new(populate, inherit) + local component = setmetatable({ + __populate = populate, + __inherit = inherit, + }, Component) + + if inherit then + component.__mt = {__index = component} + end + + return component +end + +--- Creates and initializes a new Bag. +-- @param ... The values passed to the populate function +-- @return A new initialized Bag +function Component:__initialize(...) + if self.__populate then + local bag = {} + self.__populate(bag, ...) + + if self.__inherit then + setmetatable(bag, self.__mt) + end + + return bag + end + + return true +end + +return setmetatable(Component, { + __call = function(_, ...) return Component.new(...) end, +}) diff --git a/concord/entity.lua b/concord/entity.lua new file mode 100644 index 0000000..4f2c022 --- /dev/null +++ b/concord/entity.lua @@ -0,0 +1,80 @@ +local PATH = (...):gsub('%.[^%.]+$', '') + +local List = require(PATH..".list") + +local Entity = {} +Entity.__index = Entity + +--- Creates and initializes a new Entity. +-- @return A new Entity +function Entity.new() + local e = setmetatable({ + components = {}, + removed = {}, + instances = List(), + }, Entity) + + return e +end + +--- Gives an Entity a component with values. +-- @param component The Component to add +-- @param ... The values passed to the Component +-- @return self +function Entity:give(component, ...) + self.components[component] = component:__initialize(...) + + return self +end + +--- Removes a component from an Entity. +-- @param component The Component to remove +-- @return self +function Entity:remove(component) + self.removed[component] = true + + return self +end + +--- Checks the Entity against the pools again. +-- @return self +function Entity:apply() + for i = 1, self.instances.size do + self.instances:get(i):checkEntity(self) + end + + for component, _ in pairs(self.removed) do + self.components[component] = nil + self.removed[component] = nil + end + + return self +end + +--- Destroys the Entity. +-- @return self +function Entity:destroy() + for i = 1, self.instances.size do + self.instances:get(i):removeEntity(self) + end + + return self +end + +--- Gets a Component from the Entity. +-- @param component The Component to get +-- @return The Bag from the Component +function Entity:get(component) + return self.components[component] +end + +--- Returns true if the Entity has the Component. +-- @params component The Component to check against +-- @return True if the entity has the Bag. False otherwise +function Entity:has(component) + return self.components[component] ~= nil +end + +return setmetatable(Entity, { + __call = function(_, ...) return Entity.new(...) end, +}) diff --git a/concord/init.lua b/concord/init.lua new file mode 100644 index 0000000..93b3dfa --- /dev/null +++ b/concord/init.lua @@ -0,0 +1,81 @@ +local PATH = (...):gsub('%.init$', '') + +local Fluid = {} + +function Fluid.init(settings) + Fluid.entity = require(PATH..".entity") + Fluid.component = require(PATH..".component") + Fluid.system = require(PATH..".system") + Fluid.instance = require(PATH..".instance") + + if settings and settings.useEvents then + Fluid.instances = {} + + Fluid.addInstance = function(instance) + table.insert(Fluid.instances, instance) + end + + Fluid.removeInstance = function(instance) + for i, instance in ipairs(Fluid.instances) do + table.remove(Fluid.instances, i) + break + end + end + + love.run = function() + if love.math then + love.math.setRandomSeed(os.time()) + love.timer.step() + end + + for _, instance in ipairs(Fluid.instances) do + instance:emit("load", arg) + end + + if love.timer then love.timer.step() end + + local dt = 0 + + while true do + if love.event then + love.event.pump() + for name, a, b, c, d, e, f in love.event.poll() do + for _, instance in ipairs(Fluid.instances) do + instance:emit(name, a, b, c, d, e, f) + end + + if name == "quit" then + return a + end + end + end + + if love.timer then + love.timer.step() + dt = love.timer.getDelta() + end + + for _, instance in ipairs(Fluid.instances) do + instance:emit("update", dt) + end + + if love.graphics and love.graphics.isActive() then + love.graphics.clear(love.graphics.getBackgroundColor()) + love.graphics.origin() + + for _, instance in ipairs(Fluid.instances) do + instance:emit("draw") + end + + love.graphics.present() + end + + if love.timer then love.timer.sleep(0.001) end + end + end + end + + return Fluid +end + +return Fluid diff --git a/concord/instance.lua b/concord/instance.lua new file mode 100644 index 0000000..3247c09 --- /dev/null +++ b/concord/instance.lua @@ -0,0 +1,170 @@ +local PATH = (...):gsub('%.[^%.]+$', '') + +local List = require(PATH..".list") + +local Instance = {} +Instance.__index = Instance + +--- Creates a new Instance. +-- @return The new instance +function Instance.new() + local instance = setmetatable({ + entities = List(), + systems = List(), + events = {}, + removed = {}, + }, Instance) + + return instance +end + +--- Adds an Entity to the Instance. +-- @param e The Entity to add +-- @return self +function Instance:addEntity(e) + e.instances:add(self) + self.entities:add(e) + self:checkEntity(e) + + return self +end + +--- Checks an Entity against all the systems in the Instance. +-- @param e The Entity to check +-- @return self +function Instance:checkEntity(e) + for i = 1, self.systems.size do + self.systems:get(i):__check(e) + end + + return self +end + +--- Marks an Entity as removed from the Instance. +-- @param e The Entity to mark +-- @return self +function Instance:removeEntity(e) + self.removed[#self.removed + 1] = e + + return self +end + +--- Completely removes all marked Entities in the Instance. +-- @return self +function Instance:flush() + if #self.removed > 0 then + for i = 1, #self.removed do + local e = self.removed[i] + + e.instances:remove(self) + self.entities:remove(e) + + for i = 1, self.systems.size do + self.systems:get(i):__remove(e) + end + end + + self.removed = {} + end + + return self +end + +--- Adds a System to the Instance. +-- @param system The System to add +-- @param eventName The Event to register to +-- @param callback The function name to call. Defaults to eventName +-- @param enabled If the system is enabled. Defaults to true +-- @return self +function Instance:addSystem(system, eventName, callback, enabled) + if system.__instance and system.__instance ~= self then + error("System already in instance '" ..tostring(system.__instance).."'") + end + + if not self.systems:has(system) then + self.systems:add(system) + system.__instance = self + end + + if eventName then + self.events[eventName] = self.events[eventName] or {} + + local i = #self.events[eventName] + 1 + self.events[eventName][i] = { + system = system, + callback = callback or eventName, + enabled = enabled == nil and true or enabled, + } + end + + return self +end + +--- Enables a System in the Instance. +-- @param system The System to enable +-- @param eventName The Event it was registered to +-- @param callback The callback it was registered with. Defaults to eventName +-- @return self +function Instance:enableSystem(system, eventName, callback) + return self:setSystem(system, eventName, callback, true) +end + +--- Disables a System in the Instance. +-- @param system The System to disable +-- @param eventName The Event it was registered to +-- @param callback The callback it was registered with. Defaults to eventName +-- @return self +function Instance:disableSystem(system, eventName, callback) + return self:setSystem(system, eventName, callback, false) +end + +--- Sets a System 'enable' in the Instance. +-- @param system The System to set +-- @param eventName The Event it was registered to +-- @param callback The callback it was registered with. Defaults to eventName +-- @param enable The state to set it to +-- @return self +function Instance:setSystem(system, eventName, callback, enable) + callback = callback or eventName + + local listeners = self.events[eventName] + + if listeners then + for i = 1, #listeners do + local listener = listeners[i] + + if listener.system == system and listener.callback == callback then + listener.enabled = enable + break + end + end + end + + return self +end + +--- Emits an Event in the Instance. +-- @param eventName The Event that should be emitted +-- @param ... Parameters passed to listeners +-- @return self +function Instance:emit(eventName, ...) + self:flush() + + local listeners = self.events[eventName] + + if listeners then + for i = 1, #listeners do + local listener = listeners[i] + + if listener.enabled then + listener.system[listener.callback](listener.system, ...) + end + end + end + + return self +end + +return setmetatable(Instance, { + __call = function(_, ...) return Instance.new(...) end, +}) diff --git a/concord/list.lua b/concord/list.lua new file mode 100644 index 0000000..b02ae05 --- /dev/null +++ b/concord/list.lua @@ -0,0 +1,75 @@ +local List = {} +local mt = {__index = List} + +--- Creates a new List. +-- @return A new list +function List.new() + return setmetatable({ + objects = {}, + pointers = {}, + size = 0, + }, mt) +end + +--- Clears the List completely. +-- @return self +function List:clear() + self.objects = {} + self.pointers = {} + self.size = 0 + + return self +end + +--- Adds an object to the List. +-- @param obj The object to add +-- @return self +function List:add(obj) + local size = self.size + 1 + + self.objects[size] = obj + self.pointers[obj] = size + self.size = size + + return self +end + +--- Removes an object from the List. +-- @param obj The object to remove +-- @return self +function List:remove(obj) + local index = self.pointers[obj] + local size = self.size + + if index == size then + self.objects[size] = nil + else + local other = self.objects[size] + + self.objects[index] = other + self.pointers[other] = index + + self.objects[size] = nil + end + + self.pointers[obj] = nil + self.size = size - 1 +end + +--- Gets an object by numerical index. +-- @param index The index to look at +-- @return The object at the index +function List:get(index) + return self.objects[index] +end + +--- Gets if the List has the object. +-- @param obj The object to search for +-- @param true if the list has the object, false otherwise +function List:has(obj) + return self.pointers[obj] and true +end + +return setmetatable(List, { + __call = function() return List.new() end, +}) diff --git a/concord/pool.lua b/concord/pool.lua new file mode 100644 index 0000000..3fcf4c1 --- /dev/null +++ b/concord/pool.lua @@ -0,0 +1,37 @@ +local PATH = (...):gsub('%.[^%.]+$', '') + +local List = require(PATH..".list") + +local Pool = {} +Pool.__index = Pool + +--- Creates a new Pool +-- @param name Identifier for the Pool. +-- @param filter Table containing the required Components +-- @return The new Pool +function Pool.new(name, filter) + local pool = setmetatable(List(), Pool) + + pool.name = name + pool.filter = filter + + return pool +end + +--- Checks if an Entity is eligible for the Pool. +-- @param e The Entity to check +-- @return True if the entity is eligible, false otherwise +function Pool:eligible(e) + for _, component in ipairs(self.filter) do + if not e.components[component] or e.removed[component] then + return false + end + end + + return true +end + +return setmetatable(Pool, { + __index = List, + __call = function(_, ...) return Pool.new(...) end, +}) diff --git a/concord/system.lua b/concord/system.lua new file mode 100644 index 0000000..ffe3e47 --- /dev/null +++ b/concord/system.lua @@ -0,0 +1,169 @@ +local PATH = (...):gsub('%.[^%.]+$', '') + +local Component = require(PATH..".component") +local Pool = require(PATH..".pool") + +local System = {} +System.mt = { + __index = System, + __call = function(systemProto, ...) + local system = setmetatable({ + __all = {}, + __pools = {}, + __instance = nil, + }, systemProto) + + for _, filter in pairs(systemProto.__filter) do + local pool = system:__buildPool(filter) + if not system[pool.name] then + system[pool.name] = pool + system.__pools[#system.__pools + 1] = pool + else + error("Pool with name '"..pool.name.."' already exists.") + end + end + + system:init(...) + return system + end, +} + +--- Creates a new System prototype. +-- @param ... Variable amounts of filters +-- @return A new System prototype +function System.new(...) + local systemProto = setmetatable({ + __filter = {...}, + }, System.mt) + systemProto.__index = systemProto + + return systemProto +end + +--- Default initialization function. +-- @param ... Varags +function System:init(...) +end + +--- Builds a Pool for the System. +-- @param baseFilter The 'raw' Filter +-- @return A new Pool +function System:__buildPool(baseFilter) + local name = "pool" + local filter = {} + + for i, v in ipairs(baseFilter) do + if type(v) == "table" then + filter[#filter + 1] = v + elseif type(v) == "string" then + name = v + end + end + + return Pool(name, filter) +end + +--- Checks and applies an Entity to the System's pools. +-- @param e The Entity to check +-- @return True if the Entity was added, false if it was removed. Nil if nothing happend +function System:__check(e) + local systemHas = self:__has(e) + + for _, pool in ipairs(self.__pools) do + local poolHas = pool:has(e) + local eligible = pool:eligible(e) + + if not poolHas and eligible then + pool:add(e) + self:entityAddedTo(e, pool) + self:__tryAdd(e) + + return true + elseif poolHas and not eligible then + pool:remove(e) + self:entityRemovedFrom(e, pool) + self:__tryRemove(e) + + return false + end + end +end + +--- Removed an Entity from the System. +-- @param e The Entity to remove +function System:__remove(e) + if self:__has(e) then + for _, pool in ipairs(self.__pools) do + if pool:has(e) then + pool:remove(e) + self:entityRemovedFrom(e, pool) + end + end + + self.__all[e] = nil + self:entityRemoved(e) + end +end + +--- Tries to add an Entity to the System. +-- @param e The Entity to add +function System:__tryAdd(e) + if not self:__has(e) then + self.__all[e] = 0 + self:entityAdded(e) + end + + self.__all[e] = self.__all[e] + 1 +end + +--- Tries to remove an Entity from the System. +-- @param e The Entity to remove +function System:__tryRemove(e) + if self:__has(e) then + self.__all[e] = self.__all[e] - 1 + + if self.__all[e] == 0 then + self.__all[e] = nil + self:entityRemoved(e) + end + end +end + +--- Returns the Instance the System is in. +-- @return The Instance +function System:getInstance() + return self.__instance +end + +--- Returns if the System has the Entity. +-- @param The Entity to check for +-- @return True if the System has the Entity. False otherwise +function System:__has(e) + return self.__all[e] and true +end + +--- Default callback for adding an Entity. +-- @param e The Entity that was added +function System:entityAdded(e) +end + +--- Default callback for adding an Entity to a pool. +-- @param e The Entity that was added +-- @param pool The pool the Entity was added to +function System:entityAddedTo(e, pool) +end + +--- Default callback for removing an Entity. +-- @param e The Entity that was removed +function System:entityRemoved(e) +end + +--- Default callback for removing an Entity from a pool. +-- @param e The Entity that was removed +-- @param pool The pool the Entity was removed from +function System:entityRemovedFrom(e, pool) +end + +return setmetatable(System, { + __call = function(_, ...) return System.new(...) end, +}) diff --git a/examples/simpleDrawing/conf.lua b/examples/simpleDrawing/conf.lua new file mode 100644 index 0000000..4316be5 --- /dev/null +++ b/examples/simpleDrawing/conf.lua @@ -0,0 +1,9 @@ +function love.conf(t) + t.identity = "Platformer" + t.version = "11.0" + t.console = true + + t.window.vsync = false + t.window.width = 720 + t.window.height = 720 +end diff --git a/examples/simpleDrawing/main.lua b/examples/simpleDrawing/main.lua new file mode 100644 index 0000000..f38c0e8 --- /dev/null +++ b/examples/simpleDrawing/main.lua @@ -0,0 +1,118 @@ +local Fluid = require("fluid").init({ + useEvents = true +}) +local Entity = Fluid.entity +local Component = Fluid.component +local System = Fluid.system + +local Game = Fluid.instance() +Fluid.addInstance(Game) + +local Position = Component(function(e, x, y) + e.x = x + e.y = y +end) + +local Rectangle = Component(function(e, w, h) + e.w = w + e.h = h +end) + +local Circle = Component(function(e, r) + e.r = r +end) + +local Color = Component(function(e, r, g, b, a) + e.r = r + e.g = g + e.b = b + e.a = a +end) + +local RectangleRenderer = System({Position, Rectangle}) +function RectangleRenderer:draw() + local e + for i = 1, self.pool.size do + e = self.pool:get(i) + + local position = e:get(Position) + local rectangle = e:get(Rectangle) + local color = e:get(Color) + + love.graphics.setColor(255, 255, 255) + if color then + love.graphics.setColor(color.r, color.g, color.b, color.a) + end + + love.graphics.rectangle("fill", position.x, position.y, rectangle.w, rectangle.h) + end +end + +local CircleRenderer = System({Position, Circle}) +function CircleRenderer:draw() + local e + for i = 1, self.pool.size do + e = self.pool:get(i) + + local position = e:get(Position) + local circle = e:get(Circle) + local color = e:get(Color) + + love.graphics.setColor(255, 255, 255) + if color then + love.graphics.setColor(color.r, color.g, color.b, color.a) + end + + love.graphics.circle("fill", position.x, position.y, circle.r) + end +end + +local RandomRemover = System({}) + +function RandomRemover:init() + self.time = 0 +end + +function RandomRemover:update(dt) + self.time = self.time + dt + + if self.time >= 0.25 then + self.time = 0 + + if self.pool.size > 0 then + local i = love.math.random(1, self.pool.size) + + self.pool:get(i):destroy() + end + end + + love.window.setTitle(love.timer.getFPS()) +end + +Game:addSystem(RandomRemover(), "update") +Game:addSystem(RectangleRenderer(), "draw") +Game:addSystem(CircleRenderer(), "draw") + +for i = 1, 100 do + local e = Entity() + e:give(Position, love.math.random(0, 700), love.math.random(0, 700)) + e:give(Rectangle, love.math.random(5, 20), love.math.random(5, 20)) + + if love.math.random(0, 1) == 0 then + e:give(Color, love.math.random(), love.math.random(), love.math.random(), 1) + end + + Game:addEntity(e) +end + +for i = 1, 100 do + local e = Entity() + e:give(Position, love.math.random(0, 700), love.math.random(0, 700)) + e:give(Circle, love.math.random(5, 20)) + + if love.math.random(0, 1) == 0 then + e:give(Color, love.math.random(), love.math.random(), love.math.random(), 1) + end + + Game:addEntity(e) +end diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..c9b1b9c --- /dev/null +++ b/init.lua @@ -0,0 +1 @@ +return require("concord") \ No newline at end of file