본문 바로가기
둥지/Unreal

Unreal GAS(GameplayAbilitySystem) Documentation 번역글 4부

by 까닭 2023. 6. 25.

4.7 AbilityTask

4.7.1 AbilityTask 정의

GameplayAbility는 한 프레임에서만 실행됩니다. 이는 그 자체로는 많은 유연성을 허용하지 않습니다. 시간이 지남에 따라 발생하거나 나중에 특정 시점에 발동된 델리게이트에 응답해야 하는 액션을 수행하려면 AbilityTask라는 잠재 액션을 사용합니다.

 

GAS에는 다양한 AbilityTask가 기본으로 제공됩니다:

  • 루트모션 소스로 캐릭터를 이동하는 작업
  • 애니메이션 몽타주 재생 작업
  • 어트리뷰트 변경에 대응하기 위한 작업
  • GameplayEffect 변경에 대응하기 위한 작업
  • 플레이어 입력에 응답하기 위한 작업
  • 그 외

UAbilityTask 생성자는 게임 전체에 하드코딩된 최대 1,000개의 AbilityTask 동시 실행을 강제합니다. RTS 게임처럼 월드에 수백 명의 캐릭터가 동시에 등장할 수 있는 게임의 GameplayAbility를 디자인할 때는 이 점을 염두에 두어야 합니다.

 

4.7.2 커스텀 AbilityTask

종종 커스텀 AbilityTask(C++)를 직접 만들게 될 것입니다. 

 

샘플 프로젝트에는 두 개의 커스텀 AbilityTask가 포함되어 있습니다:

  1. PlayMontageAndWaitForEvent는 기본 PlayMontageAndWait와 WaitGameplayEvent AbilityTasks의 조합입니다. 이를 통해 애니메이션 몽타주가 애니메이션 노티파이에서 GameplayEvent를 시작했던 GameplayAbility로 다시 전송할 수 있습니다. 이를 사용하여 애니메이션 몽타주 도중 특정 시간에 액션을 트리거할 수 있습니다.
  2. WaitReceiveDamage는 소유자 액터가 피해를 받을 때까지 기다립니다. 패시브 방어구 중첩은 영웅이 피해를 받으면 GameplayAbility가 방어구 중첩을 제거합니다.

 

AbilityTask는 다음으로 구성됩니다:

  • AbilityTask의 새 인스턴스를 생성하는 정적 함수입니다.
  • AbilityTask가 목적을 완료할 때 브로드캐스트되는 위임자
  • 주 작업을 시작하고 외부 델리게이트에 바인딩하는 등의 작업을 수행하는 Activate() 함수입니다.
  • 바인딩한 외부 델리게이트를 포함한 정리를 위한 OnDestroy() 함수입니다.
  • 바인딩한 외부 델리게이트에 대한 콜백 함수입니다.
  • 멤버 변수 및 내부 도우미 함수
💡Note: AbilityTask는 한 가지 유형의 출력 델리게이트만 선언할 수 있습니다. 매개변수 사용 여부에 관계없이 모든 출력 델리게이트는 이 유형이어야 합니다. 사용하지 않는 델리게이트 매개변수에는 기본값을 전달하세요.

 

AbilityTask는 소유 GameplayAbility를 실행 중인 클라이언트 또는 서버에서만 실행되지만, AbilityTask 생성자에서 bSimulatedTask = true; 를 설정하고 가상 void InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent); 를 오버라이드하고 리플리케이트할 멤버 변수를 설정하여 시뮬레이션 클라이언트에서 실행되도록 설정할 수 있습니다. 이 방법은 모든 동작 변경을 리플리케이트하지 않고 전체 동작을 시뮬레이션하려는 이동 AbilityTask와 같은 드문 상황에서만 유용합니다. 모든 루트모션 소스 AbilityTask가 이 작업을 수행합니다. 예시로 AbilityTask_MoveToLocation.h/.cpp를 참조하세요.

AbilityTask 생성자에서 bTickingTask = true; 를 설정하고 가상 void TickTask(float DeltaTime); 를 오버라이드하면 AbilityTask가 틱할 수 있습니다. 이 함수는 프레임에 걸쳐 값을 매끄럽게 처리해야 할 때 유용합니다. 예제로 AbilityTask_MoveToLocation.h/.cpp를 참조하세요.

 

4.7.3 AbilityTask 사용

C++에서 AbilityTask를 생성하고 활성화하려면 (GDGA_FireGun.cpp 에서) 다음과 같이 합니다:

UGDAT_PlayMontageAndWaitForEvent* Task = UGDAT_PlayMontageAndWaitForEvent::PlayMontageAndWaitForEvent(this, NAME_None, MontageToPlay, FGameplayTagContainer(), 1.0f, NAME_None, false, 1.0f);
Task->OnBlendOut.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnCompleted.AddDynamic(this, &UGDGA_FireGun::OnCompleted);
Task->OnInterrupted.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->OnCancelled.AddDynamic(this, &UGDGA_FireGun::OnCancelled);
Task->EventReceived.AddDynamic(this, &UGDGA_FireGun::EventReceived);
Task->ReadyForActivation();

 

 

블루프린트에서는 AbilityTask에 대해 생성한 블루프린트 노드를 사용하기만 하면 됩니다. ReadyForActivation()을 호출할 필요가 없습니다. 엔진/소스/편집기/GameplayTask 편집기/비공개/K2Node_LatentGameplay태스크 호출.cpp에서 자동으로 호출됩니다. 또한 K2Node_LatentGameplayTaskCall은 AbilityTask 클래스에 BeginSpawningActor() 및 FinishSpawningActor()가 있는 경우 자동으로 호출합니다(AbilityTask_WaitTargetData 참조). 다시 말하자면, K2Node_LatentGameplayTaskCall 은 블루프린트에 대한 오토매직 마법만 수행합니다. C++에서는 ReadyForActivation(), BeginSpawningActor(), FinishSpawningActor()를 수동으로 호출해야 합니다.

AbilityTask를 수동으로 취소하려면 블루프린트(비동기 태스크 프록시라고 함) 또는 C++에서 AbilityTask 오브젝트에서 EndTask() 를 호출하면 됩니다.

 

4.7.4 루트 모션 소스 AbilityTask

GAS에는 CharacterMovementComponent에 연결된 루트 모션 소스를 사용하여 넉백, 복잡한 점프, 당기기, 돌진 등 시간 경과에 따라 캐릭터를 움직일 수 있는 AbilityTask가 포함되어 있습니다.

💡Note: 루트모션소스 어빌리티태스크 예측은 엔진 버전 4.19 및 4.25 이상에서 작동합니다. 엔진 4.20~4.24 버전에서는 예측에 버그가 있지만, 어빌리티 태스크는 약간의 수정만 거치면 멀티플레이어에서 여전히 그 기능을 수행하며 싱글플레이어에서도 완벽하게 작동합니다. 4.25의 예측 수정 사항을 커스텀 4.20-4.24 엔진으로 체리피킹할 수 있습니다.

 

4.8 GameplayCue

4.8.1 GameplayCue 정의

GameplayCue(GC)는 사운드 효과, 파티클 효과, 카메라 흔들림 등 게임 플레이와 관련이 없는 것들을 실행합니다. GameplayCue는 일반적으로 (명시적으로 로컬에서 실행, 추가 또는 제거되지 않는 한) 리플리케이트되고 예측됩니다.

GameplayCue를 트리거하기 위해서는 ASC를 통해 GameplayCue의 필수 부모 이름과 이벤트 유형(실행, 추가 또는 제거)이 포함된 해당 GameplayTag를 GameplayCueManager에 전송합니다. GameplayCueNotify 오브젝트와 IGameplayCueInterface 를 구현하는 다른 액터는 GameplayCue의 GameplayCueTag를 기반으로 이러한 이벤트에 구독할 수 있습니다.

💡Note: 다시 한 번 말씀드리자면, GameplayCue GameplayTag는 GameplayCue의 부모 GameplayTag에서 시작해야 합니다. 예를 들어, 유효한 GameplayCue GameplayTag는 GameplayCue.A.B.C일 수 있습니다.

GameplayCueNotify에는 Static과 Actor의 두 가지 클래스가 있습니다. 이들은 서로 다른 이벤트에 반응하며, 서로 다른 유형의 GameplayEffect가 이를 트리거할 수 있습니다. 로직으로 해당 이벤트를 오버라이드하세요.

GameplayCue Class Event  GameplayEffect Type 내용
GameplayCueNotify_Static 실행 Instant or Periodic Static GameplayCueNotify는 클래스 디폴트 오브젝트에서 작동하며(인스턴스가 없음을 의미) 타격 임팩트와 같은 일회성 이펙트에 적합합니다.
GameplayCueNoify_Actor 추가 혹은 삭제 Duration or Infinite Actor GameplayCueNotify가 추가되면 새 인스턴스를 스폰합니다. 인스턴스화되어 있기 때문에 제거될 때까지 계속 동작을 할 수 있습니다. 백킹 지속 시간 또는 무한 GameplayEffect가 제거되거나 수동으로 remove를 호출하면 제거되는 사운드 및 파티클 이펙트를 루핑하는 데 좋습니다. 또한 동시에 추가할 수 있는 개수를 관리할 수 있는 옵션도 제공되므로 동일한 효과를 여러 번 적용할 때 사운드나 파티클이 한 번만 시작되도록 할 수 있습니다.

GameplayCueNotify는 기술적으로 모든 이벤트에 응답할 수 있지만 일반적으로 이 방식으로 사용합니다.

💡Note: GameplayCueNotify_Actor를 사용하는 경우, 제거에서 자동 소멸을 체크하지 않으면 해당 GameplayCueTag를 추가하는 후속 호출이 작동하지 않습니다.

전체가 아닌 ASC Replication Mode를 사용하는 경우, 서버 플레이어(리스닝 서버)에서 추가 및 제거 GC 이벤트가 두 번 발생합니다. 한 번은 GE를 적용할 때, 다른 한 번은 "최소" NetMultiCast에서 클라이언트로 전송할 때 발생합니다. 하지만 WhileActive 이벤트는 여전히 한 번만 발동합니다. 모든 이벤트는 클라이언트에서 한 번만 발생합니다.

샘플 프로젝트에는 기절 및 질주 이펙트를 위한 GameplayCueNotify_Actor가 포함되어 있습니다. 또한 파이어건의 발사체 임팩트를 위한 GameplayCueNotify_Static도 있습니다. 이러한 GC는 GE를 통해 리플리케이트하는 대신 로컬에서 트리거하여 더욱 최적화할 수 있습니다. 저는 샘플 프로젝트에서 초보자도 쉽게 사용할 수 있는 방법을 보여드리기로 했습니다.

 

4.8.2 GameplayCue 트리거링

GameplayEffect가 성공적으로 적용되었을 때(태그나 면역에 의해 차단되지 않았을 때) GameplayEffect 내부에서 트리거되어야 하는 모든 GameplayCue의 GameplayTag를 채웁니다.

UGameplayAbility는 GameplayCue를 실행, 추가, 거할 수 있는 블루프린트 노드를 제공합니다.

C++ 에서는 ASC 에서 직접 함수를 호출하거나 ASC 서브클래스의 블루프린트에 노출시킬 수 있습니다:

/** GameplayCue는 자체적으로 올 수도 있습니다. 이는 히트 결과 등을 전달하기 위해 선택적 EffectContext를 사용합니다. */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** 영구적인 GameplayCue 추가 */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** 영구 GameplayCue 제거 */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
	
/** GameplayEffect의 일부가 아닌, 자체적으로 추가된 모든 GameplayCue를 제거합니다. */
void RemoveAllGameplayCues();

 

4.8.3 로컬 GameplayCue

GameplayAbility와 ASC에서 GameplayCue를 발동하기 위해 노출된 함수는 기본적으로 리플리케이트됩니다. 각 GameplayCue 이벤트는 멀티캐스트 RPC입니다. 이로 인해 많은 RPC가 발생할 수 있습니다. 또한 GAS는 순 업데이트당 최대 두 개의 동일한 GameplayCue RPC를 적용합니다. 가능한 경우 로컬 GameplayCue를 사용하여 이를 방지합니다. 로컬 GameplayCue는 개별 클라이언트에서만 실행, 추가 또는 제거합니다.

로컬 GameplayCue를 사용할 수 있는 시나리오:

  • 발사체 충격
  • 근접 충돌 충격
  • 게애니메이션 몽타주에서 발동되는 GameplayCue

ASC 서브클래스에 추가해야 하는 로컬 GameplayCue 함수입니다:

UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

UFUNCTION(BlueprintCallable, Category = "GameplayCue", Meta = (AutoCreateRefTerm = "GameplayCueParameters", GameplayTagFilter = "GameplayCue"))
void RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);
void UPAAbilitySystemComponent::ExecuteGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters)
{
    UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Executed, GameplayCueParameters);
}

void UPAAbilitySystemComponent::AddGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters)
{
    UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::OnActive, GameplayCueParameters);
    UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::WhileActive, GameplayCueParameters);
}

void UPAAbilitySystemComponent::RemoveGameplayCueLocal(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters)
{
    UAbilitySystemGlobals::Get().GetGameplayCueManager()->HandleGameplayCue(GetOwner(), GameplayCueTag, EGameplayCueEvent::Type::Removed, GameplayCueParameters);
}

GameplayCue가 로컬로 추가된 경우, 로컬에서 제거해야 합니다. 리플리케이션을 통해 추가된 경우 리플리케이션을 통해 제거해야 합니다.

 

4.8.4 GameplayCue 파라미터

GameplayCue는 파라미터로 GameplayCue에 대한 추가 정보가 들어있는 FGameplayCueParameter 구조체를 받습니다. GameplayAbility 또는 ASC의 함수에서GameplayCue를 수동으로 트리거하는 경우, GameplayCue에 전달되는 GameplayCue 파라미터 구조체를 수동으로 채워야 합니다. GameplayCue가 GameplayEffect에 의해 트리거되는 경우, 다음 변수는 GameplayCueParameter 구조체에 자동으로 채워집니다:

  • AggregatedSourceTags
  • AggregatedTargetTags
  • GameplayEffectLevel
  • AbilityLevel
  • EffectContext
  • Magnitude (GameplayEffect에 GameplayCue tag container 위의 드롭다운에서 선택한 크기에 대한 어트리뷰트와 해당 어트리뷰트에 영향을 주는 해당 모디파이어가 있는 경우)

GameplayCueParameter 구조체의 SourceObject 변수는 GameplayCue를 수동으로 트리거할 때 임의의 데이터를 GameplayCue에 전달할 수 있는 좋은 장소가 될 수 있습니다.

💡Note: Instigator와 같은 파라미터 구조체의 일부 변수는 EffectContext 에 이미 존재할 수 있습니다. EffectContext에는 월드에서 GameplayCue를 스폰할 위치에 대한 FHitResult 를 포함할 수도 있습니다. EffectContext 를 서브클래싱하는 것은 GameplayCue, 특히 GameplayEffect에 의해 트리거되는 GameplayCue에 더 많은 데이터를 전달할 수 있는 잠재적으로 좋은 방법입니다.

자세한 내용은 UAbilitySystemGlobals에서 GameplayCueParameter 구조체를 채우는 세 가지 함수를 참조하세요. 이 함수는 가상이므로 오버라이드하여 추가 정보를 자동 채울 수 있습니다. 

/** GameplayCue 파라미터 초기화 */
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectSpecForRPC &Spec);
virtual void InitGameplayCueParameters_GESpec(FGameplayCueParameters& CueParameters, const FGameplayEffectSpec &Spec);
virtual void InitGameplayCueParameters(FGameplayCueParameters& CueParameters, const FGameplayEffectContextHandle& EffectContext);

 

4.8.5 GameplayCue Manager

기본적으로 GameplayCueManager는 전체 게임 디렉터리에서 GameplayCueNotify를 스캔하여 플레이 시 메모리에 로드합니다. GameplayCueManager가 스캔하는 경로는 DefaultGame.ini에서 설정하여 변경할 수 있습니다.

[/Script/GameplayAbilities.AbilitySystemGlobals]
GameplayCueNotifyPaths="/Game/GASDocumentation/Characters"

GameplayCueManager가 모든 GameplayCueNotify를 스캔하고 찾도록 하되, 플레이 중 모든 GameplayCueNotify를 비동기 로드하지는 않기를 원합니다. 이렇게 하면 레벨에서 사용되었는지 여부와 관계없이 모든 GameplayCueNotify와 참조된 모든 사운드 및 파티클이 메모리에 저장됩니다. 파라곤과 같은 대규모 게임에서는 수백 메가 바이트의 불필요한 에셋이 메모리에 저장되어 시작 시 버벅거림과 게임 멈춤이 발생할 수 있습니다.

시작 시 모든 GameplayCue를 비동기 로드하는 대신 GameplayCue가 게임 내에서 트리거될 때만 비동기 로드하는 방법을 사용할 수 있습니다. 이렇게 하면 모든 GameplayCue를 비동기 로드하는 동안 불필요한 메모리 사용과 잠재적인 게임 하드 프리즈를 완화하는 대신, 플레이 도중 특정 GameplayCue가 처음 트리거될 때 이펙트가 지연될 수 있습니다. SSD에서는 이러한 잠재적 지연이 발생하지 않습니다. HDD에서는 테스트하지 않았습니다. UE 에디터에서 이 옵션을 사용하는 경우, 에디터가 파티클 시스템을 컴파일해야 하는 경우 GameplayCue를 처음 로드하는 동안 약간의 버벅임이나 멈춤 현상이 있을 수 있습니다. 빌드에서는 파티클 시스템이 이미 컴파일되어 있으므로 문제가 되지 않습니다.

먼저 UGameplayCueManager 를 서브클래싱하고 AbilitySystemGlobals 클래스에 DefaultGame.ini에서 UGameplayCueManager 서브클래스를 사용하도록 지시해야 합니다.

[/Script/GameplayAbilities.AbilitySystemGlobals]
GlobalGameplayCueManagerClass="/Script/ParagonAssets.PBGameplayCueManager"

UGameplayCueManager 서브클래스에서 ShouldAsyncLoadRuntimeObjectLibraries()를 재정의합니다.

virtual bool ShouldAsyncLoadRuntimeObjectLibraries() const override
{
    return false;
}

 

4.8.6 GameplayCue가 발동되지 않도록 방지

GameplayCue가 발동되지 않기를 원할 때가 있습니다. 예를 들어 공격을 막는 경우 대미지 GameplayEffect에 첨부된 타격 효과를 재생하지 않거나 대신 커스텀 효과를 재생하고 싶을 수 있습니다. 이 작업은 GameplayEffectExecutionCalculation내에서 OutExecutionOutput.MarkGameplayCuesHandledManually()를 호출한 다음 GameplayCue 이벤트를 타겟 또는 소스의 ASC로 수동으로 전송하면 됩니다.

특정 ASC에서 GameplayCue가 발동되지 않도록 하려면 AbilitySystemComponent->bSuppressGameplayCues = true;를 설정하면 됩니다.

 

4.8.7 GameplayCue 일괄 처리

트리거되는 각 GameplayCue는 신뢰할 수 없는 NetMulticast RPC입니다. 여러 GC를 동시에 발동하는 상황에서는 이를 하나의 RPC로 압축하거나 데이터를 적게 전송하여 대역폭을 절약할 수 있는 몇 가지 최적화 방법이 있습니다.

 

4.8.7.1 수동 RPC

8개의 펠릿을 발사하는 산탄총이 있다고 가정해 보겠습니다. 이는 8개의 트레이스 및 임팩트 GameplayCue입니다. GASShooter는 모든 트레이스 정보를 EffectContextTargetData로 숨겨서 하나의 RPC로 결합하는 게으른 접근 방식을 취합니다. 이렇게 하면 RPC가 8개에서 1개로 줄어들지만 여전히 네트워크를 통해 많은 양의 데이터(약 500바이트)를 하나의 RPC로 전송합니다. 보다 최적화된 접근 방식은 타격 위치를 효율적으로 인코딩하거나 임의의 시드 번호를 제공하여 수신 측에서 타격 위치를 다시 생성/근사화하는 커스텀 구조체가 포함된 RPC를 전송하는 것입니다. 그러면 클라이언트는 이 커스텀 구조체의 압축을 풀고 로컬에서 실행되는 GameplayCue로 다시 전환합니다.

 

작동 방식:

  1. FScopedGameplayCueSendContext 를 선언합니다. 그러면 범위를 벗어날 때까지 UGameplayCueManager::FlushPendingCues() 가 억제되어, FScopedGameplayCueSendContext 가 범위를 벗어날 때까지 모든 GameplayCue가 대기열에 대기합니다.
  2. UGameplayCueManager::FlushPendingCues() 를 오버라이드하여 커스텀 GameplayTag를 기반으로 일괄 처리할 수 있는 GameplayCue를 커스텀 구조체에 병합하고 클라이언트에 RPC할 수 있습니다.
  3. 클라이언트는 커스텀 구조체를 받아 로컬에서 실행되는 GameplayCue에 언패킹합니다.

이 메서드는 피해 수치, 치명타 표시기, 깨진 방패 표시기, 치명타 맞았음 표시기 등과 같이 GameplayCueParameters가 제공하는 것과 맞지 않는 GameplayCue에 대한 특정 파라미터가 필요하지만 EffectContext에 추가하고 싶지 않을 때도 사용할 수 있습니다.

 

4.8.7.2 하나의 GE에 여러 GC

GameplayEffect의 모든 GameplayCue는 이미 하나의 RPC에 전송되어 있습니다. 기본적으로 UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec()은 ASC의 리플리케이션 모드와 상관없이 신뢰할 수 없는 NetMultiCast에서 전체 GameplayEffectSpec을 (하지만 FGameplayEffectSpecForRPC로 변환하여) 전송합니다. GameplayEffectSpec에 무엇이 있느냐에 따라 대역폭이 많이 소모될 수 있습니다. cvar AbilitySystem.AlwaysConvertGESpecToGCParams 1을 설정하여 이를 최적화할 수 있습니다. 그러면 GameplayEffectSpec을 FGameplayEffectSpec 전체가 아닌 FGameplayCueParameter 구조체로 변환하고 이를 RPC 합니다. 이렇게 하면 잠재적으로 대역폭을 절약할 수 있지만, GESpec이 GameplayCueParameters로 변환되는 방식과 GC가 알아야 하는 정보에 따라 정보가 적을 수도 있습니다.

 

4.8.8 GameplayCue 이벤트

GameplayCue는 특정 EGameplayCueEvent에 반응합니다:

EGameplayCueEvent Description
OnActive Called when a GameplayCue is activated (added).
WhileActive Called when GameplayCue is active, even if it wasn't actually just applied (Join in progress, etc). This is not Tick! It's called once just like OnActive when a GameplayCueNotify_Actor is added or becomes relevant. If you need Tick(), just use the GameplayCueNotify_Actor's Tick(). It's an AActor after all.
Removed Called when a GameplayCue is removed. The Blueprint GameplayCue function that responds to this event is OnRemove.
Executed Called when a GameplayCue is executed: instant effects or periodic Tick(). The Blueprint GameplayCue function that responds to this event is OnExecute.

GameplayCue 시작 시 발생하는 GameplayCue의 모든 이펙트에 OnActive를 사용하되, 늦게 참여하는 사람이 놓쳐도 괜찮습니다. GameplayCue 의 진행 중인 이펙트 중 뒤늦게 조인하는 사람이 보길 원하는 이펙트에는 WhileActive를 사용합니다. 예를 들어 MOBA에서 타워 구조물이 폭발하는 GameplayCue가 있다면, 초기 폭발 파티클 시스템과 폭발 사운드는 OnActive에 넣고 나머지 진행 중인 불 파티클이나 사운드는 WhileActive에 넣을 수 있습니다. 이 시나리오에서는 뒤늦게 합류한 사람이 초기 폭발을 OnActive에서 재생하는 것은 의미가 없지만, 폭발이 발생한 후 지면에 지속적이고 반복되는 불 이펙트를 WhileActive에서 볼 수 있게 하려는 것입니다. OnRemove는 OnActive와 WhileActive에 추가된 모든 것을 정리해야 합니다. WhileActive는 액터가 GameplayCueNotify_Actor의 연관성 범위에 들어올 때마다 호출됩니다. OnRemove는 액터가 GameplayCueNotify_Actor의 연관성 범위를 벗어날 때마다 호출됩니다.

 

4.8.9 GameplayCue 안전성

일반적으로 GameplayCue는 신뢰할 수 없으므로 게임 플레이에 직접적인 영향을 미치는 모든 것에 적합하지 않은 것으로 간주해야 합니다.


실행된 GameplayCue: 이러한 GameplayCue는 신뢰할 수 없는 멀티캐스트를 통해 적용되며 항상 신뢰할 수 없습니다.


GameplayEffect에서 적용된 GameplayCue입니다:

  • Autonomous proxy는 OnActive, WhileActive를 안정적으로 수신하며, OnRemoveFActiveGameplayEffectsContainer::NetDeltaSerialize()는 UAbilitySystemComponent::HandleDeferredGameplayCues()를 호출하여 OnActive 및 WhileActive를 호출합니다. FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers()는 OnRemoved를 호출합니다.
  • Simulated proxy는 WhileActive 및 OnRemoveUAbilitySystemComponent::MinimalReplicationGameplayCues의 리플리케이션 호출인 WhileActive 및 OnRemove를 안정적으로 수신합니다. OnActive 이벤트는 신뢰할 수 없는 멀티캐스트에 의해 호출됩니다.

GameplayEffect 없이 적용된 GameplayCue입니다:

  • Autonomous proxy는 안정적이지 않은 멀티캐스트에 의해 호출되는 OnRemoveThe OnActive 및 WhileActive 이벤트를 안정적으로 수신합니다.
  • Simulated proxy는 WhileActive 및 OnRemoveUAbilitySystemComponent::MinimalReplicationGameplayCues의 리플리케이션 호출인 WhileActive 및 OnRemove를 안정적으로 수신합니다. OnActive 이벤트는 신뢰할 수 없는 멀티캐스트에 의해 호출됩니다.

GameplayCue에 '신뢰할 수 있는' 무언가가 필요한 경우, GameplayEffect에서 적용하고 WhileActive를 사용하여 FX를 추가하고 OnRemove를 사용하여 FX를 제거합니다.

 

4.9 전역 AbilitySystem

AbilitySystemGlobals 클래스는 GAS에 대한 전역 정보를 담고 있습니다. 대부분의 변수는 DefaultGame.ini에서 설정할 수 있습니다. 일반적으로 이 클래스와 상호작용할 필요는 없지만, 그 존재를 알고 있어야 합니다. GameplayCueManager 또는 GameplayEffectContext와 같은 것을 서브클래싱해야 하는 경우, AbilitySystemGlobals를 통해 서브클래싱해야 합니다.

AbilitySystemGlobals를 서브클래싱하려면 DefaultGame.ini에서 클래스 이름을 설정합니다:

[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"

 

4.9.1 InitGlobalData()

UE 4.24부터 TargetData를 사용하려면 UAbilitySystemGlobals::Get().InitGlobalData()를 호출해야 하며, 그렇지 않으면 스크립트 구조 캐시 관련 오류가 발생하고 클라이언트가 서버에서 연결이 끊어집니다. 이 함수는 프로젝트에서 한 번만 호출하면 됩니다. 포트나이트는 UAssetManager::StartInitialLoading()에서 호출하고, 파라곤은 UEngine::Init()에서 호출합니다. 샘플 프로젝트에 표시된 것처럼 UAssetManager::StartInitialLoading()에 넣는 것이 좋은 위치라는 것을 알았습니다. 이 상용구 코드를 프로젝트에 복사하여 TargetData 관련 문제를 방지해야 합니다.

AbilitySystemGlobals GlobalAttributeSetDefaultsTableNames를 사용하는 도중 크래시가 발생하는 경우, 나중에 포트나이트처럼 에셋 매니저나 게임 인스턴스에서 UAbilitySystemGlobals::Get().InitGlobalData()를 호출해야 할 수도 있습니다.

 

4.10 예측

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의 예측 구현이 해결하고자 하는 문제:

  1. "제가 해도 될까요?" 예측을 위한 기본 프로토콜.
  2. "실행 취소" 예측이 실패했을 때 부작용을 실행 취소하는 방법.
  3. "재실행" 로컬에서 예측했지만 서버에서도 복제되는 부작용이 다시 재생되지 않도록 하는 방법입니다.
  4. "완전성" 우리가 모든 부작용을/정말/예측했는지 확인하는 방법.
  5. "종속성" 종속 예측 및 예측 이벤트의 체인을 관리하는 방법.
  6. "재정의" 서버에서 복제/소유하는 상태를 예측적으로 재정의하는 방법입니다.

From GameplayPrediction.h

 

4.10.1 예측 키

GAS의 예측은 클라이언트가 GameplayAbility를 활성화할 때 생성하는 정수 식별자인 예측 키의 개념에 따라 작동합니다.

  • 클라이언트는 GameplayAbility를 활성화할 때 예측 키를 생성합니다. 이것이 활성화 예측 키입니다.
  • 클라이언트는 이 예측 키를 CallServerTryActivateAbility()를 통해 서버로 전송합니다.
  • 클라이언트는 예측 키가 유효한 동안 이 예측 키를 적용하는 모든 GameplayEffect 에 이 예측 키를 추가합니다.
  • 클라이언트의 예측 키가 범위를 벗어났습니다. 동일한 GameplayAbility에서 추가로 예측된 이펙트는 새로운 범위 예측 창이 필요합니다.
  • 서버는 클라이언트로부터 예측 키를 받습니다.
  • 서버는 이 예측 키를 적용하는 모든 GameplayEffect 에 추가합니다.
  • 서버는 예측 키를 클라이언트에 다시 복제합니다.
  • 클라이언트는 서버로부터 GameplayEffect 를 적용하는 데 사용된 예측 키와 함께 리플리케이트된 GameplayEffect를 받습니다. 리플리케이트된 GameplayEffect 중 클라이언트가 동일한 예측 키로 적용한 GameplayEffect와 일치하는 것이 있으면 올바르게 예측된 것입니다. 클라이언트가 예측된 GameplayEffect를 제거할 때까지 일시적으로 대상에 GameplayEffect 의 사본 두 개가 존재합니다.
  • 클라이언트는 서버로부터 예측 키를 다시 받습니다. 이것이 복제된 예측 키입니다. 이 예측 키는 이제 부실 키로 표시됩니다.
  • 클라이언트는 이제 오래된 리플리케이트된 예측 키로 생성한 모든 GameplayEffect를 제거합니다. 서버에 의해 리플리케이트된 GameplayEffect는 유지됩니다. 클라이언트가 추가한 GameplayEffect 중 서버에서 일치하는 리플리케이트 버전을 받지 못한 것은 잘못 예측된 것입니다.

예측 키는 활성화 예측 키에서 활성화로 시작하는 GameplayAbilities의 명령어 "창"을 원자적으로 그룹화하는 동안 유효하도록 보장됩니다. 이는 한 프레임 동안만 유효하다고 생각할 수 있습니다. 어빌리티 태스크에 새로운 범위 예측 창을 생성하는 내장 동기화 지점이 없는 한, 잠복 액션 어빌리티 태스크의 모든 콜백은 더 이상 유효한 예측 키를 갖지 못합니다.

 

4.10.2 어빌리티에서 새로운 예측 창 만들기

AbilityTask의 콜백에서 더 많은 작업을 예측하려면 새 범위 예측 키를 사용하여 새 범위 예측 창을 만들어야 합니다. 이를 클라이언트와 서버 간의 동기화 지점이라고도 합니다. 모든 입력 관련 어빌리티 태스크와 같은 일부 어빌리티 태스크에는 새 범위 예측 창을 생성하는 기능이 내장되어 있으므로 어빌리티 태스크 콜백의 원자 코드에 사용할 수 있는 유효한 범위 예측 키가 있습니다. 

 

WaitDelay 작업과 같은 다른 작업에는 콜백에 대해 범위가 지정된 새 예측 창을 만드는 내장 코드가 없습니다. WaitDelay와 같이 범위가 지정된 예측 창을 생성하는 코드가 내장되어 있지 않은 AbilityTask 이후의 작업을 예측해야 하는 경우, OnlyServerWait 옵션과 함께 WaitNetSync AbilityTask를 사용하여 수동으로 수행해야 합니다. 클라이언트가 OnlyServerWait 옵션이 있는 WaitNetSync에 도달하면, GameplayAbility의 활성화 예측 키를 기반으로 새 범위 지정 예측 키를 생성하고 서버에 RPC한 다음 적용하는 모든 새 GameplayEffect에 이를 추가합니다. 

 

서버가 OnlyServerWait로 WaitNetSync에 도달하면 클라이언트에서 범위가 지정된 새 예측 키를 받을 때까지 기다렸다가 계속 진행합니다. 이 범위 지정 예측 키는 활성화 예측 키와 동일한 동작을 수행하며, GameplayEffect에 적용되고 클라이언트에 다시 리플리케이트되어 오래된 것으로 표시됩니다. 범위가 지정된 예측 키는 범위를 벗어날 때까지, 즉 범위가 지정된 예측 창이 닫힐 때까지 유효합니다. 따라서 다시 말하지만, 잠재적 연산이 아닌 원자 연산만 t를 사용할 수 있습니다.

 

4.10.3 액터 예측 스폰

클라이언트에서 액터를 예측적으로 스폰하는 것은 고급 주제입니다. GAS는 이를 바로 처리하는 기능을 제공하지 않습니다(SpawnActor AbilityTask는 서버에만 액터를 스폰합니다). 핵심 개념은 리플리케이트된 액터를 클라이언트와 서버 양쪽에서 스폰하는 것입니다.

액터가 단지 장식용이거나 게임 플레이 목적이 없는 경우, 간단한 해결책은 서버가 소유 클라이언트로 리플리케이트하지 못하도록 액터의 IsNetRelevantFor() 함수를 오버라이드하는 것입니다. 소유 클라이언트는 로컬에서 스폰된 버전을, 서버와 다른 클라이언트는 서버의 리플리케이트 버전을 갖게 됩니다.

bool APAReplicatedActorExceptOwner::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
    return !IsOwnedBy(ViewTarget);
}


스폰된 액터가 대미지를 예측해야 하는 발사체처럼 게임 플레이에 영향을 끼치는 경우, 이 문서의 범위를 벗어난 고급 로직이 필요합니다. 에픽게임즈 깃허브에서 언리얼 토너먼트가 발사체를 예측 스폰하는 방법을 살펴보세요. 여기에는 서버의 리플리케이트된 프로젝타일과 동기화되는 더미 프로젝타일이 소유 클라이언트에서만 스폰됩니다.

 

4.10.4 GAS 시스템 예측 기능의 향후 발전

GameplayPrediction.h에 따르면 향후 GameplayEffect 제거 및 주기적 GameplayEffect 예측 기능을 추가할 수 있다고 합니다.

에픽 게임즈의 데이브 라티는 재사용 대기 시간을 예측할 때 지연 시간이 긴 플레이어가 짧은 플레이어보다 불이익을 받는 지연 시간 조정 문제를 해결하는 데 관심을 표명했습니다.

에픽 게임즈의 새로운 네트워크 예측 플러그인은 이전의 CharacterMovementComponent와 마찬가지로 GAS와 완전히 상호 운용될 것으로 예상됩니다.

 

4.10.5 Network Prediction Plugin

에픽 게임즈는 최근 캐릭터 무브먼트 컴포넌트를 새로운 네트워크 예측 플러그인으로 대체하는 이니셔티브를 시작했습니다. 이 플러그인은 아직 초기 단계에 있지만 언리얼 엔진 깃허브에서 얼리 액세스로 이용할 수 있습니다. 향후 언리얼 엔진의 어떤 버전에서 실험적 베타 버전으로 출시될지는 아직 알 수 없습니다.

 

4.11 Targeting

4.11.1 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;
}

 

4.11.2 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는 로켓 발사기의 보조 능력인 호밍 로켓 조준에 퍼시스턴트 타깃을 사용합니다.

 

4.11.3 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;
}

 

4.11.4 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는 로켓 발사기의 보조 능력인 호밍 로켓 조준에 퍼시스턴트 타겟을 사용합니다.

 

4.11.5 Gameplay Effect Containers Targeting

GameplayEffectContainer에는 TargetData를 생성하는 효율적인 선택적 수단이 제공됩니다. 이 타겟팅은 클라이언트와 서버에서 EffectContainer가 적용될 때 즉시 이루어집니다. 타겟팅 오브젝트의 CDO에서 실행되므로(액터 스폰 및 소멸이 없음) TargetActor보다 효율적이지만, 플레이어 입력이 부족하고 확인 없이 즉시 발생하며 취소할 수 없고 클라이언트에서 서버로 데이터를 전송할 수 없습니다(양쪽에서 데이터를 생성합니다). 인스턴트 트레이스와 콜리전 오버랩에 잘 작동합니다. 에픽의 액션 RPG 샘플 프로젝트에는 컨테이너를 사용한 타겟팅의 두 가지 예제 유형, 즉 어빌리티 소유자를 타겟팅하는 것과 이벤트에서 타깃 데이터를 가져오는 것이 포함되어 있습니다. 또한 블루프린트에서 플레이어로부터 약간의 오프셋(자손 블루프린트 클래스에서 설정)을 두고 인스턴트 구체 트레이스를 수행하는 것도 구현되어 있습니다. C++ 또는 블루프린트에서 URPGTargetType을 서브클래싱하여 자신만의 타깃팅 유형을 만들 수 있습니다.