15.1 게임플레이 기반 시스템의 컴포넌트

대부분의 게임 엔진은 게임의 고유한 규칙과 목표, 그리고 동적 월드 구성 요소들을 만드는 기반으로 쓰이는 여러 런타임 소프트웨어 컴포넌트를 제공한다. 이런 컴포넌트를 게임플레이 기반 시스템이라고 부르기로 하자.

 

게임 엔진마다 게임 플레이 소프트웨어 디자인에 대한 접근이 조금씩 다르다.

 

대부분의 엔진이 지원하는 주요 하위 시스템

1. 런타임 게임 객체 모델

ㄴ 게임디자이너가 월드에디터를 통해 인지하는 가상의 게임 객체 모델을 실제로 구현하는 부분

 

2. 레벨 관리 및 스트리밍

ㄴ 게임 플레이가 벌어지는 가상 월드의 콘텐츠를 불러오고 내리는 시스템,

ㄴ 레벨 데이터를 메모리에 스트리밍하면 플레이어는 광활하고 연속된 월드에 있는 것처럼 인지하게 된다.

 

3. 실시간 객체 모델 업데이트

ㄴ 게임 객체들이 스스로 동작하게 하려면 모든 객체를 주기적으로 업데이트해줘야 한다.

 

4. 메세지와 이벤트 처리

ㄴ 대부분의 게임 객체들은 서로 통신해야 한다. 이런 통신은 대개 추상적 메세지 시스템을 통해 이뤄진다.

 

5. 스크립트

ㄴ 하이레벨 게임 로직을 C나 C++ 등의 프로그래밍 언어로 짜면 매번 컴파일을 해줘야 한다. (느리다)

생산성을 향상시키면서 빠른 반복 생산을 가능하게 하기 위해 게임 엔진에 스크립트 언어를 통합하는 경우가 많다.

파이썬이나 루아같이 텍스트 기반인 언어도 있고, 언리얼 엔진의 Kismet처럼 그래픽 언어인 경우도 있다.

 

6. 목적 및 게임 흐름

 

엔진마다 부분적으로 지원하는 시스템

1. 동적으로 게임 객체를 생성하고 파괴하기

ㄴ 게임월드의 동적인 구성 요소들을 게임 플레이 도중에 만들었다가 없애는 경우가 자주 있다.

 

2. 로우레벨 엔진 시스템과의 연동

ㄴ 모든 게임 객체는 적어도 한두 가지 이상의 하위 엔진 시스템과 연관이 있다. 게임 객체들이 각자 필요한 하위 엔진 시스템에 접근할 수 있게 해주는 것이 게임플레이 기반 시스템의 주요한 역할 중 하나다.

 

3. 새로운 게임 객체 타입을 정의할 수 있는 기능

ㄴ 새로운 객체 타입을 쉽게 추가하고 월드에디터에서 사용할 수 있게 유연한 게임 객체 모델을 갖는것이 중요하다.

ㄴ 아주 이상적인 상황이라면 데이터 주도 방식(스크립트)만으로 새로운 객체 타입을 정의할 수도 있지만 대부분의 엔진에서는 프로그래머의 손을 거친다.

 

4. 고유 객체 식별자(id)

ㄴ 게임 월드 안에는 다양한 종류의 게임 객체들이 수백 혹은 수천 개 존재하는 경우가 흔하기 때문에 런타임에 각 객체들을 구분하고 찾을 수 있는 기능이 중요하다.

ㄴ정수 식별자나 문자열 식별자를 사용한다.

 

5. 게임 객체 질의

ㄴ 게임 월드 내에서 게임 객체를 찾을 수 있는 방법이 반드시 있어야 한다.

ㄴ 예를 들면 플레이어 캐릭터 주위 20미터 안에 있는 모든 적을 찾는 것

 

6. 게임 객체에 대한 참조

ㄴ 객체를 찾았으면 객체를 참조하는 값을 담을 수 있어야한다.

ㄴ C++ 클래스 인스턴스에 대한 포인터나 스마트 포인터일 수도 있다.

 

7. 유한 상태 기계에 대한 지원

ㄴ 게임 객체 타입 중 유한 상태 기계로 모델링하면 가장 잘 들어맞는 것들이 종종 있다. 게임 객체가 한 번에 한 가지 상태를 가질 수 있게 지원하는 엔진도 있다.

 

8. 네트워크 레플리케이션

ㄴ멀티플레이어 게임에서의 게임 객체의 상태는 다른 모든 기계에 복제돼서 모든 플레이어들이 그 객체를 일관된 모습을 볼 수 있어야 한다.

 

9. 게임을 저장하고 불러오는 기능/객체의 영속성

ㄴ 게임 엔진이 월드 내의 게임 객체들의 현재 상태를 디스크에 저장하고 나중에 불러올 수 있도록 하는 기능

ㄴ RTTI, 리플렉션, 추상 생성같은 프로그래밍 언어의 기능을 필요로 한다.

RTTI, 리플렉션은 런타임에 객체의 타입 뿐만 아니라 그 객체의 클래스가 어떤 속성과 메서드를 지원하는지를 알 수 있게 해주고, 추상 생성은 클래스 이름을 하드코딩하지 않고도 그 클래스의 인스턴스를 사용할 수 있게 해준다.

 

15.2 런타임 객체 모델 구조

월드 에디터에서 디자이너들에게 제공되는 툴 측면 추상적 객체 모델을 게임에서 구현한 것을 런타임 객체 모델이라 할 수 있다.

런타임 객체 모델의 구현은 툴 측면의 추상적 객체 모델과 비슷할 수도 있고, 완전히 다를 수도 있다. 다양한 방식이 쓰이지만 대부분의 게임 엔진들은 다음의 두 가지 기본적인 구조 중 한 가지를 따른다.

1. 객체 중심적 구조

ㄴ 툴 측면의 게임 객체 하나는 런타임에서 클래스 인스턴스 한 개로 표현되거나, 아니면 서로 연관 있는 적은 숫자의 인스턴스 집합으로 표현된다.

ㄴ 객체는 자신의 속성과 행동을 가지며, 이것들은 객체의 클래스 내에 캡슐화 되어 있다.

 

2. 속성 중심적 구조

ㄴ 툴 측면의 게임 객체들은 각각 고유 id로만 표현된다.

ㄴ 각 게임 객체의 속성들은 속성 타입별로 데이터 테이블에 나뉘어 담겨 있으며, 접근할 때는 객체의 id를 이용한다.

 

15.2.1 객체 중심 구조

15.2.1.2 거대 단일 클래스 계층

하나의 공통 베이스 클래스를 거의 모든 객체들이 상속하는 경우

언리얼 엔진의 게임 객체 모델

 

15.2.1.3 깊고 넓은 계층 구조의 문제점

ㄴ 클래스들을 이해하기 힘들고 유지 및 수정이 어려움

 

ㄴ 여러 계열의 분류 구조를 구현할 수 없는 문제

예를 들어 생물체를 유전적인 형질을 기준으로 분류하면서 동시에 색깔에 따라 분류할 수 없다.(다중 상속의 문제)

다중 상속(is-a)보다는 합성(has-a)이나 조합을 사용하는 편이 낫다.

 

ㄴ 버블업 효과 

언리얼 엔진의 Actor 클래스는 렌더링, 물리, 월드와의 상호작용, 오디오 효과, 멀티플레이어용 네트워크 레플리케이션, 객체 생성 및 파괴, 액터 반복, 메세지 브로드캐스팅 등을 위한 데이터 멤버와 코드를 모두 가지고 있다. 이렇게 최상위 루트 클래스에 여러 기능이 버블업 하게 내버려 두면 여러 엔진 하위 시스템의 역할을 캡슐화하기 어려워진다.

 

15.2.1.4 합성을 통해 계층 구조를 단순하게 유지

합성 관계에서는 클래스 A가 클래스 B의 인스턴스를 직접 포함하거나 아니면 B의 인스턴스에 대한 포인터나 참조를 포함한다. A의 인스턴스가 생성될 때와 파괴될 때 B도 같이 생성되고 파괴되는 경우를 합성(composition) 이라고 한다. 두 클래스 사이에 포인터나 참조를 사용해 한 클래스가 다른 클래스의 생성과 파괴를 직접 관리하지 않게 만들 수도 있다. 이 경우는 조합(aggregation) 이라고 부른다.

 

컴포넌트 디자인을 활용하면 만들고자 하는 타입의 게임 객체가 꼭 필요로 하는 기능들만 넣게 선택할 수 있다.

https://lemonyun.tistory.com/78

 

14. 디커플링 패턴 - 컴포넌트

한 개체가 여러 분야를 서로 커플링 없이 다룰 수 있게 한다. 유니티 게임 오브젝트에 여러 컴포넌트를 붙였던 경험을 떠올려 쉽게 이해할 수 있었다. 멀티스레드 환경에서는 게임 코드를 분야

lemonyun.tistory.com

 

컴포넌트 생성과 소유권

ㄴ 단순한 방법 : GameObject 클래스가 가능한 모든 컴포넌트의 포인터를 갖고, 게임 객체들은 GameObject를 상속받아 정의한다. GameObject의 생성자에서는 모든 컴포넌트에 대한 포인터를 NULL로 초기화한다(하드 코딩). 상속받은 클래스의 생성자는 각자 필요에 따라 컴포넌트를 생성하면 된다.

ㄴ GameObject의 소멸자에서 모든 컴포넌트에 대한 포인터를 지워주는 식으로 구현하면 편리하다.

 

15.2.1.5 제네릭 컴포넌트

ㄴ 유연한 (구현하기에는 까다로운 방법) : 루트 클래스에 제네릭연결 리스트를 두어 컴포넌트들을 관리하는 방식

ㄴ 객체에 새로운 타입의 컴포넌트를 추가할 때 게임 객체 클래스를 수정하지 않을 수 있다.

ㄴ 연결 리스트를 순회하며 각 컴포넌트에 대해 타입을 질의한다거나 차례로 이벤트를 넘겨 처리할 기회를 주는 등 다형적인 연산을 할 수 있다.

 

15.2.1.6 순수 컴포넌트 모델 (속성 중심 디자인)

ㄴ GameObject '허브' 클래스를 둘 필요 없이 각 컴포넌트마다 해당 게임 객체의 고유 id를 복사해 넣는 방법 (컴포넌트 = 속성 중심 구조)

순수 컴포넌트 모델에서는 팩토리 패턴을 사용하면 된다. 가상 함수가 있는 팩토리 클래스를 게임 객체 타입마다 한 개씩 만들고, 가상 함수를 오버라이드해 객체마다 원하는 컴포넌트를 생성하게 한다.

 

ㄴ 순수 컴포넌트 디자인의 문제는 컴포넌트 간 통신을 중계해줄 GameObject 클래스가 없다는 것이다. 컴포넌트 간 통신이나 다른 게임 객체로 메세지를 보내는 일도 까다롭다.

 

15.2.2 속성 중심적 구조

게임 객체가 가질 수 있는 모든 속성의 집합을 정의하고, 속성마다 테이블을 만든다. 테이블에서 속성과 게임객체 ID가 매핑된다. (관계형 데이터베이스에 가깝다)

객체는 속성에 의해서만 정의되는 것이 아니라 행동에 의해서도 정의되기 때문에 행동을 구현하는 방법은 여러가지이며, 엔진마다 다르게 구현한다.

 

15.2.2.1 속성 클래스를 통해 행동 구현

속성 타입은 속성 클래스로 구현할 수 있다.

각 속성 클래스는 하드코딩된 메서드(멤버 함수)를 통해 행동을 제공한다.

어떤 게임 객체의 전체적인 행동은 그 객체의 모든 속성들의 행동이 합쳐져 결정된다.

 

15.2.2.2 스크립트를 통해 행동 구현

속성 값들은 데이터베이스형 테이블에 담아 두고 게임 객체의 행동을 스크립트 코드를 이용해 구현하는 방법이 있다.

게임 객체는 ScriptId와 같은 특수한 속성을 가질 수 있고, 이 속성이 있을 경우 이것은 해당 객체의 행동을 처리할 스크립트 코드 블록을 나타낸다.게임 월드의 이벤트에 게임 객체가 반응하게 만들 때도 스크립트 코드를 사용한다.

 

15.2.2.4 속성 중심 디자인의 장단점

장점

ㄴ 실제로 사용되고 있는 속성 데이터만 저장하면 되기 때문에 메모리를 적게 쓰는 경향이 있다.

ㄴ 게임 코드를 컴파일할 필요 없이 새 속성을 쉽게 정의할 수 있다.

ㄴ 같은 타입의 데이터가 메모리에 연속적으로 저장되기 때문에 캐시 효율이 좋을 수 있다. (배열의 구조체 방식을 사용한다면)

 

단점

ㄴ 속성들 간에 어떤 관계를 강제하기가 어려워진다.

ㄴ 게임 객체가 가지는 속성을 한눈에 볼 수 없기 때문에 디버깅이 어렵다.

 

15.3 월드 덩어리 데이터 형식

월드 청크에는 정적 월드 요소와 동적 월드 요소가 같이 들어간다.

 

월드 덩어리 데이터 파일에는 게임 객체마다 다음과 같은 것들이 저장된다.

객체 속성들의 초기 값

ㄴ 월드 청크는 모든 게임 객체가 처음 게임 월드에 생성될 때의 상태를 정의한다. 객체의 상태 데이터는 여러 형식으로 저장할 수 있다.

 

객체 타입에 대한 지정

ㄴ 객체 중심 엔진에서는 문자열, 해시 문자열 ID, 혹은 기타 고유한 타입 id 등을 이용한다. 속성 중심 디자인에서는 타입을 그대로 저장하기도 하고, 아니면 객체를 이루는 속성들에 따라 암묵적으로 정의될 수도 있다.

 

15.3.1 바이너리 객체 이미지

게임 객체들을 디스크 파일로 저장하는 방법 중에는 런타임에 메모리에 존재하는 모든 객체의 바이너리 이미지를 파일로 저장하는 방식이 있다. 

하지만 C++ 클래스 인스턴스를 바이너리 이미지로 저장하는 것은 문제가 있다. 

ㄴ 포인터와 가상 테이블을 예외적으로 처리해야 한다.

ㄴ 클래스 인스턴스 내 데이터의 endian을 처리해야 한다.

이런 이유로 객체 이미지를 저장하는 형식은 게임 객체 데이터를 저장하는데 그다지 좋은 선택이 아니다.

 

15.3.2 게임 객체 정보의 직렬화

직렬화를 사용하면 바이너리 객체 이미지를 저장하는 것보다 여러 플랫폼에 옮기기 쉽고 구현하기가 간편하다.


C#이나 자바는 모두 객체 인스턴스를 XML 텍스트 형식으로 직렬화하는 표준 방식을 지원한다. 하지만 C++은 표준화된 직렬화 기능을 지원하지는 않는다.

 

XML은 파싱이 느리다는 단점이 있어서 월드 덩어리를 읽어 들이는 시간이 느려질 수 있다. 그래서 더 빠르게 파싱할 수 있고 크기도 더 작은 자체 구현 바이너리 형식을 사용하는 게임엔진도 있다.

 

객체를 디스크에 저장하고 불러오는 직렬화 기법을 구현하는 데는 다음과 같은 기본적인 두 가지 방식 중에서 하나가 쓰인다.

 

1. 부모 클래스에 SerializeOut()과 SerializeIn() 등의 이름이 붙은 가상 함수 두 개를 넣고 상속받는 클래스는 이 함수들을 구현해 자신들의 속성을 직렬화하게 한다.

 

2. C++ 클래스에 리플렉션 시스템을 구현한다. 그런 후 리플렉션 정보가 있는 모든 C++ 객체를 자동으로 직렬화하는 제네릭 시스템을 만든다. (범용적임)

 

속성 정보에 더해 직렬화 데이터 스트림은 반드시 객체의 클래스나 타입을 나타내는 이름이나 고유 id를 포함한다. 이런 클래스 id는 디스크에서 메모리로 객체를 직렬화할 때 올바른 클래스 객체를 생성하는데 쓰인다.

 

하지만 C++에서는 이름을 나타내는 문자열이나 id만 가지고 클래스 인스턴스를 생성할 수 없다. C++에서는 이런 문제를 우회하기 위해 클래스 팩토리를 사용한다. 클래스 팩토리를 구현하는 방법은 여러 가지가 있지만, 가장 단순한 방식은 데이터 테이블을 만들어 클래스이름/id를 가지고 그 클래스 인스턴스를 생성하는 하드 코딩된 함수나 펑터로 연결해주는 것이다. id에 해당하는 함수나 펑터를 테이블에서 찾아 이것들을 호출해 클래스 인스턴스를 생성한다.

 

15.3.3 스포너와 타입 스키마

바이너리 객체 이미지와 직렬화는 게임 객체 타입의 런타임 구현에 따라 좌우되기 때문에 월드 에디터가 게임 엔진의 런타임 구현에 대해 상세히 알고 있어야 한다는 단점이 있다.

 

게임 월드 에디터와 런타임 엔진 코드 간의 결합을 끊으려면 게임 객체에 대한 정보를 구현 중립적인 방식으로 추상화하면 된다. 월드 청크 데이터 파일에 있는 모든 객체마다 작은 데이터 블록(스포너)를 저장한다. 스포너는 가볍고 데이터만 있는 게임 객체 표현으로, 런타임에 해당 게임 객체의 인스턴스를 생성하고 초기화하는 데 사용된다.

 

스포너에는 툴에서 쓰이는 게임 객체 타입에 대한 id, 속성들의 초기값을 저장한 테이블, 월드 행렬 등이 저장된다.

게임 객체를 생성(스폰)할 때 스포너의 타입을 보고 올바른 클래스 인스턴스가 생성된다. 

 

스포너는 로딩되자마자 해당하는 게임 객체를 생성하게 설정할 수도 있고 게임 실행 중 생성 요청이 올 때까지 기다릴 수도 있다.

 

스포너는 게임 월드 내의 중요 지점이나 좌표축들을 정의하는데 사용될 수도 있다.

 

15.3.3.1 객체 타입 스키마 (= 게임 객체 스키마)

스포너 방식의 디자인을 채용한 게임 월드 에디터에서는 게임 객체 타입을 나타낼 때 데이터 기반 방식의 스키마를 사용할 수 있는데, 스키마는 어떤 타입의 객체를 만들고 편집할 때 알아야 할 속성들을 정의한 것이다.

 

객체 타입 스키마를 클래스처럼 상속받는 형태로 지원하는 엔진도 있다. 객체 타입을 비롯해 런타임에 다른 게임 객체와 구분할 수 있는 고유 id는 모든 게임 객체에 있어야 하는데, 이것들은 탑 레벨의 스키마에 정의해서 다른 스키마들이 상속 받을 수 있게 한다.

 

15.3.3.2 디폴트 속성 값

게임 디자이너가 어떤 게임 객체 타입의 인스턴스를 게임 월드에 집어넣고자 할 때 수많은 속성의 값을 정해줘야 하기 때문에 스키마들의 속성들 중에 디폴트 값을 정의할 수 있는 것들이 많다면 큰 도움이 된다.

 

15.4 게임 월드의 로딩과 스트리밍

오프라인 월드 에디터와 런타임 게임 객체 모델을 연결시키려면 월드 덩어리를 메모리에 불러오고, 필요 없어지면 다시 내릴 방법이 있어야 한다.

 

게임 월드 로딩 시스템은 두 가지 중요한 역할을 맡는다.

1. 게임 월드 청크와 기타 필요한 자원들을 디스크에서 메모리로 불러 오는 파일 I/O 관리 역할

2. 자원들에 필요한 메모리를 할당하고 해제 하는 것을 관리하는 역할

 

15.4.1 단순한 레벨 로딩

가장 직관적인 게임 로딩 방식은 한 번에 오직 하나의 게임 청크만 로딩되게 하는 것이다.

ㄴ 레벨이 전환될 때 플레이어는 로딩 스크린을 보고 있어야 한다.

ㄴ 이 방식의 월드 로딩 디자인에는 스택 기반 할당자가 어울린다.

 

게임이 맨 처음 시작할 때 게임의 모든 레벨에 걸쳐 공통적으로 사용되는 자원들이 스택의 맨 아래에 불러온다. 이것을 LSR(load-andstay-resident) 데이터라고 부르며 스택에서 맨 아래 공간에 들어온다. LSR이 채워지는 공간 위의 메모리 공간에 게임 월드를 위한 데이터가 들어왔다 나갔다 한다.

 

15.4.2 심리스(Seamless : 끊김없는, 자연스러운) 로딩에 다가가기 : 에어 락

광활하고 연속된 seamless 월드를 구현하기 위한 방법이다.

 

게임 월드 자원을 위해 마련해 놓은 메모리를 똑같은 두 개의 블록으로 나누면 된다. 한쪽 블록에 레벨 A를 불러온 후 플레이어가 레벨 A에서 게임을 시작하게 하고, 그 후 스트리밍 파일 I/O 라이브러리(별도의 스레드에서 구현)를 사용해 다른 블록에 레벨 B를 불러오면 된다. 이 방식의 문제점은 월드에 할당할 수 있는 메모리가 기존의 절반으로 줄어든다는 점이다.

 

비슷하게 게임 월드 메모리를 두 개의 크기가 다른 블록으로 나누는 방법이 있다.

큰 블록에는 온전한 게임 월드 청크가 들어가고, 작은 블록은 아주 작은 청크가 겨우 들어갈 만큼의 크기를 가진다.(에어 락)

 

플레이어가 온전한 청크에서 에어 락에 들어갈 때, 플레이어는 문이나 다른 장애물 때문에 온전한 월드 지역을 보거나 다시 돌아가지 못한다. 이 와중에 온전한 청크를 메모리에서 내리고 새로운 온전한 크기의 월드 청크를 로딩한다.

 

결론적으로 에어락은 비동기 I/O를 이용해 플레이어가 로딩 스크린을 보지 않고도(게임 플레이가 끊기지 않고) 다음 단계의 월드 청크로 진행할 수 있게 해준다.

 

15.4.3 게임 월드 스트리밍

어떤 게임 디자인은 플레이어가 광활하고 이어져 있는 심리스 월드에서 플레이하고 있다는 느낌을 줘야 한다.

플레이어가 좁은 에어락 지역에 반복적으로 들어갈 필요가 없게 만들도록 해야 한다. 이를 위해 스트리밍이라는 기법을 사용한다. 

 

여러개의 월드 청크를 메모리에 동시에 올려 놓고 캐릭터가 월드 청크에 진입하게 되거나 빠져나올 때 할당, 해제하는 방식으로 스트리밍을 구현할 수도 있는데, 크기가 큰 월드 청크를 스트리밍하는 대신에 전경(foreground), 메시, 텍스처, 애니메이션 등 모든 게임 자원을 같은 크기의 블록이 되게 쪼갤 수도 있다. 그런 후에 풀 기반 메모리 할당 시스템을 사용하면 메모리 단편화 걱정 없이 필요할 때 자원 데이터를 로딩하고 내릴 수 있다.

 

15.4.3.1 불러올 자원 결정

어떤 월드가 메모리에서 내려가면 플레이어는 절대 이 월드를 볼 수 없어야 하고, 

어떤 월드가 메모리에 올라가 플레이어가 이 월드를 보기 전까지 충분한 시간을 두어야 한다.

 

15.4.4 객체 생성을 위한 메모리 관리

대부분의 게임 엔진에는 게임 객체들을 이루는 클래스들을 인스턴싱하고 필요 없어지면 파괴하는 일을 담당하는 게임 객체 생성(스포닝) 시스템이 있다.

동적 할당은 느린 과정이기 때문에 효율적으로 만들어야 한다.(메모리 단편화를 피해야 한다.)

 

15.4.4.1 객체 생성을 위한 오프라인 메모리 할당 사용

ㄴ 게임플레이 도중에 동적인 메모리 할당을 금지하는 방법

ㄴ 게임 객체들이 사용할 메모리를 월드 에디터가 오프라인에 할당할 수 있고, 월드 청크 데이터 안에 포함시킬 수 있다.

ㄴ 한 청크 안의 모든 게임 객체들이 사용하는 메모리 양이 미리 계산될 수 있기 때문에 메모리 단편화를 방지할 수 있다.

ㄴ 동적 객체 생성을 흉내내기 위해 월드에 필요한 수 만큼의 객체를 할당한 후 객체의 상태를 잠들고 깨우는 방식을 사용한다.

 

15.4.4.2 객체 생성을 위한 동적인 메모리 관리

타입이 다른 게임 객체들은 차지하는 메모리 크기가 다르기 때문에 흔히 많이 쓰이는 단편화 없는 풀 할당자를 사용할 수 없게 된다.

게임 객체들은 보통 생성된 순서와 상관 없이 파괴되기 때문에 스택 할당자도 사용할 수 없다.

남은 것은 힙 할당자 뿐인데 이는 단편화에 취약하다. 그래도 대안이 몇가지 있다.

 

1. 객체 타입마다 하나의 메모리 풀 사용

ㄴ 여러 풀을 관리해야 한다.

ㄴ 각 객체 타입별로 얼마나 많은 수를 사용할지 경험적으로 유추해야 한다.

 

2. 작은 메모리 할당자

ㄴ 관리해야 할 풀의 종류는 줄어들지만 풀마다 잠재적으로 낭비하는 메모리가 있을 수 있다.

ㄴ 할당 단위 크기가 두배로 늘어나는 메모리 풀들을 만들어 사용할 수 있다.

ㄴ 게임 객체를 위해 메모리를 할당할 때, 할당 단위가 객체의 크기와 같거나 이보다 큰 풀들 중에서 가장 작은 것을 찾는다.

 

3. 메모리 재배치

ㄴ 5.2.2.2 내용

 

15.4.5 게임 저장

월드 로딩 시스템과 저장 시스템은 비슷하지만 요구 조건이 다르기 때문에 따로 구현된다.

월드 청크는 월드 안에 있는 모든 동적 객체의 초기 상태를 담고 있고, 이에 더해 모든 정적 월드 요소에 대한 완전한 정보도 담는다.

배경 메시와 충돌 데이터 등의 정적 정보는 디스크 공간을 많이 차지하는 경우가 보통이기 때문에 월드 청크는 여러 파일로 구성되는 경우도 있고 연관된 데이터의 크기도 대개 크다.

 

게임 저장파일도 게임 객체의 현재 상태를 저장해야 하지만, 월드 청크에서 알 수 있는 정보들은 중복 저장할 필요가 없다.

그래서 게임 저장 파일은 월드 청크 파일보다는 훨씬 작으며, 데이터 압축과 생략에 더 중점을 둘 수 있다.

 

15.4.5.1 체크 포인트

체크 포인트라고 불리는 특정한 지점에서만 저장할 수 있게 하는 방식

체크 포인트 주변의 월드 청크에 게임의 상태가 저장된다.

어느 누가 플레이하든 데이터가 거의 비슷하기 때문에 따로 저장할 데이터가 적다. = 저장 파일의 크기가 작다.

 

15.4.5.2 자유 저장

게임 플레이 도중에 언제든 게임 상태를 저장할 수 있는 기능

게임 저장 파일의 크기가 커진다.

게임 저장 파일의 내용이 월드의 정적인 요소들만 빼고 월드 청크의 정보와 비슷하다.

 

15.5 객체 참조와 월드 질의

게임 객체의 id는 게임 내의 객체들을 구분하거나 런타임 검색, 객체 간 통신에서 수신자 명시 등에 쓰인다. 월드 에디터 툴에서도 검색과 구분을 위해 흔히 쓰인다.

 

런타임에는 게임 객체들을 검색할 수 있는 기능이 반드시 있어야 한다. 고유 id로 객체를 찾는 경우도 있고, 객체 타입이나 아니면 다른 조건으로 검색하는 경우도 있다.

 

질의를 통해 찾은 객체는 포인터, 핸들, 스마트 포인터를 이용해 참조를 구현할 수 있다.

 

15.5.1 포인터

객체 참조를 구현하는데 가장 빠르고 효율적이고 다루기도 쉽지만, 프로그래머에게 포인터 관리에 대한 부담이 클 수 있다. (실수하기 쉽다)

 

15.5.2 스마트 포인터

평상시에는 여느 포인터처럼 동작하지만 C/C++ 네이티브 포인터의 문제들은 해결한 작은 객체를 스마트 포인터라고 부른다.

포인터 역참조 기능을 지원하도록 *와 -> 연산자를 오버로딩해 원래 메모리 주소를 리턴한다.

 

ㄴ 3.1.3.6 내용

 

15.5.3 핸들

핸들 테이블에는 객체에 대한 포인터가 담겨 있다. 핸들은 이 테이블에 대한 인덱스일 뿐이다.

핸들의 문제점은 낡은 객체를 참조할 가능성이 있다는 점이다.

이 문제를 해결하려면 모든 핸들에 객체의 고유 id를 포함하게 만들면 된다.

 

// 핸들을 사용하여 객체를 참조할 때의 객체 클래스
class GameObject
{
private:
    //..
    GameObjectId m_uniqeud; // 객체의 고유 id
    U32 m_handleIndex; // 핸들 생성을 빠르게 하기 위해
    friend class GameObjectHandle; // id와 index에 접근할 수 있게
    
public:
    GameObject()
    {
        // 고유 id는 월드 에디터에서 올 수도 있고, 
        // 아니면 런타임에 동적으로 할당할 수도 있다.
        
        m_uniqueId = AssignUniqueObjectId();
        
        // 핸들 테이블에서 비어있는 첫 번째 슬롯을 찾아 핸들 인덱스를 얻어온다.
        m_handleIndex = FindFreeSlotInHandleTable();
    
    }
};
    
    // 글로벌 핸들 테이블
static GameObject* g_apGameObject[MAX_GAME_OBJECTS];
    
// 간단한 객체 핸들 클래스
class GameObjectHandle
{
private:
    U32 m_handleIndex; // 핸들 테이블의 인덱스
    GameObjectId m_uniquewId; // 낡은 핸들을 방지하기 위한 고유 id

public:
    explicit GameObjectHandle (GameObject& object):
        m_handleIndex(object.m_handleIndex),
        m_uniqueId(object.m_uniqueId)
        { }
    
    // 핸들을 역참조하는 함수
    GameObject* ToObject() const
    {
        GameObject* pObject = g_apGameObject[m_handleIndex];
        if(pObject != NULL && pObject -> m_uniqueId == m_uniqueId)
        {
            return pObject;
        }
        
        return NULL;
    }
};

 

15.5.4 게임 객체 질의

게임 개발 중에 어떤 타입의 질의가 가장 많이 필요할지를 미리 결정한 다음 그 질의를 수행하는데 가장 효율적인 자료구조를 구현한다.

 

고유 id로 게임 객체 찾기

ㄴ 게임 객체를 가리키는 포인터나 핸들을 해시 테이블에 넣거나 이진 검색 트리에 넣고 고유 id로 찾는다.

 

특정 조건을 충족하는 모든 게임 객체를 순회하기

ㄴ 게임 객체들을 여러 조건에 맞춰 정렬한 후 연결 리스트에 미리 저장해 둔다.

ㄴ 예를 들면 특정 타입의 모든 게임 객체를 리스트로 만들거나 플레이어로부터 특정 범위 안에 있는 모든 객체를 리스트로 만들 수 있다.

 

발사체의 이동 경로에 있거나 어떤 목표 지점까지의 위에 있는 모든 객체를 찾기

ㄴ 보통 충돌 시스템을 활용한다. 충돌 시스템은 레이 캐스트나 형상 캐스트를 통해 충돌하는 물체를 빠르게 찾아낼 수 있다.

 

15.6 실시간 게임 객체 업데이트

대부분의 로우레벨 엔진 하부 시스템(렌더링, 애니메이션, 충돌, 물리, 오디오 등)은 주기적으로 업데이트해야 하는데, 게임 객체도 마찬가지다.  모든 게임 엔진은 메인 게임 루프의 일부분에서 게임 객체 상태를 업데이트 한다고 말할 수 있다. 

 

15.6.1 단순한 접근 방식(하지만 동작하지 않는 방식)

게임 객체들 전체의 상태를 업데이트하는 가장 단순한 방법은 순회하면서 가상 함수, 즉 Update() 같은 함수를 모든 객체마다 차례대로 호출하는 것이다.

게임 객체들은 Update() 함수를 오버라이딩하고 이전 프레임과의 시간차를 인자로 전달 받는다.

 

모든 게임 객체를 아우르는 집합을 어떻게 관리할 것인가?

Update() 함수가 해야 할 일은 어떤 것인가?

 

15.6.1.1 활성 게임 객체의 집합 관리

활성(active) 게임 객체를 관리하는 데는 주로 싱글턴 매니저 클래스를 쓰는 경우가 많으며, 대개 GameWorld나 GameObjectManager 등의 이름을 갖는다.

 

게임 객체들은 게임이 진행되면서 생성되고 파괴되기 때문에 게임 객체의 집합은 보통 동적인 경우가 대부분이다.

대부분의 게임 객체는 게임 객체를 관리하는 데 단순한 연결 리스트보다는 좀 더 복잡한 자료 구조를 사용한다.

 

15.6.2 성능 조건과 일괄 업데이트

대부분의 상용 게임 엔진들은 게임 루프에서 각 엔진 하부 시스템을 직접 혹은 간접적으로 업데이트하며, 게임 객체의 Update() 함수 안에서 하지 않는다.

게임 객체가 어떤 하부 시스템을 이용할 일이 있으면 직접 하부 시스템에 요청해 자신을 위한 상태 정보를 할당하게 만들 수 있다.

 

virtual void Tank::Update(float dt)
{
    // 탱크의 상태를 업데이트 한다.
    MoveTank(dt);
    DeflectTurret(dt);
    FireIfNecessary();
    
    // 여러 엔진 하부 시스템의 속성을 설정하지만 
    // 직접 업데이트 하지는 않는다.
    
    // 조건에 따라 객체에 대한 하부 시스템의 속성을 다르게 설정
    if(justExploded)
    {
        m_pAnimationComponent-> PlayAnimation("explode");
    }
    
    if(isVisible)
    {
        m_pCollisionComponent->Activate();
    }
    else
    {
        m_pCollisionComponent->Deactivate();
    }
}

// 게임 루프
while (true)
{
    PollJoypad();
    float dt = g_gameClock.CalculateDeltaTime();
    
    // 모든 게임 객체의 update() 
    // 여러 엔진 하부 시스템들의 속성들만 설정
    for (each gameObject)
    {
     	gameObject.Update(dt);   
	}
    
    // 하부 시스템들의 일괄 업데이트
    g_animationEngine.Update(dt);
    g_physicsEngine.Simulate(dt);
    g_collisionEngine.DetectAndResolveCollision(dt);
    g_audioEngine.Update(dt);
    g_renderingEngine.RenderFrameAndSwapBuffers();
}

일괄 업데이트가 가져올 수 있는 성능 이득

 

1. 최대의 캐시 일관성

ㄴ 객체들의 데이터를 연속된 RAM 공간에 모을 수 있다.

 

2. 계산 중복 최소화

ㄴ 공통적인 연산은 한 번만 하고 매 객체는 다시 이것을 계산하기보다는 계산된 것을 가져와 활용하면 된다.

 

3. 자원 재할당 감소

ㄴ 엔진 하부 시스템들은 업데이트하는 동안 메모리를 비롯한 자원을 할당하고 관리하는 일을 하는 경우가 많은데, 일괄 업데이트를 사용하면 프레임에서 한 번만 자원을 할당한 후 모든 객체에서 돌아가며 재활용할 수 있다.

 

4. 효율적인 파이프라인화

ㄴ 엔진 하부 시스템들은 게임 월드에 있는 모든 객체마다 근본적으로 동일한 연산을 수행하는 경우가 많다. 일괄 업데이트를 이용하면 새로운 최적화를 가능하게 할 수도 있고 전용 하드웨어 자원을 활용하는 것도 가능하다.

 

동적 강체들이 존재하는 시스템에서 충돌을 해결할 때 객체 간의 교차를 해결하려면 객체들을 그룹으로 처리해야 한다. 일괄 업데이트는 이런 면에서도 필수적이다.

 

15.6.3 객체와 하부 시스템 간 상호 의존

객체간 의존 관계가 있거나 엔진 하부 시스템끼리 의존 관계가 있는 경우 업데이트의 순서를 적절하게 조절해야 한다.

 

15.6.3.1 단계적 업데이트

하부 시스템 간 의존성을 제대로 처리하려면 메인 게임 루프 안에서 하부 시스템들이 올바른 순서대로 업데이트하도록 명확하게 코드를 짜면 된다. 예를 들어 래그 돌 물리 시스템이 상호작용하는 과정을 올바로 처리하려면 다음과 같이 코드를 짤 수 있다.

while(true) // 메인 게임 루프
{
    // 
    /* 모든 객체의 상태 설정 */
    //
    
    g_animationEngine.CalculateIntermediatePoses(dt); // 중간 단계의 로컬 공간 뼈대 포즈를 계산
    g_ragdollSystem.ApplySkeletonsToRagDolls(); // 월드 공간으로 다시 계산하여 물리 시스템 내의 연결된 강체들에 적용
    g_physicsEngine.Simulate(dt); // 래그 돌의 물리 시뮬레이션 동작
    g_collisionEngine.DetectAndResolveCollisions(dt);
    g_ragdollSystem.ApplyRagDollsToSkeletons(); 
    g_animationEngine.FinalizePosAndMatrixPalette(); // 최종 월드 공간 포즈를 계산하고 스키닝 행렬 팔레트를 생성
    
    // ..
}

 

이렇게 게임 객체들이 다양한 엔진 하부 시스템의 중간 결과에 의존하는 경우 게임 객체들의 상태를 언제 업데이트 해야 하는지를 고민해야 한다.

 

15.6.3.2 버킷 업데이트

객체간 의존성이 있는 경우 문제가 생길 수 있다.

 

객체 B가 객체 A의 손에 들려 있어서 A가 완전히 업데이트 된 후(최종 월드 공간 포즈와 행렬 팔레트까지 계산이 끝난 후)에야 B를 업데이트 할 수 있다고 가정한다.

 

해결하는 방법은 객체들의 의존성 트리를 만들어 의존이 없는 객체(첫번 째 버킷) 들의 그룹(버킷)들의 완전한 게임 객체 업데이트 및 엔진 시스템을 업데이트 한 뒤 차례대로 버킷을 업데이트 하는 것이다.

 

의존성 트리들의 깊이를 제한하는 게임이 많다. (고정된 수의 버킷을 쓰기 위해서)

 

15.6.3.3 불완전한 객체 상태와 한 프레임 차 랙

게임 객체의 상태는 순차적으로 설정되기 때문에 루프 중간에 프로그램을 잠시 멈추면 어떤 객체들은 업데이트 되지 않아, S(t₁)로, 어떤 객체들은 업데이트 되어 S(t₁ + Δt)의 상태를 갖게 된다. 또한 루프의 어느 지점이느냐에 따라 객체들이 모두 일부만 업데이트된 상태일 수도 있다. 예를 들어 애니메이션 포즈 블렌딩은 이미 수행했지만, 물리와 충돌 과정은 아직 계산되지 않았을 수도 있다.

 

게임 객체들의 상태는 업데이트 루프 전과 후에는 일관되지만 루프 도중에는 일관되지 않을 수도 있다는 뜻이다.

 

문제는 주로 업데이트 루프 안에서 게임 객체들이 서로의 상태 정보를 질의할 때 발생한다.

이런 문제를 해결하려면 게임 객체를 버킷으로 그룹 짓는 방법도 있지만 객체 상태 캐싱이라는 기법을 사용할 수도 있다.

 

15.6.3.4 객체 상태 캐싱

새로운 객체 상태 벡터 S(t₂)를 계산하기 전에 이전 객테 상태 벡터 S(t₁)을 복사(캐싱)해 놓게 만드는 방법이다.

ㄴ 이전 객체이기 때문에 업데이트 순서에 상관이 없고 일관성이 있다.

ㄴ 이전 시각과 현재 시각 사이의 아무 때라도 두 상태를 선형 보간하여 얻을 수 있다. (하복의 물리엔진은 모든 강체의 이전 상태와 현재 상태를 모두 유지한다.)

 

단점

ㄴ 상태를 엎어 쓰는 방식에 비해 메모리를 두 배 더 차지한다.

ㄴ 이전 시각 t₁의 상태는 일관되지만 현재 시각 t₂의 상태는 일관성 문제가 발생할 수 있다.

 

15.6.4 병렬성을 위한 디자인

게임 객체 상태를 업데이트 하는 방식에 어떤 방식으로 병렬화를 사용할 것인가?

 

 

15.6.4.1 게임 객체 모델 자체를 병렬화

게임 객체 모델은 병렬화하기 어렵다.

ㄴ 게임 객체들은 서로 의존성이 강하다.

 

게임 객체 모델을 병렬화 한다면

ㄴ 모든 게임 객체들은 다른 게임 객체들을 참조할 수 없어야 한다. 모든 객체 간 통신은 메세지 전달 방식을 써야 하며, 객체들이 완전히 다른 메모리 영역에 있거나 물리적으로 분리된 CPU 코어에서 처리되고 있는 상황에서도 효율적인 객체 간 메세지 전달 시스템을 구현해야 한다.

 

15.6.4.2 병행 구조인 엔진 하부 시스템과의 인터페이스

일반적으로는 게임 객체 모델을 병렬화하는 방법보다는 로우레벨 엔진 시스템들을 병렬화하는 데 노력을 기울인다.

ㄴ 로우레벨 하부 시스템이 게임 객체 모델 처리보다 CPU를 더 많이 쓰기 때문

 

싱글 스레드 게임 객체 모델을 사용하더라도 병렬적으로 실행되는 엔진 하부 시스템과 상호작용을 해야 한다.

ㄴ 게임 객체들을 업데이트 할 때 블로킹 함수를 호출하면 안 된다.

ㄴ 작업 요청을 가장 일찍 보낼 수 있는 때는 언제인가? (일찍 보내야 필요할 때 완료되어있을 가능성이 커진다.)

ㄴ 작업 요청을 보내고 얼마나 오래 기다릴 수 있는가? (업데이트 루프 뒷부분까지 기다리거나 몇 프레임 랙이 생겨도 상관 없어서 이전 프레임의 결과로 현재 프레임의 객체 상태를 업데이트 할 수도 있다.) 

 

15.7 이벤트와 메세지 전달

게임은 본질적으로 이벤트 주도 방식으로 돌아간다.

이벤트가 발생했음을 연관있는 게임 객체들에게 알려주고, 그 객체들이 이벤트에 반응할 수 있게 도와 줄 방법이 필요하다.

15.7.1 정적 타입 함수 바인딩의 문제점

게임 객체에게 이벤트가 발생했음을 알리는 가장 간단한 방법은 그 객체의 멤버함수를 부르는 것이다.

 

정적 타입 가상 함수를 이벤트 핸들러로 사용하려면 베이스 클래스 GameObject에는 게임에서 발생 가능한 모든 이벤트 핸들러를 가상 함수로 선언해야 한다. (데이터 주도 방식으로 이벤트를 생성할 수 없다.)

 

이벤트에 관심이 있는 일부 객체 타입이나 일부 인스턴스들만 그 이벤트를 받게 하고 싶어도 방법이 없다.

 

동적 타입 함수 바인딩을 사용하면 된다. 이 기능을 기본적으로 지원하는 프로그래밍 언어도 있다. (C#) 그렇지 않은 경우에는 직접 코드를 짜야 한다. 

 

보통 데이터 주도형 접근 방식으로 구현한다. (함수 호출 개념을 객체로 캡슐화한 후 이것을 런타임에 전달하는 방식)

 

15.7.2 이벤트를 객체에 캡슐화

이벤트는 두 가지 구성 요소로 이뤄진다. 하나는 타입(폭발, 아군의 부상, 체력 아이템을 집어 들기) 이고, 다른 하나는 전달 인자이다.

 

공통된 루트 이벤트 클래스를 상속하여 여러 다른 타입의 이벤트를 구현하기도 한다.

 

이벤트 (메세지) 를 객체로 캡슐화 하면 여러 이득이 있다.

ㄴ 단일 이벤트 핸들러 함수 : 모든 타입의 이벤트를 처리할 가상 함수 한 개만 있으면 된다. (virtual void OnEvent(Event& event);)

ㄴ 영속성 : 함수 호출에서는 함수가 리턴하고 나면 인자들을 사용할 수 없지만 이벤트 객체는 타입과 전달 인자를 데이터로 저장하기 때문에 영속성이 있다.

ㄴ 이벤트 전달의 임의성 : 객체는 연관된 객체에 이벤트를 전달할 수 있는데 전달하는 객체는 이벤트에 대해 몰라도 된다.

 

struct Event
{
    const U32 MAX_ARGS = 8;
    EventType m_type;
    U32 m_numArgs;
    EventArg m_aArgs[MAX_ARGS];
};

 

이벤트/메세지/명령을 객체로 캡슐화 한다는 개념은 명령 패턴이라고 불린다.

https://lemonyun.tistory.com/66

 

2. 디자인 패턴 - 명령(command)

명령 패턴 - 간단하게 정리하자면 함수 호출을 매개변수화 한 것? 콜백 함수, 함수 포인터를 사용하여 구현할 수 있다. 2.1 입력키 변경 버튼을 눌렀을때 특정 메서드(행동)가 수행하도록 하는 코

lemonyun.tistory.com

 

15.7.3 이벤트 타입

// 이벤트 타입을 열거형 타입으로 정의해 구분하는 방법
enum EventType
{
    EVENT_TYPE_LEVEL_STARTED,
    EVENT_TYPE_PLAYER_SPAWNED,
    EVENT_TYPE_ENEMY_SPOTTED,
    // ...

};

이 방식의 문제점

1. 게임 전체에 쓰이는 모든 이벤트 타입에 대한 정보가 한 곳에 집중돼 있음

2. 이벤트 타입이 하드 코딩되어 있기 때문에 데이터 주도 방식으로 쉽게 추가할 수 없음

3. 열거형 값은 단순한 인덱스에 불과해서 순서에 영향을 받는다.

ㄴ 이벤트 타입을 문자열로 인코딩하는 방법을 사용하면 해결할 수 있다.

 

15.7.4 이벤트 전달 인자

이벤트 전달 인자를 구현하는 방법

1. 각 이벤트 타입마다 새 이벤트 클래스를 상속하고 전달 인자는 새 이벤트 클래스의 데이터 멤버로 하드 코딩

 

2. variant의 집합으로 저장하는 방법

ㄴ 고정 크기의 작은 배열로 구현

ㄴ 동적 크기 배열이나 연결 리스트를 이용해 구현

 

3. 키-값 쌍을 이벤트 전달 인자로 사용

ㄴ 이벤트를 보내는 쪽과 받는 쪽에서 전달 인자의 순서를 정확하게 알고 있어야 하기 때문에 등장한 개념(버그가 발생하기 쉬움)

 

15.7.5 이벤트 핸들러 (OnEvent 함수)

게임 객체가 이벤트를 받으면 그 이벤트에 반응해야 한다. 이 과정을 이벤트를 핸들링한다고 하고, 보통 이벤트 핸들러라고 부르는 함수나 스크립트 코드에서 처리한다.

 

이벤트 핸들러는 대부분 모든 타입의 이벤트를 처리할 수 있는 네이티브 가상 함수나 스크립트 함수로 구현하는 경우가 많다.

 

virtual void SomeObject::OnEvent(Event& event)
{
    switch (event.GetType())
    {
    case EVENT_ATTACK:
        RespondToAttack(event.GetAttackInfo());
        break;
    case EVENT_HEALTH_PACK:
        AddHealth(event.GetHealthPack().GetHealth());
        break;
    //...
    
    //이 객체에서 처리하지 않는 이벤트
    default:
        break;
    }
}

 

15.7.7 책임 연쇄 패턴

게임 객체들끼리는 거의 항상 의존성이 있다.

일반적으로 게임 객체들 간의 상호 연관성은 하나 이상의 관계 그래프로 생각할 수 있다.

이와 같은 관계 그래프에서 연결된 객체들 간에 당연히 이벤트를 전달할 수 있어야 한다. 여러 개의 구성요소로 이뤄진 게임 객체가 이벤트를 받으면 구성 요소 모두에 이벤트를 전달해 처리할 기회를 줘야 하는 경우도 있다.

 

객체들로 이뤄진 그래프 내에서 이벤트를 전달하는 기법은 책임 연쇄 패턴이라고 불리기도 한다.

이벤트가 전달되는 순서는 보통 엔지니어가 정하며, 이벤트의 체인이 첫 번째 객체에 전달하면 그 객체의 이벤트 핸들러에서 처리될 수 있는지 판단한다. (switch문의 case에 걸리지 않으면 다음 객체에 전달)

 

15.7.8 관심 이벤트 등록

이벤트를 브로드캐스팅하면 모든 게임객체의 이벤트 핸들러를 호출해야 한다. (비효율적)

 

게임 객체들이 특정 이벤트에 관심 등록을 할 수 있게 하는 방법

1. 이벤트 타입마다 관심 등록한 게임 객체를 연결 리스트로 두기

2. 게임 객체에 이벤트를 나타내는 비트 배열을 두고 관심 있는 것만 비트를 켜는 방법

 

15.7.9 이벤트 큐 사용 여부

15.7.9.1 이벤트 큐 방식의 몇 가지 장점

 

이벤트 처리 시점 제어

ㄴ 이벤트들은 게임 루프 안에서 어느 시점에 처리되는지에 민감한 영향을 받는 경우가 있다.

 

미래에 이벤트를 보내기

ㄴ 큐에 넣기 전에 원하는 전달 시각을 이벤트에 기록한다. 그리고 현재의 게임 클록이 이벤트의 전달 시각과 같거나 이미 지났을 경우만 이벤트를 처리하게 한다. 큐의 이벤트들을 전달 시각이 이른 순으로 정렬해 놓으면 이런 식으로 동작하도록 구현하기 수월하다.

 

이벤트 우선 순위

ㄴ 여러 이벤트가 같은 전달 시각을 갖는 경우 이벤트의 우선순위 레벨을 주어 해결할 수 있다.

 

15.7.9.2 이벤트 큐 방식의 문제점

이벤트 시스템의 복잡도 증가 (구현 난이도, 유지보수 비용 증가)

이벤트와 전달 인자를 깊은 복사해야 하는 문제

깊은 복사는 동적 메모리 할당(잠재적인 속도 저하와 메모리 단편화 문제)를 해야 할 수 도있게 만든다.

 

15.7.10 이벤트를 즉시 보낼 때의 문제점 (큐를 사용하지 않는 경우의 문제점)

엄청나게 깊은 콜 스택을 유발할 수 있다.

 

15.7.11 데이터 주도 이벤트/메세지 전달 시스템

이벤트 시스템을 데이터 주도 방식으로 구현하면 이벤트를 보내고 받는 과정을 수정할 권한을 디자이너에게 줄 수 있다.

 

월드 에디터에서 디자이너는 객체를 선택하고 그 객체가 특정 이벤트에 어떻게 반응할지 (어떤 핸들러를 사용할지) 설정할 수 있다.

엔진에서 게임 디자이너가 쓸 수 있게 간단한 스크립트 언어를 지원하는 형태가 있다. 디자이너는 특정 타입의 객체가 특정 타입의 이벤트에 어떻게 반응할지를 코드로 짤 수 있다.

 

15.7.11.1 데이터 통로 통신 시스템

모든 게임 객체들이 데이터 스트림을 연결할 수 있는 입력 포트를 하나 이상 갖고, 다른 객체들에 데이터를 보낼 출력 포트를 하나 이상 갖는다.

 

디자이너가 원하는 행동을 구현할때 GUI를 이용해 포트들을 연결할 수 있다.

 

15.8 스크립트

스크립트는 바이트코드 패턴과 연관이 있다.

https://lemonyun.tistory.com/75

 

11. 행동 패턴 - 바이트코드

가상 머신 명령어를 인코딩한 데이터로 행동을 표현할 수 있는 유연함을 제공한다. 큰 규모의 프로젝트는 빌드에 시간이 오래 걸릴 수 있기 때문에, 자주 고칠 수 있어야 하는 코드는 데이터 파

lemonyun.tistory.com

15.8.1 런타임 언어와 데이터 정의 언어

스크립트 언어가 사용되는 용도

1. 데이터 정의 언어

ㄴ 나중에 엔진에서 사용될 새 자료구조를 만들고 채워 넣는 것을 도와주는 것이 주된 목적

 

2. 런타임 스크립트 언어

ㄴ 런타임에 엔진 안에서 실행되는 스크립트 언어로, 대개 하드코딩된 엔진의 게임 객체 모델이나 다른 엔진 시스템을 확장하고 커스터마이즈하는데 쓰인다.

 

15.8.2.1 게임 스크립트 언어의 일반적 특성

 

인터프리트 방식

ㄴ 유연성, 이식성, 빠른 반복 생산을 위한 선택이다.

ㄴ 코드가 직접 CPU에서 실행되는 것이 아니라 가상 머신에서 실행되기 때문에 스크립트 코드를 실행하는 방법과 시점을 엔진에서 능동적으로 조정할 수 있다.

 

가벼움

ㄴ 가상머신은 메모리를 적게 먹는다.

 

편리성과 사용 편의성

ㄴ 스크립트 언어는 대개 특정 게임의 요구 사항에 맞게 커스터마이즈해 사용한다.

 

15.8.3 널리 쓰이는 게임 스크립터 언어

상용 혹은 오픈소스 언어를 고쳐 쓸 것인가 아니면 처음부터 새로 만들 것인가?

 

대부분의 경우 어느 정도 잘 알려지고 안정된 스크립트 언어를 골라 필요한 기능들을 추가해 가는 편이 더 편리하다.

 

15.8.3.1 QuakeC

퀘이크 엔진을 위해 만든 커스텀 스크립트 언어

 

15.8.3.2 언리얼스크립트

C++와 문법 구조가 유사하고 클래스, 지역변수, 루프, 배열, 구조체 등 C와 C++ 프로그래머가 익숙한 대부분의 개념을 지원한다.

 

클래스 계층 구조를 확장할 수 있는 능력

레이턴스 함수

언리얼에디터와의 편리한 연결

멀티플레이어 게임을 위한 네트워크 리플리케이션

 

15.8.3.3 루아

게임 스크립트 언어로서 제일 많이 쓰인다고 한다.

 

15.8.3.4 파이썬

15.8.3.5 폰/스몰/스몰-C

 

15.8.4 스크립트의 구조

스크립트 콜백

ㄴ 게임 루프에서 객체를 업데이트할 때 스크립트로 작성된 콜백 함수를 엔진에서 호출할 수 있다. 이를 통해 시간에 따른 게임 객체를 어떻게 업데이트 할 것인지 커스터마이징 할 수 있다.

 

스크립트로 짠 이벤트 핸들러

 

스크립트로 게임 객체 타입을 확장하거나 새 타입을 정의하기

 

스크립트로 구성 요소 및 속성 정의하기

 

스크립트 주도 엔진 시스템

ㄴ 게임 객체 모델 전부를 스크립트로 짜고, 네이티브 엔진 코드는 로우레벨 엔진 구성 요소를 사용해야 할 때만 호출하는 방식으로 구현한다.

 

스크립트 주도 게임

 

15.8.5 런타임 게임 스크립트 언어의 기능

대다수 게임에서 스크립트 언어를 사용하는 주된 이유는 게임플레이 기능을 구현하는 것이고, 이것은 게임의 객체 모델을 보강하거나 커스터마이즈하는 형태로 이뤄지는 경우가 일반적이다.

 

 

15.8.5.1 네이티브 프로그래밍 언어와의 인터페이스

게임 엔진이 스크립트 코드를 실행할 수 있어야 하고, 마찬가지로 스크립트 코드가 엔진에서의 작업을 시작하게 할 수 있어야 한다.

 

보통 런타임 스크립트 언어의 가상 머신은 게임 엔진 안에 들어간다. 엔진이 가상 머신을 초기화하고 필요한 때에 스크립트 코드를 실행하며, 스크립트가 실행되는 과정을 관리한다.

 

함수형 스크립트 언어의 주요 실행 단위는 함수다. 함수 이름이 위치한 바이트 코드를 찾은 후 가상 머신을 생성해 코드를 실행한다.

 

객체지향 스크립트 언어에서는 클래스가 주요 실행 단위다. 이런 시스템에서는 객체를 생성하거나 파괴하는 것이 가능하고, 각 클래스 인스턴스의 메서드를 호출할 수 있다.

 

15.8.5.2 게임 객체 핸들

 

스크립트 함수가 게임 객체를 다뤄야 하는 때도 있는데 스크립트 코드에서 게임 객체를 참조할 방법은 여러가지가 있다.

임의의 숫자로 이뤄진 핸들을 사용하는 방법이 있다. (혹은 문자열, 해시 문자열 id)

 

15.8.5.3 이벤트 받기와 처리

이벤트는 보통 개별 객체에 전달되고 그 객체 내에서 처리된다. 그렇기 때문에 스크립트로 짠 이벤트 핸들러를 어떤 식으로든 객체와 연관 지을 방법이 있어야 한다.

 

15.8.5.4 이벤트 보내기

 

15.8.5.5 객체지향 스크립트 언어

 

15.8.5.6 스크립트로 짠 유한 상태 기계

 

15.8.5.7 스크립트 멀티스레드

'읽은 책 > 게임 엔진 아키텍처' 카테고리의 다른 글

14. 게임플레이 시스템의 소개  (0) 2022.08.17
12. 충돌과 강체 역학  (0) 2022.08.17
11. 애니메이션 시스템  (0) 2022.08.13
10. 렌더링 엔진  (0) 2022.08.12
9. 디버깅과 개발 도구  (0) 2022.08.10

+ Recent posts