Because I was bored and had some time to kill before work, I decided to put together a quick demonstration of component-based Pong. I didn't get to actually finish it (work calls, and I must away) but I did implement walls, a bouncing ball, a player controlled paddle and a really, really stupid AI controlled paddle. Some of the physics and collision are a bit wonky (it was a very rush effort) and non-robust, but it works well enough to demonstrate at least the basic idea of what I was talking about.
The demo is written for the
Love library, a simple game library that uses the Lua language. Just download the Love binary, execute the script using the love executable and it should be golden. Here is the script:
[spoiler]
function createObject()
local o={x=0,y=0}
o.components={}
o.message=function(self,msg)
local comp
for _,comp in pairs(self.components) do
if comp then comp:message(msg) end
end
end
o.kill=function(self)
local comp
for _,comp in pairs(self.components) do
comp:kill()
end
self.components={}
end
o.add_component=function(self,c)
table.insert(self.components,c)
c.owner=self
end
return o
end
-- Graphics sub-system and related Component creation
GraphicSubsystem={}
function GraphicSubsystem:init()
self.componentlist={}
end
function GraphicSubsystem:createComponent(o, width, height)
local c={sprite={width=width,height=height}, subsystem=self}
c.kill=function(self)
self.subsystem:destroyComponent(self)
self.subsystem=nil
self.sprite=nil
end
c.message=function(self,msg)
end
self.componentlist[o]=c
o:add_component(c)
end
function GraphicSubsystem:destroyComponent(c)
self.componentlist[c.owner]=nil
end
function GraphicSubsystem:draw()
local o,c
for o,c in pairs(self.componentlist) do
-- Draw sprite
love.graphics.rectangle("fill",o.x-c.sprite.width/2, o.y-c.sprite.height/2, c.sprite.width, c.sprite.height)
end
end
-- Collision Sub-system and related Component creation
CollisionSubsystem={}
function CollisionSubsystem:init()
self.componentlist={}
end
function CollisionSubsystem:createComponent(o, width, height, solid)
local c=
{
boxwidth=width,
boxheight=height,
subsystem=self,
solid=solid
}
c.kill=function(self)
self.subsystem:destroyComponent(self)
self.subsystem=nil
end
c.message=function(self,msg)
end
self.componentlist[o]=c
o:add_component(c)
end
function CollisionSubsystem:destroyComponent(c)
self.componentlist[c.owner]=nil
end
function CollisionSubsystem:testCollision(b1, b2)
local dx,dy=b2.cx - b1.cx, b2.cy-b1.cy
if math.abs(dx) < (b1.width/2+b2.width/2) and math.abs(dy) < (b1.height/2+b2.height/2) then
-- calculate distance of centers as a percentage of total distance along each axis of second box
local px,py=dx/(b2.width/2),dy/(b2.height/2)
if math.abs(py) > math.abs(px) then
-- Greater horizontal penetration than vertical, so return "top" if dx is neg, bottom otherwise
if dx<0 then return true, "top" else return true, "bottom" end
else
if dy<0 then return true, "left" else return true, "right" end
end
end
return false
end
function CollisionSubsystem:update()
-- Test all objects for collision with other objects
-- Note that this is a brain-dead collision system, highly un-optimal, and meant for illustrative purposes only. For the love of God, please do not do your
-- real collision testing like this.
local o,c
for o,c in pairs(self.componentlist) do
local box0={cx=o.x, cy=o.y, width=c.boxwidth, height=c.boxheight}
local o1,c1
for o1,c1 in pairs(self.componentlist) do
if o1~=o then -- Forbid testing collision with self
--local box1={o1.x-c1.boxwidth/2, o1.y-c1.boxheight/2, o1.x+c1.boxwidth/2, o1.y+c1.boxheight/2, o1.x, o1.y}
local box1={cx=o1.x, cy=o1.y, width=c1.boxwidth, height=c1.boxheight}
local collision, plane=self:testCollision(box0, box1)
if collision then
c.owner:message({message="collide", plane=plane, solid=c1.solid})
end
end
end
end
end
-- BallPhysics Sub-system and related Component creation
BallPhysicsSubsystem={}
function BallPhysicsSubsystem:init()
self.componentlist={}
end
function BallPhysicsSubsystem:createComponent(o, speed, starting_dir_x, starting_dir_y)
local len=math.sqrt(starting_dir_x*starting_dir_x + starting_dir_y*starting_dir_y)
local vx=starting_dir_x/len
local vy=starting_dir_y/len
local c=
{
vx=vx,
vy=vy,
speed=speed
}
c.kill=function(self)
self.subsystem:destroyComponent(self)
self.subsystem=nil
end
c.message=function(self,msg)
if(msg.message=="collide") then
--print("Collide received.")
if msg.solid then
-- Only solid objects bounce a ball
if msg.plane=="top" or msg.plane=="bottom" then self.vy=self.vy*-1 end
if msg.plane=="left" or msg.plane=="right" then self.vx=self.vx*-1 end
-- Should we fudge movement here?
--o.x=o.x+self.vx*self.speed*c.lasttime*2
--o.y=o.y+self.vy*self.speed*c.lasttime*2
end
end
end
self.componentlist[o]=c
o:add_component(c)
end
function BallPhysicsSubsystem:destroyComponent(c)
self.componentlist[c.owner]=nil
end
function BallPhysicsSubsystem:update(dt)
-- Move all the balls
local o,c
for o,c in pairs(self.componentlist) do
o.x=o.x+c.vx*c.speed*dt
o.y=o.y+c.vy*c.speed*dt
c.lasttime=dt
end
end
-- PlayerPaddlePhysicsSubsystem and related Component creation
-- Implement a sub-system and a related component to provide a player-controllable paddle.
-- Hard-coded so keys 'q' and 'z' move the paddle up and down.
PlayerPaddlePhysicsSubsystem={}
function PlayerPaddlePhysicsSubsystem:init()
self.componentlist={}
end
function PlayerPaddlePhysicsSubsystem:createComponent(o,y_low_bound, y_high_bound, speed)
local c=
{
speed=speed,
ylow=y_low_bound,
yhigh=y_high_bound,
subsystem=self
}
c.kill=function(self)
self.subsystem:destroyComponent(self)
self.subsystem=nil
end
c.message=function(self,msg)
end
self.componentlist[o]=c
o:add_component(c)
end
function PlayerPaddlePhysicsSubsystem:destroyComponent(c)
self.componentlist[c.owner]=nil
end
function PlayerPaddlePhysicsSubsystem:update(dt)
-- Check for key input
local o,c
for o,c in pairs(self.componentlist) do
local vy=0
if love.keyboard.isDown('q') then vy=-c.speed*dt end
if love.keyboard.isDown('z') then vy=c.speed*dt end
o.y=o.y+vy
if o.y<c.ylow then o.y=c.ylow end
if o.y>c.yhigh then o.y=c.yhigh end
end
end
-- AIPaddlePhysicsSubsystem
-- Implement a (stupid) system to AI a paddle
AIPaddlePhysicsSubsystem={}
function AIPaddlePhysicsSubsystem:init()
self.componentlist={}
end
function AIPaddlePhysicsSubsystem:createComponent(o, y_low_bound, y_high_bound, speed)
local c=
{
speed=speed,
ylow=y_low_bound,
yhigh=y_high_bound,
subsystem=self,
vy=-speed
}
c.kill=function(self)
self.subsystem:destroyComponent(self)
self.subsystem=nil
end
c.message=function(self,msg)
end
self.componentlist[o]=c
o:add_component(c)
end
function AIPaddlePhysicsSubsystem:destroyComponent(c)
self.componentlist[c.owner]=nil
end
function AIPaddlePhysicsSubsystem:update(dt)
local o,c
for o,c in pairs(self.componentlist) do
o.y=o.y+c.vy*dt
if o.y<c.ylow then o.y=c.ylow c.vy=c.vy*-1 end
if o.y>c.yhigh then o.y=c.yhigh c.vy=c.vy*-1 end
end
end
-------------------------------------
--- The Game
walls={}
function love.load()
love.graphics.setMode(800,600,false,true,0)
love.graphics.setBackgroundColor(0,0,0)
love.graphics.setColor(255,255,255)
GraphicSubsystem:init()
CollisionSubsystem:init()
BallPhysicsSubsystem:init()
PlayerPaddlePhysicsSubsystem:init()
AIPaddlePhysicsSubsystem:init()
-- Build game world
topwall=createObject()
GraphicSubsystem:createComponent(topwall,800,32)
CollisionSubsystem:createComponent(topwall, 800, 32, true)
topwall.x=400
topwall.y=16
bottomwall=createObject()
GraphicSubsystem:createComponent(bottomwall,800,32)
CollisionSubsystem:createComponent(bottomwall, 800, 32, true)
bottomwall.x=400
bottomwall.y=600-16
leftwall=createObject()
GraphicSubsystem:createComponent(leftwall,32,600)
CollisionSubsystem:createComponent(leftwall,32,600,true)
leftwall.x=16
leftwall.y=300
rightwall=createObject()
GraphicSubsystem:createComponent(rightwall,32,600)
CollisionSubsystem:createComponent(rightwall,32,600,true)
rightwall.x=800-16
rightwall.y=300
-- Build a ball
ball=createObject()
CollisionSubsystem:createComponent(ball,8,8,true)
GraphicSubsystem:createComponent(ball,8,8)
BallPhysicsSubsystem:createComponent(ball,128,1,1.5)
ball.x=400
ball.y=300
-- Build a player paddle
ppaddle=createObject()
CollisionSubsystem:createComponent(ppaddle,16,64,true)
GraphicSubsystem:createComponent(ppaddle,16,64)
PlayerPaddlePhysicsSubsystem:createComponent(ppaddle,100,500,256)
ppaddle.x=150
ppaddle.y=300
-- Build an AI paddle
apaddle=createObject()
CollisionSubsystem:createComponent(apaddle,16,64,true)
GraphicSubsystem:createComponent(apaddle,16,64)
AIPaddlePhysicsSubsystem:createComponent(apaddle,100,500,256)
apaddle.x=800-150
apaddle.y=300
end
function love.draw()
GraphicSubsystem:draw()
end
function love.update(dt)
-- Update physics
PlayerPaddlePhysicsSubsystem:update(dt)
AIPaddlePhysicsSubsystem:update(dt)
BallPhysicsSubsystem:update(dt)
-- update collisions
CollisionSubsystem:update()
end
[/spoiler]
The way it works is this:
There are 5 currently implemented sub-systems: Graphics, Collision, BallPhysics, PlayerPaddlePhysics and AIPaddlePhysics. Each sub-system has a corresponding component that is used to interface with the system. Objects are implemented as simple containers that hold a list of components, an (x,y) coordinate (held in the container for convenience, since just about every sub-system uses it) and some basic functionality to handle message-handling and to kill the object. Killing an object will kill and remove all of its components.
The lifetime of an object is as such:
1) Create an object, using the helper function
createObject(). This creates a blank, empty entity
2) Add components using the
createComponent() method of each sub-system. For example, to create a Graphics component of a rectangle sized 32x32, we call
GraphicSubsystem:createComponent(object, 32, 32). A
createComponent() call will create a new instance of the related component which holds local state for the given object, and stick it in a table held internally by the sub-system. It will also put a reference to that component inside the object itself.
3) Initialize position to place the object in the world
The basic rundown of the loop/application is as such:
1) In love.load, initialize all the sub-systems before doing the main loop. This is to initialize the internal component-list tables for each sub-system.
2) In love.load, create the playing field by setting up 4 walls bounding the screen, a ball, a player paddle and an AI paddle. See the function
love.load() for the details.
3) In love.update, call the
update() method of each sub-system. A sub-system update method will iterate the sub-system's internal list of components, and perform the basic update on each component in the sub-system. For example, in Collision update, we check for collisions between objects, and send messages (using the
object.message() function) to an object if a collision with another object is detected; the message includes the plane of the collided-with object (top, bottom, left or right). In BallPhysics subsystem, the BallPhysics component implements a message handler that listens for these "collide" messages, and reverses the x or y velocity vector component depending on which plane the collision involves. In PlayerPaddlePhysics, the update method queries the keyboard and moves the paddle up or down based on 'q' and 'z' keypresses, bounding it between two Y values specified when the PlayerPaddlePhysicsComponent is created. And in AIPaddlePhysics, the update method causes the paddle to just reciprocate back and forth between the bounds.
4) in love.draw(), the
draw() method of GraphicsSubsystem is called, which iterates all components in the internal list and draws the corresponding rectangles.
And voila, you have Pong (albeit Pong minus scoring, minus goals, plus some slightly wonky collision and physics, minus sound). It's very rough, but it demonstrates the idea, I think.