UDN
Search public documentation:

DungeonDefenseDeveloperBlogKR
English Translation
日本語訳
中国翻译

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

던전 디펜스 개발자 블로그

문서 변경내역: Jeremy Stieglitz 작성. Jeff Wilson 수정. 홍성진 번역.

블로그 1: 1 일차

안녕하세요,

인디 개발자로서는 꽤나 흥분되는 일입니다. 작은 개발 팀의 온갖 종류 혁신적인 게임에 대해 거대 시장을 열어준 디지털 배포 방식의 출현 뿐만 아니라, 미래의 미야모토 시게루(나 킴 스위프트)처럼 되기를 간절히 바라는 그 누구에게나 명백히 게임 업계를 선도하고 있는 에픽이 그들의 플랫폼을 개방해 준 덕이기도 합니다.

오랜 기간 언리얼로 개발해 온 사람으로써 이 선두마차에 올라탔으니, 앞으로 수 개월간 UDK에 간단한 미니-게임 데모 시리즈를 올릴 계획입니다. 제 목표는 UDK 안에서 다양한 형태의 게임플레이를 구현하기 위한 방법 예제를 오픈-소스, 오픈-콘텐츠로 제공하여 커뮤니티를 활성화시키는 것입니다. 언리얼 토너먼트와 같은 풀-코스 요리보다는, 언리얼 초보 개발자가 더욱 쉽게 소화시킬 수 있는 비교적 간단한 코드와 콘텐츠가 될 것입니다.

사설이 길었으니, 자 밥 먹으러 가 봅시다! (음, 진짜 배가 고프긴 하군요.)

어떤 종류의 게임으로 시작할 것인가를 고려할 때를 살펴보니, 몇몇 분들은 언리얼이 다양한 종류의 3인칭 게임에 적합한가 걱정하시던데, 방법만 알면 아주 쉬운 일입니다. 그리고 방금 PixelJunk Monsters 를 한 판 하고 왔더니, 어째 "타워 디펜스"(Tower Defense) 분위기에 젖었습니다. 건물같은 것을 전략적으로 세워 떼로 몰려드는 적을 무찌르는 방식, 분명 재밌습니다.

그렇기에 처음 파 보려는 미니 게임은 "던전 디펜스"(Dungeon Defense)라 부를 것이며, 약간 (거의 3/4 수준의) 내려보기형 액션/타워-디펜스 하이브리드 게임이 될 것입니다. "소서러의 제자"인 작은 메이지(Mage)로써, 스승님이 자리를 비운 사이 보금자리를 지켜 내는 역할을 수행하게 됩니다. 다양한 "소환" 마법으로 보금자리 여기저기에 마법 방벽을 세우고, 방어가 밀릴 때는 마법 지팡이(Staff)로 적을 날려버려야 합니다. 액션 택틱과 자원-관리 전략이 혼합된 멋진 하이브리드가 될 것이며, 언리얼로 이런 정도 구현이야 단숨에 가능할 것입니다.

그래서 주말 새에 애셋과 콘트롤 스카마를 계획하는 디자인 작업을 한 후, 오늘 공식적으로 프로그래밍을 시작했습니다. 먼저 GameInfo, Pawn/Player, Player Controller, Camera 클래스 구현부터 시작했습니다. 그 각자의 역할을 설명해 드리겠습니다:

GameInfo 클래스에는 게임의 전체적인 규칙이 담겨 있으며, 지금까지 저의 경우 자체 커스텀 PlayerPawn 및 PlayerController 클래스 내에 스폰하기 위한 디폴트 값을 덮어썼습니다 (1). 월드의 물리적인 캐릭터는 물론 PlayerPawn 이며, 그 PlayerPawn 상의 게임플레이 액션 속으로 사용자 입력이 전송되는 곳은 Controller 클래스 내입니다. 카메라 클래스는, 카메라의 위치를 폰 위로 옮겨 UT에서처럼 눈에서 바로 보게 하지는 않았으며, 또한 플레이어가 바라보는 방향으로 약간 동적으로 오프셋시켜주기 위해 UpdateViewTarget 함수를 변경하였습니다. 약간씩 타겟 방향으로 회전하도록 하기 위해서입니다 (2). 에픽의 내장 RInterpTo 및 VInterpTo 함수를 활용하여 Rotator 및 Vector 보간을 각각 처리하였는데, 언제나처럼 간단한 일이었습니다. 이를 통해 카메라를 플레이어 현재 위치 (및 방향)에서 약간 띄울 수 있어, 그 위치에 딱 고정시키는 것보다 조금 부드러운 느낌을 냅니다.

PlayerController 에서 PlayerMove 함수를 (캐릭터의 방향을 2D 평면상에 제한시키기 위해) Rotation Yaw 만 바꾸고, Pitch 는 놔두도록 변경했습니다. 처음에는 그냥 마우스 델타를 사용하여 마우스를 스크롤하는 매 프레임마다 약간씩 Yaw 를 바꾸도록 했었으나, 그리 자연스럽지 못했습니다. PC에서는 정확도가 너무 떨어지는 것이었습니다. 가리키는 방향을 캐릭터가 직접 향하도록 하는 것이 맞겠지요? 그래서 현재 마우스 위치의 "캔버스 반투영"(canvas deprojection) 결과를 구하는 고유 코드 약간을 작성하고나서, 월드 상의 어디를 사용자가 가리키고 있는지를 알아내기 위해 월드에 대해 그 3D 벡터 광선투사(raycast)를 하고, 캐릭터가 그 지점을 향하도록 합니다. (3)

물론, 입력 방향을 카메라 회전에 의해 변형해 주기도 했는데, 그래야 입력이 직관적인 "카메라 상대적" 이동이 되기 때문입니다 (vector>>rotator 연산자에 해당하는 유용한 함수 TransformVectorByRotation 기억하십시오). 복수의 커스텀 풀-바디 애니메이션간의 블렌딩을 위해 재밌는 애니메이션 트리 작업을 약간 해 준 것 빼고, 플레이어-폰은 디폴트 폰과 다를 것이 거의 없었습니다. 그 작업 덕에 여러 애니메이션을 연속해서 재생해 봐도 튀어보이지 않았습니다 (4). 애님 트리 시스템 작업을 하다 보니, 언리얼 툴 모음집을 갖고 노는 것이 이렇게 재밌는 것이었던가 의아하기도 했습니다. 캐릭터 블렌딩을 실시간으로 확인해 가며 설정할 수 있으니 하드코딩할 필요도 없고요!

아무튼 그 작업이 두 시간 정도밖에 걸리지 않아, 작업 끝~ 을 외칠 수는 없었습니다. 다음으로 메이지의 지팡이 무기 작업에 착수했습니다. Weapon 을 서브클래싱하여 누를 때 무기를 "충전"하도록, 그리고 놓을 때 발사하도록 (지팡이에 다양한 충전 공격을 지원하도록) 변경했습니다 (5). 그리고서 Projectile 클래스도 서브클래싱하여 다양한-세기의 발사체도 지원하고, 순차적으로 비주얼 스케일도 전부 알맞게 조절했습니다. (6)

클래스를 직접 참조하는 것보다 "아키타입"(Archetype) 참조를 스폰하면 훨씬 편하다는 것도 말씀드려야 겠습니다 (7) -- "Spawn" 함수에 Archetype 을 지정하면 소위 "액터 템플릿"이 됩니다). 게임플레이에 대한 아키타입을 스폰하고나면, 게임플레이 오브젝트의 라이브러리 전체를 에디터에서 실시간으로 설정할 준비가 완료된 것입니다. 값을 조정하려 할 때마다 매번 "Default Properties" 들락거리지 않고도 말이지요. 또한 값을 시각적으로 설정하고, 미디어를 맞바꾸고, 코어 클래스는 같으면서 속성만 다른 변종을 여럿 만드는 등의 작업이 훨씬 쉬워집니다. 아키타입의 능력에 대해서는 나중에 자세히 다루도록 하겠으나, 반복작업에 큰 도움이 된다는 것으로 충분합니다. 그리고 물론, "-wxwindows" 인수를 붙여 실행시킬 수 있는 "리모트 콘트롤"(Remote Control) 역시 실시간 반복처리에 정말 유용한 툴입니다. 다른 글에서 리모트 콘트롤의 능력에 대해 더 자세히 얘기해 보도록 하겠습니다.

다음으로 첫 적 고블린(Goblin)의 AI 콘트롤러(AI Controller) 작업에 착수했습니다. (모든 액터를 기반으로 타게팅 가중치를 반환하는 "인터페이스"(Interface)를 구현하여) 대상을 고르는 "상태 로직"을 약간 작성했습니다 (8). 그리고서 길찾기(pathfinding) 수행 시점, 대상으로 직접 이동하는 시점, 길찾기/이동을 멈추고 공격을 시작하는 시점을 결정합니다 (9). AI 스크립트에 대해서는 나중에 자세히 다루겠습니다. 그리고 고블린 적에 구현한 재치있는 MeleeAttack 상태는, 현재와 이전 "손 소켓" 위치 사이의 매 프레임마다 추적(박스 스윕)을 끄고/켜고자 애니메이션 통지를 사용합니다 (10). 이를 통해 하드코딩된 피해값이 아닌, 애니메이션과 그 타이밍에 따라 고블린이 휘두르는 영역에 실제적인 피해를 입힐 수 있는 것입니다. 또한 현재 휘두르기에 맞은 액터 목록을 유지하고 그것에 대해 검사하여, 고블린이 "추적된"(traced) 액터를 한 번 휘두를 때 한 번만 피해를 입히게도 했습니다 (11). 모두 완료하고 나니 이 근접 공격은 꽤나 그럴듯해 져서 애니메이션이 시사하는 바를 정확히 전달해 주는 것 같았습니다.

그리고는 적을 공격하는 기본 "타워 터렛"(Tower Turret)을 구현하지 않을 수 없었죠. 이러한 단순 비이동 액터의 경우 AI 콘트롤러는 신경쓸 필요가 없고, 단지 타이머(Timer)를 통해 대상을 집어 주기만 하면 됩니다 (상태 로직은 콘트롤러 뿐만 아니라 어떤 오브젝트에 사용해도 된다는 점도 기억해야죠). (12). 터렛 윗부분이 선택한 대상을 바라보도록 하기 위해, 이 터렛의 애니메이션 트리에 LookAt Bone Controller 역시 추가했습니다 (13). 애니메이션 트리 구성을 마치고 나서, 어디를 볼 것인지 코드 한 줄 추가한 것이 전부였습니다. 예!

게임플레이 모습이 정말로 잡혀가기 시작하면서, 적들의 주요 목표로서 부수려 하는 "크리스털 코어"(Crystal Core)를 계속해서 구현했습니다 (14). 대상화 가능 액터용으로 만든 '인터페이스'를 사용하여 (15) 코어에다 특히 높은 우선권을 주었으니, 적들은 플레이어나 타워보다는 코어쪽에 전념하게 됩니다. '인터페이스'를 통해 클래스가 완전히 다른 액터도 같은 방식으로 상호작용 및 검사 가능하도록 공용 메서드 세트를 공유시킬 수 있습니다. 즉 "크리스털 코어" 클래스가 "플레이어 폰"(Player Pawn)에 계층적으로 직접 관련되어 있지는 않지만, 둘 다 똑같이 공유된 인터페이스가 제공하는 타게팅-가중치 함수를 구현할 수 있습니다. 적 AI가 어떤 개체를 우선 대상으로 잡을 지 결정하기 위해 일반적으로 접근할 수 있는 것입니다. 멋지죠!

그리고 마지막으로 프로젝트의 리드 아티스트 모건 로버츠(Morgan Roberts)가 메이지의 보금자리를 꽤나 멋지게 표현해 내는 테스트 레벨을 만들었고, 제가 키즈멧 설정을 조금 해 줘서 반복적으로 스폰되며 코어를 공격하기위해 진군하는 적 무리를 만들었습니다 (16). 그렇게 거의 하루만에, 본질적으로는 플레이 가능한 프로토타입이 생긴 것입니다.

게임플레이가 벌써 너무 도전을 요하는지라, 앞으로 며칠은 균형을 맞추는 작업은 물론, 추가적인 기법도 다량 구현해야 할 것이고, 가다듬는 작업도 해야 할 것입니다. 언리얼의 뛰어난 툴 덕에, 이러한 것들의 구현 작업이 그저 즐겁기만 했습니다!

다음 글에서는 위에서 간단히 얘기한 주제 중 여러가지 것들에 대해 자세히 다뤄 보고, 여러분의 흥미를 유발하거나 요긴하게 쓸만하다 생각되는 코드나 함수성들을 약간 살펴 보기도 하겠습니다. 그리고 괜찮다 싶은 비주얼이 나오기 시작하면, 스크린샷도 보여 드리겠습니다. 지화자, 금방 여러분께 이 게임을 선사해 드릴 날을 고대하도록 하겠습니다.

이제 아까전부터 뇌리를 떠돌던 피자를 먹으러 가야 겠습니다...

- 제레미(Jeremy)

블로그 1: 파일 참조

이 블로그에 언급된 정보는 아래 나열된 파일에서 온 것으로, 던전 디펜스 소스 코드의 일부이기도 합니다. 쉼표로 구분된 줄 번호는 파일 속 여러 개의 개별 라인을 말합니다. 하이픈으로 분리된 줄 번호는 파일 내 줄 범위를 말합니다.

  1. Main.uc: 618, 638
  2. DunDefPlayerCamera.uc: 240 - 248
  3. DunDefPlayerController.uc: 1561 - 1652
  4. DunDefPawn.uc: 222
  5. DunDefWeapon_MagicStaff.uc: 111
  6. DunDefProjectile_MagicBolt.uc: 24
  7. DunDefInventoryManager.uc: 13
  8. DunDefEnemyController.uc: 222
  9. DunDefEnemyController.uc: 764
  10. DunDefGoblinController.uc: 52
  11. DunDefGoblinController.uc: 32
  12. DunDefTower_ProjectileType.uc: 99
  13. DunDefTower_ProjectileType.uc: 95
  14. DunDefCrystalCore.uc: 19
  15. DunDefTargetableInterface.uc: 15
  16. DunDef_SeqAct_BasicActorSpawner.uc: 11

블로그 2: 3 일차

던전 디펜스 작업 2, 3일차에는 조준 기법, AI 이동 개선과 월드에 방어 "타워"(Tower)를 놓기 위한 직관적인 시스템의 바탕을 추가하는 데 초점을 맞췄습니다. 각 구현에 대해 더 자세히 말씀드려 보겠습니다!

조준의 경우, 초기 마우스-기반 스키마는 단지 플레이어 캐릭터의 Yaw 를 마우스가 가리키는 곳을 향하도록 할 뿐이었습니다. 3D 조준만 아니라면야 괜찮습니다. (대부분) 내려보기 시점에서 Pitch 에 대한 입력을 직관적으로 결정해 내는 실용적인 방법이 없으니, 캐릭터가 Yaw 로만 회전하는 것입니다. 여기에서 문제점은 적이 플레이어 위아래에 있을 때, 짜증나게도 맞출 수가 없다는 겁니다. 그래서 두 가지 별개의 픽스를 구현했는데, 하나는 마우스 기반 스키마이고 다른 하나는 게임패드 스키마로, 꽤나 잘 작동했습니다. 마우스-기반 스키마에 대해서는 먼저 캐릭터의 Pitch를 계산하여 마우스 화면 좌표의 "반투영"(unproject) 광선과 충돌하는 지점을 조준하도록 했습니다. (1)

여담으로 "반투영"이란 화면에서 쏘는 선과 같이 2D 화면 공간에서 3D 월드 공간으로 변형하는 것을 말하며, "투영"(projection)이란 월드 스페이스에서 화면 공간으로 변형하는 것을 말합니다. 언리얼에서는 둘 다 Player의 HUD를 통해 접근 가능한 해당 Canvas 함수를 통해 이뤄낼 수 있습니다. (2)

어느 경우에도, 허리를 굽혀 올려 내려볼 수 있도록 이 Pitch 값을 캐릭터의 애니메이션 트리에 있는 Bone Controller 셋업에 물려줬습니다 (3). 이런 방식이 정확도가 가장 높으면서도 자연스러운 PC 게임 느낌이 났습니다.

그런데 캐릭터 주변을 가리킬 때면 캐릭터가 항상 내려다보려는 것에 약간의 작업을 해 줘야 했습니다. 기본적으로 발을 조준한다는 건데, 실제로 그걸 조준하고 있기 때문입니다. 플레이어 주변을 가리키고 있을 때, 그리고 조준중인 것과의 높이 차가 크기 않을 경우에는 그냥 앞을 보도록 하는 조건을 추가하기로 결심했습니다 (4). 이를 통해 커서가 캐릭터 가까이 있을 때 발을 쳐다보며 달리지 않게 할 수 있었습니다. 물론 조준점이 급격히 바뀌더라도 캐릭터가 가혹하게 움직이지 않도록, Bone Controller 에 설정된 Yaw 와 Pitch 사이에 보간도 해 줬습니다. 추가적으로 폰 안에 실제 조준점을 저장하고 (5), 웨폰에서 찾아봐 그 지점으로 발사체를 명시적으로 조준하게도 했습니다. 그 작업은 단순히 그 Rotation을 Rotator(TargetPoint-ProjectileSpawnPoint)로, Velocity를 Speed*Normal(TargetPoint-ProjectileSpawnPoint)로 설정하여 주는 것입니다 (6). 이런 식으로 PC 게임에서도 괜찮은 느낌의 정밀 사격이 가능하면서도, 원하던 수준의 단순한 아케이드식 내려보기 시점을 유지할 수 있었습니다.

게임패드 콘트롤 스키마에 대해서는, 약간 다른 작업을 해 줘야 했습니다. 플레이어에게 빠르고 정밀한 포인팅 디바이스가 없기에, 사용자가 가리키는 위치를 정확히 바라보게 할 필요는 전혀 없는 것입니다. 그래도 일종의 3D 조준 형태가 필요하기는 합니다! 그래서 최대 거리 내의 최적 타겟을 (있으면) 결정하여 조준하며, 그 대상의 위치로 조준점을 설정하는 자동-조준 함수를 구현하기로 결정했습니다 (7). 자동-조준 역시도 조준-점 시스템을 사용하기에, PC의 콘트롤 스키마용으로 만든 기존 "look at" 메서드에도 들어맞으며, 조준점이 선택되는 방식 에서만 차이가 납니다.

그래서 최적의 자동-조준 대상을 선택하고자, 자동-조준 범위 내의 모든 'enemy' 유형 액터를 수집하기 위해 Player 에서 OverlappingActors 검사를 시작했습니다. 그리고서 그 결과를 반복처리하여 후보 대상 중 어느것이 가장 가까운지 AND 바라보는 방향에 근접한지를 알아봅니다. 가장 가까이 있으면서 바라보는 방향( 계산식은 "Normal(TargetLocation - EyeLocation) dot Vector(Rotation)")에 근접한 대상은, 각각에 대한 허용치와 가중치 내에 있는 경우 이상적인 대상이 됩니다. 허용치와 가중치 조정을 마치고 나니 이 자동-조준 선택 방법은 잘 작동했으며, 이제 게임패드로 자신이 바라보는 곳( 내외)에 세로 자동-조준이 되는 것입니다. 또한 캐릭터 등뼈(Spine) 본 상의 Yaw 회전을 대상 쪽으로 약간 더해주어, 자동-조준 상의 dot-product 허용치로 인해 약간 측면으로 날아가는 듯 보이지 않도록 했습니다 (8). 게임패드 콘트롤 스키마가 이제 마우스에 준하는 수준이 되었습니다!

참고로 자동-조준 대상에 그리는 데 있어 DebugSpheres 를 사용했는데, 선택 방법이 얼마나 잘 작동하는지 알아보는데 도움이 되었습니다. 사실 저는 분석에 있어 항상 DebugSpheres, DebugLines, DebugBoxes 를 사용하는데, 프로토타이핑 단계에서는 이들을 잘 사용하실 것을, 심지어 클래스에 코드를 놔두어 그리도록 하는 것도 강력 추천합니다. 그냥 커스텀 "bDebug" 불리언으로 토글시켜 꺼 두고, 나중에 문제에 봉착했을 때나 추가 조정 작업을 하고싶을 때 다시 켜면 됩니다. 월드의 3D 작업에까지 지속되는 코드에 무슨 일이 벌어지는지 시각화시켜 볼 수 있다는 것은, 게임플레이 프로그래머에겐 작지만 커다란 함수성입니다.

다음으로는 AI 길찾기 루틴을 언리얼의 장기간 웨이포인트-패쓰노드 내비게이션 시스템에서 새로운 내비게이션 메시 시스템으로 변경하기로 했습니다. 이런 @#$%! 너무 쉬운데다 닳디 닳은 제 눈에도 너무나 엄청난 결과를 내는 것이었습니다. 단지 레벨에 파일론(Pylon) 액터를 내리꽂고, 패쓰를 빌드하면, 그 뒤의 무거운 작업들은 알아서 해 주는 것입니다. 계산이 (믿기 어려울 정도로 빠르게) 끝나자 마자, 이와같이 환경이 완벽히 인식된 경로 네트워크를 얻을 수 있었습니다:

ddblog2.jpg

레벨 구조가 변경되어도 패쓰노드 구성을 재생성할 필요 없이, 그냥 "패쓰 리빌드" 버튼을 클릭하기만 하면 놓아둔 파일론들이 가용 패쓰 전부를 재계산하는 무거운 작업을 알아서 해 줍니다.

메시 내비게이션 시스템을 실제로 사용하기 위한 코드 측면은 어떠한가 하면, 지극히 단순합니다 (다른 메시-기반 내비게이션 기술을 경험해 보고 하는 소리입니다). 이 정도의 코드만으로도, 본질적으로 AI 콘트롤러가 내비 메시로부터 내비게이션 결과를 얻어내는 데 필요한 전부인 것입니다:

function InitNavigationHandle()
{
   if( NavigationHandleClass != None && NavigationHandle == none )
      NavigationHandle = new(self) class'NavigationHandle';
}

event vector GeneratePathToActor( Actor Goal, optional float WithinDistance, optional bool bAllowPartialPath )
{
   local vector NextDest;

   //반환값을 목적지 액터 위치와 동일하게 설정
   //직접 도달 가능하거나 길찾기 실패시, 그냥 이것을 반환.

   NextDest = Goal.Location;

   if ( NavigationHandle == None )
      InitNavigationHandle();

   //액터에 직접 도달 불가능한 경우, 그에 향하는 다음 내비게이션 지점 검색 시도.
   //가능하면 그냥 그 위치를 반환하여 직접 이동.

   if(!NavActorReachable(Goal))
   {
   class'NavMeshPath_Toward'.static.TowardGoal( NavigationHandle, Goal );
   class'NavMeshGoal_At'.static.AtActor( NavigationHandle, Goal, WithinDistance, true );
   if ( NavigationHandle.FindPath() )
      NavigationHandle.GetNextMoveLocation(NextDest, 60);
   }

   NavigationHandle.ClearConstraints();

   return NextDest;
}

그리고서 상태 코드 안에:

//WithinRange 는 TargetActor 에서 약간의 거리만 검사하며,
//아니면 GeneratePath 가 가라는 곳으로 무작정 이동.

while(!WithinRange(TargetActor))
{
  MoveTo(GeneratePathToActor(TargetActor),none,30,false);
  Sleep(0);
}

그런 것을 AI 콘트롤러에 추가시키자(9), 제 AI 캐릭터가 실패 없이 전체 레벨을 돌아다닐 수 있었으며, 이동 효율도 매우 높았습니다. 에픽 덕에, 제가 할 일은 거의 없었습니다. ^_^

길찾기용으로 사용을 시작하긴 했으나, 내비게이션 메시의 용도는 훨씬 다양합니다. 들은 바로는 (선반을 덮는 등) 커스텀 이동, (피직스 오브젝트를 돌아 이동하는 등) 동적 장애물 회피, (엘리베이터나 열차같은) 동적인 이동 내비 메시 부분에 올라타기 등의 동작에 있어 AI에 그 주변 환경에 관한 부가 정보를 제공해 줄 수 있다고도 합니다. 나중에 그 기능을 좀 더 많이 탐구해 보고프며, 끌어다 놓기 식의 내비게이션 성능에 관해서라면 이 이상이 없습니다.

새로이-강화된 기능으로 길찾기를 처리하고 나서 마무리 작업에 착수할 수 있었는데, 바로 직관적인 타워 배치 기법을 고안하여 구현하는 것이었습니다. 타워 디펜스 미니-게임이니 이는 특히나 중요했는데, 던디의 생명은 요놈들을 월드에 박아넣는 것이 얼마나 자연스럽고 "재미"가 있는가 달렸다 해도 과언이 아니었습니다.

먼저 월드에 타워를 놓는 데 관련된 렌더링 및 논리 함수성 전부를 캡슐화시킬 액터 TowerPlacementHandler (TPH)를 만들기로 했습니다. TPH 스스로, 자신이 소유하는 액터로써 초기화시키기 위해 Player Controller 의 PostBeginPlay() 를 변경하였습니다. TPH 는 실제로 물리적인 표상이 없을 테지만, 타워-배치 모드일 때만 특별히 볼 수 있게 되는 컴포넌트를 갖게 될 것입니다.

PlayerController 에 PlacingTower 라는 상태(State)를 추가시켜(10), (State Stack 상으로 푸시되며), 모든 표준 게임플레이 입력을 잠가(, 또는 차라리 거의 빈 PlayerMove() 로 무시해) 버리고 관계있는 입력 이벤트만 TPH에 전달하여 처리시킵니다. 또한 합당한 PlacingTower State를 THP에도 (11), PlayerCamera 클래스에도 (12) 추가시켰습니다. PlayerController 가 타워 배치 모드에 들어갈 때, PlayerController 가 그 자체 PlacingTower States 안에 이 자식 액터 둘을 설정하는 것도 담당합니다.

타워-배치 모드에 있을 때는 PlayerCamera 를 좀 더 극단적인 내려보기 시점쪽으로 보간시키길 원했기에, 타워-배치 카메라 상태에 들어갈 때 기존 카메라 변형을 저장한 다음, 기존 값에서 새로운 "타워-배치" 카메라 값으로 (VLerp, RLerp) 보간하는 식으로 이루어 냈습니다. (13)

이제 정말 중요한 것은 TowerPlacementHandler 클래스 자체입니다. TowerPlacementEntry 라는 구조체를 새로 만들었으며, 여기에는 게임에 있게 될 타워 타입(Tower Type)에 대한 표상 세부(그 배치 메시나 콜리전 테스팅 규모, 타워를 최종적으로 놓을 때 실제로 스폰시킬 아키타입 따위)가 포함됩니다. TPH 에는 이 구조체의 배열이 있어, 놓을 수 있는 타워 각각을 정의합니다 (14). TPH 는 놓으려는 타워의 시각적 표상으로써 SkeletalMeshComponent 를 가지며, 이는 PlacingTower 상태에 들어갈(, 그리고 그냥 TPH 액터 자체를 숨김해제하여 드러낼) 때 상응하는 TowerPlacementEntry 에 따라 메시를 동적으로 설정하는 것입니다. (15)

이 타워-배치 메시 컴포넌트가 마우스를 따라오게 하기 위해서, 플레이어의 Pitch 계산에 했던 것과 비슷하게 그 Translation 을 마우스 화면 좌표의 반투영 교차점으로 설정했습니다. 진짜 충돌점을 찾지 못한 경우, 그냥 플레이어 액터 평면상으로의 수학적인 교차를 사용했습니다.(16) 또한 타워-배치 위치가 플레이어 액터 주변 일정 반경 내에 머물도록 범위를 제한시키기도 했습니다. (17)

이 범위를 월드에 시각적으로 확실히 내보이고 싶었기에, 플레이어 밑의 지오메트리 위에 투영되는 머티리얼-애니메이팅된 데칼로써 표현하기로 했습니다. TPH 에 (스폰가능 버전인) DecalActorMovable (데칼 액터 이동가능)을 추가시켰습니다: 하나 자식으로 스폰하고, TPH 가 타워 배치 상태에 있는지에 따라 그 표시여부 상태를 관리하고, 이 상태가 되었을 때 플레이어를 가지고 데칼 액터 위치를 잡습니다. 이 데칼 액터는 타워 배치 유효 범위의 원 모양 표시기로써 머티리얼을 사용합니다. (18)

현재 타워-배치 위치에 대한 콜리전 검사도 약간 구현했습니다. 즉 타워를 새로 스폰시킬 위치에 공간이 충분한지 확인해야 했던 것입니다. 그래서 배치 위치 주변, 좌/우, 앞/뒤로 (선택된 특정 'TowerPlacementEntry'에서 구한 크기값을 사용하여) 일련의 규모 추적(Extent Traces)을 추가시키고, 뭔가 걸리면 배치를 허용하지 않도록 했습니다. (19)

다음으로 현재 마우스 위치에 타워를 놓아도 되는지를 판단하는 시각 표시기가 필요했습니다. 자세히 말하자면, 표상 타워 메시가 놓을 수 있으면 녹색으로, 아니면 빨강으로 나타나길 바란 겁니다. 이 작업은 머티리얼 인스턴스 불변(Material Instance Constant, MIC)으로 하기로 했습니다. MIC는 (보통 Scalar Parameters, Vector Parameters, Texture Parameters 형태로 오는) "파라미터"(Parameter)를 사용하여 게임플레이 도중 머티리얼 값을 동적으로 변경시킬 때 사용하는 시스템입니다.

이 경우 타워의 머티리얼에 벡터 파라미터를 추가하여 색을 변경하기로 했습니다. 그 후 에디터에서 베이스 머티리얼로 머티리얼 인스턴스 불변을 만들고, 코드에서 Mesh.CreateAndSetMaterialInstanceConstant() 를 사용하여, MIC의 고유 사본을 메시 인스턴스에 실제로 할당할 수 있습니다. (아니면 MIC 안의 값을 변경한 경우, 해당 MIC를 사용하는 메시 전부에 영향을 끼치게 되며, 어떻게 사용하는가에 따라 문제가 될 수도 안될 수도 있습니다. (20) 이래놓고서 그냥 (재치있는 'Color' 라는 이름의) 벡터 파라미터를 현재 위치에 배치 "가능한가" 아닌가에 따라 초록 또는 빨강으로 설정했습니다. (21) 딱 원했던 기본적인 비주얼 피드백이 생겼습니다.

마지막으로 마우스 클릭 처리 / 플레이어 콘트롤러에서 전달된 버튼 입력을 실제 타워 배치로 승인 처리 등의 상태(State) 로직을 약간 추가하여 TPH 에 한데 모아 묶어야 했습니다. 이 작업은 Player Controller 의 PlacingTower 상태 안에 있는 각각의 입력 'exec' 함수를 덮어쓰고, 그것을 TPH 의 PlacingTower 상태에 상응하는 이벤트로 전달해 주는 것으로 끝이었습니다. (22) 함수의 상태 덮어쓰기는, 그 함수의 범용 목적 글로벌 버전을 지저분하게 만들지 않고도 상태-전용 함수성을 캡슐화하기 좋은 방식입니다. 플레이어가 타워를 놓을 곳을 결정하고 나면, 그 방향도 정할 수 있는 옵션을 주기로 결정했습니다.

그래서 PlacingTowerRotation 라는, PlacingTower 위에 푸시되는 상태를 만들었으며, (23) 여기에는 회전 처리를 위해 덮어쓰인 업데이트/인풋 메서드가 포함되고, 단순히 타워를 마우스-투영 방향으로 회전시킵니다. (24) PlacingTowerRotation 상태에서 처리되는 확인은 문제의 타워를 실제로 스폰시키고, (25) 전체 시퀸스를 완료하게 되는 (워드를 PlayerController 로 되돌려 보내고, 그 후 PlacingTower 상태를 Pop 시킨 다음 Camera 에게 같은 작업을 시켜, 플레이어에게 원래 콘트롤을 되돌려 주는) 것입니다. (26)

이로써 UDK 와 함께 한 재밌는 게임 개발 시간을 또 하루 보냈습니다. 상큼한 조준 스키마를 새로 구현하기 위해 마우스 & 게임패드 입력 작업을 하든, 에픽의 위대한 최신 길찾기 솔루션을 되는대로 다뤄 보든, 에디터에서 약간의 VFX/머티리얼 반복작업을 하든, 사용자-입력-주도 상태 계층구조 구성 작업을 하든... 뭘 하든지간에 간에 가장 중요한 것, 게임플레이에 초점을 맞추고 즐거운 작업에 임할 수 있었습니다.

블로그 2: 파일 참조

이 블로그에 언급된 정보는 아래 나열된 파일에서 온 것으로, 던전 디펜스 소스 코드의 일부이기도 합니다. 쉼표로 구분된 줄 번호는 파일 속 여러 개의 개별 라인을 말합니다. 하이픈으로 분리된 줄 번호는 파일 내 줄 범위를 말합니다.

  1. DunDefPlayerController.uc: 1575
  2. DunDefHUD.uc: 122
  3. DunDefPlayer.uc: 304, 329
  4. DunDefPlayerController.uc: 1611
  5. DunDefPlayer.uc: 304
  6. DunDefWeapon.uc: 134, 157
  7. DunDefPlayer.uc: 241
  8. DunDefPlayer.uc: 315
  9. DunDefEnemyController.uc: 938
  10. DunDefPlayerController.uc: 577
  11. DunDefTowerPlacementHandler.uc: 340
  12. DunDefPlayerCamera.uc: 109
  13. DunDefPlayerCamera.uc: 163-174
  14. DunDefTowerPlacementHandler.uc: 89-135
  15. DunDefTowerPlacementHandler.uc: 304
  16. DunDefTowerPlacementHandler.uc: 438, 474
  17. DunDefTowerPlacementHandler.uc: 468
  18. DunDefTowerPlacementHandler.uc: 219-238, 396-404
  19. DunDefTowerPlacementHandler.uc: 492-516
  20. DunDefTowerPlacementHandler.uc: 240-249
  21. DunDefTowerPlacementHandler.uc: 524-525
  22. DunDefPlayerController.uc: 706
  23. DunDefTowerPlacementHandler.uc: 691
  24. DunDefTowerPlacementHandler.uc: 786, 795
  25. DunDefTowerPlacementHandler.uc: 801
  26. DunDefPlayerController.uc: 602

블로그 3: 7 일차

안녕하세요.

던전 디펜스 팀이 지난 블로그 밑그림을 꽤나 크게 그려놨기에, 어디서부터 쓰기 시작해야 할지 감이 잘 잡히지 않습니다! 우선 지난 글 이후 며칠간에 걸쳐 저희가 이뤄낸 작업부터 개괄적으로 살펴 보고, 그 각각에 대해 자세히 들어가 보는 것으로 하겠습니다:

  • 적이 떨어뜨리며, 플레이어 주변에 있을때 진공-흡입되는 리짓 바디 "마나 토큰"(Mana Tokens)을 추가했습니다. 이는 타워를 소환하고 여러 마법을 시전하는 데 사용되는 게임 내 소모성 자원입니다.
  • 편집가능한 구조체 배열로 (데이터 주도 시스템) 일련의 아키타입을 통해 "마법 지팡이"(Magic Staff) 무기를 업그레이드하는 시스템을 추가했습니다.
  • 분할 화면 지원 및 동적인 로컬-플레이어 참가 기능을 추가했습니다.
  • (에픽의 기존 UI 애니메이션 인프라구조 위에 세워진) 에디터-주도 애니메이션 시스템 지원을 위한 커스텀 UI 클래스를 추가했습니다.
  • 함수성 플레이스홀더 UI 씬을 다량 추가했습니다: 메인 메뉴, 일시정지 메뉴, 게임 오버 UI, 개별 플레이어 HUD, 공유 글로벌 게임 정보 HUD, 로딩 화면 등.
  • 비동기 로딩 ("심리스 이동") 지원을 위한 게임 로직을 구성하여, 백그라운드로 레벨을 로드하면서 화면을 애니메이팅되는 이행 화면이 나타나도록 했습니다.
  • 새 캐릭터 애니메이션 노드 ("이동"으로 간주될 피직스 상태(Physics States) 지정은 물론 속력 곱수 제한용 옵션을 가진 BlendBySpeed 변종) 추가 및 플레이어-캐릭터 애니메이션 트리에 상체 블렌딩 지원을 추가했습니다.
  • AI 개선: AI가 대상에 일직선상에 있다 판단했을 때 길찾기를 중지하도록 하고, 주기적으로 이상적인 대상을 다시 찾아보도록 하며, 길이 "막혔"으면 다시 내비게이션 시스템 상으로 돌아가 이동을 시도하는 일종의 안전장치를 두었습니다.
  • 전체 "타워 디펜스" 게임플레이 사이클을 지원하기 위한 키즈멧 액션을 다량 추가했으며, 그 중에는:
  1. 괜찮은 잠복성(, 즉 "시간 경과에 따른") '웨이브 스포너'(Wave Spawner) 액션, 적 그룹을 나타내는 임의의 구조체 배열로부터 적을 시간 경과에 따라 스폰시키며, 플레이어가 특정 웨이브 완료했을 때에 해당하는 출력 링크도 포함되어 있습니다.
  2. 웨이브 간의 간격과 적의 수가 동적으로 조절되는 액션, 시간 경과에 따라 점차 게임의 난이도가 상승되게 합니다.
  3. UI를 여는 다양한 액션, 커스텀 정보를 전달해 줍니다.
  4. 코어가 실제로 죽은 직후 "패배 조건"(코어 파괴) 검사를 위한 이벤트, 그런 경우 일찍 컷씬을 시작시킬 수 있습니다.

자 그럼 위의 주제들 중 일부를 자세히 살펴 봅시다. 먼저 리짓 바디 마나 토큰부터 시작하죠.

꽤나 간단히 에픽이 제공한 KActorSpawnable 클래스를 상속했는데, 이는 (컨벡스 콜리전이 설정된) 스태틱 메시 컴포넌트 기반 액터에 리짓 바디 피직스를 적용하여 이미 구성이 완료된 것을 활용했습니다. (1)

자식 클래스의 디폴트 속성에서 그냥 "bWakeOnLevelStart=true" 로 덮어쓰(어 즉시 떨어뜨리도록 하)고, 그 bBlockActors=false 로 설정하여 플레이어가 막힘없이 오브젝트를 통과해 이동할 수 있도록 했습니다. 이 '마나토큰'에는 (그 아키타입에) 자그마한 보석같은 스태틱 메시를 주고서, "DunDefEnemy" 의 Died() 함수 속에다 (아키타입으로) 이것을 여러개 스폰시키도록 했습니다.(2) 또한 스케일된 랜덤 방향 벡터 VRand() 도 적용하여 떨어지는 토큰 각각에 자극을 주어 적으로부터 흩어지도록 했습니다. 플레이어 주변 범위 내의 마나 토근을 검사하여 있으면 "모으도록" 했습니다 (말하자면 토큰을 없애고 플레이어 콘트롤러의 총 '마나' 값에 더해주는 것입니다).(3) 마지막으로 모으기 위해 꼭 닿아야 할 필요성을 없애기 위해, (모든 토큰이 아닌!) 플레이어 주변에 주기적인 OverlappingActors (겹치는 액터) 검사를 추가하여 주변 토큰을 찾아내고, 거기에 플랙을 달아 플레이어 방향으로 포스(Force)를 적용하여 빨아들이도록 합니다. 그 속도가 플레이어 방향이지 않을 때 약간의 역방향 포스를 추가하여, 본질적으로는 플레이어 쪽으로 더욱 빠르게 움직이도록 하기 위한 "이방성 마찰"을 적용하도록 했습니다.(4) 대체적으로, 토큰이 날아다니기 시작하자 만족스러운 진공 효과라 할 만 했습니다.

ddblog3-1.jpg

게임플레이 도중의 무기 업그레이드 지원을 위해, PlayerController 에서 "Summoning Tower" 상태를 확장했습니다. (기본적으로 입력을 잠그고 플레이어 캐릭터가 소환 애니메이션을 재생하도록 합니다) (5) 이 자식 상태를 "UpgradingWeapon" 이라 했으며, 함수를 둘 정도 덮어써서 다른 애니메이션과 시각 효과를 재생하도록 했습니다.(6) 이런 식으로 원래 State 함수성 전부를 그대로 활용하면서도 신경쓸 함수성만 새로 구현해 줄 수 있었습니다. 게임플레이 프로그래밍에 있어 상태 계층구조란 엄청 유용한 개념이기에, 언어적 관점에서 볼 때 언리얼스크립트 고유의 독특한 특성입니다! 이렇게 "Upgrade" 버튼을 누르면 플레이어가 고유 애니메이션을 재생하는 상태로 들어가게 만들었으니, 이제 무기에 실제로 무언가 작업을 해 줘야 하겠습니다.

"Weapon Upgrade Entries"(무기 업그레이드 항목)이란 구조체 배열을 추가시켰습니다. 여기에는 각 업그레이드 레벨에 대한 다음과 같은 정보가 포함됩니다: 마나 비용, 설명, 업그레이드에 걸리는 시간, 업그레이드가 완료되면 스폰시켜 플레이어에게 쥐어줄 무기에 대한 실제 무기 아키타입 참조.(7) 제가 왜 클래스가 아닌 (값만 저장되는) 구조체를 사용했을까요? 그것은 구조체는 에디터의 속성 에디터 내에서 동적으로 만들 수 있기에 에디터 내에서 바로 'Weapon Upgrade Entries' 값을 구성할 수 있고, 전체 시스템을 데이터 주도형으로 유지시킬 수 있기 때문입니다.

다음으로 "열거형"(enum)을 추가하여 지원되는 각 업그레이드 (최대 5) 레벨에 해당하는 항목을 포함하도록 한 후, 플레이어가 업그레이드할 때마다 단순히 다음 (현재 번호 + 1) 열거형 값을 선택한 다음, 그것을 구조체 배열에서 다음 "Weapon Upgrade Entries"을 구하기 위한 인덱스로 사용했습니다. 그리고 PlayerController 안에서 업그레이드 항목 구조체에 지정된 기간동안 단순히 (업그레이드 애니메이션을 반복 재생하며) "Upgrading Weapon" 상태로 대기하다, 시간이 지나면 (예전 것을 없애고) 새 무기 아키타입을 스폰시킵니다.(8) 모두 잘 작동했으며, 모든 값이 PlayerController 아키타입의 "Weapon Upgrade Entries" 구조체 배열에 포함되어 있다는 사실 덕에, 무기 업그레이드 비용 및 시간에 관련된 것들을 미세조정하기 위한 반복처리 작업을 리모트 콘트롤을 통해 에디터에서 실시간으로 할 수 있었습니다. 엄청 효율적이겠죠?

ddblog3-2.jpg

한 명만으로도 게임이 충분히 재밌어지기 시작했는데, 네 명이 같이 하면 네 배( 정도는 ^_~) 재밌을 테니, 분할 화면도 지원해야겠다 싶었습니다!

분할화면 멀티플레이 지원 역시도, 에픽이 제공하는 강력한 프레임워크 덕을 다시 한 번 봐서 정말 간단히 해결했습니다. 아직 플레이어에 연결되지 않은 콘트롤러에 대해 "press start" 입력을 다뤄 놓고, 그렇게 새로 생긴 콘트롤러 ID를 가지고 "CreatePlayer" 함수를 호출해 주기만 하면 되는 것이었습니다. 플레이어가 없는 게임패드에 대한 "Press Start" 입력은 Input 의 서브클래스 내 InputKey 함수에서 처리했습니다. 플레이어가 게임패드의 시작을 누르면, 이 키 이름이 InputKey 함수에 전달되고, 거기서 그에 대응하는 ControllerID 를 가지고 CreatePlayer 를 호출합니다.(9) 이 새로운 Input 클래스를 InsertInteraction() 함수를 사용하여 ViewPortClient 클래스의 "Interaction" 목록에 추가시킨 것이 다였습니다.(10) 2번 플레이어가 시작을 누르고, 둘째 PlayerController 를 뽑아내어 Player-Pawn 연결시키니 뷰포트가 자동으로 그에 맞게 분할됩니다 (화면이 분할되게 하지 않으려면, ViewportClient 클래스의 UpdateActiveSplitscreenType() (11) 함수를 덮어쓰면 되며, 그런 경우 첫 플레이어의 카메라 시점이 그려지게 됩니다). 방금까지 1인용 게임이었던 것이 동적으로 여러 명의 로컬 플레이어가 즐길 수 있게 된 것입니다! 온라인 멀티플레이어의 경우 액터 리플리케이션 시스템을 사용하여 추가적인 작업을 해 줘야 하나, 에픽이 제공하는 프레임워크 덕에 그리 큰 부담은 못됩니다. 그에 대해서는 나중에 다루도록 하겠습니다.

ddblog3-3.jpg

다음으로 완전히 플레이가능한 시스템으로써 기능할 수 있도록 게임에 기본적인 기능을 하는 유저 인터페이스에 달려들기로 했습니다. 메인 메뉴에서부터 승리 화면까지, 그리고 단일 레벨 이상의 것까지 말입니다. UI 애니메이션 시스템을 살펴 봤는데, 매우 강력했지만 DefaultProperties 를 통해서만 수정 가능했습니다. 그래서 언리얼스크립트의 힘을 빌어, 이 UI 애니메이션 클래스의 값을 구조체로 감싸 확장된 UIScene 클래스 내에서 편집가능하게 만들었습니다.(12) 커스텀 UIScene 활성화 시점에서, 이 구조체 값을 동적으로-생성된 UI 애니메이션 오브젝트 속에 복사했습니다.(13) 이리하여 에픽이 만든 기존 UI 애니메이션 시스템을 그대로 사용하면서도, 에디터에서 애니메이션 값을 수정하며 실험해 볼 수 있는 이점을 얻게 되었습니다.

이 함수성을 새로 도입하고서, 어떤 플레이스홀더 UI를 다량 만들었습니다. (HUD 클래스에 의해 열리는) Player HUD UI (14) 처럼 이들 중 일부는 각 플레이어의 뷰포트에 그릴 것이지만, 나머지는 전역적인 전체화면의 것으로 한 플레이어에게 소유되지 않는 것입니다. 지속적인 게임 상태(건설 단계에 시간이 얼마나 남았는지, 전투 단계에 적이 얼마나 남았는지 등에) 직접적으로 기반하는 이러한 Global UI를 표시하기 위해 GameInfo 클래스에 약간의 함수를 작성했습니다.(15) UI의 것에 대해 (에디터 내에서 조정되는) 작고 어울리는 (플레이스홀더) Open / Close 애니메이션을 만들었습니다.

ddblog3-4.jpg

여기에 만족하고 나니, 로딩 UI도 애니메이팅되게 하여(16), 메인 메뉴(, 실제로는 메인 메뉴 UI를 여는 레벨)에서 게임플레이 레벨로 말끔히 전환되게 하기로 했습니다. 일종의 '잠시도 지루할 틈이 없게' 하기 위함인 것입니다. 이는 다른 레벨을 임시 "전환"(transition) 맵으로 사용함과 동시에 백그라운드로 레벨을 로드하는 에픽의 함수성, SeamlessTravel 을 사용하면 됩니다. 저의 경우 전환 맵은 로딩 화면 UI 씬을 여는 것으로, 백그라운드에서 목적 레벨이 완전히 로드되어 전환 맵이 닫힐 때까지 이 맵이 표시됩니다. 그저 WorldInfo.SeamlessTravel(17) 을 호출하기만 하면 INI 에 지정된 Transition Map 이 도입되고, 백그라운드로 최종 목적 레벨이 로드되는 것입니다. 단순하고 강력하죠.

ddblog3-5.jpg

물론 "레벨 스트리밍"이라는, 게임플레이 진행에 따라 레벨 일부(방에 처음 들어설 때 건물 내부) 로드 및 오래된 부분(건물 내부에 들어설 때 외부 월드) 언로드하는 등 레벨 일부를 동적으로 스트리밍시키는(흘려들이고 보내는) 것을 말합니다. 월드가 큰 게임에 특히나 유용한 별개의 프로세스로, 키즈멧과 월드 에디터 자체적으로 처리되며, 자세한 것은 다음의 에픽 문서를 참고하시기 바랍니다: Level Streaming HowTo KR

다음으로 캐릭터 이동 애니메이션 비율(rate)을 이동 속도에 맞춰 동적으로 조절해 주면 좋겠다는 생각이 들었습니다. 에픽은 이미 이런 기능을 지원하는 애니메이션 트리 노드 BlendBySpeed 를 제공하고 있으나, 여기에 약간의 함수성을 추가하고 싶었습니다: 플레이어가 특정 물리 상태(, 말하자면 땅을 걷는 상태)에 있을 때 속력 스케일만 조절하고, 비율 스케일에 최대치를 두어 플레이어가 어떤 이유로 (폭발 등을 통해 큰 동력을 얻었다든가 해서) 너무 빨리 움직여 버려 결과적으로 이동 애니메이션이 별나 보이지 않도록 하고 싶었습니다. 다행히 이 작업은 간단했는데, 에픽의 "AnimNodeScalePlayRate" 에서 새 애니메이션 노드 클래스를 상속한 다음, 거기에 Tick 함수를 추가시키고, Tick 함수에서 그 오너 스켈레탈 메시의 액터 현재 속력을 (관심있는 클램핑 및 피직스 검사를 수행하여) 알아봅니다.(18) 이러한 틱 함수를 지원하고자 TickableAnimNode 인터페이스를 만들었으며(19), 폰이 노드를 틱하는 것을 알 수 있도록 Pawn 클래스 내 OnBecomeRelevant() 함수에서 노드를 등록(시키고, OnCeaseRelevant() 에서는 등록-해제)시켰습니다. Engine 의 베이스 클래스를 자체적으로 확장하고 거기에 언리얼스크립트로 새 함수성을 추가하여 그 프레임워크 대부분의 능력을 얻어낼 수 있으며, 키즈멧 함수성 추가를 시작할 때 명백한 것이기도 합니다. 바로 이어서 그 작업을 해 주었습니다!

(캐릭터의 상체에만 재생되도록 필터링된 CustomAnimation 노드도, 그 부모로 'AnimNodeBlendPerBone' 를 사용하고 'Spine' 본에서부터 위쪽으로 필터링하도록 설정하여 추가시켰습니다. 그리하여 다리는 독립적으로 움직이면서도 반응성 애니메이션을 재생할 수 있는 캐릭터가 된 것입니다.)(20)

기능성 UI가 있는 로컬 멀티플레이어와 함께 기본적인 자원 및 무기 업그레이드 시스템을 처리하고 나니, 전부 종합하여 시작부터 끝까지 플레이 가능한 "타워 디펜스" 게임 사이클을 만들고 싶어졌습니다. 제대로 하려면 약간의 레벨 스크립팅이 필요할 것입니다 (하드-코딩할 수는 있지만, 다른 게임타입이나 레벨에 확장할 수가 없게 되니 불구스럽다 하겠습니다!). 그리하여 "건설 후 전투" 사이클을 구동시키고자 키즈멧을 사용하기로 했는데, 이는 본질적으로: 플레이어에게 건설할 시간을 (UI를 통해 알려) 주고, 적 웨이브를 스폰하(고서 남은 수를 UI를 통해 알리)며, 이 사이클을 적의 수/간격과 건설 시간을 순차적으로 조절해 가며 반복시켜 게임의 난이도를 조금씩 상승시켜, 결과적으로 전멸에 이르게 되어야 겠지요? 하하! ^~^

우선 먼저, 적 웨이브를 스폰하는 데 있어, 즉시 완료/출력되지는 않고 시간 경과에 따라 내부적으로 업데이트되다가 내부 로직을 통한 특정 시점에만 완료되는, 잠복성 키즈멧 액션을 사용하고 싶었습니다. 그래서 "Enemy Wave Spawner" 액션을 ('Update()' 함수를 구하고자 SeqAct_Latent 를 확장하여) 만들었으며(21)), 여기에는 구조체 배열이 다수 있어 각 구조체에는 액션이 시작된 이후 일정 시간이 지난 다음에 출현하는 적 웨이브가 정의됩니다.(22) 적 웨이브를 모두 물리친 다음에야 "Wave Spawner" 키즈멧 액션이 완료되고, 그 최종 출력이 활성화됩니다.(23)

특히나 재밌는 부분은 이렇습니다. 'Wave Entries' 구조체 배열을 그냥 액션의 속성 내에서 직접 수정 가능하게 만들 수도 있었지만, 이 값들은 다수의 스포너 사이로 전달해 가며 차례로 증가시키고 UI에서 ("남은 적의 수") 정보로 처리시키기도 해야 함을 알고 있었습니다.(24). 그래서 새 Kismet Variable 클래스 SeqVar_EnemyWaveEntries 를 만들어, 그 자체에 구조체를 포함시키도록 하기로 결정했습니다.(25) Kismet Variable 오브젝트가 Wave Spawner 키즈멧 액션으로의 Variable Input 으로써 받는 것으로, 그 이후 자체적으로 사용하기 위해 그 구조체를 복사합니다.(26)

Wave Spawner 액션 내에서 직접 수정 가능한 값으로 하기 보단, Wave Entries 구조체를 감싸기 위해 Kismet Variable 오브젝트를 사용하였기에, Wave Entries 값을 키즈멧에서 시각적으로 전달할 수 있었습니다. 이런 식으로 키즈멧에서 수동 작성한 'ScaleEnemyWave' 액션으로 'Wave Entry' 변수를 연결시킬 수 있었습니다. 'ScaleEnemyWave' 는 Wave 항목과 거기에 곱해 줄 적의 수와 시간 간격을 플로트를 입력으로 받습니다.(27) 전투 사이클 이후 'Multiply Float' 키즈멧 액션으로 이 플로트 스케일을 변경하여, 한 판마다 게임의 난이도를 순차적으로 올릴 수 있었습니다. 추후 웨이브에 랜덤 아키타입 값을 갖도록 하(여 어떤 적을 마주치게 될 지 알 수 없게 만든다)거나, 스케일에 RandomFloat 를 사용하여 스폰되는 적의 양이 항상 약간씩은 편차가 나도록 하는 등, 이 시스템에 추가 작업을 할 계획입니다.

ddblog3-6.jpg

결론은 키즈멧 덕에 '에디터에서 플레이' 반복처리로 레벨 난이도를 조절할 수 있었으며, 이정표격 웨이브마다 부가 이벤트를 추가시키는 등 (예로 5판마다 보스가 등장한다던가 하는 것도, 게임플레이 오브젝트가 아키타입이니 특히나 간단합니다) 좀 더 독특한 시퀸스 구성도 가능합니다. 앞으로는 건설-전투-웨이브 사이클을 조정하는 것도, 키즈멧을 통해서라면 재미있는 디자이너-주도형 체험이 될 것입니다. 그러면 다음 시간까지... 닥 개발!

블로그 3: 파일 참조

이 블로그에 언급된 정보는 아래 나열된 파일에서 온 것으로, 던전 디펜스 소스 코드의 일부이기도 합니다. 쉼표로 구분된 줄 번호는 파일 속 여러 개의 개별 라인을 말합니다. 하이픈으로 분리된 줄 번호는 파일 내 줄 범위를 말합니다.

  1. DunDefManaToken.uc: 8
  2. DunDefEnemy.uc: 208
  3. DunDefPlayer.uc: 351
  4. DunDefManaToken.uc: 62
  5. DunDefPlayerController.uc: 812
  6. DunDefPlayerController.uc: 1266
  7. DunDefPlayerController.uc: 69
  8. DunDefPlayerController.uc: 1316-1320, 1280
  9. DunDefViewportInput.uc: 15
  10. DunDefViewportClient.uc: 474
  11. DunDefViewportClient.uc: 226
  12. DunDefUIScene.uc: 11
  13. DunDefUIScne.uc: 36
  14. DunDefHUD.uc: 27
  15. Main.uc: 223, 333, 482, 132
  16. Main.uc: 482
  17. Main.uc: 488
  18. DunDef_AnimNodeScaleRateBySpeed.uc: 17
  19. DunDefPawn.uc: 285
  20. DunDefPlayer.uc: 163
  21. DunDef_SeqAct_EnemyWaveSpawner.uc: 162
  22. DunDef_SeqAct_EnemyWaveSpawner.uc: 14
  23. DunDef_SeqAct_EnemyWaveSpawner.uc: 198, 231
  24. DunDef_SeqAct_OpenKillCountUI.uc: 31
  25. DunDef_SeqVar_EnemyWaveEntries.uc: 10
  26. DunDef_SeqAct_EnemyWaveSpawner.uc: 176
  27. DunDef_SeqAct_ScaleEnemyWave.uc: 53

블로그 4: 10 일차

안녕들 하셨습니까, 담대한 언리얼 교도들이시여!

지난 블로그 이후 며칠간 저희 소규모 팀은 연휴마저 불태우며 엄청난 진전을 이루어 냈습니다. 먼저 개괄적으로 말씀드린 다음, 각각의 주제에 대해 세밀히 들어가 보도록 하겠습니다:

  • 멋진 캐릭터 디자인에 맞는 첫 아트워크(스켈레탈 메시)가 생겼습니다! 치키 판타지 클리셰 만세! 이제 애니메이션 용으로 리깅만 하면 파멸의 UT 로봇을 대체할 수 있을 것이며, 게임의 자체적인 스타일 감이 살아날 것입니다. 환경에 대한 추가 작업에 더해, 기본 "Mage Staff" 무기 모델을 구했으며, 임시 비주얼 이펙트를 가지고도 잘 작동했습니다.
  • 어쩔 수 없이 메인 메뉴에 재치있는 Render Target 사용처를 구현했습니다. 즉, 이제 메인 메뉴에 1-4 플레이어에 대한 각 "플레이어 캐릭터" 애니메이션 이미지를(, 결과적으로는 메인 캐릭터를 색만 바꿔) 표시하여 게임에 "접속한" 사람을 나타내 줍니다. 이 메뉴에 있을 때는 언제든 "시작" 을 누른 플레이어가 게임플레이에 이어 접속할 수 있게 되며, 이는 (미선택 상태일 때는 'idle' 로 비활성화되는) "렌더 투 텍스처" 캐릭터가 반응하여 재생되는 '활성' 애니메이션 안에 반영됩니다. 깔끔하죠!
  • 메인 메뉴를 좀 더 가지고 놀다가, 커서의 위치에 파티클을 방출하는 작은 Canvas 파티클 시스템을 만들었습니다. 이 시스템은 앞으로 다른 UI 효과에도 사용할 수 있을 것입니다.
  • 마티네도 직접 손을 봐서 게임플레이-인트로와 게임-오버 시네마틱을 구현했으며, Player Controllers 에다 적절한 입력-방지 상태를 구성하여 시네마틱 도중에는 이동/발사 불가능하도록 했습니다. 플레이어가 Start/Escape 키를 누를 때 시네마틱을 건너뛰는 커스텀 솔루션도 구현해 놨습니다.
  • 플레이어 HUD 작업, 즉 머티리얼-기반 체력/진행상황 바 (커스텀 UI 콘트롤), 상태-반응형 마법 아이콘, 애니메이팅되는 마나 토큰 지시자 같은 것들을 전부 구현했습니다. 또한 타워/코어 위로 떠다니는 체력 바에 대한 HUD 오버레이 (동적 캔버스 드로)는 물론, 코어가 공격받을 때 그 방향을 가리키도록 웨이포인트를 회전시키는 것도 구현했습니다. 워, 이 모든 것이 2-4 분할화면에서도 올바르게 재생됩니다.
  • 무기에 대한 임팩트 데칼을 에픽의 매우-강력한 "머티리얼 인스턴스 시간 가변"(Material Instance Time Varying, 참 길죠?) 시스템을 활용해서 구현했습니다.
  • 원거리-공격 적을 "궁수"(Archer) 유닛으로 하기 위한 기본 함수성을 구현했습니다. 원하는 대로 AI에 약간의 (예측조준, 고의 부정확, 그리고 일종의 "훼이크"(fudge-factor) 발사체 방향 등) 조정을 가했음에도, 상태 상속을 통하니 누워 떡먹기였습니다.

자 우선, 메인 메뉴의 렌더 타겟에 대해 알아봅시다. 누가 게임에 참가해 있는지 확인할 수 있도록, 그리고 게임플레이 시작 전 준비를 위해 모두가 메인 메뉴에서 참가할 수 있게 하기 위한 UI가 필요했습니다. (물론 게임플레이 도중에도 시작 버튼만 누르면 동적으로 새 플레이어를 추가시킬 수도 있습니다.) 또 누가 게임에 있는지를 시각적으로 나타내 주기 위한 깔끔 3D 캐릭터 역시도 필요했습니다. 그래서 메뉴 레벨의 환경에 스켈레탈 메시를 넷 추가시키는 작업부터 시작해, 배경 요소 없이 렌더링되도록 멀리 허공에 띄웠습니다. 그 각각의 앞에 "SceneCapture2DActor", 즉 씬을 보는 시야를 텍스처에 렌더링하는 카메라같은 액터를 놓아 각각 고유 렌더 타겟 텍스처를 사용하도록 할당했습니다.

ddblog4-1.jpg

그리고 이 텍스처 중 하나를 사용하는 머티리얼을 만들었습니다. 그 후 이 머티리얼의 머티리얼 인스턴스 불변을 만들어 베이스 머티리얼을 넷 씩이나 별도로 만들지 않고도 다른 렌더 타겟에 대한 텍스처 파라미터를 간단히 바꿔줄 수 있도록 했습니다. 머티리얼 인스턴스 불변이란 말 그대로, 머티리얼 전체 사본을 포함할 필요 없이 인스턴스-별로 값을 맞바꿀 수 있도록 고유 "파라미터"(, 보통 스칼라, 벡터, 텍스처 파라미터)를 가질 수 있는 머티리얼의 "인스턴스"를 말합니다. 이를 통해 게임 실행 도중에도 동적으로 반응하는 머티리얼 효과의 그런 파라미터 값을 동적으로 변경할 수도 있으며, 머티리얼 애셋 관리를 쉽게 만들어 주면서 메모리를 절약해 주기도 합니다. (값 조금 바꾸자고 전체 머티리얼을 복사할 필요가 없는 것입니다.)

ddblog4-2.jpg

Material 자체에서 Render Target 텍스처를 Emissive 출력으로 전달했으나, Opacity Mask 를 카메라에 사용하도록 설정해 둔 "초록" 배경색으로 클리핑하기도 했습니다. 그리하여 결과 이미지에는 캐릭터만 보이게 될 것이며, 배경은 보이지 않게 됩니다. 말하자면 머티리얼을 사용하여, 전체 렌더 타겟 텍스처의 사각형 이미지가 아닌 캐릭터의 메시만 픽셀 단위로 보게 될 것입니다. 또 선택되지 않은 캐릭터를 어둡게 하고자 결과 이미지의 밝기를 조정하기 위한 머티리얼 스케일러 파라미터도 추가했습니다. 그후 선택 UI에 추가한 각 UI 이미지 콘트롤은 (플레이어 #1에서 4까지의) 고유 MIC 로 주었습니다.

에픽의 (필요에 따라 애니메이션을 재생하려는데 전체 Pawn은 버겁고 할 때 좋은 동적 스켈레탈 메시 액터인) SkeletalMeshActorMAT 에서 상속된 커스텀 클래스를 사용하여 이 캐릭터 메시를 만들었다는 점, 참고하십시오. 커스텀 클래스를 사용한 이유는, 코드를 통해 쉽게 블렌딩된 애니메이션을 재생할 수 있도록 하기 위함입니다.(1) 키즈멧과 마티네를 사용하여 이들 전부 리깅 작업을 할 수도 있었지만, 그랬다면 단발성 플레이어 접속 이벤트에 대한 인터페이싱 작업이 훨씬 힘들 것입니다.

사실 'UI Image' 콘트롤을 플레이어 캐릭터 접속 이미지 용으로 직접 사용하기 보단, 'UI Image' 를 확장하여 커스텀 콘트롤을 만들었습니다. 이를 통해, 접속한 플레이어에 상응하는 캐릭터 메시와 머인불변을 조작하기 위한 필수 함수를, 다른 곳이 아닌 바로 그 곳에 추가할 수 있었습니다.(2) 에픽의 풍부한 베이스 클래스를 상속하는 커스텀 UI 콘트롤을 만드는 것은, 특정 함수성을 추가하고자 할 때 매우 유용한 방법입니다. 제가 이 커스텀 UI를 만드는 데만 해도, 이 방법 덕을 많이 봤습니다. ^_^

어쨌든 마지막 단계는 코드 안에 "Press Start" 버튼 이벤트를 캡처하여 그에 따라 새 플레이어를 만드는 것이었습니다. 이 작업은 자체 함수를 플레이어-선택 UI 씬 클래스의 "OnRawInputKey" 델리게이트로 할당해 주기만 하면 되니 참 간단했습니다. (커스텀 언리얼스크립트 함수성을 갖게 하려면 커스텀 UI 씬 클래스를 사용해야 한다는 점 기억하십시오!) 그리고 그 함수 안에서 'InputKeyName' 파라미터가 게임이 사용하는 Start 버튼 중 하나인지 단순 검사하고, 그렇다면 그 Controller ID 를 가지고 (ViewPortClient 의 CreatePlayer 함수를 사용하여) 새 플레이어를 만듭니다. 물론 그 콘트롤러 ID에 이미 연결된 플레이어가 없다면 말입니다.(3) 그 후 제 Custom UI Scene은 커스텀 Player-Select Image Controls 에게 스스로 업데이트하라고 일러주며(4), 각자의 지정된 인덱스에 해당하는 로컬 플레이어를 찾은 경우, 그들 각자 자체 3D 캐릭터에서 머티리얼 업데이트와 애니메이션 재생을 처리하도록 합니다.(5) 뚝딱, 로컬 플레이어 참가에 반응하는 애니메이션 화상 캐릭터입니다!

ddblog4-3.jpg

이 작업을 완료하고 나니 (잠시후 커스텀 텍스처로 대체하려는) 커서에다 이미터를 조금 방출시켜 메뉴에 꽃단장을 조금 더 해주지 않을 수 없었습니다. 이 작업에는 게임의 ViewPortClient 클래스 안에 있는 PostRender() 함수를 덮어쓰고, 현재 월드가 "메뉴" 레벨로 플래깅되어 있는지 알아보기 위해 게임의 MapInfo를 검사했습니다. 메뉴 레벨이라면 파티클 구조체 배열로 정의된 "파티클"을 수동으로 그리기 위해 Canvas.DrawMaterialTile 을 사용합니다.(6). 각 구조체는 파티클에 대한 정보, 즉 위치, 크기, 속도, 가속도, 수명 등이 포함됩니다.(7). 그 속도를 가지고 위치를 바꾸고, 가속도를 적용하며, 수명을 줄이고, 소멸되면 리셋시키는 등 ViewPortClient 의 Tick() 함수에서 파티클을 업데이트합니다.(8)

나중에 '캔버스 파티클 시스템' 프레임워크를 확장하여 미리정의된 "defaultproperty" 애니메이션 값을 지원하도록 하여, 메뉴를 통한 클릭감을 보다 만족스럽게 만들기 위해 인풋 이벤트 등에 반응하는 UI 파티클을 추가할 수 있도록 하겠습니다. 이러한 커스텀 솔루션도 충분히 지원할 만큼 유연한 에픽 프레임워크의 유연성은 감탄할 만 합니다. UE3 툴만 그리 강력한 것이 아니라, 언리얼스크립트의 유연성을 사용하여 저만의 작은 파티클 시스템 메서드를 만들고 "굴려"볼 수 있는 것입니다!

게임플레이로 돌아가, 레벨 시작을 좀 더 흥미롭게 만들기 위해 플레이어에게 방어 대상 환경에 대해 개괄적으로 보여주도록 하고 싶었습니다. 그러한 시네마틱 제작에는 확실히 마티네만한 툴이 없으며, 그 프레임워크 덕에 이러한 작업도 아주 단순해 졌습니다. 언리얼의 Player Controller 클래스는 디폴트로 "Director" 트랙을 포함하는 마티네의 뷰를 적용합니다. (이를 일종의 스크립팅된 게임플레이 시퀸스가 아닌, '시네마틱' 마티네로써 간주하는 것입니다.)

그러나 시네마틱 도중 플레이어가 걸어돌아다니지 못하게 하려면 입력을 제대로 막아줘야 합니다. (그리고 시퀸스에 무관하다면 캐릭터를 감추기도 해야겠고요.) 이는 PlayerController 의 "NotifyDirectorControl" 을 덮어쓰는 것으로 간단히 가능한데, 여기에는 플레이어가 시네마틱 시퀸스에 들어오는지 혹은 나가는지를 가리키는 파라미터가 있습니다. 디렉터 콘트롤에 들어오는 중이라면, 대부분의 입력 함수를 무시(하고 선택적으로 콘트롤러의 폰 액터를 숨기기도)하는 "InCinematic" 상태를 플레이어 콘트롤러에 밀어주며, 디렉터 콘트롤을 나가는 중이라면, 그 상태를 꺼냅(pop)니다. 거기서부터는 "컷씬" 도중 움직이지 않는 것입니다. (9)

그리고 플레이어가 시네마틱을 건너뛸 수 있게도 하고 싶었습니다. ('컷씬을 건너뛸 수 없는 게임'이란, 게이머들의 사망선고나 마찬가지죠!) 이 기능을 내기 위해 "StartFire" 실행 함수( 및 게임 내 Escpae 키를 누르는 것에 해당하는 입력 함수)를 덮어었습니다. 무기 발사(는 물론 시네마틱 도중엔 의미가 없을테니 그) 대신 레벨의 키즈멧 내 모든 SeqAct_Interp (마티네) 전부를 반복처리하여 그 Director 트랙이 현재 우리 Player Controller 를 제어하고 있는 것을 찾아낸 다음, 그 마티네 PlayRate 를 10000 으로 하여 한 프레임에 끝나도록 합니다. (그 후 PlayRate 값은 원래대로 돌려 줍니다.)(10) 금방 끝나죠? ^_^

이제 게임에 멋진 시네마틱이 생겼으니, 플레이어의 HUD 개선 작업에 촛점을 맞춰 봅시다. 즉 HUD 클래스의 PostRender 의 DrawText 호출 약간을 게임플레이 이벤트에 반응하여 애니메이션 그래픽을 쓸 실제 UI Scene 으로 옮기는 것입니다. 새 위버-HUD 에 가장 처음 추가했으면 한 것은, 체력 게이지와 마법-시전 시간을 나타내기 위한 스타일 괜찮은 그래픽 진행상황-바입니다. 쿠킹해 둔 머티리얼을 사용하여 "퍼센트" 스케일러 파라미터에 따라 진행상황-레이어를 마스킹하도록(, 즉 머티리얼 인스턴스에 몇 퍼센트를 원하는지 알려, 전체 프레임 내에 "그만큼의" 진행상황-바 이미지만 표시되도록) 자체적인 것을 사용하기로 했습니다. 이는 머티리얼이기에, (Player HUD용) UI Scene 내의 UI Control 에는 물론(11), (타워 액터 위에 떠 있는 체력 게이지용) DrawMaterialTile 을 사용하는 동적 캔버스 오버레이에도(12) 사용할 수 있습니다. 사랑스럽죠!

조심해야할 것 한 가지는, 그런 용도로 머티리얼 인스턴스 불변을 사용할 때 그리고자 하는 각각의 바에 대해 고유의 머티리얼 인스턴스 불변을 만들어야 한다는 것입니다. 그렇게 하지 않으면 "퍼센트" 값과 같은 머인불변 파라미터를 설정할 때, 바의 효과에 영향을 끼치게 됩니다. (모두 똑같은 머인불변을 공유할 것이기 때문입니다.) 그래서 제 경우에는 단순히 각 UI HealthBar 인스턴스가(13) (또는 "Damagable" 인터페이스를 구현한 액터가(14))) 자체적으로 "새" 머티리얼 인스턴스 불변을 초기화하고, 원래 머인불변을 그 부모로 설정합니다. 이렇게 체력/진행상황 바 시스템이 UI Scene과 떠다니는 HUD 에서도 사용할 수 있을만큼 유연해 졌으며, "마법같아" (스크롤되는 블렌딩 레이어가 다채롭게 있는거죠, 하하) 보이도록 멋진 머티리얼 애니메이션도 지원할 수 있게 되었습니다.

다음으로 플레이어가 현재 하는 작업, 즉 마법을 시전할 수 있는 상태인지, 현재 적용 가능한지, 캐스팅되는 중이었는지, 캐스팅할 수 있는지 등에 따라 마법 아이콘의 시각 상태가 업데이트되도록 하고 싶었습니다. 게다가 이 마법 아이콘에다 그 마법 시전에 해당하는 인풋-버튼이 동적으로 표시되게도 하고 싶었습니다. 이를 위해 UI Image 에서 "Spell Icon" 을 상속하여, 두 개의 추가 UI Label 로의 참조를 포함할 수 있도록 파라미터를 한 쌍 추가했습니다. 하나는 마법에 드는 "마나", 또 하나는 그 마법을 시전하기 위해 눌러야 할 텍스트-버튼(이나 게임패드 버튼 이미지-아이콘) 표시용입니다.

"Spell Icon" 콘트롤은 Player State 가 바뀔 때마다 업데이트되며, Player Controller 에 그에 해당하는 마법의 사용성에 대해 물어 보고(15), 그 결과에 따라 각 고유 상태를 나타내기 위해 비주얼(불투명도/색)은 물론, 필요한 경우 상응하는 라벨 내 값도 업데이트합니다(16). 이 작업은 다음 두 가지 덕에 쉽습니다: (1) UI Control 은 씬 내 다른 UI Control 로의 참조를 포함할 수 있다는 점(17), (2) UI Control 은 그 부모 UI Scene 에 대해 알고, 부모 UI Scene 은 그 Player Owner 에 대해 안다는 점입니다. 그러므로 결과는 UI HUD를 소유하는 플레이어에만 한정될 수 있는 것이며, 이 정보를 구하기 위해 추가 참조를 하지 않아도 되는 것입니다.

또한 "Percentage Owner" 위치 계산과, (버튼 아이콘처럼) 원래 상 비율을 유지해야 하는 이미지에 대해 "Scale Height" 상 비율 크기 조정 옵션을 통해, 분할화면에서도 모든 그래픽이 멋지게 정렬 가능하다는 것을 알아냈습니다. 이 분할-화면 검증은 UI 에디터 내에서 모든 분할화면 UI 뷰 모드를 직접 토글할 수 있는, 엄청나게 강력한 기능 덕에 정말 쉽게 이루어 집니다. 마지막으로 며칠 전 에디터에 노출시킨 "UI Animation" 시스템을 더욱 활용하여, 각 콘트롤이 상태 변화에 반응하도록 하는 애니메이션 시퀸스를 만들었습니다. (꺼지거나 다시 켜지는 등) 상태 변화를 반영하기 위해 튀어다니며, 색도 확 변하기 보다는 시간에 따라 서서히 변하는 등입니다. 이로써 묘한 피드백 감이 추가되었으며, 이런 과정 자체가 UI 에디터 / PIE 내의 데이터 주도 반복처리일 뿐이니 즐기면서 했습니다.

ddblog4-4.jpg

마지막으로 "Lair Core"( 보호가 최우선 목표입니다!)가 공격받을 때 플레이어에게 알려주고, 그에 따라 코어쪽을 가리키는 부유 HUD 웨이포인트 지시자를 표시하고 싶었습니다. 이 작업을 위해 Crystal Core에 "HUD Overlay" 인터페이스를 구현했습니다 (제 Player HUD, 그 PostRender 안에 이 인터페이스를 구현하는 모든 동적 액터를 반복처리하고, 그 위에서 DrawOverlay 함수를 호출합니다). 새로이 구현된 DrawOverlay 함수 안에서 Crystal Core 가 최근에 공격받았는지 검사합니다. 그렇다면 Core 의 DrawOverlay 는 웨이포인트 아이콘을 가지고 DrawRotatedMaterialTile() 을 호출하며, 그 Rotation 은 (캔버스 중심에서) 코어가 투영된 화면 위치 쪽으로의 방향으로 계산됩니다. 그리고 "Rotated Material Tile" 로 전달된 위치에 대해서는, 화면 중심에서 그 Rotation 방향으로의 오프셋을 계산했습니다. 그리하여 웨이포인트는 중심을 축으로 원 운동하는 나침반 비슷해 지는 것입니다.(18) (그냥 텍스처가 아닌) 머티리얼을 사용했기에, 주목을 끌 수 있도록 튀어다니고 깜빡이게, 머티리얼 안에서 Waypoint Icon 을 애니메이팅할 수 있었습니다.

다음으로 마법 지팡이의 발사체에 임팩트를 주기 위해 "Oomf"를 약간 더 추가하고 싶었습니다. 이미 임팩트 지점에 스포닝되는 (라이트 컴포넌트 포함) 멋진 파티클 이미터가 있으니, 뭘 더 추가할 수 있을까요? 당연히 데칼이죠 데칼! 물론 옛 스태틱 데칼은 아니고, 엄청 강력하여 이름마저 엄청난 에픽의 "머티리얼 인스턴스 시간 가변" 시스템을 사용하기로 했습니다. 그래야 데칼 애니메이션이 예술적으로 나올 것입니다.

Projectile Impact 위에 데칼 스폰하기는 간단합니다: 제 프로젝타일 "Explode" 함수에서 단순히 WorldInfo.MyDecalManager.SpawnDecal() 를 호출, 프로젝타일과 충돌한 HitLocation 및 음수-HitNormal은 물론, 데칼 크기와 수명에 대해 상응하는 값을 전달합니다. 물론 SpawnDecal() 안에다 '프로젝타일 아키타입' 에서의 "머티리얼 인터페이스"를 전달해 주는데, 이 범용 참조는 정적인 "데칼 머티리얼" 또는 동적인 "머인가변"이 될 수도 있습니다. 머인가변인 경우, (GenericMaterialReference.IsA('MaterialInstanceTimeVarying') 를 사용하여 검사하는) 머인가변인 경우, 거기서 "새" 머티리얼 인스턴스 시간가변을 만듭니다. 스폰하는 각 데칼마다 고유 애니메이션을 가능케 하기 위해 필요한 작업입니다. 머인가변의 기간은 (머티리얼 애니메이션이 그러라 할 때까지 유지될 수 있도록) "GetMaxDurationFromAllParameters()" 에 설정한 뒤 그것을 SpawnDecal() 함수에 전달하면, 애니메이션 데칼이 뿅 생겼습니다. (19)

ddblog4-5.jpg

머인가변으로 쉽게 아티스트-주도형 데칼 페이딩이 ('Opacity' 파라미터 값을 1에서 0으로 몇초간에 걸쳐 보간시키는 키프레임을 추가하는 식으로) 가능하긴 하지만, 훨씬 다양한 용도로도 사용 가능합니다: 예를 들어 총구가 빠알갛게 달궈졌다가 검정으로 식는다든가, 복잡한 루프상을 진동하는 머티리얼같은 비-데칼 사용법이나, 심지어 게임플레이 이벤트에 반응하여 동적으로 애니메이팅되는 머티리얼도 가능합니다. 프로젝타일 임팩트에 반응하여 데칼 위에 스폰시키기 위해 간단한 코드를 구성하고 나면, 아티스트 주도하에 상상의 나래를 펼칠 수 있는 시스템이 될 것입니다. 앞으로 던전 디펜스의 VFX 아티스트가 이 기능으로 재미 좀 보기를 바랍니다.

마지막으로, 코어 게임플레이 콘텐츠를 추가 구현하기 위해 되돌아가, 이번엔 근접을 보강하기 위해 원거리-공격을 하는 적을 추가하겠습니다. "Ranged" 적 폰 자체는 베이스 적 폰에서 상속된 거의 빈 클래스나 다름 없습니다만, 그 AI Controller 에는 고유 함수성이 약간 있습니다. 프로젝타일을 쏘기 위해 멈춰선 자리에서 "공격 범위"를 크게 하는 것에 추가로, 베이스 Enemy AI Controller 에서 일반 "Attacking" 상태를 확장하였으며, 이 경우엔 공격 도중 (특히 발사 애니메이션을 재생할 때 폰으로부터 애니메이션 이벤트가 전달되어온 경우) 올바른 시간에 원거리 프로젝타일을 스폰하기 위해서입니다.(20) 여기서 마음에 드는 사실은, 일반 Attacking 상태 처리가 좀 더 추상적인 로직, 이를테면 적이 공격 도중 피해를 입은 경우 상태를 뽑아내고, 간격 검사용 "지난 번 공격받은 시간"을 설정하는 반면, 특정 "RangedAttacking" 상태는 특정 프로젝타일 발사 동작을 수행하는 것이 가능하다는 것입니다. (반면 근접 적의 "MeleeAttacking" 상태는, 기존 플로그에서 언급했듯이, 근접 추적을 합니다.)

플레이어의 마법-시전 시스템에 대해서도 광범위하게 사용한 상태 상속을 통해, 추상 상태에다 범용 함수성을 더 추가시킨 다음, 개별적인 경우마다 더욱 특화되는 자식 버전 상태를 구현할 수 있습니다. 전술한 것처럼, 이는 거의 전적으로 언리얼스크립트의 언어 디자인에서만 가능한 초강력 시스템으로, 생산성을 높이고 코드 디자인을 향상시키는 데 큰 도움이 됩니다. 어떤 경우든 프로젝타일을 실제로 스폰하는 데 있어서, 적이 플레이어의 현재 속도를 계산하여 예측 사격하도록 하는 재미용 계산을 약간 했습니다.(21) (물론 에픽의 SuggestTossVelocity 함수 역시도 이런 기능을 제공하지만, 저만의 키네마틱스 101을 더욱 연마하고 싶었습니다!)

그러나 이 적의 조준이 완벽하다면 재미가 좀 떨어지겠죠. 그래서 (발사 대상 위치를 약간의 임의 로테이터로 변형해 주어) 발사 각도에 약간 의도적 오차를 추가하였습니다.(22) 마지막으로 프로젝타일 발사 방향을 폰의 현재 회전을 중심으로 15도 정도로 제한하여, 반칙스럽게 측면 발사하지 못하도록 했습니다.(23) 그러나 어지간히 조준되고 있으면 맞을 수도 있다는 위압감을 주기 위해, "발사체 오차 범위"로 15도 정도 여유를 두어 적이 완전히 캐릭터를 향하고 있지 않더라도 발사할 수 있도록 했습니다. "쉿!" 뭐 그럴듯 하니 아무도 눈치채지 못할 것입니다. ^_^

자 여러분, 이로써 지난 양일간의 일을 마무리하겠습니다. 강력한 언리얼 기술을 등에 업고서 게임 개발 진척 상황이 (미칠듯이!) 빨라지고 있으며, 금방 여러분을 위한 데모를 선보일 수 있을 것 같습니다. 다음 글 기대해 주시고, 그럼 다음 시간까지... 닥 개발!

블로그 4: 파일 참조

이 블로그에 언급된 정보는 아래 나열된 파일에서 온 것으로, 던전 디펜스 소스 코드의 일부이기도 합니다. 쉼표로 구분된 줄 번호는 파일 속 여러 개의 개별 라인을 말합니다. 하이픈으로 분리된 줄 번호는 파일 내 줄 범위를 말합니다.

  1. DunDefPlayerSelectUICharacter.uc: 24
  2. UIImage_PlayerSelect.uc: 38, 48
  3. UI_CharacterSelect.uc: 97
  4. UI_CharacterSelect.uc: 65
  5. UIImage_PlayerSelect.uc: 38, 48
  6. DunDefViewportClient.uc: 108
  7. DunDefViewportClient.uc: 14
  8. DunDefViewportClient.uc: 122
  9. DunDefPlayerController.uc: 1713, 1726
  10. DunDefPlayerController.uc: 1832, 1809
  11. UIImage_HealthBar.uc: 55
  12. DunDefDamageableTarget.uc: 137
  13. UIImage_HealthBar.uc: 16
  14. DunDefDamageableTarget.uc: 154
  15. UIImage_SpellIcon.uc: 81
  16. UIImage_SpellIcon.uc: 90
  17. UIImage_SpellIcon.uc: 11-14
  18. DunDefCrystalCore.uc: 41-69
  19. DunDefProjectile.uc: 78-103
  20. DunDefDarkElfController.uc: 85-107, DunDefDarkElf.uc: 56
  21. DunDefDarkElfController.uc: 31, 71
  22. DunDefDarkElfController.uc: 62, 63, 71
  23. DunDefDarkElfController.uc: 74

블로그 5: 13 일차

모두들 안녕하세요.

참 개발에 정신없었던 양일간이었습니다! 2주간의 마일스톤 빌드를 준비하고, Frontend 툴로 쿠킹하고, 에이스 테스터 군단(물론, 저희 친구와 가족들이죠. ^_^)에 공개할 요량으로 패키징하고 했습니다. 메커니즘을 확고히 구축하고 나니, 드디어 편히 앉아 난이도 조절용 노트를 해 가며 게임플레이를 즐길 수 있었습니다. 준수하고 꽤 재밌는 미니-게임이 되었으며, 가장 흥분되는 것은 2 주 정도 후에 여러분도 플레이해 보실 수 있을 거라는 것입니다.

구현 측면에서, 진행상황의 주요 영역에 대한 개괄은 다음과 같은데, 대부분 '마일스톤' 준비에 관련된 것입니다:

  • 키즈멧 함수성: (언제고 부드러운 이펙트 페이드인/아웃을 내기 위해) 파라미터가 보간되는 임의의 포스트프로세스 이펙트 토글용 잠복 액션을 추가했습니다. 또한 어떤 키즈멧 플로트 값이든 게임 내 플레이어 수만큼 (플레이어의 수에 대한 곱수 배열을 가지고) 곱해주는 액션도 추가했습니다. 이는 멀티플레이어 밸런싱 작업에 중요한데, 플레이어 수가 늘어남에 따라 적 수는 늘리고 시간은 줄이기 위함으로, 꼭 고정된 선형 스케일일 필요는 없었습니다.
  • 새로운 타워형 "Blockade" 를 추가했습니다. 적이 부수거나 (기초적인 동적 길찾기를 약간 사용하여) 우회하는 장애물 역할을 합니다. 타게팅 가능한 오브젝트를 동시에 공격가능한 적의 수를 지정(된 수 이상의 적은 계속 이동하여 다른 타겟을 찾아보게) 할 수 있도록 하기 위한 Attacker Registration 시스템도 추가하였습니다. 그리하여 적들이 Blockade를 맞닿뜨리면, 일부는 공격을 하고 나머지는 우회 이동합니다!
  • 이미 UT에도 있는 게임 옵션 Friendly Fire(아군 공격) 검사도 추가하였으나, UT 클래스를 사용하지는 않는 상태라 직접 작성했습니다. 또한 플레이어 시작 위치 선택 로직도 약간 추가하여, 새로이 참가하는 플레이어 각각에 대해 알맞은 시작 위치를 순환시킬 수 있도록, 동시에 4명이 참가해도 고유 시작 지점을 확보할 수 있도록 했습니다.
  • UI Skin을 수정하여 '멋진' 자체 버튼 및 폰트는 물론, 모든 게임 시퀸스와 메뉴에 음악과 SFX를 추가시켰습니다. SFX에 대해서는 표준 Effect Actor 에 AudioComponent 를 추가시켜, 스폰되는 Visual Effect 전부에 오디오가 지원되도록 했습니다. (이들이 전부 아키타입이라 가정한다면, 게임의 이벤트 대부분에 오디오를 추가하는 작업도 단숨에 처리되며, 나머지 약간의 사운드 이펙트는 일반적으로 애님 시퀸스 브라우저를 통해 애니메이션에 묶입니다.)
  • 요리(쿠킹)해서 포장(패키징)하는 것이, 마치 케익 같았습니다.

이제 큰 그림이 완성됐으니, 이들 각 국면을 자세히 들여다 봅시다.

키즈멧 함수성

멀티플레이어를 처음 해 보니, 게임이 너무 쉬워진다는 것이었습니다. 사실 머릿수가 4명이라는 것만으로도 큰 그룹의 적 처리가 훨씬 간단했습니다. 그래서 게임의 규모에 따라 레벨 난이도를 적절히 일관되게 유지하기 위해, 현재 플레이어 수에 따라 적의 수를 늘이고 웨이브 그룹간 시간을 줄이기로 했습니다. 그러나 단순 선형 스케일이 아닌, 웨이브별로 플레이어 수 별로 조정할 수 있도록 했습니다. 이를 통해 때에 따라 이상적인 "마법 숫자"를 찾도록 했습니다. 그래서 "플레이어 수에 대해 플로트 스케일 조절"을 위한 시퀸스 액션을 작성했습니다. 여기에는 스케일해줄 값인 사용자-정의 플로트 동적 배열이 포함되어 있습니다. (배열의 각 인덱스는 플레이어 수 빼기 1 에 해당합니다.)(1) 이 액션은 실제 스케일될 플로스 시퀸스 변수를 입력으로 받기도 합니다. 활성화되면 액션은 (현재 월드 GameInfo 에서) GetNumPlayers() 값을 사용한 다음, 상응하는 스케일링-플로트-배열 인덱스를 (배열의 길이 빼기 1 범위로 제한하여) 찾아봅니다. 입력 플로트 변수의 값을 이 스케일링 값으로 곱해준 다음, 키즈멧 출력 링크를 활성화시킵니다.(2) 적의 수와 그룹-스폰간 시간을 제어하는 마스터 키즈멧 변수를 갖추고 나면, 마치 주문처럼 완성되어 작동될 테니, 이제 실제로 플레이어 수에 따른 게임 밸런싱 작업을 할 수 있겠습니다. ^_^

또한 포럼에서 보니 누군가 볼륨에 따라 임의의 포스트프로세스 머티리얼을 바꾸는 방법을 궁금해하고 있었습니다. PostProcessVolume 은 현재 미리 만들어진 에픽 포스트프로세스 이펙트만을 지원합니다만, 약간의 키즈멧(이나 자체 커스텀 Volume 클래스를 만들고, PlayerController 가 터치/언터치됐을 때 그 위에서 이벤트를 호출하게 하는 방법)을 통해 임의의 유저 머티리얼 이펙트에 대한 콘트롤을 추가시킬 수 있습니다. 저는 키즈멧 쪽을 택했는데, 그 이유는 임의의 게임 범주에 따라 커스텀 포스트프로세스 이벤트를 토글하고 싶었기 때문인데, 저의 경우 시네마틱이 재생될 때마다 만들어 둔 필름 그레인 머티리얼 이펙트를 활성화시키고 싶었습니다. 이를 이뤄내는 데는 두 단계가 있는데, 가장 간단한 접근법은 PostProcessEffect 에 있는 bShowInGame 속성을 통해 즉시 껐다 켜는 것입니다. PostProcessEffect 오브젝트에 이르기 위해, Current World Info 내의 모든 LocalPlayerControllers 를 반복처리(또는 전달된 Instigator가 있는 경우 이를 사용)했으며, 그 LocalPlayer 의 PlayerPostProcess 체인을 사용하여 이름으로 찾고자 했던 PostProcessEffect 를 검색했습니다. 찾은 후에 바라는 대로 bShowInGame 을 토글시켰습니다.(3)

그러나 이 작업을 마치고 나자, 일부 이펙트의 값을 시간에 따라 위아래로 실제 보간하도록 지원하여, 즉시 껐다/켜지기 보다는 부드러운 껐다/켰다 전환이 이루어지도록 하기로 했습니다. 약간 까다롭긴 했지만, 충분히 할만한 것이었습니다. 첫 단계는 SequenceAction 보다는 SeqAct_Latent 액션을 만들어서, 시간에 따라 postprocess 값을 조절하기 위해 Update() 함수를 구하도록 하는 것이었습니다.(4) 그리고 표시 과정에 있는 이펙트에 대한 "FadeUp" 배열과, 숨김 과정에 있는 이펙트에 대한 "FadeDown" 배열, 둘 중 하나에 현재 "보간"되고 있었던 PostProcessEffect 의 것을 저장하도록 선택했습니다. Update 함수 안에서 저는 단지 이 배열 각각을 반복처리하여, 타겟 스케일러 값에 도달할 때까지 매 프레임마다 네임드 머티리얼 스케일러 파라미터에 더하거나 뺐습니다. 타겟 값에 도달되면 Up/Down 배열에서 PostProcessEffect 엔트리(, 실제로는 보간 값도 포함하는 구조체)를 제거합니다.(5) 어느 배열에도 더이상의 엔트리가 없는 경우, Update() 함수에 거짓을 반환하여 잠복 액션을 완료합니다.(6) 잘 작동하여 원하는 대로 페이드업/페이드다운 효과를 냈으나, 따로 다뤄줘야 할 특수한 경우가 둘 있었습니다:

  1. PostProcessEffect 에 대한 FadeUp/Down 추가시 이미 FadeUp 또는 FadeDown 배열에 있었을 때, 새로이 추가되기 전에 예전 배열에서는 제거해 줘야 합니다. 그렇지 않으면 동시에 보간이 두 번 진행되어 망치게 됩니다. 이는 키즈멧에서 FadeUp/FadeDown 입력을 빠르게 트리거할 때 발생할 수 있습니다. (Touched/Untouched 를 통해 액션에 링크된 트리거 볼륨 경계에서 게다리춤을 춘다 가정해 보십시오.) (7)
  2. 분할화면을 제대로 지원하려면, 액션은 인스티게이터를 받아 그 PlayerController.LocalPlayer 의 이펙트만(, 옵션으로 모두의 것도) 변경하는 기능을 지원해야 합니다. 그러나 머티리얼 이펙트는 고유 머인불변이 있어야 고유 파라미터 값을 가질 수 있으며, 이는 현재 포스트프로세스 체인 에디터 자체내에서 직접 지정할 수 없습니다. 고로 머티리얼 이펙트의 머티리얼이 고유한지 검사하는 함수를 작성하였습니다. (이는 이펙트 상의 현재 머티리얼이 머인불변이 아닌지, 아니면 그 부모가 머인불변이 아닌 머인불변인지 검사하여 이루어 집니다. 저의 경우 둘 중 하나는 고유한 것으로 만들어지지 않았음을 뜻합니다.) 고유하지 않다면 그에 대해 새 머인불변을 만들고, 그 부모를 원래 머티리얼로 설정한 다음, 그 새로 생긴 머인불변을 머티리얼 이펙트에 적용합니다.(8) 그리하여 적절한 분할화면 고유 포스트프로세스 변경을 얻어낼 수 있는 것입니다. 휘유~

"Dynamic Blockade" 오브젝트

게임플레이로 돌아와서, 게임플레이 몰입도를 높이기 위해 또다른 "타워"형을 추가하기로 했습니다. 우회하게 (가끔은 공격하게) 만들어 적을 느리게 하는 "Blockade" 타워를 추가하기로 했습니다. 그리하여 장애물 주변으로 동적인 내비게이션이 지원되도록 해야 했습니다. 여기에 대한 솔루션은 그리 탄탄하지는 않으나, 필요한 용도로는 충분했습니다. 먼저 타게팅가능 인터페이스를 구현하(여 적이 공격할 수 있도록 하)는 단순 방어가능 오브젝트중 하나인 Blockade 오브젝트를 만들었습니다. 이 Blockade Actor에 대해 특이한 점 한 가지는, "Bump" 이벤트를 덮어쓴다는 점입니다. Blockade 는 부딪힌 액터가 'Enemy' 유형인지 검사하며, 그렇다면 그 EnemyController 에서 "MoveAroundBlockade" 함수를 호출하고, 그 자체에 (충돌한 것과 임팩트 노멀 등) 약간의 파라미터를 전달합니다.(9) 그게 Blockade 가 하는 역할 전부입니다. 진짜 마법같은 일은 이 "MoveAroundBlockade" 통지에 반응하는 Enemy Controller 에서 벌어집니다.

먼저 우선 참고로, 이러한 목적으로 Bump 이벤트를 사용하면 활성 로직을 필요로하지 않는다는 이점이 있지만, 적이 오브젝트를 실제로 건드릴 때만 트리거된다는 단점이 있는데, 제 용도 하에서는 괜찮습니다. 그러나 장애물 오브젝트를 실제로 건드리고 나서야 우회할 생각을 하는 시각적인 약점을 없애고 싶었습니다. 적 콘트롤러의 이동 로직에 앞쪽 이동 방향을 따라 Blockade형 액터가 있는지 Trace 검사를 사용하면 되겠습니다. 이렇게 하면 (적이 장애물에 닿기 전에 우회할 수 있으니) 나아보이는 장점이 있으나, 약간 느려지는 단점이 있겠습니다.

이제 Enemy Controller 에 제가 만든 MoveAroundBlockade 함수에서, 먼저 적이 현재 이 Blockade 액터를 타게팅하고 있지는 않은가, 아니면 이미 우회 시도중인가를 검사하는 로직을 먼저 약간 돌리고, 그렇다면 빠져나갑니다. (어느 상황이든 우회하면 실없을 것입니다.)(10) 게다가 Blockade 액터가 실제로 적과 그 타겟 위치( 아니면 길찾기를 사용할 때 다음 이동 위치) 사이에 있는지 Trace 를 가지고 검사합니다. 아니면 역시 빠져나갑니다.(11) 이는 적이 빗나간 측면-콜리전을 가지고 터치된 장애물 주변으로도 우회 시도하지 못하게 하기 위함이며, 유일한 관심사는 직접 이동 경로에 있는 장애물에 있는 것입니다.

MoveAroundBlockade 함수가 그 검사를 통과했다면, 다음 도달 회피점을 선택합니다. Blockade 의 콜리전 규모 거리 밖으로, 콜리전 노멀의 방향 좌우측에 있도록 합니다. 왼쪽인지 오른쪽인지는 특히 적의 현재 "오브젝트 회피 방향"에 의해 결정됩니다. (항상 '오른쪽'부터 시작됩니다.)(12) "오브젝트 회피 방향"은 적이 이동가능한 지점을 찾지 못했을 때 전환됩니다. 가용성은 (Trace 를 사용하여) 그 지점 아래에 땅이 있는지 알아보는 것으로 검사되며, (13), 에픽의 FindSpot() 명령이 참을 반환하는지 확인하는 방법도 있습니다. (FindSpot 은 바르는 위치나 그 근처에 비-지오메트리-교차된 가용 위치를 찾아 보는 액터 함수입니다.)(14)

가용 오브젝트 회피점을 찾은 경우, Controller 를 "MovingAroundBlockade" 상태로 놓아 그냥 이 타겟 위치로 직접 이동하도록 합니다. 사용할 수 없는 경우, 방향을 바꿔 다시 시도해 봅니다.(15) (다른 방향도 실패하면 그냥 빠져나와 다른 적이 금방 Blockade 를 부수기를 기다립니다!)(16)

MovingAroundBlockade 상태는 오브젝트-회피점에 도달하기 위해 단순히 "MoveToDirectNonPathPos" 를 사용합니다.(17) (오브젝트 회피 거리가 매우 짧은데다 이미 그 주변에 콜리전 지오메트리가 없다, 그 아래에 땅이 있다 등을 확인했기에 길찾기가 필요없다 가정합니다.) 오브젝트-회피점을 어떤 이유로건 도달할 수 없는 경우에 대해 4초간의 취소 타이머를 설정했습니다.(18) 적이 그 지점에 도달하면, Seeking 상태로 반환하고, 원래 타겟으로의 길찾기로 되돌려놓습니다. 물론 그 이후 또다른 Blockade(가 예를 들어 서로 인접해 있다면, 그)를 만날 것이며, 다음 장애물에 대해 "MovingAroundBlockade" 에 들어가도록 할 것이고, 계속해서 같은 방향으로 이동하여 결국 적이 Blockade 에서 벗어나게 될 것입니다.

이 접근법은 꽤나 기본적이면서도 대부분의 경우 꽤나 잘 작동합니다. Blockade가 꽤나 복잡하게 놓여있는 경우에도, 적은 같은 방향의 각각의 장애물을 하나씩 효과적으로 넘어가며, 지나갈 수 없는 벽에 걸리면 방향을 바꿔 다른 길을 찾아 봅니다. 이게 실패하는 경우라면 장애물 네트워크가 복잡하고 길게 늘어 있어서 적의 원래 타겟 위치가 볼록 장애물 네트워크 내의 임의 위치에 있는 경우, 상황에 따라 적은 절대로 해당 지점에 도착하지 못할 것입니다. 다른 말로 이는 진짜 길찾기가 아니며, 전체적 패씽 구조 개념 없이 그저 단순한 오브젝트-회피 작동법이라 볼 수 있습니다.

현재로서도 충분하긴 하지만 (특히나 장애물을 부술 수도 있고 적도 공격하려 하니), 내비 메시 동적 약도 내비게이션을 사용하면 괜찮은 향상이 있을 것 같습니다. 이에 대해서는 내주쯤 파헤쳐 보도록 하겠습니다.

공격자 등록

사실 근처 장애물을 공격하는 적의 수를 제한하지 않으면, 적은 우회하기 보단 항상 장애물을 공격할 테니 장애물-회피 시스템은 필요치 않을 것입니다. 원하는 행위, 일부는 장애물을 공격하고 나머지는 우회하는 방식을 얻기 위해서 저는 "공격자 등록" 시스템을 구현하여 어떤 장애물(이나 타게팅가능 액터)를 동시 공격할 수 있는 적의 수를 제한시켜야 했습니다. 이러한 기능은 "Targetable" 인터페이스에 RegisterAttacker 및 UnregisterAttack 함수 한 쌍을 추가시켜 간단히 이뤄냈습니다.(19)

이 작업은 물론 인터페이스를 사용하는 모든 클래스에 새 함수 구현을 요합니다. Registration 대상이 될 오브젝트(, 즉 Blockade 및 기타 놓을 수 있는 Tower)는 모두 운 좋게도 damagable 오브젝트 클래스 계층구조 안에 있어서, 새로운 함수 구현은 한 곳에서만 해 줘도 되는 것이었습니다. (Pawn 역시도 Targetable 인터페이스를 사용합니다만, 현재 Attack Registration 을 고려하지 않기에 거기에는 새 인터페이스 함수의 토막 버전만 추가했습니다.)

DamageableActor 클래스안에서 RegisterAttacker 함수는 (배열에 이미 있지 않을 때) 단순히 Attacker 를 Attackers 배열에 추가하는 것이며, UnregisterAttacker 는 제거하는 것입니다.(20) 게다가 Attackers 배열의 길이가 (추가하여 defaultproperties 에 지정한 변수인) Attackers 의 최대 수치 이상인 경우, Targeting Desirability 함수가 선호도를 -1 반환하도록 (즉 타게팅 가능하지 않도록) 변경했습니다.(21)

마지막으로 EnemyController 에서, (AI가 새 타겟을 잡을 때마다 호출되는) SetTarget 함수 내의 신/구 타겟 각각 위에다 RegisterAttacker/UnregisterAttacker 로의 호출을 추가하고,(22), Controller 가 파괴된 경우 Target 을 None 으로 설정하여, 적이 죽으면 UnregisterAttacker 를 호출할 수 있도록 했습니다.(23) 이게 다 입니다!

이제 damageable 액터는 동시에 자신을 공격 가능한 적 수를 지정할 수 있게 되었으며, 그 이상의 적은 그 타겟을 무시하고 다음으로 이동해 갑니다. 둘로 제한한 Blockade 에 대해서는 특히나 잘 작동했으며, 그 주변 나머지 적들은 바라던 대로 우회하여 이동하는 것을 볼 수 있었습니다. Blockade 를 적을 늦추거나 중요한 위치를 방어하기에 효과적인 기술로 만들면서, 무적이지는 않게 만든 것입니다.

아군 공격 & 시작 지점 선택

멀티플레이어 테스팅 작업을 추가로 계속해 가면서, 아군 공격에 문제를 발견했습니다. 적을 공격하라고 했던 것이 서로 공격하느라 바빴던 것입니다. (물론 "친구"를 푹 찌르는 것이 영화같은 재미야 있을 테니, 놔둔 옵션이긴 합니다.) UT 클래스에는 아군 공격 검사 기능이 내장되어 있으며, 그 게임에서는 GameInfo 클래스 질의를 통해 피아구분에 따라 데미지를 조정하고 있습니다. GameInfo 클래스는 그 후 팀을 검사하여 같은 팀이라면 적절히 피해를 줄입니다. 그러나 저는 저만의 방식을 쓰고 싶어서, 피해 조정에 GameInfo 대신 그냥 Targetable 인터페이스에 "IgnoreFriendlyFireDamage" 함수를 추가하여 인스티게이터를 입력으로 받고, 팀을 검사한 다음, 같은 팀인지와 GameInfo가 현재 아군 공격을 허용하고 있는지에 따라 참/거짓을 반환합니다.(24)

그리고서 "IgnoreFriendlyFireDamage" 결과를 검사하여 참이면 (피해를 적용하지 않고) 빠져나오도록 베이스 클래스의 TakeDamage 선언을 수정했습니다.(25) 이 접근법의 장점은 "IgnoreFriendlyFireDamage" 함수를 덮어씀으로 해서, 전체 게임 세팅과는 상관없이 특정 클래스에서는 아군 공격을 끌 수 있도록, 예를 들어 적들은 서로 데미지를 입히지 않도록 할 수 있게 했습니다. 걔들한테는 어드밴티지를 좀 줘야죠, 헤헤.

다음으로 플레이어가 네 명 스폰될 때, 똑같은 (가용) 스폰포인트를 선택하여 게임이 시작될 때 서로의 위에 쌓여 나타나고 있었습니다. 게임의 스폰 포인트를 순환시켜, 스폰 포인트가 충분하다면 각 플레이어가 고유 위치에 스폰되도록 하고 싶었습니다. 이를 위해 단지 GameInfo 클래스의 ChoosePlayerStart() 함수를 덮어써서, 현재 검사중인 Spawn Point 가 "Used Spawn Point" 배열에 있는지(, 그렇다면 무시하고 다음 것) 검사합니다. 스폰 포인트가 선택되면 이를 "Used Spawn Point" 배열에 추가하여, 스폰 포인트를 찾지 못했으면 Used Spawn Point 배열을 비운 다음 한 번 더 검사합니다.(26) 빙고, 스폰 포인트 순환이 잘 작동합니다.

물론 다른 방법이라면 사용되었는지 아닌지를 담는 불리언이 포함된 스폰 포인트(, 실제로는 'PlayerStart') 클래스를 새로 만들고, PlayerStarts 를 찾을 수 없으면 모든 불리언을 비우거나, 아니면 심지어 커스텀 스폰 포인트 선택 시스템을 작성하여 내장 함수성은 사용하지 않는 방법도 있습니다. 에픽의 클래스 프레임워크의 미학 중 하나는, 주어진 태스크에 대해 보통 "가장 쉬운" 접근법이 있음에도, 그 처리를 위해 커스터마이징 가능한 방법은 종종 무한히 있으며, 어떤 방법을 선택할 지는 전적으로 엿장수 마음에 달렸습니다.

UI Skinning

작은 마일스톤 빌드를 공개할 때가 다가오는데도 UI(특히나 버튼의 경우)는 여전히 월석 분화구같은 디폴트 텍스처로 된 DefaultSkin 을 사용하고 있었습니다. 게임에 기본적인 테마 스타일을 주고 싶으니, 자체 UI 스킨을 제작할 시간이 왔습니다. 먼저 에픽의 DefaultUISkin 리소스를 자체 패키지 속으로 복제(하고 DunDefSkin.DefaultSkin이라) 하고, DefaultUI.ini 의 "UISkinName" 키가 그것을 가리키도록 변경했습니다. 게다가 DefaultEngine.ini 의 StartupPackages 목록에 DunDefSkin 을 추가시켰습니다. 어플리케이션 시동시 로드되도록 하는 중요 작업이니 꼭 확인해야 합니다. 게임에 커스텀 UI 스킨을 지정했으니, 언리얼 에디터의 UI 스킨 에디터를 불러 월석을 자체 버튼 텍스처로 대체합니다. (그냥 Default Image Style 을 Button Background Style 의 모든 상태에 대한 'Texture' 값으로 맞바꿔주면 됩니다.) 또한 "Clicked" 이벤트에 대해 SoundCue 참조도 추가하여, 이제 모든 버튼을 클릭해 볼 때 소리로 피드백됩니다. 커스텀 트루 타입 폰트도 가져와서 Default Text Style 에서 사용할 폰트로 설정했습니다. 이제 UI가 약간 테마가 갖춰졌으니, 빌드 준비 전에 소리 작업에 집중해 보겠습니다!

소리와 음악

다양한 게임플레이 이벤트에 대해 적절한 피드백을 주는 소리 없이 게임에 완성이란 없습니다. 음악도 마찬가지로, 플레이어의 마음가짐을 유도하는 데 중요한 역할을 합니다. 고맙게도 소리와 음악 둘 다 언리얼의 파이프라인을 사용하여 쉽게 통합되어 있습니다. 게임에 소리를 넣는 방법은 여러가지 있는데, 이 작업에 (전부는 아니지만) 여러가지 방법을 적용해 봤습니다. 먼저 Wav 를 가져와서 에디터가 큐를 자동으로 생성하도록 했습니다 (매우 편리합니다). *3D 공간*에서 재생하려는 소리 전부는 "Attenuation" 노드로 만들어 3D 위치 & 감쇠를 갖도록 합니다. 다음으로 모든 주요 게임플레이 이벤트에 대한 여러가지 아키타입에 스폰되는 시각 효과 (Emmiter) 액터에 AudioComponent 를 추가했습니다.(27). 이를 통해 이 시각 효과 각각이 그 아키타입에 지정된 사운드를 갖도록 할 수 있습니다. 각 비주얼 이펙트 아키타입을 돌아보며 그 AudioComponent 속성에 바람직한 큐를 설정하니, 모든 시각 효과에 3D 사운드가 생겼습니다. 간혹 시각 효과가 사용되지 않은 경우는, ("AnimNotify_Sound" 애님시퀸스 통지를 사용하여) 애니메이션에 재생할 사운드큐를 설정하거나, 그냥 (마찬가지로 멋진 FadeIn/Out 옵션이 있는 Actor.PlaySound() 를 사용하여) 코드를 통해 재생할 수도 있습니다. 또 PlaySound 키즈멧 UI 액션도 추가(28), 이는 전체 스킨의 사운드 목록에 정의되지 않은 커스텀 사운드를 UI에서 재생해야 할 필요가 있을 때 단순히 WorldInfo.PlaySound() 를 호출하는 것입니다. 30 분도 안걸려 전체 게임의 모든 주요 이벤트에 재생되는 3D 사운드가 생겼으며, 확실히 게임플레이 피드백이 향상되었습니다.

다음으로 음악입니다. 원대하며 눈물을 빼는 극적인 음악! 뭐 그런 거죠. 어떤 음악이든, 언리얼로는 Wav 를 가져오는 것 만큼이나 간단합니다. (긴 음악 트랙이라면, 압축 품질을 낮춰야 좀 더 크게 압축될 수 있을 것입니다.) 그리고서 키즈멧의 "Play Music Track" 액션을 통해 fade 값도 적용시켜 재생할 수 있습니다. 코드를 통해서는 새 음악 값을 포함하는 MusicTrackStruct 파라미터를 가지고 WorldInfo.UpdateMusicTrack() 함수를 간단히 호출하면 됩니다.(29) 두 접근법은 각기 다른 목적에 사용하였으며, 이제 게임에 페이딩 효과가 포함된 멋진 음악이 레벨 내 주요 이벤트에 맞춰 흘러나옵니다. 이러한 감성 조작 기술을 약간 추가하고 나니, 유후~ 릴리스용 패키지 준비가 되었습니다.

쿠킹 및 패키징

릴리스-요리사의 길은 간단합니다: UnrealFrontend 를 열고, 게임이 사용하는 맵을 전부 (저의 경우 Entry Level, Menu Level, 심리스 트래블에 사용된 Transition Level) 알려줍니다. 또한 주요 "UDKGame" 스크립트 패키지를 DefaultEngine.ini 에 지정된 StartupPackages 목록에도 추가해야 했는데, UT 스크립트 보다도 이 스크립트 패키지에 게임 초기화를 의존하고 있었기 때문입니다. (스크립트 패키지를 여럿 두고서도 특정 레벨에 의해 개별적으로 참조 가능은 하나, 어떤 패키지가 됐든 커스텀 GameInfo 를 포함하는 패키지는 StartupPackages 에 포함되어야 한다는 점 참고하시기 바랍니다.) 그리고 "Cook" 을 클릭하면 Frontend+UDK 가 최적화된 릴리스 콘텐츠 파일 전부를 생성합니다. 마지막으로 "Package"를 클릭하면 인스톨러의 유저 인터페이스에 쓸 게임명을 물어옵니다. 이 과정이 완료되면 인스톨러 EXE 파일이 메인 디렉토리에 생성됩니다. 이 아가씨를 친구/가족/베타테스터/퍼블리셔 에게 소개시켜 줄 준비가 된 것입니다. 아으~

이리하야 언리얼을 가지고 개발 프로세스를 불사르던 불같은 이틀이 끝나게 되었습니다. 게임의 모양새는 거의 잡혔고, 지금은 콘텐츠 추가 및 미세조정 단계이니 곧 여러분께 공개할 수 있는 시점이 온 것 같습니다. 그럼 시청자 여러분, 피드백 잊지 마시고 다음 이시간까지 안녕히!

- 제레미

블로그 5: 파일 참조

이 블로그에 언급된 정보는 아래 나열된 파일에서 온 것으로, 던전 디펜스 소스 코드의 일부이기도 합니다. 쉼표로 구분된 줄 번호는 파일 속 여러 개의 개별 라인을 말합니다. 하이픈으로 분리된 줄 번호는 파일 내 줄 범위를 말합니다.

  1. DunDef_SeqAct_ScaleFloatForPlayerCount.uc: 10
  2. DunDef_SeqAct_ScaleFloatForPlayerCount.uc: 22-26
  3. DunDef_SeqAct_TogglePostProcessEffects: 70-79
  4. DunDef_SeqAct_TogglePostProcessEffects: 240
  5. DunDef_SeqAct_TogglePostProcessEffects: 171-233
  6. DunDef_SeqAct_TogglePostProcessEffects: 236, 248
  7. DunDef_SeqAct_TogglePostProcessEffects: 139-155
  8. DunDef_SeqAct_TogglePostProcessEffects: 137, 117
  9. DunDefTower_Blockade.uc: 21
  10. DunDefEnemyController.uc: 456-460
  11. DunDefEnemyController.uc: 492-509
  12. DunDefEnemyController.uc: 522-530
  13. DunDefEnemyController.uc: 536
  14. DunDefEnemyController.uc: 563
  15. DunDefEnemyController.uc: 538, 545
  16. DunDefEnemyController.uc: 549
  17. DunDefEnemyController.uc: 656
  18. DunDefEnemyController.uc: 648, 602
  19. DunDefTargetableInterface.uc: 26, 29
  20. DunDefDamageableTarget.uc: 65, 73
  21. DunDefDamageableTarget.uc: 61, 93
  22. DunDefEnemyController.uc: 259, 272
  23. DunDefEnemyController.uc: 217, 283
  24. DunDefTargetableInterface.uc: 21, DunDefPawn.uc: 67
  25. DunDefPawn.uc: 148
  26. Main.uc: 274, 286
  27. DunDefEmitterSpawnable.uc: 154
  28. DunDef_UIAction_PlaySound.uc: 13
  29. Main.uc: 156

블로그 6: 17 일차

친애하는 UDK 개발자 여러분, 그간 안녕하셨는지요!

던전 디펜스 개발의 마무리 단계에 접어들면서, 지난 며칠간 AI 행위를 다듬는 작업에 집중한 다음, 리플레이 값을 제공하는 메타-게임 시스템 한 쌍을 집중해 봤습니다. 부가적으로 최근 완료된 아트워크를 한 다발 통합시켰으며, 바로 이 부분이 게임플레이에 위대한 그림이 입혀져 생명을 얻게 되는, 개발 과정에서 가장 흥미로운 부분인 것 같습니다. 여기에 언리얼 기술의 멋진 렌더링도 한 몫 했지요.

ddblog6-1.jpg

어쩄든 코드 측면에서 개괄적인 것을 먼저 말씀드리고, 각 주제에 대해 상세히 들어가 보도록 하겠습니다:

  • 적이 (피해를 입을 때 전달된 동력으로 인해) 네비게이션 네트워크에서 밀려나는 경우 또는 어떤 이유로 타겟에 도달하지 못하는 경우에 대한 주기적인 "걸림" 검사를 추가하여 적 AI를 개선시켰습니다. 그런 경우 동적으로 내비게이션 경로에 되돌아가면서 새 타겟을 찾아봅니다. (MMO 유저에게 익숙한) "어그로" 시스템도 도입하여 적이 최근 자신에게 피해를 입힌 양에 따라 대상에 좀 더 공격성을 띄도록 하였으며, 기타 모든 타게팅 요인도 가중되도록 하였습니다. 이를 통해 적이 좀 더 살아있는 듯한 느낌은 물론 전략적인 깊이도 더할 수 있습니다. (예를 들어 약한 피해로 강력한 적의 주의를 끌어 중요 방어 지점에서 끌어낼 수도 있습니다.)
  • 기본적인 글로벌 UI 통지 시스템을 추가하여, 미션 목적이나 기타 게임 정보가 분할화면 게임중일 때도 전체화면 UI에 흘려 표시될 수 있도록 하였습니다. 통지가 여럿 쌓일 수도 있으며, 시간에 따라 화면에서 페이딩되며 표시됩니다.
  • 플레이어가 적을 잡고 웨이브를 완료하는 등 하면서 점수를 얻는 "점수" 시스템을 추가했습니다. 점수가 추가될 때마다 부가적인 "Award Name" 이 그에 부착되어 있어, (보너스였는지 특별 점수였는지 등) 그 점수를 얻게된 이유가 자그맣게 뜨게 됩니다.
  • 점수 시스템에 최고점수 시스템도 연계하여, 상위 10등까지의 점수가 메인 메뉴와 게임오버 화면에 표시되도록 했습니다. 게임 도중 상위 10등 점수를 획득하면, 게임이 끝날 때 최고 점수 등록에 들어갈 이름을 입력받습니다. 멀티플레이어에도 적용됩니다!
  • 기본 옵션 UI 를 추가하여, 옵션을 저장하고 게임플레이 도중 변경할 수도 있습니다.
  • 게임플레이 미세조정 및 밸런싱 작업과, 다양한 다듬기 작업을 많이 했으며, 모두가 즐기기에 부담이 없길 바랍니다. ^_^

자 그럼 우선, AI에 관해서: 내비메시 시스템은 매우 유용합니다만 모든 각각의 사건을 자동으로 처리할 수는 없습니다. 즉 캐릭터가 내비게이션 지역 밖으로 밀려나면 길찾기가 실패할테고, 그리고 나면 무얼 할까요? 그건 꽤나 간단합니다: NavigationHandle.FindPath() 가 거짓을 반환했는데 타겟은 여전히 직접 도달 가능하지가 않다면, AI Controller 에게 딱 눈에 보이는 주변 타겟으로(, 또는 내비게이션 노드로) 전환하게 한 다음, NavigationHandle.FindPath() 가 다시금 가용 패쓰를 반환할 때까지 타겟을 향해 (길찾기를 쓰지 않는 MoveToDirectNonPathPos() 를 사용하여) 바로 걸어갑니다. 이렇게 간단한 솔루션만으로 우리 게임에는 충분했습니다. 어디로 튕겨나간 적이든 플레이가능 지역으로 계속 돌아오려 하는 것입니다! (1)

게다가 매 초마다 적이 상당한 거리를 움직였는지를 알아보는 타이머 체크를 추가하여, 그렇지 않은 경우에 길찾기 시작 전 좌우측으로 직접 옮겨줍니다.(2) 이런 식으로 다수의 적이 동일한 타겟을 향해 갈 때 뭉쳐 막히던 것을 해결하여 서로 효율적인 이동이 가능해 졌습니다.

이런 길찾기 관련 문제를 해결하고 나니, 적이 타겟을 거리나 고정된 공식을 통해 잡기 보다는 자신을 공격하는 오브젝트에 좀 더 사실적으로 반응하게 만들고 싶었습니다. 간단히 말해서, 기본 "어그로" 시스템을 두어 최근 자신에게 피해를 입힌 적을 더욱 미워하도록 하고 싶었습니다. (아, 저 와우 안합니다. ^_^)

이는 어렵지 않은 약간의 배열 관리일 뿐입니다. 먼저 동적 배열 "Recent Attackers" 를 Enemy Controller 에 추가(하고 Controller 의 NotifyTakeHit 이벤트를 통해 그 배열에 Attacker 를 추가)했습니다.(3) 그러나 Attacker 로의 직접 참조를 저장만 하는 대신 이 배열을 Attacker 에 대한 부가 정보를 담는 소위 "AggroEntry" 구조체로 만들었습니다. 즉 Attacker 가 마지막으로 적에게 피해를 입힐 때, 그리고 그 Attacker 에 대한 현재 "Aggro Factor".(4) "Aggro Factor" 는 Attacker 가 적에게 입힌 최근 데미지의 (Enemy Health 전체의 백분율로써) 총합이 될 것입니다. 적이 타게팅 상태에 있을 때 매 프레임마다 "Aggro Entry" 배열 반복 처리를 통해, 각 항목의 "Aggro Factor" 를 시간에 따라 감소시킬 것입니다. (0 이 되면 목록에서 그 항목을 제거하고요) (5)

동시에 타겟을 잡을 때, Target 후보가 "Aggro Entry" 에 있는지 검사해 보고, 그렇다면 그 항목의 "Aggro Factor" 현재 크기에 따라 Target 후보 선호도를 올립니다.(6) "Aggro Factor" 가 다른 새 항목을 감소 시작시키기 전까지 (10초의) 고정 기간도 추가했습니다. Enemy 가 Target 간에 앞뒤로 핑퐁거리는 것을 방지하기 위해서죠. 바로 그거였습니다. 적이 방금 자신을 공격한 것에 반응하는 경향이 생겼고, 여전히 거리에 따른 전체적인 타겟-가중치나 타겟-선호도도 고려되고 말입니다. 모두를 위한 게임플레이 감도가 높아진 거죠!

다음으로 주요 임무에 대한 통지가 분할 화면이 아닌 전체 화면으로 해야겠다 싶었습니다. 고로 "플레이어 뷰포트" 단위가 아닌, FullScreen 렌더 모드로 설정된 "Global UI" 를 추가했습니다. 그리고 GameInfo.PostBeginPlay() 에서 게임플레이 시작시 이 UI 중 딱 하나만 열도록 했습니다. 이제 확실한 전체-화면 UI가 생겼으니, 여러 개의 통지가 큐에 쌓이도록 하고, 서로간에 매끄러이 사라지도록 하며, 키즈멧을 통해 추가하고 싶었습니다. 그러기 위해 Global UI 씬에다가 UI Labels 배열을 추가하고, (Scene Editor 를 통해) Global UI Scene 의 변수 내 편집가능 배열 속으로 그 참조를 설정했습니다.(7) 그런 다음 커스텀 UI Scene 클래스 안에 "ShowMajorNotification" 라는 함수를 만들었습니다. 이 함수는 그 배열의 (지난 번 활용된 Label 인덱스를 저장하고, 매번 그것을 증가시켜) 다음 Label 위젯에 텍스트를 설정(하고 애니메이션을 "팝-인")하는 역할을 합니다.(8) 그리하여 게임에는 Label 용으로 만든 메시지를 동시에 몇이든 표시할 수 있으며, 제 경우는 셋입니다. 이 방식은 잘 작동했으며, Global UI Scene 상의 "ShowMajorNotification" 함수를 호출하는 키즈멧 시퀸스 액션을 추가하기만 하면 모든 플레이어가 전체화면 통지를 받을 것입니다.(9) (이는 GameInfo 안의 그 UI Scene 에다 저장한 참조를 통해 키즈멧에서 액세스됩니다.)(10)

ddblog6-2.jpg

실제로는 플레이어별 HUD UI Scene 안에도 똑같은 시스템을 구현하여(11), UI 메시지를 모두에 대해 전체적으로 표시할 것인지, "Instigator" (PlayerController 를 가진 Pawn)가 ShowNotification 키즈멧 액션으로 전달되었는지에 따라 플레이어별로 표시할 것인지 선택 가능하도록 했습니다. (12) 괜찮죠? ^_^)

ddblog6-3.jpg

그 이후 리플레이/뽐내기용 인센티브를 제공하기 위한 기본적인 Score 시스템을 구현하기로 했습니다. 이는 간단한 작업인데, Controller 클래스 안에 Score 값을 저장한 다음 Enemy 가 죽을 때마다 (누가 죽었는 지 Enemy 의 "Died" 함수 내 "Killer" 참조를 사용해 알아내어) (13), 그리고 (적 웨이브 완료같은) 게임 내 약간의 이벤트 발생시 (14) 값을 점수에 더하는 것입니다. 그런데 이 게임 내 점수가 약간 보이도록 하고 싶어서, 구닥다리 현금 등록기처럼 플레이어의 실제 점수를 (시간에 따라) "합산하는" Score 표시기용 커스텀 UI Label 클래스를 구현했습니다. (15) 추가로 합산중에는 UI Label 상에 약간의 "딩-딩" 색과 위치 애니메이션을 재생하여 (16) 그 자체로 약간은 재밌게 주의를 끌도록 했습니다.

또한 Score 합산에 잠재적으로 ("COMBO KILL x2" 같은) "Bonus Name" 이 묶이도록 하고 싶었습니다. 위의 주요 통지와 거의 마찬가지로 이 기능을 위해, 애니메이션을 통해 시간에 따라 스크롤/페이딩 되는 텍스트 큐를 표시하기 위해 UI Label 세트를 또하나 만들었습니다.(17) 커스텀 Score Label 클래스 안에 이 UI Label 로의 참조 배열을 저장하였으며(18), 점수가 더해질 때마다 다음 UI Label 상에 "Bonus Name" 문구(가 있다면) 설정용 Score Label 을 담당시킵니다.(19) 대체로 이는 잘 작동되며, 점수 획득에 만족스러운 시각 피드백 효과를 내었습니다. UI Scenes 및 UI Controls 관련 핵심은, 만약 약간 우습고/독특한 것을 하고 싶다면, 그냥 에픽의 콘트롤을 하나 서브클래싱한 다음 원하는 대로 로직을 만들면 된다는 것입니다!

이제 Score 가 생겼으니, 여러 UI로 플레이어에게 상위-10위까지를 표시해 주기 위해, 그 값을 저장하기 위한 방법이 필요하겠습니다. 그리고 플레이어가 신기록을 갱신했을 때 자기 이름을 입력할 수 있도록도 하기 위해서요. 이를 위해 에픽의 편리한 "SaveConfig()" 함수성을 활용했습니다. 이는 보안에 상관이 없는 게임 데이터의 저장에 유용합니다. 다음 UDK 공개시에는 함수성 DLL 바인딩이 지원될 예정이라는 에픽의 공식 입장이 있었으니, 우리 UDK 개발자들은 C++ 로 쿡업할 수 있는 어떤 저장 스키마도 사용할 수 있기야 하겠지만(네이티브 데이터 처리 가능성의 무한한 세계가 열리리라!), 이런 기본적인 기능엔 벼룩잡다 초가삼간 태우는 격입니다.

그래서 "Data_HighScores" 클래스를 (Actor 가 아닌 Object 에서 파생하여) 만들었으며, High Score 정보(점수, 플레이어 이름, 도달 웨이브)를 담기 위해 그 안에 "HighScoreEntry" 구조체를 정의했고, "HighScoreEntry" 의 'config' 배열을 만들었습니다. (20) 변수 선언에서 'config' 키워드를 사용한다는 것은, 이 배열의 값이 클래스 선언 내에 지정된 INI 에서 정의된다는 뜻입니다. 그래서 "DefaultHighScores.ini" 안에다 High Scores 배열에 대한 디폴트 10 항목을 정의했습니다. (어떤 값이 적당할까 추측해 보니, 조금 더 플레이해봐야 적합한 최고 점수를 알아낼 수 있을 것 같습니다!)

이 항목은 Data_HighScores 클래스가 오브젝트로 인스턴싱될 때마다 언리얼에 의해 자동으로 로드되며, 그 인스턴싱 작업은 제 커스텀 GameViewportClient 클래스의 Init 함수에서 수행했습니다. Data_HighScores 환경설정 변수를 변경하고 (이 경우 High Score Entries), 거기서 SaveConfig() 을 호출하여 데이터를 다시 INI에다 쓰고 저장합니다. (21)

염두에 둘 것은 단지 그리 간단한 정보를 저장하는 것 보다는, 체크포인트 데이터, 또는 필요할 수 있는 기타 일반적인 게임-저장을 보관할 수도 있습니다. (최종 사용자가 들여다 봐도 괜찮다면 말이죠) 오브젝트 인스턴스 별로 저장 데이터를 저장하기 위해 "PerObjectConfig" 키워드를 사용할 수도 있는데, 사용자가 세이브를 여럿 만들거나 동적으로 세이브가능 오브젝트를 추가할 수 있을 경우 유용합니다. 여기서 'GetPerObjectConfigSections' 를 사용하여 반복처리하면 로딩할 수 있는 세이브 데이터 항목이 무엇인지 알아낼 수 있습니다.

어떤 경우에도, High Scores Loading 및 Saving 이 생겼으니, 실제로 플레이어의 점수를 언제 목록에 추가시킬지 결정하고 UI 를 통해 High Scores 를 표시해 줘야 하겠습니다.

목록에 추가하기는, 플레이어가 "게임 오버" 됐을 때입니다 (던전 디펜스는 결국 언젠가 끝이 나겠죠? ^_^). 각 플레이어의 Score 가 High Score 배열의 어느 항목보다 큰가 검사합니다. (22) 그렇다면 Edit Box 가 포함된 UI 를 (High Score 를 갱신한 플레이어마다 하나씩) 열어, 그 새 항목에 대한 "이름"을 요청합니다. 그 UI 위에 "OK"를 클릭할 때, 이 플레이어의 새 "HighScoreEntry" 구조체를 그 High Scores 배열 내 적당한 인덱스 위치에 (입력받은 플레이어 이름을 가지고) 삽입합니다. 그리고 배열 크기를 다시 10 항목으로 줄이고, 마지막으로 SaveConfig() 을 호출하여 새 값을 INI 파일로 쓰게 합니다.(23)

ddblog6-4.jpg

마지막으로 High Scores 가 UI 에 표시되게 하고 싶었는데, 그 작업은 커스텀 UI Panel 클래스 ("HighScoresPanel") 를 만들어서 거기에 10 UI Labels 로의 참조 배열을 각 High Score Entry 마다 하나씩 줍니다. "HighScoresPanel" 에 대해 커스텀 "OnCreate" 함수를 호출했으며, 이 안에서 각각의 문자열 값을 설정합니다. 그들이 UI Label 을 상응하는 인덱스의 High Score Entry 데이터로 참조했다면 말이죠.(24) 이제 항상 High Scores 를 표시하는, 원하는 UI Scene 어디에나 넣을 수 있는 재사용 가능 콘트롤이 생겼습니다. 잘 된거죠. 게임 오버 UI 는 물론 메인 메뉴에도 최고 점수를 표시하고 싶었으니까요.

ddblog6-5.jpg

마지막으로 High Scores 로 데이터를 저장하기 위해 똑같은 환경설정 메서드를 사용하여, 사용자가 특성 게임 세팅을 변경할 수 있도록 해 주는 아주 기본적인 Options UI 를 만들었습니다. 이 세팅은 단지 GameInfo 클래스에 포함된 약간의 Boolean 값일 뿐으로,(25) Options UI 안에는 체크박스로 표현한 것들입니다. Option UI 클래스 안의 SceneActivated() 이벤트에서, 체크박스의 체크된 값을 GameInfo 클래스에 상응하는 변수 값으로 설정했으며, 사용자가 OK 를 클릭하면 체크박스 값을 다시 GameInfo 의 변수로 복사한 후 GameInfo 클래스에서 SaveConfig() 을 호출합니다. (Cancel 을 클릭하면 단지 복사/저장 없이 UI를 닫을 뿐입니다.)(26) 단순하고 효율적이죠.

ddblog6-6.jpg

폴리싱 및 사용감 향상 작업용으로 AI 와 UI 작업을 마치고 나니, 벌써 개발의 막바지 단계에 접어들고 있습니다. 릴리스 전에 플레이어가 적을 늦추는 데 사용하는 간단한 트랩을 포함하여 콘텐츠를 좀 더 구현할 계획인데, 전체적으로 보면 거의 완료된 것입니다. 이 자그마한 게임을 선뵈드릴 생각에 가만있질 못하겠네요. 계속해서 앞으로의 마무리 단계 일도 올리도록 하겠습니다!

블로그 6: 파일 참조

이 블로그에 언급된 정보는 아래 나열된 파일에서 온 것으로, 던전 디펜스 소스 코드의 일부이기도 합니다. 쉼표로 구분된 줄 번호는 파일 속 여러 개의 개별 라인을 말합니다. 하이픈으로 분리된 줄 번호는 파일 내 줄 범위를 말합니다.

  1. DunDefEnemyController.uc: 972
  2. DunDefEnemyController.uc: 715
  3. DunDefEnemyController.uc: 915
  4. DunDefEnemyController.uc: 11
  5. DunDefEnemyController.uc: 109
  6. DunDefEnemyController.uc: 201, 167
  7. UI_GlobalHUD.uc: 11
  8. UI_GlobalHUD.uc: 27
  9. DunDef_SeqAct_ShowNotification.uc: 41
  10. Main.uc: 233
  11. UI_PlayerHUD.uc: 51
  12. DunDef_SeqAct_ShowNotification.uc: 36
  13. DunDefPlayerController.uc: 309
  14. Main.uc: 345
  15. UILabel_ScoreIndicator.uc: 53, 112
  16. UILabel_ScoreIndicator.uc: 59, 115
  17. UILabel_ScoreIndicator.uc: 103
  18. UILabel_ScoreIndicator.uc: 32
  19. UILabel_ScoreIndicator.uc: 65, 103
  20. Data_HighScores.uc: 11-19
  21. Data_HighScores.uc: 35-64
  22. UI_GameOver.uc: 75, Main.uc: 186-209
  23. UI_AddingHighScore.uc: 18-30, Data_HighScores.uc: 35-64
  24. UIPanel_HighScores.uc: 16-30
  25. UI_Options.uc: 31-43
  26. UI_Options.uc: 164-165, 55

블로그 7: 23 일차

모두 안녕하세요!

지난 블로그 글 이후 엄청 바쁜 반주간인 만큼, 꽤 많은 것들이 완성되었습니다! 약간의 게임 데모 마무리 작업에 있으며, 최종 마무리 & 다듬기 적용 중이니 곧 직접 확인해 보실 수 있을 것입니다. 결과적으로는 많은 변경 사항이 밸런싱과 콘텐츠 통합 쪽으로 맞춰졌습니다. 엄청난 미디어에 힘입어 많은 부분에 활기가 도는 것을 보는 것은 항상 흥분되는 일이지요. 물론 추가한 함수성도 엄청나게 많으니, 굵직한 것부터 함께 훑어 보도록 하겠습니다:

  • 키즈멧을 통해 라바 구덩이를 변경했습니다. 예전에는 엄청난 피해를 주던 Physics Volume 이었는데, 지금은 그냥 약간의 피해만 입히고 플레이어를 안전한 장소로 텔레포트시키도록 하고, 불공평하긴 해도 적의 경우는 죽도록 했습니다. 이 작업에는 트리거 볼륨을 Touched 한 것이 무엇인지 클래스 타입을 검사하기 위한 새로운 키즈멧 조건이 필요했습니다.
  • 멋진 카메라 추적 기능에 보간도 적용하여 추가하여 카메라가 벽을 통과하자 않도록 했으며, (마우스 콘트롤 스키마 내의) 카메라 회전 방법을 변경하여 화면 가장자리에 커서를 올리는 것도 포함하도록 했습니다. 또한 카메라 FoV 를 퓨보트 상 비율을 가지고 동적으로 스케일하여, 플레이어가 와이드스크린 해상도 / 가로 분할화면을 사용할 때도 멀리 볼 수 있도록 했습니다.
  • 마우스 커서 아래 어디를 조준하는지를 나타내는 부유 파티클 효과를 추가하고, Particle Color Parameter 를 통해서 적 위에 커서를 올렸을 때 빨간색으로 바뀌도록 했습니다. 오너 Player 만 뷰에서 이 파티클 효과를 보게 되며, 'bOnlyOwnerSee' 액터/컴포넌트 옵션을 사용합니다.
  • 가스가 흐트러질 때까지 적을 기침하게 만드는 "gas trap" 을 추가했습니다. 타워가 포화를 퍼붓는 동안 적들을 느리게 만들기 좋은 기술이죠.
  • 타워-배치 시스템의 상태를 상속하여 타워를 판매하는 기능을 추가했습니다. 물론 소비한 것의 일정 비율만을 그 체력에 따라 돌려받게 됩니다. 경제학 101장, 소위 '감가상각' 입죠.
  • 추후 참가하는 플레이어가 고유한 캐릭터 머티리얼 색을 갖도록 하여, 쉽게 구분할 수 있게 만들었습니다.
  • 메인 메뉴에 옵션을 한웅큼 추가했습니다: 감마, gfx, 음악 볼륨 슬라이더, 해상도 선택기, 전체화면/포스트프로세싱 토글 등.
  • 게임의 룩을 완전히 바꿨습니다. 하하! 근본적으로 지오메트리-외곽선 포스트프로세싱 머티리얼과 대비 조절을 통해 카툰풍 느낌을 강화시켰습니다. 던전 디펜스에 활기찬 분위기를 내는 뚜렷한 느낌을 주고 싶었습니다.

자 그럼, 어떻게 돌아가는지 알아보도록 합시다. 먼저 라바에 떨어진 플레이어는 '리스폰'하면서, 적은 죽이는 키즈멧 기능을 훑어 봅시다. 그 작동법은 이러하며, 그에 대한 것을 둘 정도 설명해 보겠습니다:

ddblog7-1.jpg

여기서 보듯이, (두 라바 볼륨에 대한) Touched 이벤트가 둘 있으며, 그 후 Touched 의 'Instigator' 가 적 클래스인지 검사하고, 그렇다면 엄청난 피해를 입힙니다. 아니면 Instigator 는 플레이어 클래스이며, 오브젝트 목록에서 임의의 스폰지점을 선택한 다음 플레이어를 그 지점으로 텔레포트시키고, 약간의 피해를 준 다음, 새 위치에 텔레포트 비주얼 이펙트를 스폰합니다. 끝!

딱 하나 염두에 둘 점은, Touched 이벤트에 bPlayerOnly 를 꺼서 적도 트리거시킬 수 있게 했다는 점, MaxTriggerCount 와 RefireDelay 를 0 으로 설정하여 필요한 만큼 빠르게 반복 트리거 가능하도록 했습니다. 제가 작성한 "Is Of Class" (클래스의 것인지) 조건은 단순히 편집가능 변수 'name ClassName' 을 갖고 있으며, True 또는 False 출력을 활성화시키기 위해 "TheObject.IsA(ClassName)" 결과를 사용합니다.(1) 다 됐습니다! (알아둘 점은 이런 방식은 어떤 볼륨형과도 물론 제대로 작동한다는 것으로, PhysicsVolume 을 이용한 이유는 순전히 레벨의 라바 위에 이미 놓아두었기 때문입니다.)

다음으로 카메라가 벽을 통과하는 것이 마음에 들지 않아서, 월드 지오메트리에 대한 콜리전 검사를 하기 위해 추적을 하고, 지오메트리에 걸쳐 미끄러질 때 카메라 이동이 꽤나 부드러워 지도록 그 추적의 결과로 보간합니다. 이 작업은 Camera 클래스의 UpdateViewTarget() 함수에서 수행했으며, 여기가 바로 제가 작성한 "CheckForCollision" 함수를 호출하여 카메라 위치가 계산되는 곳입니다.(2) CheckForCollision 안에 카메라의 이상적인 위치에서 뷰-타겟(폰)의 위치로 월드 지오메트리에 대한 것만 검사를 하며 추적을 수행했습니다.(3) 추적에 뭔가 걸리면, 걸린 위치의 오프셋을 원래 (이상적인) 카메라 위치에서 취하고, 그 곳으로의 보간을 시작합니다. 보간이 지속적으로 업데이트 가능하도록 이 작업을 매 프레임마다 해 주었으며, 연이은 추적에서의 타겟 오프셋도 마찬가지입니다. 여기에는 카메라 위치가 벽을 뚫고 가지 못하게 하는 효과가 있으며, VLerp 속도에 따라 충돌된 카메라 위치 사이가 부드럽게 변천되도록 합니다. 충돌된 카메라 위치에 "Hit Normal" 결과도 약간 추가하여 충돌된 표면 앞으로 약간 이동시킬 수 있도록 하였으며, Z 방향쪽으로도 약간 오프셋시킵니다. 카메라가 항상 플레이어 캐릭터 위에 밀착할 수 있도록 하기 위해서입니다. (결국 내려보기형 게임이 되는 것이지요.) (5)

다음으로 와이드스크린에서, 또는 2인 게임에서 가로 분할된 뷰포트에서 플레이하다 보니 충분히 멀리까지 볼 수가 없었으며, 그 결과 제 저격 본능이 제대로 발휘되지 못하고 있었습니다. 그래서 표준 4:3 비율에 비교해서 현재 플레이어의 상 비율에 따라, 타겟 FoV 를 동적으로 조절하기로 했습니다. 이 작업 역시도 카메라 클래스에서 했는데, UpdateViewTarget 안에 DefaultFOV 에다 FOV 를 직접 설정하기 보다는(6), PCOwner 의 (PlayerController 의) HUD 해상도를 구하는 "AdjustFOV" 함수를 작성하고, 그 상 비율에 따라 출력 FOV를 HUD 상 비율이 4:3 에 준하는 만큼 스케일합니다.(7) 그러나 선형 스케일링은 너무 극단적일 터라 하지 않았습니다. 상-비율-스케일-인수를 0.4 승까지 올려, 화면이 넓어져 감에 따라 너무 어항같아 보이지 않으면서 FoV 가 점차 상승되도록 했습니다. 만약을 대비해 스케일러 최소/최대를 0.75/1.5 로 제한시켰습니다. 엄청 넓은 화면이든 가로 분할화면이든, 게임감이 좀 나을 것입니다.

마지막으로 오른쪽 마우스 버튼을 누르고서 뷰를 회전시키는 기존 콘트를 스키마가 그리 우아하지 않다고 결정내리고, 대신 좀 더 전통적인 "마우스 위치로 이동, 화면 가장자리에 올리면 그 방향으로 회전" 방식으로 하기로 했습니다. 이 기능을 내기 위해 PlayerController 의 PlayerMove 함수 속으로 들어갔으며, 현재 마우스 위치가 화면의 좌우편 3% 이내인지 (마우스 위치를 HUD 의 X 해상도 3% 위치와 비교하여) 검사하도록 했습니다.(8)

마우스가 실제로 화면 좌/우 가장자리에 있다면, Mouse Delta 의 부호는 화면의 그 쪽 가장자리 방향에 있는 것이며, 그 후 현재 Mouse Delta X 를 내 "Rotate Camera" 함수에 적용하였습니다. 그래서 화면의 오른쪽 가장자리에서 마우스를 왼쪽으로 움직인대도 카메라가 왼쪽으로 회전하지 않는 것입니다. 이런 방식이 우클릭 방식보다 꽤나 자연스럽다고 생각했으며, RMB 를 나중에 다른 용도로 쓸 수도 있는 것입니다. 좋은게 좋은거죠!

다음으로 플레이어가 어디를 조준하고 있는지, 적을 타게팅하고 있는지 좀 더 잘 나타내 보이고 싶었습니다. 그 표지로써 순수히 UI 나 Canvas 이펙트에 의존하기 보다는 월드에 있는 Particle Effect 로 가기로 했습니다. 그래야 3D 공간에서 스케일 가능하죠. Player 클래스에다 ParticleSystemComponent 를 추가(9), Player 아키타입에 휘몰아치는 소용돌이 파티클 템플릿을 주었습니다. 이 컴포넌트에 bOnlyOwnerSee 를 설정하여 해당 플레이어의 뷰에만 보이도록 했습니다. "Foreground" 에 이 컴포넌트의 Scene Depth Priority Group 역시도 설정하여 지오메트에 방해받지 않도록 하였으며, 좀 더 UI 요소같아 보이도록 했습니다. 그리고 그 컴포넌트의 AbsoluteTranslation 을 참으로 설정하여, 그 Translation 이 액터 공간이 아닌 월드 공간에 있도록 했습니다. 마지막으로 PlayerController 에다 이 컴포넌트로의 참조를 전달했는데, 이는 포인터-타겟 위치에 있는 것의 위치를 잡는 것을 담당하게 됩니다.(10)

그리고서 내 PlayerController 클래스는 단순히 이 컴포넌트의 Translation 을 이전 블로그에 기술한 화면-광선검사된 위치로 설정하니 끝이었습니다: 가리키는 곳을 나타내는 멋진 파티클 이펙트 표지입니다.(11) 그러나 이 표지가 적을 가리킬 때는 색이 변하게 하고 싶었습니다. 이러한 경우에 대한 것은 이미 PlayerMove 에서 화면 광선테스트가 Enemy 클래스에 맞는지를 검사하여 마쳤습니다. 그래서 파티클 시스템의 색이 바뀌게 하기 위해, ParticleSystem 의 서브-이미터에 "Color Parameter" 를 추가했습니다. 이 Color Parameter Module 을 'Colorizer' 로 설정하고, 코드에서 ParticleSystemComponent.SetColorParameter('Colorizer', NewColor) 를 호출하여 게임내에서 색을 변경합니다. 적 위에 있을 때는 빨강이 되도록, 그렇지 않을 때는 하양(에 파티클의 상속 색을 곱한 것)이 되도록 변경했습니다.(12) Particle Color Parameter 모듈은 이미터에 잇따라 스폰되는 파티클에만 영향을 끼침에, 기존 파티클에는 영향을 끼치지 않음에 유의하십시오. 효과 내의 모든 파티클에 즉시 착색하려면, 이펙트의 머티리얼 착색을 완전히 동적으로 바꾸기 위해 MIC, 머인불변을 사용해야 할 것입니다. 제 경우 이런 작업이 꼭 필요하지는 않았는데, 수명이 짧은 새 파티클을 끊임없이 쏟아내는 시스템이기 때문입니다.

ddblog7-2.jpg

다음으로 멀티플레이어에서 각 플레이어의 외양과 색이 달랐으면 했습니다. 이 처리를 위해 베이스 플레이어 머티리얼의 각 컬러 채널을 맞바꾸어 디퓨즈 텍스처 변종을 4 개 만들었습니다. 그런 다음 Player 아키타입에다 Materials 배열을 추가했고(13) , PlayerController 의 PostBeginPlay 함수에서 현재 다른 LocalPlayerControllers 수가 얼마나 되는지 LocalPlayerControllers 반복처리기를 통해 검사했습니다. 존재하는 수에서 1을 빼고, 자신의 플레이어 "번호"를 정했습니다.(14) 그 후 Controller 가 Pawn 을 취하는 PlayerController.Possess 함수에서 이 플레이어 '번호'를 인덱스로 사용하여 아키타입에 지정된 해당 배열에서 바라는 Material 을 구했습니다. 마지막으로 이 선택된 머티리얼을 캐릭터 메시(제 경우 메시가 단일 머티리얼을 사용하니 0 요소)에 적용하기 위해 Mesh.SetMaterial() 을 호출했습니다.(15) 다 해 놓고 나니, 게임의 각 플레이어가 독특해 보입니다!

릴리스에 가까워 지니, UI 에 옵션을 좀 추가해야 해상도 등 여러가지 세팅을 사용자가 INI 파일을 파지 않고도 조절할 수 있겠구나 싶었습니다. 특히 일반 해상도, 전체화면/창모드, 포스트프로세스 효과 끄기 (슬프게도 그냥 안좋아하시는 분도 있고, 비디오 카드가 오래되서 그럴 수도 있고), 감마 조절/음악 볼륨/사운드 효과 볼륨 조절 슬라이더 등입니다. 이들 각각을 어떻게 구현했는지 간단히 살펴 봅시다:

  • 해상도 선택 & 전체화면 토글: 여기에는 지원 해상도 ("1024x768", "1280x720" 등)에 대한 문자열 데이터를 포함하는 체크박스 배열을 사용했습니다. 이 체크박스에는 ButtonClicked 델리게이트를 설정하여 다른 해상도 박스가 체크되었는지 확인하며, 그렇다면 더이상 체크하지 않(아 한 번에 하나의 해상도만 선택되도록 했)습니다. 또한 클릭할 때마다 이 값을 참으로 설정하여 정기적으로 체크박스 "체크해제" 행위를 방지하여, 켤 수만 있도록 했습니다. (16) 전체화면 토글은 단지 디폴트 켜고/끄기식 체크박스입니다. 마지막으로 플레이어가 "OK"를 클릭하면 "SetRes [resolution][fullscreen]" 콘솔 명령을 현재 선택된 해상도 체크박스 문자열 값과 전체화면 값으로 실행시킵니다. "SetRes" 콘솔 명령은 실제 해상도 변경을 처리하며, 사용자의 INI 에 최근 값을 저장해 주기도 합니다. (17)
  • 포스트프로세스 토글: 포스트프로세싱 토글을 위한 체크박스도 추가하였으며, 이 값은 (더이상 전역 포스트프로세싱 환경설정 값이 없어 보이기에) ViewportClient 클래스안에 환경설정 Boolean 에다 저장하는 값입니다. ViewportClient init 함수에서, 포스트-프로세싱 불리언이 거짓이라면, "show postprocess" 콘솔 명령을 내려 포스트프로세싱을 끕니다.(18) 옵션 메뉴를 통해 포스트-프로세싱 불리언이 토글될 때마다 매번 이 작업을 수행해 주며, ViewportClient 클래스에서 SaveConfig() 을 호출하여 포스트프로세싱 불리언을 사용자의 INI에 저장하기도 합니다.(19)
  • 감마 슬라이더: 옵션 메뉴에 슬라이더를 추가해, 그 최소/최대 값을 적절한 감마 값으로 설정하고, 옵션 메뉴에 있는 매 프레임마다 "Gamma [SliderValue]" 콘솔 명령을 내립니다. (그 스크립트 콜백을 만들기가 왠지 싫었거든요, 하하.)(20) 또한 현재 감마 값을 ViewportClient 클래스의 환경설정 변수로써 저장도 하였습니다. 다른 방법으로는 저장할 길이 없기 때문이며, (콘솔 명령을 사용하여) ViewportClient 초기화 내에서 이 값을 활성 Gamma 로 설정하기도 했습니다.(21)
  • 음악 & SFX 슬라이더: 여기에는 게임의 모든 SoundCue 상의 'SoundClasses' 를 지정해 주고, AudioDevice 콘트롤 상의 'SoundMode' 를 각각 SoundClasses 의 Volume 으로 설정합니다. 에픽의 SoundModesAndClasses.upk 를 (그 내장 사운드클래스 전부를 사용할 수 있도록) 게임의 스타트업 패키지에 추가했고, SoundCue 클래스의 디폴트 속성을 편집하여 디폴트 SoundClass 를 "SFX" 가 되도록 했습니다. 특히나 뮤직 큐는 에디터 내 "Music" SoundClass 의 것이 되도록 설정하고, ViewportClient 의 초기화 내 AudioDevice 상의 'Default' SoundMode 를 설정합니다. Editor 에서 'Default' SoundMode 의 Effects 배열에 두 가지 Effects 를 추가했습니다. 하나는 'SFX' , 또 하나는 'Music' 사운드클래스 용입니다. 그래야 그 볼륨 수준을 개별적으로 제어할 수 있을 테니까요. 그런 다음 (구성해 둔 "SFX" 및 "Music" 사운드클래스에 상응하는 인덱스를 가지고) Current SoundMode 의 Effects 배열의 'VolumeAdjuster' 값을 변경하기 위한 Set Volume 함수를 작성했습니다. (22) 마지막으로 SFX-Volume 및 Music-Volume 환경설정 볼륨을 ViewportClient 클래스에 추가했고, Options 메뉴에 각각의 UI Slider 값에 설정해 줬습니다.(23) 옵션 메뉴에 있을 때는, 슬라이더 관련 값을 지속적으로 AudioDevice 속에 업데이트시키기 위해 SetVolume 함수를 호출합니다.(24) 그리고 별안간, 실시간으로 개별 조절가능한 SFX 및 Music 오디오 세팅이 완성되었습니다!

ddblog7-3.jpg

결승선에 가까워 가며, 동료 아티스트 모건 로버츠에게 폭탄 선언을 했었습니다. 게임을 툰-화 시키겠다고! 정확한 셀 셰이딩이 아니라 제가 원한 것은 좀 더 극단적인 것으로, 자세히 말하자면 지오메트리 윤곽선을 그리고 좀 더 부드러운 느낌을 위해 색 대비값을 줄이고자 했습니다. 이는 PostProcess 체인에 Material 이펙트를 둘 추가시키는 것으로 완성되었습니다.

먼저 지오메트리 윤곽선을 내기 위해, 현재 픽셀의 화면-위치 주변으로 8 심도 샘플링을 한 다음 평균냈습니다. 이 평균 "근사치" 심도를 현재 픽셀의 심도와 비교하여, 현저한 (한계치보다 큰) 차이가 있으면 실제 화면 색이 아닌 검정색을 반환합니다. 고로 검정 선이 에지를 따라 그려집니다. 한계점 값을 약간씩 조절하는 것으로 완성된, 맛있는 3분 요리였습니다.

다음 게임의 전체적인 대비값을 좀 더 밝은 툰-형식으로 만들고 싶었습니다. 특히나 하이-엔드 색을 뭉개지 않으면서 저-강도 색을 꺼내올리기로 했습니다. 이 작업을 위해 화면에 0.5 값 픽셀을 찍었으며, 그 결과를 사용하여 스케일러를 (저강도를 밝게 하는) 1.5 에서 (고강도에 크게 영향을 끼치지 않는) 1.0 까지 선형보간했습니다. 그런 다음 이 스케일러에 원본 씬 색을 곱해주고, 약간 채도를 높여 좀 더 색감이 강한 느낌을 냈습니다. 그 결과를 아래에서 확인할 수 있습니다. 약간의 포스트-프로세싱이 내는 차이를 말이죠!

ddblog7-4.jpg

ddblog7-5.jpg

최소한의 아티스트적 변경사항만을 가지고도, 깨알같은 현실주의에서부터 카툰식 유희를 거쳐 형형색색의 판타지 세계를 통하는 변천을 거쳤습니다. 그런 것이 언리얼의 포스트 프로세싱 시스템의 힘이며, MIC 를 사용한 실시간 파라미터 조절의 능력이죠. 이런 시각 조절을 치우고 나니, 바야흐로 테스팅과 트위킹의 마무리 단계였습니다. 던전 디펜스를 커뮤니티에 올릴 때가 된 것이죠. 곧 최종 단계 결과물을 들고 찾아 뵙겠습니다!

블로그 7: 파일 참조

이 블로그에 언급된 정보는 아래 나열된 파일에서 온 것으로, 던전 디펜스 소스 코드의 일부이기도 합니다. 쉼표로 구분된 줄 번호는 파일 속 여러 개의 개별 라인을 말합니다. 하이픈으로 분리된 줄 번호는 파일 내 줄 범위를 말합니다.

  1. DunDef_SeqCond_IsOfClass.uc: 14
  2. DunDefPlayerCamera.uc: 243
  3. DunDefPlayerCamera.uc: 278
  4. DunDefPlayerCamera.uc: 288, 295
  5. DunDefPlayerCamera.uc: 286
  6. DunDefPlayerCamera.uc: 145, 205
  7. DunDefPlayerCamera.uc: 354
  8. DunDefPlayerController.uc: 1550-1557, DunDefHUD.uc: 56, 62
  9. DunDefPlayer.uc: 494-504
  10. DunDefPlayerController.uc: 226
  11. DunDefPlayerController.uc: 1586
  12. DunDefPlayerController.uc: 1594-1596, 1606-1609
  13. DunDefPlayer.uc: 88
  14. DunDefPlayerController.uc: 270
  15. DunDefPlayerController.uc: 224, DunDefPlayer.uc: 114
  16. UI_OptionsMenu.uc: 140-152
  17. UI_OptionsMenu.uc: 71
  18. DunDefViewportClient.uc: 297
  19. UI_OptionsMenu.uc: 68, DunDefViewportClient.uc: 359
  20. UI_OptionsMenu.uc: 94
  21. DunDefViewportClient.uc: 299, 354
  22. DunDefViewportClient.uc: 318
  23. DunDefViewportClient.uc: 51-52, UI_OptionsMenu.uc: 39-40
  24. UI_OptionsMenu.uc: 95

블로그 8: 26 일차

모두들 안녕하세요. (현재로서는) 블로그의 마지막 항목입니다. 정말 강도 높은 4 주간의 게임 개발이었는데, 전적으로 경이적인 느낌이기도 했습니다. 그림으로 설명드리겠습니다.

1 주차 끝무렵의 모습:

ddblog8-1.jpg

4 주차 끝무렵의 모습:

ddblog8-2.jpg

예상한 대로 언리얼은 프로세스 전반에 걸쳐 그 성능을 아름답게 뽑냈으며, 바로 그 덕에 그리 짧은 기한 내에 엄청난 작업을 해낼 수 있었습니다. 여러분 모두 던전 디펜스 데모를 즐기시고 이 글도 잃어 보시길 바랍니다. (멀티 태스커는 아니실테니 동시에는 말고요.) 저희 팀은 여러분이 저희 게임을 어떻게 생각하시는지, 드시는 의문에 답해드릴 준비가 언제든 되어 있습니다. 그러니 UDK 포럼에 참여하십시오. 언제든 거기서 도움이 되어 드리겠습니다.

한편 이 블로그 시리즈를 마무리하기 위해, 언리얼로 게임 및 프로토타입을 개발하기 위한 전체적인 접근법에 대해 약간 논의해 보는 기회를 갖고자 합니다. 여기 있는 것들은 저만의 의견으로, 어떤 상황에도 적용할 수 있다거나 항상 수긍할 만 하다거나 하지는 않을 것입니다. 그래도 여러분의 창의적인 노력에 약간의 도움이나마 되었으면 하는 바람입니다.

이것을 (하누카 시간에 딱 맞춘) "제레미의 언리얼 게임 개발 여신 8계명" 이라 부릅시다:

1. 코어 메커니즘을 초기에 잡은 다음 반복, 반복, 반복

여러 프로젝트를 거치면서 습득한 하나의 사실은, 단단한 기반 위가 아니고서는 집을 지어 봐야 소용이 없다는 것입니다. 다른 말로, 최고속 아트 제작 및 레벨 개발 단계에 이르기 전에 게임플레이 방식 자체가 재미있고 즐길만 하게 만들어 보라는 뜻입니다. 이런 발언은 무뇌아처럼 들리겠지만, 실제로 풀 콘텐츠 개발 단계로 바로 뛰어드는 것이 종종 재미야 있더라도, 결국 기초적인 작업을 깜빡 하게 되고 맙니다. 비싼 애셋 제작 전에 무슨 게임을 만들려 하는지 (최소한 디자인 표현법 만이라도) 확실히 알고, 거기에 이미 그 시점에서 (버그나 시각적인 요소와는 상관없이) 즐길만 한 게임이라 일컬을 수 있다면, 큰 고생은 이미 덜은 것입니다.

추가로 게임플레이 메커니즘을 일찍 잡을 수록 반복 시간도 많아져 되풀이되는 패스를 통한 다듬기가 용이해 집니다. 반복은 개발 사이클 전반에 걸쳐 일어날 수 있으나, 사전-제작/개념 프로토타이핑 단계로 더욱 많이 압축해 낼 수록 더욱 좋은 것입니다.

물론 예전에도 언급했 던 대로, 언리얼에는 빠른 반복을 지원하기 위한 뛰어난 툴이 다수 있습니다. 몇몇 꼽아 보자면, (실시간으로 값을 바꾸기 위한) Remote Control, (값을 하드코딩시키지 않고 본질적으로 데이터 주도형으로 만드는) Archetypes, (동일 어플리케이션 인스턴스에서 현재 편집중인 레벨을 플레이해 볼 수 있는) Play In Editor 등이 있습니다. 전부 하나하나, 물론 동시에도 (즉 PIE 에서 Remote Control 열기 가능) 활용하다 보면 세 배는 빨리 완성할 수 있을 것입니다. 게임플레이가 고마워 하겠죠.

2. 게임플레이 프로토타이핑에는 Placeholder 애셋 사용

3D 아티스트에게 캐릭터 모델링을, 테크니컬 아티스트에게 리깅을, 애니메이터에게 애니메이션을 한 다발 제작시켜 놨더니 결국 어느 미디어도 실제 게임플레이에 필요한 것에 적합하지 않았던 경험이 있으십니까? 없다고요? 그럼 운이 좋은겁니다. 이런 실수를 전에 저질러 봤는데, 말할 필요도 없이: 아티스트들은 싫어하죠. 좋아할 리 있겠습니까? 프로그래머와 디자이너들은 최종 아트워크가 프로덕션 파이프라인에 들어가기 전의 확인 작업으로, 둘 다 아트가 게임플레이 용도로 어떻게 구성되어야 하는지를 정확히 알아야 하며, 해당 아티스트와의 의사소통을 명확히 해야 합니다. 여기에 가장 좋은 방법은 PLACEHOLDER 애셋인 것 같습니다. 플레이스홀더란 뭐랄까, 최종 애셋을 표현하는 데 사용할 수 있는 인간형 캐릭터나 무기의 일반화된 단순 버전입니다. 이상적으로라면 플레이스홀더 애셋은 최종 애셋과 대략적인 치수, 모양, (스켈레탈 메시의 경우) 본 구조가 같을 것이나, 메커니즘의 복잡도에 따라 꼭 그럴 필요는 없습니다.

플레이스홀더 애셋을 사용하면 중요하게도 '코어 메커니즘을 초기에 잡기'에 (규칙 #1 참고) 용이할 뿐만 아니라, 아티스트가 최종 미디어의 실제 제작 단계에 들어가기 전에 게임에 의도된 방식을 확인할 수도 있게 되므로, 플레이스홀더 구현은 가장 효율적인 통신 수단이 될 수 있을 것입니다. 게다가 아티스트들 자체적으로도 보통 최종인 것에 대한 플레이스홀더 애셋을 직접 맞바꿀 수 있습니다. 즉 아티스트가 추상 공간에서만이 아닌, 실제 게임플레이 맥락에서의 비주얼을 반복처리할 수 있다는 뜻입니다. 고맙게도 언리얼에서는 임시 애셋을 최종 애셋으로 쉽게 맞바꿀 수 있습니다. 그냥 Archetype 또는 DefaultProperties 내에 (Mesh, AnimSet 등) 약간의 참조만 바꿔주면 (절대 코드 줄에는 직접 참조하지 마십시오) 다 된 것입니다. 여기까지 엔데쓰 계율 2번 이었습니다.

3. 언리얼 식을 따르라

분명 언리얼을 가지고 게임플레이 결과를 내는 방법은 여럿 있을 수 있지만, 이상적인 방법은 훨씬 적습니다. 이러한 "언리얼 식"은 보통 언리얼스크립트 인터페이스가 제공해 주는 함수성을 완전히 활용하는 것일 수도 있고, C++ 또는 Java 같은 기본적인 프로그래밍 언어 이상의 능력이 있습니다. 아무 예나 들어 보자면...

[*]뭔가 지연을 주거나 시간에 따라 발생하게 하고 싶으십니까? (Sleep 이나 MoveTo 같은) 잠복성 상태 함수성 또는 Timers 를 사용하십시오. 한 Tick 에 Time 기반 if 문장을 잔뜩 쓰지 않아도 됩니다. [*]Player 주변의 특정 Actor 를 찾아내고 싶으십니까? AllActors 에 개별적으로 World 의 모든 Actor 와의 거리를 검사하지 마시고, OverlappingActors 에다 반경을 주기만 하면 됩니다. [*]모든 플레이어 캐릭터 위에 갑옷 부착 구성을 하고 싶으십니까? 하나마다 액터를 만들지 마시고, 폰에 새 메시 컴포넌트(나 심지어 커스텀 컴포넌트 클래스)를 동적으로 생성하여 부착하면 됩니다! [*]딱 텍스처 하나만 가지고 다양한 머티리얼을 잔뜩 만들고 싶으신가요? 하나마다 고유 베이스 머티리얼을 만들지 마시고, 부모 머티리얼을 공유하는 머티리얼 인스턴스 불변을 사용한 다음 그냥 디퓨즈 텍스처 파라미터를 맞바꿔 버리면 됩니다. [*]그리고 무심고, 제발, 제발, 제발 구조체가 참조를 통해 전달될 거라 가정하지 마십시오. 함수 파라미터에 "out" 키워드를 사용하지 않고서야 디폴트는 항상 deep-copied 상태입니다. deep-copying 구조체는 불필요하게 느리거나 메모리를 잡아먹을 수 있고, 로직에서 구조체 변수가 고유하지 않은 참조라 가정한다면 잠재적인 버그 발생 가능성도 있습니다. 여기에 대해 자세히 설명되어 있는 UDN 문서가 있으니 참고하시기 바랍니다.

언리얼스크립트와 엔진 프레임워크는 C++ 로 직접 작업할 때보다 게임을 구현함에 있어 훨씬 탄탄한 감을 내도록 구성되어 있습니다. 언리얼로 개발해 가면서 예상했던 것보다 많은 상속 함수성을 찾게 될 것입니다. 언리얼로 뭔가를 구현하는데 애를 먹고 있다면, 엔진 내 이미 존재하는 편리한 함수성에 대해 모르기 때문일 수가 있습니다. 의심스러운 경우라면 보통 그렇습니다. 에픽의 코드를 읽어 보면, 그 샘플을 들여다 보면, 흥미로운 UDN 문서를 파 보면, 심지어 던전 디펜스 조차도 (완벽하진 않지만 ^_~) 살펴보다 보면, 초강력 프레임워크의 완벽 활용 방향을 짚어주는 사용 패턴을 쉽게 알아볼 수 있게 될 것입니다. 그러면 바로 이 곳으로...

4. 에픽의 코드베이스를 검색!

언리얼스크립트 엔진-프레임워크 코드베이스는 꽤나 방대합니다. 또한 문서화가 매우 잘 되어있으나, 어디서부터 시작해 봐야 할 지 당혹스러울 수가 있습니다. 처음부터 끝까지 주욱 읽어내려 가는 것은 그다지 추천할 만 하지는 못합니다. (최소한 Actor.uc & Object.uc 부터 익숙해 지시기를 추천하기는 합니다.)

궁극적으로 소름끼칠만큼 방대한 코드베이스를 대화형 지식 백과사전으로 바꿔주는 친구! 바로 Visual Studio (Express 는 프리 버전) 또는 기타 텍스트 편집 프로그램같은 Code IDE 검색 기능 "Find All in Files" 입니다. "Cursor", "Force" 등의 (보통 찾고자 하는) 키워드를 에픽의 코드 파일 전체 또는 일부 내에서 검색해 보면, 모든 종류의 공용 게임 요구를 처리하기 위해 에픽이 제공하는 관련 함수성에서의 개념을 제대로 참고해 볼 수 있을 것입니다. 좋은 경험 법칙이라면, "직접 만들기"로 결정하기 전에 에픽이 이미 "만들어 주지" 않았나 코드베이스를 검색해 보라는 것입니다! 내가 찾고 있던 것들을 얼마나 많이 이미 해놓았는가, 보시면 깜짝 놀라실 겁니다. 그리고 기어즈 오브 워를 플레이해 봤다면, 그다지 놀랍지도 않을 것입니다.

5. nFringe 사용. 마침표.

PixelMine 친구들에게 찬사를 보내야 겠네요. nFringe 정말 죽입니다. 그 'Intellisense' 및 코드 파싱은 보통 정확하며, 그 구문 검사도 (걍 한심한 버그와는 달리) 한심한 똥꾸빵꾸 버그를 줄이는 데도 큰 도움이 됩니다. nFringe 를 사용하면 코딩 생산성이 (적어도 저의 경우에는) 크게 향상되었으며, 에픽(과 자신)의 코드베이스 탐험도 훨씬 빨리 할 수 있을 것입니다. Intellisense 및 멤버 리스트를 통해, 클래스를 이리저리 돌아다니며 변수와 함수, 상태를 빠르게 살펴볼 수 있을 것입니다.

아무리 강조해도 지나치지가 않습니다: 언리얼 프로그래밍이 처음이라면, nFringe 의 도움으로 첫 걸음마를 훨씬 빨리 뗄 수 있을 것입니다. 현재로서는 문제가 딱 하나 있습니다: 언리얼에는 강력한 디버거가 있으나, nFringe 는 PixelMine 에서 상용 nFringe 라이선스를 구매하지 않고서는 접근 권한이 없습니다. 프로 개발자들에게만 해당되는 사항이죠. PixelMine 에 청합니다. 이 엄청난 툴을 대중에 공개하시면 신으로 숭배받을 수 있을 것입니다! 뭐 그런 정도는 되겠죠. 아무튼 네, 지금 nFringe 를 (없으시면 Visual Studio Express 도) 받으시고, 코딩이 처음인 양 코딩을 시작하시기 바랍니다!

6. 사용할 수 있는 디버깅 방법은 모두 사용

nFringe 가 있든 없든, 언리얼 프레임워크를 가지고 게임플레이를 디버깅할 수 있는 방법은 여러가지가 있으니, 최대의 결과를 내기 위해서는 다양한 기술을 전부 활용해야 할 것입니다. 그 중에서 제가 가장 선호하는 것은:

[*]Debug Draws (Spheres, Lines, Boxes 등): 3D 공간에서 뭐가 벌어지는지를 시각화해 보는 데 도움이 됩니다. 계산된 3D 변형의 결과를 보려 한다거나 크기 개념을 잡아보는 등에 좋습니다. [*]로그 문장: 아 로그: 스팸인가 정보인가. 특히나 nFringe 디버거가 작동하지 않는다면야! 로그를 가지고 어떤 데이터형이든 꽤 많은 양을 출력 창에 즉시 출력시킬 수 있으며 (showlog 콘솔 명령), 한 줄에 최대한의 정보를 얻을 수 있도록 "@" 심볼을 사용하여 복수의 문자열을 연쇄시킬 수 있습니다. 단지 Tick 함수에 넣고서 깜빡하지만 않도록 조심하시기 바랍니다. 로그를 스팸시키면 게임이 느려질 수 있습니다. 사실 Actor 클래스에 "bDebug" 토글이 설정된 경우에만 로그가 활성화되도록 하는 것이 최선인데, 이 토글은 필요에 따라 실행시간에도 편집가능 변수를 통해 전환시킬 수 있습니다. [*]라이팅제외 모드, 와이어프레임 모드: 뭔가 그래픽 작업을 하는 중에 레벨 라이팅이 꼬인 경우, 그냥 F2 키를 눌러 라이팅제외 모드로 변경하십시오. 또는 벽을 투시해 (치터!) 보고자 하는 경우는 F1키 와이어프레임 모드입니다. AI가 자신을 볼 수 없을 때 어떻게 행동하는지 알아보기에 매우 유용합니다. (흘끔!) [*]"Show Collision" 콘솔 명령은 월드의 모든 콜리전을 시각화시켜, 콜리전 관련 문제에 부딪힌 것 같을 때에 좋습니다. 통로를 통과할 수가 없나요? 게임 버그가 아니라, 레벨 디자이너가 심통좀 부리려고 크고 오래된 투명 블로커 메시를 입구 앞 밑쪽에다 놓았기 때문일 수도 있습니다. 'Show Collision' 으로 모두 보입니다! (좀 더 사실적으로, Pawn 의 콜리전 크기가 얼마나 되나 확인하기에 매우 좋습니다.) [*]게임 내 시간을 말 그대로 늦추고 싶을 때는 Remote Control 상의 Time Dilation 세팅을 사용하십시오. (현실에서라면 좋겠지만, 언리얼이 아직 그만큼 강력하지는 않습니다.) 이 기능은 이상한 점을 발견하고자 미시적 게임플레이 디테일에서의 애니메이션과 비주얼 이펙트가 정확히 어떻게 돌아가나 확인하고자 할 때 매우 유용한 기능입니다. 타이밍 관련 문제를 해결하기에 좋습니다. [*]Remote Control 은 Actor List 상의 "시계" 아이콘을 클릭하면 게임플레이 도중 스폰된 Dynamic Actors 전체를 표시해 주기도 합니다. 소멸되었어야 하는데도 아직 살아있는 액터가 없는지(, 예로 탄환이 충돌한 이후 어딘가에 걸려있다든지 등을) 이중 확인하기에 좋습니다. [*]모든 키즈멧 액션에는 "Output Comment To Screen" 옵션이 있는데, 켜면 게임내 콘솔 디스플레이에 주석이 표시될 것입니다. 어떤 액션이 언제 트리거되는지 알아보기에 좋습니다. 또는 프로 키즈멧 고수의 경우, ("말하기" 위해) Console Command 키즈멧 액션을 사용하여, 원하는 키즈멧 변수를 아무거나 출력해 내기 위해 Concatenate String 액션과 결합할 수도 있습니다. ^_^

여러가지 접근법 중 이러한 접근법들을 사용하고, 언젠가 (바라건데) nFringe 디버거의 혜택을 누구나 누릴 수 있는 날이 오게 되면, 눈이 왕방울만한 버그먹는 하마가 되실 수 있을 것입니다. 괜찮겠죠?

7. 키즈멧 사용은 똑똑히, 그러나...

오 위대한 키즈멧이시여! 레벨 디자이너에게 게임플레이 구현 능력을 하사하시고, 게임 디자이너에게 번개같은 프로토타이핑 능력을 부여하시었으니. 가져가시고, 주시도다. 키즈멧은 레벨 상호작용이나 심지어 특정 레벨-향 게임플레이 메커니즘 프로토타이핑에 있어서도 대적할 상대가 없는 환상적인 툴입니다. 그러나 거기에도 한계는 있습니다: 특별히 객체지항형/상속가능하지는 않습니다. 언리얼스크립트처럼 빠르지도 않고, 그만큼의 디버깅 능력이 있는 것도 아닙니다.

고로 키즈멧을 통해 모든 것을 다 하려는 것은 대부분의 최종 버전 게임을 구성하기 위한 접근법으로서는 적합하지 않습니다. 어떻게든 기를 쓰고 그러고자 한다면, 가능한 만큼 (확실히 많은 부분이 가능하니) 키즈멧으로 게임플레이 프로토타이핑은 가능하겠지만, 최종 제품에 가서는 그 대부분을 다시 쓰게 될 확률이 높다는 점 염두에 두시기 바랍니다. 키즈멧으로 뭔가가 잘 안된다면, 제 의견으로는 언리얼스크립트로 (또는, 능력 확장을 위해 키즈멧 액션을 새로 작성)하는 것을 고려해 보시기 바랍니다.

제 일반적인 규칙은: 레벨의 영속성 디자인 상에 발생하는 무언가라면, 키즈멧으로 하십시오. 레벨 자체적인 것이 아닌 동적인 오브젝트의 행위에 관련되거나 동적으로 스폰되는 무언가라면, 언리얼스크립트로 하는 것이 낫습니다. 키즈멧을 죽도록 사랑하다 못해 지구 끝까지 들었다 놨다 했던 다년간의 경험에서 나온 견해입니다. 오해하지는 마십시오: 이론적으로는 전체 게임을 동적으로-스폰되는 프리팹을 사용하여 작성할 수 있지만, 일정 시점을 지나게 되었을 때는 분명 무언가가 있을 거라는 것입니다. 그래도 존경스런 독자 여러분 걱정일랑 마세요. 키즈멧을 향한 저의 사랑은 식지 않을테니: 던전 디펜스의 경우 전체 하이-레벨 게임플레이 로직을 키즈멧으로 돌렸으니까요.

8. 중요한 것은 재미

우리는 인디 개발자입니다. 즉 대체로 재미에 초점을 맞추며, 억대 예산은 꿈도 못꿉니다. 물론 그 억대가, 더 나은 자신의 능력이 여러분 대부분의 목표겠죠. 언리얼이 그 목표를 이루는 능력을 갖추는 데 도움이 될 것입니다.

하지만 항상 잊지 말아야 할 사실은, 게임이 얼마나 괜찮은지, 모두의 시간을 뺏을 만한 가치가 있는지 알리러 나왔을 때는 그래픽이 얼마나 이쁜지 (물론 도움이야 되겠지만) 플레이타임이 얼마나 되는지 (오블리비언!) 메인 캐릭터의 가슴에 폴리곤 수가 얼마나 되는지는 중요하지 않다는 것입니다. 중요한 것은 플레이어가 만족할 만한 방식으로 괜찮은 피드백과 보수를 통해 상호작용식 경험에 빠져들게 만드는 것입니다.

아이러니인 것은, 가끔 동료 디자이너가 매일매일 최선의 판단을 내리지는 못한다는 것입니다. 친구, 가족, 동료, 개한테도 중요 시점을 플레이하게 해 보고, 피드백에 대해 말하거나/짖어 보게 해 보시기 바랍니다. 항상 듣기 좋지만은 않겠지만, 몸에 좋은 약은 입에 쓴 법이고, 아픈 만큼 게임은 성숙해 질 것입니다. 언리얼은 두말할 것 없이 전 세계 최강의 게임 제작 기술이지만, 균형이 잘 잡힌 재밌는 게임이 될 것인지, 또다른 Monster Madness 가 될 것인지는 전부 당신 손에 달렸습니다!

모두들 몸 건강히 지내십시오. 꿈★은 이루어진다! ^_^

-제레미 스티글리츠 (Jeremy Stieglitz)