UE5 CharacterMovement 심층 분석
개요 — CMC의 역할과 구조
섹션 제목: “개요 — CMC의 역할과 구조”UCharacterMovementComponent(CMC)는 UE5에서 ACharacter의 이동을 처리하는 핵심 컴포넌트입니다. 단순한 위치 이동을 넘어 다음을 담당합니다.
- Movement Mode 관리: Walking, Falling, Swimming, Flying, Custom 등
- 물리 기반 이동: 중력, 마찰, 공중 제어
- 네트워크 예측: 클라이언트 측 예측 + 서버 보정
- 루트 모션 통합: 애니메이션 루트 모션과 이동 로직 통합
CMC 업데이트 흐름──────────────────────────────TickComponent() └─ PerformMovement() ├─ StartNewPhysics() ← 현재 Mode에 맞는 물리 계산 │ ├─ PhysWalking() │ ├─ PhysFalling() │ ├─ PhysSwimming() │ └─ PhysCustom() ← 커스텀 구현 진입점 └─ MoveUpdatedComponent() ← 실제 위치 적용1. Movement Mode 이해
섹션 제목: “1. Movement Mode 이해”1.1 기본 Mode 전환
섹션 제목: “1.1 기본 Mode 전환”// 이동 모드 전환void AMyCharacter::StartFlying(){ if (UCharacterMovementComponent* CMC = GetCharacterMovement()) { CMC->SetMovementMode(MOVE_Flying); }}
void AMyCharacter::StartSwimming(){ GetCharacterMovement()->SetMovementMode(MOVE_Swimming);}
// 커스텀 모드 진입 (Mode = MOVE_Custom, CustomMode = 원하는 값)void AMyCharacter::StartWallRunning(){ GetCharacterMovement()->SetMovementMode(MOVE_Custom, static_cast<uint8>(EMyMovementMode::WallRunning));}1.2 커스텀 Movement Mode 열거형
섹션 제목: “1.2 커스텀 Movement Mode 열거형”UENUM(BlueprintType)enum class EMyMovementMode : uint8{ None UMETA(DisplayName = "None"), WallRunning UMETA(DisplayName = "Wall Running"), Sliding UMETA(DisplayName = "Sliding"), Climbing UMETA(DisplayName = "Climbing"),};2. CMC 서브클래싱 — 커스텀 이동 구현
섹션 제목: “2. CMC 서브클래싱 — 커스텀 이동 구현”2.1 클래스 선언
섹션 제목: “2.1 클래스 선언”#pragma once
#include "CoreMinimal.h"#include "GameFramework/CharacterMovementComponent.h"#include "MyCharacterMovementComponent.generated.h"
UCLASS()class MYGAME_API UMyCharacterMovementComponent : public UCharacterMovementComponent{ GENERATED_BODY()
public: UMyCharacterMovementComponent();
// 커스텀 모드 물리 처리 virtual void PhysCustom(float DeltaTime, int32 Iterations) override;
// 이동 가능 여부 쿼리 virtual bool CanAttemptJump() const override;
// 서버 검증 (네트워크) virtual bool ServerCheckClientError(float ClientTimeStamp, float DeltaTime, const FVector& Accel, const FVector& ClientWorldLocation, const FVector& RelativeClientLocation, UPrimitiveComponent* ClientMovementBase, FName ClientBaseBoneName, uint8 ClientMovementMode) override;
// 벽타기 진입/종료 void EnterWallRunning(const FVector& WallNormal); void ExitWallRunning();
bool IsWallRunning() const;
// 벽타기 파라미터 UPROPERTY(EditDefaultsOnly, Category = "Wall Running") float WallRunSpeed = 800.f;
UPROPERTY(EditDefaultsOnly, Category = "Wall Running") float WallRunGravityScale = 0.25f;
private: void PhysWallRunning(float DeltaTime, int32 Iterations);
FVector WallNormal = FVector::ZeroVector; bool bIsWallRunning = false;};2.2 PhysCustom 구현
섹션 제목: “2.2 PhysCustom 구현”void UMyCharacterMovementComponent::PhysCustom(float DeltaTime, int32 Iterations){ // 현재 커스텀 모드에 따라 분기 switch (static_cast<EMyMovementMode>(CustomMovementMode)) { case EMyMovementMode::WallRunning: PhysWallRunning(DeltaTime, Iterations); break;
case EMyMovementMode::Sliding: // PhysSliding(DeltaTime, Iterations); break;
default: Super::PhysCustom(DeltaTime, Iterations); break; }}
void UMyCharacterMovementComponent::PhysWallRunning(float DeltaTime, int32 Iterations){ if (DeltaTime < MIN_TICK_TIME) return;
// 중력 스케일 임시 적용 const float SavedGravityScale = GravityScale; GravityScale = WallRunGravityScale;
// 벽 방향에 수직인 이동 방향 계산 FVector WallRunDirection = FVector::CrossProduct(WallNormal, FVector::UpVector); WallRunDirection.Normalize();
// 속도 적용 Velocity = WallRunDirection * WallRunSpeed; Velocity.Z = FMath::Max(Velocity.Z, -200.f); // 너무 빠르게 내려가지 않도록
// 가속 적용 및 위치 이동 Iterations++; bJustTeleported = false;
FVector OldLocation = UpdatedComponent->GetComponentLocation(); const FVector Adjusted = Velocity * DeltaTime;
FHitResult Hit(1.f); SafeMoveUpdatedComponent(Adjusted, UpdatedComponent->GetComponentQuat(), true, Hit);
if (Hit.IsValidBlockingHit()) { // 벽에 충돌 — 계속 벽에 붙어 있는지 확인 FVector ImpactNormal = Hit.ImpactNormal; if (FMath::Abs(ImpactNormal.Z) < 0.2f) // 수직에 가까운 벽 { WallNormal = ImpactNormal; } else { ExitWallRunning(); } }
GravityScale = SavedGravityScale;
// 이 프레임에서 이동한 속도 계산 const FVector NewLocation = UpdatedComponent->GetComponentLocation(); Velocity = (NewLocation - OldLocation) / DeltaTime;}
void UMyCharacterMovementComponent::EnterWallRunning(const FVector& InWallNormal){ WallNormal = InWallNormal; bIsWallRunning = true; SetMovementMode(MOVE_Custom, static_cast<uint8>(EMyMovementMode::WallRunning));}
void UMyCharacterMovementComponent::ExitWallRunning(){ bIsWallRunning = false; WallNormal = FVector::ZeroVector; SetMovementMode(MOVE_Falling);}
bool UMyCharacterMovementComponent::IsWallRunning() const{ return MovementMode == MOVE_Custom && CustomMovementMode == static_cast<uint8>(EMyMovementMode::WallRunning);}3. 네트워크 예측 — SavedMove 패턴
섹션 제목: “3. 네트워크 예측 — SavedMove 패턴”멀티플레이어 게임에서 CMC는 클라이언트 측 예측으로 입력 반응성을 유지하고, 서버와 주기적으로 동기화합니다. 커스텀 이동 데이터를 네트워크로 전송하려면 FSavedMove_Character를 확장해야 합니다.
// MyCharacterMovementComponent.h 추가class MYGAME_API FSavedMove_My : public FSavedMove_Character{ typedef FSavedMove_Character Super;
public: // 커스텀 저장 데이터 uint8 bSavedWantsToWallRun : 1;
virtual bool CanCombineWith(const FSavedMovePtr& NewMove, ACharacter* InCharacter, float MaxDelta) const override; virtual void Clear() override; virtual uint8 GetCompressedFlags() const override; virtual void SetMoveFor(ACharacter* C, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData) override; virtual void PrepMoveFor(ACharacter* C) override;};
class MYGAME_API FNetworkPredictionData_Client_My : public FNetworkPredictionData_Client_Character{public: FNetworkPredictionData_Client_My(const UCharacterMovementComponent& ClientMovement); virtual FSavedMovePtr AllocateNewMove() override;};// SavedMove 구현void FSavedMove_My::Clear(){ Super::Clear(); bSavedWantsToWallRun = 0;}
uint8 FSavedMove_My::GetCompressedFlags() const{ uint8 Result = Super::GetCompressedFlags(); if (bSavedWantsToWallRun) Result |= FLAG_Custom_0; return Result;}
void FSavedMove_My::SetMoveFor(ACharacter* C, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData){ Super::SetMoveFor(C, InDeltaTime, NewAccel, ClientData);
if (UMyCharacterMovementComponent* CMC = Cast<UMyCharacterMovementComponent>(C->GetCharacterMovement())) { bSavedWantsToWallRun = CMC->bWantsToWallRun; }}
void FSavedMove_My::PrepMoveFor(ACharacter* C){ Super::PrepMoveFor(C);
if (UMyCharacterMovementComponent* CMC = Cast<UMyCharacterMovementComponent>(C->GetCharacterMovement())) { CMC->bWantsToWallRun = bSavedWantsToWallRun; }}
// CMC에서 커스텀 PredictionData 반환FNetworkPredictionData_Client* UMyCharacterMovementComponent::GetPredictionData_Client() const{ if (!ClientPredictionData) { UMyCharacterMovementComponent* MutableThis = const_cast<UMyCharacterMovementComponent*>(this); MutableThis->ClientPredictionData = new FNetworkPredictionData_Client_My(*this); } return ClientPredictionData;}4. 주요 파라미터 튜닝
섹션 제목: “4. 주요 파라미터 튜닝”// AMyCharacter::BeginPlay 또는 생성자에서 설정void AMyCharacter::SetupMovementParameters(){ UCharacterMovementComponent* CMC = GetCharacterMovement(); if (!CMC) return;
// 걷기 CMC->MaxWalkSpeed = 600.f; CMC->MaxWalkSpeedCrouched = 300.f; CMC->BrakingDecelerationWalking = 2048.f; CMC->GroundFriction = 8.f;
// 점프 CMC->JumpZVelocity = 600.f; CMC->AirControl = 0.35f; // 공중 제어 비율 (0=무제어, 1=완전제어) CMC->AirControlBoostMultiplier = 2.f; CMC->FallingLateralFriction = 0.f;
// 중력 CMC->GravityScale = 1.75f; // 기본(1.0)보다 빠른 낙하 — 더 빠릿한 조작감
// 계단 처리 CMC->MaxStepHeight = 45.f; CMC->SetWalkableFloorAngle(46.f); // 등반 가능한 최대 경사각
// 네트워크 CMC->NetworkSmoothingMode = ENetworkSmoothingMode::Exponential; CMC->NetworkMaxSmoothUpdateDistance = 92.f; CMC->NetworkNoSmoothUpdateDistance = 140.f;}5. 정리
섹션 제목: “5. 정리”| 기능 | 진입점 |
|---|---|
| 커스텀 이동 구현 | PhysCustom() 오버라이드 |
| 이동 모드 전환 | SetMovementMode(MOVE_Custom, CustomMode) |
| 네트워크 예측 데이터 | FSavedMove_Character 서브클래스 |
| 이동 불가 조건 | CanAttemptJump() 등 오버라이드 |
| 파라미터 조정 | MaxWalkSpeed, GravityScale, AirControl 등 |
6. OnMovementModeChanged — 이동 모드 전환 알림
섹션 제목: “6. OnMovementModeChanged — 이동 모드 전환 알림”이동 모드가 바뀔 때 엔진이 호출하는 훅입니다. 코요테 타임, 착지 효과, 상태 전환 등 모드 변경 시점에 처리할 로직을 여기에 작성합니다.
void UMyCharacterMovementComponent::OnMovementModeChanged( EMovementMode PreviousMovementMode, uint8 PreviousCustomMode){ Super::OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode);
// 지상 → 낙하: 코요테 타임 시작 if (MovementMode == MOVE_Falling && PreviousMovementMode == MOVE_Walking) { bCoyoteTimeActive = true; GetWorld()->GetTimerManager().SetTimer(CoyoteTimerHandle, [this]() { bCoyoteTimeActive = false; }, CoyoteTime, /*bLoop=*/false); }
// 낙하 → 착지: 코요테 타임 해제, 착지 효과 처리 if (MovementMode == MOVE_Walking && PreviousMovementMode == MOVE_Falling) { bCoyoteTimeActive = false; GetWorld()->GetTimerManager().ClearTimer(CoyoteTimerHandle); OnLanded(); }}루트 모션 설정
섹션 제목: “루트 모션 설정”// BeginPlay 또는 생성자에서 루트 모션 모드 설정// 애니메이션 몽타주가 Actor 이동을 직접 제어하게 함GetMesh()->SetRootMotionMode(ERootMotionMode::RootMotionFromMontagesOnly);
// 루트 모션 중 물리 회전 억제 (슬라이딩 방지)GetCharacterMovement()->bAllowPhysicsRotationDuringAnimRootMotion = false;
// 루트 모션 활성 여부 확인bool bHasRootMotion = GetMesh()->IsPlayingRootMotion();| 설정 | 설명 |
|---|---|
RootMotionFromMontagesOnly | 몽타주 재생 중에만 루트 모션 적용 |
RootMotionFromEverything | 모든 애니메이션에서 루트 모션 적용 |
NoRootMotionExtraction | 루트 모션 비활성 (기본) |