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