[게임 프로그래밍 패턴] Chapter14 컴포넌트

Jangmanbo·2023년 11월 26일
0

컴포넌트 패턴
한 개체가 여러 분야를 서로 커플링 없이 다룰 수 있도록 각 분야의 코드를 별도의 컴포넌트 클래스에 둔다.
개체 클래스는 컨포넌트들의 컨테이너일 뿐이다.

주인공 클래스에 충돌 처리, 애니메이션, 렌더링 등등의 코드를 다 넣는다면 유지보수하기 매우 어렵다.
서로 분야가 다른 코드는 서로 모르는 것이 좋다.


디커플링

if(collidingWithFloor() && (getRenderState() != INVISIBLE)) {
    playSound(HIT_FLOOR);
}

이 코드를 수정하려면 물리, 그래픽, 사운드를 모두 알아야 한다.

이런 문제는 클래스를 분야에 따라 여러 컴포넌트로 나누면 된다.
예를 들어 입력은 InputComponent 클래스로 옮기고 클래스가 InputComponent 인스턴스를 갖게 한다.
컴포넌트 클래스들은 같은 클래스에 있더라도 서로에 대해 알지 못한다.

다시 합치기
이렇게 만든 컴포넌트들은 주인공 클래스 말고 다른 클래스에서 재사용할 수 있다.

클래스상호작용렌더링
DecorationXO
PropOO
ZoneOX

컴포넌트 패턴 없이 다음의 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 함수에 여러 분야의 코드가 있다.

분야별로 나누기

1. 입력

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;
};

2. 물리

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_;
};

3. 그래픽스

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_;
};

Character 클래스

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 클래스는

  1. 자신을 정의하는 컴포넌트 집합을 관리하고 컴포넌트들이 공유하는 상태를 들고 있다.
    • 위치, 속도는 여러 컴포넌트들이 공유하는 상태이므로 Character클래스에 정의
  2. 컴포넌트들이 서로 커플링되지 않고도 쉽게 통신할 수 있다.

오토-캐릭터

사용자 입력 없이 자동으로 움직이는 캐릭터를 만들어보자.

  1. 추상 상위 클래스 InputComponent 정의
// 추상 클래스
class InputComponent {
public:
    virtual ~InputComponent() {}
    virtual void update(Character& character = 0);
};

  1. 기존의 사용자 입력 처리 코드는 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;
};

  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

이제 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());
}


디자인 결정

생략..

0개의 댓글