이 글은 조만간 출판될 Game Programming Gems 3 한국어판(류광 역, 정보문화사)의 일부입니다. 이 글에 대한 모든 권리는 정보문화사가 가지고 있으며, 사전 허락 없이는 웹 사이트 게시를 비롯한 어떠한 형태의 재배포도 금지되어 있습니다.

이 글은 최종 교정 전의 상태이므로 오타나 오역이 있을 수 있습니다. 또한 웹 페이지의 한계 상, 실제로 종이에 인쇄된 형태와는 다를 수 있습니다. 실제 책에서는 표나 수식이 좀 더 정확한 형태로 표시될 것이며 그림/도표 안의 영문도 적절히 한글화될 것입니다.

Game Programming Gem 3 한국어판에 대한 정보는 GPG 스터디(http://www.gpgstudy.com/)에서 얻을 수 있습니다.

1.2 객체 조합식 게임 프레임웍

Scott Patterson, Next Generation Entertainment
scottp_at_tonebyte.com

이 글에서는 객체 조합(object composition)에 기반한 게임 프레임웍의 설계에 대해 소개하고, 그러한 프레임웍이 지닌 장점과 설계 철학을 설명한다. 게임에 필요한 작업의 구현에 이런 종류의 프레임웍이 유용한 이유들을 살펴보게 될 것이다.

이 게임 프레임웍을 독자 자신의 게임 시스템을 위한 하나의 참고로 사용할 수 있을 것이다. 이 프레임웍을 기반으로 해서 게임에 필요한 기능들을 갖춘 새로운 시스템과 게임에 필요한 동작들을 수행하는 새로운 작업들을 만들어내는 것이 가능하다.

프로그래밍 프레임웍(framework)이라고 하는 것은, 특정한 서비스들을 제공하기 위해 협동적으로 작동하는 객체들을 담는 커다란 틀로써의 시스템을 이르는 말이다. 응용 프로그램 프레임웍은 응용 프로그램을 만들어내는 데 필요한 서비스들을 제공하는 클래스들의 모음이다. 이 글의 목표는 게임 응용 프로그램의 제작에 도움이 되는 프레임웍은 어떤 것이며, 또 어떻게 설계해야 하는 지를 밝히는 것이다.

응용 프로그램을 만들 때 프레임웍이 유용한 이유는 여러 가지인데, 기본적으로는 쓸만한 프로그램을 매우 빠르게 만들어 낼 수 있다는 점을 들 수 있다. 시간의 단축이 곧 비용의 절감을 의미함은 이미 모두 아는 사실이다. 일반적으로 프레임웍들은 내장 기능들, 일관성있는 행동과 구조, 객체의 접근과 소유, 수명에 대한 잘 알려진 규칙들을 담고 있다.

이 글에서는 우선 게임 개발에 필요한 작업들을 짚어보기 위해 게임 개발의 단계들을 요약한다. 그런 다음에는 게임 프레임웍의 설계 문제들을 논의한다. 마지막으로는 CD-ROM에 수록된 게임 프레임웍 구현을 개괄적으로 소개한다.

게임 개발 단계들

게임 프레임웍의 필요성은 개발 단계마다 조금씩 다를 수 있다. 표 1.2.1은 전형적인 게임 개발 단계들과 각 단계에서의 일반적인 목표를 정리한 것이다.

표 1.2.1 게임 개발 과정에서 전형적인 목표들

 단계  목표 
 개념  기능성 및 미학의 설계. 캐릭터, 스토리, 미션 개념들을 고안. 
 원형  핵심 게임플레이 요소들을 보여주는 ‘개념 검증’ 데모 작성. 기술 시연을 위한 데모 작성. 
 플레이가능  적어도 하나의 미션 또는 레벨을 처음부터 끝까지 플레이할 수 있는 데모 작성. 
 제작  모든 미션과 레벨에 대한 설계 및 구현을 완성. 
 통합  다양한 게임 모드와 화면들의 통합. 스토리 섹션, 튜토리얼 모드, 미션/레벨 선택, 승리/패배, 상태/점수, 저장/로드, 정지/재시작, 옵션/설정 등. 
 테스트  설계 및 구현 문제 해결. 호환성 문제 해결. 다른 드라이버들의 통합. 
 출시  게임을 첫 번째 플랫폼으로 출시. 파티! 
 이식  다른 언어 버전, 다른 플랫폼 버전 작성. 

초기 단계들(개념, 원형, 플레이가능성)에서는 프로그램을 빨리 작동하도록 만드는 것이 프레임웍 선택의 주요 기준이 된다. 이 시점에서는 개념 검증 데모(concept-proof demo)의 제작이 프레임웍보다 더 중요하게 느껴질 수도 있다. 그러나 여기서 다음 단계들에 필요한 프레임웍을 제대로 설계해 놓지 않으면 리팩토링에 많은 시간을 소비하게 될 것이다.

제작 단계에서는 표시기나 편집기 같은 도구들이 필수적으로 요구된다. 개발자가 자신의 작업물을 ‘게임 안에서’ 살펴볼 수 있으려면 표시기가 필요하다. 또한 개발자가 게임의 여러 측면들을 조정할 수 있으려면 편집기가 필요하다. 이런 종류의 도구들을 게임 응용 프로그램과 개별적으로 독립시킬 수도 있겠지만, 게임에 직접 통합시켜야 하는 경우도 흔하다. 그러한 통합을 위해서는 ‘소비자’ 측면을 위한 코드뿐만 아니라 ‘개발자’ 측면을 위한 코드도 있어야 한다. 목표는, 양 측면들이 공유하는 요소를 포괄하는, 그러면서도 각 측면들에 대한 개별적 개발이 가능한 프레임웍을 만드는 것이다.

통합 단계에서는 다양한 게임 모드들과 화면들을 하나의 완전한 제품으로 통합시켜야 한다. 이러한 통합 단계에서는 종종 출시를 지연시키는, 또는 원래의 설계를 뒤엎어야 하는 사태가 발생하기도 한다. 게임 모드들과 화면들, 그리고 그들 사이의 전환을 관리하는 데 프레임웍을 사용한다면, 통합 단계를 별 사고 없이 좀 더 매끄럽게 통과하는 데 도움이 된다.

테스트 단계에서는 다양한 모드들 또는 게임의 특정한 지점 안에서 게임을 바로 시작할 수 있어야 한다. 프레임웍을 통해서 이러한 종류의 유연성을 얻으려면, 다양한 모드들과 진입점들을 프레임웍에서 쉽게 정의할 수 있어야 한다. 또한 게임 안에서 드라이버들을 전환할 수도 있어야 한다. 만일 프레임웍이 특정 종류의 비디오 기술을 응용 프로그램 안에 고정시킨다면, 비디오 드라이버의 전환은 불가능해진다. 그리고, 드라이버 전환을 지원하든 그렇지 않든, 호환성 테스트를 쉽게 하려면 프레임웍이 로그(log) 기록 기능을 제공해야 한다.

출시 단계에 도달하면, 게임 팀은 게임의 이식 단계로 넘어가거나, 또는 이식 전문팀에게 작업을 넘겨줄 수도 있다. 어떤 경우이든, 프레임웍을 다른 플랫폼으로 이식하기가 힘들다면 이식 작업에서 지연이 생기게 된다. 이식 팀이 다른 플랫폼에서 게임을 돌리는 문제로 진땀을 빼는 일 없이 새로운 기능의 추가나 게임의 개선에 전념할 수 있도록 하려면 프레임웍을 이식하기 편하게 만들어야 한다.

게임 프레임웍의 설계

지금까지 게임의 제작에 필요한 여러 가지 일들을 간략히 살펴보았다. 그럼 그러한 일들을 쉽게 만들어주는 프레임웍의 설계 문제로 넘어가자. 여기서는 플랫폼 독립성, 게임 독립성, 객체 조합, 상속, 프레임 기반 코드, 함수 기반 코드, 작동 순서, 객체의 수명, 작업 통합 등에 대해 이야기한다.

플랫폼 독립적 대 플랫폼 의존적

일반적으로, 게임은 운영체제나 플랫폼 기술들과는 무관한 여러 가지 개념들로 채워진다. 그리고 그러한 개념들은 ‘게임플레이’와 ‘깊이’를 통해서 플레이어의 재미를 결정하는 요인이 된다. 또 한편으로는, 게임은 특정한 하드웨어 기능들을 최대한 활용해서 시청각적 품질을 높일 수 있도록 작성된다. 이러한 시청각적 품질은 게임의 ‘몰입감’을 강화하는 요인이 된다.

프레임웍은 운영체제와 플랫폼 기술에 국한될 필요가 없다. 프레임웍에 대해 플랫폼 의존적인 시스템 인터페이스를 정의하는 것보다는 플랫폼 독립적인 시스템 인터페이스를 정의하는 것이 좋다. 프레임웍의 인터페이스들이 플랫폼에 대해 독립적이라 해도, 팩토리 시스템을 통해서 특정 플랫폼에 대한 구체적인 구현을 생성할 수 있다.

게임의 개념적인 작업을 플랫폼에 국한된 사항들로부터 독립시킬수록, 이식 과정에서 플랫폼에 국한된 코드를 다른 것으로 대체하기가 쉬워진다. 따라서 게임 개념들을 구체적인 운영 체제와 플랫폼 기술로부터 최대한 또는 가능한 한도 내에서 분리시키기 쉽도록 만드는 것은 게임 프레임웍 제작의 한 가지 목표가 된다. 또한 플랫폼에 국한된 기능의 구현을 단순화하는 것 역시 중요한 목표라 할 수 있다.

게임 독립적 대 게임 의존적

하나의 프레임웍으로 여러 개의 게임들을 만들어 내는 경우라면, 그 프레임웍은 게임에 독립적이 되어야 한다. 반대로 하나의 프레임웍으로 하나의 게임을 만들고 그것을 여러 플랫폼들로 이식하는 경우라면, 프레임웍의 일부가 게임에 의존적이라 해도 큰 문제가 되지 않는다.

예를 들어서, 게임이 캐릭터의 상태에 기반해서 여러 가지 동적인 그래픽을 표현해야 한다면, 렌더링 코드가 게임의 구체적인 상태들에 접근하게 허용하고 그를 통해서 객체의 렌더링 방식을 결정할 수 있게 만들 수도 있다. 이런 식의 직접적인 접근은 프레임웍을 게임에 좀 더 의존적인 것으로 만들지만, 대신 시스템 인터페이스 호출의 횟수를 줄일 수 있으므로 속도 향상에는 도움이 된다.

객체 조합 대 상속

직관적인 응용 프로그램 프레임웍을 만드는 한 가지 방법은, 템플릿 메서드 설계 패턴을 적용해서 응용 프로그램 클래스의 서브클래스들을 작성하는 것이다. 이 경우에는 응용 프로그램의 초기화나 종료 등을 서블클래스들이 재정의할 수 있는 알고리즘으로 취급하게 된다. 이런 종류의 설계는 “클래스 행동” 패턴에 속하는데, 왜냐하면 이 패턴에서는 상속을 통해서 행동을 여러 클래스들로 분산시키기 때문이다.

응용 프로그램 초기화와 종료 단계들을 정의하는 또 다른(상속을 사용하지 않는) 방법은, 그런 단계들을 일련의 작업들의 목록으로 정의하는 것이다. 이 경우 작업(task) 시스템 클래스가 작업 수행을 조정하고, 자원 시스템은 작업 목록과 작업 객체들을 참조, 관리한다. 이런 방식은 객체 조합을 이용하며 자원 시스템이 그 객체들의 중재자(mediator)로 작용한다는 점에서 “객체 행동” 패턴에 속한다.

이런 식의 작업 시스템을 사용하면 상속 대신 객체 조합에 기반해서 프레임웍을 구축할 수 있다. 이러한 작업 시스템은 작업 객체들의 인터페이스를 통해서 그 객체들을 제어하며, 작업들은 객체 인터페이스들을 호출함으로써 자신의 작업을 수행한다. 이는 프레임웍이 템플릿 메서드 패턴에서 비롯되는 전형적인 ‘뒤집힌’ 제어 구조를 가지지 않는다는 의미이기도 하다. 이러한 프레임웍에서는 작업 객체들이 소프트웨어를 제어한다. 이런 면에서 클래스 프레임웍보다는 객체 프레임웍에 가깝다고 할 수 있으나, 어쨌든 프레임웍인 것만은 분명하다.

Design Patterns 책 [GoF94]에는 객체 조합의 여러 이점들이 서술되어 있다. 또한 다음과 같은 객체 지향적 설계의 두 가지 원칙들도 강조하고 있다.

프레임 기반 작동 대 함수 기반 작동

프레임 타이밍과는 관련이 없는 소프트웨어도 많다. 그런 소프트웨어에서는 함수들이 몇 초, 몇 분, 또는 그 이상의 시간을 소비하기도 한다. 이러한 함수 기반 작동은 프레임 기반 작동에 비해 프로그래밍하기가 훨씬 쉽다.

대부분의 게임들은 시청각적으로 좋은 반응성을 갖춰야 하며, 매 프레임마다 수많은 애니메이션들과 세부적인 그래픽을 그려야 한다. 시각적 이미지나 오디오 버퍼가 한 번 표현될 때마다 하나의 프레임이 만들어지는데, 게임들은 대체로 최대 50에서 60 정도의 프레임율로 렌더링된다. 이러한 프레임 기반 작동 방식 때문에, 게임 소프트웨어는 짧은 시간 동안 수행되어야 한다. 만일 수행할 작동의 시간이 길다면(60 분의 1 초 이상), 그 작동을 더 작은 조각들로 나누거나, 아니면 배경 작업으로 수행해야 한다.

이 글에서 이야기하는 프레임웍의 경우 프레임 기반 작동을 가능하게 하려면, 프레임에 동기화되는 작업들을 언제 수행할 것인지 작업 시스템 클래스에게 알려주는 프레임 시스템 클래스가 필요하다. 또한 작업 시스템은 프레임에 동기화되지 않은 작업들(이를 “비동기 작업들”이라 부르도록 하겠다)도 처리할 수 있어야 한다.

프레임 시스템은 프레임에 동기화되는 작업들의 호출 시점을 제어하므로, 프레임들을 수동으로 몇 단계씩 진행시키거나 특정한 프레임율을 선택할 수 있는 능력도 필요하다. 그런 능력은 애니메이션 재생을 세부적으로 점검한다던가 기타 디버깅 및 테스트 등에 유용하게 쓰일 수 있다.

동적인 연산 순서 대 정적인 연산 순서

소프트웨어의 연산들 중에는 특정한 순서로 수행되어야 하는 것들이 있다. 그런 연산들의 경우 함수들을 특정한 순서로 호출한다거나 작업들을 작업 시스템에 특정한 순서로 등록시킬 수 있어야 한다. 이런 방식은 정적인 작동 순서에 해당한다.

게임에는 다양한 화면들과 변화들이 존재하는데, 일반적으로 그런 것들의 순서는 고정되어 있지 않으며, 대체로 실행 시점에서 플레이어의 행동에 의해 결정된다. 이런 방식은 동적인 작동 순서에 해당한다.

이 글의 프레임웍의 경우는 작업 객체들이 작업 시스템 인터페이스에 접근해서 새로운 작업들을 등록할 수 있도록 하는 식으로 동적인 작업 순서를 지원한다.

동적인 객체 수명 대 정적인 객체 수명(그리고 소유권 문제들)

계통적인 시스템에서는 상속된 객체들이 기반 객체들의 특정한 객체들을 상속하며, 그와 함께 기반 객체는 알지 못하는 또 다른 객체들을 생성하는 식의 구조가 존재한다. 종종 그런 객체들의 수명은 계통구조 자체에 내재되어 있으며, 상속된 객체들은 그런 객체들을 동적으로 생성하거나 삭제할 수 없다. 이 경우 그런 객체들의 수명은 정적이라고 할 수 있다.

이 글의 게임 프레임웍 같은 객체 조합 시스템의 경우, 특정 객체가 언제 다른 객체를 소유하는 지 알아내기가 좀 혼란스러울 수 있다. 이런 문제를 피하기 위해, 이 프레임웍에서는 객체의 소유권을 자원 시스템에 배정한다. 이렇게 하면 임의의 작업을 필요에 따라 시스템 자원에 연결시킬 수 있으며, 그 작업에는 관리 책임이 부여되지 않는다. 자원 시스템에는 객체의 소유권을, 반면 작업들에는 객체에 대한 접근 권한을 부여하게 되면 소유권 문제에 대한 혼란을 피할 수 있다.

작업 수행 명령들을 자원 시스템이 가지게 하면, 이러한 객체들의 수명을 동적으로 만드는 것도 가능하다. 자원 시스템에게 객체들을 하나의 묶음으로 로드하거나 내보내도록 명령할 수 있다. 예를 들어서, 일련의 객체들을 필요로 하는 작업을 수행하기 전에 그 객체들에 대한 하나의 ‘묶음 로드’ 명령을 자원 시스템에게 내리고, 작업이 끝나면 다시 자원 시스템에게 그 객체들에 대한 하나의 ‘묶음 해제’ 명령을 내리는 식이다. 아니면, 객체들이 응용 프로그램의 수명 내내 존재하게 만들 수도 있다.

수평적 작업 통합 대 수직적 작업 통합

계통적 시스템에서는 응용 프로그램이 어떠한 객체들의 ‘제어 하에서’ 움직이며 응용 프로그램과 그 객체들 사이의 관계가 ‘고정되어’ 있다는 느낌을 받게 된다. 또한 작업들은 수직적으로 조직화된 것처럼 느껴지며, 시스템 상층부에 대한 변경의 여파는 시스템 하층부의 행동에까지 미치게 된다.

이 프레임웍 같은 객체 조합 시스템의 경우에는 프로그래밍 환경이 평면적이며, 객체들과의 관계가 좀 더 동적이다. 작업들은 수평적으로 조직화된다는 느낌을 주며, 시스템의 특정 작업들에 대한 변경은 다른 작업들에게 거의 영향을 미치지 않는다.

게임 프레임웍의 구현

이제 위의 사항들을 고려한 프레임웍의 구현에 대해 간략히 소개해 보겠다. 이 프레임웍은 시스템들과 작업들로 구성된다. 전체적인 시청각적 렌더링과 로직은 ‘프레임 재생기(frame player)’라는 특별한 종류의 작업에 의해 제어된다.

시스템들

Systems_t 클래스는 게임이 사용할 시스템들에 대한 순수 인터페이스(pure interface)[Stroustrup97]들로의 포인터들을 담는다. 순수 인터페이스들을 통해서 시스템에 접근하아므로 동적인 시스템 전환이 가능하며 플랫폼 의존적인 시스템 코드가 시스템들에 접근하는 플랫폼 독립적 코드로부터 분리된다. Systems_t 클래스에서 접근할 수 있는 인터페이스들이 표 1.2.2에 나와 있다.

표 1.2.2 Systems_t 클래스에서 사용 가능한 인터페이스들

 시스템  요약 
 LogSys_t  게임으로부터의 모든 메시지 로깅을 처리한다. 추가적으로 텍스트 상자나 파일로의 출력도 지원한다. 
 ErrorSys_t  모든 오류 정보 및 상태를 처리한다. 
 TimeSys_t  타이밍 정보를 보고한다. 
 FactorySys_t  팩토리 ID를 사용해서 객체들을 생성한다. 
 ResourceSys_t  인트턴스 ID를 이용해서 객체 인스턴스들을 관리한다. 
 TaskSys_t  작업 수행 및 제어를 관리한다. 
 WindowSys_t  윈도우 관리 및 제어를 제공한다. 
 FrameSys_t  프레임 동기화 서비스 및 제어를 제공한다. 
 InputSys_t  입력 장치 관리의 및 제어를 제공한다. 
 VisualSys_t  그래픽 시스템의 관리 및 제어를 제공한다. 
 AudioSys_t  오디오 시스템의 관리 및 제어를 제공한다. 
 NetworkSys_t  네트웍 시스템의 관리 및 제어를 제공한다. 

각 시스템에는 Init(Systems_t *pSystems) 메서드와 Shutdown() 메서드가 있다. 객체들에게는 Systems_t의 포인터가 전달되며, 객체들은 그 포인터를 통해서 시스템 인터페이스에 접근하게 된다. 시스템 코드에 Systems_t 클래스를 포함시켜도 컴파일러 의존성은 생기지 않는다. 포인터를 통한 시스템으로의 접근에 필요한 것은 전방 참조(forward reference)뿐이기 때문이다(형만 미리 선언해주면 된다). 이러한 의존성 문제는 프레임웍의 많은 클래스들이 Systems_t 포인터들을 사용한다는 점에서 중요한 사항이라 할 수 있다. 물리적 의존성을 줄이는 것은 좋은 물리적 설계를 위한 주요 목표 중 하나이다[Lakos96].

이러한 시스템들은 모두 순수 인터페이스로 정의되며 구체적인 구현은 모두 은폐되어 있으므로, 시스템 구현들이 정적인 링크 의존성을 가지지 않는 한 이들을 동적으로 전환하는 것이 가능하다. 의존적인 구현들을 동적으로 로드 가능한 구성 요소들로 분할하는 것은 패키지 패턴의 한 예이다[Noble01].

CD-ROM에는 그래픽 시스템을 동적으로 전환하는 방법을 보여주는 예제가 담겨 있다. 그 예제는 VisualSys_t 인터페이스에 대한 서로 다른 구현들을 담고 있는 동적 연결 라이브러리(DLL)들을 이용해서 동적인 그래픽 시스템 전환을 수행한다. 다음은 그래픽 시스템의 전환을 제어하는 코드의 한 예이다.

 FactorySys_t *pFS = m_pSystems->GetFactorySys();
 pFS->DeleteVisualSys( m_pSystems->GetVisualSys() );
 pFS->SetVisualSysDriverID( m_nVisualSysDriverID );
 m_pSystems->SetVisualSys( pFS->CreateVisualSys() );

작업

TaskSys_t 클래스는 작업 시스템에 대한 인터페이스를 제공한다. 작업 시스템에 작업을 추가할 때에는 Post_TaskCommand 함수를 이용한다. 이 때 작업의 종류를 지정한다. 작업의 종류는 프레임에 동기화되는 작업 아니면 비동기 작업이다. 두 작업 종류의 유일한 차이는 작업이 호출되는 시점뿐이다. 프레임에 동기화되는 작업들은 프레임 시스템이 다음 프레임을 처리할 시점임을 알려줄 때 호출되며, 비동기 작업들은 작업 시스템의 루프 반복 시에 호출된다.

Post_TaskCommand을 통해서 언제라도 작업을 추가하거나 제거하는 것이 가능하다. 다음은 현재의 비동기 작업을 중지시키고 프레임에 동기화되는 작업 하나를 시작하도록 작업 제어 명령을 내리는 예이다.

 // 작업 시스템을 얻는다.
 TaskSys_t *pTaskSys = m_pSystems->GetTaskSys();
 // 자원 시스템을 얻는다.
 ResourceSys_t *pResSys = m_pSystems->GetResourceSys();
 // 현재의 비동기 작업을 제거한다.
 pTaskSys->Post_TaskCommand( ASYNC_REMOVE, this );
 // 새 작업을 시작한다.
 Task_t *pTask = pResSys->GetTask( INSTANCE_ID_TASK_INTRO );
 // 새 프레임에 동기화되는 작업을 목록에 추가한다.
 pTaskSys->Post_TaskCommand( FRAMESYNC_PUSH_BACK, pTask );

코드를 보면, 자원 시스템의 GetTask 함수를 호출할 때 작업의 인스턴스 ID를 넣어서 해당 작업에 접근할 수 있게 된다는 점을 알 수 있을 것이다. 작업이 가지고 있는 Systems_t 포인터는 작업이 자신의 Connect(Systems_t *pSystems) 함수를 통해서 시스템에 연결될 때 전달받은 것이다.

계층들

일반적으로, 게임 개발 과정에서 시각적인 렌더링 디자인에 관련된 작업의 양은 대단히 많다. 렌더링을 고수준에서 관리할 때 계층 시스템이 쓰일 수 있다. 계층 시스템은 게임 화면이 계층적인 방식으로 렌더링될 때 더욱 중요해진다. 예를 들어 3D 게임에서 게임 세계를 하나의 층으로, 그리고 게임 객체들을 또 다른 층으로, 마지막으로 HUD(heads-up display)를 세 번째 층으로 그리는 경우 등을 생각해 볼 수 있다. 프레임웍이 이런 방식을 지원하게 하려면, 장면에 새 계층들을 추가하고 그것들에 적절한 시청각 효과들과 입력 처리 로직을 부여할 수 있어야 한다.

시각적 화면들이나 오디오 데이터, 입력 처리 로직 등의 계층들을 제어하기 위해, FramePlayer_t라는 특별한 종류의 작업을 도입한다. 이 작업은 프레임에 동기화되며 시청각층(AVLayer_t) 객체들과 로직층(LogicLayer_t) 객체들을 관리한다.

시청각층 객체들은 매 프레임마다 다음과 같은 방식으로 호출된다.

 // AV 층들을 갱신
 for( 각각의 시청각층에 대해(순차적으로) )
 {
     AVLayer_t *pAVL = 반복자의 내용
    pAVL->Update();
 }
 // 그래픽 렌더링 시작
 if( m_pSystems->GetVisualSys()->BeginRender() )
 {
 for( 각각의 시청각층에 대해(순차적으로) )
    {
        AVLayer_t *pAVL = 반복자의 내용;
        pAVL->RenderVisual();
    }
    m_pSystems->GetVisualSys()->EndRender();
 }
 // 그래픽 렌더링 끝
 // 오디오 렌더링 시작
 if( m_pSystems->GetAudioSys()->BeginRender() )
 {
     for( 각각의 시청각층에 대해(순차적으로) )
    {
        AVLayer_t *pAVL = 반복자의 내용;
        pAVL->RenderAudio();
    }
    m_pSystems->GetAudioSys()->EndRender();
 }
 // 오디오 렌더링 끝

각각의 시청각층은 우선 Update() 호출에 의해 갱신된다. 이 때 시청각 객체들은 자신의 애니메이션 상태에 따라 적절히 갱신된다. 그런 다음에는 시청각층을 렌더링한다.

로직층 객체들은 매 프레임마다 다음과 같은 방식으로 호출된다.

 // 로직층의 갱신
 for( 각각의 로직층에 대해(역순으로) )
 {
     LogicLayer_t *pLL = 반복자의 내용;
     pLL->Update();
     if( pLL->IsExclusive() ) break;
 }

각 로직층은 그냥 Update() 호출 한번으로 갱신된다. 시청각층의 갱신은 애니메이션을 처리하기 위한 것인 반면, 로직층의 갱신은 게임 로직과 플레이어 입력을 처리하기 위한 것이다.

로직층들은 역순으로 처리되며, 한 로직층이 독점적으로 표시되어 있는 경우에는(IsExclusive()) 그 시점에서 처리가 중단되므로, m_LogicLayerPtrList의 마지막 로직층이 이전의 로직층들을 무시하도록 만들 수 있다. 따라서, 로직층 하나를 추가하는 것으로도 게임 메뉴나 표시기, 편집기 등을 위한 플레이어 입력의 처리 방식을 완전히 바꾸는 것이 가능하다. 반면 새로운 시청각층의 추가는 화면이나 음향에 새로운 요소 또는 효과를 추가한다는 의미일 뿐이다.

작업 시스템이 작업 명령들을 가지듯이, FramePlayer_t 클래스는 계층 명령들을 가진다. 다음은 계층 명령들을 수행하는 예이다.

 AVLayer_t *pAVL;
 LogicLayer_t *pLL;
 // 자원 시스템을 얻는다.
 ResourceSys_t *pResSys = m_pSystems->GetResourceSys();
 // 수정할 작업을 얻는다.
 Task_t *pTask = pResSys->GetTask(INSTANCE_ID_TASK_INTRO);
 // 이 작업이 프레임 재생기임은 이미 알고 있다.
 FramePlayer_t *pFP = (FramePlayer_t *)pTask;
 // 시청각 층을 하나 추가.
 pAVL = pResSys->GetAVLayer(INSTANCE_ID_AVLAYER_INTRO);
 pFP->Post_AVLayerCommand(PUSH_BACK, pAVL);
 // 로직층을 하나 추가.
 pLL = pResSys->GetLogicLayer(INSTANCE_ID_LOGICLAYER_INTRO);
 pFP->Post_LogicLayerCommand(PUSH_BACK, pLL);

코드에서 볼 수 있듯이, 기존의 시청각층을 얻을 때에는 GetAVLayer 함수에 그 층의 인스턴스 ID를 넣는다. 로직층의 경우 역시 비슷한 방식으로, 자원 시스템의 GetLogicLayer 함수에 해당 인스턴스 ID를 넣어서 얻으면 된다. 계층이 가지고 있는 Systems_t 포인터는 계층이 자신의 Connect(Systems_t *pSystems) 함수를 통해서 시스템에 연결될 때 전달받은 것이다.

이러한 계층 시스템에서는 시청각층들을 동적으로 변경할 수 있다. 통합 단계에서 언급했던 모든 게임 모드들과 화면들은 계층과 작업 명령들을 통해서 구현될 수 있다. 예를 들어서, HUD가 필요하다면 HUD를 위한 시청각층을 계층 시스템에 추가하면 된다. 마찬가지로 팝업 메뉴가 필요하다면 해당 시청각층과 로직층을 추가하면 된다. 새로운 모드나 화면을 게임에 추가하는 경우, 계층 명령들을 통해서 프레임 재생기 작업의 계층들을 수정할 수도 있고 아니면 이전에 이야기했던 작업 명령들을 통해서 프레임 재생 작업 자체를 교체할 수도 있다.

마지막으로, 계층과 작업들을 정교하게 사용함으로써 보다 세련된 모드 또는 화면 전환의 구현이 가능하다. 예를 들면 한 게임 화면의 요소들(첫 번째 시청각층)이 다른 게임 화면의 요소들(두 번째 시청각층)을 점차적으로 덮게 만드는 경우 등을 생각해 볼 수 있다. 이러한 전환이 완료되고 두 번째 계층만 보이게 되면 첫 번째 계층을 시청각 처리에서 제거한다.

소스 코드

CD-ROM에는 이 글에서 이야기한 게임 프레임웍 구현 관련 소스 코드와 몇 가지 추가적인 문서들이 들어 있다. 수록된 소스코드는 이 글에서 살펴 본 게임 프레임웍의 개념을 보여주기 위한 것일 뿐, 게임 기술의 개념들을 위한 것은 아니다. 독자의 요구와 필요에 맞게 좀 더 개선시켜 나가길 바란다. 그림 1.2.1은 소스 코드에 구현되어 있는 모드들, 작업들, 계층들이다.

그림 1.2.1 게임 프레임웍의 사용: 게임 모드들을 작업들, 시청각층들, 로직층들로 구현하는 한 가지 예

참고자료