Last active
April 15, 2016 14:30
-
-
Save oliver-dew/8ec0a8d73b85150f7652 to your computer and use it in GitHub Desktop.
Component System using Mix-ins for Codea / Lua
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# 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