컴포넌트 패턴
한 개체가 여러 분야를 서로 커플링 없이 다룰 수 있도록 각 분야의 코드를 별도의 컴포넌트 클래스에 둔다.
개체 클래스는 컨포넌트들의 컨테이너일 뿐이다.
주인공 클래스에 충돌 처리, 애니메이션, 렌더링 등등의 코드를 다 넣는다면 유지보수하기 매우 어렵다.
서로 분야가 다른 코드는 서로 모르는 것이 좋다.
디커플링
if(collidingWithFloor() && (getRenderState() != INVISIBLE)) {
playSound(HIT_FLOOR);
}
이 코드를 수정하려면 물리, 그래픽, 사운드를 모두 알아야 한다.
이런 문제는 클래스를 분야에 따라 여러 컴포넌트로 나누면 된다.
예를 들어 입력은 InputComponent
클래스로 옮기고 클래스가 InputComponent
인스턴스를 갖게 한다.
컴포넌트 클래스들은 같은 클래스에 있더라도 서로에 대해 알지 못한다.
다시 합치기
이렇게 만든 컴포넌트들은 주인공 클래스 말고 다른 클래스에서 재사용할 수 있다.
클래스 | 상호작용 | 렌더링 |
---|---|---|
Decoration | X | O |
Prop | O | O |
Zone | O | X |
컴포넌트 패턴 없이 다음의 3개의 클래스를 정의하려면, 다중 상속 없이는 충돌 처리 코드나 렌더링 코드가 중복될 수밖에 없다.
그러나 컴포넌트 패턴을 사용한다면 상속은 전혀 필요없다.
PhysicsComponent
, GraphicsComponent
두 클래스만 있으면 된다.
컴포넌트 패턴을 적용하지 않았을 때보다 더 복잡해질 가능성이 높다.
컴포넌트 패턴은 한 무르이 객체를 생성 및 고히화하고 알맞게 묶어줘야 한다.
컴포넌트끼리 통신도 어렵고, 컴포넌트들을 메모리 어디에 둘지 제어하는 것도 어렵다.
또 무엇을 하든지 한 단계 거쳐야 할 때가 많다.
그래도 코드베이스 규모가 크면 이러한 복잡성으로 인한 손해보다 디커플링, 코드 재사용으로 얻는 이득이 더 높을 것이다.
컴포넌트 패턴을 적용하지 않은 클래스
class Character {
public:
Character: velocity_(0), x_(0), y_(0) {}
void update(World& world, Graphics& graphics);
private:
static const int WALK_ACCELERATION = 1;
int velocity_;
int x_, y_;
Volume volume_;
Sprite spriteStand_;
Sprite sprriteWalkLeft_;
Sprite spriteWalkRight_;
};
void Character::update(World& world, Graphics& graphics) {
// 1. 입력에 따라 주인공 속도 조절 (입력)
switch(Controller::getJoystickDirection()) {
case DIR_LEFT:
velocity_ -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
velocity_ += WALK_ACCELERATION;
break;
}
// 2. 속도에 따라 위치 변경 (물리)
x_ += velocity_;
world.resolveCollision(volume_, x_, y_, velocity_);
// 3. 방향에 따라 스프라이트 그리기 (그래픽스)
Sprite* sprite = &spriteStand_;
if(velocity_ < 0) { sprite = &spriteWalkLeft_; }
else if(velocity_ > 0) { sprite = &spriteWalkRight_; }
graphics.draw(*sprite, x_, y_);
}
아직 기초적인 코드만 작성했는데도 update
함수에 여러 분야의 코드가 있다.
class InputComponent {
public:
void update(Character& character) {
switch(Controller::getJoystickDirection()) {
case DIR_LEFT:
character.velocity -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
character.velocity += WALK_ACCELERATION;
break;
}
}
private:
static const int WALK_ACCELERATION = 1;
};
class PhysicsComponent {
public:
void update(Character& character, World& world) {
character.x += character.velocity;
world.resolveCollision(volume_, character.x, character.y, character.velocity);
}
private:
Volume volume_;
};
class GraphicsComponent {
public:
void update(Character& character, Graphics& graphics) {
Sprite* sprite = &spriteStand_;
if(character.velocity < 0) { sprite = &spriteWalkLeft_; }
else if(character.velocity > 0) { sprite = &spriteWalkRight_; }
graphics.draw(*sprite, character.x, character.y);
}
private:
Sprite spriteStand_;
Sprite spriteWalkLeft_;
Sprite spriteWalkRight_;
};
class Character {
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_;
GraphcisComponent graphics_;
};
이렇게 컴포넌트 패턴을 적용한 Character
클래스는
Character
클래스에 정의사용자 입력 없이 자동으로 움직이는 캐릭터를 만들어보자.
InputComponent
정의// 추상 클래스
class InputComponent {
public:
virtual ~InputComponent() {}
virtual void update(Character& character = 0);
};
InputComponent
인터페이스를 상속받아 구현class PlayerInputComponent : public InputComponent {
public:
// 가상 함수이므로 속도는 조금 느려짐..
virtual void update(Character& character) {
switch(Controller::getJoystickDirection()) {
case DIR_LEFT:
character.velocity -= WALK_ACCELERATION;
break;
case DIR_RIGHT:
character.velocity += WALK_ACCELERATION;
break;
}
}
private:
static const int WALK_ACCELERATION = 1;
};
Character
클래스는 PlayerInputComponent
인스턴스가 아닌 InputComponent
인터페이스 포인터를 들게 한다.class Character {
public:
int velocity;
int x, y;
Character(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_;
};
->이제 Character
객체를 생성할 때, 입력 컴포넌트를 전달할 수 있다.
Character* character = new Character(new PlayerInputComponent());
DemoInputComponent
를 정의하여 Character
생성자에 전달하면 AI 캐릭터 객체를 생성할 수 있다.
class DemoInputComponent : public InputComponent {
public:
virtual void update(Character& character) {
// AI가 알아서 Character을 조정
}
};
이렇게 다양한 입력 컴포넌트를 전달할 수 있게 하여 다양한 모드의 캐릭터를 생성할 수 있게 되었다.
(참고로 물리 컴포넌트, 그래픽스 컴포넌트는 입력 컴포넌트가 PlayerInputComponent
인지, DemoInputComponent
인지 전혀 모른다.)
이제 Character
클래스는 컴포넌트 묶음일 뿐이다.
GameObject
클래스로 바꾸어 게임에 필요한 온갖 객체를 컴포넌트의 조합으로 만들 수 있도록 하자.
// 물리 컴포넌트 인터페이스
class PhysicsComponent
{
public:
virtual ~PhysicsComponent() {}
virtual void update(GameObject& obj, World& world) = 0;
};
class PlayerPhysicsComponent : public PhysicsComponent
{
public:
virtual void update(GameObject& obj, World& world)
{
// 물리 코드...
}
};
// 그래픽스 컴포넌트 인터페이스
class GraphicsComponent
{
public:
virtual ~GraphicsComponent() {}
virtual void update(GameObject& obj, Graphics& graphics) = 0;
};
class PlayerGraphicsComponent : public GraphicsComponent
{
public:
virtual void update(GameObject& obj, Graphics& graphics)
{
// 그래픽스 코드...
}
};
입력 컴포넌트와 마찬가지로 인터페이스와 구현부로 나눈다.
// Character > 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_;
};
이제 별도의 캐릭터 클래스 없이 GameObject
클래스 만으로 캐릭터 객체를 만들 수 있다.
GameObject* createPlayer()
{
return new GameObject(
new PlayerInputComponent(),
new PlayerPhysicsComponent(),
new PlayerGraphicsComponent());
}
생략..