언리얼3 캐릭터 구조
목차
캐릭터
UE3를 활용하여 자신의 프로젝트에 맞는 캐릭터를 구현하기에 앞서 UE3에 이미 구현되어 있는 캐릭터 작동 방식을 분석하기로 한다. 그리하여 캐릭터 구현에 필요한 기능이 중복구현됨을 막고 효율적으로 UE3에 통합될 수 있도록 한다.
여기서 살펴볼 내용은 Core기능에 해당하는 AnimTree, AnimSet, Morph 등의 기능이 아니라, 로직에 해당하는 Pawn, PlayerController, PlayerReplicationInfo 등의 오버뷰와 이들이 엔진 내 어떤 포지션에 해당하는지를 중점으로 살펴본다.
캐릭터 Bone 구조
분석 요소
UE3에서 캐릭터를 구현하는데 필요한 요소들을 살펴본다.
Pawn
- 특징
- Actor→Pawn 상속
- player 나 AI 가 컨트롤하기 위한 Base Actor 클래스에 해당한다.
- mesh, collision, physics 등의 기능이 있고, 데미지를 주며 소리를 내고 무기나 다른 소지품을 들고 다니며, 월드상 다른 Pawn 과의 물리적 인터렉션 등을 담당한다.
- 기능
- 캐릭터가 돌아 다닐 수 있는 floor 에 대한 성격을 정의한다.
- Pawn 이 서있는 지면의 Normal ( Floor, PHYS_Spider, PHYS_Walking 에서만 쓰인다. )
- 올라갈 수 있는 floor 높이를 정의. ( MaxStepHeight, MaxJumpHeight )
- 내려갈 수 있는 floor 높이를 정의. ( LedgeCheckThreshold )
- 이동 가능한 floor 경사면 값 지정. ( WalkableFloorZ, 이는 floor 의 normal 벡터와 UpVector 를 내적한 스칼라 값 이며 Pawn은 기본적으로 0.7 (~= 45 degree) 로 초기화 되어 있다. )
- 상기 3가지 사항은 캐릭터 이동 및 PathFinding 등에 활용된다.
- 모든 Pawn을 링크드리스트로 관리. ( NextPawn, 월드 상 모든 Pawn 순회에 용이하다. )
- Crouch 관련 정보 정의 가능
- 웅크릴 수 있다. ( bCanCrouch )
- 웅크리길 원한다. ( bWantsToCrouch )
- 웅크린 상태이다. ( bIsCrouched )
- 다시 일어서고 싶다. ( bTryToUncrouch )
- Crouch 시, 실린더 정보 ( CrouchHeight, CrouchRadius )
- 웅크리기 & 일어서기 이벤트 함수 제공.
// APawn::performPhysics 로부터 호출 void APawn::Crouch(INT bClientSimulation) { ... } void APawn::UnCrouch(INT bClientSimulation) { ... }
- 이동 목적지에 다다르면 smooth하게 속도를 줄이면서 멈춘다. ( bReducedSpeed )
- 여러 행위들에 대한 플래그
- 점프기능이 있다. ( bJumpCapable )
- 점프할 수 있다. ( bCanJump )
- 데미지 입을 수 있다. ( bCanBeDamaged )
- 웅크릴 수 있다. ( bCanCrouch )
- 날 수 있다. ( bCanFly )
- 수영할 수 있다. ( bCanSwim )
- 텔레포트할 수 있다. ( bCanTeleport )
- 걸을 수 있다. ( bCanWalk )
- 사다리에 오를 수 있다. ( bCanClimbLadders )
- 횡이동 할 수 있다. ( bCanStrafe )
- 이 밖에 다양한 플래그 들 ( bAvoidLedges, bStopAtLedges, bAllowLedgeOverhang, bSimulatedGravity, bIgnoreForces, bCanWalkOffLedges, bCanBeBaseForPawns, bSimGravityDisabled, bDirectHitWall, bPushesRigidBodies, bForceFloorCheck, bForceKeepAnchor )
- 곧 Pawn 에서 제거될 것 같은? 플래그 들 ( bCanMantle, bCanClimbUp, bCanClimbCeilings, bCanSwatTurn, bCanLeap, bCanCoverSlip )
- AI 관련 변수
- 성별 ( bIsFemale )
- 아이템을 집을 수 있다. ( bCanPickupInventory )
- AI가 날 무시하게 할 수 있다. ( bAmbientCreature )
- 소리를 들을 수 있다. ( bLOSHearing )
- 벽을 통해 들려오는 숨죽인 소리를 들을 수 있다. ( bMuffledHearing )
- 게임 시작 시 controller 에 의해 소유되지 않도록 한다. vehicle 들은 게임 시작 시 controller 에 의해 선점되면 안되겠지.. ( bDontPossess )
- 난 움직일 수 없다. ( bStationary )
- 무기 사용할 수 없다. ( bNoWeaponFiring )
- 뭔가를 사용할 수 있다. ( bCanUse )
- 이 밖에 다양한 플래그 들 ( bModifyReachSpecCost, bModifyNavPointDest, bPathfindingsAsVehicle, )
- Path Finding 관련
- 내가 길을 찾는 방법 ( PathSearchType )
- 기타 관련 변수 들 ( PathConstraintList, PathGoalList )
- 들을 수 있는 거리 ( HearingThreshold )
- 소리를 들을 수 있는 각성도 ( Alertness, -1 ~ 1 사이의 값으로써 높을 수록 소리를 더 잘 들을 수 있다. )
- 시야 거리 ( SightRadius )
- 시야 각 ( PeripheralVision, degree 의 cosine 값, default 로 -0.75 ~= 140 도 )
- 물리적 업데이트할 모니터링 시간 ( AvgPhysicsTime, AI 가 목적지로 가기 위해 업데이트할 시간? 구불구불하게 가거나 똑바로 가게 하는 등의 처리를 위한 변수 )
- 이동 관련 속성
- 희망 무브먼트 속도 ( DesiredSpeed & MaxDesiredSpeed, GroundSpeed 에 곱해지므로 실질적으로 Pawn 의 전체적 무브먼트 스피드라고 보아도 무방 )
// Pawn.uc defaultproperties { ... DesiredSpeed=+00001.00000 ... } // UnController.cpp void AController::MoveTo( ... ) { ... Pawn->DesiredSpeed = Pawn->MaxDesiredSpeed; ... } void AController::MoveToward( ... ) { ... Pawn->DesiredSpeed = Pawn->MaxDesiredSpeed; ... } // UnPhysic.cpp FLOAT APawn::MaxSpeedModifier() { ... if ( !IsHumanControlled() ) { Result *= DesiredSpeed; // 사람이 조종하지 않는 녀석에 한해서만 적용 } ... }
- 가장 가까운 path ( Anchor )
- nav mesh 인덱싱 ( AnchorItem, 헌데 사용되지는 않음 )
- 최근에 도달한 가까운 path ( LastAnchor )
- 마지막 path finding 실패 시기 ( FindAnchorFailedTime, FindPath() 함수 시도가 실패한 마지막 시간 )
- 마지막 유효 path 발견 시기 ( LastValidAnchorTime )
- 도착점 offset ( DestinationOffset )
- 루트상 다음 지점의 반지름 ( NextPathRadius )
- 구불구불 이동
- 방향 ( SerpentineDir )
- 거리 ( SerpentineDist )
- 시간 ( SerpentineTime, 구불구불 이동 시 횡이동 시도하기 전까지 직진할 시간 )
- 물리?
- Pawn 의 질량 ( Mass )
// Pawn.uc function CrushedBy( Pawn OtherPawn ) { TakeDamage( ( 1 - OtherPawn.Velocity.Z / 400 ) * OtherPawn.Mass / Mass, // 데미지, 위에서 내리누를 때 상대방과의 질량에 대비하여 데미지를 가감하는 용도. OtherPawn.Controller, // 유발자 Controller Location, // HitLocation vect( 0, 0, 0 ), // Momentum class'DmgType_Crushed' ); // 데미지 타입 } event TakeDamage( ... ) { ... momentum = momentum / Mass; // 데미지가 가해질 때 전해진 충격량으로부터 움직일 속도를 구함. F=ma -> a=F/m ... }
- 수영할 때 적용할 물 부양성 ( Buoyancy, 1=자연스러운 부양성 0=no부양성 )
// UnPhysic.cpp void APawn::CalcVelocity( ... ) { ... if ( bBuoyant ) { Velocity.Z += GetGravityZ() * DeltaTime * ( 1.f - Buoyancy ); // Buoyancy 가 0 이면 중력을 그대로 적용한다. } ... }
- 밀리어택 최대거리 ( MeleeRange, 일반적인 밀리어택이 아니라 이동하는 도중 목적지까지의 거리를 가늠하는데 사용하는 것 같음 )
- 일반
- 스폰 시간 ( SpawnTime, 스폰 후 일정시간동안 데미지 감소따위를 하는 데 사용. 관련변수 UTGame.SpawnProtectionTime )
- view pitching 제한 ( MaxPitchLimit )
- 무브먼트
- controller 없이도 physics 돌려라~ ( bRunPhysicsWithNoController, acceleration 이 아닌 velocity 에 의해서만 움직이게 되겠다. )
- 풀 악셀 ( bForceMaxAccel, 기존 acceleration 무시하고 풀악셀로 최대 velocity 를 이끌어 낸다. )
- 최대 지형 이동 속도 ( GroundSpeed )
- 최대 수영 이동 속도 ( WaterSpeed )
- 최대 활강 이동 속도 ( AirSpeed )
- 최대 등반 이동 속도 ( LadderSpeed )
- 가속 비율 ( AccelRate, 이것이 곱해져 acceleration 을 구한다. )
- 점프 속도 ( JumpZ, 수직 up 방향 속도 )
- 수중 이탈 속도 ( OutofWaterZ, 점프로 물 밖으로 이탈할 때 z up 방향 속도. 물 근처 난간위로 올라가는 것을 보장하기 위해 설정하는 값인듯 )
// Pawn.uc function JumpOutOfWater( vector jumpDir ) { ... velocity.Z = OutofWaterZ; // set here so physics uses this for remainder of tick ... }
- 수중 이탈 가능 높이 ( MaxOutOfWaterStepHeight, 수영하며 수중이탈 가능한올라갈 수 있는 높이 )
- 낙하 최대 가속도 제한할까? ( bLimitFallAccel )
- 공중에서의 컨트롤 시간 factor ( AirControl, acceleration 을 이 시간만큼 적용한 후 테스트하는 용도 )
// UnPhysic.cpp void APawn::physFalling( FLOAT deltaTime, INT Iterations ) { ... if ( !bDoRootMotion && TickAirControl > 0.05f ) { // 현재 velocity 에 TickAirControl 시간만큼 경과 후 delta velocity 까지 더한 후 이동거리를 체크한다. FVector TestWalk = ( TickAirControl * AccelRate * Acceleration.SafeNormal() + Velocity ) * deltaTime; TestWalk.Z = 0.f; ... // 이후는 현재 Location 으로부터 TestWalk 만큼 이동한 곳에 특정 world 오브젝트가 있는지 (지형 포함) 체크한다. } ... }
- 걷기&웅크리기 속도 퍼센티지 ( WalkingPct & CrouchedPct, 기본 이동 속도에 곱하여 걷기속도 및 웅크리기속도를 구하는 방식에 사용 )
// UnPhysic.cpp FLOAT APawn::MaxSpeedModifier() { ... if ( bIsCrouched ) { Result *= CrouchedPct; } else if ( bIsWalking ) { Result *= WalkingPct; // 바로 위에서 Pawn 의 무브먼트 속도를 Result 에 누적하여 구하고 그것을 Walking 상태여부에 따라 곱하여 현재 무브먼트 속도를 구한다. } ... }
- 데미지 없이 낙하 가능한 속도 ( MaxFallSpeed, velocity 와 비교된다. )
- AI들은 이보다 적은 낙하속도가 가능한 길을 택할 것이다. ( AIMaxFallSpeedFactor, 바로 위 변수와 곱하여 AI를 위한 낙하 속도를 구함 MaxFallSpeed * AIMaxFallSpeedFactor )
- Camera 관련
- Pawn 카메라 높이 ( BaseEyeHeight )
// UnPawn.cpp FVector APawn::GetPawnViewLocation() { return Location + FVector( 0.f, 0.f, 1.f ) * BaseEyeHeight; }
- 계산된/조절된 Pawn 카메라 높이 ( EyeHeight )
- 숨쉬기/HP
- 숨을 쉬기 위한 머리 ( HeadVolume )
- HP ( Health )
- 최대 HP ( HealthMax )
- HP 를 모든 클라에게 복제할까? ( bReplicateHealthToAll )
- 숨쉬기 타이머 ( BreathTime, 물에 빠졌을 때 일정 시간마다 데미지를 주기 위한 주기 )
// UnLevTic.cpp void APawn::TickSpecial( FLOAT DeltaSeconds ) { // Authority 이고 BreathTime 중이라면 if ( Role == ROLE_Authority && BreathTime > 0.f ) { BreathTime -= DeltaSeconds; if ( BreathTime < 0.001f ) { // 때가 됐다면 BreathTimer 호출 (바로 아래) BreathTime = 0.0f; eventBreathTimer(); } } ... } // Pawn.uc event BreathTimer() { if ( HeadVolume.bWaterVolume ) { if ( Health < 0 || WorldInfo.NetMode == NM_Client || DrivenVehicle != None ) return; // 죽었거나 클라이언트거나 무엇인가를 타고있다면 무시 TakeDrowningDamage(); // 익사 피해 if ( Health > 0 ) BreathTime = 2.0; // 2초 후 다시 BreathTimer 호출 } else { BreathTime = 0.0; // 더 이상 피해는 없음 } }
- 얼마만큼 숨을 참을 수 있는가? ( UnderWaterTime, 최초 입수 시 BreathTime 에 대입되는 값. 이 시간이 지나면 위의 BreathTimer 함수에 나와있는대로 2초마다 데미지 )
// UTPawn.uc event HeadVolumeChange( PhysicsVolume newHeadVolume ) { ... else if ( ... ) { BreathTime = UnderWaterTime; // 입수 시 숨쉬기 타이머 발동 } } defaultproperties { ... UnderWaterTime=+00020.000000 // 20초 ... }
- 마지막으로 피해를 입은 시간 ( LastPainTime, BOT 의 경우 피격받고 일정 시간동안 Aim 을 불안정하게 하기위한 용도로써 활용된다. )
// Pawn.uc function PlayHit( ... ) { ... LastPainTime = WorldInfo.TimeSeconds; }
- 루트모션
- 루트모션 속도 ( RMVelocity, 클라이언트에서 루트모션을 재현하기 위한 용도의 변수. 또는 Controller.bPreciseDestination정확한 이동 기능이 활성화일 때 사용되기도 함. )
- 강제 루트모션 속도 사용 ( bForceRMVelocity, 이 값이 true 면 APawn::CalcVelocity() 에서 RMVelocity 값을 직접적으로 사용 )
// UnPhysic.cpp void APawn::CalcVelocity( ... ) { ... if ( bForceRMVelocity ) { Velocity = RMVelocity; return; } ... }
- 강제 일반적인 속도계산 ( bForceRegularVelocity, 이 값이 true 면 APawn::CalcVelocity() 에서 RMVelocity 값을 절대로 사용하지 않는다. )
- 사운드와 노이즈
- 여러 변수들 ( noise1spot, noise1time, noise1other, noise1loudness, noise2spot, noise2time, noise2other, noise2loudness )
- 사운드 음 꺾기 ( SoundDampening )
- 데미지
- 데미지 증가 ( DamageScaling ) *
Controller
PlayerController
PlayerReplicationInfo
UTGame 관련
샘플로 제공되는 언리얼토너먼트3 (이하 UTGame) 의 캐릭터를 구현하는데 활용된 여러 요소들을 추가적으로 살펴본다.
UTFamilyInfo
UTPlayerReplicationInfo
이동
정확한 이동
엔진에서 목표지점destination까지 정확하게 이동시켜주는 로직이 존재한다.
대략적인 방법은 이렇다.
- 정확히 도달해야 하는 목표지점을 설정한다. (by controller)
- 매 프레임마다 도착했는지 여부를 검사하고 아직 도달하지 못했다면 적절한 velocity 를 계산한다.
- 목표지점destination에서의 허용 Offset 안에 도달하면 이벤트함수를 호출한다. ( Controller.ReachedPreciseDestination() )
관련 변수/함수는 아래와 같다.
- 변수
// Pawn.uc var float DestinationOffset; // 목표지점으로부터의 허용 Offset // Controller.uc var bool bPreciseDestination; // 목표지점에 맞는 velocity 를 강제할 것인지의 여부, 정확한 이동을 수행할 것인지 여부와 상통 var BasedPosition DestinationPosition; // 목표지점
- 함수
// UnPhysic.cpp void Pawn::CalcVelocity( ... ) { ... // RooMotion 일 경우를 제외하고 '정확한 이동' 처리를 수행한다. if ( !bDoRootMotionAccel && Controller && Controller->bPreciseDestination ) { FVector Dest = controller->GetDestinationPosition(); // Controller.DestinationPosition 을 Vector 로 형변환하여 리턴 if ( ReachedDestination( Location, Dest, NULL ) ) { Controller->bPreciseDestination = FALSE; // '정확한 이동'을 종료 Controller->eventReachedPreciseDestination(); // 종료 이벤트 호출 Velocity = FVector( 0.f ); Acceleration = FVector( 0.f ); } else if ( bForceMaxAccel ) { const FVector Dir = (Dest - Location).SafeNormal(); Acceleration = Dir * MaxAccel; Velocity = Dir * MaxSpeed; } else { Velocity = (Dest - Location) / DeltaTime; } ... } ... } // UnPawn.cpp UBOOL APawn::ReachedDestination( ... ) { ... return ReachThresholdTest( ... ); } UBOOL APawn::ReachThresholdTest( ... ) { ... FLOAT Threshold = ThresholdAdjust + CylinerComponent->CollisionRadius + DestinationOffset; // 도착으로 인정할 유효 반지름을 계산 ... if ( Dir.SizeSquared() > Threshold * Threshold ) return FALSE; ... // 적절하게 테스트하고 return TRUE; // 도착했다고 판정 }
상기 APawn::ReachThresholdTest 함수의 동입부에 도착지점으로부터의 허용 반지름을 계산하는 부분이 있는데, 이 값에 음수를 주어 좀 더 정확한 목표지점에 도달하게 할 수 있다.
무브먼트
언리얼엔진3에 기본적으로 제공되는 UTGame 을 기반으로 한 분석내용이다.
웅크리기 Crouch
코드플로우
- 발생
- DefaultInput.ini
- .Bindings=(Name=“C”,Command=“GBA_Duck”)
- .Bindings=(Name=“GBA_Duck”,Command=“Duck | onrelease UnDuck | Axis aUp Speed=-1.0 AbsoluteAxis=100”)
- simulated exec function Duck() (UTPlayerInput.uc)
- …
- bDuck = true;
- 루프1
- state PlayerWalking::ProcessMove(…) (PlayerController.uc) ← state PlayerWalking::ProcessMove(…) (UTPlayerController.uc) ← state PlayerWalking::PlayerMove(float DeltaTime) (PlayerController.uc) ← state PlayerWalking::PlayerMove(float DeltaTime) (UTPlayerController.uc) ← event PlyaerTick(float DeltaTime) (PlayerController.uc) ← event PlayerTick(float DeltaTime) (UTPlayerController.uc)
- …
- CheckJumpOrDuck();
- function CheckJumpOrDuck() (UTPlayerController.uc)
- 점프나 더블점프를 수행하는 함수를 호출
- Pawn.ShouldCrouch(bDuck != 0);
- funciton SouldCrouch( bool bCrouch ) (Pawn.uc)
- bWantsToCrouch = bCrouch;
- 루프2
- AActor::TickAuthoritative ← AActor::Tick ← APawn::Tick ← TickActors<FDeferredTickList::FGlobalActorIterator>
- …
- performPhysics 호출
- APawn::performPhysics(float DeltaSeconds) (UnPhysic.cpp)
- …
- 적절한 조건을 체크한 후 (bWantsToCrouch, bIsCrouched 따위) Crouch() 나 UnCrouch() 를 호출
- …
- APawn::Crouch(INT bClientSimulation) (UnPawn.cpp)
- Collision Cylinder 의 반지름과 높이를 CrouchRadius 와 CrouchHeight 로 변경
- Crouch Collision Cylinder 가 더 커졌다면 지면을 침범했는지 체크한다.
- eventStartCrouch( 높이차이 ) 호출
- simulated event StartCrouch(float HeightAdjust) (UTPawn.uc)
- Super.StartCrouch(HeightAdjust);
- CrouchMeshZOffset = HeightAdjust;
- simulated event StartCrouch(float HeightAdjust) (Pawn.uc)
- EyeHeight 조절
- OldZ 조절
- BaseEyeHeight 조절
특이사항
- 웅크리기에 사용될 충돌영역 정보인 CrouchRadius, CrouchHeight 를 세팅해야 한다.
- 이때 MaxStepHeight 가 CollisionHeight - CrouchHeight 보다는 커야 한다. MaxStepHeight 는 캐릭터가 자연스럽게 이동 가능한 최대 허용높이이다. 웅크리기가 발동되면 내부적으로 캐릭터 바운딩실린더 높이를 조절하는데 그 갭이 MaxStepHeight 보다 크면 캐릭터는 Falling 운동을 하게끔 처리된다. (엔진 내부적으로) 때문에 주의가 필요하다.
점프 Jump
코드 플로우
- 발동
// DefaultInput.ini .Bindings=(Name="GBA_Jump",Command="Jump | Axis aUp Speed=+1.0 AbsoluteAxis=100")
- 플래그 체크
// UTPlayerInput.uc exec function Jump() { ... Super.Jump(); ... } // PlayerInput.uc exec function Jump() { ... bPressedJump = true; }
- 지연 점프
// PlayerController.uc state PlayerWalking { ... function PlayerMove( float DeltaTime ) { ... // 지금 점프할 수 없는 상황이면 지연시킨다. if ( bPressedJump && Pawn.CannotJumpNow() ) { bSaveJump = true; bPressedJump = false; } ... } ... }
- 점프 시도여부 체크 (매 프레임)
// UTPlayerController.uc function CheckJumpOrDuck() { ... else if ( bPressedJump ) { Pawn.DoJump( bUpdateing ); } ... } // PlayerController.uc function CheckJumpOrDuck() { if ( bPressedJump && (Pawn != None) ) { Pawn.DoJump( bUpdating ); } }
- 실제 점프로직 처리
// UTPawn.uc function bool DoJump( bool bUpdating ) { ... if ( Physics ==PHYS_Spider ) Velocity = JumpZ * Floor; else if ( Physics == PHYS_Ladder ) Velocity.Z = 0 else if ( bIsWalking ) Velocity.Z = Default.JumpZ; else // 보통 이부분에 걸린다. Velocity.Z = JumpZ; ... }
부가기능
TurnInPlace
Pawn 의 현재 Rotation 을 기준으로 좌우 일정 반경을 회전할 동안 발이 땅에 접지한 상태로 있는 기능을 지원한다. UnrealEngine3 에서는 이 기능을 Turn-In-Place 라고 지칭한다. 이를 수행하는 레이어는 UDKPawn 이다.
분석 포인트
- AnimTree 노드 캐싱
// UTPawn.uc simulated event PostInitAnimTree(SkeletalMeshComponent SkelComp) { ... // 허리쪽 Bone 을 제어하는 컨트롤 캐싱 (RootRot 이란 이름을 가진 SkelControlSingleBone 컨트롤) RootRotControl = SkelControlSingleBone( mesh.FindSkelControl( 'RootRot' ) ); ... // 조준 노드 캐싱 AimNode = AnimNodeAimOffset( mesh.FindAnimNode( 'AimNode' ) ); ... }
- 업데이트 로직
// UDKPawn.cpp void AUDKPawn::TickSpecial( FLOAT DeltaSeconds ) { ... // 현재 Aim pitch 와 yaw 를 얻는다. INT PawnAimPitch; if ( Controller ) { // 컨트롤러의 Pitch를 얻는다. PawnAimPitch = Controller->Rotation.Pitch; } else { // Pawn 의 Pitch를 얻는다. PawnAimPitch = Rotation.Pitch; if ( PawnAimPitch == 0 ) { PawnAimPitch = RemoteViewPitch << 8; } } // Pawn 의 최종 조준 Pitch PawnAimPitch = UnwindRot( PawnAimPitch ); INT PawnAimYaw = UnwindRot( Rotation.Yaw ); // Pawn 의 최종 조준 Yaw (가 될 값) INT AimYaw = 0; if ( Physics == PHYS_Walking && Velocity.Size() < KINDA_SMALL_NUMBER ) { // PawnAimYaw 는 손이 향하는 방향, RootYaw 는 발이 향하는 방향이라 생각하면 이해가 쉽다. INT CurrentAimYaw = UnwindRot( PawnAimYaw - RootYaw ); INT RootRot = 0; if ( CurrentAimYaw > MaxYawAim ) { RootRot = ( CurrentAimYaw - MaxYawAim ); } else if ( CurrentAimYaw < -MaxYawAim ) { RootRot = ( CurrentAimYaw - (-MaxYawAim) ); } RootYaw += RootRot; RootYawSpeed += ( (FLOAT)RootRot ) / DeltaSeconds; // 최종 손과 발의 offset. 이것이 곧 Aim 노드에 적용될 Yaw AimYaw = UnwindRot( PawnAimYaw - RootYaw ); } else { RootYaw = Rotation.Yaw; RootYawSpeed = 0.f; AimYaw = 0; } // 좌우 90 도 회전을 각각 -1, 1 로 매핑시킨 값으로 변환 if ( !bNoWeaponFiring ) { CurrentSkelAim.X = Clamp<FLOAT>( ( (FLOAT)AimYaw / 16384.f ), -1.f, 1.f ); CurrentSkelAim.Y = Clamp<FLOAT>( ( (FLOAT)PawnAimPitch / 16384.f ), -1.f, 1.f ); } // 허리를 Aim 의 반대쪽으로 회전. 즉, 손의 방향이 Pawn Rotation 과 일치하고 발의 방향을 보정하는 방식 if ( RootRotControl ) { RootRotControl->BoneRotation.Yaw = -AimYaw; } // Aim 업데이트 if ( AimNode ) { AimNode->Aim = CurrentSkelAim; } ... }