PART 6 · 강의 6/8

이동 예측 및 보정

Client-Side Prediction과 Server Reconciliation

01

Client-Side Prediction

지연을 숨기는 클라이언트 예측

클라이언트 예측은 서버 응답을 기다리지 않고 로컬에서 먼저 이동을 처리하여 반응성을 높이는 기법입니다. CharacterMovementComponent는 이를 내장하고 있습니다.

예측 흐름
  • 1. 입력 발생 — 클라이언트가 이동 입력을 받음
  • 2. 로컬 예측 — 서버 응답 전에 로컬에서 먼저 이동
  • 3. 서버 전송 — 입력을 서버에 전송
  • 4. 서버 처리 — 서버가 권한적으로 이동 처리
  • 5. 보정 — 서버 결과와 다르면 클라이언트 위치 보정
CharacterMovementComponent의 내장 예측

언리얼의 CharacterMovementComponent는 이미 클라이언트 예측과 서버 보정을 구현하고 있습니다. 기본 이동은 별도 작업 없이 네트워크에서 잘 동작합니다.

02

커스텀 이동 복제

FSavedMove 확장

대시, 글라이딩 같은 커스텀 이동 모드를 네트워크에서 제대로 예측하려면 FSavedMove를 확장해야 합니다.

CustomCharacterMovement.h UCLASS() class MYGAME_API UCustomCharacterMovement : public UCharacterMovementComponent { GENERATED_BODY() public: // 저장된 이동 데이터 확장 class FSavedMove_Custom : public FSavedMove_Character { public: // 커스텀 데이터 추가 bool bWantsToDash; float DashCooldown; virtual void Clear() override; virtual void SetMoveFor( ACharacter* C, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData) override; virtual bool CanCombineWith( const FSavedMovePtr& NewMove, ACharacter* InCharacter, float MaxDelta) const override; virtual void PrepMoveFor(ACharacter* C) override; }; virtual class FNetworkPredictionData_Client* GetPredictionData_Client() const override; protected: UPROPERTY() bool bWantsToDash; UPROPERTY(Replicated) float DashCooldown; public: void RequestDash(); UFUNCTION(Server, Reliable) void Server_PerformDash(); };
CustomCharacterMovement.cpp void UCustomCharacterMovement::FSavedMove_Custom::Clear() { FSavedMove_Character::Clear(); bWantsToDash = false; DashCooldown = 0.0f; } void UCustomCharacterMovement::FSavedMove_Custom::SetMoveFor( ACharacter* C, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData) { FSavedMove_Character::SetMoveFor(C, InDeltaTime, NewAccel, ClientData); if (UCustomCharacterMovement* CustomMove = Cast<UCustomCharacterMovement>(C->GetCharacterMovement())) { bWantsToDash = CustomMove->bWantsToDash; DashCooldown = CustomMove->DashCooldown; } } void UCustomCharacterMovement::RequestDash() { if (DashCooldown <= 0.0f) { bWantsToDash = true; // 클라이언트 예측: 즉시 대시 수행 PerformDashLocally(); // 서버에 알림 Server_PerformDash(); } } void UCustomCharacterMovement::Server_PerformDash_Implementation() { // 서버에서 검증 및 수행 if (DashCooldown <= 0.0f) { PerformDashLocally(); DashCooldown = 2.0f; // 2초 쿨다운 } }
03

Lag Compensation

지연 보상으로 공정한 히트 판정

지연 보상은 서버가 클라이언트의 과거 시점으로 되돌아가 히트 판정을 수행하는 기법입니다. 언리얼은 이를 내장하지 않으므로 직접 구현해야 합니다.

LagCompensationManager.h UCLASS() class MYGAME_API ULagCompensationManager : public UActorComponent { GENERATED_BODY() public: // 히스토리 엔트리 struct FPositionHistoryEntry { FVector Location; FRotator Rotation; FVector BoundsExtent; float ServerTime; }; protected: // 위치 히스토리 (최대 1초) TArray<FPositionHistoryEntry> PositionHistory; static const int32 MAX_HISTORY_SIZE = 60; // 60Hz 기준 public: virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; // 특정 시간의 위치 가져오기 bool GetPositionAtTime( float ServerTime, FVector& OutLocation, FRotator& OutRotation) const; // 지연 보상 히트 검사 bool PerformLagCompensatedTrace( const FVector& Start, const FVector& End, float ClientTimestamp, FHitResult& OutHit); private: void RecordPosition(); };
LagCompensationManager.cpp (핵심 부분) bool ULagCompensationManager::PerformLagCompensatedTrace( const FVector& Start, const FVector& End, float ClientTimestamp, FHitResult& OutHit) { // 모든 잠재적 타겟의 위치를 과거로 되돌림 TMap<AActor*, FTransform> OriginalTransforms; for (TActorIterator<APawn> It(GetWorld()); It; ++It) { APawn* Pawn = *It; if (Pawn == GetOwner()) continue; if (ULagCompensationManager* OtherLagComp = Pawn->FindComponentByClass<ULagCompensationManager>()) { FVector HistoricalLocation; FRotator HistoricalRotation; if (OtherLagComp->GetPositionAtTime( ClientTimestamp, HistoricalLocation, HistoricalRotation)) { // 원래 위치 저장 OriginalTransforms.Add(Pawn, Pawn->GetActorTransform()); // 과거 위치로 이동 Pawn->SetActorLocationAndRotation( HistoricalLocation, HistoricalRotation); } } } // 히트 검사 수행 bool bHit = GetWorld()->LineTraceSingleByChannel( OutHit, Start, End, ECC_Pawn); // 모든 액터를 원래 위치로 복원 for (auto& Pair : OriginalTransforms) { Pair.Key->SetActorTransform(Pair.Value); } return bHit; }
04

보간 기법과 스냅샷 시스템

Simulated Proxy의 부드러운 이동 표현

다른 플레이어(Simulated Proxy)의 움직임은 서버에서 주기적으로 수신하는 위치 스냅샷을 기반으로 보간하여 표현합니다. 네트워크 전송 빈도보다 렌더링 프레임이 훨씬 빠르므로, 스냅샷 사이를 보간하지 않으면 캐릭터가 끊기며 이동하는 현상이 발생합니다.

주요 보간 방식
  • Linear Interpolation -- 두 스냅샷 사이를 직선으로 보간하는 가장 기본적인 방식
  • Cubic/Hermite Interpolation -- 속도 벡터를 함께 활용하여 곡선형 경로로 보간, 더 자연스러운 이동
  • Extrapolation -- 스냅샷 도착이 지연될 때 마지막 속도를 기반으로 예측 위치를 계산
  • Snapshot Buffer -- 2~3개의 스냅샷을 버퍼에 저장하고 과거 시점을 재생하여 지터를 흡수
C++ // NetworkSmoothing.h - 스냅샷 기반 보간 시스템 USTRUCT() struct FNetworkSnapshot { GENERATED_BODY() FVector Location; FRotator Rotation; FVector Velocity; float ServerTimestamp = 0.0f; float ReceivedTime = 0.0f; }; UCLASS() class MYGAME_API UNetworkSmoothingComponent : public UActorComponent { GENERATED_BODY() public: // 스냅샷 버퍼 (최근 N개 보관) static constexpr int32 SNAPSHOT_BUFFER_SIZE = 4; // 새 스냅샷 수신 시 호출 void ReceiveSnapshot(const FNetworkSnapshot& Snapshot) { SnapshotBuffer[WriteIndex] = Snapshot; WriteIndex = (WriteIndex + 1) % SNAPSHOT_BUFFER_SIZE; SnapshotCount = FMath::Min(SnapshotCount + 1, SNAPSHOT_BUFFER_SIZE); } // Hermite 보간으로 부드러운 위치 계산 FVector GetInterpolatedLocation(float RenderTime) const { if (SnapshotCount < 2) return GetLatestSnapshot().Location; const FNetworkSnapshot& From = GetSnapshotBefore(RenderTime); const FNetworkSnapshot& To = GetSnapshotAfter(RenderTime); float Duration = To.ServerTimestamp - From.ServerTimestamp; if (Duration <= 0.0f) return To.Location; float Alpha = (RenderTime - From.ServerTimestamp) / Duration; Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f); // Hermite 보간: 위치 + 속도를 함께 활용 return FMath::CubicInterp( From.Location, From.Velocity * Duration, To.Location, To.Velocity * Duration, Alpha); } private: FNetworkSnapshot SnapshotBuffer[SNAPSHOT_BUFFER_SIZE]; int32 WriteIndex = 0; int32 SnapshotCount = 0; // 보간 지연 (스냅샷 1개분 과거를 렌더링) UPROPERTY(EditAnywhere, Category = "Smoothing") float InterpolationDelay = 0.1f; // 100ms 과거 };
보간 지연(Interpolation Delay) 설정

보간 지연은 일반적으로 서버 틱 간격의 2배 정도로 설정합니다. 서버가 30Hz로 동작하면 약 66ms, 60Hz면 약 33ms가 적절합니다. 너무 크면 반응이 느려지고, 너무 작으면 스냅샷 부족으로 외삽(Extrapolation)에 의존하게 됩니다.

Simulated Proxy vs Autonomous Proxy

보간은 Simulated Proxy(다른 플레이어)에만 적용됩니다. 로컬 플레이어(Autonomous Proxy)는 클라이언트 예측으로 즉시 이동하고, 서버 보정 시에만 위치가 수정됩니다. CharacterMovementComponent의 NetworkSmoothingModeExponential 또는 Linear로 설정하여 보정 시 스무딩 방식을 제어할 수 있습니다.

SUMMARY

핵심 요약

  • Client-Side Prediction -- 서버 응답 전 로컬에서 이동 처리하여 반응성 향상
  • Server Reconciliation -- 서버 결과와 차이 시 클라이언트 위치 보정
  • FSavedMove -- 커스텀 이동 데이터를 예측 시스템에 통합
  • Lag Compensation -- 과거 시점으로 되돌려 히트 판정
  • Snapshot Interpolation -- 스냅샷 버퍼와 보간으로 Simulated Proxy의 부드러운 이동 표현
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: CharacterMovement 네트워크 모드 이해

UCharacterMovementComponent의 네트워크 모드(Autonomous Proxy, Simulated Proxy, Authority)를 이해하세요. Net Emulation으로 200ms 지연을 설정하고, 클라이언트 예측 이동과 서버 보정을 관찰하세요.

실습 2: 서버 보정 임계값 조정

CharacterMovement의 NetworkMaxSmoothUpdateDistance, NetworkSimulatedSmoothLocationTime을 조정하여, 보정 시 캐릭터가 순간이동하는 현상을 부드럽게 처리하세요.

심화 과제: 커스텀 무브먼트 예측 구현

RPG 대시/텔레포트 같은 특수 이동을 네트워크에서 예측/보정하세요. FSavedMove_Character를 상속하고, CanCombineWith()으로 무브 압축을 구현하여 네트워크 효율을 높이세요.