클라이언트 사이드에서 이동 후, 서버에 알리는 과정에 대한 요약
GearsOfWar2 의 마커스의 무브먼트를 분석한 내용이다.
XBox360 패드의 왼쪽트리거, PC에서는 마우스 우클릭을 하면 조준모드가 된다. 이때 카메라 FOV가 바뀌고, 캐릭터의 Stance가 변경되고, HUD에 크로스헤어가 표시되는 등 일련의 동작들이 이루어진다. 여기서는 (UnrealEngine3로 개발된) GoW2 소스 내에서 조준 트리거가 발동되고 이어지는 일련의 과정들을 분석해본다.
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; ... } ... }
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 를 처리하는데 필요한 추가 플래그
// 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 에니메이션이 끝날 때 호출되는 함수
// GearPawn.uc simulated event DoSpecialMove(ESpecialMove NewMove, optional bool bForceMove, optional GearPawn InInteractionPawn, optional INT InSpecialMoveFlags)
// 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
// 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 을 찾는다. } ... }
// 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 뒤에 체인될 수 있다. }
// GearPlayerInput.uc function PreProcessInput(float DeltaTime) { ... if ( bIsDoingASpecialMove ) Pawn.SpecialMoves[Pawn.SpecialMove].PreProcessInput(Self); ... }
// GearPlayerInput_Base.uc function Actor.EDoubleClickDir CheckForDoubleClickMove( float DeltaTime ) { ... if ( bIsDoingASpecialMove ) Pawn.SpecialMoves[Pawn.SpecialMove].PreDoubleClickCheck(Self); ... }
// GearPlayerInput.uc function bool FilterButtonInput(Name ButtonName, bool bPressed, int ButtonIdx) { ... if ( MyGearPawn.SpecialMoves[MyGearPawn.SpecialMove].ButtonPress(ButtonName) ) return true; ... }
// GearGame.cpp void AGearPawn::TickSpecial( FLOAT DeltaSeconds ) { ... if ( SpecialMove != SM_None && SpecialMoves( SpecialMove ) != NULL ) { SpecialMoves( specialMove )->TickSpecialMove( DeltaSeconds ); } ... }
// 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) ... }
// GearPawn.uc simulated function Destroyed() { if ( SpecialMove != SM_None && SpecialMoves[SpecialMove] != None ) EndSpecialMove(); }
// GearPawn.uc simulated event ReplicatedEvent( name VarName ) { ... switch ( VarName ) { case 'ReplicatedSpecialMoveStruct' : DoSpecialMoveFromStruct(ReplicatedSpecialMoveStruct, TRUE); break; } ... }
// GearPawn.uc simulated function Tick(float DeltaTime) { ... if ( SpecialMove != SM_None && SpecialMove[SpecialMove] != None ) SpecialMoves[SpecialMove].Tick(DeltaTime); ... }
// 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); } ... }
// 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; } ... }
// 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; } ... }
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); }
해석하자면 이렇다.
모든 로디런 초기에 이 혜택이 부여되는 것은 아니고 Mantle 과 Pickup 후에 발동되는 로디런만 이 혜택이 부여되더라.
특정 방향으로 빠르게 회피하는 행위
// GearPlayerInput.uc function HandleActionButton( ... ) { if ( bPressed ) { ... } else { ... TryToEvade( bDblClickMove ? CurrentDoubleClickDir : DCLICK_None ); ... } }
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 } };
// 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() ) // 엄폐 시도 { } ... } ... }
// GearPawn.uc simulated event ReplicatedEvent( name VarName ) { ... case 'AcquiredCoverInfo' : ... DoSpecialMove(SM_Run2MidCov, TRUE); ... DoSpecialMove(SM_Run2StdCov, TRUE); ... break; ... }
function TransitionFromRoadieRunToCover( CovPosInfo FoundCovInfo, optional bool bNoCameraAutoAlign ), GearPC.uc
function bool TryToRunToCover(optional bool bSkipSpecialMoveCheck, optional float CheckScale, optional EDoubleClickDir DblClickDirToCheck=DCLICK_None, optional bool bPreventSideEntry), GearPC.uc
CoverAcquired(CoverInfo, bNoCameraAutoAlign);
MyGearPawn.CoverAcquired(CovInfo);
DoSpecialMove(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' ); ... }
커버의 양쪽 엣지에서 앞으로 빠르게 치고 나아가는 행위
// GearPC.uc simulated state PlayerTakingCover extends PlayerWalking { exec funciton bool TryASpecialMove( bool bRunActions ) { ... else if ( bDoMonkeyMoveChecks && CanDoSpecialMove( SM_CoverSlip ) ) { DoSpecialMove( SM_CoverSlip ); return TRUE; } ... } }
낮은 커버에 몸을 기댔을 경우, 커버를 올라타고 앞으로 뛰어넘는 행위
GSM_BaseVariableFall // 일반적인 Jump 에 대한 정의. Jump, Fall, Land 의 세가지 단계 ↑ GSM_MantleOverCoverBase // Cover (엄폐물) 를 타 넘는 형식의 특별한 Jump 를 정의 ↑ GSM_MidLvlCoverBase // 낮은 (MidLevel) 엄폐물만 넘도록 제한
// GSM_BaseVariableFall.uc enum EMoveType { EMT_Jump, // 점프. 중력에 의한 낙하가 아닌 에니메이션 재생만 한다. EMT_Fall, // 낙하. 점프 에니메이션이 끝나면 중력에 의해 낙하 속도가 계산된다. EMT_Land, // 착지. 낙하 중 낙하방향에 충돌이 예상되면 착지 에니메이션이 재생된다. };
// 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 ); }
// 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() ); } } ... }
역시 SpecialMove 는 아니고 커버 상태에서 벗어나는 과정을 기록해 둔다.
// XTPlayerController.uc simulated state PlayerTakingCover extends PlayerWalking { ... simulated function UpdatePlayerPosture( ... ) { ... BreakFromCoverHoldTimer += DeltaTime; // 일정시간동안 뒤로 땡기면 if ( BreakFromCoverHoldTimer >= BreakFromCoverHoldTime ) { DoSpecialMove( SM_PushOutOfCover, TRUE ); // 커버 벗어나는 SM 발동 } ... } ... }
GearsOfWar2 에서 다이나믹오브젝트를 미는 행위
// GearPC.uc reliable server function ServerKeepPushing() { 추후 참조 }
// GSM_PushObject simulated function ButtonPressed() { 물체를 움직이는 처리 }
어떤 무브먼트 중, 혹은 끝날 시점에 다른 무브먼트로 자연스럽게 전이되는 과정을 분석한다.
엄폐물을 타넘고 바로 로디런으로 들어가는 과정을 살펴보자.
// GSM_MantleOverCoverBase.uc simulated function CheckTransitionToAMove() { ... if ( PCOwner.CanDoSpecialMove(SM_RoadieRun) ) { PawnOwner.CanDoSpecialMove(TRUE); bAllowTransitionToAMove = FALSE; } ... }
// SkeletalMesh.uc var() editfixedsize array<BoneMirrorInfo> SkelMirrorTable; // 모든 Bone 각각의 Mirror 방식에 대한 정보를 담은 배열 var EAxis SkelMirrorAxis; // 값에 음수값을 취할 축 (기본적으로 X축) var EAxis SkelMirrorFlipAxis; // flip 할 축 (기본적으로 Z축)
// 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; } }
// 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; } ... }
//----------------------------------------- // 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 노드를 관리한다.