Game Development, 게임개발/디자인패턴

Flyweight Pattern, 경량 패턴 [디자인패턴]

게임이 더 좋아 2021. 9. 28. 16:21
반응형
728x170

 

우리는 게임을 만들 때

반복작업을 하는 경우가 많다.

웬만해서는 거의 반복작업 위주다.

그렇다면 반복작업하는 시간을 줄이거나 반복 작업을 효율적으로 할 수 있다면

전체 프로젝트의 효율도 꽤나 올라갈 것이라고 생각된다.

반복된 데이터에 대해서 하드웨어의 부담을 줄여주는 패턴이 있다.

경량 패턴이다.

알아보자

 


 

우선 게임의 배경을 생각해보자

나는 항상 중세 판타지 게임이 먼저 생각난다.

기사, 괴물, 마녀, 용 등이 있다.

내가 좋아하는 게임 중 하나인 위쳐 또한 그렇다.

 

 

저기 배경이 보이는가

무엇보다 수많은 나무가 배경을 채우고 있다.

멀리서 보기엔 다 비슷비슷해보인다. 

프레임마다 저 모든 나무를 구현하는 것이야 어렵진 않겠지만.. 너무 비효율적이라고 생각이 든다.

과연 모두 일일이 렌더링을 해야하는가?

 

물론 렌더링을 해야 한다.

뭐 저것이 정말 먼 배경이거나 캐릭터가 상호작용을 할 수 없는 정도의 거리라면

배경을 객체가 아니라 하나의 이미지로 만드는 것도 생각하겠지만

여기선 그렇게 하지 않고 모두 렌더링을 하기로 했다.

 

그렇다면 반복적인 나무 렌더링을 어떻게하는 것이 효율적이겠는가?

를 생각해보자

 

나무는 다 비슷비슷하다.

결국 나무 mesh, pos ... 등의 컴포넌트가 합쳐져서 나무가 되었을 것이다.

 

여기서 요점은 나무가 비슷하다거나 같다는 것이다.

나무의 위치는 다를 지 몰라도 모든 나무를 일일이 그래픽 디자이너에게 시키지 않으므로 저 나무들의 대부분은

같은 모양을 가지고 있다고 할 수 있다.

 

그렇다면 여기서 나무의 외적인 부분의 경우 모든 인스턴스가 같은 값을 가지고 있다고 생각해도 무방하다

 

그래서 이렇게 모든 나무 인스턴스가 다 가지고 있는 데이터를 구분해보았다.

class TreeModel{
private:
    Mesh mesh_;
    Texture bark_;
    Texture leaves_;
};

 

다시 말하면 게임 내에서 Mesh, Texture를 계속해서 인스턴스마다 올릴 필요가 없다고 생각된다.

나무 인스턴스들이 개별 데이터를 가진 채

TreeModel만 참조해서 Mesh와 Texture를 받아서 이용하면 된다고 생각한다.

class Tree{
private:
    TreeModel* model_;
    
    Vector position_;
    double height_;
    double thickness_;
    Color barkTint_;
    Color leafTint_;
};

 

줄여서 말하자면

여러 인스턴스들이 공유하는 데이터를 다른 객체에 위임해서 메모리를 공유함으로써 성능을 높이기 위함이다.

 

아래의 그림과 같다.

게임프로그래밍패턴 60p

 

메모리의 사용량을 줄이기 위해서는

공유 데이터인 TreeModel을 딱 한 번만 보내서 모두 구현하게 해야한다.

 

즉, 개별 데이터를 다 전달하고 마지막으로 공유 데이터를 사용하여 구현한다.

이러한 그래픽 작업 기법을 인스턴스 렌더링, Instanced Rendering이라고 한다

**DirectX, OpenGL 에서 지원하는 기능 중 하나임.

 

 


 

그렇게 나무를 메모리를 절약하면서 구현해봤고

다시 요약하자면

경량 패턴은 어떤 객체의 수가 많은데 해당 객체가 다 비슷하거나 공유할 수 있는 데이터가 많아서

가볍게 만들 수 있을 때 쓰는 패턴이다.

 

때문에

경량패턴을 구현하기 위해서는

 

객체 데이터를 2종류로 나눈다.

1. 모든 객체의 데이터 값이 같아서 공유할 수 있는 데이터, 고유 상태 (Intrinsic state || context-free)

2. 인스턴스 마다 값이 다른 개별 데이터, 외부 상태 (extrinsic state)

 

물론 앞서 말했듯이 기초적인 자원 공유 기법같다.

더군다나 공유 객체가 명확하지 않으면 경량 패턴을 쓰고 있는지도 모른다.하나의 객체가 여러 곳에 동시에 있는 것처럼 보인다.그 예를 알아보자.

 


 

만약 지형에 관해서 구현한다면 어떨까?

모든 땅 타일마다 그래픽 디자이너가 손수 만들까?

절대 아니다. 돈이 많으면 가능할지도?

 

 


 

일반적으로 지형을 표시를 어떻게할 지를 생각해보자

 

땅은 우선 타일 기반으로 만든다. 

그렇다면 타일에도 공유데이터와 개별데이터가 존재할 것이다.

enum Terrain{
    TERRAIN_GRASS,
    TERRAIN_HILL,
    TERRAIN_RIVER,
    ....
}

Terrain은 종류가 무척 많을 것이고 enum(열거형)으로 관리한다.

 

땅을 구현 할 World에서는

Terrain을 받아서 어디 위치에 해당 타일을 놓을지 결정한다.

 

class World{
private:
	Terrain tiles_[WIDTH][HEIGHT];
};

 

또한 타일 데이터는 이와 같이 얻는다.

간략하게 알아보자.

 

int World::getMovementCost(int x, int y){
    switch(tiles_[x][y]){
        case TERRAIN_GRASS: return 1;
        case TERRAIN_HILL: return 3;
        case TERRAIN_RIVER: return 2;
        ...
    }
}

 

위의 함수로 열거형에서 무엇을 가져올지를 return 하고

++예를 들면 GRASS가 열거형에서 첫번째 원소

 

아래 함수에서 해당 위치에 놓을 지 말지를 결정하겠다.

bool World::isWater(int x, int y){
    switch (tiles_[x][y]){
        case TERRAIN_GRASS: return false;
        case TERRAIN_HILL: return false;
        case TERRAIN_RIVER: return true;
        ...
        
    }
}

 

우선 위와 같이 짰을 때

동작에는 무리가 없으나

이동에 대한 cost나 지형이 무엇인지에 대한 여부는 지형에 관한 데이터인데

이 코드에서 하드 코딩이 되어있다.

또한 같은 지형 종류에 대한 데이터가 여러 메서드에 나누어져 있다.

 


 

우선 아래와 같이 하나의 클래스로 만들 수 있다.

 

class Terrain{
public:
    Terrain(int movementCost, bool isWater, Texture texture) : movementCost_(movementCost), isWater_(isWater), texture_(texture)
    {
    }
    
    int getMovementCost() const { return movementCost_;}
    bool isWater() const{return isWater_;}
    const Texture& getTexture() const { return texture_;}
    
    //모든 메서드를 Const로 만들었다. 
    //같은 Terrain 객체를 여러 곳에서 공유하게 만들 것이기 때문에
    // 한 곳에서 값을 바꾸게 된다면 모든 곳에 영향을 미칠 것이다.
    // 때문에 Const로 값을 고정시켜서 immutable(변경 불가능)하게 만들었다.
    
private:
    int movementCost_;
    bool isWater_;
    Texture texture_;
};

 

하지만 하나로 만들어서 타일마다 인스턴스를 가지게 되는 비용이 커질 수 있으므로 다른 방법을 찾고 싶다.

 

다시 말하자면

위의 Terrain 클래스에는 타일 위치와 관련된 개별 타일에 관한 내용이 없으므로

위 클래스는 고유 상태, Intrinsic State라고 볼 수 있다.

따라서 지형 종류별로 Terrain 객체를 가지기만 하면 된다.

지형 종류 하나에 여러 객체가 필요하지 않다.

-> 각각의 타일에서 Terrain을 참조해서 공유 데이터를 가져온다는 뜻이다.

지형에 들어가는 모든 타일은 동일하다.

다시 말해서 World 클래스의 격자 멤버 변수에 열거형(enum)이나 Terrain 대신

Terrain 객체의 Pointer를 넣는다.

class World{
private:
    Terrain* tiles_[WIDTH][HEIGHT];
    ....
};

 

그림으로 따지자면

지형 종류가 같다면 같은 Terrain 인스턴스를 가리키게 하면 된다.

종류별로 Terrain을 하나씩 갖는다.

** Terrain 객체가 꼭 하나는 아니지만 땅의 종류당 하나씩인 것을 인지하자.

 


 

 

또한 Terrain 인스턴스가 여러 곳에서 사용되니까

life cycle, 생존 주기를 관리하기 어려울 수 있다.

그래서 World 클래스에서 관리를 해준다.

-> 월드 클래스에서 해당 Terrain 객체를 생성하게끔 만든다. (생성자로)

 

class World{
public:
    World()
    : grassTerrain_(1, false, GRASS_TEXTURE),
      hillTerrain_(3, false, HILL_TEXTURE),
      riverTerrain_(2, true, RIVER_TEXTURE){
    }
private:
    Terrain grassTerrain_;
    Terrain hillTerrain_;
    Terrain riverTerrain_;
    ....
};

 

World 클래스에서 만들어 놓고 생존주기를 조절하는 것이다.

 


 

실제 사용은 어떻게 할까??

 

void World::generateTerrain() {
    //땅에 풀 타일 채우기
    for(int x = 0; x < WIDTH; x++){
        for(int y = 0; y < HEIGHT; y++){
            //언덕 타일도 채우기
            if(random(10) == 0){
                tiles_[x][y] = &hillTerrain_;
            }
            else{
                tiles_[x][y] = &grassTerrain_;
            }
        }
    }
    
    //강 타일 채우기
    int x = random(WIDTH);
    for(int y = 0; y < HEIGHT; y++){
        tiles_[x][y] = &riverTerrain_;
    }
}

 

World에서 관리함으로써 지형의 속성 값을 World의 메서드가 아닌 Terrain 객체에서 바로 받을 수 있다.

const Terrain& World::getTile(int x, int y) const{
    return *tiles_[x][y];
}

 

World 클래스는 더 이상 지형의 세부 정보와 커플링 되지 않는다.

타일 속성은 바로 Terrain 객체에게서 얻을 수 있다.

 

예를 들어서

int cost = world.getTile(2,3).getMovementCost();

객체로 바로 작동한다.

 


다시 요약하자면

반복적인 작업을 많이하는데

해당 반복적인 작업이 공유하는 데이터가 있다?

그렇다면 경량 패턴을 써서 성능을 높일 수 있을거라고 기대할 수 있다.

반응형
그리드형