게임 개발 메모장
[ UE5 ] GameplayAbilitySystem : 심화 본문
전역 AbilitySystem
AbilitySystemGlobals 클래스는 GAS에 대한 전역 정보를 담고 있습니다. 대부분의 변수는 DefaultGame.ini에서 설정할 수 있습니다. 일반적으로 이 클래스와 상호작용할 필요는 없지만, 그 존재를 알고 있어야 합니다. GameplayCueManager 또는 GameplayEffectContext와 같은 것을 서브클래싱해야 하는 경우, AbilitySystemGlobals를 통해 서브클래싱해야 합니다.
AbilitySystemGlobals를 서브클래싱하려면 DefaultGame.ini에서 클래스 이름을 설정합니다:
[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"
InitGlobalData()
UE 4.24부터 TargetData를 사용하려면 UAbilitySystemGlobals::Get().InitGlobalData()를 호출해야 하며, 그렇지 않으면 스크립트 구조 캐시 관련 오류가 발생하고 클라이언트가 서버에서 연결이 끊어집니다. 이 함수는 프로젝트에서 한 번만 호출하면 됩니다. 포트나이트는 UAssetManager::StartInitialLoading()에서 호출하고, 파라곤은 UEngine::Init()에서 호출합니다. 샘플 프로젝트에 표시된 것처럼 UAssetManager::StartInitialLoading()에 넣는 것이 좋은 위치라는 것을 알았습니다. 이 상용구 코드를 프로젝트에 복사하여 TargetData 관련 문제를 방지해야 합니다.
AbilitySystemGlobals GlobalAttributeSetDefaultsTableNames를 사용하는 도중 크래시가 발생하는 경우, 나중에 포트나이트처럼 에셋 매니저나 게임 인스턴스에서 UAbilitySystemGlobals::Get().InitGlobalData()를 호출해야 할 수도 있습니다.
예측
GAS는 기본적으로 클라이언트 측 예측을 지원하지만 모든 것을 예측하지는 않습니다. GAS의 클라이언트 측 예측은 클라이언트가 GameplayAbility를 활성화하고 GameplayEffect를 적용하기 위해 서버의 허가를 기다릴 필요가 없음을 의미합니다. 이 작업을 수행할 권한을 부여하는 서버를 '예측'하고 GameplayEffect를 적용할 타깃을 예측할 수 있습니다. 그런 다음 서버는 클라이언트가 활성화된 후 GameplayAbility 네트워크 지연 시간을 실행하여 예측이 맞았는지 아닌지를 클라이언트에게 알려줍니다. 클라이언트의 예측이 틀린 경우, 클라이언트는 서버와 일치하도록 '잘못된 예측'에서 변경 사항을 '롤백'합니다.
GAS 관련 예측의 결정적인 소스는 플러그인 소스 코드의 GameplayPrediction.h입니다.
에픽의 사고방식은 "예측할 수 있는" 것만 예측하는 것입니다. 예를 들어, 파라곤과 포트나이트는 피해를 예측하지 않습니다. 어차피 예측할 수 없는 대미지를 위해 ExecutionCalculation를 사용하는 경우가 대부분입니다. 그렇다고 해서 피해량과 같은 특정 사항을 예측할 수 없다는 것은 아닙니다. 물론 그렇게 해서 잘 작동한다면 좋은 일입니다.
"모든 것을 완벽하게 자동으로 예측하는" 솔루션에 올인하는 것도 아닙니다. 저희는 여전히 플레이어 예측을 최소한으로 유지하는 것이 가장 좋다고 생각합니다(즉, 플레이어가 피할 수 있는 최소한의 것만 예측하는 것이 좋습니다).
새로운 Network Prediction Plugin에 대한 에픽 게임즈의 데이브 라티의 코멘트
예측되는 내용:
- Ability activation
- Triggered Events
- GameplayEffect application:Attribute modification (EXCEPTIONS: Executions do not currently predict, only attribute modifiers)GameplayTag modification
- Gameplay Cue events (both from within predictive gameplay effect and on their own)
- Montages
- Movement (built into UE5 UCharacterMovement)
예측할 수 없는 것:
- GameplayEffect removal
- GameplayEffect periodic effects (dots ticking)
From GameplayPrediction.h
GameplayEffect 적용은 예측할 수 있지만 GameplayEffect 제거는 예측할 수 없습니다. 이 제한을 해결할 수 있는 한 가지 방법은 GameplayEffect를 제거할 때 역효과를 예측하는 것입니다. 이동 속도가 40% 느려질 것으로 예측한다고 가정해 봅시다. 이동 속도 버프를 40%로 적용하여 예측적으로 제거할 수 있습니다. 그런 다음 두 GameplayEffect를 동시에 제거합니다. 이 방법이 모든 시나리오에 적합한 것은 아니며, GameplayEffect 제거 예측에 대한 지원이 여전히 필요합니다. 에픽 게임즈의 데이브 라티는 향후 GAS 반복작업에 이 기능을 추가하고 싶다는 의사를 표명했습니다.
GameplayEffect 제거를 예측할 수 없기 때문에 GameplayAbility 쿨타임을 완전히 예측할 수 없으며, 이에 대한 역 GameplayEffect 우회 방법도 없습니다. 서버의 리플리케이트된 쿨다운 GE는 클라이언트에 존재하며, 이를 우회하려는 시도(예: 최소 리플리케이션 모드)는 서버에 의해 거부됩니다. 즉, 지연 시간이 긴 클라이언트는 서버에 재사용 대기시간 시작을 알리고 서버의 재사용 대기시간 GE 제거를 받는 데 시간이 더 오래 걸립니다. 즉, 지연 시간이 긴 플레이어는 지연 시간이 짧은 플레이어보다 발사 속도가 낮아져 지연 시간이 짧은 플레이어보다 불리하게 됩니다. 포트나이트는 재사용 대기시간 GE 대신 사용자 지정 장부를 사용하여 이 문제를 방지합니다.
피해량 예측과 관련해서는, 대부분의 사람이 가스 게임을 시작할 때 가장 먼저 시도하는 방법 중 하나이지만 개인적으로 추천하지 않습니다. 특히 사망을 예측하는 것은 추천하지 않습니다. 피해를 예측할 수는 있지만 그렇게 하는 것은 까다롭습니다. 대미지를 잘못 예측하면 플레이어는 적의 체력이 다시 올라가는 것을 보게 됩니다. 특히 사망을 예측하려고 할 때 이런 상황은 매우 어색하고 답답할 수 있습니다. 캐릭터의 사망을 잘못 예측하여 래그돌링을 시작했는데 서버가 이를 수정하면 래그돌링을 멈추고 계속 총을 쏜다고 가정해 보세요.
💡Note: 속성을 변경하는 인스턴트 GameplayEffect(예: Cost GE)는 본인에 대해 원활하게 예측할 수 있으며, 다른 캐릭터에 대한 인스턴트 속성 변경을 예측하면 해당 캐릭터의 속성에 짧은 이상 현상 또는 '깜박임'이 표시됩니다. 예측된 인스턴트 GameplayEffect는 실제로 무한 GameplayEffect와 같이 취급되므로 예측이 잘못되었을 경우 롤백할 수 있습니다. 서버의 GameplayEffect가 적용될 때, 동일한 GameplayEffect가 두 개 존재하여 모디파이어가 두 번 적용되거나 잠시 동안 전혀 적용되지 않을 수 있습니다. 결국에는 저절로 수정되지만 때때로 플레이어가 이 문제를 알아차릴 수 있습니다.
GAS의 예측 구현이 해결하고자 하는 문제:
- "제가 해도 될까요?" 예측을 위한 기본 프로토콜.
- "실행 취소" 예측이 실패했을 때 부작용을 실행 취소하는 방법.
- "재실행" 로컬에서 예측했지만 서버에서도 복제되는 부작용이 다시 재생되지 않도록 하는 방법입니다.
- "완전성" 우리가 모든 부작용을/정말/예측했는지 확인하는 방법.
- "종속성" 종속 예측 및 예측 이벤트의 체인을 관리하는 방법.
- "재정의" 서버에서 복제/소유하는 상태를 예측적으로 재정의하는 방법입니다.
From GameplayPrediction.h
예측 키
GAS의 예측은 클라이언트가 GameplayAbility를 활성화할 때 생성하는 정수 식별자인 예측 키의 개념에 따라 작동합니다.
- 클라이언트는 GameplayAbility를 활성화할 때 예측 키를 생성합니다. 이것이 활성화 예측 키입니다.
- 클라이언트는 이 예측 키를 CallServerTryActivateAbility()를 통해 서버로 전송합니다.
- 클라이언트는 예측 키가 유효한 동안 이 예측 키를 적용하는 모든 GameplayEffect 에 이 예측 키를 추가합니다.
- 클라이언트의 예측 키가 범위를 벗어났습니다. 동일한 GameplayAbility에서 추가로 예측된 이펙트는 새로운 범위 예측 창이 필요합니다.
- 서버는 클라이언트로부터 예측 키를 받습니다.
- 서버는 이 예측 키를 적용하는 모든 GameplayEffect 에 추가합니다.
- 서버는 예측 키를 클라이언트에 다시 복제합니다.
- 클라이언트는 서버로부터 GameplayEffect 를 적용하는 데 사용된 예측 키와 함께 리플리케이트된 GameplayEffect를 받습니다. 리플리케이트된 GameplayEffect 중 클라이언트가 동일한 예측 키로 적용한 GameplayEffect와 일치하는 것이 있으면 올바르게 예측된 것입니다. 클라이언트가 예측된 GameplayEffect를 제거할 때까지 일시적으로 대상에 GameplayEffect 의 사본 두 개가 존재합니다.
- 클라이언트는 서버로부터 예측 키를 다시 받습니다. 이것이 복제된 예측 키입니다. 이 예측 키는 이제 부실 키로 표시됩니다.
- 클라이언트는 이제 오래된 리플리케이트된 예측 키로 생성한 모든 GameplayEffect를 제거합니다. 서버에 의해 리플리케이트된 GameplayEffect는 유지됩니다. 클라이언트가 추가한 GameplayEffect 중 서버에서 일치하는 리플리케이트 버전을 받지 못한 것은 잘못 예측된 것입니다.
예측 키는 활성화 예측 키에서 활성화로 시작하는 GameplayAbilities의 명령어 "창"을 원자적으로 그룹화하는 동안 유효하도록 보장됩니다. 이는 한 프레임 동안만 유효하다고 생각할 수 있습니다. 어빌리티 태스크에 새로운 범위 예측 창을 생성하는 내장 동기화 지점이 없는 한, 잠복 액션 어빌리티 태스크의 모든 콜백은 더 이상 유효한 예측 키를 갖지 못합니다.
어빌리티에서 새로운 예측 창 만들기
AbilityTask의 콜백에서 더 많은 작업을 예측하려면 새 범위 예측 키를 사용하여 새 범위 예측 창을 만들어야 합니다. 이를 클라이언트와 서버 간의 동기화 지점이라고도 합니다. 모든 입력 관련 어빌리티 태스크와 같은 일부 어빌리티 태스크에는 새 범위 예측 창을 생성하는 기능이 내장되어 있으므로 어빌리티 태스크 콜백의 원자 코드에 사용할 수 있는 유효한 범위 예측 키가 있습니다.
WaitDelay 작업과 같은 다른 작업에는 콜백에 대해 범위가 지정된 새 예측 창을 만드는 내장 코드가 없습니다. WaitDelay와 같이 범위가 지정된 예측 창을 생성하는 코드가 내장되어 있지 않은 AbilityTask 이후의 작업을 예측해야 하는 경우, OnlyServerWait 옵션과 함께 WaitNetSync AbilityTask를 사용하여 수동으로 수행해야 합니다. 클라이언트가 OnlyServerWait 옵션이 있는 WaitNetSync에 도달하면, GameplayAbility의 활성화 예측 키를 기반으로 새 범위 지정 예측 키를 생성하고 서버에 RPC한 다음 적용하는 모든 새 GameplayEffect에 이를 추가합니다.
서버가 OnlyServerWait로 WaitNetSync에 도달하면 클라이언트에서 범위가 지정된 새 예측 키를 받을 때까지 기다렸다가 계속 진행합니다. 이 범위 지정 예측 키는 활성화 예측 키와 동일한 동작을 수행하며, GameplayEffect에 적용되고 클라이언트에 다시 리플리케이트되어 오래된 것으로 표시됩니다. 범위가 지정된 예측 키는 범위를 벗어날 때까지, 즉 범위가 지정된 예측 창이 닫힐 때까지 유효합니다. 따라서 다시 말하지만, 잠재적 연산이 아닌 원자 연산만 t를 사용할 수 있습니다.
액터 예측 스폰
클라이언트에서 액터를 예측적으로 스폰하는 것은 고급 주제입니다. GAS는 이를 바로 처리하는 기능을 제공하지 않습니다(SpawnActor AbilityTask는 서버에만 액터를 스폰합니다). 핵심 개념은 리플리케이트된 액터를 클라이언트와 서버 양쪽에서 스폰하는 것입니다.
액터가 단지 장식용이거나 게임 플레이 목적이 없는 경우, 간단한 해결책은 서버가 소유 클라이언트로 리플리케이트하지 못하도록 액터의 IsNetRelevantFor() 함수를 오버라이드하는 것입니다. 소유 클라이언트는 로컬에서 스폰된 버전을, 서버와 다른 클라이언트는 서버의 리플리케이트 버전을 갖게 됩니다.
bool APAReplicatedActorExceptOwner::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
return !IsOwnedBy(ViewTarget);
}
스폰된 액터가 대미지를 예측해야 하는 발사체처럼 게임 플레이에 영향을 끼치는 경우, 이 문서의 범위를 벗어난 고급 로직이 필요합니다. 에픽게임즈 깃허브에서 언리얼 토너먼트가 발사체를 예측 스폰하는 방법을 살펴보세요. 여기에는 서버의 리플리케이트된 프로젝타일과 동기화되는 더미 프로젝타일이 소유 클라이언트에서만 스폰됩니다.
Future of Prediction in GAS
GameplayPrediction.h에 따르면 향후 GameplayEffect 제거 및 주기적 GameplayEffect 예측 기능을 추가할 수 있다고 합니다.
에픽 게임즈의 데이브 라티는 재사용 대기 시간을 예측할 때 지연 시간이 긴 플레이어가 짧은 플레이어보다 불이익을 받는 지연 시간 조정 문제를 해결하는 데 관심을 표명했습니다.
에픽 게임즈의 새로운 네트워크 예측 플러그인은 이전의 CharacterMovementComponent와 마찬가지로 GAS와 완전히 상호 운용될 것으로 예상됩니다.
Network Prediction Plugin
에픽 게임즈는 최근 캐릭터 무브먼트 컴포넌트를 새로운 네트워크 예측 플러그인으로 대체하는 이니셔티브를 시작했습니다. 이 플러그인은 아직 초기 단계에 있지만 언리얼 엔진 깃허브에서 얼리 액세스로 이용할 수 있습니다. 향후 언리얼 엔진의 어떤 버전에서 실험적 베타 버전으로 출시될지는 아직 알 수 없습니다.
Targeting
Target Data
FGameplayAbilityTargetData는 네트워크를 통해 전달되는 Target Data를 위한 일반 구조체입니다. Target Data에는 보통 AActor/UObject 레퍼런스, FHitResults, 기타 일반 위치/방향/원점 정보가 들어갑니다. 하지만 서브클래스를 통해 클라이언트와 서버 간 데이터 전달을 위한 간단한 수단으로 원하는 것은 무엇이든 그 안에 넣을 수 있습니다. 베이스 구조체 FGameplayAbilityTargetData는 직접 사용하는 것이 아니라 서브클래싱해야 합니다. GAS 에는 GameplayAbilityTargetTypes.h에 몇 개의 서브클래싱된 FGameplayAbilityTargetData 구조체가 기본적으로 제공됩니다.
TargetData는 일반적으로 TargetActor에서 생산하거나 ActivityTasks 및 GameplayEffects에서 수동으로 생성하여 EffectContext를 통해 소비합니다. EffectContext에 포함된 결과 Execution, MMC, GameplayCue, AttributeSet에서 대상 데이터에 액세스할 수 있습니다.
일반적으로 FGamePlayAbilityTargetData를 직접 전달하지 않고, 대신 FGamePlayAbilityTargetData에 대한 포인터의 내부 TRAray가 있는 FGamePlayAbilityTargetDataHandle를 사용합니다. 이 중간 구조는 타겟 데이터의 다형성을 지원합니다.
FGameplayAbilityTargetData에서 상속하는 예제입니다:
USTRUCT(BlueprintType)
struct MYGAME_API FGameplayAbilityTargetData_CustomData : public FGameplayAbilityTargetData
{
GENERATED_BODY()
public:
FGameplayAbilityTargetData_CustomData()
{ }
UPROPERTY()
FName CoolName = NAME_None;
UPROPERTY()
FPredictionKey MyCoolPredictionKey;
// This is required for all child structs of FGameplayAbilityTargetData
virtual UScriptStruct* GetScriptStruct() const override
{
return FGameplayAbilityTargetData_CustomData::StaticStruct();
}
// This is required for all child structs of FGameplayAbilityTargetData
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
// The engine already defined NetSerialize for FName & FPredictionKey, thanks Epic!
CoolName.NetSerialize(Ar, Map, bOutSuccess);
MyCoolPredictionKey.NetSerialize(Ar, Map, bOutSuccess);
bOutSuccess = true;
return true;
}
}
template<>
struct TStructOpsTypeTraits<FGameplayAbilityTargetData_CustomData> : public TStructOpsTypeTraitsBase2<FGameplayAbilityTargetData_CustomData>
{
enum
{
WithNetSerializer = true // This is REQUIRED for FGameplayAbilityTargetDataHandle net serialization to work
};
};
대상 데이터를 핸들에 추가합니다:
UFUNCTION(BlueprintPure)
FGameplayAbilityTargetDataHandle MakeTargetDataFromCustomName(const FName CustomName)
{
// Create our target data type,
// Handle's automatically cleanup and delete this data when the handle is destructed,
// if you don't add this to a handle then be careful because this deals with memory management and memory leaks so its safe to just always add it to a handle at some point in the frame!
FGameplayAbilityTargetData_CustomData* MyCustomData = new FGameplayAbilityTargetData_CustomData();
// Setup the struct's information to use the inputted name and any other changes we may want to do
MyCustomData->CoolName = CustomName;
// Make our handle wrapper for Blueprint usage
FGameplayAbilityTargetDataHandle Handle;
// Add the target data to our handle
Handle.Add(MyCustomData);
// Output our handle to Blueprint
return Handle;
}
값을 가져오려면 타입 안전 검사를 수행해야 하는데, 핸들의 대상 데이터에서 값을 가져오는 유일한 방법은 타입 안전하지 않은 일반 C/C++ 캐스팅을 사용하는 것이므로 객체 조각화 및 충돌을 일으킬 수 있습니다. 타입 검사를 수행하는 방법에는 여러 가지가 있지만(정직하게 원하는 대로) 두 가지 일반적인 방법이 있습니다:
- GameplayTag: 서브클래스 계층구조를 사용하면 특정 코드 아키텍처의 기능이 발생할 때마다 기본 부모 유형에 대해 캐스팅하고 해당 GameplayTag를 가져온 다음 상속된 클래스에 대한 캐스팅을 위해 이를 비교할 수 있습니다.
- 스크립트 구조체 & 정적 구조체: 대신 직접 클래스 비교를 할 수도 있습니다(if문을 많이 사용하거나 템플릿 함수를 만들 수도 있습니다). 아래는 이를 수행하는 예제이지만, 기본적으로 FGameplayAbilityTargetData에서 스크립트 구조체를 가져와서(USTRUCT이고 상속된 클래스가 GetScriptStruct 에 구조체 유형을 지정해야 한다는 장점이 있습니다) 원하는 유형인지 비교하면 됩니다. 아래는 유형 검사에 이러한 함수를 사용하는 예제입니다:
UFUNCTION(BlueprintPure)
FName GetCoolNameFromTargetData(const FGameplayAbilityTargetDataHandle& Handle, const int Index)
{
// NOTE, there is two versions of this '::Get(int32 Index)' function;
// 1) const version that returns 'const FGameplayAbilityTargetData*', good for reading target data values
// 2) non-const version that returns 'FGameplayAbilityTargetData*', good for modifying target data values
FGameplayAbilityTargetData* Data = Handle.Get(Index); // This will valid check the index for you
// Valid check we have something to use, null data means nothing to cast for
if (Data == nullptr)
{
return NAME_None;
}
// This is basically the type checking pass, static_cast does not have type safety, this is why we do this check.
// If we don't do this then it will object slice the struct and thus we have no way of making sure its that type.
if (Data->GetScriptStruct() == FGameplayAbilityTargetData_CustomData::StaticStruct())
{
// Here is when you would do the cast because we know its the correct type already
FGameplayAbilityTargetData_CustomData* CustomData = static_cast<FGameplayAbilityTargetData_CustomData*>(Data);
return CustomData->CoolName;
}
return NAME_None;
}
Target Actor
GameplayAbility는 TargetActor를 WaitTargetData AbilityTask와 함께 스폰하여 월드의 타겟 정보를 시각화 및 캡처합니다. TargetActor는 선택적으로 GameplayAbilityWorldReticle를 사용하여 현재 타겟을 표시할 수 있습니다. 확인되면 타깃 정보는 TargetData로 반환되어 GameplayEffect에 전달할 수 있습니다.
TargetActor는 AActor를 기반으로 하므로 스태틱 메시나 데칼 등 타깃팅하는 위치와 방법을 나타내는 모든 종류의 보이는 컴포넌트를 가질 수 있습니다. 스태틱 메시는 캐릭터가 만들 오브젝트의 배치를 시각화하는 데 사용할 수 있습니다. 데칼은 지면에 효과 영역을 표시하는 데 사용할 수 있습니다. 샘플 프로젝트에서는 지면에 데칼이 있는 AGameplayAbilityTargetActor_GroundTrace를 사용하여 메테오 어빌리티의 피해 영역을 나타냅니다. 또한 아무것도 표시하지 않아도 됩니다. 예를 들어 GASShooter에서 사용되는 것처럼 목표물에 즉시 선을 추적하는 히트스캔 총에 아무 것도 표시하지 않는 것이 좋습니다.
기본 트레이스 또는 콜리전 오버랩을 사용하여 타깃 정보를 캡처하고 그 결과를 TargetActor 구현에 따라 FHitResults 또는 AActor 배열로 타깃 데이터로 변환합니다. WaitTargetData AbilityTask는 TEnumAsByte<EGameplayTargetingConfirmation::Type> ConfirmationType 파라미터를 통해 타깃이 확인되는 시기를 결정합니다. TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant 를 사용하지 않는 경우, 타깃 액터는 일반적으로 Tick() 에서 트레이스/오버랩을 수행한 다음 구현에 따라 그 위치를 FHitResult 에 업데이트합니다. Tick() 에서 트레이스/오버랩을 수행하지만, 리플리케이트되지 않고 일반적으로 한 번에 하나 이상의 (더 많은) 타깃 액터를 실행하지 않기 때문에 일반적으로 나쁘지 않습니다. 다만 Tick() 를 사용하며, 일부 복잡한 타깃 액터는 GASShooter 의 로켓 발사기의 보조 어빌리티처럼 많은 작업을 수행할 수 있다는 점만 유의하세요. Tick()의 트레이싱은 클라이언트에 매우 반응이 좋지만, 퍼포먼스 타격이 너무 크면 타깃 액터의 틱 속도를 낮추는 것을 고려할 수 있습니다. TEnumAsByte<EGameplayTargetingConfirmation::Type::Instant 의 경우, 타깃 액터는 즉시 스폰되어 타깃 데이터를 생성하고 소멸합니다. Tick() 은 호출되지 않습니다.
EGameplayTargetingConfirmation::Type | 대상이 확인된 경우 |
Instant | 타겟팅은 '실행' 시점을 결정하는 특별한 로직이나 사용자 입력 없이 즉시 이루어집니다. |
UserConfirmed | 타깃팅은 사용자가 또는 UAbilitySystemComponent::TargetConfirm()을 호출하여 타깃팅을 확인하면 발생합니다. 타깃 액터는 바인딩된 취소 입력에 응답하거나 UAbilitySystemComponent::TargetCancel()을 호출하여 타깃팅을 취소할 수도 있습니다. |
Custom | GameplayTargeting Ability는 UGameplayAbility::ConfirmTaskByInstanceName() 을 호출하여 타깃팅 데이터가 언제 준비되었는지 결정하는 역할을 합니다. 타깃 액터는 또한 UGameplayAbility::CancelTaskByInstanceName() 에 응답하여 타깃팅을 취소합니다. |
CustomMulti | GameplayTargeting Ability는 UGameplayAbility::ConfirmTaskByInstanceName() 을 호출하여 타깃팅 데이터가 언제 준비되었는지 결정하는 역할을 합니다. 타깃 액터는 또한 UGameplayAbility::CancelTaskByInstanceName() 에 응답하여 타깃팅을 취소합니다. 데이터 생성 시 어빌리티 태스크를 종료해서는 안 됩니다. |
모든 타깃 액터가 모든 EGameplayTargetingConfirmation::Type 을 지원하지는 않습니다. 예를 들어, AGameplayAabilityTargetActor_GroundTrace는 즉시 확인을 지원하지 않습니다.
WaitTargetData AbilityTask는 AGameplayAbilityTargetActor 클래스를 파라미터로 받아 AbilityTask가 활성화될 때마다 인스턴스를 스폰하고, AbilityTask가 종료되면 타깃 액터를 소멸시킵니다. WaitTargetDataUsingActor AbilityTask는 이미 스폰된 타깃 액터를 받지만, AbilityTask가 종료되면 여전히 소멸시킵니다. 이 두 AbilityTask는 모두 스폰되거나 사용할 때마다 새로 스폰된 타겟 액터가 필요하다는 점에서 비효율적입니다. 프로토타이핑에는 좋지만, 프로덕션에서는 자동 소총의 경우처럼 타깃 데이터를 지속적으로 생성하는 경우가 있다면 최적화를 고려해 볼 수 있습니다. GASShooter에는 커스텀 서브클래스인 AGameplayAbilityTargetActor와 새로운 WaitTargetDataWithReusableActor가 있습니다. AbilityTask를 처음부터 새로 작성하여 타겟 액터를 파괴하지 않고 재사용할 수 있습니다.
Target Actor는 기본적으로 리플리케이트되지 않지만, 게임에서 로컬 플레이어가 타겟팅하는 위치를 다른 플레이어에게 보여주기 위해 필요하다면 리플리케이트되도록 만들 수 있습니다. 여기에는 WaitTargetData AbilityTask 의 RPC 를 통해 서버와 통신하는 기본 기능이 포함되어 있습니다. 타겟 액터의 ShouldProduceTargetDataOnServer 프로퍼티가 false로 설정되어 있으면, 클라이언트는 UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()의 CallServerSetReplicatedTargetData() 를 통해 확인 시 타겟 데이터를 서버에 RPC 합니다. ShouldProduceTargetDataOnServer 가 참이면 클라이언트는 일반 확인 이벤트인 EAbilityGenericReplicatedEvent::GenericConfirm, RPC 를 UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()에서 서버로 전송하고 서버는 RPC 를 수신하면 추적 또는 오버랩 검사를 수행하여 서버에서 데이터를 생성합니다. 클라이언트가 타깃팅을 취소하면 일반 취소 이벤트인 EAbilityGenericReplicatedEvent::GenericCancel, RPC를 UAbilityTask_WaitTargetData::OnTargetDataCancelledCallback에서 서버로 전송합니다. 보시다시피 타겟 액터와 WaitTargetData AbilityTask 모두에 많은 델리게이트가 있습니다. 타깃 액터는 입력에 반응하여 델리게이트 준비, 확인 또는 취소를 위한 타깃 데이터를 생성 및 브로드캐스트합니다. WaitTargetData는 타겟 액터의 타깃 데이터 준비, 확인, 취소 델리게이트를 수신하고 해당 정보를 다시 GameplayAbility와 서버에 전달합니다. 서버에 타깃데이터를 전송하는 경우, 서버에서 유효성 검사를 수행하여 타깃데이터가 타당한지 확인하여 부정행위를 방지할 수 있습니다. 타깃데이터를 서버에서 직접 생성하면 이 문제를 완전히 피할 수 있지만, 소유 클라이언트가 잘못된 예측을 할 가능성이 있습니다.
사용하는 AGameplayAbilityTargetActor의 특정 서브클래스에 따라, WaitTargetData AbilityTask 노드에 노출되는 ExposeOnSpawn 파라미터가 달라집니다. 몇 가지 일반적인 파라미터는 다음과 같습니다:
Common TargetActor Parameter | 내용 |
Debug | True 면, 타깃 액터가 출시되지 않은 빌드에서 트레이스를 수행할 때마다 디버그 트레이싱/중첩 정보를 그립니다. 인스턴트 타깃 액터가 아닌 타깃 액터는 Tick() 에서 트레이스를 수행하므로 이러한 디버그 드로 콜은 Tick() 에서도 발생합니다. |
Filter | [옵션] 트레이스/중첩이 발생할 때 타깃에서 액터를 필터링(제거)하기 위한 특수 구조체입니다. 일반적인 사용 사례는 플레이어의 폰을 필터링하거나 타깃이 특정 클래스여야 하는 경우입니다. 더 고급 사용 사례는 Target Data Filter을 참조하세요. |
Reticle Class | [옵션] 타깃 액터가 스폰할 AGameplayAbilityWorldReticle 의 서브클래스입니다. |
Reticle Parameters | [선택 사항] 레티클을 구성합니다. 레티클을 참조하세요. |
Start Location | 트래킹을 시작할 위치에 대한 특수 구조체입니다. 일반적으로 플레이어의 시점, 무기 총구 또는 폰의 위치가 됩니다. |
기본 타겟 액터 클래스를 사용하면 액터가 트레이스/오버랩에 직접 있을 때만 유효한 타깃이 됩니다. 트레이스/오버랩을 벗어나면(움직이거나 한눈을 팔면) 더 이상 유효하지 않습니다. 타겟 액터가 마지막으로 유효한 타깃을 기억하도록 하려면 커스텀 타깃 액터 클래스에 이 기능을 추가해야 합니다. 이를 퍼시스턴트 타깃이라고 부르는 이유는 타겟 액터가 확인 또는 취소를 받거나, 타겟 액터가 트레이스/오버랩에서 새로운 유효한 타깃을 찾거나, 타깃이 더 이상 유효하지 않게(소멸) 될 때까지 지속되기 때문입니다. GASShooter는 로켓 발사기의 보조 능력인 호밍 로켓 조준에 퍼시스턴트 타깃을 사용합니다.
Target Data Filter
Make GameplayTargetDataFilter와 Make Filter Handle 노드를 모두 사용하여 플레이어의 폰을 필터링하거나 특정 클래스만 선택할 수 있습니다. 고급 필터링이 필요한 경우, FGameplayTargetDataFilter 를 서브클래싱하고 FilterPassesForActor 함수를 오버라이드하면 됩니다.
USTRUCT(BlueprintType)
struct GASDOCUMENTATION_API FGDNameTargetDataFilter : public FGameplayTargetDataFilter
{
GENERATED_BODY()
/** Returns true if the actor passes the filter and will be targeted */
virtual bool FilterPassesForActor(const AActor* ActorToBeFiltered) const override;
};
하지만 대기 타깃 데이터 노드에 바로 적용되지는 않는데, FGameplayTargetDataFilterHandle 이 필요하기 때문입니다. 서브클래스를 받으려면 새로운 커스텀 Make Filter Handle 을 만들어야 합니다:
FGameplayTargetDataFilterHandle UGDTargetDataFilterBlueprintLibrary::MakeGDNameFilterHandle(FGDNameTargetDataFilter Filter, AActor* FilterActor)
{
FGameplayTargetDataFilter* NewFilter = new FGDNameTargetDataFilter(Filter);
NewFilter->InitializeFilterContext(FilterActor);
FGameplayTargetDataFilterHandle FilterHandle;
FilterHandle.Filter = TSharedPtr<FGameplayTargetDataFilter>(NewFilter);
return FilterHandle;
}
Gameplay Ability World Reticle
AGameplayAbilityWorldReticle(레티클)은 즉시 확인되지 않은 TargetActor로 타겟팅할 때 타겟팅하는 대상을 시각화합니다. 타깃 액터는 모든 레티클의 생성 및 소멸 수명을 담당합니다. 레티클은 AActor이므로 모든 종류의 비주얼 컴포넌트를 사용하여 표현할 수 있습니다. GASShooter에서 볼 수 있는 일반적인 구현은 위젯 컴포넌트를 사용하여 화면 공간에 (항상 플레이어의 카메라를 향하도록) UMG 위젯을 표시하는 것입니다. 레티클은 어느 AActor 에 있는지 모르지만, 커스텀 타깃 액터에서 그 함수성을 서브클래싱할 수 있습니다. 타깃 액터는 일반적으로 매 Tick() 때마다 레티클의 위치를 타깃의 위치로 업데이트합니다.
GASShooter는 레티클을 사용하여 로켓 발사기의 보조 능력인 호밍 로켓의 고정된 타깃을 표시합니다. 적의 빨간색 표시기는 레티클입니다. 비슷한 흰색 이미지는 로켓 발사기의 십자선입니다.
레티클에는 디자이너를 위한 (블루프린트에서 개발하도록 되어 있는) 블루프린트 구현가능 이벤트가 몇 개 포함되어 있습니다:
/** Called whenever bIsTargetValid changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnValidTargetChanged(bool bNewValue);
/** Called whenever bIsTargetAnActor changes value. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnTargetingAnActor(bool bNewValue);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnParametersInitialized();
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamFloat(FName ParamName, float value);
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void SetReticleMaterialParamVector(FName ParamName, FVector value);
레티클은 타깃 액터에서 제공하는 FWorldReticleParameter를 선택적으로 사용하여 구성할 수 있습니다. 기본 구조체는 FVector AOEScale 변수 하나만 제공합니다. 기술적으로 이 구조체를 서브클래싱할 수는 있지만, TargetActor는 기본 구조체만 받습니다. 기본 타겟 액터로 서브클래싱할 수 없도록 하는 것은 다소 근시안적인 것 같습니다. 하지만 커스텀 타겟 액터를 직접 만드는 경우, 커스텀 레티클 파라미터 구조체를 제공한 다음 스폰할 때 AGameplayAbilityWorldReticles의 서브클래스에 수동으로 전달할 수 있습니다.
레티클은 기본적으로 리플리케이트되지 않지만, 로컬 플레이어가 타겟팅하는 다른 플레이어를 표시하는 것이 게임에 적합하다면 리플리케이트되도록 할 수 있습니다.
레티클은 현재 유효한 타겟에 기본 타깃액터가 있는 경우에만 표시됩니다. 예를 들어 타겟을 추적하는 데 AGameplayAbilityTargetActor_SingleLineTrace를 사용하는 경우, 레티클은 적이 추적 경로에 바로 있을 때만 나타납니다. 한눈을 팔면 적은 더 이상 유효한 타깃이 아니며 레티클이 사라집니다. 레티클이 마지막 유효한 타겟에 계속 표시되도록 하려면, 타겟 액터를 커스터마이징하여 마지막 유효한 타깃을 기억하고 레티클을 계속 표시하도록 하면 됩니다. 이를 퍼시스턴트 타겟이라고 부르는 이유는 타겟 액터가 확인 또는 취소를 받거나, 타겟 액터가 추적/중첩에서 새로운 유효한 타깃을 찾거나, 타겟이 더 이상 유효하지 않게(소멸) 될 때까지 지속되기 때문입니다. GASShooter는 로켓 발사기의 보조 능력인 호밍 로켓 조준에 퍼시스턴트 타겟을 사용합니다.
Gameplay Effect Containers Targeting
GameplayEffectContainer에는 TargetData를 생성하는 효율적인 선택적 수단이 제공됩니다. 이 타겟팅은 클라이언트와 서버에서 EffectContainer가 적용될 때 즉시 이루어집니다. 타겟팅 오브젝트의 CDO에서 실행되므로(액터 스폰 및 소멸이 없음) TargetActor보다 효율적이지만, 플레이어 입력이 부족하고 확인 없이 즉시 발생하며 취소할 수 없고 클라이언트에서 서버로 데이터를 전송할 수 없습니다(양쪽에서 데이터를 생성합니다). 인스턴트 트레이스와 콜리전 오버랩에 잘 작동합니다. 에픽의 액션 RPG 샘플 프로젝트에는 컨테이너를 사용한 타겟팅의 두 가지 예제 유형, 즉 어빌리티 소유자를 타겟팅하는 것과 이벤트에서 타깃 데이터를 가져오는 것이 포함되어 있습니다. 또한 블루프린트에서 플레이어로부터 약간의 오프셋(자손 블루프린트 클래스에서 설정)을 두고 인스턴트 구체 트레이스를 수행하는 것도 구현되어 있습니다. C++ 또는 블루프린트에서 URPGTargetType을 서브클래싱하여 자신만의 타깃팅 유형을 만들 수 있습니다.
'언리얼 엔진 > 기능' 카테고리의 다른 글
[UE5] Check-In/Out (0) | 2024.03.16 |
---|---|
[ UE5 ] Input(입력) 을 Gameplay Ability System을 활용 (0) | 2024.01.15 |
[ UE5 ] GameplayAbilitySystem : GameplayCue (0) | 2024.01.10 |
[ UE5 ] GameplayAbilitySystem : AbilityTask (0) | 2024.01.10 |
[ UE5 ] GameplayAbilitySystem : Gameplay Ability (0) | 2024.01.10 |