It looks like you're new here. If you want to get involved, click one of these buttons!
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
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!
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.
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:
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.
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:
Draw loop:
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!
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)
My runtime mix-in system:
https://gist.github.com/Utsira/8ec0a8d73b85150f7652