Skip to content

Instantly share code, notes, and snippets.

@oliver-dew
Last active April 15, 2016 14:30
Show Gist options
  • Save oliver-dew/8ec0a8d73b85150f7652 to your computer and use it in GitHub Desktop.
Save oliver-dew/8ec0a8d73b85150f7652 to your computer and use it in GitHub Desktop.
Component System using Mix-ins for Codea / Lua
--# Main
-- Component System by Utsira. uses runtime mix-ins to add component's methods to entities (ie not a pure entity - component - system model).
-- TO USE:
-- Entity is a superclass that adds the method :add, for adding (including) components to game objects(entities).
-- Components are tables, but use : to name their methods.
-- If component has an iterator table then it is a system. Iterate will run through every entity that is part of that system
-- If component has an :init function, this will not be added to the entity, but will instead be run when the component is added
-- Use this function to perform your initial setup
function setup()
centre=vec2(WIDTH, HEIGHT) * 0.5
physics.gravity(0,0)
Assets()
stick=Stick()
player=Player()
entities={}
for i=1,12 do
entities[i]=Rock(vec2(math.random(WIDTH), math.random(HEIGHT)), 50)
end
reportSystems()
end
function draw()
background(40, 40, 50)
--iterate systems
Iterate(Draggable, "processTouches")
Iterate(Body, "move")
Iterate(DrawSheet, "draw")
--draw sheet meshes
Rock.mesh:draw()
Player.mesh:draw()
Stick.mesh:draw()
end
function touched(touch)
Iterate(Draggable, "drag", touch)
end
function Assets()
Stick.assets()
end
--# Entity
Entity = class() --add support for mix-in of components, and will populate a component.iterator table if the components is a system(to be updated every frame)
local systems = {} --which systems are in play at the moment. could use this to automate the iteration loop, but would be tricky in terms of priority
function Entity:add(component, ...)
--add the component's methods to the class, except for init (should this be done only once per class?)
for k, v in pairs(component) do
if type(v) == "function" and v ~= component.init then --copy all funcs except init
if self[k] then
print(k.." being overwritten by "..component.id)
end
self[k] = v
end
end
--if component is a system then...
if component.iterator then
--keep a record of which systems are added (for debug purposes)
systems[component] = component.id
--entity remembers its systems
if not self.components then self.components={} end
self.components[component]=component.id --this component is active
--print (component)
--add the entity to the components' iterator
local insertPoint = math.max(1, #component.iterator) --default priority = last-but-one
if priority == "high" then insertPoint = #component.iterator + 1 --high priority is end of array
elseif priority == "low" then insertPoint = 1 --low priority is start of array
end
table.insert(component.iterator, insertPoint, self)
end
--run the component init routine, if there is one. Same as .included function in other mixin implementations? except, this is run for each instance...
if component.init then component.init(self, ...) end
end
function Iterate(component, func, ...)
-- local func=func or "update"
for i=#component.iterator, 1, -1 do
local v=component.iterator[i]
if v.components[component] then
v[func](v, ...) --run the specified component function
else
table.remove(component.iterator, i) --permanently remove the entity from the component iterator
end
end
end
function reportSystems()
print ("Systems active:")
for component, name in pairs(systems) do
print(name..":"..#component.iterator.." entities")
end
end
--# Player
Player = class(Entity)
Player.mesh=mesh()
Player.mesh.texture=readImage("Tyrian Remastered:Boss D")
function Player:init()
self:add(Position, vec2(centre.x, centre.y))
self:add(Dimensions, 58, 69)
self:add(Body, {CIRCLE, self.ww}, {linearDamping=1, angularDamping=4})
self:add(DrawSheet)
self:add(Input)
end
--# Stick
Stick = class(Entity)
function Stick:init()
local offset = 200
self:add(Position, vec2(WIDTH-offset, offset))
self:add(Dimensions, 100)
self:add(DrawSheet)
self:add(StickInput) --adds Draggable as a subcomponent
end
function Stick.assets()
Stick.mesh=mesh()
local s=100
local img=image(s,s)
setContext(img)
strokeWidth(20)
stroke(color(255,16))
fill(color(255,32))
ellipse(s*0.5,s*0.5,s)
setContext()
Stick.mesh.texture=img
end
--# Rock
Rock = class(Entity)
Rock.mesh = mesh() --shared mesh
Rock.mesh.texture = readImage("Tyrian Remastered:Rock 5")
local sp = 120
local sp2 = sp * 0.5
function Rock:init(pos,size,vel,ang)
local vel = vel or vec2(math.random(sp)-sp2, math.random(sp)-sp2)
local ang = ang or math.random(sp*2)-sp2*2
self:add(Position, pos)
self:add(Dimensions, size)
self:add(
Body, {CIRCLE, self.ww}, {linearVelocity=vel, angularVelocity=ang}
)
self:add(DrawSheet)
end
--# Position
Position = {} --components start here. Components just tables, but use : naming to access self when mixed-in to entities
function Position:init(x,y)
self:setPosition(x,y)
end
function Position:setPosition(pos) --can be overridden by Body
self.pos = pos
end
function Position:getPosition()
return self.pos.x, self.pos.y
end
--# Dimensions
Dimensions = {}
function Dimensions:init(w,h)
self:setDimensions(w,h)
end
function Dimensions:setDimensions(w,h)
local h = h or w
self.w, self.h = w,h
self.ww, self.hh = w * 0.5, h * 0.5
end
function Dimensions:getDimensions()
return self.w, self.h
end
--# Body
Body = {}
Body.iterator={}
Body.id = "Body"
local Bodies = {}
function Body:init(bod, bodArgs)
local body=physics.body(unpack(bod))
body.interpolate=true
body.position=self.pos
for k,v in pairs(bodArgs) do
body[k]=v
end
Bodies[body]=self --allows collision detection to work
self.body=body
end
function Body:move()
local x = boundsWrap(self.body.x, -self.ww, WIDTH+self.ww)
local y = boundsWrap(self.body.y, -self.hh, HEIGHT+self.hh)
if x then self.body.x = x end
if y then self.body.y = y end
--make Body compatible with the rest of the API. Or, use getPosition, setPosition?
self.pos = self.body.position
self.angle = math.rad(self.body.angle)
end
function Body:getPosition()
return self.body.x, self.body.y
end
function Body:destroy()
Bodies[self.body]=nil
self.body:destroy()
self:removeAll()
end
function Body:collide(contact, bod)
--null. Is null function faster than "if self.collide then self:collide"?
end
function collide(contact)
if contact.state==BEGAN then
local bodA, bodB = contact.bodyA, contact.bodyB
Bodies[bodA]:collide(contact, bodB)
Bodies[bodB]:collide(contact, bodA)
end
end
--# DrawSheet
DrawSheet = {} --component for objects with the same texture that share a single mesh
DrawSheet.iterator={}
DrawSheet.id="DrawSheet"
function DrawSheet:init(priority)
self.rect=self.mesh:addRect(self.pos.x,self.pos.y,self.w,self.h)
if not self.angle then self.angle = 0 end
end
function DrawSheet:draw()
local x,y = self:getPosition()
self.mesh:setRect(self.rect, x , y, self.w, self.h, self.angle) --use self:getPosition?
end
--# StickInput
StickInput = {}
function StickInput:init(priority, output)
self.startPos = self.pos
self:add(Draggable, self.processStick) --subcomponent!
end
function StickInput:processStick(touch, tpos)
if touch.state==BEGAN then
self.startPos = tpos
elseif touch.state==MOVING then
self.pos=tpos --is this Ok, or should it be self:setPosition(tpos)
local max,factor,dead,norm,ang=200,3,0,vec2(0,0) --150, 4
local diff=tpos-self.startPos
local speed=diff:len()
if speed<dead then
speed=0
diff=vec2(0,0)
else
local norm=diff:normalize()
speed = speed - dead
if speed>max then
speed=max+dead
local limit=norm*(max+dead)
-- self.startPos = self.startPos + diff - limit --move stick base
diff = limit
self.pos = self.startPos + diff
end
end
player:acceptStickInput(diff) --hard link to player class
elseif touch.state==ENDED then
self.pos = self.startPos
end
end
--# Draggable
Draggable = {} --items that can be picked up and held (ie trigger a callback while finger is onscreen regardless of whether finger is moving)
Draggable.iterator={}
Draggable.id = "Draggable"
function Draggable:init(callback)
self.callback = callback
self.bounds = vec2(self.ww, self.hh)
end
function Draggable:drag(touch) --iterate this from touched (per touch event)
self.tpos=vec2(touch.x, touch.y)
self.touch=touch
if touch.state==ENDED then
tween.delay(0.001, function() self.touching=false end) --delay a frame so that touch ending event can be sent to callback
elseif not self.touching then
if not outOfBounds(self.tpos, self.pos+self.bounds, self.pos-self.bounds) then --check bounds on first touch
self.touching=true
end
end
end
function Draggable:processTouches() --iterate this from draw (per cycle)
if self.touching then self.callback(self, self.touch, self.tpos) end
end
--# Input
Input = {} --is there much point having this as a component given that only the player will use it?
--dependencies: Body
local gain=2
local damp=0.5
function Input:acceptStickInput(vector)
--calculate movement
local anchor = self.body:getWorldPoint(vec2(-10, 0))
local target = self.pos+vector*0.2
local diff = target - anchor
local vel = self.body:getLinearVelocityFromWorldPoint(anchor)
self.body:applyForce( (1/1) * diff * gain - vel * damp) --anchor
--calculate angle (just animation)
local targetAngle = math.deg(math.atan2(vector.y, vector.x))-90
local diffAngle = targetAngle-self.body.angle
if diffAngle<-180 then diffAngle = diffAngle + 360
elseif diffAngle>180 then diffAngle = diffAngle - 360
end
self.body.angle = self.body.angle + diffAngle * 0.1
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 return v
elseif v>b then v = v - d return v
end
return nil
end
function outOfBounds(pos, bb, aa) --returns true if outofbounds
local aa=aa or vec2(0,0)
if pos.x<aa.x then return true
elseif pos.x>bb.x then return true
end
if pos.y<aa.y then return true
elseif pos.y>bb.y then return true
end
return false
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment