한 개체가 여러 분야를 서로 커플링 없이 다룰 수 있게 한다.
유니티 게임 오브젝트에 여러 컴포넌트를 붙였던 경험을 떠올려 쉽게 이해할 수 있었다.
멀티스레드 환경에서는 게임 코드를 분야별로 스레드의 방법을 사용한다. AI, 사운드, 렌더링을 각자 스레드에서 실행시킨다. 분야별로 디커플링 해야 교착상태 같은 동시성 버그를 피할 수 있다.
서로 통신이 필요한 컴포넌트가 있다면 필요한 컴포넌트 끼리의 결합을 사용할 수 있다.
14.3 패턴
여러 분야를 다루는 하나의 개체가 있다. 분야별로 격리하기 위해, 각각의 코드를 별도의 컴포넌트 클래스에 둔다. 이제 개체 클래스는 단순히 이들 컴포넌트들의 컨테이너 역할만 한다.
14.4 언제 쓸 것인가?
게임 개체를 정의하는 핵심 클래스(GameObject)에서 가장 많이 사용되지만, 다음 조건 중 하나라도 만족한다면 다른 분야에서도 유용하게 쓸 수 있다.
1. 한 클래스에서 여러 분야를 건드리고 있어서 이들을 서로 디커플링하고 싶다.
2. 클래스가 거대해져서 작업하기가 어렵다.
3. 여러 다른 기능을 공유하는 다양한 객체를 정의하고 싶다. 단, 상속으로는 딱 원하는 부분만 골라서 재사용할 수가 없다.
14.5 주의사항
컴포넌트 패턴을 적용하면 클래스 하나에 코드를 모아놨을 때보다 더 복잡해질 가능성이 높다.
컴포넌트끼리 통신하기 어렵다.
각 컴포넌트의 객체를 생성하고 초기화를 잘 해줘야 한다.
대상 개체의 데이터에 접근하거나 행동을 수행하기 위해서 개체의 컴포넌트 객체를 거쳐야 하기 때문에 성능이 민감한 내부 루프 코드에서 이런 식으로 포인터를 따라가다 보면 성능이 떨어질 수 있다.
14.6 예제 코드
class GameObject {
public:
int velocity;
int x, y; // 컴포넌트간 통신을 위한 멤버 변수
GameObject(InputComponent* input,
PhysicsComponent* physics,
GraphicsComponent* grpahics):
input_(input),
pyhics_(physics),
graphics_(graphics) {
}
void update(Wolrd& world, Graphics& graphics) {
input_->update(*this);
physics_->update(*this, world);
graphics_->update(*this, graphics);
}
private:
InputComponent* input_;
PhysicsComponent* physics_;
GraphicsComponent* graphics_;
};
class PhysicsComponent {
public:
virtual ~PhysicsComponent() {}
virtual void update(GameObject& obj, World& world) = 0;
};
class BjronPhysicsComponent : public PhysicsComponent {
public:
virtual void update(GameObject& obj, World& world) {
}
};
14.7 디자인 결정
객체는 컴포넌트를 어떻게 얻는가?
1. 객체가 필요한 컴포넌트를 알아서 생성할 때
ㄴ 객체는 항상 필요한 컴포넌트를 가지게 된다.
ㄴ 객체를 변경하기가 어렵다. (컴포넌트 패턴의 강점 중 하나는 컴포넌트 재조합만으로 새로운 종류의 객체를 만들 수 있다는 점인데 어떤 컴포넌트를 사용할지를 하드코딩해놓으면, 이런 유연성을 잃게 된다.)
2. 외부 코드에서 컴포넌트를 제공할 때
ㄴ 객체가 훨씬 유연해진다. 컴포넌트 구현 클래스만 변경해도 다르게 동작하는 컴포넌트를 객체에 제공할 수 있다.
ㄴ 객체를 구체 컴포넌트 자료형 (예제의 BjronPhysicsComponent)으로부터 디커플링 할 수 있다.
컴포넌트들끼리는 어떻게 통신할 것인가? (중복 가능)
1. 컨테이너 객체의 상태를 변경(공유)하는 방식 (멤버 변수를 공유 정보로써 사용할 수 있다.)
ㄴ 컴포넌트들이 공유하는 정보를 컨테이너 객체에 전부 넣어야 한다.
ㄴ 컴포넌트끼리 암시적으로 통신하다 보니 컴포넌트 실행 순서에 의존하게 된다.
2. 컴포넌트가 서로 참조하는 방식
ㄴ 컴포넌트 구체 클래스의 생성자로 참조할 컴포넌트 객체를 인수로 전달한다.
ㄴ 참조하는 컴포넌트들 끼리 강하게 결합된다.
3. 메세지를 전달하는 방식
ㄴ 컴포넌트가 컨테이너에 메세지를 보내면, 컨테이너는 자기에게 있는 모든 컴포넌트에 이를 전파한다.
ㄴ 메시징은 호출하고 나서 신경 안 써도 되는 사소한 통신에 쓰기 좋다. 예를 들어 물리 컴포넌트에서 객체가 무엇인가와 충돌했다고 전파하면 오디오 컴포넌트가 이를 받아서 소리를 내는 식이다.
ㄴ 상태 공유 방식에서처럼 상위 컨테이너 객체를 통해서 통신하기 때문에, 컴포넌트들은 메세지 값과 커플링되지만 하위 컴포넌트들끼리는 디커플링 상태를 유지한다.
class Component {
public:
virtual ~Component() {}
virtual void receive(int message) = 0;
};
class ContainerObject {
public:
void send(int message) {
for (int i=0; i < MAX_COMPONENTS; i++){
if (components_[i] != NULL){
components_[i]->receive(message);
}
}
}
private:
static const int MAX_COMPONENTS = 10;
Component* components_[MAX_COMPONENTS];
};
'읽은 책 > 게임 프로그래밍 패턴' 카테고리의 다른 글
16. 디커플링 패턴 - 서비스 중개자 (0) | 2022.07.19 |
---|---|
15. 디커플링 패턴 - 이벤트 큐 (0) | 2022.07.18 |
13. 행동 패턴 - 타입 객체 (0) | 2022.07.17 |
12. 행동 패턴 - 하위 클래스 샌드박스 (0) | 2022.07.16 |
11. 행동 패턴 - 바이트코드 (0) | 2022.07.16 |