ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 게임 프로그래밍 패턴 Part 2 디자인 패턴 다시 보기
    보관함 2020. 1. 12. 17:24

    게임 프로그래밍 패턴로버트 나이스트롬 (Robert Nystrom)
    상세보기

    이 시리즈는 [게임 프로그래밍 패턴]에 등장하는 팁을 정리하고 패턴을 직접 구현하거나 구현되어 있는 패턴을 확인하는 것으로 해당 패턴에 대해 이해하는 것을 목표로 한다.

     

    이번 포스팅에서는 저자가 '대자인 패턴 다시 보기'라는 파트를 다루는 이유와 이 파트에서 다루는 패턴에 대해 간단하게 정리한다.


    저자가 이번 파트를 다루는 이유)

    이 책의 2부에서는 [GoF의 디자인 패턴]에 나온 여러 디자인 패턴을 게임 개발에 어떻게 적용할 수 있을지를 다룬다.

    저자는 기본적으로 프로그래밍 패턴을 게임에 어떻게 적용할 수 있을지 이해를 돕기 위해 이 책을 작성했으며 그 일환으로 디자인 패턴 중 유용하거나 재미있는 것을 Part 2에서 다룬다.

     

    저자가 이 책에서 다루는 디자인 패턴과 각기 다루는 이유는 다음과 같다.

        1. 명령 패턴  =====> 과소평가 되어서

        2. 경량 패턴  =====> 게임 개발과의 연관성을 살펴보기 위해

        3. 관찰자 패턴 ====> 2와 동일한 이유

        4. 프로토타입 패턴  => 광범위한 프로그래밍 분야에서 패턴이 어떻게 얽혀 있는지 살펴보기 위해

        5. 싱글턴 패턴  ====> 남용되고 있어서

        6. 상태 패턴 ======> 4와 동일한 이유

    명령 패턴)

    명령 패턴은 메서드 호출을 실체화한 것이다. 이것은 함수 호출을 객체로 감쌌다는 의미이다. 이는 프로그래밍 언어 배경에 따라 '콜백', '일급 함수', '함수 포인터', '클로저', '부분 적용 함수'와 비슷하게 들릴 테고, 실제로 그렇다.

    가령 다음과 같은 코드가 있다고 하자.

    void ProcessInput()
    {
        if(input == space_bar) jump();
        else if(input == x) attack();
        else if(input == z) pickUpItem();
        else if(input == c) changeItem();
    }

    이 코드에 명령 패턴을 적용하면 다음과 같이 작성할 수 있다.

    class Commnad
    {
    public:
        virtual void execute() = 0;
    }
    
    class JumpCommnad : public Command
    {
    public:
        virtual void execute() override { jump(); };
    }
    
    Command* m_space_bar;
    Command* m_x;
    Command* m_z;
    Command* m_c;
    
    void ProcessInput()
    {
        if(input == space_bar) m_space_bar->execute();
        else if(input == x) m_x->execute();
        else if(input == z) m_z->execute();
        else if(input == c) m_c->execute();
    }

    이렇게 작성했을 때 해당 키가 어떤 동작을 해야 하는지 수정할 수 있도록 만들 수 있다.

     

    만약 C#과 같이 함수 자체를 등록하여 사용할 수 있는 언어의 경우 해당 기능을 사용하여 구현하면 더 깔끔하게 코드를 작성할 수 있다.

    Action m_space_bar;
    Action m_x;
    Action m_z;
    Action m_c;
    
    void ProcessInput()
    {
        if(input == space_bar) m_space_bar.invoke();
        else if(input == x) m_x.invoke();
        else if(input == z) m_z.invoke();
        else if(input == c) m_c.invoke();
    }

     

    경량 패턴)

    경량 패턴을 사용하면 객체를 마구 늘리지 않으면서도 객체지향 방식의 장점을 취할 수 있다. 열거형을 선택해 수많은 다중 선택문을 만들 생각이라면, 경량 패턴을 먼저 고려해보자.

    최근 그래픽카드와 API에서는 '인스턴싱'을 지원한다. 여기에서 인스턴싱을 쉽게 말하자면 랜더링 할 때 필요한 데이터를 실제 랜더링 자체에 필요한 메쉬, 텍스처 등과 위치, 로테이션 정보 등 각 객체의 특성을 나타는 값으로 분리하여 메쉬, 텍스처 정보는 미리 등록하여 사용하고 객체의 특성 값만 바꾸어 그리는 것을 말한다.

     

    그리고 경량 패턴도 이러한 아이디어에서 출발한다. 공유할 수 있는 자료를 최대한 공유하는 것이다.

    가령 타일 맵으로 지형을 그린다고 가정하자.

    class Tile
    {
    public:
        Tile(int moveCost, bool isWater, Texture tex)
            : m_moveCost(moveCost), m_isWater(isWater), m_tex(tex)
        {
        }
        
    private:
        int m_moveCost;
        bool m_isWater;
        Texture m_tex;
    }
    
    Tile m_forest(1, false, FOREST_TEXTURE);
    Tile m_river(2, true, RIVER_TEXTURE);
    Tile m_hill(3, false, HILL_TEXTURE);
    
    Tile* m_map[Y_MAX][X_MAX];
    
    void MakeMap()
    {
        bool riverIndex = random(0, Y_MAX);
        for(int i=0; i<Y_MAX; ++i)
        {
        	for(int j=0; j<X_MAX; ++j)
            {
                m_map[i][j] = &m_forest;
                if(j % 10 == 0)
                	m_map[i][j] = &m_hill;
                if(i == riverIndex)
                	m_map[i][j] = &m_river;
            }
        }
    }

    이런 식으로 필요한 데이터를 매번 만들어 사용하는 것이 아니라 공유되는 데이터를 미리 만들어두고 주소만 공유하는 방식으로 메모리를 절야할 수 있을 것이다.

     

    관찰자 패턴)

    관찰자 패턴을 적용하면 어떤 코드에서 흥미로운 일이 생겼을 때 누가 받든 상관없이 알림을 보낼 수 있다.

    관찰자 패턴은 워낙 흔해서 자바에서는 아예 핵심 라이브러리에 들어가 있고, C#에서는 event 키워드로 지원한다.

     

    관찰자 패턴은 특정 시스템이 필요한 정보를 받은 경우에만 동작을 할 수 있도록 해준다.

     

    만약 게임에 다리에서 뛰어내리는 업적이 있는 경우 물리 엔진에서는 업적 시스템을 위한 콜백을 만들고 업적 시스템이 이 것을 등록한 경우 물리 엔진에서 떨어지는 경우에 업적 시스템에 콜백을 보낸다면 이때에만 업적 시스템이 업적 달성을 판단하게 될 것이다.

     

    C#의 이벤트를 간단하게 보면 다음 코드와 같다.

    public class Publisher
    {
        public delegate void SimpleEventHandler();
    
        public event SimpleEventHandler SimpleEvent;
    
        protected virtual void RaiseSimpleEvent()
        {
            if(SimpleEvent != null)
            	SimpleEvent.Invoke();
        }
    }

     

    프로토타입 패턴)

    프로토타입 패턴의 좋은 점은 프로토타입의 클래스뿐만 아니라 상태도 같이 복제한다는 점이다.

    가령 몬스터를 생성하는 경우 동일한 능력치를 가진 몬스터라도 생성된 이후에는 각자 관리되어야 할 것이다. 이럴 때 프로토타입으로 사용할 몬스터를 놔두고 그것을 복제하도록 하면 쉽게 동일한 몬스터를 생성할 수 있다.

     

    다음과 같이 몬스터를 소환하는 클래스가 있다고 하자.

    class Spawner
    {
    public:
        virtual Monster* spawnMonster() = 0;
    }
    
    class GhostSpawner : public Spawner
    {
    public:
        virtual Monster* spawnMonster()
        {
        	return new Ghost();
        }
    }
    
    class DemonSpawner : public Spawner
    {
    public:
    	virtual Monster* spawnMonster()
        {
        	return new Demon();
        }
    }

    이런 경우라면 몬스터를 각각의 스포너에서 생성한 뒤 각각의 몬스터를 다시 설정해 줘야 할 것이다.

     

    그런데 몬스터의 상위 클래스에 Clone을 추가하는 경우 다음과 같이 몬스터를 생성할 때 기존의 값을 그대로 복사하여 생성할 수 있을 것이다.

    class Monster
    {
    public:
    	virtual Monster* Clone() = 0;
    }
    
    class Ghost : public Monster
    {
    private:
    	Ghost(int health, int speed)
        	: m_health(health), m_speed(speed)
        {
    	}
    
    public:
    	virtual Monster* Clone()
        {
    		return new Ghost(m_health, m_speed);
    	}
        
    private:
    	int m_health;
        int m_speed;
    }

     

    싱글턴 패턴)

    GoF의 싱글턴 패턴은 의도와는 달리 득보다는 실이 많다. GoF도 싱글턴 패턴을 남용하지 말라고 강조했지만, 게임 개발자들 중에서 귀담아듣는 이는 많지 않았다.

    저자는 싱글턴이 남용되고 있으며 이를 피할 방법을 제시한다.


    싱글턴을 사용하는 이유는?

    • 오직 한 개의 클래스 인스턴스만 갖도록 보장
    • 전역 접근점을 제공

    싱글턴이 문제가 되는 이유는?

    • 싱글턴은 전역 변수이다.
    • 전역 변수는 코드를 이해하기 어렵게 한다.
    • 전역 변수는 커플링을 조장한다.
    • 전역 변수는 멀티스레딩 같은 동시성 프로그래밍에 알맞지 않다.

    저자는 한 개의 클래스 인스턴스만 갖도록 보장하는 것과 인스턴스에 쉽게 접근하는 것을 분리해야 한다고 주장한다.

     

    먼저, 한 개의 클래스 인스턴스만 갖도록 보장하는 것은 assert를 사용하여 한 개 이상의 인스턴스를 생성할 때 assert에 걸리도록 만드는 것이다.

    class FileSystem
    {
    public:
    	FileSystem()
        {
        	assert(!m_instantiated);
            m_instantiated = true
        }
    
    private:
    	bool m_instantiated{ false };
    }

    다만 이 방법에는 런타임에 인스턴스의 개수를 파악한다는 단점이 있다.

     

    다음으로 인스턴스에 쉽게 접근하는 방법은 다음과 같다.

    1. 넘겨주기  =============> 객체를 필요로 하는 함수에 인수로 넘겨주는 방법이다.
    2. 상위 클래스로부터 얻기 ====> 상위 클래스에 객체를 저장하고 상속받은 클래스만 접근하도록 하는 것이다.
    3. 이미 전역인 객체로부터 얻기 => 전역 상태를 모두 제거하는 것은 불가능에 가깝다. 그러므로 전역 상태에 존재하는 클래스에 새롭게 들어갈 클래스를 만들어 사용하는 방법을 사용하면 전역 객체를 늘리지 않으면서 쉽게 접근이 가능하다.

    상태 패턴)

    GoF의 상태 패턴이 FSM을 구현하는 방법 중 하나이다.

    FSM은 유한 상태 기계를 뜻하며 요점은 다음과 같다.

    • 가질 수 있는 '상태'가 한정된다.
    • 한 번에 '한 가지' 상태만 될 수 있다.
    • '입력'이나 '이벤트'가 기계에 전달된다.
    • 각 상태에는 입력에 따라 다음 상태로 바뀌는 '전이'가 있다.

    가령 플랫포머 게임을 만들 때 하나의 업데이트 문에서 모든 처리를 하는 대신 한 상태에서 하나의 처리만을 담당하게 하고 입력에 따라 상태만 바꾸도록 하는 것이다.

     

    입력에 따라 다른 상태로 전이된다.

    댓글

Designed by Tistory.