Component
Intent
Allow a single entity to span multiple domains without coupling the domains to each other.
The Pattern
Imagine we have a class that spans multiple domains. We could have a single class manage all of the complexity, or we could split each domain into its own class, and have our class just manage the components.
When to Use It
-
You have a class that touches multiple domains which you want to keep decoupled from each other.
-
A class is getting massive and hard to work with.
-
You want to be able to define a variety of objects that share different capabilities, but using inheritance doesn’t let you pick the parts you want to reuse precisely enough.
Splitting out a Domain
class Bjorn {
public:
Bjorn() : velocity_(0), x_(0), y_(0) {}
void update(World& world, Graphics& graphics);
private:
static const int WALK_ACCELERATION = 1;
int velocity_;
int x_;
int y_;
Volume volume_;
Sprite spriteStand_;
Sprite spriteWalkLeft_;
Sprite spriteWalkRight_;
};
void Bjorn::update(World& world, Graphics& graphics) {
switch (Controller::getJoystickDirection()) {
case DIR_LEFT:
velocity_ -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
velocity_ += WALK_ACCELERATION;
break;
}
x_ += velocity_;
world.resolveCollision(volume_, x_, y_, velocity_);
Sprite* sprite = &spriteStand_;
if (velocity < 0) sprite = &spriteWalkLeft_;
else if (velocity_ > 0) sprite = &spriteWalkRight_;
graphics.draw(*sprite, x_, y);
}
Splitting Out a Domain
class InputComponent {
public:
void update(Bjorn& bjorn) {
switch (Controller::getJoystickDirection()) {
case DIR_LEFT:
bjorn.velocity -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
bjorn.velocity += WALK_ACCELERATION;
break;
}
}
private:
static const int WALK_ACCELERATION = 1;
};
class Bjorn {
public:
int velocity;
int x;
int y;
void update(World& world, Graphics& graphics) {
input_.update(*this);
x += velocity;
world.resolveCollision(volume_, x, y, velocity);
Sprite* sprite = &spriteStand_;
updateSprite(*sprite);
graphics.draw(*sprite, bjorn.x, bjorn.y)
}
private:
void updateSprite(Sprite& sprite) {
if (velocity < 0) sprite = &spriteWalkLeft_;
else if (velocity > 0) sprite = &spriteWalkRight_;
}
InputComponent input_;
Volume volume_;
Sprite spriteStand_;
Sprite SpriteWalkLeft_;
Sprite spriteWalkRight_;
};
Let’s keep splitting:
class PhysicsComponent
{
public:
void update(Bjorn& bjorn, World& world)
{
bjorn.x += bjorn.velocity;
world.resolveCollision(volume_,
bjorn.x, bjorn.y, bjorn.velocity);
}
private:
Volume volume_;
};
class GraphicsComponent
{
public:
void update(Bjorn& bjorn, Graphics& graphics)
{
Sprite* sprite = &spriteStand_;
if (bjorn.velocity < 0)
{
sprite = &spriteWalkLeft_;
}
else if (bjorn.velocity > 0)
{
sprite = &spriteWalkRight_;
}
graphics.draw(*sprite, bjorn.x, bjorn.y);
}
private:
Sprite spriteStand_;
Sprite spriteWalkLeft_;
Sprite spriteWalkRight_;
};
Here’s our Bjorn Class.
class Bjorn
{
public:
int velocity;
int x, y;
void update(World& world, Graphics& graphics)
{
input_.update(*this);
physics_.update(*this, world);
graphics_.update(*this, graphics);
}
private:
InputComponent input_;
PhysicsComponent physics_;
GraphicsComponent graphics_;
};
Robo Bjorn
One more step, we can make this better by abstracting out the behavior to interfaces.
class InputComponent {
public:
virtual ~Inputcomponent() {}
virtual void update(Bjorn& bjorn) = 0;
};
Next, we have our current user input handling code conform to that interface:
class PlayerInputComponent: public InputComponent {
public:
virtual void update(Bjorn& bjorn) {
switch (Controller::getJoystickDirection()) {
case DIR_LEFT:
bjorn.velocity -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
bjorn.velocity += WALK_ACCELERATION;
break;
}
}
private:
static const int WALK_ACCELERATION = 1;
};
Now Bjorn should hold a pointer to that input component instead of having an inline instance:
class Bjorn {
public:
int velocity;
int x, y;
Bjorn(InputComponent* input)
: input_(input)
{}
void update(World& world, Graphics& graphics) {
input_->update(*this);
physics_.update(*this, world);
graphics_.update(*this, graphics);
}
private:
InputComponent* input_;
PhysicsComponent physics_;
GraphicsComponent graphics_;
};
Now, when we instantiate Bjorn, we pass in an input component for it.
Bjorn* bjorn = new Bjorn(new PlayerInputComponent());
We pay the cost of a virtual function call for our update()
functions,
but instead, we can swap out the InputComponent
at will:
Let’s say we wanted an AI to control Bjorn, like on a Demo screen:
class DemoInputComponent: public InputComponent {
public:
virtual void update(Bjorn& bjorn) {
// Allow AI to control Bjorn
}
};
Now we wire him up with our new component:
Bjorn* bjorn = new Bjorn(new DemoInputComponent());
No Bjorn at all?
Actually, Bjorn is just a component bag. Let’s go further with the physics and graphics and make them interfaces too.
class PhysicsComponent {
public:
virtual ~PhysicsComponent() {}
virtual void update(GameObject& obj, World& world) = 0;
};
class GraphicsComponent {
public:
virtual ~GraphicsComponent() {}
virtual void update(GameObject& obj, Graphics& graphics) = 0;
};
Rename Bjorn as GameObject:
class GameObject {
public:
int velocity;
int x, y;
GameObject(InputComponent* input,
PhysicsComponent* physics,
GraphicsComponent* graphics)
: input_(input),
physics_(physics),
graphics_(graphics)
{}
void update(World& world, Graphics& graphics) {
input_->update(*this);
physics_->update(*this, world);
graphics_->update(*this, graphics);
}
private:
InputComponent* input_;
PhysicsComponent* physics_;
GraphicsComponent* graphics_;
};
class BjornPhysicsComponent : public PhysicsComponent {
public:
virtual void update(GameObject& obj, World& world) {
// Physics code...
}
};
class BjornGraphicsComponent : public GraphicsComponent {
public:
virtual void update(GameObject& obj, Graphics& graphics) {
// Graphics code...
}
};
And now we can build Bjorn again, without even having a dedicated class for him.
GameObject* createBjorn() {
return new GameObject(new PlayerInputComponent(),
new BjornPhysicsComponent(),
new BjornGraphicsComponent());
}
How does the object get its components?
- If the object creates its own components:
- The object itself ensures it has the components it needs.
- It’s harder to reconfigure the object.
- If outside code provides the components:
- The object becomes flexible
- It can be decoupled from the concrete component types.
How do components communicate?
- By modifying the container object’s state.
- By referring directly to each other.
- By sending messages There’s no one right answer
Prev: type-object Next: event-queue