Hi Again,
I figured I'd post a little something about it since I've been using it for a bit. I wrote up a quickie (although a bit lengthy) example of how it could work in blitzmax.
Let's start with the syntax of how it'd be used since ultimately that's what we're trying to go for once all the nitty-gritty is implemented:
so let's say your game requires physics for multiple objects. But you have one particular type of 'object' that needs physics AND be controlled by the player BUT you don't want collision detection for the player as they can walk through walls for instance:
' lets make an object that has physics, AND can be controlled by our player.
Local myPlayer:TBaseObject = TBaseObject.Create("Robot", 40, 40)
'...now to give our new empty object physics and controls we simply add the functionality to them
myPlayer.AddComponent(New TPhysicsComponent)
myPlayer.AddComponent(New TControllerComponent)
myPlayer.Init()
' lets run it!
myPlayer.Update()
Following that logic, say your player can shoot different types of bullets. Bullets collide with objects but don't obey your normal physics. Let's say one of these types of bullets is a laser. So the setup code could look something like this, note that I don't add a physics component because my bullets don't need them:
' now lets make a bullet with laser flight
Local myBullet:TBaseObject = TBaseObject.Create("Laser", 30, 30)
myBullet.AddComponent(New TLaserComponent)
myBullet.AddComponent(New TCollisionComponent)
myBullet.Init()
myBullet.Update()
' and so on...
By now hopefully things make a little more sense as to how different object 'types' can be composed, and potentially how much flexibility you now have when designing your in-game objects. Now when I want to tweak how my laser logic works I just edit the laser component, with very little worry about affecting other portions of the 'whole'.
Ok, an example implementation follows. I apologize for the length, but it's a complete working example so for those that wish to know the basics of how it works hopefully can benefit from this.
Let's start with our base object that will be housing these components:
rem
this is our 'container' class.
it provides very basic functionality that we
determine every 'object' should have.
in this example I've only determined
that each 'object type' should have a name and position
end rem
Type TBaseObject
Field _name:String
Field _components:TMap = CreateMap()
Field _x:Float
Field _y:Float
' factory method to help create a base object.
Function Create:TBaseObject(name:String, x:Float, y:Float)
Local obj:TBaseObject = New TBaseObject
obj._name = name
obj._x = x
obj._y = y
Return obj
End Function
' some basic setters/getters
Method SetPosition(x:Float, y:Float)
_x = x
_y = y
End Method
Method GetX:Float()
Return _x
End Method
Method GetY:Float()
Return _y
End Method
Method GetName:String()
Return _name
End Method
' to provide some coherency
' we'll add an init() function
' that we call once all the components
' for an object have been added
' and wish to initialize themselves
Method Init()
For Local comp:TComponent = EachIn _components.Values()
comp._OnAdd(Self)
Next
End Method
' our update function becomes
' simplified for this object
' since all the functionality
' is tucked away in it's components.
Method Update()
For Local comp:TComponent = EachIn _components.Values()
comp.Update()
Next
End Method
' this allows use to search for a particular component
' and functionality based on name. Since it is
' using a tmap, retrevial is fast compared to a linked list.
' the only downside is 'update' order is not easily determined. a trade-off
' but could be solved with a more complex solution.
Method GetComponent:TComponent(componentName:String)
If _components.Contains(componentName) Then
Return TComponent(_components.ValueForKey(componentName))
End If
Return Null
End Method
' this adds a new type of component
' to this object. In this implementation
'you cannot add, for instance, 2 physics components, as it doesn't make much sense
Method AddComponent(component:TComponent)
If Not _components.Contains(component.GetType()) Then
_components.Insert(component.GetType(), component)
End If
End Method
End Type
Nothing too mind-blowing out of the above. I chose to use a TMap as my collection class for components for faster 'look ups'. Others may choose to stick with lists, but that detail is irrelevant for this example. One constraint about the above system: you cannot add the same type of component more than once to a single base object.
Now for our component class:
rem
our base component class.
each component class _must_ have a way of identifying what type
of component it is. For this example we will just use a 'type' string.
This isn't the most 'strongly' typed way to do it, but for simplicity
it will do.
end rem
Type TComponent Abstract
Field _owner:TBaseObject
Field _type:String
Method New ()
_type = "TComponent"
End Method
Method GetOwner:TBaseObject()
Return _owner
End Method
Method GetType:String()
Return _type
End Method
Method SetType(typeName:String)
_type = typeName
End Method
Method OnAdd(owner:TBaseObject) Abstract
Method OnRemove() Abstract
Method Update() Abstract
rem
this is consider an 'internal' method
used only by our base component class to help facilitate the aggregation
end rem
Method _OnAdd(owner:TBaseObject)
_owner = owner
OnAdd(owner)
End Method
rem
another internal method to help facilitate the component structure
end rem
Method _OnRemove()
_owner = Null
OnRemove()
End Method
End Type
A rather small class. It only requires children to implement an 'Update','OnAdd', and 'OnRemove'. So it is pretty open to do essentially whatever you want. You could even require components to have a Draw() method, making components a viable way of implementing different rendering styles.
Now how is a component class implemented from the above code?
' a basic physics component could encapsulate the physics calculations
' and formulas needed to do simulations.
Type TPhysicsComponent Extends TComponent
Field _velx:Float
Field _vely:Float
Method New ()
SetType("TPhysicsComponent")
End Method
Method OnAdd(owner:TBaseObject)
' sometimes a component may want to query for other components
' in the owner to gain access to other data if there are dependencies.
End Method
Method OnRemove()
End Method
' a trivial example for 'physics' functionality.
Method Update()
Local owner:TBaseObject = Self.GetOwner()
Local x:Float = _velx + 3
Local y:Float = _vely + 3
owner.SetPosition(x, y)
Print "physics update!"
End Method
End Type
A very rudimentary (ie non-functioning) physics component. You could imagine a physics component that did actual physics calculations would be very helpful, and actually very re-usable when implemented in this fashion.
I'm only going to paste one more implementation that addresses an issue that arises from this type of design:
' a basic controler behavior that could attached.
Type TControllerComponent Extends TComponent
Field _physics:TPhysicsComponent
Method New ()
SetType("TRobotAIComponent")
End Method
Method OnAdd(owner:TBaseObject)
'lets say for example this controller type needs to know the object's velocity to function. lets query for it:
_physics = TPhysicsComponent(owner.GetComponent("TPhysicsComponent"))
' now the above is not ideal if you wish to decouple your components. for more information on a solution that'll
' solve that look into Data ports:
' <a href="http://www.gamasutra.com/view/feature/1779/implementing_dataports.php" target="_blank">http://www.gamasutra.com/view/feature/1779/implementing_dataports.php</a>
' these can be easily adapted to the component architecture where the 'baseObject' object is it's own
' individual data port manager, and components and register/read/write to it's owner's data ports.
End Method
Method OnRemove()
End Method
Method Update()
Print "controller update!"
End Method
End Type
For those just skimming over the code:
Sometimes components (for example, a physics component and a collision component) need to communicate with each other in order to function. This is not uncommon, and when doing the 'large class' way it's easy for different sections to communicate. Components are supposed to be a way to de-couple functionality from each other and allow it to be reusable. You could easily 'query' for the dependent components (as above) if you don't care too much about coupling. But if you do then I highly recommend reading this article for a solution to this problem:
Implementing DataportsOk so that's a quick run down of a way to implement aggregation/components in bmax. Hope it's helpful :).
The whole compilable bmx file is
here.