글의 목적
포션을 사용할 때 매 번 서버에게 확인 받고 사용하게 되면 딜레이가 생기게 되고, 이는 좋지 못한 플레이 경험이 되는데, 이를 보완하기 위한 Prediction에 대해서 학습하고 어떻게 작동하는지 탐구하기 위해 작성합니다.
배운 점
GameplayAbilitySystem에서 클라이언트 - 서버 간 Prediction에 대해서 학습했습니다. FPredictionKey를 사용해 예측할 행동과 그로 인해 생기는 side effect들 모두에 대해서 롤백이 가능하다는 걸 깨닫게 되었습니다.
생각해 볼만한 주제
만약 Ability X가 활성화 되었고, X가 곧바로 이벤트를 트리거해서 Ability Y가 활성화 되었고 Y가 이벤트를 트리거해서 Ability Z가 활성화 되었다고 해봅시다. 종속성 관계는 X→Y→Z 입니다. 그리고 이 모두는 롤백이 필요합니다. Y가 거절되면 Z는 절대로 생기지 않아야 합니다. 그러나 서버는 Z를 시도한 적 조차 없기 때문에 서버에서는 “Z는 실행되면 안된다” 라는 것을 결정하지 못합니다. 즉 아이러니하게 생겨선 안되는 이벤트를 서버에서 검사해야되는 상황이 생깁니다. 어떻게 이것을 해결해야할까요?
Gameplay Ability Prediction 개요
목표
- GameplayAbility를 구현할 때
*GameplayAbility level
의 Prediction은 자동으로 처리됩니다. 능력은 "X -> Y -> Z를 실행"한다고 명시하면, 가능한 부분을 자동으로 예측합니다.
- GameplayAbility 코드에서 "권한이 있다면 X 실행, 아니면 예측된 X 실행"과 같은 로직을 피하고자 합니다.
- 현재까지는 모든 사례가 해결된 것은 아니지만, 클라이언트 측 Prediction을 위한 견고한 프레임워크를 제공합니다.
클라이언트 측 Prediction 정의
- 클라이언트가 게임 시뮬레이션 상태를 예측하는 것입니다. 예를 들어, 주문 시 마나가 100에서 90으로 줄어드는 예측은 클라이언트 측 예측입니다.
- 발자국 소리처럼 완전히 클라이언트에서만 처리되는 요소는 이 시스템과 상관없습니다.
현재 예측되는 요소
- 초기 GameplayAbility 활성화 (체인 활성화 포함)
- 트리거된 이벤트
- GameplayEffect 적용:
- 속성(Attribute) 수정 (단, 실행(Execution)은 예측되지 않음. 오직
attribute modifiers
만 예측)
- GameplayTag 수정
- Gameplay Cue 이벤트
- 몽타주
- 움직임 (UE의
UCharacterMovement
내장)
현재 예측되지 않는 요소
- GameplayEffect 제거
- 주기적인 GameplayEffect 효과 (예: 지속 피해)
에픽에서 정의한 해결하려는 문제
- "이것을 실행할 수 있는가?" - 기본 예측 프로토콜
- "되돌리기" - 예측이 실패했을 때 side effects을 되돌리는 방법
- "다시 실행" - 로컬에서 예측한 side effects가 서버에서 복제될 때 중복 실행 방지
- "완결성" - 모든 side effects를 예측했는지 확인
- "종속성" - 예측된 이벤트와 그 체인을 관리
- "상태 재정의" - 서버가 소유한 상태를 예측적으로 재정의
구현 세부 사항
FPredictionKey
- 예측 키는 클라이언트에서 생성된 고유 ID입니다. 클라이언트는 이 키를 서버로 보내 예측적 행동과 side effects을 연관시킵니다.
- 서버는 이 예측 키를 승인하거나 거부하며, 서버에서 생성된 side effects와 키를 연관시킵니다.
- 클라이언트 -> 서버로는 항상 복제되지만, 서버 -> 클라이언트로는 예측 키를 보낸 클라이언트에만 복제됩니다.
능력 활성화
- 능력 활성화는 주요 예측 작업입니다. 초기 예측 키를 생성하고 서버와 클라이언트 간에 통신합니다.
- 능력 활성화 중에 발생하는 side effects는 예측 키와 연결됩니다.
- 클라이언트는
TryActivateAbility -> ServerTryActivateAbility -> ClientActivateAbility
로 서버와 통신합니다.
GameplayEffect Prediction
- GameplayEffect는 Ability Actication의 side effects로 간주되어 별도의 승인/거부 과정이 없습니다.
- Attribute, GameplayCue, GameplayTag는 예측된 GameplayEffect와 함께 예측됩니다.
- 서버는 클라이언트에서 생성된 예측 키를 확인하고 중복 실행을 방지합니다.
- 즉 클라이언트에서 빨간 포션 1개를 소모했다면, 이게 네트워크 장애나 다른 이유로 여러번 서버에게 알려준다고 해도 PredictionKey로 예측하기 때문에 여러 번 실행되지 않습니다.
Attribute Prediction
- Attribute는 일반적인 UProperty로 복제되기 때문에 예측 수정이 어렵습니다.
- Attribute 예측은 절대 값이 아닌 변화량(Delta) 예측으로 처리합니다. 예: 마나가 -10 감소하는 예측.
void UMyHealthSet::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION_NOTIFY(UMyHealthSet, Health, COND_None, REPNOTIFY_Always);
}
void UMyHealthSet::OnRep_Health()
{
GAMEPLAYATTRIBUTE_REPNOTIFY(UMyHealthSet, Health);
}
Gameplay Cue Events
- GameplayEffects와 별도로 독립적으로 실행될 수 있습니다. 예측 키를 고려하여 클라이언트와 서버 간 이벤트가 처리됩니다.
생각해볼만한 주제의 답
- 종속성 문제
- 글의 처음에서 말씀드렸던 종속성의 문제에 대한 해결책입니다.
- Base PredictionKey라는 컨셉이 필요합니다. TryActivateAbility를 실행할때 현재 PredictionKey를 서버에 전달합니다. 이 PredicitonKey는 새롭게 생성된 PredictionKey의 Base가 됩니다. 즉 이런 방식으로 Y가 거절되면 Z는 자동적으로 invalidate 됩니다. 다시말해 X는 Y와 Z의 Base Key가 되고 Y→Z 로의 종속성은 완전하게 Client에서 처리합니다. Y가 거절 혹은 승인되었을 때 Z를 거절, 승인 하는 델리게이트를 만들어 클라이언트 측에서 해결합니다.
- 이렇게 되면 클라이언트 쪽에서 종속성을 해결하기 때문에 서버는 이 행동들이 이전에 거절되었는지 판단하지 못합니다. 즉 Combo1 → Combo2 이런 종속성이 있다고 할 때, 서버측에서 Combo1에 대한 거절은 Combo2가 일어났을 때 입니다. 이를 보완하기 위해 GameplayTag를 사용해서 특정 GameplayTag가 성공적으로 활성화 되었을 때만 이벤트를 실행되도록 하면됩니다.