4.7 AbilityTask
4.7.1 AbilityTask 정의
GameplayAbility는 한 프레임에서만 실행됩니다. 이로 인해 유연성이 제한됩니다. 시간이 지남에 따라 발생하는 작업이나 특정 시점에 호출되는 델리게이트에 반응해야 하는 작업을 수행하기 위해 우리는 AbilityTask라는 지연 작업을 사용합니다. GAS는 기본적으로 여러 종류의 AbilityTask를 제공합니다:
- RootMotionSource로 캐릭터 이동을 위한 작업
- 애니메이션 몽타주를 재생하는 작업
- Attribute 변경에 반응하는 작업
- GameplayEffect 변경에 반응하는 작업
- 플레이어 입력에 반응하는 작업
- 그 외의 작업들
UAbilityTask 생성자는 게임 전역에서 동시에 실행할 수 있는 최대 1000개의 AbilityTask만을 허용합니다. 이는 수백 명의 캐릭터가 동시에 존재하는 게임(예: RTS 게임)을 설계할 때 유의해야 합니다.
4.7.2 커스텀 AbilityTask
여러분은 종종 자신만의 커스텀 AbilityTask(C++)를 만들게 될 것입니다. 샘플 프로젝트에는 두 가지 커스텀 AbilityTask가 포함되어 있습니다:
- PlayMontageAndWaitForEvent: 기본 PlayMontageAndWait와 WaitGameplayEvent AbilityTask를 결합한 것입니다. 이 AbilityTask는 애니메이션 몽타주가 AnimNotify에서 발생한 GameplayEvent를 GameplayAbility로 다시 전달하도록 합니다. 애니메이션 몽타주 중 특정 시점에 행동을 트리거하는 데 사용합니다.
- WaitReceiveDamage: 해당 AbilityTask는 OwnerActor가 피해를 받을 때를 감지합니다. 패시브 갑옷 스택 능력은 영웅이 피해를 입을 때마다 갑옷 스택을 제거합니다.
AbilityTask는 다음과 같은 구성 요소로 이루어집니다:
- AbilityTask의 새 인스턴스를 생성하는 정적 함수
- AbilityTask가 완료되었을 때 방송되는 델리게이트
- 주요 작업을 시작하고 외부 델리게이트에 바인딩하는 Activate() 함수
- 외부 델리게이트와의 바인딩을 해제하는 등 정리를 위한 OnDestroy() 함수
- 바인딩된 외부 델리게이트에 대한 콜백 함수
- 멤버 변수와 내부 헬퍼 함수들
💡Note: AbilityTask는 한 가지 유형의 출력 델리게이트만 선언할 수 있습니다. 매개변수 사용 여부에 관계없이 모든 출력 델리게이트는 이 유형이어야 합니다. 사용하지 않는 델리게이트 매개변수에는 기본값을 전달해야 합니다.
AbilityTask는 해당 GameplayAbility를 실행하는 클라이언트나 서버에서만 실행됩니다. 하지만 AbilityTask는 bSimulatedTask = true;를 AbilityTask 생성자에 설정하고, InitSimulatedTask(UGameplayTasksComponent& InGameplayTasksComponent)를 오버라이드하며, 필요한 멤버 변수들을 리플리케이트되도록 설정하면 시뮬레이션 클라이언트에서 실행되도록 설정할 수 있습니다. 이는 모든 이동 변경 사항을 리플리케이트하는 대신 전체 이동 AbilityTask를 시뮬레이션하고자 하는 드문 상황에서 유용합니다. 모든 RootMotionSource 관련 AbilityTask가 이렇게 동작합니다. AbilityTask_MoveToLocation.h/.cpp를 예시로 참고할 수 있습니다.
AbilityTask는 생성자에서 bTickingTask = true;를 설정하고 virtual void TickTask(float DeltaTime)를 오버라이드하면 틱(Tick)을 실행할 수 있습니다. 이는 프레임 간에 부드럽게 값을 보간(lerp)해야 할 때 유용합니다. AbilityTask_MoveToLocation.h/.cpp에서 예시를 확인할 수 있습니다.
4.7.3 AbilityTask 사용
C++(GDGA_FireGun.cpp)에서 AbilityTask를 생성하고 활성화하려면 다음과 같이 합니다:
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();
Blueprint에서는 AbilityTask에 대해 생성한 Blueprint 노드를 사용하면 됩니다. ReadyForActivation()을 호출할 필요가 없으며, 이는 Engine/Source/Editor/GameplayTasksEditor/Private/K2Node_LatentGameplayTaskCall.cpp에서 자동으로 호출됩니다. K2Node_LatentGameplayTaskCall은 또한 AbilityTask 클래스에 BeginSpawningActor()와 FinishSpawningActor()가 있으면 자동으로 호출합니다(예: AbilityTask_WaitTargetData 참조). 다시 한 번 강조하자면, K2Node_LatentGameplayTaskCall은 Blueprint에서만 자동으로 호출됩니다. C++에서는 ReadyForActivation(), BeginSpawningActor(), FinishSpawningActor()를 수동으로 호출해야 합니다.
Blueprint에서 AbilityTask를 수동으로 취소하려면, AbilityTask 객체(Async Task Proxy)에서 EndTask()를 호출하거나 C++에서 동일하게 호출하면 됩니다.
4.7.4 Root Motion Source AbilityTask
GAS에는 CharacterMovementComponent에 연결된 Root Motion Source를 사용하여 넉백, 복잡한 점프, 당기기, 돌진 등 시간 경과에 따라 캐릭터를 움직일 수 있는 AbilityTask가 포함되어 있습니다.
RootMotionSource AbilityTasks의 예측은 엔진 버전 4.19와 4.25+에서는 정상적으로 작동합니다. 하지만 4.20-4.24 버전에서는 예측에 버그가 있어, 멀티플레이어에서 네트워크 수정이 필요하며, 싱글 플레이에서는 완벽하게 작동합니다. 4.25의 예측 수정 사항을 4.20-4.24 버전의 엔진에 적용하는 것도 가능합니다.
💡Note: RootMotionSource AbilityTask의 예측은 엔진 버전 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는 해당 GameplayTag와 반드시 GameplayCue라는 부모 이름 및 이벤트 유형(Execute, Add, Remove)을 함께 ASC를 통해 GameplayCueManager로 보내어 트리거됩니다. GameplayCueNotify 객체와 IGameplayCueInterface를 구현한 다른 액터들은 GameplayCue의 GameplayTag(GameplayCueTag)에 따라 이 이벤트를 구독할 수 있습니다.
💡Note: 다시 한 번 말씀드리자면, GameplayCue의 GameplayTag는 반드시 GameplayCue라는 부모 GameplayTag로 시작해야 합니다. 예를 들어, 유효한 GameplayCue GameplayTag는 GameplayCue.A.B.C와 같습니다.
GameplayCueNotify에는 Static과 Actor라는 두 가지 종류가 있습니다. 각각은 서로 다른 이벤트에 응답하고, 다른 유형의 GameplayEffect가 이들을 트리거할 수 있습니다. 해당 이벤트를 오버라이드하여 필요한 로직을 구현하면 됩니다.
GameplayCue 클래스 | 이벤트 | GameplayEffect 유형 | 설명 |
GameplayCueNotify_Static | Execute | Instant or Periodic | Static GameplayCueNotify는 CDO에서 작동하며(인스턴스가 없음을 의미) 타격 임팩트와 같은 일회성 효에 적합합니다. |
GameplayCueNotify_Actor | Add 혹은 Remove | Duration or Infinite | Actor GameplayCueNotify가 추가되면 새 인스턴스를 스폰합니다. 인스턴스화되어 있기 때문에 제거될 때까지 계속 동작을 할 수 있습니다. 백킹 지속 시간 또는 무한 GameplayEffect가 제거되거나 수동으로 remove를 호출하면 제거되는 사운드 및 파티클 이펙트를 루핑하는 데 좋습니다. 또한 동시에 추가할 수 있는 개수를 관리할 수 있는 옵션도 제공되므로 동일한 효과를 여러 번 적용할 때 사운드나 파티클이 한 번만 시작되도록 할 수 있습니다. Actor GameplayCueNotify는 추가될 때마다 새로운 인스턴스를 생성합니다. 이들은 인스턴스화되어 있으므로 시간이 지나면서 반복되는 소리나 파티클 효과를 처리할 수 있습니다. 이들은 해당 Duration이나 Infinite GameplayEffect가 제거되면 자동으로 제거됩니다. 또는 수동으로 제거를 호출할 수도 있습니다. 또한 여러 번 적용된 동일한 효과가 소리나 파티클을 한 번만 시작하도록 허용하는 옵션도 있습니다. |
GameplayCueNotify는 기술적으로 모든 이벤트에 응답할 수 있지만 일반적으로 위 방식을 사용합니다.
💡Note: GameplayCueNotify_Actor를 사용할 때, Remove 시 Auto Destroy를 체크하지 않으면 이후 동일한 GameplayCueTag에 대한 Add 호출이 작동하지 않을 수 있습니다.
전체가 아닌 ASC Replication Mode를 사용하는 경우, 서버 플레이어(리스닝 서버)에서 추가 및 제거 GC 이벤트가 두 번 발생합니다. 한 번은 GE를 적용할 때, 다른 한 번은 "최소" NetMultiCast에서 클라이언트로 전송할 때 발생합니다. 하지만 WhileActive 이벤트는 여전히 한 번만 발동합니다. 모든 이벤트는 클라이언트에서 한 번만 발생합니다.
ASC 리플리케이션 모드가 Full이 아닌 경우, 서버 플레이어(리스닝 서버)에서는 Add 및 Remove GC 이벤트가 두 번 발생합니다. 첫 번째는 GE를 적용하는 것이고, 두 번째는 최소한의 NetMultiCast를 통해 클라이언트에 전달됩니다. 하지만
WhileActive 이벤트는 여전히 한 번만 발생합니다. 모든 이벤트는 클라이언트에서 한 번만 발생합니다.
샘플 프로젝트에는 스턴과 스프린트 효과를 위한 GameplayCueNotify_Actor와 FireGun의 발사체 충돌을 위한 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);
/** 자체적으로 추가된 GameplayCue를 제거합니다. 즉, GameplayEffect의 일부로 추가되지 않은 경우입니다. */
void RemoveAllGameplayCues();
4.8.3 로컬 GameplayCue
GameplayAbility와 ASC에서 GameplayCue를 발사하는 함수는 기본적으로 리플리케이트됩니다. 각 GameplayCue 이벤트는 멀티캐스트 RPC입니다. 이로 인해 많은 RPC 호출이 발생할 수 있습니다. GAS는 동일한 GameplayCue RPC가 네트워크 업데이트당 최대 두 번만 실행되도록 제한합니다. 이를 피하기 위해 가능한 경우 로컬 GameplayCue를 사용합니다. 로컬 GameplayCue는 개별 클라이언트에서만 Execute, Add, 또는 Remove가 실행됩니다.
로컬 GameplayCue를 사용할 수 있는 시나리오:
- 발사체 충돌
- 근접 충돌 충돌
- 애니메이션 몽타주에서 발동되는 GameplayCue
로컬 GameplayCue 함수(ASC 서브클래스에 추가해야 할 함수들) 입니다:
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는 FGameplayCueParameters 구조체를 받아 해당 GameplayCue에 대한 추가 정보를 전달합니다. 만약 GameplayCue가 GameplayAbility나 ASC의 함수에서 수동으로 트리거된다면, GameplayCue에 전달되는 FGameplayCueParameters 구조체를 수동으로 채워야 합니다. 만약 GameplayCue가 GameplayEffect에 의해 트리거된다면, 다음과 같은 변수들이 FGameplayCueParameters 구조체에 자동으로 채워집니다:
- AggregatedSourceTags
- AggregatedTargetTags
- GameplayEffectLevel
- AbilityLevel
- EffectContext
- Magnitude (만약 GameplayEffect에 Magnitude를 위한 Attribute가 선택되어 있고, 그 Attribute에 영향을 미치는 해당 Modifier가 있는 경우)
FGameplayCueParameters 구조체의 SourceObject 변수는 GameplayCue를 수동으로 트리거할 때 임의의 데이터를 GameplayCue로 전달하는 데 유용한 장소일 수 있습니다.
💡Note: Instigator와 같은 일부 변수는 이미 EffectContext에 존재할 수도 있습니다. EffectContext는 또한 GameplayCue를 월드에 어디에 스폰할지에 대한 FHitResult를 포함할 수 있습니다.
EffectContext 를 서브클래싱하는 것은 GameplayEffect에 의해 트리거되는 GameplayCue에 더 많은 데이터를 전달하는 좋은 방법일 수 있습니다.
자세한 내용은 FGameplayCueParameters 구조체를 채우는 UAbilitySystemGlobals의 3가지 함수들을 참조해주세요. 해당 함수들은 가상 함수이므로, 이를 오버라이드하여 더 많은 정보를 자동으로 채울 수 있습니다.
/** 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를 찾고, 게임 실행 시 이를 메모리에 로드합니다. 이 경로를 변경하려면 DefaultGame.ini에서 설정할 수 있습니다.
[/Script/GameplayAbilities.AbilitySystemGlobals]
GameplayCueNotifyPaths="/Game/GASDocumentation/Characters"
GameplayCueManager가 모든 GameplayCueNotify를 스캔하고 찾도록 할 수 있지만, 게임 시작 시 모든 것을 비동기적으로 로드하지 않도록 설정할 수 있습니다. 이렇게 하면 GameplayCueNotify와 그와 관련된 모든 사운드와 파티클이 레벨에서 사용되었는지와 관계없이 메모리에 로드되지 않습니다. Paragon과 같은 대형 게임에서는 이로 인해 수백 메가바이트의 불필요한 자산이 메모리에 로드되어 게임 시작 시 Hitching(버벅거림)이나 Freezing(게임 멈춤)을 초래할 수 있습니다.
게임 시작 시 모든 GameplayCue를 비동기적으로 로드하는 대신, GameplayCue가 게임 내에서 트리거될 때만 비동기적으로 로드하도록 설정할 수 있습니다. 이 방법은 불필요한 메모리 사용을 줄이고 게임이 시작될 때 GameplayCue를 비동기적으로 로드할 때 발생할 수 있는 게임의 하드 프리징을 방지하는 데 도움이 됩니다. 그러나 특정 GameplayCue가 게임 중 처음 트리거될 때 약간의 지연이 발생할 수 있습니다. 이 지연은 SSD에서는 발생하지 않으며, HDD에서는 테스트되지 않았습니다. UE Editor를 사용할 경우, GameplayCue가 처음 로드될 때 파티클 시스템을 컴파일해야 할 수 있으므로 약간의 Hitching이나 Freezing이 발생할 수 있습니다. 그러나 빌드에서는 이미 파티클 시스템이 컴파일되었으므로 문제가 되지 않습니다.
먼저 UGameplayCueManager를 서브클래싱하고, AbilitySystemGlobals 클래스가 우리의 UGameplayCueManager 서브클래스를 사용하도록 DefaultGame.ini에서 설정해야 합니다.
[/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()를 호출하고, 이후 ASC의 Target이나 Source에 수동으로 GameplayCue 이벤트를 전송할 수 있습니다.
특정 ASC에서 GameplayCue가 전혀 실행되지 않게 하려면, AbilitySystemComponent->bSuppressGameplayCues = true;로 설정할 수 있습니다.
4.8.7 GameplayCue 일괄 처리
GameplayCue가 트리거되었을 때 각 GameplayCue는 Unreliable NetMulticast RPC입니다. 여러 GameplayCue를 동시에 발사하는 상황에서는, 이를 하나의 RPC로 압축하거나 데이터를 덜 전송하여 대역폭을 절약할 수 있는 몇 가지 최적화 방법이 있습니다.
4.8.7.1 수동 RPC
예를 들어, 샷건이 8개의 총알을 발사한다고 가정해 보겠습니다. 그러면 8개의 트레이스와 임팩트 GameplayCue가 발생합니다. GASShooter는 이를 하나의 RPC로 합치는 간단한 방법을 사용하여 모든 트레이스 정보를 EffectContext에 TargetData로 저장합니다. 이렇게 하면 RPC 수가 8에서 1로 줄어들지만, 여전히 그 하나의 RPC에서 약 500바이트의 데이터가 전송됩니다. 더 최적화된 방법은 임팩트 위치를 효율적으로 인코딩하는 커스텀 구조체를 사용하여 RPC를 보내거나, 랜덤 시드 번호를 사용해 수신 측에서 임팩트 위치를 재생성하거나 근사화하는 방법입니다. 클라이언트는 이 커스텀 구조체를 언팩하여 로컬에서 실행되는 GameplayCue로 변환합니다.
이 방법은 다음과 같이 작동합니다:
- FScopedGameplayCueSendContext를 선언합니다. 이것은 UGameplayCueManager::FlushPendingCues()의 호출을 범위 밖으로 나올 때까지 억제하여, 모든 GameplayCue가 FScopedGameplayCueSendContext 범위 밖으로 나올 때까지 큐에 저장됩니다.
- UGameplayCueManager::FlushPendingCues()를 오버라이드하여, 일부 GameplayTag에 따라 배치할 수 있는 GameplayCue들을 커스텀 구조체에 병합하고 이를 클라이언트로 RPC로 전송합니다.
- 클라이언트는 커스텀 구조체를 수신하고 이를 로컬에서 실행되는 GameplayCue로 언팩합니다.
이 방법은 FGameplayCueParameters에 맞지 않는 특정 GameplayCue 파라미터가 필요할 때, 예를 들어 피해 수치, 치명타 표시, 방어구가 파괴된 표시, 치명적인 타격 표시 등을 추가하려고 할 때 유용하게 사용할 수 있습니다.
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가 알아야 하는 정보에 따라 정보가 적을 수도 있습니다.
모든 GameplayCue는 이미 하나의 RPC로 전송됩니다. 기본적으로 UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec()는 전체 GameplayEffectSpec(그러나 FGameplayEffectSpecForRPC로 변환된 형태)을 NetMulticast로 전송합니다. 이는 ASC의 리플리케이션 모드와 관계없이 신뢰할 수 없는 방식으로 전송됩니다. 이 방식은 GameplayEffectSpec에 포함된 데이터에 따라 많은 대역폭을 차지할 수 있습니다. 이를 최적화하려면 cvar AbilitySystem.AlwaysConvertGESpecToGCParams 1을 설정할 수 있습니다. 이 설정은 GameplayEffectSpec을 FGameplayCueParameters 구조체로 변환하여, FGameplayEffectSpecForRPC 대신 이 구조체만 RPC로 전송하게 합니다. 이 방식은 대역폭을 절약할 수 있지만, FGameplayCueParameters로 변환되는 과정에서 정보가 일부 손실될 수 있으며, 이는 각 GameplayCue가 요구하는 정보에 따라 다를 수 있습니다.
4.8.8 GameplayCue 이벤트
GameplayCue는 특정 EGameplayCueEvent에 반응합니다:
EGameplayCueEvent | 설명 |
OnActive | GameplayCue가 활성화(추가)될 때 호출됩니다. |
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. GameplayCue가 활성 상태일 때 호출되며, 실제로 바로 적용되지 않았더라도 진행 중인 경우에 호출됩니다(진행 중 조인 등). 이는 Tick이 아니며, GameplayCueNotify_Actor가 추가되거나 유효해질 때 한 번만 호출됩니다. Tick()이 필요하면 GameplayCueNotify_Actor의 Tick()을 사용해야 합니다. 결국 이것은 AActor입니다. |
Removed | GameplayCue가 제거될 때 호출됩니다. 이 이벤트에 응답하는 블루프린트 GameplayCue 함수는 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가 실행될 때 호출됩니다: Instant Effect나 Periodic(주기적인) Tick(). 이 이벤트에 응답하는 블루프린트 GameplayCue 함수는 OnExecute입니다. |
GameplayCue 시작 시 발생하는 GameplayCue의 모든 이펙트에 OnActive를 사용하되, 늦게 참여하는 플레이어가 놓쳐도 괜찮은 경우 사용합니다. WhileActive는 GameplayCue에서 지속적으로 발생하는 효과에 사용하며, 늦게 합류한 플레이어도 볼 수 있도록 해야 합니다. 예를 들어 MOBA에서 타워 구조물이 폭발하는 GameplayCue가 있을 때, 초기 폭발 파티클 시스템과 폭발 사운드는 OnActive에 넣고 폭발 후 지속적으로 발생하는 불꽃 파티클이나 사운드는 WhileActive에 넣을 수 있습니다. 이 시나리오에서는 뒤늦게 합류한 플레이어가 초기 폭발을 OnActive에서 재생하는 것은 의미가 없지만, 폭발이 발생한 후 지면에 지속적이고 반복되는 불꽃 이펙트를 WhileActive에서 볼 수 있게 하려는 것입니다. OnRemove는 OnActive와 WhileActive에 추가된 모든 항목을 정리해야 합니다. WhileActive는 액터가 GameplayCueNotify_Actor의 연관성 범위에 들어올 때마다 호출됩니다. OnRemove는 액터가 GameplayCueNotify_Actor의 연관성 범위를 벗어날 때마다 호출됩니다.
4.8.9 GameplayCue 안전성
실행된 GameplayCue: 비신뢰성 멀티캐스트(Unreliable Multicast)를 통해 적용되며 항상 신뢰성이 보장되지 않습니다.
GameplayEffect에서 적용된 GameplayCue입니다:
- Autonomous Proxy는 OnActive, WhileActive, OnRemove 이벤트를 신뢰성 있게 수신합니다. FActiveGameplayEffectsContainer::NetDeltaSerialize()는 OnActive와 WhileActive를 호출하기 위해 UAbilitySystemComponent::HandleDeferredGameplayCues()를 실행합니다. FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers()는 OnRemoved를 호출합니다.
- Simulated Proxy는 WhileActive와 OnRemove를 신뢰성 있게 수신합니다. UAbilitySystemComponent::MinimalReplicationGameplayCues의 리플리케이션은 WhileActive와 OnRemove를 호출합니다. OnActive 이벤트는 비신뢰성 멀티캐스트에 의해 호출됩니다.
GameplayEffect 없이 적용된 GameplayCue입니다:
- Autonomous Proxy는 OnRemove를 신뢰성 있게 수신합니다. OnActive와 WhileActive 이벤트는 비신뢰성 멀티캐스트로 호출됩니다.
- Simulated Proxy는 WhileActive와 OnRemove를 신뢰성 있게 수신합니다. UAbilitySystemComponent::MinimalReplicationGameplayCues의 리플리케이션은 WhileActive와 OnRemove를 호출합니다. OnActive 이벤트는 비신뢰성 멀티캐스트에 의해 호출됩니다.
GameplayCue에서 신뢰성이 필요한 경우, 해당 GameplayCue를 GameplayEffect를 통해 적용하고, WhileActive에서 FX를 추가하며 OnRemove에서 FX를 제거하도록 설정하세요.
4.9 전역 AbilitySystem
AbilitySystemGlobals 클래스는 GAS에 대한 전역 정보를 담고 있습니다. 대부분의 변수는 DefaultGame.ini에서 설정할 수 있습니다. 일반적으로 이 클래스와 상호작용할 필요는 없지만, 그 존재를 알고 있어야 합니다. GameplayCueManager 또는 GameplayEffectContext와 같은 것을 서브클래싱해야 하는 경우, AbilitySystemGlobals를 통해 서브클래싱해야 합니다.
AbilitySystemGlobals 클래스는 GAS에 대한 전역 정보를 보유합니다. 대부분의 변수는 DefaultGame.ini에서 설정할 수 있습니다. 일반적으로 이 클래스를 직접 다룰 필요는 없지만, 그 존재에 대해 알고 있어야 합니다. 예를 들어 GameplayCueManager나 GameplayEffectContext 같은 것을 서브클래싱하려면 AbilitySystemGlobals를 통해 이를 처리해야 합니다.
AbilitySystemGlobals를 서브클래싱하려면 DefaultGame.ini에서 클래스 이름을 설정하세요:
[/Script/GameplayAbilities.AbilitySystemGlobals]
AbilitySystemGlobalsClassName="/Script/ParagonAssets.PAAbilitySystemGlobals"
4.9.1 InitGlobalData()
UE 4.24 ~ 5.2 사이에서는 TargetData를 사용하기 위해 UAbilitySystemGlobals::Get().InitGlobalData()를 호출해야 하며, 그렇지 않을 경우 ScriptStructCache와 관련된 오류가 발생하고 클라이언트가 서버와 연결이 끊길 수 있습니다. 이 함수는 프로젝트에서 한 번만 호출하면 됩니다. 만약 AbilitySystemGlobals의 GlobalAttributeSetDefaultsTableNames를 사용하는 도중 충돌이 발생한다면, Fortnite처럼 AssetManager나 GameInstance에서 UAbilitySystemGlobals::Get().InitGlobalData()를 나중에 호출해야 할 수도 있습니다. 는 이 함수를 UAssetManager::StartInitialLoading()에서 호출했고, Paragon은 UEngine::Init()에서 호출했습니다. 샘플 프로젝트에서는 이를 UAssetManager::StartInitialLoading()에 배치하는 것이 좋은 방법으로 제시됩니다. 이 코드는 TargetData 문제를 방지하기 위해 프로젝트에 복사해 사용하는 기본 코드로 간주할 수 있습니다. UE 5.3부터는 이 함수가 자동으로 호출됩니다.
만약 AbilitySystemGlobals의 GlobalAttributeSetDefaultsTableNames를 사용하는 도중 크래시가 발생한다면, Fortnite처럼 AssetManager나 GameInstance에서 UAbilitySystemGlobals::Get().InitGlobalData()를 나중에 호출해야 할 수도 있습니다.
4.10 예측(Prediction)
GAS는 클라이언트 측 예측을 기본적으로 지원하지만, 모든 것을 예측하지는 않습니다. GAS에서의 클라이언트 측 예측은 클라이언트가 서버의 승인을 기다리지 않고 GameplayAbility를 활성화하고 GameplayEffect를 적용할 수 있다는 의미입니다. 클라이언트는 서버가 이를 허용할 것이라고 예측하고, 예측한 대로 타겟에 GameplayEffect를 적용합니다. 그 후 서버는 클라이언트가 예측한 것이 맞았는지 여부를 알려줍니다. 만약 클라이언트가 잘못 예측했다면, 서버와 일치하도록 변경 사항을 롤백합니다.
GAS 관련 예측의 결정적인 출처는 GameplayPrediction.h에 있는 플러그인 소스 코드입니다. 에픽 게임즈의 마인드셋은 할 수 있는 것만 예측하라입니다. 예를 들어, Paragon과 Fortnite는 피해를 예측하지 않습니다. 대부분 ExecutionCalculation를 사용하여 피해를 처리하며, 이는 예측할 수 없습니다. 그렇다고 해서 피해 같은 것을 예측할 수 없다는 것은 아닙니다. 만약 예측이 잘 된다면, 그렇게 하는 것도 좋습니다.
"모든 것을 완벽하게 자동으로 예측하는" 솔루션에 올인하는 것도 아닙니다. 저희는 여전히 플레이어 예측을 최소한으로 유지하는 것이 가장 좋다고 생각합니다(즉, 플레이어가 피할 수 있는 최소한의 것만 예측하는 것이 좋습니다).
Network Prediction Plugin에 대한 에픽 게임즈의 데이브 라티의 코멘트.
예측되는 것:
- Ability 활성화
- 트리거된 이벤트
- GameplayEffect 적용
- Attribute 수정(예외: Execution은 전혀 예측되지 않으며, Attribute Modifier에서 예측됩니다.)
- GameplayTag 수정
- GameplayCue 이벤트(예측된 GameplayEffect와 그 자체로도 가능합니다.)
- 몽타주
- 움직임 (UE5 UCharacterMovement 내장)
예측할 수 없는 것:
- GameplayEffect 제거
- GameplayEffect 주기적 효과 (예: 지속 피해)
From GameplayPrediction.h
우리는 GameplayEffect의 적용은 예측할 수 있지만, GameplayEffect의 제거는 예측할 수 없습니다. 이 제한을 해결하기 위한 방법 중 하나는, GameplayEffect를 제거하려고 할 때 그 반대 효과를 예측하는 것입니다. 예를 들어, 40%의 이동 속도 감소를 예측한다고 가정합시다. 이를 예측적으로 제거하려면 40%의 이동 속도 증가를 적용한 후, 두 개의 GameplayEffect를 동시에 제거하는 방법을 사용할 수 있습니다. 이 방법은 모든 시나리오에 적합하지 않으며, GameplayEffect 제거 예측에 대한 지원은 여전히 필요합니다. Dave Ratti는 이 기능을 GAS의 향후 버전에 추가하고자 하는 의사를 표명했습니다.
GameplayEffect 제거를 예측할 수 없기 때문에 GameplayAbility의 Cooldown도 완전히 예측할 수 없습니다. Cooldown GameplayEffect에 대한 반대 효과는 없기 때문에, 서버의 Cooldown GE는 클라이언트에 복제되어 있으며, 이를 우회하려는 시도(예: Minimal 리플리케이션 모드)는 서버에서 거부됩니다. 즉, 높은 지연 시간을 가진 클라이언트는 서버에 Cooldown을 요청하고 서버의 Cooldown GE 제거를 받는 데 더 오랜 시간이 걸립니다. 이로 인해 높은 지연 시간을 가진 플레이어는 낮은 지연 시간을 가진 플레이어들보다 더 낮은 발사 속도를 가지게 되어, 낮은 지연 시간의 플레이어들에게 불리한 상황이 발생합니다. Fortnite는 이 문제를 커스텀 기록 관리로 해결합니다.
피해 예측에 대해 저는 개인적으로 추천하지 않습니다. 이는 많은 사람들이 GAS를 처음 시작할 때 가장 먼저 시도하는 부분입니다. 특히 죽음 예측은 추천하지 않습니다. 피해를 예측할 수 있지만, 이를 잘못 예측하는 것은 어려운 문제입니다. 예를 들어, 적의 피해를 예측하는 데 실패하면, 플레이어는 적의 체력이 갑자기 회복된 것을 볼 수 있습니다. 죽음 예측을 시도하면 특히 불편하고 혼란스러울 수 있습니다. 예를 들어, 캐릭터가 죽었다고 예측하여 Ragdoll 상태가 시작되었지만, 서버가 이를 수정하면 Ragdoll이 멈추고 계속해서 플레이어를 공격할 수 있습니다.
💡Note: 속성을 변경하는 인스턴트 GameplayEffect(예: Cost GE)는 본인에 대해 원활하게 예측할 수 있으며, 다른 캐릭터에 대한 인스턴트 속성 변경을 예측하면 해당 캐릭터의 속성에 짧은 이상 현상 또는 '깜박임'이 표시됩니다. 예측된 인스턴트 GameplayEffect는 실제로 무한 GameplayEffect와 같이 취급되므로 예측이 잘못되었을 경우 롤백할 수 있습니다. 서버의 GameplayEffect가 적용될 때, 동일한 GameplayEffect가 두 개 존재하여 모디파이어가 두 번 적용되거나 잠시 동안 전혀 적용되지 않을 수 있습니다. 결국에는 저절로 수정되지만 때때로 플레이어가 이 문제를 알아차릴 수 있습니다.
Instant GameplayEffect(예: Cost GE)는 자신에게 Attribute를 예측하는 데 원활하게 예측할 수 있습니다. 그러나 다른 캐릭터에 대한 Instant Attribute 변경을 예측하면, 그들의 Attribute에 잠시 간격이 생기거나 깜빡임(Blip)이 나타날 수 있습니다. 예측된 Instant GameplayEffect는 Infinite GameplayEffect처럼 처리되어 잘못 예측되었을 경우 롤백할 수 있습니다. 서버의 GameplayEffect가 적용되면 두 개의 동일한 GameplayEffect가 존재하게 되어 잠시 동안 Modifier가 두 번 적용되거나 적용되지 않을 수 있습니다. 결국 수정되지만, 이 깜빡임은 플레이어에게 눈에 띄는 경우가 있을 수 있습니다.
GAS의 예측 구현이 해결하려는 문제들:
- "해도 되는가?" 예측을 위한 기본 프로토콜
- "실행 취소" 예측이 실패했을 때 부작용을 되돌리는 방법
- "재실행" 클라이언트가 예측한 부작용을 서버에서 리플리케이트하여 다시 실행하지 않도록 하는 방법
- "완전성" 모든 부작용을 진정으로 예측했는지 확인하는 방법
- "종속성" 예측된 이벤트와 의존적인 이벤트의 관리 방법
- "재정의" 서버에서 리플리케이되거나 소유된 상태를 예측적으로 오버라이드하는 방법
From GameplayPrediction.h
4.10.1 예측 키(Prediction Key)
GAS의 예측은 예측 키(Prediction Key)라는 개념을 기반으로 작동하며, 이는 클라이언트가 GameplayAbility를 활성화할 때 생성하는 정수 식별자입니다.
- 클라이언트는 GameplayAbility를 활성화할 때 예측 키를 생성합니다. 이것이 Activation Prediction Key입니다.
- 클라이언트는 이 예측 키를 CallServerTryActivateAbility()와 함께 서버에 전송합니다.
- 클라이언트는 예측 키가 유효한 동안 적용하는 모든 GameplayEffect에 이 예측 키를 추가합니다.
- 클라이언트의 예측 키가 범위를 벗어나면, 같은 GameplayAbility에서 추가로 예측된 효과에는 새로운 Scoped Prediction Window가 필요합니다.
- 서버는 클라이언트로부터 예측 키를 받습니다.
- 서버는 자신이 적용하는 모든 GameplayEffect에 이 예측 키를 추가합니다.
- 서버는 예측 키를 클라이언트에게 다시 리플리케이트하여 전송합니다.
- 클라이언트는 서버로부터 리플리케이트된 GameplayEffect를 예측 키와 함께 수신합니다. 클라이언트가 적용한 GameplayEffect와 동일한 예측 키를 가진 리플리케이트된 GameplayEffect가 일치하면, 이는 올바르게 예측된 것입니다. 이때 대상에는 잠시 두 개의 GameplayEffect가 존재하게 되며, 클라이언트는 예측한 것을 제거합니다.
- 클라이언트는 서버로부터 예측 키를 다시 받습니다. 이것이 Replicated Prediction Key입니다. 이 예측 키는 이제 stale(유효하지 않음)로 표시됩니다.
- 클라이언트는 이제 stale한 Replicated Prediction Key로 생성한 모든 GameplayEffect를 제거합니다. 서버에서 복제된 GameplayEffect는 계속 유지됩니다. 클라이언트가 추가했지만 서버에서 일치하는 복제본을 받지 못한 GameplayEffect는 잘못 예측된 것입니다.
예측 키는 GameplayAbility에서 Activation Prediction Key로 시작되는 명령어 window를 통해 원자적으로 그룹화하는 동안 유효하도록 보장됩니다. 이를 한 프레임 동안만 유효한 것으로 생각할 수 있습니다. 지연된 작업을 처리하는 AbilityTask의 콜백은 더 이상 유효한 예측 키를 가지지 않으며, Synch Point가 내장된 AbilityTask가 새로운 Scoped Prediction Window를 생성해야만 예측 키가 유효합니다.
4.10.2 Ability에서 새로운 예측 창 만들기
AbilityTask의 콜백에서 더 많은 작업을 예측하려면, 새로운 Scoped Prediction Window와 새로운 Scoped Prediction Key를 생성해야 합니다. 이것은 클라이언트와 서버 간의 Synch Point라고도 불립니다. 입력과 관련된 모든 AbilityTask는 기본적으로 새로운 scoped prediction window를 생성하는 기능을 가지고 있어서, AbilityTask의 콜백에서 실행되는 원자적 코드에는 유효한 scoped prediction key가 제공됩니다.
그러나 WaitDelay와 같은 다른 AbilityTask는 콜백에 대한 새로운 scoped prediction window를 생성하는 기본 코드를 제공하지 않습니다. WaitDelay와 같이 기본 코드가 없는 AbilityTask 후에 행동을 예측해야 할 경우, WaitNetSync AbilityTask를 사용하여 수동으로 새로운 scoped prediction window를 생성해야 합니다. OnlyServerWait 옵션을 사용한 WaitNetSync에 도달하면, 클라이언트는 GameplayAbility의 활성화 예측 키를 기반으로 새로운 scoped prediction key를 생성하고 이를 서버에 RPC로 전송한 후, 새로운 GameplayEffect에 이를 추가합니다.
서버는 OnlyServerWait 옵션이 있는 WaitNetSync에 도달하면, 클라이언트로부터 새로운 scoped prediction key를 받기 전까지 기다립니다. 이 scoped prediction key는 activation prediction key와 동일한 방식으로 작동하며, GameplayEffect에 적용되고 클라이언트로 복제되어 stale로 표시됩니다. scoped prediction key는 범위에서 벗어날 때까지 유효하며, 즉 scoped prediction window가 닫힐 때까지 유효합니다. 다시 말해, 원자적 작업만 새 scoped prediction key를 사용할 수 있으며, 지연된 작업은 사용할 수 없습니다.
필요한 만큼 여러 개의 scoped prediction window를 생성할 수 있습니다.
자신의 AbilityTask에 synch point 기능을 추가하고 싶다면, 입력 관련 AbilityTask들이 WaitNetSync AbilityTask 코드를 어떻게 주입하는지 살펴보세요.
💡Note: WaitNetSync를 사용할 때, 이는 서버의 GameplayAbility가 클라이언트로부터 정보를 받을 때까지 실행을 차단합니다. 이는 악의적인 사용자가 게임을 해킹하여 새로운 scoped prediction key를 보내는 것을 의도적으로 지연시킬 수 있기 때문에 잠재적으로 악용될 수 있습니다. Epic은 WaitNetSync를 신중하게 사용하고 있으며, 이러한 문제가 우려되는 경우 클라이언트 없이 자동으로 계속 진행되는 지연을 포함한 새로운 버전의 AbilityTask를 빌드하는 것을 권장합니다.
샘플 프로젝트에서는 Sprint GameplayAbility에서 stamina cost를 적용할 때마다 새로운 scoped prediction window를 생성하기 위해 WaitNetSync를 사용하여 이를 예측할 수 있도록 하고 있습니다. 이상적으로는 Cost와 Cooldown을 적용할 때 유효한 예측 키를 갖는 것이 좋습니다.
예측된 GameplayEffect가 소유 클라이언트에서 두 번 재생된다면, 예측 키가 stale되고 redo 문제가 발생한 것입니다. 보통은 GameplayEffect를 적용하기 전에 WaitNetSync AbilityTask를 OnlyServerWait 옵션으로 배치하여 새로운 scoped prediction key를 생성하면 이 문제를 해결할 수 있습니다.
4.10.3 액터 생성 예측
클라이언트에서 예측적으로 Actor를 생성하는 것은 고급 주제입니다. GAS에서는 이 기능을 기본적으로 제공하지 않으며, SpawnActor AbilityTask는 서버에서만 Actor를 생성합니다. 핵심 개념은 서버와 클라이언트 모두에서 리플리케이트된 Actor를 생성하는 것입니다.
만약 Actor가 단순히 장식용이거나 게임 플레이에 영향을 미치지 않는다면, 간단한 해결책은 Actor의 IsNetRelevantFor() 함수를 오버라이드하여 서버가 해당 클라이언트로 리플리케이트되는 것을 제한하는 것입니다. 이렇게 하면 소유 클라이언트는 로컬에서 생성된 Actor를 사용하고, 서버와 다른 클라이언트는 서버의 리플리케이트된 Actor를 사용하게 됩니다.
bool APAReplicatedActorExceptOwner::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
return !IsOwnedBy(ViewTarget);
}
이 코드는 소유자가 아닌 클라이언트만 리플리케이된 Actor를 받을 수 있도록 하여, 각 클라이언트가 소유한 로컬 버전을 사용하게 합니다.
만약 생성된 Actor가 게임플레이에 영향을 미치는 경우(예: 투사체처럼 피해 예측이 필요한 경우), 고급 로직이 필요하며 이는 이 문서의 범위를 벗어납니다. 예를 들어, Unreal Tournament에서는 투사체를 소유 클라이언트에서만 더미로 생성하고, 서버에서 리플리케이트된 투사체와 동기화하는 방법을 사용하고 있습니다. 이 방법은 에픽 게임즈의 GitHub에서 예측적으로 투사체를 생성하는 방법을 확인할 수 있습니다.
4.10.4 GAS 예측 기능의 향후 발전
GameplayPrediction.h에서는 향후 GameplayEffect 제거 및 주기적인 GameplayEffect의 예측을 추가할 수 있는 기능을 제공할 수 있다고 명시되어 있습니다.
에픽 게임즈의 데이브 라티는 Cooldown 예측에서 발생하는 지연 시간 불일치 문제를 해결하려는 관심을 표명했으며, 이 문제는 높은 지연 시간을 가진 플레이어가 낮은 지연 시간을 가진 플레이어보다 불리한 상황을 초래할 수 있습니다.
에픽 게임즈의 새로운 Network Prediction 플러그인은 이전의 CharacterMovementComponent와 마찬가지로 GAS와 완전히 호환될 것으로 예상됩니다. 이 플러그인은 GAS의 예측 기능을 개선하고, 예측을 보다 원활하게 처리할 수 있도록 도와줄 것입니다.
4.10.5 Network Prediction Plugin
에픽 게임즈는 CharacterMovementComponent를 새로운 네트워크 예측 플러그인으로 대체하는 프로젝트를 시작했습니다. 해당 플러그인은 아직 초기 단계에 있지만 언리얼 엔진 깃허브에서 얼리 액세스로 이용할 수 있습니다. 향후 언리얼 엔진의 어떤 버전에서 실험적 베타 버전으로 출시될지는 아직 알 수 없습니다.
4.11 Targeting
4.11.1 Target Data
FGameplayAbilityTargetData는 네트워크를 통해 전달될 Target Data용으로 설계된 일반 구조체입니다. Target Data에는 보통 AActor/UObject 레퍼런스, FHitResults, 그 이외의 위치/방향/원점 정보 등이 포함됩니다. 또한, 해당 구조체를 서브클래싱하면 원하는 데이터를 포함할 수 있어, 클라이언트와 서버 간 데이터를 간편하게 주고 받을 수 있습니다. FGameplayAbilityTargetData는 기본적으로 직접 사용되기보다는 서브클래싱하여 사용하는 것을 목적으로 합니다. GAS는 이미 몇 가지 서브클래싱된 FGameplayAbilityTargetData 구조체를 제공하며, 이는 GameplayAbilityTargetTypes.h에 정의되어 있습니다.
TargetData는 일반적으로 Target Actor에 의해 생성되거나 수동으로 만들어지며, AbilityTask 및 GameplayEffect에서 EffectContext를 통해 소비됩니다. EffectContext에 포함된 덕분에 Execution, MMC, GameplayCue, 그리고 AttributeSet의 백엔드 함수에서 TargetData에 접근할 수 있습니다.
일반적으로 FGameplayAbilityTargetData를 직접 전달하지 않고, FGameplayAbilityTargetDataHandle을 사용합니다. 이 핸들 구조체는 내부적으로 FGameplayAbilityTargetData 포인터를 담은 TArray를 가지고 있으며, 이를 통해 TargetData의 다형성을 지원합니다.
FGameplayAbilityTargetData를 상속한 예제입니다:
USTRUCT(BlueprintType)
struct MYGAME_API FGameplayAbilityTargetData_CustomData : public FGameplayAbilityTargetData
{
GENERATED_BODY()
public:
FGameplayAbilityTargetData_CustomData()
{
}
UPROPERTY()
FName CoolName = NAME_None;
UPROPERTY()
FPredictionKey MyCoolPredictionKey;
// FGameplayAbilityTargetData를 상속한 모든 하위 구조체에서 필수입니다.
virtual UScriptStruct* GetScriptStruct() const override
{
return FGameplayAbilityTargetData_CustomData::StaticStruct();
}
// 이는 FGameplayAbilityTargetData를 상속한 모든 하위 구조체에 필요합니다.
bool NetSerialize(FArchive& Ar, class UPackageMap* Map, bool& bOutSuccess)
{
// 엔진은 이미 FName과 FPredictionKey에 대해 NetSerialize를 정의했습니다. 감사합니다, 에픽 게임즈!
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
};
};
핸들에 Target Data를 추가하는 방법:
UFUNCTION(BlueprintPure)
FGameplayAbilityTargetDataHandle MakeTargetDataFromCustomName(const FName CustomName)
{
// 우리의 Target Data 타입을 생성합니다.
// 핸들은 소멸될 때 데이터를 자동으로 정리하고 삭제합니다.
// 만약 이 데이터를 핸들에 추가하지 않는다면 메모리 관리와 메모리 누수 문제가 발생할 수 있으니,
// 안전하게 프레임 내 어느 시점에라도 항상 핸들에 추가하는 것이 좋습니다!
FGameplayAbilityTargetData_CustomData* MyCustomData = new FGameplayAbilityTargetData_CustomData();
// 구조체의 정보를 설정하여 입력된 이름과 우리가 원하는 다른 변경 사항을 적용합니다.
MyCustomData->CoolName = CustomName;
// Blueprint에서 사용할 핸들 래퍼를 만듭니다.
FGameplayAbilityTargetDataHandle Handle;
// 타겟 데이터를 핸들에 추가합니다.
Handle.Add(MyCustomData);
// 핸들을 Blueprint로 출력합니다.
return Handle;
}
값을 가져오기 위해서는 타입 안전성 검사가 필요합니다. 핸들의 Target Data에서 값을 가져오는 유일한 방법은 기본 C/C++ 캐스팅을 사용하는 것이지만, 이는 타입 안정성을 보장하지 않으므로 객체 슬라이싱이나 크래시를 유발할 수 있습니다. 타입 검사를 수행하는 방법에는 여러 가지가 있으며, 원하는 방식으로 구현할 수 있습니다. 하지만 흔히 사용되는 두 가지 방법은 다음과 같습니다.
- GameplayTag: 서브클래스 계층 구조를 사용하여 특정 코드 아키텍처의 기능이 실행될 때, 기본 부모 타입으로 캐스팅하고 해당 객체의 GameplayTag를 가져옵니다. 그런 다음, 이를 기반으로 자식 클래스의 태그와 비교하여 해당 자식 클래스로 캐스팅할 수 있습니다.
- Script Struct & Static Struct: 직접적인 클래스 비교를 수행하는 방법입니다. 여기에는 많은 if 문을 사용하거나 템플릿 함수를 작성하는 과정이 포함될 수 있습니다. 아래는 이러한 방식의 예제입니다. FGameplayAbilityTargetData에서 Script Struct를 가져올 수 있다는 점을 활용합니다. 해당 기능은 USTRUCT로 정의된 구조체를 사용하며, 상속된 클래스는 GetScriptStruct에서 구조체 타입을 명시해야 하기 때문에 가능합니다. 이를 통해 원하는 타입인지 확인하고 비교할 수 있습니다.
아래는 이러한 함수들을 사용하여 타입 검사를 수행하는 예제입니다:
UFUNCTION(BlueprintPure)
FName GetCoolNameFromTargetData(const FGameplayAbilityTargetDataHandle& Handle, const int Index)
{
// NOTE: ::Get(int32 Index) 함수에는 두 가지 버전이 있습니다;
// 1) const 버전은 const FGameplayAbilityTargetData*를 반환하며, Target Data 값을 읽기에 적합합니다.
// 2) non-const 버전은 FGameplayAbilityTargetData*를 반환하며, Target Data 값을 수정하기에 적합합니다.
FGameplayAbilityTargetData* Data = Handle.Get(Index); // 이는 인덱스를 유효성 검사해줍니다.
// 사용할 수 있는 데이터가 있는지 확인, null 데이터는 캐스팅할 수 없음을 의미합니다.
if (Data == nullptr)
{
return NAME_None;
}
// 이것은 기본적으로 타입 검사 단계입니다. static_cast는 타입 안전성이 없기 때문에 이 검사를 수행합니다.
// 이 검사를 하지 않으면 구조체가 객체 슬라이싱되어 타입을 확인할 방법이 없어집니다.
if (Data->GetScriptStruct() == FGameplayAbilityTargetData_CustomData::StaticStruct())
{
// 이제 캐스팅을 하는 부분입니다. 이미 올바른 타입임을 알기 때문에 안심하고 캐스팅할 수 있습니다.
FGameplayAbilityTargetData_CustomData* CustomData = static_cast<FGameplayAbilityTargetData_CustomData*>(Data);
return CustomData->CoolName;
}
return NAME_None;
}
4.11.2 Target Actor
GameplayAbility는 WaitTargetData AbilityTask와 함께 TargetActor를 생성하여, World에서 타겟 정보를 시각화하고 캡처할 수 있습니다. TargetActor는 선택적으로 GameplayAbilityWorldReticle을 사용하여 현재 타겟을 표시할 수 있습니다. 타겟 정보가 확인되면, 해당 정보는 TargetData로 반환되어 GameplayEffect에 전달될 수 있습니다.
TargetActor는 AActor를 기반으로 하므로 타겟이 어디에 있는지, 어떻게 타겟팅하는지를 나타내기 위해 정적 메시나 데칼과 같은 시각적 컴포넌트를 가질 수 있습니다. 정적 메시를 사용하여 캐릭터가 생성할 물체의 배치 위치를 시각화하거나, 데칼을 사용하여 땅에 영향을 미치는 영역을 표시할 수 있습니다. 샘플 프로젝트에서는 Meteor 능력의 피해 범위를 나타내기 위해 땅에 데칼을 사용하는 AGameplayAbilityTargetActor_GroundTrace를 사용합니다. 그러나 어떤 경우에는 표시할 것이 없을 수도 있습니다. 예를 들어, GASShooter에서처럼 즉시 타겟을 추적하는 히트스캔 총의 경우, 아무것도 표시할 필요가 없을 수 있습니다.
TargetActor는 기본적으로 트레이스나 충돌 오버랩을 사용하여 타겟팅 정보를 캡처하고, 그 결과를 FHitResult 또는 AActor 배열로 변환하여 TargetData로 전달합니다. WaitTargetData AbilityTask는 TEnumAsByteEGameplayTargetingConfirmation::Type ConfirmationType 매개변수를 통해 타겟이 확인될 시점을 결정합니다. TEnumAsByteEGameplayTargetingConfirmation::Type::Instant을 사용하지 않는 경우, TargetActor는 일반적으로 Tick()에서 트레이스/오버랩을 수행하고 구현에 따라 FHitResult에 위치를 업데이트합니다. 이 방법은 Tick()에서 트레이스/오버랩을 수행하지만, 복제되지 않으며 보통 동시에 실행되는 TargetActor가 하나뿐이므로 성능에 큰 영향을 미치지 않습니다. 다만, 복잡한 TargetActor는 Tick()에서 많은 작업을 할 수 있으므로 성능에 부담이 될 수 있습니다. Tick()에서 트레이스를 하는 것은 클라이언트에서 매우 반응성이 좋지만, 성능 저하가 너무 크다면 TargetActor의 틱 속도를 낮추는 것을 고려할 수 있습니다. TEnumAsByteEGameplayTargetingConfirmation::Type::Instant을 사용하는 경우, TargetActor는 즉시 생성되어 TargetData를 생성한 후 바로 파괴됩니다. 이 경우 Tick()은 호출되지 않습니다.
EGameplayTargetingConfirmation::Type | 타겟이 확인되는 시점 |
Instant | 타겟팅은 즉시 발생하며 특별한 로직이나 사용자 입력이 필요하지 않습니다. |
UserConfirmed | 타겟팅은 사용자가 Ability에 바인딩된 Confirm 입력을 통해 또는 UAbilitySystemComponent::TargetConfirm()을 호출하여 확인되었을 때 발생합니다. TargetActor는 바인딩된 Cancel 입력 또는 UAbilitySystemComponent::TargetCancel() 호출에 의해 타겟팅을 취소할 수도 있습니다. |
Custom | GameplayTargeting Ability가 UGameplayAbility::ConfirmTaskByInstanceName()을 호출하여 타겟팅 데이터가 준비된 시점을 결정합니다. TargetActor는 또한 UGameplayAbility::CancelTaskByInstanceName()을 호출하여 타겟팅을 취소할 수 있습니다. |
CustomMulti | GameplayTargeting Ability가 UGameplayAbility::ConfirmTaskByInstanceName()을 호출하여 타겟팅 데이터가 준비된 시점을 결정합니다. TargetActor는 또한 UGameplayAbility::CancelTaskByInstanceName()을 호출하여 타겟팅을 취소할 수 있습니다. 데이터 생성 시 AbilityTask를 종료하지 않아야 합니다. |
모든 EGameplayTargetingConfirmation::Type이 모든 TargetActor에서 지원되는 것은 아닙니다. 예를 들어, AGameplayAbilityTargetActor_GroundTrace는 Instant 확인을 지원하지 않습니다.
WaitTargetData AbilityTask는 AGameplayAbilityTargetActor 클래스를 매개변수로 받아, AbilityTask가 활성화될 때마다 인스턴스를 생성하고 AbilityTask가 종료되면 TargetActor를 파괴합니다. WaitTargetDataUsingActor AbilityTask는 이미 생성된 TargetActor를 매개변수로 받지만, 여전히 AbilityTask가 종료될 때 TargetActor를 파괴합니다. 이 두 AbilityTask는 새로운 TargetActor를 생성하거나 요구하므로 비효율적일 수 있습니다. 이는 프로토타입에 적합하지만, 자동 소총처럼 지속적으로 TargetData를 생성해야 하는 경우, 최적화를 고려할 수 있습니다. GASShooter는 AGameplayAbilityTargetActor의 사용자 정의 서브클래스와 새로 작성된 WaitTargetDataWithReusableActor AbilityTask를 사용하여 TargetActor를 재사용하고 파괴하지 않습니다.
TargetActor는 기본적으로 복제되지 않지만, 게임에서 다른 플레이어에게 로컬 플레이어의 타겟팅 위치를 보여줄 필요가 있다면 복제할 수 있습니다. TargetActor는 WaitTargetData AbilityTask를 통해 서버와 통신하는 기본 기능을 포함하고 있습니다. 만약 TargetActor의 ShouldProduceTargetDataOnServer 속성이 false로 설정되어 있다면, 클라이언트는 타겟팅이 확인되면 TargetData를 서버로 RPC를 통해 전송합니다. 만약 ShouldProduceTargetDataOnServer가 true라면, 클라이언트는 EAbilityGenericReplicatedEvent::GenericConfirm을 RPC로 서버에 보내고, 서버는 이를 받아 트레이스 또는 오버랩을 수행하여 서버에서 데이터를 생성합니다. 클라이언트가 타겟팅을 취소하면, EAbilityGenericReplicatedEvent::GenericCancel을 RPC로 서버에 보내고, 서버는 이를 받아 타겟팅을 취소합니다. 이처럼 TargetActor와 WaitTargetData AbilityTask는 많은 델리게이트를 사용합니다. TargetActor는 타겟팅 데이터를 준비, 확인, 또는 취소하는 델리게이트를 방송하고, WaitTargetData는 이를 듣고 GameplayAbility 및 서버로 전달합니다. 서버로 TargetData를 보낼 때는 부정행위를 방지하기 위해 서버에서 데이터가 합리적인지 검증하는 것이 좋습니다. 서버에서 직접 TargetData를 생성하면 이 문제를 완전히 피할 수 있지만, 클라이언트에서 예측 오류가 발생할 수 있습니다.
사용하는 AGameplayAbilityTargetActor의 서브클래스에 따라, WaitTargetData AbilityTask 노드에서 여러 ExposeOnSpawn 매개변수가 노출됩니다. 일부 일반적인 매개변수는 다음과 같습니다:
Common TargetActor Parameter | 정의 |
Debug | true일 경우, TargetActor가 트레이스를 수행할 때마다 디버그 트레이싱/오버랩 정보를 그려냅니다. Shipping이 아닌 빌드에서만 표시됩니다. 일반적으로 non-Instant TargetActor는 Tick()에서 트레이스를 수행하므로 이 디버그 드로우 호출도 Tick()에서 발생합니다. |
Filter | [옵션] 트레이스/오버랩이 발생할 때 Actor를 필터링하는 특수 구조체입니다. 일반적으로 플레이어의 Pawn을 제외하거나 특정 클래스만 타겟팅하려는 경우 사용됩니다. 더 고급 사용 사례는 Target Data Filter을 참조하세요. |
Reticle Class | [옵션] TargetActor가 생성할 AGameplayAbilityWorldReticle의 서브클래스입니다. |
Reticle Parameters | [옵션] Reticle을 설정합니다. Reticle을 참조하세요. |
Start Location | 트레이싱이 시작될 위치를 설정하는 특수 구조체입니다. 보통 플레이어의 시점, 무기 총구, 또는 Pawn의 위치입니다. |
기본 타겟 액터 클래스를 사용하면 액터가 트레이스/오버랩에 직접 있을 때만 유효한 타깃이 됩니다. 트레이스/오버랩을 벗어나면(움직이거나 한눈을 팔면) 더 이상 유효하지 않습니다. 타겟 액터가 마지막으로 유효한 타깃을 기억하도록 하려면 커스텀 타깃 액터 클래스에 이 기능을 추가해야 합니다. 이를 퍼시스턴트 타깃이라고 부르는 이유는 타겟 액터가 확인 또는 취소를 받거나, 타겟 액터가 트레이스/오버랩에서 새로운 유효한 타깃을 찾거나, 타깃이 더 이상 유효하지 않게(소멸) 될 때까지 지속되기 때문입니다. GASShooter는 로켓 발사기의 보조 능력인 호밍 로켓 조준에 퍼시스턴트 타깃을 사용합니다.
기본 TargetActor 클래스에서는 Actor가 트레이스/오버랩 내에 있을 때만 유효한 타겟으로 간주됩니다. 트레이스/오버랩을 벗어나면 더 이상 유효하지 않습니다. TargetActor가 마지막 유효 타겟을 기억하도록 하려면 커스텀 TargetActor 클래스에서 이 기능을 추가해야 합니다. 이를 Persistent Target(지속 타겟)이라고 부르며, TargetActor가 확인 또는 취소를 받기 전까지, 새로운 유효 타겟을 찾기 전까지, 또는 타겟이 더 이상 유효하지 않으면 계속 유지됩니다. GASShooter는 로켓 발사기의 보조 능력에서 지속 타겟을 사용하여 유도 로켓 타겟팅을 구현합니다.
4.11.3 Target Data Filter 사용
Make GameplayTargetDataFilter와 Make Filter Handle 노드를 사용하여 플레이어의 Pawn을 제외하거나 특정 클래스만 선택할 수 있습니다. 더 고급 필터링이 필요한 경우, FGameplayTargetDataFilter를 서브클래싱하여 FilterPassesForActor 함수를 오버라이드할 수 있습니다.
USTRUCT(BlueprintType)
struct GASDOCUMENTATION_API FGDNameTargetDataFilter : public FGameplayTargetDataFilter
{
GENERATED_BODY()
/** 액터가 Filter를 통과하면 타겟팅되도록 true를 반환합니다 */
virtual bool FilterPassesForActor(const AActor* ActorToBeFiltered) const override;
};
하지만 이것은 Wait Target Data 노드에 바로 적용되지 않으며, 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(Reticle)는 즉시 확인되지 않은 TargetActor를 사용할 때 타겟팅 중인 대상을 시각화합니다. TargetActor는 모든 Reticle의 생성과 소멸 수명을 담당합니다. Reticle은 AActor이므로 화면 공간에서 항상 플레이어의 카메라를 향해 보이는 UMG 위젯을 표시하는 WidgetComponent와 같은 시각적 컴포넌트를 사용할 수 있습니다. Reticle은 자신이 어떤 AActor에 있는지 알지 못하지만, 커스텀 TargetActor에서 이 기능을 서브클래싱하여 추가할 수 있습니다. 일반적으로 TargetActor는 매 Tick()마다 Reticle의 위치를 타겟의 위치로 업데이트합니다.
GASShooter에서는 Reticle을 사용하여 로켓 발사기의 보조 능력인 유도 미사일의 잠금된 타겟을 표시합니다. 적의 빨간 표시가 Reticle이고, 유사한 흰색 이미지는 로켓 발사기의 조준선입니다.
Reticle은 디자이너가 개발할 수 있도록 Blueprint에서 구현할 수 있는 몇 가지 BlueprintImplementableEvent를 제공합니다:
/** bIsTargetValid 값이 변경될 때마다 호출됩니다. */
UFUNCTION(BlueprintImplementableEvent, Category = Reticle)
void OnValidTargetChanged(bool bNewValue);
/** bIsTargetAnActor 값이 변경될 때마다 호출됩니다. */
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);
Reticle은 TargetActor가 제공하는 FWorldReticleParameter를 선택적으로 사용할 수 있습니다. 기본 구조체는 하나의 변수인 FVector AOEScale만 제공합니다. 이 구조체는 서브클래싱이 가능하지만, TargetActor는 기본 구조체만 수용할 수 있습니다. 기본 TargetActor에서 이 구조체를 서브클래싱할 수 없다는 점은 다소 단기적인 시각으로 보입니다. 그러나 자신만의 커스텀 TargetActor를 만들면, 자신만의 커스텀 Reticle 파라미터 구조체를 제공하고, 이를 AGameplayAbilityWorldReticles의 서브클래스를 생성할 때 수동으로 전달할 수 있습니다.
Reticle은 기본적으로 리플리케이트되지 않지만, 로컬 플레이어가 타겟팅하는 대상을 다른 플레이어에게 표시할 필요가 있는 경우 리플리케이트할 수 있습니다.
Reticle은 기본 TargetActor를 사용할 경우 현재 유효한 타겟에만 표시됩니다. 예를 들어, AGameplayAbilityTargetActor_SingleLineTrace를 사용하여 타겟을 추적하는 경우, Reticle은 적이 추적 경로에 있을 때만 표시됩니다. 시선을 돌리면 적은 더 이상 유효한 타겟이 아니므로 Reticle은 사라집니다. Reticle이 마지막 유효한 타겟에 계속 표시되도록 하려면, TargetActor를 커스터마이징하여 마지막 유효한 타겟을 기억하고 그 위에 Reticle을 유지해야 합니다. 이러한 타겟을 지속 타겟(persistent target)이라고 하며, 이는 TargetActor가 확인 또는 취소를 받을 때, TargetActor가 새로운 유효한 타겟을 찾을 때, 또는 타겟이 더 이상 유효하지 않게 될 때까지 유지됩니다. GASShooter에서는 로켓 발사기의 보조 능력인 유도 미사일 타겟팅을 위해 지속 타겟을 사용합니다.
4.11.5 GameplayEffect Containers Targeting
GameplayEffectContainer는 Target Data를 생성하는 효율적인 방법을 옵션으로 제공합니다. 해당 타겟팅은 EffectContainer가 클라이언트와 서버에서 적용될 때 즉시 발생합니다. 이는 TargetActor보다 효율적이며, 타겟팅 객체의 CDO에서 실행되므로(액터를 생성하거나 파괴하지 않음) 성능이 뛰어납니다. 그러나 플레이어 입력이 없고, 확인 없이 즉시 발생하며, 취소할 수 없고, 클라이언트에서 서버로 데이터를 전송할 수 없습니다(두 곳에서 데이터가 생성). 이 방식은 인스턴트 트레이스와 충돌 오버랩에 잘 작동합니다. 에픽 게임즈의 Action RPG 샘플 프로젝트는 두 가지 예시 타겟팅 방법을 GameplayEffectContainer와 함께 제공합니다. 하나는 Ability 소유자를 타겟으로 하고, 다른 하나는 이벤트에서 TargetData를 가져오는 방식입니다. 또한, Blueprint에서 플레이어로부터 일정 오프셋(자식 Blueprint 클래스에서 설정)을 두고 인스턴트 구체 트레이스를 수행하는 예시도 구현되어 있습니다. URPGTargetType을 C++ 또는 Blueprint에서 서브클래스하여 자신만의 타겟팅 유형을 만들 수 있습니다.
'둥지 > Unreal' 카테고리의 다른 글
Unreal GAS(GameplayAbilitySystem) Documentation 번역글 5부(完) (0) | 2023.07.28 |
---|---|
언리얼 LNK 2001 Error (0) | 2023.07.07 |
Unreal GAS(GameplayAbilitySystem) Documentation 번역글 3부 (0) | 2023.06.06 |
언리얼 C4430 오류 (0) | 2023.05.30 |
Unreal GAS(GameplayAbilitySystem) Documentation 번역글 2부 (0) | 2023.05.27 |