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

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

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

3.5 AI 에이전트, 객체, 퀘스트를 위한 확장성있는 트리거 시스템

Steve Rabin, Nintendo of America, Inc.
steve_at_aiwisdom.com

게임의 1인용 버전에 식상해버린 플레이어는 웹에서 더 많은 레벨들이나 레벨 편집기를 찾게 된다. 그리고 게임이 성공하려면 그런 기대에도 부응할 수 있어야 한다. 확장성 있는 레벨들과 퀘스트들은 잘 설계된 게임에 대한 하나의 품질 보증서로 간주될 수 있으며, 게임의 일반적인 수명을 상당히 늘려줄 수 있다. 게임은 그것이 RTS이든 아니면 RPG나 액션이든, 플레이어가 어떠한 형태로든 레벨들을 커스텀화하거나 새로운 영역들을 추가하는 것이 가능해야 한다.

Baldur’s Gate, StarCraft, Dungeon Siege가 훌륭한 게임으로 평가되는 이유에는 플레이어가 새로운 퀘스트들을 만들거나 심지어는 AI를 수정하고 확장하는 것이 가능하다는 측면도 포함된다. 그러나 플레이어는 프로그래머가 아니므로 플레이어들에게 가상의 프로그래밍 언어를 배우도록 하거나 디버깅을 강제하는 것은 불가능하다. 평범한 플레이어도 스스로 레벨이나 퀘스트를 만들 수 있으려면 무엇보다도 단순함이 보장되어야 하는데, 그러한 단순함은 확장성있는 트리거 시스템을 통해서 제공할 수 있다.

트리거 시스템의 소개

트리거 시스템은 ‘조건을 평가하고 반응을 수행한다’라는 한 가지 목적을 가진 중앙 집중화된 코드이다. 일련의 조건들이 만족되면, 일련의 반응들이 수행된다. 이러한 간단한 시스템은 우아하고, 구현하기 쉬우며, 데이터 주도적[Rabin0]으로 만들기도 쉽다. 트리거 시스템은 다양한 범위의 문제들을 해결할 수 있으며, 특히 디자이너와 플레이어에 의해 수정될 수 있다는 점에서 바람직하다. 트리거 시스템의 매력은 무엇보다도 플레이어가 탐험할 흥미롭고 상호작용적이며 새로운 환경을 쉽게 만들 수 있도록 한다는 데 있다.

일단의 모험가들이 던전을 탐험하는 게임을 생각해보자. 모험대는 지하 묘지를 통과하는 도중 무너져 내린 기둥에 대장을 잃게 된다. 그리고 인상적인 석문에 도달했을 때에는 찬 바람이 불어 와 횃불도 꺼뜨린다. 마지막 횃불에 불을 붙인 후 석문을 보니 “무거운 심장이여 이 문을 통과하리니”라고 새겨진 문구가 눈에 띈다. 잠시 생각해본 후, 체중이 무거운 대원 하나를 심장 모양의 받침대에 올려놓았더니 문이 서서히 열린다.

간단한 조건-반응 패러다임을 이용하는 트리거 시스템으로 이러한 사건들을 지정하는 것이 가능하다. 대원이 특정 기둥 객체에 1미터 이내로 접근하면 바람 소리에 해당하는 음향 효과를 재생하고 그 주변의 횃불들을 꺼뜨린다던가, 플레이어의 파티 대원들 중 하나가 심장 모양의 받침대 위로 올라가면 문을 열고 돌이 갈리는 듯한 음향 효과를 재생하는 등은 모두 조건-반응 패러다임에 속한다.

트리거 시스템을 잘 구현한다면, 디자이너와 플레이어 모두가 독창적인 시나리오 및 퀘스트의 작성을 위해 여러 시간 동안 몰두하도록 만들 수 있는 장치들을 갖출 수 있다. StarCraft 맵 편집기는 참고할 만한 좋은 트리거 시스템의 예이다. 기존의 시스템을 연구하고 새로운 아이디어를 만들어 내는 것은 개발자의 기본적인 자세이다.

객체가 소유하는 트리거 시스템

트리거 시스템을 처음 만들 때에는 하나의 마스터 트리거 시스템([Orkin02]에 나온 것 같은)을 떠올리게 될 것이다. 그러나 좀 더 강력한 구조를 원한다면 어떠한 에이전트나 객체, 퀘스트도 소유할 수 있는 트리거 시스템 클래스를 생각해 볼 수 있다. 모든 객체가 트리거 시스템을 가져야 하는 것은 아니지만, 객체마다 자신의 트리거 시스템 인스턴스를 가질 수 있다면 객체 안에 데이터를 캡슐화하는 데에도 도움이 되며 시스템이 좀 더 유연하고 객체지향적이 될 수 있다. 또한 트리거라는 것 자체가 특정 객체에 대해 작동하게 되므로, 플레이어가 개념을 잡는 데에도 도움이 된다.

누군가가 다가오면 무너져 내리는 기둥을 생각해보자. 그러한 기둥을 레벨 편집기 안에서 정의하고 거기에 무너져 내리는 행동을 강조하는 하나의 트리거 정의를 부여한다. 그리고 디자이너나 플레이어가 게임의 여러 곳에 그러한 기둥들을 배치하면, 신통하게도 의도했던 대로의 모습을 볼 수 있게 된다. 이는 트리거 행동이 객체에 직접 부착되어 있기 때문에 가능한 것이다. 이런 방식에서는 어떠한 에이전트나 객체, 퀘스트도 전적으로 해당 개체만을 위해 존재하는 자신만의 고유한 트리거 시스템을 가질 수 있다.

조건의 정의

게임 안에서 수량화할 수 있는 이벤트나 상태라면 어떠한 것이라도 조건이 될 수 있다. 조건은 실행 파일 안에 고정되지만 인수들이나 레벨 편집기를 통해서 조절할 수 있는 여지도 매우 크다. 다음은 가능한 몇 가지 조건들이다.

부울 논리로 연결된 조건들

조건들을 AND, OR, NOT, XOR 같은 부울 연산자들로 결합할 수 있다면 조건들이 좀 더 유연해질 수 있다. 예를 들어 플레이어가 얼음검, 얼음방패, 얼음갑옷을 장착해야만 어떤 문이 열리게 되어 있다면 조건들은 AND로 결합되어야 한다. 또 어떤 문은 플레이어가 은열쇠나 해골열쇠 중 하나만 가지고 있어도 열릴 수 있다면 조건들이 OR로 결합되어야 한다. 그림 3.5.1과 3.5.2는 그러한 조건들을 트리 구조로 표현한 것이다.

                      AND        참이면 트리거 발동            문 열림

 얼음검 장착    얼음방패 장착    얼음갑옷 장착

그림 3.5.1 세 조건 모두 “참”이면 문이 열린다.

                     OR         참이면 트리거 발동              문 열림

 은열쇠를 가지고 있음    해골열쇠를 가지고 있음

그림 3.5.2 둘 중 하나라도 “참”이면 문이 열린다.

좀 더 복잡한 경우로, 플레이어가 얼음검, 얼음방패, 얼음갑옷을 장착하고 있으며 은열쇠 또는 해골열쇠를 가지고 있어야 문이 열리게 된다면 어떻게 될까? 그림 3.5.3이 그러한 조건들을 표현한 것이다.

이와 같은 그림들을 통한 시각화는 코드를 구조화하는 좋은 방법을 제시한다는 점에서 중요한 의의를 갖는다. 각 요소가 하나의 클래스인 경우 두 종류의 클래스가 필요한데, 하나는 Operator 클래스이고 또 하나는 Condition 클래스이다. Operator 클래스는 임의의 부울 연산자처럼 작동하도록 만들어야 할 것이다. 또한 여러 연산자, 피연산자들이 포함된 표현식을 나타내기 위해서는 다른 Operator 인스턴스들이나 Condition 인스턴스들로의 포인터들의 목록도 담아야 한다. Condition 클래스는 임의의 판정 가능한 조건들을 평가할 수 있어야 하며, 조건의 커스텀화를 위한 인수들을 담아야 한다.

              AND       참이면 트리거 발동               문 열림

 얼음검 장착  얼음방패 장착  얼음갑옷 장착              OR

                                     은열쇠를 가지고 있음   해골열쇠를 가지고 있음

그림 3.5.3 문을 좀 더 복잡한 조건들

반응의 정의

게임 안에서 변경하고자 하는 상태나 행동이라면 어떠한 것도 반응이 될 수 있다. 조건과 마찬가지로, 반응 자체는 실행 파일 안에 고정되지만 인수들 또는 레벨 편집기를 통해서 조절될 수 있다. 다음은 가능한 반응들의 목록이다.

특정 조건들의 집합이 만족되면 그에 해당하는 반응이 수행된다. 이를 단 하나의 반응이 아니라 여러 개의 반응들의 집합으로 확장할 수도 있다. 그렇게 하면 하나의 트리거 발동이 여러 가지 것들에 동시에 영향을 미치게 할 수도 있고, 여러 반응들 중 임의의 것이 수행되도록 할 수도 있다.

트리거의 평가

트리거에 조건들이 정의되었다고 할 때, 다음으로 필요한 것은 한 트리거의 조건들을 평가해서 발동 여부를 판단하는 구조이다. 우선 고려할 것은, 특정 조건이 이벤트 주도적인지(트리거 시스템에 이벤트가 통지되기를 기다리는 것) 아니면 게임 세계를 주기적으로 점검해야 하는지를 결정하는 것이다. 두 방식 모두 지원하는 것이 유연성에 도움이 된다.

이벤트 주도적인 조건의 경우, 이벤트가 트리거 시스템에 통지될 수 있도록 하는 인터페이스가 필요하다. 가장 간단한 방식은 이벤트 메시지를 사용하는 것이다. 이벤트 메시지는 발생한 이벤트의 종류와 이벤트에 관련된 기타 데이터 등으로 구성된다. 이벤트 메시지에 대한 좀 더 자세한 내용은 [Rabin02]를 참고하기 바란다.

주기적 점검 방식의 조건이라면, 트리거 시스템 안에서 각각의 조건들을 점검하는 어떠한 갱신 함수를 호출하는 방식에 대해 생각해볼 수 있다. 그런 함수에서 이벤트 주도적 조건들에 대한 점검은 일어나지 않아야 한다.

이벤트 메시지나 주기적 점검 갱신이 트리거 시스템에 들어왔다면, 그것을 조건들에게 전파해야 한다. 그림 3.5.4는 이벤트 메시지와 주기적 점검 모두를 요구하는 하나의 조건 집합의 예이다. 왼쪽 조건은 충돌 이벤트를 기다리는 반면, 반면 오른쪽 조건은 주기적 점검 갱신이 들어왔을 때 조건을 점검한다.

이벤트 메시지나 주기적 점검 갱신이 트리거 시스템에 들어오면, 그것을 각 트리거의 루트 Operator 인스턴스에 전달한다. Operator는 그것을 자신의 자식들에게 넘겨주며, 자식들은 그에 대한 판단 결과를 “참” 또는 “거짓”으로 돌려준다. 각 자식 역시 그러한 요청을 자신의 자식들에 넘겨준다. 그러한 요청이 실제 조건에 도달하면 참/거짓 판정이 일어나게 되고, 그 결과들이 상위 Operator 인스턴스에 올라가서 부울 연산자에 의한 참/거짓 판정이 일어난다. 그런 식으로 참/거짓이 루트에게까지 올라가면 최종적인 결과가 만들어진다.

 이벤트 메시지 및 
 주기적 점검 갱신

                       AND                참이면 트리거 발동         쥐 10 마리 생성

 플레이어가 10 미터 이내     플레이어의 건강이 50% 이상

그림 3.5.4 하나의 트리거가 이벤트 주도적 조건(아래 왼쪽)과 주기적 점검 기반의 조건(아래 오른쪽)을 모두 가진 예

Operator 클래스는 자식들을 처리할 때 늦은 평가(lazy evaluation)를 사용해야 한다는 점을 명심하기 바란다. 어떠한 조건이 해당 연산자에 대해 만족되지 않은 경우, 트리거의 판정은 그 시점에서 끝난다. 예를 들어 그림 3.5.4에서 어떠한 이벤트 메시지가 왼쪽 조건에 전달되고 그것이 “거짓”으로 판정되었다면 오른쪽 조건으로는 그 이벤트 메시지가 전달되지 않도록 하는 것이다. 이렇게 하면 처리 시간을 줄이는 데 도움이 된다.

또 다른 중요한 점 하나는, 이벤트 주도적 조건의 경우 조건이 명시적으로 재설정되기 전까지는 이벤트들을 기억하고 있어야 한다는 점이다. 그림 3.5.4의 예에서는 플레이어가 10미터 이내로 접근하면 충돌 이벤트가 트리거에 전달되어야 한다. 왼쪽 조건은 그 이벤트를 기억해 두고(플레이어가 10 미터 바깥으로 나가기 전까지는), 이후의 이벤트 메시지나 주기적 점검 갱신에 대해 항상 “참”을 돌려줘야 한다.

어떠한 시점에서, 특정 트리거의 조건들이 모두 참을 돌려준다면 트리거가 발동된다. 트리거가 발동되면 발동되었다는 사실을 기억해서 연달아 다시 발동되는 일이 없도록 해야 한다.

단일 발동과 재설정 시간

모든 트리거들에는 추가적으로 두 개의 속성들이 더 필요하며, 이 속성들은 디자이너가 정의하게 된다.

 bool SingleShot;  // 트리거가 한 번만 발동되어야 하는지의 여부
 float ReloadTime; // 트리거가 여러 번 발동되어야 하는 경우,
                   // 재설정되기까지의 시간

이 두 속성들은 트리거가 한 번 이상 발동될 수 있도록 한다. SingleShot 속성은 트리거가 한 번만 발동되어야 하는지 아니면 여러 번 발동될 수 있는지의 여부를 가리킨다. SingleShot이 “거짓”일 때, ReloadTime은 트리거가 한 번 발동된 후 다시 발동될 수 있을 때까지, 즉 조건들을 초기화하고 다시 이벤트를 받게 될 때까지 걸리는 시간을 결정한다.

트리거를 플래그 및 카운터와 결합

트리거들이 함께 결합될 수 있으려면, 시스템 안의 모든 트리거들이 접근할 수 있으며 즉시 설정할 수 있는 상태들이 존재해야 한다. 따라서 트리거 시스템은 발동된 트리거들의 상태를 추적할 수 있는 일련의 플래그들과 카운터들을 갖출 필요가 있다. 이를 최대한 일반화하기 위해, 각각의 트리거가 문자열 이름으로 접근할 수 있는 임의의 플래그들을 만들 수 있도록 하자. 트리거 시스템은 플래그 이름이 참조될 때 플래그를 생성하거나, 생성되어 있다면 아니면 플래그를 설정한다. 생성된 플래그는 시스템이 종료될 때까지 유지된다.

다음과 같은 새로운 조건들을 생각해보자.

이런 조건들이 추가된다면, 다음과 같은 새로운 반응들이 필요할 것이다.

이러한 플래그들과 카운터들이 있으면 트리거 시스템은 특정 이벤트에 표시를 하거나 발동 횟수를 셀 수 있으며, 이를 통해서 예를 들면 플레이어가 특정 지역에 들린 횟수 등을 알 수 있다. 또한 이벤트들이 특정한 순서로 발생했을 때에만(예를 들면 세 개의 타일들을 특정한 순서로 밟았을 때 등) 트리거가 발동되도록 할 수도 있다. 이러한 플래그들과 카운터들은 상태 정보를 담으므로, 좀 더 많은 종류의 트리거들이 가능해진다.

그림 3.5.5는 플레이어가 어떤 문을 열지 못해서 특정 지역을 반복적으로 찾아오는 경우에 문 열기에 대한 단서를 제공하는 예이다. 세 개의 개별적인 트리거들이 “Visited”라는 이름의 카운터를 통해서 서로 협동적으로 작동한다는 점을 주목하기 바란다.

             AND           "Visited"를 증가             AND           "Visited"를 증가
                                                                                      
플레이어가        "Visited"가               플레이어가        "Visited"가              
지역 A 안에 있음  짝수                      지역 C 안에 있음  홀수                     

                             AND      문 D에 대한 
                                      단서를 떨어뜨림

                    "Visited"가   문 D가 
                    8과 같음      잠겼음

_그림 3.5.5 세 개의 트리거들이 “Visited"라는 이름의 카운터를 통해서 함께 작동하는 예. 플레이어가 문 D를 열지 못한 채로 영역 A와 C를 번갈아 8번 방문하면 단서가 제시된다.

플래그와 카운터가 추가되면, 트리거 시스템의 형태는 흑판 아키텍쳐[Isla02]와 매우 비슷해진다. 플래그들과 카운터들은 흑판이 되고 트리거들은 흑판의 내용을 조작하는 지식 원천(knowledge source)의 역할을 하게 되는 것이다. 그러나 트리거들은 대부분 흑판의 외부로부터 비롯된 데이터에 대해 작용하므로, 엄밀히 말해서 트리거 시스템이 곧 흑판 아키텍쳐인 것은 아니다.

트리거 시스템 대 스크립팅 언어

트리거 시스템의 기능성이 스크립팅 언어의 기능성과 비슷하다는 점을 눈치 챈 독자도 있을 것이다. 특히 상태 정보가 추가되면 더욱 비슷해진다. 양 쪽의 기능성이 겹치는 부분도 존재하나, 완전한 기능을 갖춘 스크립팅 언어에 비해 트리거 시스템은 다음과 같은 장점들을 가진다.

한계

이 글에서 살펴 본 시스템의 주된 한계는 규모가변성(scalability)이 좋지 않다는 점이다. 그러나 이러한 문제는 무관한 트리거들을 걸러내는 추가적인 코드를 통해서 해결할 수 있다. 근접성을 통한 제외는 이미 그 효과가 상당함이 입증되었다.

또 다른 한계는 조건들과 반응들을 정의하기 위한 어휘가 실행파일 안에 고정되어 있다는 점이다. 사용자의 설정을 코드와 연결하는 부분은 프로그래머의 손을 거쳐야 한다. 이는 게임을 임의의 또는 불순한 의도의 조작으로부터 보호할 수 있다는 장점이 되기도 하다.

결론

부족한 개발 일정에 시달리는 게임 개발자들에게 있어 확장성 있는 트리거 시스템은 어찌 보면 사치스럽게 느껴질 수도 있을 것이다. 그러나 투자한 만큼의 가치와 깊이를 게임에 부여할 수 있다는 점은 잊지 말아야 할 것이다. 확장성있는 트리거 시스템은 또한 프로그래밍 능력을 갖추지 못한 레벨 디자이너도 로직을 구축할 수 있게 하는 효과적인 수단이다. 언뜻 보면 트리거 시스템이 너무 간단한 해결책으로 느껴질 수도 있겠지만, 목표는 디자이너와 플레이어에게 더 많은 능력을 부여하는 데 있음을 잊어서는 안 된다. 내용과 레벨 관련 로직을 정의하는 방식이 쉬우면 쉬울수록 게임은 더욱 확장되고 플레이어는 더 많은 재미를 얻을 수 있다.

참고자료