프로젝트를 진행하면서, 기존 Character Movement Component를 적극적으로 사용해 개발해야 하다보니 뛰거나 나는데 어떤 변수가 어떻게 영향을 주는지, 어떻게 계산되는지 파악할 필요가 있었다. 따라서, 실질적으로 게임의 물리적인 움직임을 담당하는 함수인 PerformMovement()
를 대략적으로나마 읽어보기로 했다. 무브먼트 모드가 Walking
일때를 기준으로 함수 내부에서 어떤 일들이 일어나는지 따라가며 확인해볼 것이다!
// no movement if we can't move, or if currently doing physical simulation on UpdatedComponent
if (MovementMode == MOVE_None || UpdatedComponent->Mobility != EComponentMobility::Movable || UpdatedComponent->IsSimulatingPhysics())
{
if (!CharacterOwner->bClientUpdating && !CharacterOwner->bServerMoveIgnoreRootMotion)
{
// Consume root motion
if (CharacterOwner->IsPlayingRootMotion() && CharacterOwner->GetMesh())
{
TickCharacterPose(DeltaSeconds);
RootMotionParams.Clear();
}
if (CurrentRootMotion.HasActiveRootMotionSources())
{
CurrentRootMotion.Clear();
}
}
// Clear pending physics forces
ClearAccumulatedForces();
return;
}
먼저, Movement Mode가 None이거나 이동 상태가 Movable이 아닐 때, 물리 시뮬레이션중일 때는 움직일 수 없다.
void UCharacterMovementComponent::MaybeUpdateBasedMovement(float DeltaSeconds)
{
bDeferUpdateBasedMovement = false;
UPrimitiveComponent* MovementBase = CharacterOwner->GetMovementBase();
if (MovementBaseUtility::UseRelativeLocation(MovementBase))
{
// Need to see if anything we're on is simulating physics or has a parent that is.
if (!MovementBaseUtility::IsSimulatedBase(MovementBase))
{
bDeferUpdateBasedMovement = false;
UpdateBasedMovement(DeltaSeconds);
// If previously simulated, go back to using normal tick dependencies.
if (PostPhysicsTickFunction.IsTickFunctionEnabled())
{
PostPhysicsTickFunction.SetTickFunctionEnable(false);
MovementBaseUtility::AddTickDependency(PrimaryComponentTick, MovementBase);
}
}
else
{
// defer movement base update until after physics
bDeferUpdateBasedMovement = true;
// If previously not simulating, remove tick dependencies and use post physics tick function.
if (!PostPhysicsTickFunction.IsTickFunctionEnabled())
{
PostPhysicsTickFunction.SetTickFunctionEnable(true);
MovementBaseUtility::RemoveTickDependency(PrimaryComponentTick, MovementBase);
}
if (CharacterMovementCVars::BasedMovementMode == 2)
{
UpdateBasedMovement(DeltaSeconds);
}
}
}
else
{
// Remove any previous physics tick dependencies. SetBase() takes care of the other dependencies.
if (PostPhysicsTickFunction.IsTickFunctionEnabled())
{
PostPhysicsTickFunction.SetTickFunctionEnable(false);
}
}
}
MaybeUpdateBasedMovement()
함수는 캐릭터가 다른 오브젝트 위에 있을 때, 그 오브젝트의 움직임에 따라 캐릭터의 움직임을 업데이트해야 하는 상황을 처리하는 함수이다.
캐릭터가 서있는 MovementBase를 가져오고(지금 상황에선 그냥 floor), 그 오브젝트로부터 캐릭터의 위치가 상대적인지 절대적인지를 판단하여 적당한 처리를 해주는듯 하다. floor의 경우 물리 시뮬레이션을 사용하지 않기 때문에, BaseMovement를 업데이트 해주고 함수를 종료한다.
OldVelocity = Velocity;
OldLocation = UpdatedComponent->GetComponentLocation();
새로운 움직임을 만들어내기 전, 이전 Velocity와 Location을 저장해준다.
void UCharacterMovementComponent::ApplyAccumulatedForces(float DeltaSeconds)
{
const FVector GravityRelativePendingImpulseToApply = RotateWorldToGravity(PendingImpulseToApply);
const FVector GravityRelativePendingForceToApply = RotateWorldToGravity(PendingForceToApply);
if (GravityRelativePendingImpulseToApply.Z != 0.0 || GravityRelativePendingForceToApply.Z != 0.0)
{
// check to see if applied momentum is enough to overcome gravity
if ( IsMovingOnGround() && (GravityRelativePendingImpulseToApply.Z + (GravityRelativePendingForceToApply.Z * DeltaSeconds) + (GetGravityZ() * DeltaSeconds) > UE_SMALL_NUMBER))
{
SetMovementMode(MOVE_Falling);
}
}
Velocity += PendingImpulseToApply + (PendingForceToApply * DeltaSeconds);
// Don't call ClearAccumulatedForces() because it could affect launch velocity
PendingImpulseToApply = FVector::ZeroVector;
PendingForceToApply = FVector::ZeroVector;
}
ApplyAccumulatedForces()
함수는 캐릭터의 이동에 영향을 미치는 누적된 힘과 충격을 적용하고, 중력의 영향을 고려한 정확한 이동을 보장한다. 만약 Z축 방향으로의 힘이 존재하는 경우, 캐릭터가 지면에 있고 중력 방향의 누적된 충격과 힘이 중력의 영향을 극복할 수 있을 정도이면 이동 모드를 Falling
으로 변경한다.
그리고 현재 캐릭터의 Velocity
에 PendingImpulseToApply
와 PendingForceToApply
를 더해 갱신해준다.
void UCharacterMovementComponent::ClearAccumulatedForces()
{
PendingImpulseToApply = FVector::ZeroVector;
PendingForceToApply = FVector::ZeroVector;
PendingLaunchVelocity = FVector::ZeroVector;
}
ClearAccumulatedForces()
함수를 통해 캐릭터의 이동에 영향을 미칠 수 있는 누적된 힘과 속도를 초기화한다. 다음 프레임에서 이전의 힘이 지속적으로 영향을 미치지 않도록 만들어준다.
PendingImpulseToApply
는 캐릭터에 즉시 적용되는 충격량을 나타내며, PendingForceToApply
는 캐릭터에 지속적으로 적용되는 힘을 나타내고 PendingLaunchVelocity
는 캐릭터를 발사할 때 적용되는 속도를 나타낸다.
PendingLaunchVelocity
는 캐릭터가 특정한 이벤트나 행동으로 인해 순간적으로 빠른 속도로 발사되거나 이동될 때 사용된다. LaunchCharacter()
함수에서 사용되어 캐릭터에 순간적인 속도를 부여한다.
void UCharacterMovementComponent::StartNewPhysics(float deltaTime, int32 Iterations)
{
if ((deltaTime < MIN_TICK_TIME) || (Iterations >= MaxSimulationIterations) || !HasValidData())
{
return;
}
if (UpdatedComponent->IsSimulatingPhysics())
{
UE_LOG(LogCharacterMovement, Log, TEXT("UCharacterMovementComponent::StartNewPhysics: UpdateComponent (%s) is simulating physics - aborting."), *UpdatedComponent->GetPathName());
return;
}
const bool bSavedMovementInProgress = bMovementInProgress;
bMovementInProgress = true;
switch ( MovementMode )
{
case MOVE_None:
break;
case MOVE_Walking:
PhysWalking(deltaTime, Iterations);
break;
case MOVE_NavWalking:
PhysNavWalking(deltaTime, Iterations);
break;
case MOVE_Falling:
PhysFalling(deltaTime, Iterations);
break;
case MOVE_Flying:
PhysFlying(deltaTime, Iterations);
break;
case MOVE_Swimming:
PhysSwimming(deltaTime, Iterations);
break;
case MOVE_Custom:
PhysCustom(deltaTime, Iterations);
break;
default:
UE_LOG(LogCharacterMovement, Warning, TEXT("%s has unsupported movement mode %d"), *CharacterOwner->GetName(), int32(MovementMode));
SetMovementMode(MOVE_None);
break;
}
bMovementInProgress = bSavedMovementInProgress;
if ( bDeferUpdateMoveComponent )
{
SetUpdatedComponent(DeferredUpdatedMoveComponent);
}
}
StartNewPhysics()
함수에서는 무브먼트 모드에 따라 Phys* 함수를 실행시키고, 각각의 함수에서 캐릭터의 위치와 속도를 업데이트 한다.
if (bAllowPhysicsRotationDuringAnimRootMotion || !HasAnimRootMotion())
{
PhysicsRotation(DeltaSeconds);
}
void UCharacterMovementComponent::PhysicsRotation(float DeltaTime)
{
if (!(bOrientRotationToMovement || bUseControllerDesiredRotation))
{
return;
}
if (!HasValidData() || (!CharacterOwner->Controller && !bRunPhysicsWithNoController))
{
return;
}
FRotator CurrentRotation = UpdatedComponent->GetComponentRotation(); // Normalized
CurrentRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): CurrentRotation"));
FRotator DeltaRot = GetDeltaRotation(DeltaTime);
DeltaRot.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): GetDeltaRotation"));
FRotator DesiredRotation = CurrentRotation;
if (bOrientRotationToMovement)
{
DesiredRotation = ComputeOrientToMovementRotation(CurrentRotation, DeltaTime, DeltaRot);
}
else if (CharacterOwner->Controller && bUseControllerDesiredRotation)
{
DesiredRotation = CharacterOwner->Controller->GetDesiredRotation();
}
else if (!CharacterOwner->Controller && bRunPhysicsWithNoController && bUseControllerDesiredRotation)
{
if (AController* ControllerOwner = Cast<AController>(CharacterOwner->GetOwner()))
{
DesiredRotation = ControllerOwner->GetDesiredRotation();
}
}
else
{
return;
}
const bool bWantsToBeVertical = ShouldRemainVertical();
if (bWantsToBeVertical)
{
if (HasCustomGravity())
{
FRotator GravityRelativeDesiredRotation = (GravityToWorldTransform * DesiredRotation.Quaternion()).Rotator();
GravityRelativeDesiredRotation.Pitch = 0.f;
GravityRelativeDesiredRotation.Yaw = FRotator::NormalizeAxis(GravityRelativeDesiredRotation.Yaw);
GravityRelativeDesiredRotation.Roll = 0.f;
DesiredRotation = (WorldToGravityTransform * GravityRelativeDesiredRotation.Quaternion()).Rotator();
}
else
{
DesiredRotation.Pitch = 0.f;
DesiredRotation.Yaw = FRotator::NormalizeAxis(DesiredRotation.Yaw);
DesiredRotation.Roll = 0.f;
}
}
else
{
DesiredRotation.Normalize();
}
// Accumulate a desired new rotation.
const float AngleTolerance = 1e-3f;
if (!CurrentRotation.Equals(DesiredRotation, AngleTolerance))
{
// If we'd be prevented from becoming vertical, override the non-yaw rotation rates to allow the character to snap upright
if (CharacterMovementCVars::bPreventNonVerticalOrientationBlock && bWantsToBeVertical)
{
if (FMath::IsNearlyZero(DeltaRot.Pitch))
{
DeltaRot.Pitch = 360.0;
}
if (FMath::IsNearlyZero(DeltaRot.Roll))
{
DeltaRot.Roll = 360.0;
}
}
// PITCH
if (!FMath::IsNearlyEqual(CurrentRotation.Pitch, DesiredRotation.Pitch, AngleTolerance))
{
DesiredRotation.Pitch = FMath::FixedTurn(CurrentRotation.Pitch, DesiredRotation.Pitch, DeltaRot.Pitch);
}
// YAW
if (!FMath::IsNearlyEqual(CurrentRotation.Yaw, DesiredRotation.Yaw, AngleTolerance))
{
DesiredRotation.Yaw = FMath::FixedTurn(CurrentRotation.Yaw, DesiredRotation.Yaw, DeltaRot.Yaw);
}
// ROLL
if (!FMath::IsNearlyEqual(CurrentRotation.Roll, DesiredRotation.Roll, AngleTolerance))
{
DesiredRotation.Roll = FMath::FixedTurn(CurrentRotation.Roll, DesiredRotation.Roll, DeltaRot.Roll);
}
// Set the new rotation.
DesiredRotation.DiagnosticCheckNaN(TEXT("CharacterMovementComponent::PhysicsRotation(): DesiredRotation"));
MoveUpdatedComponent( FVector::ZeroVector, DesiredRotation, /*bSweep*/ false );
}
}
bOrientRotationToMovement
나 bUseControllerDesiredRotation
가 true
일 경우 진행되는 함수이다. 이 함수는 이동 방향이나 컨트롤러의 의도에 맞춰 회전 값을 진행하고, 이를 적용하여 캐릭터가 자연스럽게 회전하도록 한다.
bOrientRotationToMovement
가 true일 경우 이동 방향에 맞춘 회전 값을 계산하고, bUseControllerDesiredRotation
가 true일 경우 컨트롤러의 회전 값을 가져온다. 컨트롤러가 없고 물리엔진을 사용하는 경우 캐릭터 소유자의 회전 값을 가져온다.
OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity);
OnMovementUpdated()
는 아무것도 없는 빈 함수이지만, 커스텀 무브먼트 컴포넌트를 만들어 오버라이드하면 새로운 이동 로직을 포함할 수 있다.
void UCharacterMovementComponent::CallMovementUpdateDelegate(float DeltaTime, const FVector& OldLocation, const FVector& OldVelocity)
{
SCOPE_CYCLE_COUNTER(STAT_CharMoveUpdateDelegate);
// Update component velocity in case events want to read it
UpdateComponentVelocity();
// Delegate (for blueprints)
if (CharacterOwner)
{
CharacterOwner->OnCharacterMovementUpdated.Broadcast(DeltaTime, OldLocation, OldVelocity);
}
}
CallMovementUpdateDelegate()
는 캐릭터의 이동이 업데이트 될 때마다 호출되는 델리게이트를 실행하는 함수이다.
LastUpdateLocation = NewLocation;
LastUpdateRotation = NewRotation;
LastUpdateVelocity = Velocity;
이전 Location 값과 Rotation 값, Velocity를 방금 만들어진 움직임으로 저장하고 함수가 끝난다.
PerformMovement()
함수는 캐릭터의 이동 모드에 따라 적절한 움직임을 수행하고, 중력과 루트모션 적용, 네트워크 동기화 등 캐릭터의 움직임을 실제로 처리하는 핵심 메소드인 것을 알 수 있었다. 이제는 내 입력값에 따라 실제로 Velocity를 건들이는 부분인 AddMovementInput()
함수를 살펴보기로 했다.
void APawn::AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/)
{
UPawnMovementComponent* MovementComponent = GetMovementComponent();
if (MovementComponent)
{
MovementComponent->AddInputVector(WorldDirection * ScaleValue, bForce);
}
else
{
Internal_AddMovementInput(WorldDirection * ScaleValue, bForce);
}
}
여기서 MovementComponent의 AddInputVector()
로 들어가보면,
void UPawnMovementComponent::AddInputVector(FVector WorldAccel, bool bForce /*=false*/)
{
if (PawnOwner)
{
PawnOwner->Internal_AddMovementInput(WorldAccel, bForce);
}
}
PawnOwner의 Internal_AddMovementInput()
를 호출한다.
void APawn::Internal_AddMovementInput(FVector WorldAccel, bool bForce /*=false*/)
{
if (bForce || !IsMoveInputIgnored())
{
ControlInputVector += WorldAccel;
}
}
Internal_AddMovementInput()
함수에서는 ControlInputVector
에 WorldAccel
를 더해주고 있다.
이 ControlInputVector()
는 GetPendingInputVector()
에서 리턴해주고 있고, GetPendingInputVector()
는 ApplyControlInputToVelocity()
함수에서 호출되고 있다.
void UFloatingPawnMovement::ApplyControlInputToVelocity(float DeltaTime)
{
const FVector ControlAcceleration = GetPendingInputVector().GetClampedToMaxSize(1.f);
const float AnalogInputModifier = (ControlAcceleration.SizeSquared() > 0.f ? ControlAcceleration.Size() : 0.f);
const float MaxPawnSpeed = GetMaxSpeed() * AnalogInputModifier;
const bool bExceedingMaxSpeed = IsExceedingMaxSpeed(MaxPawnSpeed);
if (AnalogInputModifier > 0.f && !bExceedingMaxSpeed)
{
// Apply change in velocity direction
if (Velocity.SizeSquared() > 0.f)
{
// Change direction faster than only using acceleration, but never increase velocity magnitude.
const float TimeScale = FMath::Clamp(DeltaTime * TurningBoost, 0.f, 1.f);
Velocity = Velocity + (ControlAcceleration * Velocity.Size() - Velocity) * TimeScale;
}
}
else
{
// Dampen velocity magnitude based on deceleration.
if (Velocity.SizeSquared() > 0.f)
{
const FVector OldVelocity = Velocity;
const float VelSize = FMath::Max(Velocity.Size() - FMath::Abs(Deceleration) * DeltaTime, 0.f);
Velocity = Velocity.GetSafeNormal() * VelSize;
// Don't allow braking to lower us below max speed if we started above it.
if (bExceedingMaxSpeed && Velocity.SizeSquared() < FMath::Square(MaxPawnSpeed))
{
Velocity = OldVelocity.GetSafeNormal() * MaxPawnSpeed;
}
}
}
// Apply acceleration and clamp velocity magnitude.
const float NewMaxSpeed = (IsExceedingMaxSpeed(MaxPawnSpeed)) ? Velocity.Size() : MaxPawnSpeed;
Velocity += ControlAcceleration * FMath::Abs(Acceleration) * DeltaTime;
Velocity = Velocity.GetClampedToMaxSize(NewMaxSpeed);
ConsumeInputVector();
}
입력벡터의 값이 클램핑되어 ControlAcceleration
변수에 저장되고, MaxSpeed
값을 이용해 MaxPawnSpeed
를 계산한 뒤, IsExceedingMaxSpeed()
함수를 사용해 최대 스피드를 넘었는 지 계산한다.
만약 AnalogInputModifier
값이 0이상(입력이 존재)이고 최대 스피드를 넘지 않았다면, 입력 벡터 방향으로 속도를 변경한다.
입력이 없거나 최대 속도를 초과하는 경우, 감속을 적용하여 속도의 크기를 줄인다.
ApplyControlInputToVelocity()
함수는, 플레이어의 입력을 기반으로 속도를 업데이트하고 최대 속도를 넘지 않게 하며, 가속과 감속을 적용해 속도를 부드럽게 변경하는 것을 알 수 있다.