2024. 6. 7. 11:16ㆍGame Develop/Design Pattern
관찰자 패턴은 GoF의 세가지 패턴 중 생성 패턴에 속한다.
생성 패턴은 클래스나 객체의 생성과 참조 과정을 정의하는 패턴이다.
프로토타입 패턴의 특징은 다음과 같다.
- 원본 객체를 복제하는 방법으로 객체를 생성
- 일반적인 방법으로 객체를 생성하며, 비용이 큰 경우 주로 이용함
RPG 게임을 만든다고 해보자. 몬스터들은 영웅을 해치기 위해 떼지어 다닌다. 이 몬스터들은 스포너를 통해 게임 레벨에 등장하는데, 몬스터 종류마다 스포터가 따로 있다
게임에 나올 몬스터마다 클래스를 만들어 보자. Ghost, Demon, Sorcerer 3 종류의 몬스터가 있다.
class Monster{
// 데이터...
};
class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public Monster {};
한 가지 스포너는 한 가지 몬스터 인스턴스만 만든다. 모든 몬스터를 소환하기 위해 각 클래스 마다 스포너 클래스도 따로 만든다고 치자. 이렇게 하면 스포너 클래스 상속 구조가 몬스터 클래스 상속 구조를 따라가게 된다.
이를 구현한 코드는 다음과 같다
class Spawner{
public:
virtual ~Spawner() {}
virtual Monster* spawnMOnster() = 0;
};
class GhostSpawner : publicSpawner{}
public:
virtual Monster* spawnMOnster() {
return new Ghost();
}
};
class DemonSpawner : publicSpawner{}
public:
virtual Monster* spawnMOnster() {
return new Demon();
}
};
class SorcererSpawner : publicSpawner{}
public:
virtual Monster* spawnMOnster() {
return new Sorcerer();
}
};
위와 같이 객체마다 클래스를 하나하나 만들어 주면 전혀 효율이 나오지 않는다.
위의 문제를 프로토타입 패턴으로 해결할 수 있다. 핵심은 어떤 객체가 자기와 비슷한 객체를 소환할 수 있다는 점이다.
유령 객체 하나로 다른 유령 객체를 악마 객체로부터도 다른 악마 객체를 만들 수 있듯이, 어떤 몬스터 객체든지 자신과 비슷한 몬스터 객체를 만드는 원형(Prototypal) 객체로 사용할 수 있다.
이를 구현하기 위해 상위 클래스인 Monster에 추상 메서드 clone을 추가한다.
class Monster{
public:
virtual ~Monster() {}
virtual Monster* clone() = 0;
// 기타 등등
};
Monster의 하위 클래스에서는 자신과 자료형과 상태가 같은 새로운 객체를 반환하도록 clone을 구현한다.
class Ghost : public Monster {
public:
Ghost(int health, int speed)
: health_(health), speed_(speed) {
}
virtual Monster* clone() {
reutrn new Ghost(health_, speed_);
}
private:
int health_;
int speed_;
};
class Demon : public Monster {
public:
Demon(int health, int speed)
: health_(health), speed_(speed) {
}
virtual Monster* clone() {
reutrn new Demon(health_, speed_);
}
private:
int health_;
int speed_;
};
class Sorcerer : public Monster {
public:
Sorcerer(int health, int speed)
: health_(health), speed_(speed) {
}
virtual Monster* clone() {
reutrn new Sorcerer(health_, speed_);
}
private:
int health_;
int speed_;
};
Monster를 상속받는 모든 클래스에 clone 메서드가 있다면, 스포터 클래스를 종류별로 만들 필요 없이 하나만 만들면 된다.
class Spawner{
public:
Spawner(Monster* prototype) : prototype_(prototype) {}
Monster* spawnMonster() {
return prototype_->clone();
}
private:
Monster* prototype_;
};
Spawner 클래스 내부에 몬스터 객체가 있어서 자기와 같은 Moster 객체를 동장 찍듯 만들어내는 역할만 한다.
만약 유령 스포너를 만들고 싶다면 원형으로 사용할 유령 인스턴스를 만든 후에 스포너에 전달한다.
Monster* ghostPrototype = new Ghost()15, 3;
Spawner* ghostSpawner = new Spawner(ghostPrototype);
프로토타입 패턴의 좋은 점은 프로토타입의 클래스뿐만 아니라 상태도 같이 복제한다는 점이다. 이 말인 즉슨 원형으로 사용할 유령 객체만 잘 만들어 놓으면 이걸 조금만 변경하면 빠른 유령, 강한 유령용 스포너 같은 것도 쉽게 만들 수 있다.
이제 몬스터 마다 Spawner 클래스를 따로 만들지 않아도 되지만, Monster 클래스 마다 clone() 을 구현해야 하기 때문에 코드의 양은 별반 다를게 없다. 또, clone()으로 만들다 보면 객체를 깊은 복사를 해야할지, 얕은 복사를 해야할지 애매할 때도 있다. 따라서 요즘은 개체 종류별로 클래스보다는 컴포넌트나 타입 객체로 모델링하는 것을 선호한다.
앞서 모든 몬스터마다 별도의 클래스가 필요로 했었다. 메서드로 만들어보자.
Monster* SpawnGhost() {
reutrn new Ghost();
}
몬스터 종류마다 클래스를 만드는 것보다 훨씬 양이 줄었다. 이제 스포터 클래스에는 함수 포인터 하나만 두면 된다.
typedef Monster* (*SpawnCallback)();
class Spawner {}
public:
Spawner(SpawnCallback spawn) : spawn_(spawn) {}
Monster* spawnMonster() { return spawn_(); }
private:
SpawnCallback spawn_;
;
이러면 유령을 소환하는 객체는 이렇게 만들 수 있다.
SPawner* ghostSpawner = new Spawner(spawnGhost);
만약 스포너 클래스를 이용해 인스턴스를 생성하고 싶지만 특정 몬스터 클래스를 하드코딩하기는 싫다면 몬스터 클래스를 템플릿 타입 매개변수로 전달하면 된다.
class Spawner {}
public:
virtual ~Spanwer() {}
virtual MOnster* spawnMOnster() = 0;
;
template <class T>
class SpawnerFor : public Spawner {
public:
virtual Monster* spawnMonster() { return new T(); }
};
템플릿으로 만들면 사용법은 다음과 같다.
Spanwer* ghostSpawner = new SpawnerFor<Ghost>();
일급 자료형인 동적 자료형 언어에서는 굳이 위의 코드를 작성할 필요 없이 그저 원하는 몬스터 클래스(심지어 실제 런타임 객체)를 그냥 전달해주기만 하면 된다. 이러한 편의성 때문에 프로토타입의 위치는 약간 애매한것 같다.
게임용 데이터 모델을 정의할 때 프로토타입 패턴이 좋은 방법이 될 수 있다.
기획자는 몬스터와 아이템 속성을 파일 어딘가에 정의해야 한다. 이때 가장 많이 사용하는 방법중 하나가 JSON이다.
고블린을 정의한다면 아마 다음과 같이 정의할 것이다.
{
"이름": "고블린 보병"
"기본체력": 20,
"최대 체력": 30,
"내성": ["추위", "독"],
"약점": ["불", "빛"]
}
다른 종류의 고블린 데이터도 같이 만들어 보자.
{
"이름": "고블린 마법사"
"기본체력": 20,
"최대 체력": 30,
"내성": ["추위", "독"],
"약점": ["불", "빛"],
"마법": ["화염구", "번개 화살"]
}
{
"이름": "고블린 궁수"
"기본체력": 20,
"최대 체력": 30,
"내성": ["추위", "독"],
"약점": ["불", "빛"],
"공격방법": ["단궁"]
}
고블린 종류가 다르다고 하더라도 중복되는 데이터가 상당히 많아 굉장히 거슬린다. 위처럼 작성하면 유지보수에 골머리를 앓을 수 있다. 모든 고블린을 강하게 만들어야 한다면 고블린 종류 하나마다 전부 고쳐야 한다.
하지만 여기서 객체에 프로토타입 필드가 있어서 다른 객체의 이름을 찾을 수 있다고 정한다면 첫 번째 객체에서 원하는 속성이 없다면 프로토타입 필드가 가리키는 객체에서 대신 찾는다.
그러면 고블린들의 JSON 데이터를 단순하게 만들 수 있다.
{
"이름": "고블린 보병"
"기본체력": 20,
"최대 체력": 30,
"내성": ["추위", "독"],
"약점": ["불", "빛"]
}
{
"이름": "고블린 마법사"
"프로토타입": "고블린 보병",
"약점": ["불", "빛"]
}
{
"이름": "고블린 궁수"
"프로토타입": "고블린 보병",
"공격방법": ["단궁"]
}
데이터 모델에 단순한 위임을 하나 추가했을 분인데 중복을 많이 제거할 수 있었다.
프로토 타입 기반 시스템에서는 새로 정의되는 객체를 만들 때 어떤 객체든 복제로 사용할 수 있는게 당연하다. 특히 일회성 특수 개체가 자주 나오는 게임에 잘 맞는 방식이다.
위와 같이 기획자는 기존 무기나 몬스터를 약간 변형해 쉽게 게임 월드를 풍성하게 만들 수 있게 되었다. 이런 것이야 말로 게임을 즐기는 유저가 원하는 것일지도 모르겠다.
'Game Develop > Design Pattern' 카테고리의 다른 글
State Pattern(상태 패턴) (2) | 2024.06.12 |
---|---|
Singleton Pattern(싱글턴 패턴) (1) | 2024.06.10 |
Command Pattern(명령 패턴) (0) | 2024.06.06 |
Observer Pattern(관찰자 패턴) (0) | 2024.06.05 |
Flyweight Pattern(경량 패턴) (2) | 2024.06.04 |