programming practice: composition (aggregation)?
BlitzMax Forums/BlitzMax Programming/programming practice: composition (aggregation)?
| ||
AlexO pointed out composition(aggregation) over inheritance in this thread, and I'd like to know more about how to use it in bmax (syntax+examples) Inheritance is something I know how to use, and I've just recently heard about composition (or aggregation). Could somebody provide a sample bmax code that uses composition - since right now I'm very new to bmax+OO including composition) stuff in general... I've read & seen pictures about what this is, but wasn't sure how it works in bmax. I know that my game would seriously benefit from having a composition (since in Blitz3D I had this one HUGE "class" that had EVERYTHING in it :D). Now I'd like to understand how compose things in bmax. Any pointers/links/aid would be great. |
| ||
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: ' http://www.gamasutra.com/view/feature/1779/implementing_dataports.php ' 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 Dataports Ok 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. |
| ||
Oh one more thing I wanted to mention. This design is not only great for game logic, but for the structure of the game application itself. I've applied it to my framework and it's been a treat to work with. Also worth noting, XNA Framework's design is of a similar nature when it comes to adding functionality to the game (They use components to provide functionality and services as a means to access them). So if you have a similar generic game class you could theoretically add in game components like 'GUI', 'Networking', 'Player profile saving', etc. |
| ||
Thanks AlexO, I'll take a look at this and comment later. (Also spotted this: http://geekswithblogs.net/mahesh/archive/2006/07/05/84120.aspx - comes handy for those of us who are learning OO standards... I presume in bmax there's no "interfaces"?) |
| ||
sadly no interfaces in bmax :(. Not in the way that article describes it anyway (C#). |
| ||
Very interesting/useful example Alex, thanks for sharing. :) |
| ||
In a simple example this is aggregation and composition in C++ c++ Composition, similar Association struct A { private: B* b; }; Bmax composition Type A Field b:B; End Type c++ Aggregation struct Ship { private: std::list<Weapon*> m_weapons; }; Bmax Aggregation Type Ship Field m_weapons:TList; End Type I'm pretty sure that's right. But Bamx lists are not as powerfull as C lists, and vectors. |
| ||
In what way are bmax lists not as powerful as C lists? I'd be interested in adding some functionality to make them more useful. |
| ||
This might be one of them, I did go over it with a friend who has been programming in C for about 16 years, I've only been using it for about 4 months, I think this is what we decided. In Blitz you define a list like this Local myList:Tlist That creates a general list object, for anything, there is no targeted scope, or type, it just seems vague to what exactly that list is holding, whereas in C you do this. std::vector<Entity*> entity_list; or std::vector<Enemy*> enemy_list; That way you are creating a list object with a defined type, this is more logical for object oriented programming, isn't it? So say you have a list that you want to store a bunch of Weapons, or Items, in Bmax they would be defined like Field m_weapons:TList At the end of the day, that is a list called m_weapons, anything could be added to that, it's just simply a list, of somethings that can be added. I just don't see how it's logical to OO programming, I might be wrong, it was a while ago when we were looking into it. |
| ||
That's not exactly an indication of 'power' though. I really like Bmax implementation of lists and if you're REALLY going down the OO route you would be hiding list names and only accessing them via the objects methods.... wouldn't you? Or is this all over-analysis for, what appears to me, an undefined problem? |
| ||
Ah, your right, you do Local p:Player = EachIn entity_list don't you, it's a bit more long winded than that in C++, you have to do type_casting, I'll have a re-think and see what it was that we were talking about on this subject. |
| ||
Isn't that just a typesafe issue in effect only objects of a specific type can be added to a c++ STL list. Note this is not a default C++ language feature but the vector template in the Standard Template Library (a very nice addon to the C++ langugae). You can have a nice messy pointer based list in C++ as well! As blitzmax has reflection it would be possible to create a typesafe wrapper for a List that prevent's objects being added which do not belong to a specific type but this would have an overhead when adding items to the list! In effect though the C++ compiler will complain when you attempt to add objects of the wrong type whereas in BlitzMax you would only recieve an error at runtime! |
| ||
Thinking about it further and having a quick chat with him he said was it something to do with the lack of making lists private? Something to do with other Types having the ability to access the lists and adding themselves, but that's again a design thing, not directly to do with lists. |
| ||
Thanks guys. I've been going through this... and the example 'composition' for some reason doesn't seem similar to the composition examples the book I'm reading has (Head First: Design Patterns) - maybe it's just me :) I'll read the book chapter couple of more times and compare with this thread... I wonder what's the main difference between an interface and an abstract class is - can they be exactly the same too?? |
| ||
Thanks AlexO. I loved your tutorial and plan to make use of it. I thought it was interesting because it is a different take on an approach I have been doing anyway. In my Ultima IV type RPG I might do something like this: So I would have these other objects ( your compnents ), or pointers to them, defined in my base object in case that particular object needed that functionality. So my Chest entity would have a pointer to TContainer but would not have one to TMove or TVehicle etc. What I didn't like is I would end up with a long list of these pointers in my base object and everytime I think of a new one I would have to modify my base class. So your approach functionally ends up the same but I think is much more elegant and probably flexible. I don't have to explicitly code fields for each possible component into my base class. If you are inclined to explain other similar types of design patterns and how they are used in BM I would be a big fan of that. I think this little tutorial should go in the Tutorial thread. |