How to use "Component-Based Game Object System"

Started by
5 comments, last by aaron_ds 14 years, 1 month ago
Hi guys, I've surveyed component-based game object system for a long time and created one for my current project. I know its idea is dividing functionalities of game objects to several components to prevent deep class hierarchy. However, I still cannot figure out how to build my game upon component-based system. For example, a character (controlled by player) fires a RPG. Then rocket flies over the sky, hits the building and then explodes. How do I code these events and logics into components? Assume that the character, RPG, and rocket are game objects. Following is my approach

GameObject* character = scene->createGameObject("player", "character.xml");
character->getComponent<CharacterLogic>()->fireWeapon("RPG");

and somewhere inside CharacterLogic::fireWeapon(), there is a piece of code like this

GameObject* rpg = _scene->getGameObject("rocket");
rpg->getComponent<RpgLogic>()->fire();

Inside RpgLogic::fire()

GameObject* rocket = _scene->createGameObject(some_obj_name, "bullet_rocket.xml");

rocket->setPosition( this->getObject()->getPosition() + _offset );
rocket->getComponent<RigidPhysics>()->setVelocity( this->getObject()->getOrientation()*Vector3::AxisX()*_rocketSpeed );

There is a component RocketLogic inside the rocket. When RigidPhysics component of the rocket detects collision between the rocket and some other objects, it will send a message and notify RocketLogic component. So RocketLogic calls its member function RocketLogic::onContact() to trigger explosion effect.

//inside RocketLogic::onContact()
GameObject* explosionEffect = _scene->createGameObject( some_obj_name, "explosion.xml" );
explosionEffect->getComponent<ParticleEffect>()->run();
//I don't known whether it is appropriate to implement the explosion effect as a game object

I've noticed that my design has many drawbacks. Every time you need to do some operation, you have to call getComponent with a specific component type as template arguments. So it takes some time for game object to find the component. (look up hash_map ) And if the specified component ( by getComponent ) doesn't exists in the game object, we need to do some error handling, and thus causing a lot of error handling code in my program. Could anyone give me some suggestion?
Advertisement
I use a messagesystem, with it you can simply send a message to any component and if the component nows the message it will react to it. To continue your example, you could send a message "FireWeapon" with an parameter RPG. Your component would get the message and the parameter and would then create the rocket. Also you could store more information into the message, like the point it was fired etc.
This of course has some drawbacks, mainly that you cant be sure a component understood your message, also it can make your code a lot less typesafe, depending on you implementation of the message system.
I've been using a component system for the last 3 games I've worked on. Yes I have the same lookup/down-casting code. I usually try to store pointers to components for use later on.

You could keep a CharacterLogic* around for use whenever the character needs to fire its weapon. An RpgLogic* could be kept in the CharacterLogic class so that fire() may be called on it, and the RpgLogic* could be swapped out with another instance whenever the character's weapon is changed.

As for error handling, I've gotten by with throwing exceptions for missing components and for trying to add a new component with the same name as an old component. What sort of errors are you handling beyond this?


My scene nodes are your game objects and my scene node properties are your components.
SceneNodeProperty& SceneNode::AddSceneNodeProperty(const std::string& name, SceneNode::SceneNodePropertyPtr scenenodeproperty){        if(properties.find(name)==properties.end()){                properties.insert(SceneNodeProperties::value_type(name, scenenodeproperty));                return *scenenodeproperty;        }else{                throw SceneNodePropertyAlreadyExists(name.c_str());        }}SceneNodeProperty& SceneNode::GetSceneNodeProperty(const std::string& name)const{        if(properties.find(name)!=properties.end()){                return *properties.find(name)->second;        }else{                throw SceneNodePropertyDoesNotExist(name.c_str());        }}
As long as you are very careful about when components might be changed or freed ( do it all at once at end of frame simulation, for instance, then you can keep pointers around for speed.
Thanks for your suggestion, guys.

I have another issue encountered recently for my component-based framework.

Since there are many kinds of weapons in my game, I create an "abstract" component (It's an abstract class) called ComWeaponLogic, and there are derived components implemented for each kind of weapons. For example,
class ComRpgLogic : public ComWeaponLogic
.

However, if I want to get the component pointer from weapon game object, I must name all derived weapon logic component to the same string, eg. "weapon". Then, I retrieve the weapon logic component like this
GameObject* go = _scene->getObject( weaponName );ComWeaponLogic* weapon = static_cast<ComWeaponLogic*>(go->getComponent("weapon"));
.

I not sure whether it is suitable to make all derived weapon components have the same name. This kind of solution is wired. Although, this kind of problem may be solved by message passing pattern, I still concern about the speed of routing message.

Any opinion?




Quote:Original post by alvinsay
...
I not sure whether it is suitable to make all derived weapon components have the same name. This kind of solution is wired. ...
The name is used as a kind of type system. As such the common "weapon" denotes a base-type for all weapons, regardless which one is the actually used specialization of the weapon component. Why should that be wired? E.g. all OOP languages have such a mechanism.
I find myself using the strategy pattern here a lot. This way the strategy[weapon] can be selected at runtime. I'm of the opinion that this more closely follows the open/closed principle.

Your example would translate something like this.
class Component{    public:    virtual ~Component(){}};class IWeapon{    public:    virtual void Fire()=0;};class WeaponComponent: public Component{    IWeapon& weapon;    public:    WeaponComponent(IWeapon& weapon):weapon(weapon){}    IWeapon& GetWeapon(){        return weapon;    }    const IWeapon& GetWeapon()const{        return weapon;    }};class RPGWeapon: public IWeapon{    public:    void Fire(){        GameObject* rocket = _scene->createGameObject(some_obj_name, "bullet_rocket.xml");        rocket->setPosition( this->getObject()->getPosition() + _offset );        rocket->getComponent<RigidPhysics>()->setVelocity( this->getObject()->getOrientation()*Vector3::AxisX()*_rocketSpeed );    }};class PistolWeapon: public IWeapon{    public:    void Fire(){        //however your pistol fires    }};

This topic is closed to new replies.

Advertisement