감시자 패턴은 객체 사이에 일 대 다의 의존 관계를 정의해두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지 받고 자동으로 업데이트될 수 있게 만듭니다.

 

관찰차 패턴을 java에서는 java.util.Observer 라이브러리로 지원하고, C# 에서는 event 키워드로 지원한다.

4.1 업적 달성

업적의 종류가 보통 광범위하기 때문에 단순하게 생각한다면, 업적의 조건을 만족하게 되는 부분에 업적 달성 함수를 호출하게 된다면 코드가 지저분해질 것이다.

 

대상 객체는 자신을 관찰하는 모든 관찰자들 배열 (혹은 리스트)를 가지며, 특정 기준이나 사건 발생 시에 onNotify 함수를 호출시켜 알림을 모두에게 보낸다. 

순차적으로 onNotify 메서드를 호출하기 때문에 (동기적) 한 관찰자의 onNotify 반환이 늦으면 느려질 수 있다.

관찰자 포인터 배열을 사용하는 대신에 연결 리스트를 사용할 수도 있다.

공유를 통해 많은 수의 소립(fine-grained) 객체들을 효과적으로 지원하는 패턴

 

3.1 숲에 들어갈 나무들

나무들이 동일한 메시 정보(폴리곤, 정점), 텍스처를 사용한다면 이 특성들을 공유 데이터로써 GPU에 한 번만 보내야 한다.

3.2 수천 개의 인스턴스

나무 인스턴스를 그릴 때 공유 데이터를 사용하도록 한다.

 

Direct3D, OpenGL에서는 하드웨어적으로 인스턴스 렌더링을 지원한다.

https://lemonyun.tistory.com/57

 

16. 인스턴싱과 절두체 선별

인스턴싱 : 한 장면에서 같은 물체를 여러 번 그리는 것, 성능을 크게 향상할 수 있다. 절두체 선별 : 시야 절두체 바깥에 있는 일단의 삼각형들을 간단한 판정으로 골라내서 기각하는 기법 16.1

lemonyun.tistory.com

DirectX 12를 이용한 3D 게임 프로그래밍 입문 16장에서 다룬 인스턴싱에서는 아래와 같은 자료를 셰이더에 전달했다.

 

 

공유 데이터 : 재질 자료를 담은 구조적 버퍼, 텍스처 배열, 정점 버퍼 뷰, 인덱스 버퍼 뷰

개별 데이터 : 인스턴스별 자료 항목을 담은 구조적 버퍼 (material index, world matrix, textransform)

3.4 지형 정보 (경량 패턴 사용 예시)

맵이 격자 타일 형태라고 할때 

Terrain tiles_[WIDTH][HEIGHT] 형태의 배열을 사용하여 타일 정보를 관리할 수 있다.

ㄴ Terrain을 enum으로 사용하는 경우

ㄴ 지형 종류에 대한 데이터 (예를 들면 해당 지형에서의 기온)를 위한 함수가 별도로 필요함

 

지형 종류에 대한 데이터 캡슐화를 위해 Terrain을 클래스로 정의

하나씩 선언된 물, 풀, 용암에 대한 Terrain이 공유 데이터로써의 역할을 한다.

명령 패턴 - 간단하게 정리하자면 함수 호출을 매개변수화 한 것?

콜백 함수, 함수 포인터를 사용하여 구현할 수 있다.

 

2.1 입력키 변경

 

버튼을 눌렀을때 특정 메서드(행동)가 수행하도록 하는 코드가 있다면

 

1. 모든 행동을 실행하는 공통 상위 클래스 Command를 정의 (execute 함수가 있는 인터페이스)

2. 행동별로 상위 클래스를 상속받는 하위 클래스를 만들고 execute 함수를 정의 (execute에서 실제로 메서드를 호출)

3. 입력 핸들러 코드에는 버튼마다 상위 클래스 포인터를 저장

4. 입력 핸들러 초기화 시점에 버튼에 하위 클래스 객체를 바인드함

 

2.2 액터에게 지시하기

위의 예에서 execute 함수에 매개변수로 GameActor 객체를 받는다.

 

명령을 실행할 때 액터만 바꾸면 플레이어가 아닌 액터에 명령을 실행 할 수 있다.

플레이어와 같은 명령을 사용하는 AI 캐릭터가 있다면 적용할 수 있다.

 

Command 객체를 선택하는 부분 (입력 핸들러 코드)과 액터(누가 명령을 수행하는지)를 디커플링함으로써 코드가 유연해진다.

 

2.3 실행취소와 재실행

Command 클래스에 가상 함수 Undo()를 정의한다.

 

어떤 일을 하는지를 정의한 명령 객체(Command를 상속받은)를 반복해서 생성한다.

그 명령 객체는 이전 명령의 상태를 저장할 수 있어야 한다. (execute() 함수에서 현재 상태를 이전 상태에 기록하고 상태를 변경해야 한다.)

 

Undo() 함수는 현재 상태를 이전 상태로 바꿔야한다.

 

이전 명령의 상태를 저장하고 있어야 하므로 Command 자료형을 저장할 수 있는 배열이나 스택같은 곳에 명령 객체를 동적으로 생성해 저장하고 있어야 한다. (버튼 등의 입력으로 명령을 수행할 때 마다)

 

이전 상태로 돌아가기 위해서는 배열이나 스택의 포인터를 이동해가면서 포인터가 가리키는 곳의 명령 객체의 Undo() 함수를 호출하면 된다. 

 

재실행은 게임에서 잘 쓰이지 않을 수도 있지만, '리플레이'는 게임에서 자주 쓰인다.

매프레임마다 전체 게임 상태를 저장하는 대신 전체 개체가 실행하는 명령 모두를 매 프레임 저장한 뒤 리플레이 할때 저장한 명령들을 순서대로 실행해 게임을 시뮬레이션한다.

 

1.1 좋은 소프트웨어 구조란?

코드를 얼마나 쉽게 변경할 수 있느냐가 코드 설계를 평가하는 척도가 된다.

 

코드를 고치려면 고치려는 부분의 기존 코드를 이해해야 하기 때문에 작업에 관련된 코드의 양을 줄여야 한다.

이 관점에서 보면 커플링이 적은 코드가 곧 좋은 소프트웨어 구조를 만든다.

 

하지만 지나치게 확장성에 신경쓰다 보면 추상화를 위한 보조 코드가 더 많아져 실제 작업 코드를 찾기가 어려워질 수도 있고, 좋은 구조를 유지하는데에도 꾸준한 노력이 필요하기 때문에 비용이 추가적으로 발생할 수도 있다.

 

1.3 성능과 속도

프로그램의 유연성과 성능은 반비례 관계에 있다.

코드를 유연하게 만드는 많은 패턴이 가상함수, 인터페이스, 포인터, 메세지 같은 메커니즘에 의존하는데, 다들 어느정도의 런타임 비용을 요구하기 때문이다.

하지만 유연성이 좋아야 게임을 쉽게 변경할 수 있고, 개발 속도가 빨라진다.

 

최적화 기법은 구체적인 제한을 선호한다. 

ㄴ 인터페이스나 가상함수의 사용을 줄인다.

ㄴ 성능상 비용이 줄어든다.

 

처음에는 코드를 유연하게 유지하다가 기획이 확실해진 다음에 추상 계층을 제거하여 최적화를 진행하는 타협안이 있다.

 

1.4 나쁜 코드의 장점

기획 확인이 필요할때, 필요한 기능만 대강 돌아가도록 하는 코드는 적절하다.

이렇게 만든 버릴 코드는 확실히 버릴 수 있어야 한다.

 

+ Recent posts