4. Design Patterns

혀니앤·2022년 10월 19일
0

지난 시간

  • 게임 구조와 시뮬레이션
  • 게임 프로덕션 : 아이템을 어떻게 구성하여 프로덕션을 어떻게 할까
  • 이번엔 게임에서 많이 쓰이는 디자인 패턴을 공부해보자

디자인 게임 엔진

  • 얼마나 많은 컴포넌트가 필요할까?
    • 렌더링, 애니메이션
    • 전체 게임 월드를 관리하는 컴포넌트

디자인 패턴?

  • 프로그래밍을 하면서 반복적으로 발생하는 문제를 해결하는 표준화된 방법
  • 다 따로 만들 필요 없이 공통적인 것을 만들고 → 다른 부분을 세세하게 추가하는 것이 효율적이다 → 어떻게 만들까?
  • 반복가능한 솔루션!

💡Abstract Prototype Pattern

  • 상대 비행기들이 있을 때, 나타나고 싸우고, patrol하고 도망가는 행동들을 전부 따로 만들 필요가 있을까?
  • 공통의 행동을 가지지만 세부 설정만 다른 것이다
  • Base Class를 만들어서 자식 method가 상속받도록 하자! ⇒ 중복 구현의 문제를 해결할 수 있다

Issue

1) 구조적 이슈 : 게임엔진에서 깔끔하게 코드를 작성할 수 있을까?

  • singleton

2) Storage 이슈 : 많은 데이터를 어떻게 효과적으로 관리할 수 있을까?

  • Factory
  • Flyweight

3) Behavioral 이슈 : 100가지 몬스터가 있을 때 다 따로 코딩하면 비효율적!

  • spatial index
  • strategy
  • composite

1. Architectural Issues

  • 어떤 데이터들은 다른 모듈에서 읽힐 필요가 있다 (플레이어, 인풋 컨트롤, 오브젝트 매니저)
  • 게임 월드 변수는 모든 함수에서 읽을 수 있어야 한다 → 어떻게 할까?
  • 방법 1 : 전역 변수 사용
    • 소스 코드가 엄청 많고 게임 엔진이 거대한데, 버그가 발생했을 때 디버깅하기 어렵다
    • 병렬 처리로 하나의 모듈이 실행중일 때 다른 모듈이 데이터를 건들면서 동기화 문제가 생기거나, 각 모듈의 의존도가 높아진다
  • 방법 2 : 함수 파라미터로 전달하기
    • 다 넘겼다가는 전역변수랑 차이가 없으므로 필요한 것들만 전달해야 하는데, 점점 파라미터가 늘어난다
    • 함수 정의가 엄청 길어진다 → 코드 읽기가 어려워진다 (Readablilty)

→ Singleton 사용!

💡 Singleton 패턴

  • instantiation method를 하나만 쓴다
  • 다른 클래스는 권한을 받아서 접근한다
class Singleton {
public:
	static Singleton* Instance(); //한 번만 호출된다
protected:
	Singleton();
	static Singleton* m_pInstance;
	... 
};
---------------------------
Singleton* Singleton::m_pInstance = NULL; 
Singleton* Singleton::Instance() {
	if(m_pInstance==NULL) m_pInstance = newSingleton;
	return m_pInstance;
}
  • 병렬성 문제까지 해결할 수 있다는 장점이 있다

2. Storage Issues

  • 여러개의 함수, 모듈이 있다면 생성과 소멸이 어렵다 !
  • 생성과 소멸을 centralize 해야한다
    • 메모리 할당을 효율적으로 할 수 있다
    • 내가 만든 함수나 변수를 재활용하기 좋다

💡 Factroy Pattern

  • 여러 개의 생성, 소멸자를 집중시킨다
  • NPC, 아이템 등을 집중해놓았기 때문에 1000개의 몬스터를 만들어야 할 때 미리 1000개의 공간을 할당해놓을 수 있다

Abstract Factory

  • Factory Pattern 의 예시
  • 스테이지에서 구동 방식은 같지만 데이터가 다를 때, 특성, 데이터만 다르게 설정하여 Factory를 사용할 수 있다

  • 코드 예시
class Product;
class Texture: public Product; 
class Mesh: public Product; 
class Item: public Product;

class AbstractFactory { 
public:
	Product* Create(int id);
}

Product* AbstractFactory::Create(int id) {
// 원하는 오브젝트를 생성한다
	switch (id) {
		case TEXTURE: return new Texture; break; 
		case MESH: return new Mesh; break;
		case ITEM: return new Item; break;
	}
	return NULL;
}
class Factory { 
public:
	// 이 안에서 AbstractFactory::Create(TEXTURE) 를 호출한다
	Texture* CreateTexture(); 
	Mesh* CreateMesh(); 
	Item* CreateItem();
};

Storage Issues 2

  • 유닛들의 행동 패턴은 동일하지만 작은 차이만 가지고 있다. → 다 따로 만드는 것은 비효율적
  • 어떻게 만들까? Abstract Class 와 Real Class 을 분리하자!
    • Abstract : 공통적 특성
    • Real : 각각의 세부적 특성

💡 Flyweight Pattern

  • 하나의 Factory를 가지고, 특정한 타입을 Create 해준다
  • 공유된 행동 값을 가지면서, 차이점을 Extrinsic Parameter 로 전달한다
class Soldier: public AbstractFlyweight 
{
public:
  //각 솔져마다 다른 값의 Extrinsic Parameter를 가진다
	void methods(ExtrinsicParameters* ); 
protected:
  //전략(전진, 후퇴) 등의 공통된 행동은 공유한다
	IntricsicParameters m_params; 
};

class SoldierInstance 
{
public:
	void methods(); 
protected:
	ExtrinsicParameters m_info; 
};

class FlyweightFactory 
{
public:
	AbstractFlyweight* GetFlyweight(int type);
	static FlyweightFactory* Instance(); 
protected:
	AbstractFlyweight* m_aFlyweight;
	AbstractFlyweight* Create(int type); // Factory로 특정 타입을 만들어낸다
	...
};

AbstractFlyweight* FlyweightFactory::GetFlyweight(int type)
{
	//타입 오브젝트가 존재하지 않는다면 만들어주는 부분
	if (m_aFlyweight[type] not exists) m_aFlyweight[type] = Create(type);
	return m_aFlyweight[type]; 
}

void SoldierInstance::methods() 
{
	FlyweightFactory* ff = FlyweightFactory::Instance();
	Soldier* soldier = ff->GetFlyweight(SOLDIER);
	soldier->method(m_info)
}

3. Behavioral Issues

  • 가장 가까운 오브젝트를 찾고자 할 때, n개의 몬스터와 m개의 대상이 있다면 거의 O(n^2)의 알고리즘이 필요하다 → 너무 비효율적이다!
  • 실제로 충돌 검사가 가장 오래걸린다

💡 Spatial Index Pattern

1) Linked List

  • 오브젝트들을 Linked List로 연결한다
  • 그룹으로 연결지어놓고 그 그룹만 확인한다
  • 단점 : 실제 게임에서는 그룹이 너무 크고, 전체적으로 연결되어 있다

2) Regular Grid

  • 내가 움직일 수 있는 범위 내의 Grid만 검사한다
  • 하나의 단일 공간을 나눔으로써 너무 많은 오브젝트를 검사하지 않아도 된다
  • 이것만으로 충분하지 않은 경우가 생긴다
    • 하나의 그리드 안에 너무 많은 오브젝트가 생긴다면?

      → 필요에 따라서 그리드 안에 그리드를 또 넣어보자

3) QuadTree 구조

  • 필요에 따라 그리드를 촘촘하게 나누는 방식
  • 점점 나눠가면서 일정 개수만 들어있도록 한다
  • 공간을 나눔으로써 원하는 객체를 좀더 쉽게 찾을 수 있다
  • QuadTree, Octree는 계속해서 Tree를 만들어야 하므로 오브젝트의 개수가 적다면 오히려 비효율적일것 → 오브젝트가 많고, 어느 특정 부분에 몰려있는 경우에 쓰자

Behavioral Issue 2

  • AI 의 행동이 주변에 따라 달라진다
  • 어떻게 구현할까?
    • switch 구문 사용 : 너무 길어지고 읽기 어려워진다
    • derived class 를 사용한다
      • 각각 상속받아서 구현한다

        → 점점 알고리즘이 복잡해진다

      • 전체를 관리하기 어렵다

      • 기본적 물리 속성은 같고, 행동 특성만 다른 것이다

        → 코드 읽고 관리하는 데에 오히려 비효율적이다!

💡 알고리즘을 파라미터로 전달하기

  • 어떤 전략을 가지는지 (무모, 소심 등등)
  • 엔진 자체는 바꾸지 않으면서 전략 값만 변경할 수 있다
class Fighter 
{
public:
	//전략을 파라미터로 전달한다
	Fighter(Strategy*);
	void recalc_AI();
	// 전략을 변경한다
	void change_strategy(Strategy*);
private:
	Strategy* m_pStrategy
}

void Fighter::recalc_AI()
{
	m_pStrategy->recalc(...);
}

class Strategy {
public:
	virtual void recalc(...); 
}

//전략마다 새로운 클래스를 만든다
class FightStrategy: public Strategy; 
class EscapeStrategy: public Strategy; 
...

Behavioral Issues 3

  • 여러종류의 오브젝트를 효과적으로 관리하려면 어떻게 해야할까?
  • ex) scene graph
    • 캐릭터, npc 등의 공통적 위치와 특성, 동시에 서로 다른 특성들을 어떻게 관리할까?
  • 해결
    • Abstract class 와 virtual function으로 구현하기

💡 Composite Pattern

  • 하나나 그 이상의 유사한 특성을 갖는 물체를 조합하여 만드는 패턴
  • 트리 하나의 노드는 그 자식 노드들을 add, remove 하여 만들 수 있다.
  • 새로운 노드를 만들기 편해진다

  • ex) 공항 게임을 만든다?
    • 비행기들이 엄청 많고, 날고 이륙하고 착륙하는 큰 행동을 동일하다
    • 도킹이 두번 되는 비행기 등의 세부 특성만 다르기 때문에 Instance 로 추가해준다

💡 UI 에서의 디자인 패턴

  • 사용자가 잘못된 입력을 하지 않도록 돕는 패턴
  • Shield (Input shielding)
    • ‘정말 실행하시겠습니까?’ 등의 확인문구를 출력하게 하기
    • 추가적 행동을 통해 잘못된 입력 방지
  • State Changing : 동일한 클릭에 대해 걸어가는 클릭인지, 탈것을 클릭한것인지 모드를 변경해줘야 함
  • Magnetism : 클릭을 보정해서 자석처럼 그 근처를 인식시킨다
  • Focus : 클릭할 수 없는 부분은 안보이거나 흐리게 처리하기
  • Progress : 맵 로딩바를 출력하기

정리

  • 고려할 점
    • 이게 정말 효율적인가?
    • Readablilty 가 좋은가?
    • 상황에 맞게 잘 결정해야 한다
  • Ogre3D, OpenSceneGraph 등을 보면 상당히 많은 디자인 패턴을 볼 수 있다
profile
일단 시작하기

0개의 댓글