PART 6 - 강의 2/3
Prediction Key 시스템
클라이언트 예측과 서버 검증
01
예측의 필요성
레이턴시 숨기기
클라이언트 예측은 네트워크 레이턴시를 숨기기 위해 서버 응답을 기다리지 않고 클라이언트에서 먼저 결과를 적용하는 기법입니다. GAS는 이를 위해 Prediction Key 시스템을 제공합니다.
예측이 없는 경우
// 예측 없이 어빌리티 사용 (레이턴시 체감)
// 1. 클라이언트: 입력 감지
// 2. 클라이언트 -> 서버: RPC 전송 (50ms)
// 3. 서버: 어빌리티 실행, Effect 적용
// 4. 서버 -> 클라이언트: 결과 복제 (50ms)
// 5. 클라이언트: 결과 표시
// 총 지연: ~100ms
// 예측을 사용하는 경우
// 1. 클라이언트: 입력 감지
// 2. 클라이언트: 즉시 예측 실행 (0ms)
// 3. 클라이언트 -> 서버: RPC 전송 (백그라운드)
// 4. 서버: 검증 후 확정 또는 롤백
// 5. 클라이언트: 서버 결과와 비교, 필요시 보정
// 체감 지연: ~0ms
02
Prediction Key 구조
예측 식별자
FPredictionKey 구조체
struct FPredictionKey
{
// 현재 키 ID (16비트)
int16 Current;
// 기반 키 ID (종속 예측용)
int16 Base;
// 서버에서 확인됨
bool bIsServerInitiated;
// 스테일 여부 (더 이상 유효하지 않음)
bool bIsStale;
};
// Prediction Key 생성 시점
// 1. 클라이언트에서 어빌리티 활성화 시
// 2. 새로운 예측 범위 시작 시
// 사용 흐름
void UMyAbility::ActivateAbility(...)
{
// 현재 Prediction Key 확인
FPredictionKey PredictionKey = GetCurrentActivationInfo().GetActivationPredictionKey();
if (PredictionKey.IsValidKey())
{
// 이 키로 예측된 Effect들이 추적됨
UE_LOG(LogGAS, Log, TEXT("Prediction Key: %d"), PredictionKey.Current);
}
}
03
예측 범위
FScopedPredictionWindow
예측 범위 사용
// 예측 범위 내에서 Effect 적용
void UMyAbility::ApplyPredictedEffect()
{
UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
// 예측 범위 시작
{
FScopedPredictionWindow ScopedPrediction(ASC, true);
// 이 범위 내의 모든 Effect는 예측됨
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(
DamageEffectClass, GetAbilityLevel());
// 예측 키가 자동으로 연결됨
ApplyGameplayEffectSpecToTarget(
CurrentSpecHandle, SpecHandle, TargetData);
}
// 범위 종료 시 예측 키 정리
}
// 중첩 예측 범위
void UMyAbility::NestedPrediction()
{
FScopedPredictionWindow OuterScope(ASC, true);
// 외부 범위 Effect
ApplyEffect1();
{
// 새로운 Base 키로 내부 범위
FScopedPredictionWindow InnerScope(ASC, true);
ApplyEffect2(); // 다른 키로 추적
}
ApplyEffect3(); // 외부 키로 추적
}
예측 범위 중요성
FScopedPredictionWindow는 관련된 예측들을 그룹화합니다. 서버에서 하나라도 실패하면 같은 범위의 모든 예측이 롤백됩니다.
04
서버 검증과 롤백
Misprediction 처리
예측 확정과 롤백
// 서버에서 예측 확정
// 1. 클라이언트에서 예측한 Effect 적용
// 2. 서버에서 같은 어빌리티 실행
// 3. 서버가 Prediction Key를 확인(Confirm)
// 4. 클라이언트의 예측이 확정됨
// 예측 실패 시 롤백
// 1. 서버에서 어빌리티 실패 (조건 미충족 등)
// 2. 서버가 Prediction Key를 거부(Reject)
// 3. 클라이언트의 예측된 Effect 제거
// 롤백 처리 콜백
void UMyAbilitySystemComponent::OnPredictiveGameplayEffectRemoved(
const FActiveGameplayEffect& RemovedEffect)
{
// 예측 실패로 Effect가 제거됨
// UI 업데이트, 상태 복원 등
UE_LOG(LogGAS, Warning, TEXT("Predicted effect rolled back: %s"),
*RemovedEffect.Spec.Def->GetName());
}
// 어빌리티에서 예측 실패 감지
void UMyAbility::OnPredictionKeyRejected()
{
// 예측이 서버에서 거부됨
// 시각 효과 취소, 상태 복원
if (CurrentMontageTask)
{
CurrentMontageTask->EndTask();
}
}
05
예측 가능한 항목
무엇이 예측 가능한가
예측 가능/불가능 항목
// 예측 가능한 항목
// 1. 어빌리티 활성화
// 2. GameplayEffect 적용 (클라이언트 자신에게)
// 3. 어트리뷰트 변경 (자신)
// 4. GameplayCue 실행 (로컬)
// 5. 몽타주 재생
// 예측 불가능한 항목
// 1. 다른 플레이어에게 Effect 적용
// 2. 서버 권한 데이터 변경
// 3. 월드 오브젝트 스폰 (발사체 등은 별도 처리)
// 4. AI 관련 로직
// 발사체 예측 처리
void UMyProjectileAbility::SpawnPredictedProjectile()
{
// 발사체는 로컬에서 시뮬레이션
// 서버에서 실제 발사체 스폰
if (GetOwningActorFromActorInfo()->HasAuthority())
{
// 서버: 실제 발사체 스폰
AMyProjectile* Projectile = GetWorld()->SpawnActor<AMyProjectile>(...);
Projectile->SetReplicates(true);
}
else
{
// 클라이언트: 시뮬레이션용 발사체
AMyProjectile* FakeProjectile = GetWorld()->SpawnActor<AMyProjectile>(...);
FakeProjectile->SetReplicates(false);
FakeProjectile->bIsPredicted = true;
}
}
예측의 한계
다른 플레이어에게 직접 Effect를 적용하는 것은 예측할 수 없습니다. 이런 경우 서버에서만 처리하고, 시각적 피드백은 GameplayCue로 처리하세요.
SUMMARY
핵심 요약
- Prediction Key: 클라이언트 예측을 식별하고 추적
- FScopedPredictionWindow: 예측 범위 그룹화
- 서버 확정: 예측이 맞으면 유지, 틀리면 롤백
- 예측 가능: 자신에게 Effect, 어트리뷰트, Cue
- 예측 불가: 다른 플레이어, 서버 권한 데이터
PRACTICE
도전 과제
배운 내용을 직접 실습해보세요
실습 1: Prediction Key 관찰
LocalPredicted 어빌리티에서 FPredictionKey::CreateNewPredictionKey()가 생성하는 키를 로그로 추적하세요. 서버 확인/거부 시 키의 상태 변화를 관찰하세요.
실습 2: ScopedPredictionWindow 사용
FScopedPredictionWindow를 사용하여 하나의 어빌리티에서 여러 예측 GE를 단일 예측 키로 묶어 적용하세요. 롤백 시 모든 GE가 함께 제거되는지 확인하세요.
심화 과제
undefined