Bladeren bron

Initial commit

Eiyeron Fulmincendii 8 jaren geleden
commit
fa5993e49d
8 gewijzigde bestanden met toevoegingen van 532 en 0 verwijderingen
  1. 41 0
      Component.lua
  2. 162 0
      ECSWorld.lua
  3. 35 0
      Entity.lua
  4. 22 0
      System.lua
  5. 106 0
      SystemFilter.lua
  6. 61 0
      class.lua
  7. 50 0
      main.lua
  8. 55 0
      object.lua

+ 41 - 0
Component.lua

@@ -0,0 +1,41 @@
+--[[
+    ECS Component class
+    @author : Eiyeron Fulmincendii
+
+    A component is a data container for entities. It determines also which systems the holder entity
+    will be processed on.
+]]--
+local class = require("class")
+local object = require("object")
+local __cid = 0
+
+local component = class(object)
+
+local function eq(a, b) return a.__cid == b.__cid end
+local function le(a, b) return a.__cid <= b.__cid end
+local function lt(a, b) return a.__cid < b.__cid end
+
+local function make_component(...)
+    local c = class(component, ...)
+    __cid = __cid + 1
+    -- Component unique ID. If you don't want to break everything, avoid touching this.
+    c.__cid = __cid
+    -- Override this variable at class creation to have a more explicit tostring result.
+    c.__name = "Component"
+    -- Used in SystemFilters
+    c.__lt = lt
+    c.__le = le
+    c.__eq = eq
+
+    local mt = getmetatable(c)
+    -- Used in SystemFilters
+    mt.__lt = lt
+    mt.__le = le
+    mt.__eq = eq
+    -- Class type functions.
+    mt.__tostring = function(a) return string.format("{Component class %s (id:%d)}", a.__name, a.__cid) end
+    return c
+end
+
+
+return make_component

+ 162 - 0
ECSWorld.lua

@@ -0,0 +1,162 @@
+--[[
+    ECS World class
+    @author : Eiyeron Fulmincendii
+
+    Contains entities, components and system and orchestrates them.
+]]--
+local class = require("class")
+local object = require("object")
+local SystemFilter = require("SystemFilter")
+local Entity = require("Entity")
+local utils = require("utils")
+
+ECSWorld = class(object)
+
+function ECSWorld:init( )
+    self.entities = {}
+    self.components = {}
+    self.system_filters = {}
+end
+
+--[[
+    Search a filter that matches the required components.
+]]--
+function ECSWorld:searchFilter(required_components)
+    -- TODO : inquire why it doesn't call a composant's metamethod.
+    local component_list = table.sort(required_components)
+    for i,l in ipairs(self.system_filters) do
+        if l:compareComponentList(required_components) then
+            return l
+        end
+    end
+    return nil
+end
+
+--[[
+    - system : system to register
+    - ... : array of components to require
+]]--
+function ECSWorld:registerSystem(system_type, ...)
+    local required_components = {...}
+    local found_filter = self:searchFilter(required_components)
+    if found_filter then
+        found_filter:registerSystem(system_type)
+    else
+        local new_filter = SystemFilter:new(required_components)
+        self.system_filters[#self.system_filters + 1] = new_filter
+        self:searchAndRegisterEntitiesInNewSystemFilter(new_filter)
+        new_filter:registerSystem(system_type)
+    end
+end
+
+--[[
+    On component attachment, register the holder entity to
+    every filter that can handle it now.
+]]
+function ECSWorld:searchAndRegisterEntityInSystemFilter(entity)
+    for i,system_filter in ipairs(self.system_filters) do
+        if system_filter:checkEntityCompatibility(entity) and not system_filter:hasEntity(entity) then
+            system_filter:registerEntry(entity)
+        end
+    end
+end
+
+--[[
+    On SystemFilter creation, register every compatible entity.
+]]
+function ECSWorld:searchAndRegisterEntitiesInNewSystemFilter(new_filter)
+    for i,entity in ipairs(self.entities) do
+        if new_filter:checkEntityCompatibility(entity) and not new_filter:hasEntity(entity) then
+            new_filter:registerEntry(entity)
+        end
+    end
+end
+
+--[[
+    On Component detachment, prune the filters from the now-incompatible holder entity.
+]]--
+function ECSWorld:searchAndPruneEntityFromSystemFilter(entity)
+    for i,system_filter in ipairs(self.system_filters) do
+        if system_filter:hasEntity(entity) and not system_filter:checkEntityCompatibility(entity) then
+            system_filter:unregisterEntry(entity)
+        end
+    end
+end
+
+
+--[[
+    - Copies the entity into the ECSWorld's entities
+    - Registers it in every SystemFilter already registered
+]]
+function ECSWorld:createEntity()
+    local index = #self.entities + 1
+    for i,entity_entry in ipairs(self.entities) do
+        if entity_entry.__destroyed then
+            index = i
+            break
+        end
+    end
+    local e = Entity:new(index, self)
+    self.entities[index] = e
+    return e
+end
+
+--[[
+    Called by World itself to mark an entity as destroyed (to override it later if needed)
+]]--
+function ECSWorld:deleteEntity(entity)
+    self:searchAndUnregisterEntitiesInSystemFilter(entity)
+    entity.__destroyed = true
+end
+
+--[[
+    Called by Systems via their Entity. Creates and attach a component to the holder
+    entity.
+]]--
+function ECSWorld:attachComponentToEntity(entity, component_type, ...)
+    -- Create on the fly the component list if the world doesn't have it for this component class.
+    if not self.components[component_type] then self.components[component_type] = {} end
+    self.components[component_type][entity.__eid] = component_type:new(...)
+    self:searchAndRegisterEntityInSystemFilter(entity)
+end
+
+--[[
+    Called by Systems via their Entity. Deletes and detaches the component from this entity.
+]]--
+function ECSWorld:detachComponentFromEntity(entity, component_type)
+    -- TODO : better way to remove component
+    self.components[component_type][entity.__eid] = nil
+    self:searchAndPruneEntityFromSystemFilter(entity)
+end
+
+--[[
+    Called by Systems via their Entity.
+]]
+function ECSWorld:entityGetComponent(entity, component_type)
+    if not self.components[component_type] then return nil end
+    return self.components[component_type][entity.__eid]
+end
+
+--[[
+    Called by Systems via their Entity.
+]]
+function ECSWorld:entityHasComponent(entity, component_type)
+    return self:entityGetComponent(entity, component_type) ~= nil
+end
+
+--[[
+    Supposed to be called by the world owner. Calls every system and prunes marked for deletion entities
+]]
+function ECSWorld:update(dt)
+    for i,system_filter in ipairs(self.system_filters) do
+        system_filter:update(dt, self.entities)
+    end
+    -- Entity release. Done post-update.
+    for i,entity in ipairs(self.entities) do
+        if entity.__destroy_required == true then
+            self:deleteEntity(entity)
+        end
+    end
+end
+
+return ECSWorld

+ 35 - 0
Entity.lua

@@ -0,0 +1,35 @@
+--[[
+    ECS Entity class
+    @author : Eiyeron Fulmincendii
+
+    An entity is not much more than its own ID and a few helpers to manage its own components via the holder world.
+]]--
+local class = require("class")
+local object = require("object")
+local utils = require("utils")
+
+Entity = class(object)
+function Entity:init(index, world)
+    self.__destroyed = false
+    self.__destroy_required = false
+    self.__eid = index
+    self.__world = world
+end
+
+function Entity:addComponent(component_type, ...)
+    self.__world:attachComponentToEntity(self, component_type, ...)
+end
+
+function Entity:removeComponent(component_type)
+    self.__world:detachComponentFromEntity(self, component_type)
+end
+
+function Entity:hasComponent(component_type)
+    return self.__world:entityHasComponent(self, component_type)
+end
+function Entity:getComponent(component_type)
+    return self.__world:entityGetComponent(self, component_type)
+end
+
+
+return Entity

+ 22 - 0
System.lua

@@ -0,0 +1,22 @@
+--[[
+    ECS System class
+    @author : Eiyeron Fulmincendii
+
+    A System (also seen as Processor) is an object or function that'll check for every entities that has its required Components
+    and processes routines with them, such as rendering sprites for every entities that has related components (like Sprite, Transform, etc...)
+]]--
+local class = require("class")
+local object = require("object")
+local __sid = 0
+
+local system = class(object)
+
+local function make_system(...)
+    local s = class(system, ...)
+    __sid = __sid + 1
+    -- System Unique ID, if you don't want to break everything, you should avoid editing this value.
+    s.__sid = __sid
+    return s
+end
+
+return make_system

+ 106 - 0
SystemFilter.lua

@@ -0,0 +1,106 @@
+--[[
+    ECS SystemFilter class
+    @author : Eiyeron Fulmincendii
+
+    A SystemFilter is just an internal class to avoid multiplying the same requirement list for every System.
+    Instead of blindly storing the given requirement lists on World's side, this class intends to store all Systems
+    that share the same component requirement list.
+]]--
+local class = require("class")
+local object = require("object")
+local utils = require("utils")
+
+local SystemFilter = class(object)
+
+function SystemFilter:init(required_components)
+    self.required_components = required_components
+    self.systems = {}
+    self.registered_entries = {}
+end
+
+--[[
+    Called by World to register an entity for the systems to process it.
+]]--
+function SystemFilter:registerEntry(entity)
+    local index = #self.registered_entries+1
+    for i,reg in ipairs(self.registered_entries) do
+        if not reg then
+            index = i
+            break
+        end
+    end
+    self.registered_entries[index] = entity
+end
+
+--[[
+    Called by World to unregister an entity.
+]]--
+function SystemFilter:unregisterEntry(entity)
+    for i,entity in ipairs(self.registered_entries) do
+        if entity.__eid == entity.__eid then
+            self.registered_entries[i] = nil
+            return
+        end
+    end
+end
+
+function SystemFilter:hasEntity(entity)
+    for _,current_entity in ipairs(self.registered_entries) do
+        if current_entity.__eid == entity.__eid then
+            return true
+        end
+    end
+    return false
+end
+
+--[[
+    Called by World to register a compatible system.
+]]--
+function SystemFilter:registerSystem(system)
+    if not self.systems[system.__sid] then
+        self.systems[system.__sid] = system
+        return true
+    end
+    return false
+end
+
+--[[
+    Checks if the givent component list matches the current filter's list.
+    Warning : it requires the list to be sorted for now.
+]]
+function SystemFilter:compareComponentList(component_list)
+    if #component_list ~= #self.required_components then
+        return false
+    end
+    -- Compare every registered system
+    for j=0,#self.required_components do
+        if component_list[j] ~= self.required_components[j] then
+            return false
+        end
+    end
+    return true
+end
+
+--[[
+    Returns true if the entity has all the components for the current filter.
+]]
+function SystemFilter:checkEntityCompatibility(entity)
+    local num_valid_components = 0
+    for i,component in ipairs(self.required_components) do
+        if entity:hasComponent(component) then
+            num_valid_components = num_valid_components + 1
+        end
+    end
+    return num_valid_components == #self.required_components
+end
+
+--[[
+    Called by world to dispatch the update event to the systems.
+]]
+function SystemFilter:update(dt)
+    for j,system in ipairs(self.systems) do
+        system.update(dt, self.registered_entries)
+    end
+end
+
+return SystemFilter

+ 61 - 0
class.lua

@@ -0,0 +1,61 @@
+--[[
+    Class system
+    @author : Siapran Candoris
+    Supports memoized multiple inheritance
+]]--
+
+local ipairs = ipairs
+
+do
+	local function metatable_search( k, list )
+		for _,e in ipairs(list) do
+			local v = e[k]
+			if v then return v end
+		end
+	end
+
+	local function metatable_cache( self, k )
+		local v = metatable_search(k, self.__parents)
+		self[k] = v
+		return v
+	end
+
+	-- genealogy is
+	local function make_genealogy( self, res, has )
+		res = res or {}
+		has = has or {}
+		local parents = self.__parents
+		if has[self] then
+			return
+		end
+		if parents and #parents > 0 then
+			for _,parent in ipairs(parents) do
+				make_genealogy(parent, res, has)
+			end
+		end
+		res[#res + 1] = self
+		has[self] = true
+		return res
+	end
+
+	-- make a class with simple or multiple inheritance
+	-- inheritance is implemented as cached first found
+	-- do NOT change class methods at runtime, just don't
+	function make_class( ... )
+		local res = {}
+		res.__parents = {...}
+		res.__genealogy = make_genealogy(res)
+		res.__instanceof_cache = {}
+		-- inherited methods are cached to improve runtime performance
+		-- caching is done per class, not per object
+		if #res.__parents > 0 then
+			setmetatable(res, {__index = metatable_cache})
+		end
+
+		res.__index = res
+
+		return res
+	end
+end
+
+return make_class

+ 50 - 0
main.lua

@@ -0,0 +1,50 @@
+--[[
+    Just a small test
+]]--
+local utils = require("utils")
+local class = require("class")
+local Component = require("Component")
+local World = require("ECSWorld")
+local SystemFilter = require("SystemFilter")
+local System = require("System")
+
+local world = ECSWorld:new()
+
+local LifeComponent = Component()
+LifeComponent.__name = "LifeComponent"
+function LifeComponent:init(life, life_max)
+    self.life = life
+    self.life_max = life_max
+end
+
+local ManaComponent = Component()
+ManaComponent.__name = "ManaComponent"
+function ManaComponent:init(mana, mana_max)
+    self.mana = mana
+    self.mana_max = mana_max
+end
+
+local CreatureSystem = System()
+function CreatureSystem.update(dt, entities)
+    for i,entity in ipairs(entities) do
+        print(string.format(
+[[HP : %d/%d
+MP : %d/%d]],
+        entity:getComponent(LifeComponent).life,
+        entity:getComponent(LifeComponent).life_max,
+        entity:getComponent(ManaComponent).mana,
+        entity:getComponent(ManaComponent).mana_max))
+    end
+end
+
+local e = world:createEntity()
+world:registerSystem(CreatureSystem, LifeComponent, ManaComponent)
+utils.printTable(LifeComponent)
+print(LifeComponent)
+
+e:addComponent(LifeComponent, 10, 20)
+e:addComponent(ManaComponent, 10, 20)
+world:update(0)
+e:removeComponent(ManaComponent)
+world:update(0)
+

+ 55 - 0
object.lua

@@ -0,0 +1,55 @@
+--[[
+    Object base class
+    @author : Siapran Candoris
+    Features instance of class determination and prototype function (init)
+]]--
+
+local make_class = require("class")
+--------------------------------
+-- object class
+--
+local object = make_class()
+
+function object:new( ... )
+	local res = {}
+	setmetatable(res, self)
+	if res.init then
+		res:init(...)
+	end
+	return res
+end
+
+function object:instanceof( class )
+	local cache = self.__instanceof_cache
+	if cache[class] ~= nil then
+		return cache[class]
+	else
+		for _,v in ipairs(self.__genealogy) do
+			if class == v then
+				cache[class] = true
+				return true
+			end
+		end
+		cache[class] = false
+		return false
+	end
+end
+
+-- general purpose utils
+
+-- asssign unique ids to tables
+do
+    local cache = setmetatable({}, {__mode = "k"})
+    local id = 0
+    function identifier( table )
+        if cache[table] then
+            return cache[table]
+        else
+            cache[table] = id
+            id = id + 1
+            return id
+        end
+    end
+end
+
+return object