Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Attempt at an entity/ component system

in Examples Posts: 2,020

This is inspired in part by this thread http://codea.io/talk/discussion/6446/draggable-objects-and-design-patterns

I thought it was worth starting a new thread as this is perhaps a little far from the OP's comment.

It's a first attempt at an entity/ component system (bearing in mind I hadn't heard of either term a few days ago).

The idea is that objects (entities) hold no methods, only shared data, while components hold all of the methods. This should make for a very flat structure (ie no hierarchy of subclasses), and greater code portability.

It uses 2 features of the Codea class system which might not be obvious at first. First, a class can call a function that lies outside itself using someOtherClass.function(self) (is this a recognised technique, and if so is there a name for it? virtual method?). I use this to keep all methods outside of the object classes. Second, if you set a variable or a table with the name someClass.table, outside of any of that class's functions, then that table can be shared as a global variable as you would expect, but can also be called within the class (or here, the super class) with self.table. I use this for each component's entities table.

I would be very grateful to get feedback and criticism on this.

--# Main
-- Entity Component System

-- Use this function to perform your initial setup
function setup()
    centre=vec2(WIDTH, HEIGHT) * 0.5
    physics.gravity(0,0)
    player=Player()
    enemies={}
    for i=1,12 do
        enemies[i]=Rock(math.random(WIDTH), math.random(HEIGHT))
    end
end

function draw()
    background(40, 40, 50)
    Iterate(Draw2D)   
    Iterate(DrawSheet)
    Iterate(Move)
    Rock.mesh:draw()
end

--# Component
Component = class() --superclass for all components

function Component:setup(e, priority) --all components must call this in their init. priority is optional
    self.entity = e
    if not e.components then e.components={} end
    e.components[self]=true --object remembers which components it has. Used to delete an object

    self.active=true --whether component is active

    --add self to shared entities table (used by iterator)
    --establish priority
    local insertPoint = math.max(1, #self.entities) --default priority = last-but-one
    if priority == "high" then insertPoint = #self.entities + 1 --high priority is end of array
    elseif priority == "low" then insertPoint = 1 --low priority is start of array
    end
    table.insert(self.entities, insertPoint, self)
end

function Component:reset() --ie, when resetting the game state, starting a new level etc
    self.entities={}
end

function Iterate(component) --pass the name of a component class
    for i=#component.entities, 1, -1 do
        local v=component.entities[i]
        if v.kill then
            table.remove(component.entities, i) --permanently remove this component from the entity
        elseif v.active then
            component.update(v.entity) --pass the object as "self" to component "update" function
        end
    end
end

--these aren't components as such (ie they don't update or have any methods). they just populate the entity with data
function Position(e,x,y)
    e.pos = vec2(x,y)
end

function Dimensions(e,w,h)
    local h = h or w
    e.w, e.h = w,h
    e.ww, e.hh = w * 0.5, h * 0.5
end

--# Draw2D
Draw2D = class(Component) --component for objects with their own individual mesh

Draw2D.entities={} --all component classes must have a ClassName.entities array. nb this is shared among all instances of the class, but can still be called as self.entities

function Draw2D:init(e, img, priority)
    local m=mesh() --setup mesh
    m.texture=img
    m:addRect(0,0,e.w,e.h)
    e.mesh=m
   -- if not e.angle then e.angle = 0 end
    self:setup(e, priority) --all components must make this call
end

function Draw2D:update() --nb the iterator passes the object's self to this
    pushMatrix()
    translate(self.pos.x, self.pos.y)
    rotate(math.deg(self.angle))
    self.vector=vecMat(vec2(1,0),modelMatrix()) --calculate vector
    self.mesh:draw()
    popMatrix()
end

--# DrawSheet
DrawSheet = class(Component) --component for objects with the same texture that share a single mesh

DrawSheet.entities={} 

function DrawSheet:init(e, priority)
    e.rect=e.mesh:addRect(e.pos.x,e.pos.y,e.w,e.h)
  --  if not e.angle then e.angle = 0 end
    self:setup(e, priority)
end

function DrawSheet:update() --nb the iterator passes the object's self to this
    self.vector = vec2(math.sin(self.angle),math.cos(self.angle)) --calculate vector
   self.mesh:setRect(self.rect, self.pos.x, self.pos.y, self.w, self.h, self.angle)
end

--# Move
Move = class(Component) --not compatible with Body

Move.entities = {}

function Move:init(e, speed, angle)
    e.angle = angle or math.random()*(math.pi*2)
    e.speed = speed or 0
    self:setup(e) --priority not needed for move?
end

function Move:update()
    self.pos = self.pos + (self.vector * self.speed)
    self.pos.x = boundsWrap(self.pos.x, -self.ww, WIDTH+self.ww)
    self.pos.y = boundsWrap(self.pos.y, -self.hh, HEIGHT+self.hh)
end

--# Body 
Body = class(Component) --not used yet. In future, will use this to test how easy it is to swap out one component (Move) and add another (this one)

Body.entities={}

function Body:init(e, bod, bodArgs)
    local body=physics.body(unpack(bod))
    body.interpolate=true
    for k,v in pairs(bodArgs) do
        body[k]=v
    end 
    e.body=body
    self:setup(e)
end

function Body:update()
    --make Body compatible with the rest of the API
    self.pos = self.body.position 
    self.angle = math.rad(self.body.angle)
end

--# Entity
Entity = class() --adds a bit of component management to each of the entity classes

function Entity:removeAllComponents()
    for component,_ in pairs(self.components) do
        component.kill=true
    end
end

--# Player
Player = class(Entity) --objects have no methods (beyond component management), just a container for data

function Player:init()
    Position(self, centre.x, centre.y)
    Dimensions(self, 58, 69)
    Move(self)
    Draw2D(self, "Tyrian Remastered:Boss D")
end

--# Rock
Rock = class() 

Rock.mesh = mesh() --shared mesh
Rock.mesh.texture = readImage("Tyrian Remastered:Rock 5")

function Rock:init(x,y)
    Position(self, x,y)
    Dimensions(self, 50)
    Move(self, math.random(2)+1)
    DrawSheet(self)
end

--# Helpers
function vecMat(vec, mat) --rotate vector by current transform. 
    return vec2(mat[1]*vec.x + mat[5]*vec.y, mat[2]*vec.x + mat[6]*vec.y)
end

function boundsWrap (v, a, b) --value, start, end
   return ((v-a)%(b-a+1))+a
    --[[
     local d = b - a --difference
    if v<a then v = v + d
    elseif v>b then v = v - d
    end
    return v
      ]]
end

Comments

  • Jmv38Jmv38 Mod
    edited April 2015 Posts: 3,295

    It is difficult to make comments on such a code without really studying it.
    To test the relevance of your choices I would suggest:
    1/ finish the game: add interaction, shooting, explosions, sounds, a score, save it.
    2/ refactor your project several times to improve it. If refactoring is incredibly easy then you've probably found an excellent way to produce code.
    3/ Inform us so we can benefit of your experience.
    Thanks for sharing!

  • Posts: 25

    If you have components which change properties of some other objects, you are really straying away from OOP. In fact your component is then just a glorified namespace for a series of functions. In a loosely typed language like Lua this means you cannot check/know if a given object is compatible to be passed to such a component.

    To be fair, I use this to some extent in my basic implementation of Draggable, but the power comes from the fact that my Draggable component can use OOP features such as polymorphism. I.e. the original draggable component determines that the touch is inside using a default implementation, but the user of the component can speficy an alternative 'IsInside' method.

    Just a piece of feedback on the code above. Why is Iterate not a class method? Again, it doesn't really make a big difference in this case, but by making it a class method you indicate that this function is applicable only to an object of this type (although, as you pointed out yourself, you can always do someOtherclass.somemethod(objofsomeclass), but then you are definitely taking risks that that method borrowed from another class doesn't make sense of the object that is passed.

  • Posts: 2,020

    To be fair, I use this to some extent in my basic implementation of Draggable, but the power comes from the fact that my Draggable component can use OOP features such as polymorphism. I.e. the original draggable component determines that the touch is inside using a default implementation, but the user of the component can speficy an alternative 'IsInside' method.

    This is a very good point. With this code above, a given component cannot easily call another component's functions, as even the object doesn't really know which components it has. I'm thinking of starting from scratch, but using something similar to @Jmv38 and @toffer 's code for extending a table to add components to a class. Something like this:

    function Entity:add(component)
        --add the components methods to the entity
        for k, v in pairs(component) do
            print (k)
            if type(v) == "function" and v ~= Component.setup then 
    
                self[k] = v 
            end
        end
    end
    --then, in the object class, you add the components you need
    Entity:add(Body)
    

    Although, you could argue that this method is starting to slide towards multiple inheritance, and would lead to things like "the diamond problem", if two of the components you add both have a function with the same name, you might not realise that one overwrites the other.

  • Posts: 2,020

    Why is Iterate not a class method?

    Good point. I guess it was because iterate is something done to the class as a whole, not something that each individual instance should have access to. But perhaps it could look like this:

    function Component:iterate(...) 
        for i=#self.entities, 1, -1 do
            local v=self.entities[i]
            if v.kill then
                table.remove(self.entities, i) --permanently remove this component from the entity
            elseif v.active then
                self.update(v.entity, ...) --pass the object as "self" to component "update" function
            end
        end
    end
    

    Draw loop:

        Body:iterate() 
        Draw2D:iterate()   
        DrawSheet:iterate() 
        Move:iterate() 
    

    I'm not sure I like the idea (in the code at the top of the page) of any given game entity having multiple "selves", ie one for its data, and one for each of its methods. It's too complex!

  • Posts: 2,020

    Well, there's certainly an enormous amount of literature online about the benefits of various paradigms. Sometimes people are discussing proprietary systems, so they can only show you code snippets or pseudo-code.

    This is an open-sourced entity system, for the LÖVE implementation of Lua:

    https://github.com/lovetoys/lovetoys

    This article is quite interesting, in that it describes the various levels to which this approach can be taken:

    http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/

    The most extreme, "object as pure aggregation", the game object is just a unique id, but contains no data or methods at all, purely the sum of its parts. I can understand the appeal of the various components being completely self-contained. But sharing data between components would probably be quite complex in this system, and I guess you would lose some of the useful object-oriented stuff like polymorphism, as @joelhoro pointed out

    As an alternative to this, I've also been playing around with various ways of implementing mix-ins. The entity gets extended with extra methods, dynamically during runtime. If you are overwriting an existing method, a warning is printed to the console (to try to stop unintended overwrites). Method overwriting though allows you to have polymorphism. Unlike inheritance, there is no hierarchy. I'll post some code later (though I guess this mix-in approach is not really an "entity - component - system" example any more, so I might have to change the title of this thread)

Sign In or Register to comment.