GoW2 에서의 커버

GearsOfWar 에서 중요한 요소 중 하나는 엄폐이다. GoW 액션의 특징은 근접액션인데 이를 위해 게임 내 엄폐물을 두어 서로간의 근접 전투를 유도한다. (엑박 컨트롤러에 의한 영향도 있긴 하지만 논외로 한다.)

여기선 GoW 시리즈에서 프로그래밍 관점에서 엄폐물을 어떻게 다루는지에 대해 살펴본다.

Cover? (커버)

먼저 살펴보아야 할 것은 UE3 엄폐물 객체인 Cover (이하 커버) 이다. 그리고 레벨상에 이런식으로 설치된다.

커버에 대한 내용은 굉장히 많지만 중요한 것만 추리면 아래와 같다.

CoverLink.uc
class CoverLink Extends NavigationPoint
    native
    DependsOn( Pylon )
    placeable
    config( Game );
 
...
 
// 커버에서 행할 수 있는 액션
enum ECoverAction
{
    CA_Default,                 // 아무 액션 없음
    CA_BlindLeft,               // 왼쪽 방향으로 무기만 내민 채 공격하는 행동 (소극적 공격)
    CA_BlindRight,              // 오른쪽 방향. 이하 상동
    CA_LeanLeft,                // 왼쪽 방향으로 몸 전체를 내민 채 공격하는 행동 (적극적 공격)
    CA_LeanRight,               // 오른쪽 방향. 이하 상동
    CA_PopUp,                   // 커버 위로 몸 전체를 일으켜 세워 공격하는 행동 (적극적 공격)
    CA_BlindUp,                 // 커버 위로 무기만 내민 채 난사하는 행동 (소극적 공격)
    CA_SwatTurn,                // 현재 커버에서 반대쪽 커버로 재빠르게 이동하는 행동
 
    // AI 전용 행동
    CA_PeekLeft,                // 커버 왼쪽으로 고개를 내미는 행동
    CA_PeekRight,               // 커버 오른쪽. 이하 상동
    CA_PeekUp,                  // 커버 윗쪽. 이하 상동
};
 
enum ECoverType
{
    CT_None,                    // 커버가 아님
    CT_Standing,                // 풀 스탠딩 커버. 캐릭터가 서서 충분히 숨을 수 있는 높이의 커버
    CT_MidLevel,                // 캐릭터가 몸을 웅크려야만 숨을 수 있는 크기의 커버
};
 
// CoverLink 는 다수의 CoverSlot 을 가질 수 있다.
struct immutablewhencooked native CoverSlot
{
    var Pawn                 SlotOwner;         // 이 슬롯을 선점한 주인
    ...
    var array<ECoverAction>  Actions;           // 이 커버에서 수행 가능한 액션들
};
 
...

보면 알겠지만 커버에는 종류가 있으며 (스탠딩,미드레벨) 여기서 행할 수 있는 행동도 제한되어 있다.

CoverLink 는 1개 이상의 CoverSlot 을 가질 수 있으며 실제 캐릭터가 숨을 수 있는 영역은 CoverSlot (혹은 CoverSlot 과 CoverSlot 사이) 이다.

CoverLink 는 관리상 편의를 위한 레이어 (클래스) 이며, 실제 캐릭터가 엄폐를 위해 핸들링하는 객체는 CoverSlot 이다.

CoverSlot 은 CoverSlotMarker 에 의해 NavigationOctree 에 추가되어 씬관리된다. 이후 프로그래머의 쿼리에 의해 일정 영역에 존재하는 CoverSlotMarker 를 반환하게 되며 프로그래머는 이들을 순회하며 적당한 커버를 찾고 일거리를 수행하게 된다. 관계도는 이걸 보면 좀 도움이 된다.

캐릭터가 적당한 커버를 찾는 과정

캐릭터는 레벨에 놓여있는 커버를 사용하기 위해 먼저 적당한 커버를 찾는 행동을 취하게 된다. 먼저 이 과정에 대해 알아보자.

GearPawn

  • CanPrepareRun2Cover
    • 시작지점, 방향, 거리, 커버FOV 보정 후
    • FindCoverFromLocAndDir
  • native FindCoverFromLocAndDir
    • Collision 반지름 x 1.1 보다 거리를 크게 보정
    • 바로 앞에 벽이 있다면 뒤로 후진 (체크 시작 위치로부터)
    • (Debug) 플레이어의 FOV 를 그린다.
    • 월드의 모든 NavigationPoint.bAlreadyVisited = FALSE 로 초기화
    • 월드의 NativationOctree 에서 근처 반경의 NavigationOctreeObject 리스트를 쿼리로 얻어온다.
    • 찾은 리스트를 순회하며
      • 오브젝트의 Owner 가 CoverSlotMarker 인 놈을 찾고
      • 아직 방문하지 않았다면 if bAlreadyVisited == FALSE
      • 마커가 가지고 있는 커버의 Link 와 SlotIdx 를 구한다.
      • 해당 슬롯이 무효하거나 이미 선점당했다면 제끼고
      • Link 내 Slot 이 하나인지 여러개인지에 따라 적절하게 확인한다.
        • FillCoverPosInfo
        • ValidatePotentialCover
    • 엣지 있게 커버인 하기 위한 코드 (필자가 별도로 추가한 코드. GoW2 엔 없음)
  • FillCoverPosInfo
    • 왼/오른 슬롯의 위치들로 이뤄진 segment 와 체크위치, 방향간 SegmentDistToSegment 검사 후 결과를 기록하고 반환
  • ValidatePotentialCover
    • … 찾은 커버가 유효한지 확인하는 함수

GearPC

  • CancelAction ← 시작
    • TryToRunToCover
  • TryToRunToCover
    • if CanRunToCover then AcquireCover
  • CanRunToCover
    • 체크할 방향, 거리를 구한 후
    • CTPawn::CanPrepareRun2Cover
  • AcquireCover
    • 로컬 유저인지 체크
    • CoverAcquired
  • CoverAcquired
    • … 이후로는 커버액션 및 복제 과정이므로 아래서 설명

핸드라이팅 버전은 여기

캐릭터가 취한 커버의 정보를 복제하는 과정

적당한 커버를 찾았다면 이제 커버를 대상으로 적당한 일거리를 수행해야 할 차례이다.

GoW2 에서는 커버를 향해 향해 달려드는 행동 (GSM_Run2MidCov 또는 GSM_Run2StdCov) 을 취하며, 이를 또 서버를 통해 다른 클라이언트로 복제를 수행하게 되는데 여기선 이 과정에 대해 알아본다.

GearPawn

  • FindBestCoverSideFor
    • 서버라면 클라이언트에서 복제된 값 (ReplicatedCoverDirection) 을 리턴
    • 엣지 있게 커버인 중이라면 적절하게 방향을 결정하고 리턴 (필자가 별도로 추가한 코드. GoW2 엔 없음)
    • CoverInfo.Normal x -1 (원래 Cover 의 Rotation) 에서 Pawn.Rotation 간 offset 을 구하고
    • 그 offset 이 일정 수치 이상이라면 sideways 커버인이라는 가정하에 카메라의 방향으로 바라볼 방향을 결정한다. 그리고 리턴
    • CoverToPawn 방향과 CovInfo.Tangent (커버입장에서 좌측) 간 dot 가 0 이상이면 좌측, 아니면 우측을 리턴
  • CoverAcquired
    • 커버액션을 CA_Default 로 초기화. 서버일 경우 복제됨 GearPawn.CoverAction
    • SetCovPosInfo
    • 현재 커버 Link 에서 캐릭터가 위치한 Slot 을 선점한다. CoverLink::Claim
    • AcquiredCoverInfo = CovInfo 서버라면 복제됨
    • SM_Run2Cov 수행 로컬, 서버 양측에서 수행될 것이므로 정상적으로 복제가 이뤄질 것이다.
  • SetCovPosInfo (클라의 경우 GearPawn.AcquiredCoverInfo 를 복제받을 시 호출됨)
    • 커버 관련된 변수들을 GearPawn 에 모두 대입한다.
    • Autonomous 프록시라면 복제를 위해 서버에 알린다. GearPC::ServerCoverTransition
    • 커버타입을 세팅한다. 서버일 경우 복제됨 GearPawn.CoverType
  • LeaveCover
    • AcquiredCoverInfo 초기화. 서버라면 복제됨
    • 커버타입을 리셋한다. 서버일 경우 복제됨 GearPawn.CoverType

GearPC

  • CoverAcquired ← 시작
    • 커버할 방향을 찾는다. GearPawn::FindBestCoverSideFor
    • GearPawn::CoverAcquired
    • PlayerTakingCover 상태로…
  • ServerCoverTransition
    • 서버인 경우만 동작. 복제되어 넘어온 파라메터를 ReplicatedCoverDirection 에 세팅
    • 현재 커버슬롯을 선점할 수 없다면 ClientInvalidCoverClaim
    • CoverAcquired
  • ClientInvalidCoverClaim
    • LeaveCover
  • LeaveCover (GearSpecialMove::BreakFromCover 에 의해 호출됨. 로컬/서버에서만)
    • 현재 커버 Link 에 대한 선점해제
    • GearPawn::LeaveCover
    • PlayerWalking 상태로…

핸드라이팅 버전은 여기

커버의 확장

쭉 봐와서 알겠지만 GoW2 에서는 커버를 '숨을 수 있는 공간에 명시적으로 심어놓은 마커들의 상관관계를 정의해 놓은 메타데이터' 를 이용하여 구현한 것이다.

이 말인 즉슨 숨을 수 있는 공간이 아닌 다른 용도로써의 공간으로 확장도 가능하다는 얘기다. 당연히 별도의 로직 수정은 필요하지만 말이다.

배트맨 아캄 어사일럼에서 주인공이 와이어액션으로 매달릴 수 있는 석상이나 난간 등을 상상해보면 이해가 빠를 것이다.1)

1) 그렇다고 반드시 커버와 비슷한 형태로 구현되었을 것이라고 확신하는 것은 아니다. 하지만 이 방식이 효율적이기 때문에 비슷한 용도의 메타데이터를 생성하여 구현했을 것이라 추측된다.