캐릭터

이동 복제

클라이언트 사이드에서 이동 후, 서버에 알리는 과정에 대한 요약

코드 플로우

  1. PlayerController::ReplicateMove
  2. GearPC::CallServerMove
    1. SendServerMove
      • MantleServerMove
      • RMServerMove
      • CoverServerMove
      • RoadieServerMove
      • ServerMove

무브먼트

GearsOfWar2 의 마커스의 무브먼트를 분석한 내용이다.

조준 구현

개요

XBox360 패드의 왼쪽트리거, PC에서는 마우스 우클릭을 하면 조준모드가 된다. 이때 카메라 FOV가 바뀌고, 캐릭터의 Stance가 변경되고, HUD에 크로스헤어가 표시되는 등 일련의 동작들이 이루어진다. 여기서는 (UnrealEngine3로 개발된) GoW2 소스 내에서 조준 트리거가 발동되고 이어지는 일련의 과정들을 분석해본다.

코드 플로우

  • 발생
    1. exec function ButtonPress(coerce Name ButtonName) ( GearPlayerInput_Base.uc )
      • InputButtonDataList[ButtonIdx].Active = 1; 아래 루핑에서 이 변수를 참조
    2. 각 버튼의 deligator 함수 호출
      • InputButtonDataList[ButtonIdx].ButtonHandler(TRUE);
  • 루프
    1. event PlayerTick ( GearPC.uc )
      • UpdateTargetingStatus();
    2. simulated function UpdateTargetingStatus() ( GearPC.uc )
      • if CanTarget && WantsToTarget
        • final function bool WantsToTarget() ( GearPC.uc )
          • final function bool WantsToTarget() ( GearPC.uc )
            • IsButtonActive(GB_LeftTrigger) ( GearPC.uc )
              • final function bool IsButtonActive(EGameButtons Button, optional bool bRaw) ( GearPC.uc )
                • GearPlayerInput_Base(PlayerInput).IsButtonActive( Button, bRaw );
                  • simulated final function bool IsButtonActive( EGameButtons Button, optional bool bRaw ) ( GearPlayerInput_Base.uc )
                    • bRaw 플래그에 따라 InputButtonDataList[???].RawActive 나 InputButtonDataList[???].Active 를 참조하여 bool 형 리턴
      • then SetTargetingMode(bNewIsTargeting);
    3. final function SetTargetingMode(bool bNewIsTargeting) ( GearPC.uc )
      • bIsTargeting = bNewIsTargeting;
      • TargetingModeChanged();
    4. final function TargetingModeChanged() ( GearPC.uc )
      • MyGearPawn.SetTargetingMode(bIsTargeting);
    5. final function SetTargetingMode(bool bNewTargetingMode) ( GearPawn.uc )
      • bIsTargeting = bNewTargetingMode;
        • void UGearAnim_BlendByTargetingMode::TickAnim(float DeltaSecons, float TotlaWeight) ( GearAnimNodes.cpp )
          • GearPawn 의 bIsTargeting 을 체크하여 AR_Idle_Downsights_Aim 시퀀스로 연결
      • TargetingModeChanged();
    6. final simulated function TargetingModeChanged()
      • 일반 모드로 돌아오는데 지연시간 SetWeaponAlert( float ) 설정
      • 해당 무기에 알림 MyGearWeapon.TargetinModeChanged(Self);
        • simulated function TargetingModeChanged(GearPawn P ) ( GearWeapon.uc )
      • HUD에 알림. 무기별 크로스헤어를 그린다거나..

Lean 조준 (Cover액션)

Cover 의 양쪽 엣지에서 조준버튼을 누르면 빼꼼히 고개를 내밀며 조준자세를 취하는 행위 (엣지가 아닌 Cover 중앙에서 역시 조준이 가능하지만 작동원리가 크게 다르지 않은 관계로 여기선 생략)

분석 포인트

  • 발동
    // GearPC.uc
    simulated state PlayerTakingCover extends PlayerWalking
    {
        ...
        simulated function DetermineLeanDirection( ... )
        {
            ...
            if ( bTargeting )
            {
                // 적절하게 양쪽 엣지에 있는지 확인한 후
                OutPawnCD = CD_Right;
                OutPawnCA = CA_LeanRight;
            }
            ...
        }
        ...
    }
  • 에니메이션 노드
    // GearAnimNodes.cpp
    void UGearAnim_CoverBlend::TickAnim( FLOAT DeltaSeconds, FLOAT TotalWeight )
    {
        ...
        switch ( GearPawnOwner->CoverAction )
        {
            ...
            case CA_LeanRight :
            case CA_LeanLeft  : DesiredChildIdx = 5;  PickedCoverAction = GearPawnOwner->CoverAction;  break;
            ...
        }
        ...
    }

SpecialMove

개요

GearsOfWar2 에서 캐릭터의 무브먼트를 표현하는 수단이다. 회피, 로디런, 엄폐In/Out, 슬립, 재장전, etc. 는 모두 SpecialMove 기반으로 구현되어 있다. 때문에 GearsOfWar2 의 캐릭터 무브먼트를 파악하기 전에 SpecialMove 방식을 이해하는 것이 중요하다.

분석 포인트

  • 변수
    // GearPawn.uc
    struct native SMStruct
    {
        var ESpecialMove   SpecialMove;
        var GearPawn       InteractionPawn;
        var INT            Flags;
    };
    var ESpecialMove            SpecialMove;                  // 현재 진행중인 SpecialMove
    var ESpecialMove            PreviousSpecialMove;          // 이전 진행했던 SpecialMove
    var SMStruct                PendingSpecialMoveStruct;     // 다음에 진행될 SpecialMove 정보
    var transient bool          bEndingSpecialMove;           // SpecialMove 를 종료중에 있는가?
    var repnotify SMStruct      ReplicatedSpecialMoveStruct;  // 복제된 SpecialMove 정보
    var Array<class<GearSpecialMove>  SpecialMoveClasses;     // SpecialMove 가 구현된 클래스의 배열
    var GearPawn                      InteractionPawn;        // SpecialMove 행위가 일어나는 Pawn
    var INT                           SpecialMoveFlags;       // SpecialMove 를 처리하는데 필요한 추가 플래그
  • SpecialMove 인터페이스
    // GearSpecialMove.uc
    function bool CanOverrideMoveWith(ESpecialMove NewMove)                   // NewMove 가 현재 진행중이 SpecialMove 를 덮어버릴 수 있는가?
    function bool CanOverrideSpecialMove(ESpecialMove InMove)                 // 현재 SpecialMove 가 InMove 를 대체할 수 있는가?
    function bool CanChainMove(ESpecialMove NextMove)                         // 현재 SpecialMove 가 끝난 뒤, NextMove 로 연결되어질 수 있는가?
    final function bool CanDoSpecialMove( optional bool bForceCheck )         // 현재 SpecialMove 가 수행될 수 있는가?
     
    function SpecialMoveStarted(bool bForced, ESpecialMove PrevMove)          // SpecialMove 가 시작될 시점에 호출되는 이벤트
    function SpecialMoveEnded(ESpecialMove PrevMove, ESpecialMove NextMove)   // SpecialMove 가 종료될 시점에 호출되는 이벤트
    function bool ShouldReplicate()                                           // 다른 클라이언트에게 복제되어야 하는가?
     
    function PreProcessInput(GearPlayerInput Input)                           // InputSystem 에 의해 호출되며, SpecialMove 에게 Input 을 읽고 수정할 기회를 준다.
    simulated function PreDoubleClickCheck( GearPlayerInput_Base Input )      // 위와 비슷하게 더블클릭을 체크할 수 있는 기회를 준다.
    simulated function BS_AnimEndNotify(AnimNodeSequence SeqNode, float PlayedTime, float ExcessTime) // BodyStance 에니메이션이 끝날 때 호출되는 함수
  • SpecialMove 시작
    // GearPawn.uc
    simulated event DoSpecialMove(ESpecialMove NewMove, optional bool bForceMove, optional GearPawn InInteractionPawn, optional INT InSpecialMoveFlags)
  • DoSpecialMove 함수 분석
    // GearPawn.uc
    simulated event DoSpecialMove(ESpecialMove NewMove, optional bool bForceMove, optional GearPawn InInteractionPawn, optional INT InSpecialMoveFlags)
    {
        // 대략적인 수행내용을 나열한 것이고, 이중에는 상호배제되는 실행내용도 있으니 반드시 코드를 참조하면서 보길 바란다.
        이미 하고 있는 SM이라면 리턴
        NewMove 의 SpecialMove 인스턴스 검사 by VerifySMHasBeenInstanced(NewMove)
        하고 있던 SpecialMove 가 로디런이라면 토탈 로디런타임 갱신
        NewMove 정보를 가지고 SMStruct 세팅
        다른 SpecialMove 가 종료중에 있다면 (bEndingSpecialMove==true) SMStruct 를 PendingSpecialMoveStruct 로 대입 후 return. 다음턴을 예약
        NewMove 가 현재 SpecialMove 를 덮어쓸 수 있는지 (Cur.CanOverrideMoveWith(NewMove)) 혹은 NewMove가 현재 SpecialMove 를 대체할 수 있는지 (New.CanOvrrideSpecialMove(SpecialMove)) 를 체크
        현재 SpecialMove 다음에 NewMove 를 체인할 수 있는지 체크. 만약 그렇다면 NewMove 를 PendingSpecialMoveStruct 에 대입 후 return. 다음턴을 예약
        현재 SpecialMove 의 종료이벤트 호출. SpecialMoveEnded(PrevMove, NewMove);
        NewMove 의 정보들을 현재 Pawn에 대입 (SpecialMove, InteractionPawn, SpecialMoveFlags)
        NewMove 가 복제되야 한다면 (NewMove.ShouldReplicate==true) NewMoveStruct를 ReplicatedSpecialMoveStruct 에 대입. 다른클라이언트에게 복제
        현재 SpecialMove 를 PreviousSpecialMove 로 백업
        NewMove 의 시작 이벤트 호출. SpecialMoveStarted(NewMove, PrevMove, bForceMove);
        bForceMove 가 true라면 PendingSpecialMoveStruct 를 깨끗하게 비움. NewMove 수행 중에 방해받지 않게 하기 위해서
        NewMove 가 None이라면 PendingSpecialMoveStruct 를 수행. DoSpecialMoveFromStruct(PendingSpecialMoveStruct, false);
    }
     
    simulated final function DoSpecialMoveFromStruct(SMStruct InSpecialMoveStruct, optinal bool bForceMove)
    {
        DoSpecialMove(InSpecialMoveStruct.SpecialMove, bForceMove, InSpecialMoveStruct.InteractionPawn, InSpecialMoveStruct.Flags);
    }
     
    simulated function LocalDoSpecialMove(ESpecialMove NewMove, optional bool bForceMove=FALSE, optional GearPawn InInteractionPawn, optional INT InSpecialMoveFlags=0)
    {
        로컬 유저가 조종하는 PC가 아니라면 리턴
        DoSpecialMove 호출
    }
     
    simulated function ServerDoSpecialMove(ESpecialMove NewMove, optional bool bForceMove=FALSE, optional GearPawn InInteractionPawn, optional INT InSpecialMoveFlags=0, optional ESpecialMove SMToChain)
    {
        서벙에서 SpecialMove 수행한 후 클라이언트들에게 복제한다.
    }
     
    // EndSpecialMove 도 이와 비슷한 세트를 가지고 있다.
  • 기타 함수
    // GearGame.cpp
    UBOOL AGearPawn::IsDoingASpecialMove() const
    {
        return (SpecialMove != SM_None && !bEndingSpecialMove);
    }
     
    UBOOL AGearPawn::IsDoingSpecialMove(BYTE AMove) const
    {
        return (SpecialMove == AMove && !bEndingSpecialMove);
    }
     
    UBOOL AGearPawn::IsDoingDeathAnimSpecialMove() const  // 이하 SpecialMove 상태를 조회하는 도움함수들
    UBOOL AGearPawn::IsDoingMeleeHoldSpecialMove() const
    UBOOL AGearPawn::IsDoingSpecialMeleeAttack() const
    UBOOL AGearPawn::IsSpecialMeleeVictim() const
    UBOOL AGearPawn::IsDBNO() const
    UBOOL AGearPawn::IsAHostage() const
    UBOOL AGearPawn::IsAKidnapper() const
  • PawnToPawn 행위에 대한 정의 - Pawn 과 Pawn 이 서로 같이 수행하는 SpecialMove 에 대한 리스트를 따로 관리한다. 이것은 PawnToPawnInteractionList 키워드로 조회하면 로직사용 부분을 바로 찾을 수 있다. GearsOfWar2 는 기본적으로 Kidnapper 처리를 이것으로 한다.
    // GearPawn_Infantry.uc
    defaultproperties
    {
        ...
        PawnToPawnInteractionList=(SM_Kidnapper)
        ...
    }
     
    // GearPawn.uc
    final simulated function bool CheckPawnToPawnInteractions(out ESpecialMove out_SpecialMove, out GearPawn out_InteractionPawn)
    {
        ...
        foreach PawnToPawnInteractionList(InteractionSM)
        {
            현 상황에서의 적절한 interaction 을 찾는다.
        }
        ...
    }
  • SpecialMove 체인가능 여부에 대한 체크
    // GearPawn.uc
    simulated final function bool CanChainSpecialMove(ESpecialMove NextMove)
    {
        return (SpecialMove == SM_None ||                                     // 현재 SpecialMove 가 없거나
                SpecialMoves[SpecialMove].CanChainMove(NextMove) ||           // 현재 SpecialMove 다음에 NextMove 가 체인될 수 있다거나
                SpecialMoves[SpecialMove].CanOverrideModeWith(NextMove) ||    // 현재 SpecialMove 를 NextMove 가 오버라이드할 수 있다거나
                SpecialMoves[NextMove].CanOverrideSpecialMove(SpecialMove));  // NextMove 가 현재 SpecialMove 를 대체해버릴 수 있다면
                // NextMove 는 현재 SpecialMove 뒤에 체인될 수 있다.
    }
  • Input 후킹
    // GearPlayerInput.uc
    function PreProcessInput(float DeltaTime)
    {
        ...
        if ( bIsDoingASpecialMove )
            Pawn.SpecialMoves[Pawn.SpecialMove].PreProcessInput(Self);
        ...
    }
  • Double Click 후킹
    // GearPlayerInput_Base.uc
    function Actor.EDoubleClickDir CheckForDoubleClickMove( float DeltaTime )
    {
        ...
        if ( bIsDoingASpecialMove )
            Pawn.SpecialMoves[Pawn.SpecialMove].PreDoubleClickCheck(Self);
        ...
    }
  • Button 필터
    // GearPlayerInput.uc
    function bool FilterButtonInput(Name ButtonName, bool bPressed, int ButtonIdx)
    {
        ...
        if ( MyGearPawn.SpecialMoves[MyGearPawn.SpecialMove].ButtonPress(ButtonName) )
            return true;
        ...
    }
  • 매 프레임 TickSpecialMove 호출
    // GearGame.cpp
    void AGearPawn::TickSpecial( FLOAT DeltaSeconds )
    {
        ...
        if ( SpecialMove != SM_None && SpecialMoves( SpecialMove ) != NULL )
        {
            SpecialMoves( specialMove )->TickSpecialMove( DeltaSeconds );
        }
        ...
    }

코드 플로우

  • SpecialMoveClasses 세팅
    // GearPawn_Infantry.uc
    defaultproperties
    {
        ...
        SpecialMoveClasses(SM_Emerge_Type1)                  = class'GSM_Emerge'
        SpecialMoveClasses(SM_Emerge_Type2)                  = class'GSM_EmergeBurst'
        ~
        SpecialMoveClasses(SM_BloodMountDriver_CalmMount)    = class'GSM_BloodMountDriver_CalmMount'
        PawnToPawnInteractionList=(SM_Kidnapper)
        ...
    }
  • Pawn 파괴 시
    // GearPawn.uc
    simulated function Destroyed()
    {
        if ( SpecialMove != SM_None && SpecialMoves[SpecialMove] != None )
            EndSpecialMove();
    }
  • SpecialMove 복제
    // GearPawn.uc
    simulated event ReplicatedEvent( name VarName )
    {
        ...
        switch ( VarName )
        {
            case 'ReplicatedSpecialMoveStruct' :
                DoSpecialMoveFromStruct(ReplicatedSpecialMoveStruct, TRUE);
                break;
        }
        ...
    }
  • SpecialMove 틱 (스크립트)
    // GearPawn.uc
    simulated function Tick(float DeltaTime)
    {
        ...
        if ( SpecialMove != SM_None && SpecialMove[SpecialMove] != None )
            SpecialMoves[SpecialMove].Tick(DeltaTime);
        ...
    }
  • SpecialMove 틱 (네이티브)
    // GearGame.cpp
    void AGearPawn::TickSpecial(FLOAT DeltaTime) // <- AActor::Tick, UnLevTick.cpp
    {
        ...
        // Tick current Special Move
        if ( SpecialMove != SM_None && SpecialMoves(SpecialMove) != NULL )
        {
            SpecialMoves(SpecialMove)->TickSpecialMove(DeltaSeconds);
        }
        ...
    }
  • SpecialMove 데미지 보정
    // GearPawn.uc
    function AdjustPawnDamage(
                   out  int                    Damage,
                        Pawn                   InstigatedBy,
                        Vector                 HitLocation,
                   out  Vector                 Momentum,
                        class<GearDamageType>  GearDamageType,
        optional   out  TraceHitInfo           HitInfo
    )
    {
        ...
        if ( IsDoingASpecialMove() )
        {
            if ( SpecialMoves[SpecialMove].bOnlyInteractionPawnCanDamageMe && InstigatedBy != InteractionPawn && InstigatedBy != Self )
            {
                Damage = 0;
                return;
            }
     
            Damage *= SpecialMoves[SpecialMove].default.DamageScale;
        }
        ...
    }
  • 멀티플레이 시 커버에서의 데미지 보정
    // GearPawn.uc
    simulated final function float MultiplayerCoverDamageAdjust( vector ShotDirection, Pawn InstigatedBy, vector HitLocation, float Damage )
    {
        ...
        if ( SpecialMove == SM_Run2MidCov ||
             SpecialMove == SM_Run2StdCov ||
             CoverType == CT_None ||
             CoverAction == CA_LeanLeft ||
             CoverAction == CA_LeanRight ||
             CoverAction == CA_PopUp )
        {
            return Damage;
        }
        ...
    }
  • 죽었을 시 SpecialMove
    // GearPawn.uc
    simulated function PlayDying(class<DamageType> DamageType, vector HitLoc)
    {
        ...
        SpecialMoveWhenDead = SpecialMove; // TODO: ? 이건 GearPawn.SpecialMove 일텐데 여기서 세팅한다는 것은 이전에 이미 Die 관련 SpecialMove 를 세팅한다는 의미? 시간날 때 추적해 볼것
        ...
    }

특징

  • 非전투 시 이속&재생 속도 보정
    // GearGame.cpp
    FLOAT AGearPawn::MaxSpeedModifier()
    {
        ...
     
        // 상황에 맞는 이속&재생 속도 factor 를 얻는다.
        if ( SpecialMove != SM_None )
        {
            Result = SpecialMoves(SpecialMove)->GetSpeedModifier();   // SpecialMove 하고 있다면 그놈의 SpeedModifier 를 얻는다.
        }
        else if ( bIsCrouched )
        {
            Result = CrouchedPct;                                     // Crouch 하고 있다면 지정된 이속 퍼센티지를 얻는다.
        }
        else if ( ... )
        {
            ...                                                       // 그밖에 상황에 맞는 이속&재생 factor를 얻는다.
        }
     
        ...
     
        // 非전투시엔 평상시의 85% 속도로 이동&에니메이션재생 한다.
        if ( !bIsInCombat )
        {
            Result *= 0.85f;
        }
     
        ...
    }

RoadieRun

개요

PC가 빠르게 달려가는 행위. SpecialMove 의 한 종류로 구현되었다.

분석 포인트

  • 변수
    // GearPawn.uc
    var config bool         bCanRoadieRun;
    var config bool         bCanBeForcedToRoadieRun;
    var transient float     RoadieRunBoostTime;             // 로디런 진입 시, 초반 부스트보너스를 위한 타이머
     
    // GearPC.uc
    var() config float      RoadieRunTimer;                 // MoveAction 버튼을 얼마나 눌러야 로디런이 발동되나? default=0.5
  • 로디런 발동
    //GearPC.uc
    state PlayerWalking
    {
        exec function bool TryASpecialMove( bool bRunActions ) // 이 함수는 GearPlayerInput.uc, HandleActionButton 에서 호출된다.
        {
            ...
            SetTimer( RoadieRunTimer, FALSE, nameof( TryToRoadieRun ) );
            ...
        }
    }
     
    funal function TryToRoadieRun()
    {
        ...
        DoSpecialMove( SM_RoadieRun );
        ...
    }
  • 로디런 종료
    // GearPlayerInput.uc
    function HandleActionButton( ... )
    {
        ...
        // 버튼을 떼었을 때 이쪽으로 흘러들어온다.
        if ( IsDoingSpecialMove( SM_RoadieRun ) )
        {
            EndSpecialMove();
        }
        ...
    }
  • 로디런 강제 종료조건 계산
    // GearSpecialMoves.cpp
    void UGSM_RoadieRun::TickSpecialMove(FLOAT DeltaTime)
    {
        ...
        if ( PawnOwner->Velocity.Size() < PawnOwner->DefaultGroundSpeed * 0.3f )
        {
            RunAbortTimer += DeltaTime;
            if ( RunAbortTimer > 0.3f )
            {
                // velocity 가 일정 수치 미만으로 0.3초 이상 떨어졌다면 종료신호를 보냄. 엄폐나 기타 물체에 부딛혀 속도가 줄어드는 경우에 해당됨
                PawnOwner->eventLocalEndSpecialMove(PawnOwner->SpecialMove);
            }
        }
        else
        {
            RunAbortTimer = 0.f;
        }
        ...
    }
  • 로디런 초반 보너스
    // GearSpecialMoves.cpp
    FLOAT UGSM_RoadieRun::GetSpeedModifier()
    {
        ...
        BoostModifier = ((1.f - ((PawnOwner->WorldInfo->TimSeconds - PawnOwner->RoadieRunBoostTime)/1.5f)) * 0.5f);
        ...
        return (Super::GetSpeedModifier() + BoostModifier);
    }

해석하자면 이렇다.

  • 초반 1.5초동안은 약간의 보너스 부스트혜택을 가진다는 것
  • 로디런 시작과 동시에 0.5의 factor 보너스를 가지며, 이것은 1.5초동안 0으로 감쇠
  • 그러므로 최종 factor 변화량은 시작 시 1.65에서 1.5초동안 1.6으로 줄어드는 셈

모든 로디런 초기에 이 혜택이 부여되는 것은 아니고 MantlePickup 후에 발동되는 로디런만 이 혜택이 부여되더라.

Evade

개요

특정 방향으로 빠르게 회피하는 행위

분석 포인트

  • 발동
    // GearPlayerInput.uc
    function HandleActionButton( ... )
    {
        if ( bPressed )
        {
            ...
        }
        else
        {
            ...
            TryToEvade( bDblClickMove ? CurrentDoubleClickDir : DCLICK_None );
            ...
        }
    }

Move2Idle

Run2Cover

개요

PC가 엄폐물로 붙는 행위. SpecialMove 의 한 종류로 구현되었다.

분석 포인트

  • 변수
    // GearPawn.uc
    var(SpecialMoves)   config   float      Run2CoverMaxDist;                // 정면으로 Cover 할 수 있는 최대 거리. 이 값을 벗어나면 PC를 Evading을 한다.
    var(SpecialMoves)   config   float      Run2CoverPerpendicularMaxDist;   // 옆방향으로 Cover 할 수 있는 최대 거리.
     
    // CoverLink.uc
    struct immutablewhencooked native CovPosInfo
    {
        var CoverLink      Link;
        var int            LtSlotIdx;
        var int            RtSlotIdx;
        var float          LtToRtPct;
        var vector         Location;
        var vector         Normal;
        var vector         Tangent;
     
        structdefaultproperties
        {
            LtSlotIdx=-1
            RtSlotIdx=-1
            LtToRtPct=+0.f
        }
    };
  • SpecialMove 세팅
    // GearPawn_Infantry.uc 의 defaultproperties
    SpecialMoveClasses(SM_Run2MidCov) = class'GSM_Run2MidCov'
    SpecialMoveClasses(SM_Run2StdCov) = class'GSM_Run2StdCov'
  • 발동
    // GearPlayerInput.uc
    function HandleActionButton( ... )
    {
        ...
        if ( CanTryToRunToCover() ) // 엄폐 시도나 할 수 있는 상황인가? 즉, 얕은 체크 (내 SpecialMove 를 조회)
        {
            if ( ... && TryToRunToCover() ) // 엄폐 시도
            {
            }
            ...
        }
        ...
    }
  • Replication 에 의한 발동
    // GearPawn.uc
    simulated event ReplicatedEvent( name VarName )
    {
        ...
        case 'AcquiredCoverInfo' :
            ...
            DoSpecialMove(SM_Run2MidCov, TRUE);
            ...
            DoSpecialMove(SM_Run2StdCov, TRUE);
            ...
            break;
        ...
    }

코드 플로우

  • 발동1
    function TransitionFromRoadieRunToCover( CovPosInfo FoundCovInfo, optional bool bNoCameraAutoAlign ), GearPC.uc
  • 발동2
    function bool TryToRunToCover(optional bool bSkipSpecialMoveCheck, optional float CheckScale, optional EDoubleClickDir DblClickDirToCheck=DCLICK_None, optional bool bPreventSideEntry), GearPC.uc
  • 처리 과정
    1. function AcquireCover(CovPosInfo CovInfo, optional bool bNoCameraAutoAlign), GearPC.uc

CoverAcquired(CoverInfo, bNoCameraAutoAlign);

  1. function CoverAcquired(CovPosInfo CovInfo, optional bool bNoCameraAutoAlign), GearPC.uc

MyGearPawn.CoverAcquired(CovInfo);

  1. simulated function CoverAcquired(CovPosInfo CovInfo), GearPawn.uc

DoSpecialMove(Move);

  1. simulated event DoSpecialMove(ESpecialMove NewMove, optional bool bForceMove, optional GearPawn InInteractionPawn, optional INT InSpecialMoveFlags), GearPawn.uc

심화

  • Try… 에 관련한 세가지 함수가 있고 그 차이는 다음과 같다.
    • function bool CanTryToRunToCover(), GearPC.uc - 현재 진행중인 SpecialMove 등을 조사하여 커버행위를 할 수 있는 상황인지 체크한다.
    • function bool CanRunToCover(...), GearPC.uc - 현재 플레이어 위치로부터 입력이 일어난 방향 (앞,좌,우) 으로 커버할 엄폐물이 있는지 체크한다.
    • function bool TryToRunToCover(...), GearPC.uc - 커버를 할수 있으면 (CanRunToCover) 한다. (AcquireCover)
  • function bool CanRunToCover(...), GearPC.uc 의 처리 과정
    1. 키보드 더블클릭 입력이나 아날로그 스틱의 움직임을 검사하여 플레이어로부터 체크해야 할 방향을 구한다.
    2. 방향에 따라 적절하게 체크할 거리를 계산하고 MyGearPawn.CanPrepareRun2Cover 를 호출하여 적절한 커버링크의 위치를 구한다.
  • simulated final function bool CanPrepareRun2Cover(...), GearPawn.uc 의 처리 과정
    1. 파라메터들을 적절히 보정하고
    2. FindCoverFromLocAndDir 를 호출한다.
  • UBOOL AGearPawn::FindCoverFromLocAndDir(...), GearGame.cpp 의 처리 과정
    1. 체크거리 = max(체크거리, 충돌실린더 반지름)
    2. 체크방향을 2D 좌표계 (X,Y) 로 노멀라이즈
    3. 만약 벽에 파뭍혀있다면 살짝 뒤로 뺀다. (SingleLineCheck)
    4. 그 위치로부터 체크거리를 반지름 삼아 영역내에 있는 ACoverSlotMarker 들을 구한다.
    5. 발견된 ACoverSlotMarker 들을 체크하여 붙기에 가장 적절한 놈을 찾는다.

Cover Move

개요

커버에 숨어서 좌우로 이동하는 행위. SpecialMove 는 아니지만 어떻게 처리되는지를 기록해 둔다.

분석 포인트

  • 상태
    // GearPC.uc
    state PlayerTakingCover extends PlayerWalking
    {
        // 커버진입 시 PlayerTakingCover 상태로 전이된다.
        // 여기서 이동 및 커버아웃 조건을 판별한다.
        ...
    }
  • 이동
    // GearPC.uc
    state PlayerTakingCover extends PlayerWalking
    {
        ...
        simulated funciton UpdtaePlayerPosture( ... )
        {
            ...
            MyGearPawn.CurrentSlotDirection = CD_Left;
            ...
            MyGearPawn.CurrentSlotDirection = CD_Right;
            ...
        }
        ...
    }
     
    // GearGame.cpp
    void AGearPawn::CalcVelocity( ... )
    {
        ...
        else if ( bIsInCover && ( !Controller || !Controller->bPreciseDestination ) )
        {
            // 커버에서의 움직임 계산
        }
        ...
    }
  • 커버아웃
    // GearPC.uc
    state PlayerTakingCover extends PlayerWalking
    {
        ...
        event PlayerTick( float DeltaTime )
        {
            ...
            if ( bBreakFromCover )
            {
                ...
                LeaveCover();
                ...
            }
            ...
        }
        ...
    }
     
    simulated function LeaveCover(optional bool bPushOut)
    {
        ...
        GotoState( 'PlayerWalking' );
        ...
    }

CoverSlip

개요

커버의 양쪽 엣지에서 앞으로 빠르게 치고 나아가는 행위

분석 포인트

  • 발동
    // GearPC.uc
    simulated state PlayerTakingCover extends PlayerWalking
    {
        exec funciton bool TryASpecialMove( bool bRunActions )
        {
            ...
            else if ( bDoMonkeyMoveChecks && CanDoSpecialMove( SM_CoverSlip ) )
            {
                DoSpecialMove( SM_CoverSlip );
                return TRUE;
            }
            ...
        }
    }

CoverMantle

개요

낮은 커버에 몸을 기댔을 경우, 커버를 올라타고 앞으로 뛰어넘는 행위

분석 포인트

  • SM 상속관계
    GSM_BaseVariableFall            // 일반적인 Jump 에 대한 정의. Jump, Fall, Land 의 세가지 단계
             ↑
    GSM_MantleOverCoverBase         // Cover (엄폐물) 를 타 넘는 형식의 특별한 Jump 를 정의
             ↑
    GSM_MidLvlCoverBase             // 낮은 (MidLevel) 엄폐물만 넘도록 제한
  • 세가지 상태. 점프, 낙하, 착지
    // GSM_BaseVariableFall.uc
    enum EMoveType
    {
        EMT_Jump,         // 점프. 중력에 의한 낙하가 아닌 에니메이션 재생만 한다.
        EMT_Fall,         // 낙하. 점프 에니메이션이 끝나면 중력에 의해 낙하 속도가 계산된다.
        EMT_Land,         // 착지. 낙하 중 낙하방향에 충돌이 예상되면 착지 에니메이션이 재생된다.
    };
  • Mantle 가능 여부 체크
    // XTSM_MantleOverCoverBase.uc
     
    // Mantle 할 수 있는지 체크하는 함수. 가장 중요한 함수라 할 수 있다.
    protected function bool InternalCanDoSpecialMove()
    {
        // 커버에 있는가?
        ...
     
        // 스틱을 (Remapped) 앞쪽으로 충분히 땡기고 있는 상태인가?
        ...
     
        // 특정 슬롯 안에, 혹은 2개의 슬롯 사이에 위치하고 있는가?
        ...
     
        // Cover In 중에 너무 멀리서 시도하고 있지는 않은가?
        ...
     
        // 플레이어가 속해있는 슬롯의 MantleTarget 을 체크한다.
        FindMantleDistance() 호출
    }
     
    simulated function bool FinddMantleDistance()
    {
        // Mantle 시작위치, 끝위치, 거리 를 구함.
        ...
     
        // 끝위치에 뭔가 걸리는게 있는가? 있다면 그놈이 무시해도 될 녀석인가?
        ...
     
        // 끝위치에서 시작위치로 라인체크.
        ...
     
        // 뭔가 걸리는게 있다면 걸린면의 Normal 을 가지고 다시 라인체크
        EndTrace = StartTrace - HitNormal * MantleDistance;
        HitActor = POwner.Trace( HitLocation, HitNormal, EndTrace, StartTrace, TRUE, Vect( 1.f, 1.f, 1.f ) );
        ...
     
        // 이제 뭔가 걸린부분이 Mantle 의 진정한 끝부분이라고 할 수 있다. Mantle 의 끝부분 위치를 확정한다.
        ExtentRadius = CollisionRadius * Sqrt( 2.f ) + 1.f; // 충돌실린더를 쓰지 않고 AABB만 사용하는 actor가 있을지도 모르니 최악의 상황을 대비하여 충돌실린더의 대각선방향을 구한다. +1 은 안전빵의 의미
        SetBasedPosition( MantleEndLoc, HitLocation + Normal( StartTrace - EndTrace ) * ExtentRadius );
     
        // 그리고 Mantle 거리를 다시 구한다.
        ...
     
        // 깔끔한 착지를 보장하기 위해 착지지점을 한번 훑는다.
        VerifyLandingClear( GetBasedPosition( MantleStartLoc ), GetBasedPosition( MantleEndLoc ) );
        ...
    }
     
    function bool VerifyLandingClear( Vector InMantleStartLoc, Vector InMantleEndLoc )
    {
        // 착지지점부터 예상되는 거리 (예상Mantle속도 * t) 만큼 떨어진 위치까지 수평! 라인체크.
        ...
     
        // 뭔가를 발견했고 그것을 처리할 수 없다면 ( ~= 치울 수 없는 것이라면 ) 아쉽지만 넘어갈 수 없다.
        ...
     
        // 이제 StartDownTrace 로부터 아래로 100 만큼 수직 라인체크. 위와 마찬가지로 뭔가 발견했고 그것을 처리할 수 없다면 넘어갈 수 없다.
          // 한가지 이상한게, HandleHitActor 의 파라메터 중 StartDownTrace 란 녀석. 욘석이 암래도 out (C++로 따지자면 &) 이 붙어야 할 듯 싶다.
          // HandleHitActor 를 호출하는 곳에서 이녀석이 갱신되는 것을 가정하고 사용되는 듯 한데...
          // HandleHitActor 함수의 StartDownTrace 파라메터를 out 키워드 붙이고 아래 '수직 라인체크' 부분이 바로 이전에 있던 '수평 라인체크' 안에 포함되어야 할듯.
        ...
     
        // 이제 충돌할 만한것이 암것도 없다. 월드 Geo 에 대해 수직으로 충돌체크하여 착지지점 확정
        ...
     
        // 착지지점 세팅. Extent (원본의 1/4) 기준으로 충돌처리 하였기 때문에 Extent 높이의 2배 하여 원본의 1/2 (착지모션 기준) 로 맞춰준다.
        locEstLandingSpot += vect( 0, 0, 1 ) * Extent.Z * 2.f;
        SetBasedPosition( EstimatedLandingLoc, locEstLandingSpot );
    }
  • 도착지점 선점처리 Mantle 이 깔끔하게 끝나는 것을 보장하기 위해서 도착 예상지점에 미리 충돌실린더를 설치해 놓아, 다른 플레이어나 AI가 이 장소에 들어올 수 없게 처리하는 로직이 존재한다.
    // XTSM_MantleOverCoverBase.uc
    function SpecialMoveStarted( bool bForced, ESpecialMove PrevMove )
    {
        ...
        if ( DoCylinder )
        {
            // Mantle End 지점으로부터의 도착 예상지점 (fall 처리동안의 이동거리를 반영한 도착 예상지점)
            CylLoc = GetBasedPosition( EstimatedLandingLoc );   // 미리 구한 착지 예상 지점
            PlaceholderCylinder = POwner.Spane( class'MantlePlaceholderCylinder', , , CylLoc );
            if ( PlaceholderCylinder != None )
            {
                PlaceholderCylinder.PawnToIgnore = POwner;      // Mantle 행위자만 이것을 무시할 수 있다.
                PlaceholderCylinder.SetCollisionSize( POwner.GetCollisionRadius() * 1.15f, POwner.GetCollisionHeight() ); // 만약을 위해 행위자보다 살짝 두껍게
            }
     
            // Mantle End 지점. 바로 위 예상지점과 같이 선점하여 이 두 구간 사이를 다른 Pawn 들이 끼어들지 못하도록 한다.
            CylLoc = GetBasedPosition( MantleEndLoc );
            PlaceholderCylinder2 = POwner.Spawn( class'mantlePlaceholderCylinder', , , CylLoc );
            if ( PlaceholderCylinder2 != None )
            {
                PlaceholderCylinder2.PawnToIgnore = POwner;
                PlaceholderCylinder2.SetCollisionSize( POwner.GetCollisionRadius() * 1.35f, POwner.GetCollisionHeight() );
            }
        }
        ...
    }

코드 플로우

  1. GSM_MantleOverCoverBase::SpecialMoveStarted( … )
    • 실린더로 MantleEnd지점 및 도착예상지점 에 충돌실린더 생성
  2. GSM_BaseVariableFall::SpecialMoveStarted( … )
    • PlayJump 함수 호출
  3. GSM_MantleOverCoverBase::PlayJump()
    • EMT_Jump 로 상태변경
    • 루트모션 ON!
    • 에니메이션 종료 알림 설정
    • 에니메이션 재생
    • 충돌 실린더 재조정 ( 0.5배 )
  4. GSM_MantleOverCoverBase::BS_AnimEndNotify( … ), Jump 에니메이션 종료시 호출되는 이벤트
    • MoveType 이 EMT_Jump 면 bJumpToFallPhysicsTransition = TRUE 로 설정
  5. UGSM_MantleOverCoverBase::TickSpecialMove( float DeltaTime ), 매 프레임 호출되는 ticking 함수
    • bJumpToFallPhysicsTransition 이 TRUE 면 eventDoingJumpToFallPhysicsTransition() 함수 호출
  6. GSM_MantleOverCoverBase::DoingJumpToFallPhysicsTransition()
    • PlayFallAnimation 호출
      1. GSM_MantleOverCoverBase::PlayFallAnimation( float StartTime )
        • 에니메이션 종료 알림 해제
        • (Jump 에니메이션이 종료된) 이 시점까지도 Mantle 거리의 80% 채 도달하지 못했다면 강제로 MantleEnd 지점을 세팅
        • Fall 에니메이션 재생
        • 에니메이션 종료 알림 설정 ( 서버이고 다른 클라이언트의 Pawn일 경우에만 설정. 착지 에니메이션을 한번 재생해주기 위해 )
    • PlayFall 호출
      1. GSM_MantleOverCoverBase::PlayFall()
        • 이 EMT_Fall 로 상태변경
        • 루트모션 OFF!
        • 떨어질때 밑에 뭔가 있다면 그 위로 착지될 때까지 시간 예측하여 에니메이션 재생속도 조절
  7. UGSM_BaseVariableFall::TickSpecialMove( FLOAT TimeDelta ), 매 프레임 호출되는 ticking 함수
    • EMT_Fall 일 경우 Pawn→Physics 를 체크하여 PHYS_Falling, PHYS_Flying, PHYS_None 이 아니라면 eventLanded( 0, 0 ) 호출. 엔진 내부적으로 착지 시, Pawn→Physics 를 PHYS_Walking 으로 변경해주므로 결과적으로 eventLanded 가 호출된다.
  8. GSM_MantleOverCoverBase::Landed( … )
    • Super.Landed( … ) 호출
    • 0.025 초 후 AllowTransitionToAMove 함수 호출되도록 타이머 설정
  9. GSM_BaseRaviableFall::Landed( … )
    • PlayLand 함수 호출

PushOutOfCover

역시 SpecialMove 는 아니고 커버 상태에서 벗어나는 과정을 기록해 둔다.

분석 포인트

  • 키 입력에 따른 발동
    // XTPlayerController.uc
    simulated state PlayerTakingCover extends PlayerWalking
    {
        ...
        simulated function UpdatePlayerPosture( ... )
        {
            ...
            BreakFromCoverHoldTimer += DeltaTime;         // 일정시간동안 뒤로 땡기면
     
            if ( BreakFromCoverHoldTimer >= BreakFromCoverHoldTime )
            {
                DoSpecialMove( SM_PushOutOfCover, TRUE ); // 커버 벗어나는 SM 발동
            }
            ...
        }
        ...
    }

PushObject

개요

GearsOfWar2 에서 다이나믹오브젝트를 미는 행위

분석 포인트

  • 푸쉬 신호
    // GearPC.uc
    reliable server function ServerKeepPushing()
    {
        추후 참조
    }
  • 버튼 눌림 & 물체 움직임 처리
    // GSM_PushObject
    simulated function ButtonPressed()
    {
        물체를 움직이는 처리
    }

무브먼트 전이

어떤 무브먼트 중, 혹은 끝날 시점에 다른 무브먼트로 자연스럽게 전이되는 과정을 분석한다.

Mantle to RoadieRun

엄폐물을 타넘고 바로 로디런으로 들어가는 과정을 살펴보자.

분석 포인트

  • 조건체크
    // GSM_MantleOverCoverBase.uc
    simulated function CheckTransitionToAMove()
    {
        ...
        if ( PCOwner.CanDoSpecialMove(SM_RoadieRun) )
        {
            PawnOwner.CanDoSpecialMove(TRUE);
            bAllowTransitionToAMove = FALSE;
        }
        ...
    }

부가기능

Mirroring

분석 포인트

  • 변수
    // SkeletalMesh.uc
    var() editfixedsize array<BoneMirrorInfo>   SkelMirrorTable;      // 모든 Bone 각각의 Mirror 방식에 대한 정보를 담은 배열
    var   EAxis                                 SkelMirrorAxis;       // 값에 음수값을 취할 축 (기본적으로 X축)
    var   EAxis                                 SkelMirrorFlipAxis;   // flip 할 축 (기본적으로 Z축)
  • TM 의 Mirror 함수
    // UnMatrix.h
     
    /*
     * Transform을 특정 평면을 기준으로 미러링 시키고, 축 자체를 flip 해주는 함수
     */
    inline void FMatrix::Mirror( BYTE MirrorAxis, BYTE FlipAxis )
    {
        // 특정 평면으로부터 값을 미러링
        if ( MirrorAxis == AXIS_X )
        {
            M[0][0] *= -1.f;
            M[1][0] *= -1.f;
            M[2][0] *= -1.f;
     
            M[3][0] *= -1.f;
        }
        else if ( MirrorAxis == AXIS_Y )
        {
            M[0][1] *= -1.f;
            M[1][1] *= -1.f;
            M[2][1] *= -1.f;
     
            M[3][1] *= -1.f;
        }
        else if ( MirrorAxis == AXIS_Z )
        {
            M[0][2] *= -1.f;
            M[1][2] *= -1.f;
            M[2][2] *= -1.f;
     
            M[3][2] *= -1.f;
        }
     
        // 축 자체를 flip
        if ( FlipAxis == AXIS_X )
        {
            M[0][0] *= -1.f;
            M[0][1] *= -1.f;
            M[0][2] *= -1.f;
        }
        else if ( FlipAxis == AXIS_Y )
        {
            M[1][0] *= -1.f;
            M[1][1] *= -1.f;
            M[1][2] *= -1.f;
        }
        else if ( FlipAxis == AXIS_Z )
        {
            M[2][0] *= -1.f;
            M[2][1] *= -1.f;
            M[2][2] *= -1.f;
        }
    }
  • MirroredAtom 을 구하는 함수
    // UnAnimTree.cpp
    void UAnimNodeBlendBase::GetMirroredBoneAtoms( ... )
    {
        // 모든 TM 을 돌면서
        ...
        // 에디터 상에서 flip 할 축을 별도로 지정하지 않았다면 기본 flip 축 (Z축) 을 취한다.
        BYTE FlipAxis = SkelMesh->SkelMirrorFlipAxis;
        if ( SkelMesh->SkelMirrorTable(BoneIdex).BoneFlipAxis != AXIS_None )
        {
            FlipAxis = SkelMesh->SkelMirrorTable(BoneIndex).BoneFlipAxis;
        }
     
        // RootBone 이고 RootMotion 이 있다면, 적절하게 Mirroring
        if ( BoneIndex == 0 )
        {
            if ( bHasRootMotion && (!RootMotionDelta.Translation.IsZero() || Square(RootMotionDelta.Rotation.W) < 1.f - DELTA * DELTA) )
            {
                FQuatRotationTranslationMatrix   RootTM( RootMotionDelta.Rotation, RootMotionDelta.Translation );
                RootTM.Mirror( SkelMesh->SkelMirrorAxis, FlipAxis );
                RootMotionDelta.Translation = RootTM.GetOrigin();
                RootMotionDelta.Rotation = RootTM.Rotator().Quaternion();
            }
        }
     
        // MirrorTable 에 Source 로 지정한 Index 를 취하고
        const INT SourceIndex = SkelMesh->SkelMirrorTable(BoneIndex).SourceIndex;
     
        // 그것이 자신의 Index 와 같다면 스스로 Mirroring
        if ( BoneIndex == SourceIndex )
        {
            BoneTM(BoneIndex).Mirror( SkelMesh->SkelMirrorAxis, FlipAxis );
            BoneMirrored( BoneIndex ) = TRUE;
        }
        // Source Bone 과 서로 Mirroring
        else
        {
            BYTE SourceFlipAxis = SkelMesh->SkelMirrorFlipAxis;
            if ( SkelMesh->SkelMirrorTable(SourceIndex).BoneFlipAxis != AXIS_None )
            {
                SourceFlipAxis = SkelMesh->SkelMirrorTable(SourceIndex).BoneFlipAxis;
            }
     
            FBoneTransform  BoneTransform0 = BoneTM(BoneIndex);
            FBoneTransform  BoneTransform1 = BoneTM(SourceIndex);
            BoneTransfrom0.Mirror( SkelMesh->SkelMirrorAxis, FourceFlipAxis );
            BoneTransfrom1.Mirror( SkelMesh->SkelMirrorAxis, FlipAxis );
            BoneTM(BoneIndex)         = BoneTransform1;
            BoneTM(SourceIndex)       = BoneTransform0;
            BoneMirrored(BoneIndex)   = TRUE;
            BoneMirrored(SourceIndex) = TRUE;
        }
        ...
    }
  • Master 와 TransitionBlend 노드
    //-----------------------------------------
    // GearAnim_Mirror_TransitionBlend.uc
    class GearAnim_Mirror_TransitionBlend extends AnimNodeBlend
     
    var const transient Array<AnimNodeSequence>    SeqNodes;    // transition 에 사용할 에니메이션 시퀀스
     
    ...
     
    cpptext
    {
        // 초기화. Transition 에 사용할 에니메이션 시퀀스를 캐싱해 놓는다.
        virtual void  InitAnim( USkeletalMeshComponent* MeshComp, UAnimNodeBlendBase* Parent );
     
        // BlendInTime 으로 캐싱된 에니메이션을 재생한다. (Transition Animation)
        void          StartTransition( FLOAT BlendInTime );
    }
     
     
    //-----------------------------------------
    // GearAnim_Mirror_Master.uc
    class GearAnim_Mirror_Master extends AnimNodeBlendBase;
     
    ...
     
    // AnimTree 에 있는 모든 TransitionBlend 노드를 캐싱해두는 배열. Mirror 시 가장 적합한 Transition 노드를 찾는데 사용된다.
    var   Array<GearAnim_Mirror_TransitionBlend>  TransitionNodes;
     
    ...
     
    // Transition 을 수행할 BodyStance 노드. 이것을 통해 Transition 을 수행하기도 한다.
    var() Array<GearAnim_Slot>                    BodyStanceNodes;
     
    // Transition 을 수행하는데 방해가 될만한 SkelControl 을 미리 캐싱해두고, Transition 전에 off 시키기 위한 SkelControl 배열
    var   Array<SkelControlBase>                  Drive_SkelControls;
     
    ...
     
    // 상기 Drive_SkelControls 에 캐싱해 둔 SkelControl 을 모두 off 시킨다. off 된 SkelControl 은 Transition 이 끝난 시점에 다시 원복된다.
    native final function ForceDrivenNodesOff();

상기 코드에 나와있는 GearAnim_Mirror_TransitionBlend 에서 실질적인 Transition 이 수행되며 GearAnim_Mirror_Master 에서는 TransitionBlend 노드를 관리한다.