UE3를 활용하여 자신의 프로젝트에 맞는 캐릭터를 구현하기에 앞서 UE3에 이미 구현되어 있는 캐릭터 작동 방식을 분석하기로 한다. 그리하여 캐릭터 구현에 필요한 기능이 중복구현됨을 막고 효율적으로 UE3에 통합될 수 있도록 한다.
여기서 살펴볼 내용은 Core기능에 해당하는 AnimTree, AnimSet, Morph 등의 기능이 아니라, 로직에 해당하는 Pawn, PlayerController, PlayerReplicationInfo 등의 오버뷰와 이들이 엔진 내 어떤 포지션에 해당하는지를 중점으로 살펴본다.
UE3에서 캐릭터를 구현하는데 필요한 요소들을 살펴본다.
// APawn::performPhysics 로부터 호출 void APawn::Crouch(INT bClientSimulation) { ... } void APawn::UnCrouch(INT bClientSimulation) { ... }
// 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; // 사람이 조종하지 않는 녀석에 한해서만 적용 } ... }
// 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 ... }
// UnPhysic.cpp void APawn::CalcVelocity( ... ) { ... if ( bBuoyant ) { Velocity.Z += GetGravityZ() * DeltaTime * ( 1.f - Buoyancy ); // Buoyancy 가 0 이면 중력을 그대로 적용한다. } ... }
// Pawn.uc function JumpOutOfWater( vector jumpDir ) { ... velocity.Z = OutofWaterZ; // set here so physics uses this for remainder of tick ... }
// 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 오브젝트가 있는지 (지형 포함) 체크한다. } ... }
// UnPhysic.cpp FLOAT APawn::MaxSpeedModifier() { ... if ( bIsCrouched ) { Result *= CrouchedPct; } else if ( bIsWalking ) { Result *= WalkingPct; // 바로 위에서 Pawn 의 무브먼트 속도를 Result 에 누적하여 구하고 그것을 Walking 상태여부에 따라 곱하여 현재 무브먼트 속도를 구한다. } ... }
// UnPawn.cpp FVector APawn::GetPawnViewLocation() { return Location + FVector( 0.f, 0.f, 1.f ) * BaseEyeHeight; }
// 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; // 더 이상 피해는 없음 } }
// UTPawn.uc event HeadVolumeChange( PhysicsVolume newHeadVolume ) { ... else if ( ... ) { BreathTime = UnderWaterTime; // 입수 시 숨쉬기 타이머 발동 } } defaultproperties { ... UnderWaterTime=+00020.000000 // 20초 ... }
// Pawn.uc function PlayHit( ... ) { ... LastPainTime = WorldInfo.TimeSeconds; }
// UnPhysic.cpp void APawn::CalcVelocity( ... ) { ... if ( bForceRMVelocity ) { Velocity = RMVelocity; return; } ... }
샘플로 제공되는 언리얼토너먼트3 (이하 UTGame) 의 캐릭터를 구현하는데 활용된 여러 요소들을 추가적으로 살펴본다.
엔진에서 목표지점destination까지 정확하게 이동시켜주는 로직이 존재한다.
대략적인 방법은 이렇다.
관련 변수/함수는 아래와 같다.
// 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 을 기반으로 한 분석내용이다.
// 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; ... }
Pawn 의 현재 Rotation 을 기준으로 좌우 일정 반경을 회전할 동안 발이 땅에 접지한 상태로 있는 기능을 지원한다. UnrealEngine3 에서는 이 기능을 Turn-In-Place 라고 지칭한다. 이를 수행하는 레이어는 UDKPawn 이다.
// 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; } ... }