Home [게임 프로그래밍 패턴] Design Patterns Revisited: Prototype
Post
Cancel

[게임 프로그래밍 패턴] Design Patterns Revisited: Prototype

Prototype

The Prototype Design Pattern

  • Spawner 를 통해 Monster가 생성된다고 가정하자.
  • 이 Spawner는 Monster 종류마다 따로 있다. - 이 때 brute-force로 구현하고자하면 아래와 같은 상속구조를 가지도록 할 수 있다.

  • 프로토타입으로 이를 구현하면, 이를 좀 간결하게 구현할 수 있다.
    • 어떤 객체가 자기와 비슷한 객체를 스폰할 수 있게된다.
    • Monster 객체를 자신과 비슷한 객체를 생성하는 프로토타입 객체로 만드는것.
1
2
3
4
5
6
7
8
class Monster
{
public:
  virtual ~Monster() {}
  virtual Monster* clone() = 0;

  // Other stuff...
};
  • Monster 하위 클래스에서, 새 객체를 리턴하도록 구현해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Ghost : public Monster {
public:
  Ghost(int health, int speed)
  : health_(health),
    speed_(speed)
  {}

  virtual Monster* clone()
  {
    return new Ghost(health_, speed_);
  }

private:
  int health_;
  int speed_;
};
  • 이를 상속받는 클래스를 사용하면, 이제 종류별로 만들 필요 없이 하나만 만들면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Spawner
{
public:
  Spawner(Monster* prototype)
  : prototype_(prototype)
  {}

  Monster* spawnMonster()
  {
    return prototype_->clone();
  }

private:
  Monster* prototype_;
};
  • Spawner는 Monster 객체를 생산하는 역할만 한다.

  • 아래와 같이 사용할 수 있다.
1
2
Monster* ghostPrototype = new Ghost(15, 3);
Spawner* ghostSpawner = new Spawner(ghostPrototype);
  • 장점
    • 클래스와 상태를 클론한다.
    • 프로토타입으로 사용하는 객체를 잘 설정하면, 여러 상태를 가진 몬스터(빠른, 약한, 강한)를 생성하는 스포너를 만들 수 있다.

How well does it work?

  • 깊은 복사(deep clone), 얕은 복사(shallow clone) 등 구현할 때 생각할만한 것들이 많다.

  • 요즘 나오는 게임엔진은 이처럼 몬스터마다 클래스를 따로 만들지 않는다.

  • 클래스 상속구조가 복잡하면, 유지보수가 힘들다.

    • 요즘은 Component 와 Type Object을 사용하여 각각의 클래스를 만들지 않고 다양한 종류의 entity를 모델링한다.

Spawn functions

  • 각 몬스터마다 별도의 스포너 클래스를 만드는 대신 스폰 함수를 만들 수 있다.
1
2
3
4
Monster* spawnGhost()
{
  return new Ghost();
}
  • 몬스터 종류마다 클래스를 만드는 것보다 코드가 적다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 함수포인터 typedef : https://stackoverflow.com/questions/4295432/typedef-function-pointer
typedef Monster* (*SpawnCallback)();

class Spawner
{
public:
  Spawner(SpawnCallback spawn)
  : spawn_(spawn)
  {}

  Monster* spawnMonster()
  {
    return spawn_();
  }

private:
  SpawnCallback spawn_;
};
  • 다음과 같이 객체를 생성한다.
1
2
Spawner* ghostSpawner = new Spawner(spawnGhost);

Templates

  • 특정 몬스터 클래스를 생성하는 함수들을 하드코딩하기 싫다면, 몬스터 클래스를 템플릿 타입 매개변수로 전달하면된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Spawner
{
public:
  virtual ~Spawner() {}
  virtual Monster* spawnMonster() = 0;
};

template <class T>
class SpawnerFor : public Spawner
{
public:
  virtual Monster* spawnMonster() { return new T(); }
};
  • 아래와 같이 사용할 수 있다.
1
Spawner* ghostSpawner = new SpawnerFor<Ghost>();

First-class types

  • C++ 의 타입들은 first-class가 아니라 위처럼 타입을 넘겨야한다.
  • JS, python, ruby등과 같은 동적 타입 언어(dynamically-typed language)는 더 직접적으로 해결할 수 있다.

    • 그냥 원하는 몬스터를 런타임 객체를 그냥 전달.
  • 사실 저자는 이 디자인 패턴이 이상적으로 사용할 수 있는 곳이 없다고 생각한다.

  • 저자는 언어 패러다임으로서의 프로토타입에대해 이후 기술한다.

The Prototype Language Paradigm

  • OOP가 데이터와 코드를 묶어주는 ‘객체’를 직접 정의할 수 있게 한다.
    • 상태와 동작을 함께 묶는것.

Self

  • 셀프는 클래스는 없지만 OOP에서 할 수 있는걸 다 할 수 있는 언어이다.

    • 클래스 기반 언어보다 더 객체 지향이다.
  • 클래스기반 언어

    • 상태가 인스턴스 안에 들어있다.
    • 동작은 클래스에 있다.(메서드 호출 시 인스턴스의 클래스를 찾음)
    • 상태와 메서드는 다르다.

  • 셀프에는 이런 구별이 없다. - 무엇이든 객체에서 바로 찾을 수 있다. - 인스턴스는 상태와 동작 둘다 가짐.

C++에서는 가상 메서드를 호출할 때, 먼저 인스턴스에서 vtable 포인터를 구하고, 다시 vtable로부터 메서드를 찾는다.

vtable: 컴파일 시 가상함수가 정의된 클래스가 있으면, 가상 함수 테이블이 만들어져, 바이너리 ‘rdata’ 영역에 기록되며, 해당 클래스로 만들어진 객체에서 함수를 호출할 때 해당 클래스의 vtable을 참조해서 함수를 호출한다.

  • 클래스 기반 언어
    • 약간의 단점: 상속
    • 장점: 다형성을 통한 코드 재사용과 중복 코드 최소화
  • 셀프에서는 클래스의 장점을 얻을 수 있는 위임(delegation) 개념이 있다.

  • 해당 객체에 필드나 메서드가 없으면, 상위(parent) 객체를 찾는다.

    • 상위 객체는 그냥 다른 객체 레퍼런스
    • 상위레벨에서 찾는걸 계속 반복한다.
    • 즉, 위임하는것을 반복한다.
  • 클래스 == 자기 자신의 인스턴스 factory

  • 셀프에서는 프로토타입과 같이 복제(clone)한다.
    • 즉, 모든 객체가 프로토타입 디자인 패턴을 지원하는것.

How did it go?

  • 언어를 구현하는건 비교적 쉽지만 사용자는 사용하기 복잡하다.

  • 이해하기 어려움.

What about JavaScript?

  • 자스는 프로토타입 기반.
  • 셀프로부터 영감 받아 만들어진것.
  • 객체 == 아무 속성 값이나 가짐
  • 속성 == 필드나 메서드(필드로서 저장된 함수)
  • 객체 == ‘프로토타입’이라고 부르는 다른 객체를 지정 가능
    • 자기 자신에 없는 필드를 프로토타입에 위임 가능
  • 하지만 클래스 기반 언어에 더 가까움.

    • 복제(clone) 기능이 없기 때문.
  • 자스는 자료형을 정의하는 객체로부터 new를 호출하는 생성자 함수를 통해 객체를 생성한다.
  • 상태는 인스턴스 그 자체에 저장한다.
  • 동작은 자료형이 같은 객체 모두가 공유하는 메서드 집합을 대표하는 별도 객체인 프로토타입에 저장되고, 위임을 통해 간접 접근됨.

Prototypes for Data Modeling

  • 사실상 프로토타입은 사용하기 어려움.

  • 옛날에는 모든걸 절차적으로 생성했다.(용량을 줄이기 위해)

  • 요즘 게임 콘텐츠는 모두 데이터에 정의됨.

  • 프로그래밍 언어를 사용하는 이유: 복잡성을 제어할 수 있는 수단을 가지고 있음.

  • 게임 데이터도 규모가 일정 이상되면, 코드와 비슷한 기능이 필요하다.

    • 데이터 모델링, 프로토타입과 위임을 활용해 데이터를 재사용하는 기법

데이터

  • 몬스터와 아이템 속성을 파일 어딘가에 정의한다고 해보자.

    • Json을 많이 사용함.
    • 키/값 구조로 이루어진 데이터 개체로 맵, 속성목록(property bag) 라고 불림

      데이터 개체 관련 용어 => 범용 디자인 패턴

  • 아래와 같이 몬스터 구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "name": "goblin grunt",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"]
}
{
  "name": "goblin wizard",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"],
  "spells": ["fire ball", "lightning bolt"]
}

{
  "name": "goblin archer",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"],
  "attacks": ["short bow"]
}
  • 이러면 개체에 중복이 많다.
    • 유지보수를 어렵게하는 원인
  • JSON에는 추상 자료형이라는 개념이 없다.
  • 객체에 ‘프로토타입’필드가 있어서, 여기에서 위임하는 다른 객체의 이름을 찾을 수 있다고 가정하자.
    • 첫 번째 객체에서 원하는 속성이 없으면, 프로토타입 필드가 가리키는 객체에서 대신찾는다.

      즉, 프로토타입 필드는 실제 데이터가 아닌 메타데이터이다.

  • 이제 아래와 같이 단순하게 만들 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "name": "goblin grunt",
  "minHealth": 20,
  "maxHealth": 30,
  "resists": ["cold", "poison"],
  "weaknesses": ["fire", "light"]
}

{
  "name": "goblin wizard",
  "prototype": "goblin grunt",
  "spells": ["fire ball", "lightning bolt"]
}

{
  "name": "goblin archer",
  "prototype": "goblin grunt",
  "attacks": ["short bow"]
}
  • 이제 반복 입력할 필요가 없다.
  • 일회성 특수 개체가 자주 나오는 게임에 잘맞는 방식이다.

  • 보스와 유니크 아이템은 일반 몹과 아이템을 약간 다듬어 만들 때가 많으므로, 프로토타입 방식의 위임을 사용하기 좋다.(조금 더 게임을 풍성하게)

출처

Prototype

vtable

This post is licensed under CC BY 4.0 by the author.

[note] mediapipe와 ipc

[게임 프로그래밍 패턴] Design Patterns Revisited: Singleton