Component Based Objects?

Anything and everything that's related to OGRE or the wider graphics field that doesn't fit into the other forums.
Post Reply
User avatar
Zeal
Ogre Magi
Posts: 1260
Joined: Mon Aug 07, 2006 6:16 am
Location: Colorado Springs, CO USA

Post by Zeal »

especially when it can be done with no loss in performance.
Well yeah sure, if it doesnt effect performance. However I dont see how any implementation could be faster than hard coding the components into c (and referencing them with static enums). Your designers can still compose GameObjects (given the existing components), and when you need a NEW component, you just add it to the raw c source.

Which IMO is a good thing. Because A - it wont happen that often (relatively speaking, as most times youll just be combining existing components to form new 'hybrids'). And B - it seems like a bad idea to give DESIGNERS the ability to monkey with the raw components (a technical job like that should be left up to the c programmer).

*unless of course youre talking about how to name/reference a composed GameObject (of which there would be a infinite amount of combinations, thus enums wouldnt work)? Im talking about using enums just for the components themselves, not all the possible combinations (GameObjects) youll make from them.
User avatar
JohnJ
OGRE Expert User
OGRE Expert User
Posts: 975
Joined: Thu Aug 04, 2005 4:14 am
Location: Santa Clara, California
x 4

Post by JohnJ »

Im talking about using enums just for the components themselves, not all the possible combinations (GameObjects) youll make from them
Yeah, me too.
Well yeah sure, if it doesnt effect performance. However I dont see how any implementation could be faster than hard coding the components into c (and referencing them with static enums).
Well if you look at my post on page 3 you'll see just how my idea works to allow you to access components based on string, while at the same time achieving performance exceeding your ID-based iterative component retrieval technique.

In a way it takes a little more work to write new components in that you need to implement a prefetch() function, but this is really almost no bother at all (as you can see), especially considering the gained performance, flexibility, and ease of use through name-based component access.
Because A - it wont happen that often (relatively speaking, as most times youll just be combining existing components to form new 'hybrids').
True, you won't need to add new components often, but that's no reason not to plan ahead.
And B - it seems like a bad idea to give DESIGNERS the ability to monkey with the raw components (a technical job like that should be left up to the c programmer).
True, but again, that's no reason not to plan ahead. Chances are you (the programmer) are going to want to add new components later, and one of the great advantages to component-based design is the fact that you can easily add new components later without recoding other objects to accept the change. And since it can be done without losing performance, you may as well make the addition of new components as easy and seamless as possible.
sirGustav
Gnoblar
Posts: 9
Joined: Sun Oct 21, 2007 9:40 am

Post by sirGustav »

Here is my design, code shown are pseudo C++ish:

Definitions
There is a master Game that contains a World(level) that contains Objects(player, pickups, npc's etc). Object owns a number of Components. Upon construction the components each gets a object pointer.Component instances are unique to each object. They may store data however they like.
So far my design looks like the other component designs I've encountered.

Types of communication
The components communicate between each with a few different techniques:

First there are messages.
Messages have a scope and a target. The scope can be global, sector or local. Globals are sent to every object in the world. Sectors are sent within a (small) section of the world, that can be a sphere, cube, visibible etc. Locals are only sent within the object.
Target can be specified to only target object with specified components, or even components with special values(Party == MyPary).
Messages are roughly implemented as map<string, FunctionCallbackList> that exists in the Object. The function callbacks all have the following signature: Result MyFunctionCallback(map<string, boost::any>).

Second there are direct communication. As previously said some components need direct communication. This is done by quering the object for some component based on id(string). This is rarely used.

Third there are properties. Some objects need to access data only components, such as position and this is more or less a nicer way to do it. Each object has a property map that really is a map<string, boost::any> and with a simple rule, properties are only added or modified, never removed. So grabbing a reference to a position property and treating it as a local shared variable is never a problem.
Since attributes are strings they can be read from a config file. The WithinFrame component that my meny used kept the cursor within the frame, but also the random movement uv-displaced background within its borders.

Fourth there are scripting. Each object has a local "scripting class instance" that they can register and call/read local functions and variables from. I had a Move component that registered move functions, then I had a Input component that registered key presses and mouse movements and called a onIput function that called those move functions. This may seem like a bad idea, but when was the last time you played a game where you wished you could change the interaction :) If nothing else interaction can be easily changed which is a good thing.

Finally there are outside (system) communication. In my world there are two kinds of systems. Game and World. For the Game such systems migh be some SoundSystem, ConfigurationSystem or some FileSystem. For world such systems might be a PathFinder and a AIPlanner. Upon creation componets gets a map<string, System> that they can use to get references for the systems that they need.

Layers
This seems like a complete system, but lets consider a simple zombie shooter(this happens to be my pet project). The player faces zombies and soldiers. When someone dies we want to transform them into a ragdoll, but zombies will rise again when killed, it's just a matter of time.

Killed zombies(awaiting resurrection) should look like dead zombies(not awaiting resurrection). To me this obviously translates to layers, something like photoshops layers, but then again not. Each component has a priority and one(or several) responsibilities.
By adding a ragdoll component with a higher responsibility than the the others and a timed life we can have our killed zombie that the player thinks is dead but really isn't.

Conclusion
So after this long post(my first here), and my limited experience with components, I feel that the component design shine through. As with the f.e.a.r. AI design I believe that you start to see the power of this system after some time.

This system could easily be used to be the basis of some configurable magic system. An "increase some property overtime for each team members" is an easy feat - and I haven't even designed it for such a feature.

My Components
Finally a list of my would-be components (incomplete) for my cowboy game (my pet project is taking a break):
health_hitpoint
recharge
phys_ragdoll
phys_player (physx player object)
phys_entity (like box, sphere etc)
onHit
MessageTranformer (responds to one message and possible send another)
ObjectTranformer (responds to a message and removes, adds components)
BipedMovement
BipedAiming
CurrentWeapon
Inventory
MeshDisplay
BipedSkeleton (proved a interface to move a mesh biped skeleton)
InputMovement
InputAiming
InputWeapon

String defined enum
Regarding the strings vs enum. I prefer strings because it's easy to extend, especially when it comes to scripting(and in the post mortem from dungeon siege they used 21 C++ components and 148 components made in script) However they have no real error detection, "mesage" or "message" have no (C++) syntax difference, and given that it is a message you can't check what components are registered, though you could loop through each component and make sure the message string is supported. Here is a little trick that might prove useful (forum written):

Code: Select all

class EnumType {
public:
  explicit EnumType(const string& aName) : name(aName) {}
  void add(const string& str, const int i);
  int stringToInt(const string& str) const; // throw an exception if not found
  string intToString(const int i) const; // return "???" if not found
  const string& getName() const;
private:
  const string name;
  map<string, int> mapString;
  map<int, string> mapInt;
};

class Enum {
public:
  Enum(const EnumType& aType, const string& aValue) : type(&aType), value(aType.stringToInt(aValue) {
  }
  string asString() const { return type->intToString(value); }
  bool operator==(const Enum& e) const {
    // throw exception if the types are different
    return e.value == value;
  }
  void operator=(const Enum& e) {
    type = e.type;
    value = e.value;
  }
private:
  Enum() {}
  int value;
  EnumType* type;
};

// usage
EnumType Messages("Messages");
LoadFromFile(Messages); // could use the name to form a path

const Enum kInit(Messages, "init");
const Enum kDeath(Messages, "death");

EnumType Components("Components");
LoadFromFile(Components);
const Enum kHealth(Components, "health");

Enum myEnum = kInit;

bool a = myEnum == kInit; // true
bool b = myEnum == kDeath; // false
bool c = myEnum == kHealth; // exception
The only downer is that you can't use them in a switch, but hey :)
User avatar
Zeal
Ogre Magi
Posts: 1260
Joined: Mon Aug 07, 2006 6:16 am
Location: Colorado Springs, CO USA

Post by Zeal »

The function callbacks all have the following signature: Result MyFunctionCallback(map<string, boost::any>).
Eeek! From my experience boost::any was INSANELY slow. Are you runnign into any performance problems?

And interesting enum/string class. I didnt have time to look at it closely, but you say it gives better performance than using straight strings? Or does it have some other functionality I missed..
User avatar
Game_Ender
Ogre Magi
Posts: 1269
Joined: Wed May 25, 2005 2:31 am
Location: Rockville, MD, USA

Post by Game_Ender »

What part of boost::any is slow? Is it all the copy constructors?, if I remember from looking at the code the casts all use static_cast, so it can't be *that* slow. Of course that's already several more steps than a standard variable has to go through.
Lacero
Halfling
Posts: 72
Joined: Tue Feb 13, 2007 1:57 am

Post by Lacero »

That function callback looks like it copies the entire map, including all strings, every time it's called. I'd be more worried about hte map and string memory allocation than the boost::any copy constructor. Though they could all be as bad :)
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

sirGustav, thanks for sharing. :)

So I plan to have a GameObjectManager, which gives access, creates, and destroys GO's. Should there be some sort of Component Manager? Components are functionality specific, and they all need different data to work, so it would be hard to make this work with any manager. (mCManager->createC1(...), createC2(...), etc.)

Is there any other way to do this using some sort of manager, or does the user have to manually create and destroy components themselves? It would be preferrable to not let the user worry about creation/deletion of components. (the main idea behind managers, right? :wink: )
Creator of QuickGUI!
User avatar
Zeal
Ogre Magi
Posts: 1260
Joined: Mon Aug 07, 2006 6:16 am
Location: Colorado Springs, CO USA

Post by Zeal »

Kinda curious about this too. Currently it seems like YOU have to 'new' all the components, but then you pass responsibility (to 'delete') to the object/manager. That seems like bad design...
User avatar
Falagard
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 2060
Joined: Thu Feb 26, 2004 12:11 am
Location: Toronto, Canada
x 3
Contact:

Post by Falagard »

KungFooMasta wrote:sirGustav, thanks for sharing. :)

So I plan to have a GameObjectManager, which gives access, creates, and destroys GO's. Should there be some sort of Component Manager? Components are functionality specific, and they all need different data to work, so it would be hard to make this work with any manager. (mCManager->createC1(...), createC2(...), etc.)

Is there any other way to do this using some sort of manager, or does the user have to manually create and destroy components themselves? It would be preferrable to not let the user worry about creation/deletion of components. (the main idea behind managers, right? :wink: )
Class factories with an initialization class or an ID which loads data from file is your solution. Basically a plugin system.

Register class factories with the ComponentManager. It gets a plugin name and looks up the appropriate class factory to instantiate the object.
Once instantiated, it calls an init function passing a class with the data required to init the class, or alternatively, a config file where the class loads its own data.

ComponentManager::create(String pluginType, ComponentInitializer* initializer)
and/or
ComponentManager::create(String pluginType, String id)
where the ComponentManager or the instantiated Component itself use the id to read a data file to get its settings.

Ogre uses factories and name/value pairs instead of an initializer but name/value pairs are pretty limited.
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

Yah, I was affraid of that. I'm not a big fan of factories, they always seem like a lot of work. I remember Game_Ender posted a Maker pattern in the QuickGUI threads, that might be useful too.

This is what I'm thinking, please tell me if it sounds limiting or bad design:

Make a Component Class, where the constructor and destructor are protected or private. Force users to implement a create method:

Code: Select all

virtual Component* create(Component::Data* d)=0;
create a map in the ComponentManager:

Code: Select all

typedef void (*componentConstructor)(Data*);
map<string,componentConstructor> mCConstructors;
And then allow users to register component types:

Code: Select all

void registerComponent(string type, componentConstructor);
void unregisterComponent(string type);
And then the user can create components via the manager:

Code: Select all

CRender* r = mCManager->createComponent("Render",&CRenderData);
CMesh* m = mCManager->createComponent("Mesh",&CMeshData);
Any pitfalls with this? (I hate not being able to use Enums... switch statements feel so much better than a ton of if statements..)

[edit] Or maybe you can make a function pointer to the constructor directly, I don't remember if I tried that or not. Using the method above, the create method might have to be static. It's essentially a wrapper for the constructor. [/edit]
Creator of QuickGUI!
User avatar
JohnJ
OGRE Expert User
OGRE Expert User
Posts: 975
Joined: Thu Aug 04, 2005 4:14 am
Location: Santa Clara, California
x 4

Post by JohnJ »

I'm still considering using component-based objects in my game engine, since it occurred to me that you can very easily construct different vehicle types from components like:

CChasis
CTurret
CTracks
CWheels
CHoverJets
CWings

With a system like this, you could make a traditional tank with CChasis + CTurret + CTracks, a hover-tank with CChasis + CTurret + CHoverJets, a simple drivable vehicle with CChasis + CWheels, an airplane with CChasis + CWings, a VTOL with CChasis + CWings + CHoverJets, and the list goes on, of course.

My main problem now is that most of these components rely on the physics system, and may possibly rely on each other. For example, when initializing a CTurret, it may need to ask CChasis for it's physics body in order to operate properly. Unfortunately, this isn't available until CChasis is initialized, so this means one of the following:

A. CChasis must always be initialized before CTurret/etc. (fast)
B. CTurret/etc. must be re-initialized after CChasis is loaded if it was loaded first (slow)
C. A messaging system must be used for all inter-component communications, which would be the most flexible since it eliminates the need for a specific initialization order (slow, clumsy)

Currently I'm planning to implement method A, since it's fast and not too messy. Method B could possibly be easy and flexible, but components would have to be loaded and unloaded repeatedly during the creation of a single GameObject, which is unacceptable. Method C is the most flexible and true to the spirit of a component design, but may not be fast enough and will make new components more difficult to implement.

If you have any suggestions, or other techniques I can use here when a certain component's initialization function relies on the output of another component which is only available after it has been initialized, let me know :)
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

Requiring an order to how the components are added sound like a bad idea, and error prone. Maybe something like:

Code: Select all

virtual void Component::onComponentAdded(string ID) {}
virtual void Component::init() {}
If the required component's don't exist, or the component has already been initialized, return. Otherwise, carry out the intended functionality. Now you should be able to add components in any time you want.

Did you read my post? Any thoughts on using that method in combination with a ComponentManager to create Components? :P

[edit]
Or just bypass the onComponentAdded notification and have every component try to init itself when a component is added or removed. If already initialized, it will immediately return. (if desired) This could even allow some complex functionality, for example, if a component behaves differently when a component is not present. You can add/remove components in real time and it will adapt correctly. 8)
[/edit]
Creator of QuickGUI!
User avatar
Falagard
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 2060
Joined: Thu Feb 26, 2004 12:11 am
Location: Toronto, Canada
x 3
Contact:

Post by Falagard »

Factories are dead easy, but whatever floats your boat :-)
User avatar
JohnJ
OGRE Expert User
OGRE Expert User
Posts: 975
Joined: Thu Aug 04, 2005 4:14 am
Location: Santa Clara, California
x 4

Post by JohnJ »

virtual void Component::onComponentAdded(string ID) {}
That may work, but not in the event of a "chain" of dependancies, for example:

CPhysics --> CChasis --> CTurret

I'm not saying this is a good idea, but just pointing out that in this case, something like this might happen:

1. CTurret is added and initialized.
2. CChasis is added and initialized. CTurret is notified that CTurret is added, and attempts to access it's physics body object. Program crashes, because CChasis is actually waiting for CPhysics to be initialized in order for it to be able to create it's physics body object.

There are other problems I can think of, but this best demonstrates the root issue with this method and "random" dependencies. At worst, it's no better than before, since whether your application crashes or not largely depends on the order which Components were added in.
Or just bypass the onComponentAdded notification and have every component try to init itself when a component is added or removed. If already initialized, it will immediately return. (if desired) This could even allow some complex functionality, for example, if a component behaves differently when a component is not present. You can add/remove components in real time and it will adapt correctly.
That's basically the Method B I described. The problem is constantly reinitializing every time a new component is added could result in O(N(N+1)/2) initialization time where normally it should be O(N) (where N is the number of components for the object), so a 5 component object might perform 15 initializations where it should only need 5.
Did you read my post? Any thoughts on using that method in combination with a ComponentManager to create Components?
I'm really too new to component designs to be very helpful, but I think string based factory construction is really useful, since you can load your world from XML/ogre-script-style files without any knowledge of installed components.

Of course, it would be really convenient if C++ supported reflection, but it doesn't so we have to use ugly work-arounds like factories.
User avatar
Zeal
Ogre Magi
Posts: 1260
Joined: Mon Aug 07, 2006 6:16 am
Location: Colorado Springs, CO USA

Post by Zeal »

@KungFoo

The example app that Questor / Fused posted earlier seems to contain a simple component manager/factory...

Code: Select all


#ifndef __COMPTEMPLATEMGR_HPP__
#define __COMPTEMPLATEMGR_HPP__

#include <map>
#include "ComponentTemplate.hpp"

class CompTemplateMgr {
protected:
	typedef std::map<const comp_id_type, ComponentTemplate*> comp_table_t;

	CompTemplateMgr() {}
public:
	~CompTemplateMgr() {}
	
	static CompTemplateMgr *getInstance() {
		if(mInstance == NULL)
			mInstance = new CompTemplateMgr();
		return mInstance;
	}

	void clearComponents() {
		comp_table_t::iterator iter;
		for(iter = mTemplates.begin(); iter != mTemplates.end(); iter++) {
			delete iter->second;
		}
		mTemplates.clear();
	}

	void registerTemplate(ComponentTemplate *templ) {
		mTemplates[templ->componentID()] = templ;
	}

	Component *createComp(comp_id_type& comp) {
		return mTemplates[comp]->makeComponent();
	}

protected:
	comp_table_t mTemplates;

	static CompTemplateMgr *mInstance;
};

#endif

Code: Select all


#ifndef __COMPONENTTEMPLATE_HPP__
#define __COMPONENTTEMPLATE_HPP__

#include "Component.hpp"

class Object;

class ComponentTemplate {
public:
	ComponentTemplate() {}
	virtual ~ComponentTemplate() = 0 {};

	// returns the ComponentID that, by default, we should
	// register created Component as
	virtual const comp_id_type& componentID() const = 0;
	virtual const comp_id_type& familyID() const = 0;

	virtual Component* makeComponent() = 0;
};

#endif
Seems pretty simple. You just register all your components with this Manager/singleton, then you can create new components THROUGH the manager (so it can manager the new/delete).
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

Good point. We should not check for existance of required components, but initialization of required components. CChasis won't be initialized unless CPhysics is added, and CTurret won't work unless CHassis is present and initialized, etc.

So whenever a component is initialized it will try to init the other components.

Like you said the big O notation might not be ideal, but they are minimal comparisons, and shouldn't cost a whole lot. (and not run every frame)

Code: Select all

void CTurret::init()
{
     if( (mOwner->getComponent("Chasis") == NULL) || !mOwner->getComponent("Chasis")->initialized()  || mInitialized )
          return;
     ...
}
I also thought of the scenario where adding on parts would contribute to weight, which could affect factors like move speed. Removing parts would also affect weight, so either way you'd want to be notified when components are initialized, or removed. Not every component will depend on others, some components will have just one comparison, which checks if its already initialized, and if so, returns.

Factories aren't hard to implement, its just a lot of code, and requires users to create a factory class for each instance. That's one extra step per class. But maybe after a while I'll get used to using factories all over the place. I hope not. :P
Last edited by KungFooMasta on Mon Oct 22, 2007 11:33 pm, edited 1 time in total.
Creator of QuickGUI!
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

Thanks for bringing that up, it seems really similar to my idea! Instead of a static function pointer, he has a creator class, which is most likely a friend of the component is creates.

I'll probably start with this route and modify it to my needs.

Thanks!

[edit]
I guess I will be using factories.. oh well. :lol:
[/edit]
Creator of QuickGUI!
User avatar
Zeal
Ogre Magi
Posts: 1260
Joined: Mon Aug 07, 2006 6:16 am
Location: Colorado Springs, CO USA

Post by Zeal »

So correct me if im wrong, but that code I posted IS in fact a "factory" right? They dont seem so scary..
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

Yah, I would say they are a factory.

Some things that will need to be modified for that code to be useful:

Code: Select all

Component *createComp(comp_id_type& comp) { 
      return mTemplates[comp]->makeComponent(); 
   }
Changed to

Code: Select all

Component *createComp(comp_id_type& comp,Data* d) { 
      return mTemplates[comp]->makeComponent(d); 
   }
Which means the makeComponent method needs to be altered to accept argument(s). At least, I can't imagine all components able to be created with no data initially.. (a mesh component should have a mesh file, right?)
Creator of QuickGUI!
User avatar
Falagard
OGRE Retired Moderator
OGRE Retired Moderator
Posts: 2060
Joined: Thu Feb 26, 2004 12:11 am
Location: Toronto, Canada
x 3
Contact:

Post by Falagard »

Code: Select all

FactoryTemplate<MyClass> myClassFactory;
FactoryManager->addFactory("MyClass", myClassFactory);
Ouch! My fingers are sore from all that typing.
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

Die. :lol:

So that's all it takes? I remember looking at the SceneManager source, and there are factories for each type, and methods to handle metadata and such.

Well now that we've discussed it, and thanks to falagard's enthusiasm, I see how simple it is to use the factory design.

:P
Creator of QuickGUI!
User avatar
Zeal
Ogre Magi
Posts: 1260
Joined: Mon Aug 07, 2006 6:16 am
Location: Colorado Springs, CO USA

Post by Zeal »

Component *createComp(comp_id_type& comp,Data* d) {
return mTemplates[comp]->makeComponent(d);
}
Passing data with a void pointer? Seems like it wouldbe safer to 'make' the component with no parms, then once you get the ptr back, cast it to its correct component type, and use 'setter'methods to set the mesh, properties, ect...

Besides, it would be nice if all the customizeable properties of your components could be changed via setter methods at any time. That way you could change the mesh property on a object without having to destroy/recreate the whole thing.
User avatar
Game_Ender
Ogre Magi
Posts: 1269
Joined: Wed May 25, 2005 2:31 am
Location: Rockville, MD, USA

Post by Game_Ender »

Well a more powerful factory pattern templated on the "Data" type with a custom creation function/class for each type of component can work around that. If you want to get super crazy you can put a generic property system in the component interface, but then you get into using things like Boost.Any again.
User avatar
KungFooMasta
OGRE Contributor
OGRE Contributor
Posts: 2087
Joined: Thu Mar 03, 2005 7:11 am
Location: WA, USA
x 16
Contact:

Post by KungFooMasta »

Is it really a void pointer? I was thinking of an actual class, lets say

Code: Select all

class ComponentData
And then have classes like CMeshData, CXData, etc.

CMesh* m = mComponentManager->createComponent("Mesh",myMeshData);
Creator of QuickGUI!
User avatar
Zeal
Ogre Magi
Posts: 1260
Joined: Mon Aug 07, 2006 6:16 am
Location: Colorado Springs, CO USA

Post by Zeal »

CMesh* m = mComponentManager->createComponent("Mesh",myMeshData);
But what will the declaration for createComponent() look like? It cant accept variable data type unless you use a boost:any or a void*.

I still think my idea would work well, where all components just have a empty constructor, and you set the initial parms AFTER the component is made via setter functions.

*major edit

Nevermind I didnt see all your data classes derive from a common base. So yeah you could pass em through as base data, then cast em back once inside your component. Although that would require writing a special data class for every component, and then doing all the casts, but in the end i guess its about the same extra work as my method.
Post Reply