[Unreal Engine] GAS Documentation 번역본

이매·2025년 6월 20일

Unreal GAS

목록 보기
7/7
post-thumbnail

충처 : tranek GASDocumentation

GAS 문서 번역본

해당 문서는 Unreal Engine 5의 멀티플레이어 샘플 프로젝트의 GameplayAbilitySystem 플러그인의 이해를 돕는 문서입니다. 이 문서는 공식 문서가 아니며, 프로젝트 역시 에픽게임즈의 검증을 받지 않았습니다. 따라서 문서 정보의 정확성을 보장하지 않습니다.

이 문서는 GAS의 주요 개념과 클래스들을 설명하고, 경험을 바탕으로 몇 가지 추가 코멘트를 제공하는 것을 목적으로 합니다.

샘플 프로젝트와 문서는 언리얼 엔진 5.3(UE5) 버전입니다. 이전 버전의 언리얼 엔진을 위한 이 문서의 브랜치가 있지만, 더 이상 지원되지 않으며 버그나 오래된 정보가 있을 수 있습니다. 사용 중인 엔진 버전과 일치하는 브랜치를 사용하시기 바랍니다.

GASShooter는 멀티플레이어 FPS/TPS를 위한 GAS의 심화 기술을 보여주는 또 다른 샘플 프로젝트입니다.

또한 언제나 플러그인의 원본 소스 코드를 참고하는 것이 좋습니다.

목차

  1. GameplayAbilitySystem Plugin의 소개
  2. 샘플 프로젝트
  3. GAS 프로젝트 세팅 방법
  4. GAS Concepts
    4.1 Ability System Component
         4.1.1 Replication Mode
         4.1.2 설정 및 초기화
    4.2 Gameplay Tags
         4.2.1 GameplayTag 변경에 응답하기
         4.2.2 플러그인 .ini 파일에서 GameplayTag 불러오기
    4.3 Attributes
         4.3.1 Attribute 정의
         4.3.2 BaseValue vs CurrentValue
         4.3.3 Meta Attributes
         4.3.4 Attribute 변경에 응답하기
         4.3.5 Derived Attributes
    4.4 Attribute Set
         4.4.1 AttributeSet 정의
         4.4.2 AttributeSet 설계
               4.4.2.1 개별 Attribute를 가진 서브 컴포넌트
               4.4.2.2 런타임에 AttributeSet 추가 및 제거하기
               4.4.2.3 아이템 Attribute (무기 탄약)
                     4.4.2.3.1 아이템에 일반 float 사용
                     4.4.2.3.2 아이템의 AttributeSet
                     4.4.2.3.3 아이템의 ASC
         4.4.3 Attribute 정의
         4.4.4 Attribute 초기화
         4.4.5 PreAttributeChange()
         4.4.6 PostGameplayEffectExecute()
         4.4.7 OnAttributeAggregatorCreated()
    4.5 Gameplay Effects
         4.5.1 Gameplay Effect 정의
         4.5.2 Gameplay Effect 적용
         4.5.3 Gameplay Effect 삭제
         4.5.4 Gameplay Effect Modifiers
               4.5.4.1 Multiply 및 Divide Modifiers
               4.5.4.2 Modifier의 GameplayTag
         4.5.5 Gameplay Effect Stacking(중첩)
         4.5.6 Ability 부여
         4.5.7 Gameplay Effect Tags
         4.5.8 면역
         4.5.9 Gameplay Effect Spec
               4.5.9.1 SetByCallers
         4.5.10 Gameplay Effect Context
         4.5.11 Modifier Magnitude Calculation
         4.5.12 Gameplay Effect Execution Calculation
               4.5.12.1 Sending Data to Execution Calculations
                     4.5.12.1.1 SetByCaller
                     4.5.12.1.2 Backing Data Attribute Calculation Modifier
                     4.5.12.1.3 Backing Data Temporary Variable Calculation Modifier
                     4.5.12.1.4 Gameplay Effect Context
         4.5.13 Custom Application Requirement
         4.5.14 Cost Gameplay Effect
         4.5.15 Cooldown Gameplay Effect
               4.5.15.1 Cooldown GameplayEffect의 남은 시간 얻어내기
               4.5.15.2 Cooldown 시작 및 종료 청취(Listening)
               4.5.15.3 Predicting Cooldowns
         4.5.16 Changing Active Gameplay Effect Duration
         4.5.17 런타임에서 GameplayEffect 동적 생성하기
         4.5.18 Gameplay Effect Containers
    4.6 Gameplay Abilities
         4.6.1 Gameplay Ability 정의
               4.6.1.1 Replication Policy(리플리케이션 정책)
               4.6.1.2 Server Respects Remote Ability Cancellation
               4.6.1.3 Replicate Input Directly(입력 직접 복제)
         4.6.2 ASC에 입력 바인딩
               4.6.2.1 GameplayAbility를 활성화하지 않고 입력 바인딩
         4.6.3 Ability 부여
         4.6.4 Ability 활성화
               4.6.4.1 패시브 Ability
               4.6.4.2 Activation Failed Tags
         4.6.5 Ability 취소
         4.6.6 활성화된 Ability 얻기
         4.6.7 Instancing Policy
         4.6.8 Net Execution Policy
         4.6.9 Ability Tags
         4.6.10 Gameplay Ability Spec
         4.6.11 Ability에 데이터 전달하기
         4.6.12 Ability Cost and Cooldown
         4.6.13 Ability 레벨업
         4.6.14 Ability Sets
         4.6.15 Ability Batching
         4.6.16 Net Security Policy
    4.7 Ability Tasks
         4.7.1 Ability Task 정의
         4.7.2 Custom Ability Tasks
         4.7.3 Ability Tasks 사용
         4.7.4 Root Motion Source Ability Tasks
    4.8 Gameplay Cues
         4.8.1 Gameplay Cue 정의
         4.8.2 Triggering Gameplay Cues
         4.8.3 Local Gameplay Cues
         4.8.4 Gameplay Cue Parameters
         4.8.5 Gameplay Cue Manager
         4.8.6 GameplayCue가 발동되지 않도록 방지
         4.8.7 Gameplay Cue Batching(일괄 처리)
               4.8.7.1 수동 RPC
               4.8.7.2 하나의 GE에 여러 개의 GC
         4.8.8 Gameplay Cue Events
         4.8.9 Gameplay Cue Reliability(신뢰성)
    4.9 Ability System Globals
         4.9.1 InitGlobalData()
    4.10 Prediction
         4.10.1 Prediction Key
         4.10.2 Ability에서 새로운 Prediction Windows 만들기
         4.10.3 액터 스폰 예측
         4.10.4 GAS Prediction 기능의 미래
         4.10.5 Network Prediction Plugin
    4.11 Targeting
         4.11.1 Target Data
         4.11.2 Target Actors
         4.11.3 Target Data Filters
         4.11.4 Gameplay Ability World Reticles
         4.11.5 Gameplay Effect Containers Targeting

1. GameplayAbilitySystem Plugin의 소개

Official Documentation 참고,

게임플레이 어빌리티 시스템(Gameplay Ability System) 은 RPG나 MOBA 타이틀에서 볼 수 있는 어빌리티 및 어트리뷰트 유형을 구축하기 위한 고도로 유연한 프레임워크입니다. 게임 내 캐릭터가 사용할 액션이나 패시브 어빌리티, 이러한 액션의 결과로 다양한 어트리뷰트를 높이거나 낮추는 상태 이펙트를 만들 수 있고, '재사용 대기 시간' 타이머나 자원 비용을 구현하여 액션의 사용 빈도를 조절하거나, 어빌리티의 레벨과 레벨에 따른 이펙트를 변경하거나, 파티클 및 사운드 이펙트를 활성화하는 등의 작업이 가능합니다. 게임플레이 어빌리티 시스템을 사용하면 점프처럼 단순한 것부터 최신 RPG나 MOBA 타이틀 내 인기 캐릭터의 기술 모음처럼 복잡한 것까지 다양한 인게임 어빌리티를 설계 및 구현하고 효과적으로 연결할 수 있습니다.

게임플레이어빌리티시스템 플러그인은 Epic Games에서 개발했으며 Unreal Engine에서 사용 가능합니다. 또한 해당 플러그인은 Paragon, Fortnite와 같은 AAA급 상용 게임에서 테스트와 검증을 마쳤습니다.

플러그인은 싱글 및 멀티플레이어 게임에서 바로 사용할 수 있는 다음과 같은 솔루션들을 제공합니다:

  • 조정가능한 자원과 쿨타임을 가진 level-based 캐릭터 능력 혹은 스킬 구현(GameplayAbilities)
  • 액터가 소유한 수치(데이터)Attributes 조작(Attributes)
  • 액터에 상태 효과 적용 (GameplayEffects)
  • 엑터에 GameplayTags 적용 (GameplayTags)
  • 시각 효과 또는 음향 효과 생성 (GameplayCues)
  • 위에 언급된 모든 요소들의 Replication 지원

멀티플레이어 게임에서 GAS는 client-side prediction을 지원합니다:

  • 어빌리티 활성화
  • 애니메이션 몽타주 재생
  • Attributes 변경
  • GameplayTags 적용
  • GameplayCues 스폰
  • CharacterMovementComponent에 연결된 RootMotionSource를 통한 움직임

GAS는 기본적으로 C++로 개발하는 것을 권장합니다. 하지만 GameplayAbilitiesGameplayEffects는 디자이너가 블루프린트에서 생성할 수도 있습니다.

현재 알려진 GAS 이슈:

  • GameplayEffect latency reconciliation (can't predict ability cooldowns resulting in players with higher latencies having lower rate of fire for low cooldown abilities compared to players with lower latencies).
  • Cannot predict the removal of GameplayEffects. We can however predict adding GameplayEffects with the inverse effects, effectively removing them. This is not always appropriate or feasible and still remains an issue.
  • Lack of boilerplate templates, multiplayer examples, and documentation. Hopefully this helps with that!

⬆ 위로 가기

2. 샘플 프로젝트

이 문서에는 언리얼 엔진을 처음 접하는 분들을 위해 멀티플레이어 3인칭 슈팅 샘플 프로젝트가 포함되어 있습니다. 이 글을 통해 GAS를 접하시는 분들은 언리얼 엔진의 C++, 블루프린트, UMG, 리플리케이션 및 기타 중급 주제에 대해 어느 정도 알고 계셔야 합니다. 이 프로젝트는 기본적인 3인칭 슈팅 멀티플레이어 프로젝트를 설정하는 예제를 제공합니다. 플레이어와 AI가 제어하는 영웅은 PlayerState 클래스의 AbilitySystemComponent(ASC)를 사용하고, AI가 제어하는 미니언은 Character 클래스의 ASC를 사용하는 방식입니다.

이 프로젝트의 목표는 게임 개발에서 흔히 요청되는 Ability를 잘 설명된 코드로 시연하면서, GAS의 기본에 대해 최대한 단순하게 설명하는 것입니다. 이 프로젝트는 초급자를 대상으로 하기 때문에 발사체 예측과 같은 고급 주제는 보여주지 않습니다.

시연 내용:
PlayerState와 Character의 ASC
리플리케이션된 Attribtute
리플리케이션된 애니메이션 몽타주
GameplayTag 사용 예시
GameplayAbility 내부 및 외부에서 GameplayEffect 적용 및 제거
캐릭터의 체력 변화 시 갑옷에 의한 피해 데미지 적용
GameplayEffectExecutionCalculation 사용 예시
스턴 효과
죽음 및 리스폰
서버의 어빌리티에서 액터(발사체) 스폰
조준 사격 및 질주 시 로컬 플레이어의 속도를 예측적으로 변화시키는 효과
질주 시 스태미나를 지속적으로 소모
마나를 사용하여 어빌리티 발동
패시브 어빌리티
GameplayEffect 중첩
액터 타겟팅
블루프린트로 GameplayAbility 생성
C++로 GameplayAbility 생성
액터별 인스턴스화된 GameplayAbility
인스턴스화되지 않은 GameplayAbility(점프)
정적 GameplayCue (파이어건 발사체 충격 파티클 이펙트)
액터 GameplayCue (질주 및 기절 파티클 이펙트)

Hero 클래스에는 다음과 같은 어빌리티가 존재합니다:

어빌리티입력 바인드PredictedC++ / Blueprint설명
점프스페이스바YesC++영웅이 점프합니다.
마우스 왼쪽 버튼NoC++영웅의 총에서 발사체를 발사합니다. 애니메이션은 예측되지만 발사체는 예측되지 않습니다.
조준경 조준마우스 오른쪽 버튼YesBlueprint버튼을 누른 상태에서 영웅은 천천히 걷고 카메라는 확대되어 총으로 더 정확한 조준을 할 수 있습니다.
질주왼쪽 시프트YesBlueprint버튼을 누른 상태에서 영웅은 스테미나를 사용하여 더 빠른 속도로 달릴 수 있습니다.
전방 대쉬QYesBlueprint스테미나를 소모하여 전방으로 돌진합니다.
아머 스택(패시브)패시브NoBlueprint매 4초마다 영웅은 최대 4개의 방어력를 획득합니다. 데미지를 받으면 방어력 스택 하나가 제거됩니다.
메테오RNoBlueprint플레이어는 적이 위치한 곳에 메테오를 떨어뜨려 피해를 입히고 기절시킵니다. 타겟팅는 예측되지만 스폰되는 메테오는 예측되지 않습니다.

GameplayAbilities가 C++ 또는 Blueprint에서 만들었는지 여부는 중요하지 않습니다. 위 Ability들은 두 가지를 혼합하여 각 언어마다 어떻게 사용하는지를 설명하기 위함입니다.

미니언에는 미리 정의된 GameplayAbilities가 없습니다. 붉은 미니언들은 체력 회복량이 많고, 푸른 미니언들은 시작 체력이 좀 더 높습니다.

GameplayAbilities의 이름을 지을 때, GameplayAbility의 로직이 Blueprint에 생성되었음을 나타내기 위해 접미사 _BP를 사용했습니다. 접미사가 없으면 로직이 C++로 생성되었다는 것을 의미합니다.

블루프린트 에셋 작명 접두사

접두사에셋 타입
GA_GameplayAbility
GC_GameplayCue
GE_GameplayEffect

⬆ 위로 가기

3. GAS를 사용하는 프로젝트 설정

GAS를 사용하여 프로젝트를 설정하는 기본 단계:
1. 에디터에서 GameplayAbilitySystem 플러그인 활성화.
1. [YourProjectName].Build.cs 파일의 PrivateDependencyModuleNames에 "GameplayAbilities", "GameplayTags","GameplayTasks"을 추가.
1. Visual Studio 프로젝트 파일 새로 고침/재생성.
1. 4.24부터는 UAbilitySystemGlobals::Get().InitGlobalData()를 호출하여 TargetData를 사용합니다. 샘플 프로젝트는 이 작업을 UAssetManager::StartInitialLoading()을 수행합니다. 자세한 내용은 InitGlobalData()를 참조해주세요.

이것으로 GAS를 활성화하는 데 필요한 모든 작업이 완료되었습니다. 이제 Character 혹은 PlayerStateASCAttributeSet을 추가하고 GameplayAbilitiesGameplayEffects를 만들기 시작해보세요!

⬆ 위로 가기

4. GAS Concepts

Sections

4.1 Ability System Component
4.2 Gameplay Tags
4.3 Attributes
4.4 Attribute Set
4.5 Gameplay Effects
4.6 Gameplay Abilities
4.7 Ability Tasks
4.8 Gameplay Cues
4.9 Ability System Globals
4.10 Prediction

4.1 Ability System Component

AbilitySystemComponent (ASC)는 GAS의 핵심입니다. 시스템과의 모든 상호 작용을 처리하는 UActorComponent (UAbilitySystemComponent)입니다. GameplayAbilities을 사용하거나Attributes를 가지거나, GameplayEffects를 받으려는 액터에는 반드시 ASC가 하나씩 붙어 있어야 합니다. 이러한 오브젝트는 모두 ASC 안에 존재하며, (AttributeSet에 의해 리플리케이트되는Attributes를 제외하고) ASC에 의해 관리 및 리플리케이트됩니다. 이를 subclass할 것을 권장드리지만 필수는 아닙니다.

ASC가 붙은 액터ASCOwnerActor라고 합니다. 그리고 ASC의 물리적 표현 액터AvatarActor라고 합니다. MOBA 게임에서 단순한 AI 미니언의 경우처럼 OwnerActorAvatarActor가 동일한 액터일 수도 있고, 혹은 플레이어가 조종하는 영웅과 같이 OwnerActorPlayerState이고 AvatarActor가 영웅의 Character 클래스인 경우도 있을 수도 있습니다. 대부분의 액터는 자체적으로 ASC를 갖습니다. MOBA 게임의 영웅처럼 액터가 리스폰되고 스폰 사이에 Attributes 또는 GameplayEffects의 지속성이 필요한 경우, PlayerState 클래스가 ASC의 이상적인 위치입니다.

Note: ASCPlayerState에 있는 경우, PlayerStateNetUpdateFrequency를 늘려야 합니다. 기본적으로 PlayerState에서 매우 낮은 값으로 설정되어 있어 클라이언트에서Attributes 그리고 GameplayTags 등의 변경이 일어나기 전에 지연이 발생하거나 감지될 수 있습니다. Adaptive Network Update Frequency를 활성화하세요. 포트나이트 또한 이를 사용합니다.

OwnerActorAvatarActor 가 다른 액터인 경우 둘 다 IAbilitySystemInterface를 구현해야 합니다. 이 인터페이스에는 반드시 오버라이드해야 하는 함수가 하나 있는데, 바로 ASC에 대한 포인터를 반환하는 UAbilitySystemComponent* GetAbilitySystemComponent() const입니다. ASC는 이 인터페이스 함수를 찾아 시스템 내부에서 서로 상호작용합니다.

ASC는 현재 활성화된 GameplayEffectsFActiveGameplayEffectsContainer ActiveGameplayEffects에 보관합니다.

ASC는 부여된 Gameplay AbilitiesFGameplayAbilitySpecContainer ActivatableAbilities에서 보관합니다. ActivatableAbilities.Items을 반복처리할 때마다, 어빌리티 제거로 인해 목록이 변경되지 않도록 루프 위에 ABILITYLIST_SCOPE_LOCK();을 추가해야 합니다. 범위 내 모든 ABILITYLIST_SCOPE_LOCK();AbilityScopeLockCount를 증가시킨 다음 범위를 벗어나면 감소합니다. ABILITYLIST_SCOPE_LOCK(); 범위 내에서 어빌리티를 제거하려고 시도하시면 안됩니다. (어빌리티를 지우는 함수는 내부적으로 AbilityScopeLockCount를 확인하여 목록에 락이 걸렸을 경우 어빌리티를 제거하지 못하도록 합니다).

4.1.1 Replication Mode

ASCGameplayEffects, GameplayTagsGameplayCues를 리플리케이트하기 위한 세 가지의 리플리케이션 모드(Full, Mixed, and Minimal)를 지정할 수 있습니다. Attributes는 해당 AttributeSet에 의해 리플리케이트됩니다.

리플리케이션 모드사용 시기설명
FullSingle Player모든 GameplayEffects가 모든 클라이언트에 리플리케이트됩니다.
MixedMultiplayer, Player가 컨트롤하는 액터GameplayEffects는 소유 클라이언트에만 리플리케이트됩니다. GameplayTagsGameplayCues만 모두에게 리플리케이트됩니다.
MinimalMultiplayer, AI가 컨트롤하는 액터GameplayEffects가 아무에게도 리플리케이트되지 않습니다. GameplayTagsGameplayCues만 모두에게 리플리케이트됩니다.

Note: Mixed 리플리케이션 모드에서는 OwnerActor's의 소유자가 Controller일 것으로 예상합니다. 기본적으로 PlayerState's의 소유자는 Controller이지만 Character's의 소유자는 그렇지 않습니다. Mixed 리플리케이션 모드를PlayerState가 아닌 OwnerActor와 함께 사용하시는 경우, 유효한 Controller를 가진 OwnerActorSetOwner()를 호출해야 합니다.

4.24부터 PossessedBy()는 이제Pawn의 소유자를 새 Controller로 설정합니다.

⬆ 위로 가기

4.1.2 설정 및 초기화

ASC는 일반적으로 OwnerActor의 생성자에서 생성되며 명시적으로 리플리케이트된 것으로 표시됩니다. 이 작업은 C++에서 수행해야 합니다.

AGDPlayerState::AGDPlayerState()
{
	// 어빌리티 시스템 컴포넌트를 생성하고 명시적으로 리플리케이트되도록 설정합니다.
	AbilitySystemComponent = CreateDefaultSubobject<UGDAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
	AbilitySystemComponent->SetIsReplicated(true);
	//...
}

ASC는 서버와 클라이언트 모두 OwnerActorAvatarActor를 사용하여 초기화해야 합니다. PawnController를 설정한 후(소유한 후) 초기화할 수 있습니다. 싱글 플레이어 게임은 서버 경로만 신경쓰면 됩니다.

ASCPawn에 있는 플레이어 캐릭터의 경우 PawnPossessedBy() 함수를 서버에서 초기화하고 PlayerControllerAcknowledgePossession() 함수를 클라이언트에서 초기화합니다.

void APACharacterBase::PossessedBy(AController * NewController)
{
	Super::PossessedBy(NewController);

	if (AbilitySystemComponent)
	{
		AbilitySystemComponent->InitAbilityActorInfo(this, this);
	}

	// ASC Mixed 모드에서 리플리케이션이 되려면 ASC 소유자의 소유자가 컨트롤러여야 합니다.
	SetOwner(NewController);
}
void APAPlayerControllerBase::AcknowledgePossession(APawn* P)
{
	Super::AcknowledgePossession(P);

	APACharacterBase* CharacterBase = Cast<APACharacterBase>(P);
	if (CharacterBase)
	{
		CharacterBase->GetAbilitySystemComponent()->InitAbilityActorInfo(CharacterBase, CharacterBase);
	}

	//...
}

플레이어가 제어하는 캐릭터가 PlayerState에 존재하는 ASC의 경우, 일반적으로 PawnPossessedBy() 함수에서 서버를 초기화하고 PawnOnRep_PlayerState() 함수에서 클라이언트를 초기화합니다. 이렇게 하면 PlayerState가 클라이언트에 존재하게 됩니다.

// Server only
void AGDHeroCharacter::PossessedBy(AController * NewController)
{
	Super::PossessedBy(NewController);

	AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
	if (PS)
	{
		// 서버에서 ASC를 설정합니다. 클라이언트는 OnRep_PlayerState()에서 이 작업을 수행합니다.
		AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());

		// AI에는 PlayerController가 없으므로 여기서 다시 초기화할 수 있습니다.   
		// PlayerController가 있는 영웅일 경우 두 번 초기화해도 아무런 문제가 없습니다.
		PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
	}
	
	//...
}
// Client only
void AGDHeroCharacter::OnRep_PlayerState()
{
	Super::OnRep_PlayerState();

	AGDPlayerState* PS = GetPlayerState<AGDPlayerState>();
	if (PS)
	{
		// 클라이언트에 대한 ASC를 설정합니다. 서버는 PossessedBy에서 이 작업을 수행합니다.
		AbilitySystemComponent = Cast<UGDAbilitySystemComponent>(PS->GetAbilitySystemComponent());

		// 클라이언트에 대한 ASC 액터 정보를 초기화합니다. 서버가 새 액터를 보유하면 ASC를 초기화합니다.
		AbilitySystemComponent->InitAbilityActorInfo(PS, this);
	}

	// ...
}

만약 LogAbilitySystem: Warning: Can't activate LocalOnly or LocalPredicted ability %s when not local!라는 메시지가 표시될 경우 클라이언트에서 ASC를 초기화하지 않은 것입니다.

⬆ 위로 가기

4.2 Gameplay Tags

FGameplayTagsGameplayTagManager에 등록된 Parent.Child.Grandchild...와 같은 형식의 계층적 이름입니다. 이러한 태그는 오브젝트의 상태를 분류하고 설명하는 데 매우 유용합니다. 예를 들어, 어떤 캐릭터가 기절할 경우 우리는 그 캐릭터에게 기절 시간 동안 State.Debuff.Stun GameplayTag을 줄 수 있습니다.

여러분은 아마 bool이나 열거형으로 다루던 것들을 GameplayTags로 바꾸고 객체에 특정 GameplayTags가 있는지 여부에 대해 boolean 논리로 체크하는 자신을 발견하게 될 겁니다.

오브젝트에 태그를 부여할 때는 일반적으로 해당 오브젝트에 ASC가 있는 경우 이를 추가하여 GAS가 해당 오브젝트와 상호작용할 수 있도록 합니다. UAbilitySystemComponent는 소유한 GameplayTags에 접근할 수 있는 함수를 제공하는 IGameplayTagAssetInterface를 구현합니다.

여러 개의 GameplayTagsFGameplayTagContainer에 저장할 수 있습니다. FGameplayTagContainer는 효율적으로 동작하기 때문에 단순 TArray<FGameplayTag> 쓰기보단 GameplayTagContainer를 사용하는 것이 좋습니다. 또한 태그는 표준 FNames이지만, 프로젝트 세팅에서 Fast Replication이 활성화된 경우 리플리케이션을 위해 FGameplayTagContainer에 함께 패킹하여 효율적으로 사용할 수 있습니다. Fast Replication을 사용하려면 서버와 클라이언트에 동일한 GameplayTags 목록이 있어야 합니다. 일반적으로는 문제가 되지 않기 때문에 이 옵션을 활성화하는 것이 좋습니다. GameplayTagContainer를 순회하고 싶을 경우 TArray<FGameplayTag>를 반환받는 것도 가능합니다.

FGameplayTagCountContainer에 저장된 GameplayTags에는 해당 GameplayTag의 인스턴스 수를 저장하는 TagMap이 있습니다. FGameplayTagCountContainerGameplayTag가 존재하는데도 TagMapCount가 0일 수 있습니다. 디버깅 했을 때 ASC에 아직 GameplayTag가 남아있는 경우 이 문제가 발생할 수 있습니다.HasTag(), HasMatchingTag()와 같은 함수는 TagMapCount를 검사하여 GameplayTag가 없거나 TagMapCount가 0인 경우 false를 반환합니다.

GameplayTagsDefaultGameplayTags.ini에서 미리 정의해줘야 합니다. UE5 에디터부터는 프로젝트 세팅에 인터페이스를 제공하여 개발자가 DefaultGameplayTags.ini를 수동으로 편집하지 않고도 GameplayTags를 관리할 수 있도록 합니다. GameplayTag 에디터는 GameplayTags 생성, 이름 변경, 레퍼런스 검색, 삭제가 가능합니다.

GameplayTag Editor in Project Settings

GameplayTag 참조를 검색하면 에디터에 익숙한 Reference Viewer 그래프가 표시되어 GameplayTag를 참조하는 모든 에셋이 표시됩니다. 하지만 GameplayTag를 참조하는 C++ 클래스는 표시되지 않습니다.

GameplayTags 이름을 바꾸면 리디렉션이 생성되어 원래 GameplayTags를 여전히 참조하는 에셋이 새 GameplayTags로 리디렉션할 수 있습니다. 가능하면 새 GameplayTags를 생성하고 모든 참조를 새 GameplayTags로 수동으로 업데이트한 다음 이전 GGameplayTags를 삭제하여 리디렉션이 생성되지 않도록 하는 것이 좋습니다.

Fast Replication 외에도, GameplayTag 에디터는 자주 복제되는 GameplayTags를 자동으로 채워 최적화할 수 있는 옵션을 제공합니다.

GameplayEffectGameplayTags를 추가한 경우 해당 GameplayTags는 리플리케이트됩니다. ASC는 리플리케이트되지 않는 LooseGameplayTags를 추가할 수 있으며, 이는 수동으로 관리해야 합니다. 샘플 프로젝트에서는 State.DeadLooseGameplayTags를 사용하여 소유 클라이언트가 생명력이 0으로 떨어졌을 때 즉시 반응할 수 있도록 합니다. 리스폰 시 에는 TagMapCount를 다시 0으로 설정합니다. LooseGameplayTags를 사용할 때는 TagMapCount를 수동으로 조정하는 것보다 UAbilitySystemComponent::AddLooseGameplayTag()UAbilitySystemComponent::RemoveLooseGameplayTag() 함수를 사용하는 것이 더 바람직합니다.

C++에서 GameplayTag에 대한 참조를 얻는 방법:

FGameplayTag::RequestGameplayTag(FName("Your.GameplayTag.Name"))

부모 또는 자식 GameplayTags 가져오기와 같은 고급 GameplayTag 조작은 GameplayTagManager에서 제공하는 함수를 참고해주세요. GameplayTagManager에 액세스하려면 GameplayTagManager.h를 포함하고 UGameplayTagManager::Get().FunctionName과 같은 방식으로 호출합니다. GameplayTagManager는 실제로 GameplayTags를 관계형 노드(부모, 자식 등)로 저장하기 때문에 문자열 조작이나 비교 연산보다 더 빠르게 처리됩니다.

GameplayTagsGameplayTagContainers에는 선택적으로 UPROPERTY 지정자인 Meta = (Categories = "GameplayCue")가 있어 Blueprint에서 GameplayCue 부모 태그를 가진 GameplayTags만 필터링하여 표시할 때 유용합니다. 이 변수는 GameplayTag 또는 GameplayTagContainer 변수가 GameplayCues에만 사용되어야 한다는 것을 알고 있을 때 유용합니다.

또 다른 방법으로는 FGameplayCueTag라는 구조체가 존재하는데, 이는 FGameplayTag를 캡슐화하고 Blueprint에서 GameplayCue 부모 태그를 가진 GameplayTags만 자동으로 필터링합니다.

함수에서 GameplayTag 파라미터를 필터링하려면 UFUNCTION 지정자 Meta = (GameplayTagFilter = "GameplayCue")를 사용합니다. 하지만 GameplayTagContainer 파라미터는 이 방식으로 필터링할 수 없습니다. 이를 가능하게 하려면 엔진을 수정해야 하는데, Engine\Plugins\Editor\GameplayTagsEditor\Source\GameplayTagsEditor\Private\SGameplayTagGraphPin.cpp 파일의 SGameplayTagGraphPin::ParseDefaultValueData()가 어떻게 FilterString = UGameplayTagsManager::Get().GetCategoriesMetaFromField(PinStructType);를 호출하고, FilterStringSGameplayTagWidget에 전달하여 SGameplayTagGraphPin::GetListContent()에서 필터를 적용하는지 확인해 보세요. Engine\Plugins\Editor\GameplayTagsEditor\Source\GameplayTagsEditor\Private\SGameplayTagContainerGraphPin.cppGameplayTagContainer 버전 함수는 메타 필드 속성을 확인하지 않고 필터를 전달합니다.

샘플 프로젝트에선 GameplayTags를 광범위하게 사용합니다.

⬆ 위로 가기

4.2.1 GameplayTag 변경에 응답하기

ASCGameplayTags가 추가되거나 제거될 때 호출되는 델리게이트를 제공합니다. 이 델리게이트는 EGameplayTagEventType을 받아, GameplayTag가 추가/제거될 때만 실행되도록 하거나, GameplayTagTagMapCount가 변경될 때마다 실행되도록 지정할 수 있습니다.

AbilitySystemComponent->RegisterGameplayTagEvent(
	FGameplayTag::RequestGameplayTag(FName("State.Debuff.Stun")),
	EGameplayTagEventType::NewOrRemoved).AddUObject(this, &AGDPlayerState::StunTagChanged);

콜백 함수에는 GameplayTag와 새로운 TagCount를 파라미터 받습니다.

virtual void StunTagChanged(const FGameplayTag CallbackTag, int32 NewCount);

⬆ 위로 가기

4.2.2 플러그인 .ini 파일에서 GameplayTag 불러오기

자신만의 .ini 파일에 GameplayTags가 포함된 플러그인을 만든 경우, 플러그인의 StartupModule() 함수에서 해당 플러그인의 GameplayTag .ini 디렉토리를 로드할 수 있습니다.

예를 들어, 언리얼 엔진에 포함된 CommonConversation 플러그인은 다음과 같이 처리합니다:

void FCommonConversationRuntimeModule::StartupModule()
{
	TSharedPtr<IPlugin> ThisPlugin = IPluginManager::Get().FindPlugin(TEXT("CommonConversation"));
	check(ThisPlugin.IsValid());
	
	UGameplayTagsManager::Get().AddTagIniSearchPath(ThisPlugin->GetBaseDir() / TEXT("Config") / TEXT("Tags"));

	//...
}

이 코드는 Plugins\CommonConversation\Config\Tags 디렉토리에서 GameplayTags가 포함된 .ini 파일을 찾고, 플러그인이 활성화된 경우 엔진 시작 시 프로젝트에 로드합니다.

⬆ 위로 가기

4.3 Attributes

4.3.1 Attribute 정의

AttributeFGameplayAttributeData 구조체로 정의된 float값입니다. 이 값들을 통해 캐릭터의 체력, 레벨, 포션의 충전 수 등 무엇이든 나타낼 수 있습니다. 게임플레이와 관련된 수치값이 Actor에 속해 있다면, 해당 값은 Attribute로 정의하는 것이 좋습니다. Attribute는 일반적으로 GameplayEffects에 의해서만 수정되어야 ASC가 변경 사항을 예측할 수 있습니다.

AttributeAttributeSet에 의해 정의되고 관리됩니다. AttributeSetAttribute를 리플리케이트하고 관리합니다. Attribute를 정의하는 방법은 AttributeSets 섹션을 참조하세요.

Tip: 만약 에디터의 Attribute 목록에 Attribute을 표시하고 싶지 않다면, Meta = (HideInDetailsView)라는 property specifier를 사용하면 됩니다.

⬆ 위로 가기

4.3.2 BaseValue vs CurrentValue

AttributeBaseValueCurrentValue라는 두 개의 값으로 구성됩니다. BaseValueAttribute의 영구적인 값이고 CurrentValueBaseValueGameplayEffect의 임시 수정값이 더해진 값입니다. 예를 들어, Character의 이동 속도 AttributeBaseValue가 600u/s(단위/초)라고 가정해보겠습니다. 아직 이동 속도를 변경하는 GameplayEffect가 없다면 CurrentValue도 600u/s일 것입니다. 여기서 일시적으로 50u/s 이동 속도 버프를 받으면 BaseValue는 600u/s로 동일하게 유지되고 CurrentValue는 600 + 50이 되어 총 650u/s가 됩니다. 이동 속도 버프가 만료되면 CurrentValue는 다시 BaseValue인 600u/s로 되돌아갑니다.

GAS를 처음 접하는 사람들이 BaseValueAttribute의 최대값의 개념을 혼동하여 잘못 처리하는 경우가 종종 있습니다. BaseValue와 최대값은 다른 개념입니다. 어빌리티나 UI에서 변경하거나 참조할 수 있는 Attribute의 최대값은 별도의 Attribute로 정의해야 합니다. 하드코딩된 최대값과 최소값의 경우, FAttributeMetaDataDataTable을 정의하는 방법이 있지만, 구조체 위에 있는 에픽 게임즈의 주석을 보면 "아직 진행 중인 작업"이라고 되어 있습니다. 자세한 정보는 AttributeSet.h를 참조해보세요. 혼동을 방지하기 위해 어빌리티나 UI에서 참조할 수 있는 최대값은 별도의 Attribute로 만들고, Attribute 클램핑에만 사용되는 하드코딩된 최대값과 최소값은 AttributeSet에 하드코딩된 float로 정의하는 것을 권장드립니다. Attribute 클램핑은 CurrentValue 변경에 대한 PreAttributeChange()GameplayEffect로부터의 BaseValue 변경에 대한 PostGameplayEffectExecute()에서 논의됩니다.

BaseValue에 대한 영구적인 변경은 Instant GameplayEffect에서 발생하며, DurationInfinite GameplayEffectCurrentValue를 변경합니다. Periodic GameplayEffectInstant GameplayEffect처럼 취급되어 BaseValue를 변경합니다.

⬆ 위로 가기

4.3.3 Meta Attributes

일부 AttributeAttribute와 상호작용하기 위해 임시 값에 대한 자리 표시자(placeholder)로 사용됩니다. 이러한 Attribute를 Meta Attribute라고 합니다. GamepalyEffect가 직접적으로 캐릭터의 체력 Attribute를 변경하는 대신, 데미지를 Meta Attribute로 정의하여 사용할 경우 데미지 값이 버프 및 디버프와 함께 GameplayEffectExecutionCalculation에서 수정될 수 있고, AttributeSet에서 추가로 조정하는 것도 가능합니다. 예를 들어, 데미지를 현재의 방어막 Attribute에서 먼저 차감한 후 나머지를 체력 Attribute에서 차감하는 식입니다. 데미지 Meta AttributeGameplayEffect 간에 지속되지 않으며, 매번 덮어쓰여집니다. Meta Attribute은 일반적으로 리플리케이션되지 않습니다.

Meta Attribute는 "얼마나 많은 데미지를 가했는가?"와 "이 데미지를 어떻게 처리할 것인가?" 사이의 논리적 구분을 제공합니다. 이러한 논리적 구분을 통해 GameplayEffectExecution Calculation은 타겟이 데미지를 어떻게 처리하는지 알 필요가 없게 됩니다. 데미지 예시를 계속 이어서 말해보자면, GameplayEffect는 얼마만큼의 데미지를 가할지 결정한 다음 AttributeSet이 해당 데미지를 어떻게 처리할지 결정합니다. 모든 캐릭터가 동일한 Attribute를 갖고 있지 않을 수도 있으며, 특히 AttributeSet을 서브 클래스로 분류하는 경우 더욱 그렇습니다. 기본 AttributeSet 클래스에는 체력 Attribute만 있고 서브 클래싱된 AttributeSet에 방어막 Attribute를 추가한 경우 방어막 Attribute를 가진 AttributeSet의 서브 클래스는 기본 AttributeSet 클래스와 다르게 받은 데미지를 분배할 것입니다.

Meta Attribute는 좋은 설계 패턴이지만 필수는 아닙니다. 모든 데미지의 인스턴스에 대해 하나의 Execution Calculation과 모든 캐릭터가 공유하는 하나의 AttributeSet 클래스를 사용한다면, Execution Calculation 내부에서 체력, 방어막 등으로 데미지를 직접 분배하고 이러한 Attribute를 직접 수정하는 것도 괜찮은 방법일 겁니다. 유연성은 포기하는 방식이지만, 상황에 따라 괜찮을 수 있습니다.

⬆ 위로 가기

4.3.4 Attribute 변경에 응답하기

UI 또는 다른 게임플레이를 업데이트하기 위해 Attribute가 변경될 때 감지하려면, UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)를 사용합니다. 이 함수는 바인딩할 수 있는 델리게이트를 반환하며, Attribute가 변경될 때마다 자동으로 호출됩니다. 델리게이트는 NewValue, OldValue 그리고 FGameplayEffectModCallbackData를 포함한 FOnAttributeChangeData 매개 변수를 제공합니다.

Note: FGamePlayEffectModCallbackData는 서버에서만 설정됩니다.

AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(AttributeSetBase->GetHealthAttribute()).AddUObject(this, &AGDPlayerState::HealthChanged);
virtual void HealthChanged(const FOnAttributeChangeData& Data);

샘플 프로젝트는 GDPlayerState에서 Attrubute 값 변경 델리게이트에 바인딩하여 HUD를 업데이트하고, 체력이 0에 도달했을 때 플레이어 죽음에 반응하는 방식으로 사용합니다.

이를 AsyncTask로 래핑하는 커스텀 블루프린트 노드가 샘플 프로젝트에 포함되어 있습니다. 해당 노드는 UI_HUD UMG 위젯에서 체력, 마나 및 스태미나를 업데이트하는 데 사용됩니다. 이 AsyncTaskEndTask()를 수동으로 호출할 때까지 영구적으로 유지되며, UMG Widget의 Destruct 이벤트에서 이를 수행합니다. 자세한 내용은 AsyncTaskAttributeChanged.h/cpp를 참조해주세요.

Listen for Attribute Change BP Node

⬆ 위로 가기

4.3.5 Derived Attributes

하나 이상의 다른 Attribute에서 일부 또는 전체 값을 파생하는 Attribute를 만들려면, 하나 이상의 Attribute Based 또는 MMC(Modular GameplayEffect Calculation) Modifiers가 포함된 Infinite GameplayEffect를 사용합니다. Derived Attribute는 자신이 의존하고 있는 Attribute가 업데이트될 때 자동으로 업데이트됩니다.

Derived Attribute에 대한 모든 Modifier의 최종 공식은 Modifier Aggregators 공식과 동일합니다. 만약 특정 순서대로 계산할 경우, 모든 계산을 MMC 내에서 수행해야 합니다.

((CurrentValue + Additive) * Multiplicitive) / Division

Note: PIE에서 여러 클라이언트를 사용하는 경우 Editor Preferences에서 Run Under One Process(하나의 프로세스 아래에서 실행) 옵션을 비활성화해야 합니다. 그렇지 않으면 첫 번째 클라이언트를 제외한 다른 클라이언트에서 독립적인 Attributes가 업데이트될 때 Derived Attribute가 갱신되지 않습니다.

예를 들어, Infinite GameplayEffectTestAttrA의 값을 TestAttrBTestAttrC Attribute로부터 파생시킵니다. 사용된 공식은 TestAttrA = (TestAttrA + TestAttrB) * (2 * TestAttrC)이며, TestAttrA는 해당 Attribute 중 하나가 값을 업데이트할 때마다 자동으로 다시 계산됩니다.

Derived Attribute Example

⬆ 위로 가기

4.4 AttributeSet

4.4.1 AttributeSet 정의

AttributeSetAttribute를 정의 및 관리하고 변경 사항을 처리하는 역할을 합니다. 사용자는 UAttributeSet의 서브 클래스를 구현해서 사용해야 합니다. Owner Actor의 생성자에서 AttributeSet을 생성하면 자동으로 해당 ASC에 등록됩니다. 이 작업은 C++에서 수행해야 합니다.

⬆ 위로 가기

4.4.2 AttributeSet 설계

ASC는 하나 혹은 여러 개의 AttributeSet을 가질 수 있습니다. AttributeSet은 메모리 오버헤드가 미미하기 때문에 얼마나 많은 AttributeSet을 사용할지는 사용자의 조직적인 결정에 달려있습니다.

게임 내 모든 액터가 공유하는 하나의 큰 모놀리식 AttributeSet을 사용하고, 필요한 Attribute만 사용하는 방식도 가능합니다. 이 경우 사용되지 않는 Attribute는 무시합니다.

다른 방법으로 Attribute들을 그룹화하여 여러 개의 AttributeSet을 만들어 Actor에 필요한 것만 선택적으로 추가할 수도 있습니다. 예를 들어, 체력 관련 Attribute를 위한 AttributeSet, 마나 관련 Attribute를 위한 AttributeSet을 만들 수 있습니다. MOBA 게임에서 영웅은 마나를 필요로 하지만 미니언은 필요하지 않다면, 영웅은 마나 관련 AttributeSet을, 미니언은 이를 제외한 AttributeSet을 가지게 하면 됩니다.

또한 AttributeSet은 서브 클래싱할 수 있기 때문에, 이를 통해 Actor가 가질 Attribute를 선택적으로 결정할 수 있습니다. Attribute들은 내부적으로 AttributeSetClassName.AttributeName 형식으로 참조되는데, AttributeSet을 서브 클래싱했을 경우에도 부모 클래스의 Attribute들은 똑같이 부모 클래스의 이름을 접두사로 사용하게 됩니다.

여러 개의 AttributeSet을 가질 수는 있지만, 동일한 클래스의 AttributeSet은 하나만 ASC에 포함시킬 수 있습니다. 동일한 클래스의 AttributeSet을 두 개 이상 추가할 경우 ASC가 어느 AttributeSet을 사용할지 알지 못하고, 그냥 그 중 하나를 선택하게 됩니다.

4.4.2.1 개별 Attribute를 가진 서브 컴포넌트

Pawn에 여러 개의 피해를 입을 수 있는 컴포넌트가 있을 경우(예: 각기 다른 갑옷 부위별 피해 계산), 최대 수의 피해를 입을 수 있는 컴포넌트를 알고 있다면, 하나의 AttributeSet에 여러 개의 Attribute(예: DamageableCompHealth0, DamageableCompHealth1 등)를 정의하여 각 슬롯에 해당하는 피해 컴포넌트를 나타낼 수 있습니다. 그런 다음, 피해를 입을 각 컴포넌트의 인스턴스에서 슬롯 번호를 지정하여 GameplayAbilityExecutions에서 어떤 Attribute에 피해를 적용할지 알 수 있도록 합니다. 만약 Pawn이 가진 피해 컴포넌트가 최대 피해 슬롯 수보다 적거나 아예 없더라도, 동작하는 데에 큰 문제는 없습니다. AttributeSetAttribute이 있다고 해서 반드시 그 Attribute을 사용해야 하는 것은 아닙니다. 사용하지 않는 Attribute는 매우 적은 메모리만 차지합니다.

하지만 서브 컴포넌트가 많은 Attribute를 가질 경우 서브 컴포넌트가 너무 많거나, 서브 컴포넌트가 분리되어 다른 플레이어와 공유되거나, 기타 이유로 이 방식이 적합하지 않다면 Attribute 대신 컴포넌트에서 일반적인 float 값을 저장하는 방식으로 변경하는 것이 좋습니다. 이 경우 Item Attributes를 참고해보세요.

4.4.2.2 런타임에 AttributeSet 추가 및 제거하기

AttributeSet은 런타임에 ASC에서 추가 및 제거할 수 있지만, 다소 위험할 수 있습니다. 예를 들어, 클라이언트에서 서버보다 먼저 AttributeSet을 제거한 후 서버에서 Attribute 값 변경이 클라이언트로 리플리케이트되면 클라이언트에서는 Attribute가 해당 AttributeSet을 찾지 못해 게임이 크래시가 일어날 수 있습니다.

예시. 무기를 인벤토리에 추가할 때:

AbilitySystemComponent->GetSpawnedAttributes_Mutable().AddUnique(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();

예시. 무기를 인벤토리에서 제거할 때:

AbilitySystemComponent->GetSpawnedAttributes_Mutable().Remove(WeaponAttributeSetPointer);
AbilitySystemComponent->ForceReplication();

4.4.2.3 아이템 Attribute (무기 탄약)

Attribute`를 가진 장착 가능한 아이템(무기 탄약, 방어구 내구도 등)을 구현하는 방법은 여러 가지가 있습니다. 이 모든 접근 방식은 아이템에 직접 값을 저장하며, 이는 여러 플레이어가 아이템을 장착할 수 있는 경우 필수적입니다.

  1. 아이템에 float 사용 (권장)
  2. 아이템에 별도의 AttributeSet 사용
  3. 아이템에 별도의 ASC 사용

4.4.2.3.1 아이템에 일반 float 사용

Attribute를 사용하는 대신, 아이템 클래스 인스턴스에 일반 float 값을 저장해보세요. 포트나이트와 GASShooter는 해당 방식으로 총기의 탄약을 관리합니다. 예를 들어, 총기의 경우 최대 탄창 크기, 현재 탄창 내 탄약, 보유 탄약 등을 총기 인스턴스에 COND_OwnerOnly로 리플리케이트된 float 값으로 직접 저장합니다. 무기가 보유 탄약을 공유한다면, 보유 탄약을 캐릭터의 Attribute로 옮겨 공유되는 탄약 AttributeSet에 저장할 수 있습니다. (재장전 어빌리티는 Cost GE를 사용해 보유 탄약에서 탄창 내 탄약으로 이동시킬 수 있습니다). 현재 탄창 내 탄약을 Attribute로 사용하지 않기 때문에, UGameplayAbility의 일부 함수를 재정의하여 총기에서 float 값을 기준으로 비용을 확인하고 적용해야 합니다. GamepalyAbility를 부여할 때 총기를 GameplayAbilitySpecSourceObject로 설정하면, 어빌리티 내부에서 해당 어빌리티를 부여한 총기에 접근할 수 있습니다.

자동 사격 중 로컬 탄약 수가 리플리케이트되어 클라이언트 측 탄약 수가 서버의 탄약 수로 덮어씌워지지 않도록 하려면, PreReplication()에서 IsFiring GameplayTag가 있는 동안에는 리플리케이트를 비활성화하면 됩니다. 이로써 자체적으로 로컬 예측을 수행하게 됩니다.

void AGSWeapon::PreReplication(IRepChangedPropertyTracker& ChangedPropertyTracker)
{
	Super::PreReplication(ChangedPropertyTracker);

	DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, PrimaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
	DOREPLIFETIME_ACTIVE_OVERRIDE(AGSWeapon, SecondaryClipAmmo, (IsValid(AbilitySystemComponent) && !AbilitySystemComponent->HasMatchingGameplayTag(WeaponIsFiringTag)));
}

장점:
1. AttributeSet 사용의 제한을 피할 수 있습니다. (아래 참조)

제한 사항:
1. 기존 GameplayEffect 워크플로를 사용할 수 없음. (탄약 소모에 대한 Cost GE 등)
1. UGameplayAbility의 주요 함수를 오버라이드하여 총기의 float 값에 대해 탄약 비용을 확인하고 적용해야 합니다.

4.4.2.3.2 아이템의 AttributeSet

플레이어의 인벤토리에 아이템을 추가할 때 플레이어의 ASC에 추가되는 아이템에 별도의 AttributeSet을 사용하면 작동할 수 있지만 몇 가지 주요 제한사항이 있습니다. 제 경우 GASShooter 초기 버전에서 무기의 탄약 시스템에서 해당 방식을 사용한 적이 있습니다. 무기는 최대 탄창 크기, 현재 탄창에 있는 탄약, 예비 탄약 등과 같은 Attribute를 무기 클래스에 있는 AttributeSet에 저장합니다. 만약 무기가 예비 탄약을 공유하는 경우, 예비 탄약을 캐릭터의 공유 탄약 AttributeSet으로 이동시키는 것이 좋습니다. 무기가 서버에서 플레이어의 인벤토리에 추가되면, 무기는 자신의 AttributeSet을 플레이어의 ASC::SpawnedAttributes에 추가합니다. 그러면 서버는 이를 클라이언트에 리플리케이트합니다. 무기가 인벤토리에서 제거되면, 무기의 AttributeSetASC::SpawnedAttributes에서 제거됩니다.

AttributeSetOwnerActor가 아닌 다른 곳(예: 무기)에 있는 경우, 처음에는 AttributeSet에서 컴파일 오류가 발생할 수 있습니다. 이를 해결하려면 AttributeSet을 생성할 때 생성자 대신 BeginPlay()에서 생성하고 무기에 IAbilitySystemInterface(플레이어 인벤토리에 무기를 추가할 때 ASC에 대한 포인터를 설정)를 구현하면 됩니다.

void AGSWeapon::BeginPlay()
{
	if (!AttributeSet)
	{
		AttributeSet = NewObject<UGSWeaponAttributeSet>(this);
	}
	//...
}

이 부분은 GASShooter의 이전 버전을 통해 확인할 수 있습니다.

장점:
1. 기존 GameplayAbilityGameplayEffect 워크플로를 사용할 수 있습니다. (탄약 사용에 대한 Cost GE 등)
1. 아이템이 매우 적은 경우 설정이 간단합니다.

제한 사항:
1. 모든 무기 유형에 대해 새로운 AttributeSet 클래스를 만들어야 합니다. Attribute를 변경하면 ASCSpawnedAttributes 배열에서 해당 AttributeSet 클래스의 첫 번째 인스턴스를 찾기 때문에 ASC는 기능적으로 한 클래스의 AttributeSet 인스턴스를 하나만 가질 수 있습니다. 동일한 AttributeSet 클래스의 인스턴스를 추가할 경우 무시됩니다.
1. 플레이어의 인벤토리에는 각 유형의 무기가 하나씩만 지닐 수 있는데, 이는 앞서 설명한 AttributeSet클래스당 하나의 AttributeSet 인스턴스만 허용하기 때문이었습니다.
1. AttributeSet을 제거하는 것은 위험합니다. GASShooter에서 플레이어가 로켓으로 자폭한 경우, 플레이어는 즉시 인벤토리에서 로켓 발사기를 제거(ASC에서 해당 AttributeSet 포함)합니다. 서버가 로켓 발사기의 탄약 Attribute 변경을 클라이언트에 리플리케이트할 때 해당 AttributeSet가 클라이언트의 ASC에 더 이상 존재하지 않게 되어 게임이 크래시합니다.

4.4.2.3.3 아이템의 ASC

각 아이템에 AbilitySystemComponent를 통째로 넣는다는 것은 극단적인 접근 방식입니다. 개인적으로 이 작업을 시도해본 적도 없고 본 적도 없습니다. 이렇게 작동하려면 많은 엔지니어링이 필요할 것입니다.

질문: 여러 개의 AbilitySystemComponent를 동일한 소유자(Owner)에게 두고, 서로 다른 아바타(예: pawn, 무기/아이템/투사체)에 대해 사용하려는 경우가 가능할까요? (소유자는 PlayerState로 설정)

여기서 제가 처음으로 떠올린 문제는 소유 Actor에 대해 IGameplayTagAssetInterface와 IAbilitySystemInterface를 구현하는 것입니다.

전자의 경우에는 가능할 수도 있습니다. 모든 ASC에서 태그를 집계하는 방식을 사용할 수 있을 것입니다. 하지만 주의해야 할 점은 HasAllMatchingGameplayTags가 교차 ASC 집계를 통해서만 충족될 수도 있다는 것입니다. 단순히 각 ASC로 호출을 전달하고 결과를 OR 연산으로 결합하는 방식은 충분하지 않을 수 있습니다. 하지만 후자의 경우는 더 까다롭습니다. 어떤 ASC가 권위적인(권한을 가진) 것일까요? 누군가가 GE를 적용하려 할 때, 어느 ASC가 이를 받아야 할까요? 이런 부분을 해결할 수 있을지도 모르겠지만, 소유자 아래에 여러 ASC가 있을 때 발생하는 문제는 가장 어려운 부분일 것입니다.

Pawn과 무기에 별도의 ASC를 두는 것은 자체적으로 의미가 있을 수 있습니다. 예를 들어, 무기를 설명하는 태그와 소유 Pawn을 설명하는 태그를 구분하는 경우입니다. 무기에 부여된 태그가 소유자에게도 "적용"되고 그 외에는 아무런 영향이 없다는 접근 방식이 말이 될 수도 있습니다. (예: Attribute와 GameplayEffect는 독립적이지만 소유자는 위에서 설명한 것처럼 소유한 태그를 집계합니다.) 이 방식은 작동할 가능성이 충분히 있습니다. 하지만 동일한 소유자를 가진 여러 ASC를 두는 것은 복잡한 상황을 초래할 수 있습니다.

Dave Ratti from Epic's answer to community questions #6

장점:
1. 기존 GameplayAbilityGameplayEffect 워크플로를 사용할 수 있음. (탄약 사용에 대한 cost GE 등)
1. AttributeSet 클래스 재사용 가능. (각 무기의 ASC에 하나씩)

제한 사항:
1. 엔지니어링 비용이 어느 정도일지 알 수 없음.
1. 실제로 구현이 가능하지 불확실.

⬆ 위로 가기

4.4.3 Attribute 정의

AttributeAttributeSet의 헤더 파일에서 C++로만 정의할 수 있습니다. 이 매크로 블록을 모든 AttributeSet 헤더 파일의 맨 위에 추가하는 것이 좋습니다. 그러면 Attribute에 대한 getter 및 setter 함수가 자동으로 생성됩니다.

// AttributeSet.h의 매크로 사용
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

리플리케이트되는 생명력 Attribute는 다음과 같이 정의할 수 있습니다:

UPROPERTY(BlueprintReadOnly, Category = "Health", ReplicatedUsing = OnRep_Health)
FGameplayAttributeData Health;
ATTRIBUTE_ACCESSORS(UGDAttributeSetBase, Health)

또한 헤더 파일에 OnRep 함수를 다음과 같이 정의합니다:

// .h
UFUNCTION()
virtual void OnRep_Health(const FGameplayAttributeData& OldHealth);

AttributeSet의 .cpp 파일에서는 예측 시스템에서 사용하는 GAMEPLAYATTRIBUTE_REPNOTIFY 매크로를 사용하여 OnRep 함수를 다음과 같이 작성합니다:

// .cpp
void UGDAttributeSetBase::OnRep_Health(const FGameplayAttributeData& OldHealth)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UGDAttributeSetBase, Health, OldHealth);
}

마지막으로, AttributeGetLifetimeReplicatedProps에 다음과 같이 추가해야 합니다:

void UGDAttributeSetBase::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME_CONDITION_NOTIFY(UGDAttributeSetBase, Health, COND_None, REPNOTIFY_Always);
}

REPNOTIFY_Always는 로컬 값이 서버에서 내려오는 값과 이미 동일한 경우에도 OnRep 함수가 트리거되도록 설정합니다(예측으로 인해 발생). 기본적으로 로컬 값이 서버에서 내려오는 값과 동일하면 OnRep 함수는 트리거되지 않습니다.

AttributeMeta Attribute처럼 리플리케이트되지 않는 경우에는 OnRep 함수와 GetLifetimeReplicatedProps 단계를 생략할 수 있습니다.

⬆ 위로 가기

4.4.4 Attribute 초기화

Attribute(BaseValue와 그에 따른 CurrentValue)을 초기화하는 방법에는 여러 가지가 있습니다. 에픽 게임즈는 instant GameplayEffect를 사용하는 방법을 권장합니다. 이 방법은 샘플 프로젝트에서도 사용된 방식입니다.

샘플 프로젝트에서 Attributes를 초기화하는 instant GameplayEffect를 만드는 방법은 샘플 프로젝트의 GE_HeroAttributes 블루프린트를 참고하세요. 이 GameplayEffect의 적용은 C++에서 이루어집니다.

Attribute를 정의할 때 ATTRIBUTE_ACCESSORS 매크로를 사용했다면, 각 Attribute에 대해 AttributeSet에서 자동으로 초기화 함수가 생성되며, 해당 함수는 C++에서 원하는 대로 호출할 수 있습니다.

// InitHealth(float InitialValue)는 ATTRIBUTE_ACCESSORS 매크로로 정의된 'Health' Attribute에 대해 자동으로 생성된 함수입니다.
AttributeSet->InitHealth(100.0f);

Attribute 초기화 방법에 대한 자세한 내용은 AttributeSet.h에서 확인할 수 있습니다.

Note: 이전에는 FAttributeSetInitterDiscreteLevelFGameplayAttributeData와 함께 작동하지 않았습니다. 이 기능은 Attribute가 원시 float 값일 때 사용되었으며, FGameplayAttributeDataPlain Old Data (`POD)가 아니라고 오류를 발생시킵니다. 이 문제는 4.24에서 수정되었습니다. (참고: UE-76557)

⬆ 위로 가기

4.4.5 PreAttributeChange()

PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)AttributeCurrentValue가 변경되기 전에 그 변화를 처리하기 위해 AttributeSet에서 사용하는 주요 함수 중 하나입니다. 이 함수는 NewValue라는 참조 매개변수를 통해 CurrentValue에 대한 변경 사항을 클램핑(제한)하는 이상적인 위치입니다.

예를 들어, 샘플 프로젝트에서 이동 속도 Modifier를 클램핑하는 방법은 다음과 같습니다:

if (Attribute == GetMoveSpeedAttribute())
{
	// 150 units/s 미만으로 감속할 수 없고 1000 units/s 이상으로 부스트할 수 없습니다.
	NewValue = FMath::Clamp<float>(NewValue, 150, 1000);
}

GetMoveSpeedAttribute() 함수는 AttributeSet.h에 추가한 매크로 블록(Attribute 정의)에 의해 생성됩니다.

이 함수는 Attribute에 대한 변경이 있을 때마다 트리거됩니다. Attribute 설정자( AttributeSet.h Attribute 정의 매크로 블록에서 정의된)나 GameplayEffects를 통해 변경이 발생할 수 있습니다.

Note: 여기서 이루어지는 클램핑은 ASC에서 Modifier의 값을 영구적으로 변경하지 않습니다. 단지 Modifier를 쿼리할 때 반환되는 값을 변경합니다. 즉, GameplayEffectExecutionCalculationsModifierMagnitudeCalculations와 같은 Modifier의 값을 기반으로 CurrentValue를 다시 계산하는 모든 로직은 클램핑을 다시 구현해야 합니다.

Note: PreAttributeChange()에 대한 에픽 게임즈의 코멘트는 이를 GameplayEvent에 사용하지 말고 주로 클램핑을 위해 사용하라고 안내하고 있습니다. Attribute 변경에 대한 GameplayEvent를 처리하기 위한 추천 위치는 UAbilitySystemComponent::GetGameplayAttributeValueChangeDelegate(FGameplayAttribute Attribute)입니다. (Attribute 변경에 응답하기).

⬆ 위로 가기

4.4.6 PostGameplayEffectExecute()

PostGameplayEffectExecute(const FGameplayEffectModCallbackData & Data)는 Instant GameplayEffect로 인해 AttributeBaseValue가 변경된 후에만 트리거됩니다. 이는 GameplayEffect로 인해 Attribute가 변경된 후 추가적인 Attribute 조작을 수행하기 적합한 위치입니다.

예를 들어, 샘플 프로젝트에서는 여기에서 최종 피해 Meta Attribute를 생명력 Attribute에서 빼는 작업을 수행합니다. 만약 방어막 Attribute가 있었다면, 먼저 방어막에서 피해를 빼고, 남은 피해를 생명력에서 차감하는 방식입니다. 샘플 프로젝트는 또한 이 위치를 사용하여 피격 반응 애니메이션을 적용하고, 피해량 UI를 표시하며, 킬러에게 경험치와 골드 보상을 할당합니다. 설계상, 피해 Meta Attribute는 항상 Instant GameplayEffect를 통해 전달되며 Attribute 설정자를 통해서는 전달되지 않습니다.

마나와 스태미나와 같이 Instant GameplayEffect에 의해서만 BaseValue가 변경되는 다른 Attribute들도 여기에서 최대값에 맞춰 클램핑될 수 있습니다.

Note: PostGameplayEffectExecute()가 호출될 때 Attribute의 변경은 이미 이루어졌지만, 아직 클라이언트로 복제되지 않은 상태입니다. 따라서 여기에서 값을 클램핑해도 클라이언트에 두 번의 네트워크 업데이트가 발생하지 않습니다. 클라이언트는 클램핑 후에만 업데이트를 받게 됩니다.

⬆ 위로 가기

4.4.7 OnAttributeAggregatorCreated()

OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator)는 해당 AttributeSet에서 Attribute에 대한 Aggregator가 생성될 때 트리거됩니다. 해당 함수는 FAggregatorEvaluateMetaData의 커스텀 설정을 가능하게 합니다. AggregatorEvaluateMetaDataAggregatorAttribute에 적용된 모든 Modifiers를 기반으로 AttributeCurrentValue를 평가할 때 사용됩니다. 기본적으로 AggregatorEvaluateMetaDataAggregator가 어떤 Modifier가 자격이 있는지 판단하는 데 사용됩니다. 예를 들어, MostNegativeMod_AllPositiveMod는 모든 플러스 Modifier는 허용하지만, 마이너스 Modifier는 가장 큰 음수 값 하나만 허용합니다. 이 방식은 Paragon에서 사용되었으며, 플레이어에게 여러 개의 느려짐 효과가 있을 때 가장 큰 음수의 이동 속도 감소 효과만 적용되도록 하고, 모든 플러스적인 이동 속도 버프는 모두 적용되도록 했습니다. 자격이 없는 ModifierASC에 여전히 존재하지만, 최종 CurrentValue에는 집계되지 않습니다. 조건이 변경되면 나중에 자격이 될 수 있습니다. 예를 들어, 가장 마이너스인 Modifier가 만료되면, 다음으로 마이너스인 Modifier(만약 존재한다면)가 자격을 얻습니다.

AggregatorEvaluateMetaData를 사용하여 가장 마이너스인 Modifier 하나와 모든 플러스적인 Modifier를 허용하는 예시:

virtual void OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const override;
void UGSAttributeSetBase::OnAttributeAggregatorCreated(const FGameplayAttribute& Attribute, FAggregator* NewAggregator) const
{
	Super::OnAttributeAggregatorCreated(Attribute, NewAggregator);

	if (!NewAggregator)
	{
		return;
	}

	if (Attribute == GetMoveSpeedAttribute())
	{
		NewAggregator->EvaluationMetaData = &FAggregatorEvaluateMetaDataLibrary::MostNegativeMod_AllPositiveMods;
	}
}

자격을 위한 커스텀 AggregatorEvaluateMetaDataFAggregatorEvaluateMetaDataLibrary에 정적 변수로 추가해야 합니다.

⬆ 위로 가기

4.5 Gameplay Effects

4.5.1 Gameplay Effect 정의

GameplayEffects(GE)는 Ability가 자신을 포함한 다른 객체의 AttributesGameplayTags를 변경하기 위한 수단입니다. 즉각적인 Attribute 변화를 일으킬 수 있거나(예: 피해나 치유) 이동 속도 증가나 기절과 같은 장기적인 상태 버프/디버프를 적용할 수도 있습니다. UGameplayEffect 클래스는 단일 GameplayEffect를 정의하는 데이터 전용 클래스입니다. 추가적인 로직은 GameplayEffect에 포함되지 않아야 합니다. 보통 디자이너는 UGameplayEffect의 여러 블루프린트 자식 클래스를 생성하여 사용합니다.

GameplayEffectModifiersExecutions (GameplayEffectExecutionCalculation)을 통해 Attribute를 변경합니다.

GameplayEffects에는 세 가지 지속 시간 타입을 가집니다: Instant, Duration, Infinite

또한, GameplayEffectGameplayCues를 추가하거나 실행할 수 있습니다. Instant GameplayEffectGameplayCue GameplayTag에서 Execute를 호출하는 반면, Duration 또는 Infinite GameplayEffectGameplayCue GameplayTag에서 AddRemove를 호출합니다.

Duration 타입GameplayCue 이벤트사용 시기
InstantExecuteAttributeBaseValue에 즉각적이고 영구적인 변화를 줄 때 사용합니다. 이 경우 GameplayTag는 적용되지 않으며, 한 프레임도 존재하지 않습니다.
DurationAdd & RemoveAttributeCurrentValue를 일시적으로 변경하거나 특정 기간 동안 GameplayTag를 적용할 때 사용합니다. 해당 기간은 UGameplayEffect 클래스 또는 블루프린트에서 정의됩니다.
InfiniteAdd & RemoveAttributeCurrentValue를 일시적으로 변경하거나 무한으로 GameplayTag를 적용할 때 사용합니다. 해당 Effect는 스스로 만료되지 않으며, 반드시 Ability나 ASC를 통해 수동으로 제거해야 합니다.

DurationInfinite 타입의 GameplayEffectPeriodic Effect(주기적 효과)를 사용할 수 있습니다. Periodic Effect는 설정된 주기(Period)마다 해당 ModifierExecution을 실행합니다. Periodic EffectAttributeBaseValue를 변경하거나 GameplayCue를 실행할 때 Instant 타입의 GameplayEffect로 처리됩니다. 이는 DOT(Damage Over Time) 같은 효과에 유용합니다.

Note: Periodic Effectpredicted될 수 없습니다.

또한 DurationInfinite 타입의 GameplayEffectOngoing Tag Requirement(진행 중 태그 요구 조건)를 통해 Gameplay Effect Tags를 일시적으로 비활성화하거나 활성화할 수 있습니다. 비활성화되면 Modifier와 적용된 GameplayTag는 제거되지만, GameplayEffect 자체는 제거되지 않습니다. 이후 요구 조건이 충족되면 ModifierGameplayTag가 다시 적용됩니다.

Duration 또는 Infinite GameplayEffectModifiers를 수동으로 다시 계산해야 하는 경우(Attribute 에서 가져오지 않는 데이터를 사용하는 MMC 가 있다고 가정할 때), UAbilitySystemComponent::ActiveGameplayEffects.SetActiveGameplayEffectLevel(FActiveGameplayEffectHandle ActiveHandle, int32 NewLevel)을 호출하면 됩니다.UAbilitySystemComponent::ActiveGameplayEffects.GetActiveGameplayEffect(ActiveHandle).Spec.GetLevel() 을 사용하여 이미 가지고 있는 레벨과 동일하게 설정할 수 있습니다.
Attribute를 기반으로 한 Modifier는 해당 Attribute가 업데이트될 때 자동으로 업데이트됩니다.

SetActiveGameplayEffectLevel() 함수가 Modifier를 업데이트하는 핵심 작업은 다음과 같습니다:

MarkItemDirty(Effect);
Effect.Spec.CalculateModifierMagnitudes();
//Private 함수이기 때문에, 만약 호출할 수 있었다면 레벨을 굳이 다시 설정하지 않고도 이 세 함수를 호출했을 것입니다.
UpdateAllAggregatorModMagnitudes(Effect);

GameplayEffect는 일반적으로 인스턴스화되지 않습니다. Ability나 ASCGameplayEffect를 적용할 때, GameplayEffectClassDefaultObject를 기반으로 GameplayEffectSpec이 생성됩니다. 성공적으로 적용된 GameplayEffectSpecFActiveGameplayEffect라는 구조체에 추가되며, ASC는 이를 특별한 컨테이너 구조체인 ActiveGameplayEffect에서 관리합니다.

⬆ 위로 가기

4.5.2 Gameplay Effect 적용

GameplayEffect는 다양한 방법으로 적용할 수 있으며, 주로 GameplayAbilities의 함수나 ASC의 함수를 통해 이루어집니다. 이러한 함수들은 보통 ApplyGameplayEffectTo 형태를 가지며, 이러한 다양한 함수들은 결국 TargetUAbilitySystemComponent::ApplyGameplayEffectSpecToSelf()를 호출하여 Effect를 적용합니다.

GameplayAbility 외부(예: 투사체)에서 GameplayEffect를 적용하려면, TargetASC를 가져와서 해당 ASC의 함수 중 하나를 사용해 ApplyGameplayEffectToSelf를 호출해야 합니다.

Duration 또는 Infinite 타입의 GameplayEffectASC에 적용되었을 때 이를 감지하려면, 다음과 같이 델리게이트에 바인딩하면 됩니다:

AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf.AddUObject(this, &APACharacterBase::OnActiveGameplayEffectAddedCallback);

해당 콜백 함수는 다음과 같습니다:

virtual void OnActiveGameplayEffectAddedCallback(UAbilitySystemComponent* Target, const FGameplayEffectSpec& SpecApplied, FActiveGameplayEffectHandle ActiveHandle);

서버는 항상 이 함수를 호출하며, 리플리케이션 모드와 관계없이 호출됩니다. Autonomous Proxy는 FullMixed 모드에서만 리플리케이트된 GameplayEffect에 대해 이 함수를 호출합니다. Simulated Proxy는 Full replication mode에서만 이 함수를 호출합니다.

⬆ 위로 가기

4.5.3 Gameplay Effect 삭제

GameplayEffectGameplayAbilities의 함수나 ASC의 함수를 통해 다양한 방식으로 제거할 수 있습니다. 일반적으로 RemoveActiveGameplayEffect 형태를 가지며, 이러한 함수들은 결국 TargetFActiveGameplayEffectsContainer::RemoveActiveEffects()를 호출합니다.

GameplayAbility 외부(예: 투사체)에서 GameplayEffect를 제거하려면, 타겟의 ASC를 가져와서 해당 ASC의 함수 중 하나를 사용해 RemoveActiveGameplayEffect를 호출해야 합니다.

Duration 또는 Infinite 타입의 GameplayEffectASC에서 제거되었을 때 이를 감지하려면, 다음과 같이 델리게이트에 바인딩하면 됩니다:

AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate().AddUObject(this, &APACharacterBase::OnRemoveGameplayEffectCallback);

해당 콜백 함수는 다음과 같습니다:

virtual void OnRemoveGameplayEffectCallback(const FActiveGameplayEffect& EffectRemoved);

서버는 항상 이 함수를 호출하며, 리플리케이션 모드와 관계없이 호출됩니다. Autonomous Proxy는 FullMixed 모드에서만 리플리케이트된 GameplayEffect에 대해 이 함수를 호출합니다. Simulated Proxy는 Full replication mode에서만 이 함수를 호출합니다.

⬆ 위로 가기

4.5.4 Gameplay Effect Modifiers

ModifierAttribute를 변경하며, 예측적으로 Attribute를 변경할 수 있는 유일한 방법입니다. GameplayEffectModifier를 0개 혹은 여러 개 가질 수 있습니다. 각 Modifier는 지정된 연산을 통해 하나의 Attribute만 변경할 수 있습니다.

연산내용
AddModifier에서 지정한 Attribute에 결과 값을 더합니다. 빼기를 원할 경우 음수를 사용합니다.
MultiplyModifier에서 지정한 Attribute에 결과 값을 곱합니다.
DivideModifier에서 지정한 Attribute를 결과 값으로 나눕니다.
OverrideModifier에서 지정한 Attribute를 결과 값으로 덮어씁니다.

AttributeCurrentValue는 모든 ModifierBaseValue에 추가된 집합적인 결과입니다.

Modifier가 어떻게 집계되는지는 GameplayEffectAggregator.cpp의 FAggregatorModChannel::EvaluateWithBase에 다음과 같은 공식으로 정의됩니다:

((InlineBaseValue + Additive) * Multiplicitive) / Division

Override Modifier는 최종 값을 덮어쓰며, 가장 마지막에 적용된 Modifier가 우선권을 가집니다.

Note: 퍼센트 기반의 변화를 사용할 때는 Multiply 연산을 사용하여 덧셈 이후에 적용되도록 하세요.

Note: 퍼센트 변경은 Prediction과 함께 사용 시 문제가 발생할 수 있습니다.

Modifier에는 네 가지 타입이 있습니다: Scalable Float, Attribute Based, Custom Calculation Class, Set By Caller. 이들은 모두 float 값을 생성하며, Modifier의 연산 값을 기반으로 연산을 수행해 지정된 Attribute를 변경합니다.

Modifier 타입내용
Scalable FloatFScalableFloat는 Data Table의 행(변수)과 열(레벨)을 참조하는 구조체입니다. Scalable Float는 자동으로 지정된 테이블 행의 값을 Ability의 현재 레벨(또는 GameplayEffectSpec에서 오버라이드된 레벨)에서 읽습니다. 이 값은 추가적으로 계수(coefficient)를 통해 조정될 수 있습니다. Data Table/Row가 지정되지 않으면 값을 1로 간주하고, 계수를 사용하여 모든 레벨에서 단일 값을 하드 코딩할 수 있습니다. ScalableFloat
Attribute BasedAttribute Based ModifierSource (GameplayEffectSpec을 생성한 주체)나 Target(GameplayEffectSpec을 받은 대상)의 CurrentValue 또는 BaseValue 를 사용합니다. 이 값은 계수, 전/후 추가 값을 사용해 추가적으로 수정됩니다. SnapshottingGameplayEffectSpec이 생성될 때 백업된 Attribute 값을 캡처하며, Non-SnapshottingGameplayEffectSpec이 적용될 때 값을 캡처합니다.
Custom Calculation ClassCustom Calculation Class는 복잡한 Modifier에 가장 큰 유연성을 제공합니다. 이 ModifierModifierMagnitudeCalculation 클래스를 사용하며, 추가적으로 계수와 전/후 추가 값을 사용해 결과 float 값을 수정할 수 있습니다.
Set By CallerSetByCaller Modifier는 런타임에 Ability 또는 GameplayEffectSpec을 생성한 주체가 외부에서 값을 설정하는 Modifier입니다. 예를 들어, 플레이어가 버튼을 누른 시간에 따라 Ability의 피해량을 설정하려면 SetByCaller를 사용할 수 있습니다. SetByCallerTMap<FGameplayTag, float>으로 GameplayEffectSpec에 저장됩니다. ModifierAggregator에 지정된 GameplayTag와 연결된 SetByCaller 값을 확인하도록 지시합니다. GameplayTag 버전만 사용할 수 있으며 FName 버전은 비활성화됩니다. ModifierSetByCaller로 설정되었지만 GameplayEffectSpec에 올바른 GameplayTag와 연결된 SetByCaller가 존재하지 않는 경우, 런타임 오류가 발생하고 값이 0으로 반환됩니다. Divide 연산의 경우 문제가 발생할 수 있습니다. SetByCallers의 사용 방법에 대한 더 많은 정보는 SetByCaller 관련 문서를 참조하세요.

⬆ 위로 가기

4.5.4.1 Multiply 및 Divide Modifiers

기본적으로, 모든 MultiplyDivide ModifierAttributeBaseValue에 곱하거나 나누기 전에 서로 더해집니다.

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Additive = SumMods(Mods[EGameplayModOp::Additive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Additive), Parameters);
	float Multiplicitive = SumMods(Mods[EGameplayModOp::Multiplicitive], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Multiplicitive), Parameters);
	float Division = SumMods(Mods[EGameplayModOp::Division], GameplayEffectUtilities::GetModifierBiasByModifierOp(EGameplayModOp::Division), Parameters);
	...
	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
	...
}
float FAggregatorModChannel::SumMods(const TArray<FAggregatorMod>& InMods, float Bias, const FAggregatorEvaluateParameters& Parameters)
{
	float Sum = Bias;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Sum += (Mod.EvaluatedMagnitude - Bias);
		}
	}

	return Sum;
}

from GameplayEffectAggregator.cpp

이 공식에서 MultiplyDivide ModifierBias 값이 1로 설정됩니다. (참고로 AdditionBias0입니다.) 위 코드는 다음과 같이 해석됩니다:

1 + (Mod1.Magnitude - 1) + (Mod2.Magnitude - 1) + ...

위 공식은 몇 가지 예상치 못한 결과를 초래합니다. 첫째, 이 공식은 모든 Modifier를 더한 후 BaseValue에 곱하거나 나누어 적용합니다. 대부분의 사람들은 서로 곱하거나 나누어 계산될 것이라고 예상합니다. 예를 들어 Multiply Modifier가 1.5인 경우 두 개를 적용하면 BaseValue1.5 x 1.5 = 2.25배가 되어야 할 것으로 예상하지만, 실제로는 1.5 + 1.5 = 2가 되어 BaseValue2를 곱하게 됩니다 (50% 증가 + 또 다른 50% 증가 = 100% 증가). 이 예시는 GameplayPrediction.h의 예시와 같습니다. 기본 속도 50010% 속도 버프를 적용하면 550이 됩니다. 여기에 또 다른 10% 버프를 추가하면 600이 됩니다.

그리고 둘째, 이 공식은 Paragon에 맞게 설계되었기 때문에 값에 대해 문서화되지 않은 규칙이 있습니다.

Multiply와 Divide의 덧셈 공식에 대한 규칙:

  • 규칙 1: (값이 1 미만인 항이 1개 이하) AND (여러 개의 값이 [1, 2) 범위에 존재 가능)
  • 규칙 2: (값이 2 이상인 항은 하나만 존재 가능)

이 공식에서 Bias는 범위 [1, 2) 내의 숫자의 정수 자릿수를 빼줍니다. 첫 번째 ModifierBias는 합산 시작 값에서 빼지기 때문에 (합산 시작 값은 루프 전에 Bias로 설정됨), 개별 값 하나만 있을 때는 작동하며, 1 미만의 값이 하나만 존재하는 경우에도 제대로 작동합니다.

Multiply의 몇 가지 예시:
Multipliers: 0.5
1 + (0.5 - 1) = 0.5, correct

Multipliers: 0.5, 0.5
1 + (0.5 - 1) + (0.5 - 1) = 0.

혹시 1을 예상하셨나요? Multiply Modifier가 여러 개 있는 경우 1 미만의 값은 의미가 없습니다. Paragon은 greatest negative value for Multiply Modifiers만 사용하도록 설계되었기 때문에 1 미만의 값이 하나만 존재하게 됩니다.

Multipliers: 1.1, 0.5
1 + (0.5 - 1) + (1.1 - 1) = 0.6, correct

Multipliers: 5, 5
1 + (5 - 1) + (5 - 1) = 9.
혹시 10을 예상하셨나요? Modifier의 합계는 항상sum of the Modifiers - number of Modifiers + 1이 됩니다.

많은 게임들은 MultiplyDivide ModifierBaseValue에 곱하거나 나누어지기 전에 서로 곱하고 나누어지기를 원합니다. 이를 구현하려면 FAggregatorModChannel::EvaluateWithBase()엔진 코드를 변경해야 합니다.

float FAggregatorModChannel::EvaluateWithBase(float InlineBaseValue, const FAggregatorEvaluateParameters& Parameters) const
{
	...
	float Multiplicitive = MultiplyMods(Mods[EGameplayModOp::Multiplicitive], Parameters);
	float Division = MultiplyMods(Mods[EGameplayModOp::Division], Parameters);
	...

	return ((InlineBaseValue + Additive) * Multiplicitive) / Division;
}
float FAggregatorModChannel::MultiplyMods(const TArray<FAggregatorMod>& InMods, const FAggregatorEvaluateParameters& Parameters)
{
	float Multiplier = 1.0f;

	for (const FAggregatorMod& Mod : InMods)
	{
		if (Mod.Qualifies())
		{
			Multiplier *= Mod.EvaluatedMagnitude;
		}
	}

	return Multiplier;
}

⬆ 위로 가기

4.5.4.2 Modifier의 GameplayTag

SourceTagTargetTag는 각 Modifier에 대해 설정할 수 있습니다. 이들은 GameplayEffectApplication Tag requirements처럼 작동합니다. 즉, 태그는 효과가 적용될 때만 고려됩니다. 예를 들어, 주기적인 Infinite GameplayEffect에서는 첫 번째 적용 시에만 태그가 고려되고, Periodic Execution에서는 고려되지 않습니다.

Attribute Based Modifier는 또한 SourceTagFilterTargetTagFilter를 설정할 수 있습니다. 이 필터들은 Attribute Based Modifier의 소스가 되는 Attribute의 Magnitude를 결정할 때 사용되어 특정 Modifier를 그 Attribute에서 제외시킵니다. 소스 또는 타겟에 필터의 모든 태그가 없을 경우 해당 Modifier는 제외됩니다.

위 내용은 다음을 의미합니다:
source ASC와 target ASC의 태그는 GameplayEffect에 의해 캡처됩니다. source ASC 태그는 GameplayEffectSpec이 생성될 때 캡처되고, target ASC 태그는 효과가 실행될 때 캡처됩니다. Infinite GameplayEffect나 Duration GameplayEffect의 Modifier가 적용될 자격이 있는지(즉, Aggregator가 자격이 있는지) 결정할 때 필터가 설정된 경우, 캡처된 태그는 필터와 비교됩니다.

⬆ 위로 가기

4.5.5 Gameplay Effect Stacking(중첩)

기본적으로 GameplayEffect는 새로운 인스턴스를 적용할 때 이전에 존재한 GameplayEffectSpec에 대해 알지 못하고 신경 쓰지 않습니다. GameplayEffect는 스택되도록 설정할 수 있으며, 이 경우 새로운 GameplayEffectSpec 인스턴스가 추가되는 대신 현재 존재하는 GameplayEffectSpec의 스택 수가 변경됩니다. 스택은 DurationInfinite에서만 동작합니다.

스택에는 두 가지 유형이 있습니다:Aggregate by Source와 Aggregate by Target.

스택 유형설명
Aggregate by SourceSource ASC마다 타겟에 대한 별도의 스택 인스턴스가 있습니다. 각 Source는 X 만큼의 스택을 적용할 수 있습니다.
Aggregate by Target타겟에 대해 하나의 스택 인스턴스만 존재합니다. 각 Source는 공유된 스택 한도까지 스택을 적용할 수 있습니다.

스택에는 만료, 지속 시간 새로 고침, 주기 초기화에 대한 Policy도 있습니다. 이들에 대한 도움말 툴팁은 GameplayEffect Blueprint에서 확인할 수 있습니다.

샘플 프로젝트에는 GameplayEffect 스택 변경 사항을 수신하는 커스텀 Blueprint 노드가 포함되어 있습니다. HUD UMG 위젯은 이를 사용하여 플레이어의 패시브 방어구 스택 수를 업데이트합니다. 이 AsyncTask는 수동으로 EndTask()가 호출될 때까지 계속 살아 있습니다. 호출은 UMG 위젯의 Destruct 이벤트에서 수행됩니다. AsyncTaskEffectStackChanged.h/cpp를 참조하십시오.

Listen for GameplayEffect Stack Change BP Node

⬆ 위로 가기

4.5.6 Ability 부여

GameplayEffectASC에 새로운 GameplayAbilities를 부여할 수 있습니다. Duration 혹은 Infinite GameplayEffect만이 Ability를 부여할 수 있습니다.

일반적인 사용 사례 중 하나는 다른 플레이어가 특정 동작(예: 넉백이나 끌어당김)에 반응하도록 강제하는 것입니다. 예를 들어, 특정 행동을 적용하기 위해 GameplayEffect를 그들에게 부여하고, 자동으로 활성화되는 GameplayAbility(부여 시 Ability를 자동으로 활성화하는 방법에 대해서는 Passive Abilities 참조)를 부여하면 원하는 동작이 실행됩니다.

디자이너는 GameplayEffect가 어떤 Ability를 부여할지, 어떤 레벨로 부여할지, 어떤 입력에 바인딩할지, 그리고 부여된 Ability의 Removal Policy(제거 방침)을 선택할 수 있습니다.

제거 정책설명
Cancel Ability ImmediatelyGameplayEffect가 제거될 때 부여된 Ability는 즉시 취소되고 제거됩니다.
Remove Ability on End부여된 Ability는 완료될 때까지 유지되며 이후 타겟에서 제거됩니다.
Do NothingGameplayEffect가 제거되더라도 부여된 Ability는 영향을 받지 않습니다. Ability는 수동으로 제거될 때까지 영구적으로 유지됩니다

⬆ 위로 가기

4.5.7 Gameplay Effect Tags

GameplayEffect는 여러 개의 GameplayTagContainers를 가질 수 있습니다. 디자이너는 각 카테고리에 대해 추가된 GameplayTagContainers제거된 GameplayTagContainers를 설정할 수 있으며, 그 결과는 컴파일 시 Combined GameplayTagContainer에 표시됩니다.

  • 추가된 태그: 상위 클래스에 없던 태그를 해당 GameplayEffect가 추가하는 경우입니다.
  • 제거된 태그: 상위 클래스에는 있지만 해당 서브 클래스에는 없는 태그를 의미합니다.
카테고리설명
Gameplay Effect Asset TagsGameplayEffect가 가진 태그입니다. 해당 태그는 별도의 기능을 수행하지 않으며 GameplayEffect를 설명하는 용도로만 사용됩니다.
Granted TagsGameplayEffect에 존재하며, 해당 GameplayEffect가 적용된 ASC에도 전달되는 태그입니다. GameplayEffect가 제거되면 ASC에서도 태그가 제거됩니다. 이는 DurationInfinite GameplayEffect에만 작동합니다.
Ongoing Tag RequirementsGameplayEffect가 적용된 후, 해당 태그들은 GameplayEffect가 활성(on) 또는 비활성(off) 상태인지 결정합니다. GameplayEffect는 비활성 상태에서도 적용될 수 있습니다. 태그 요구 사항을 충족하지 않아 비활성 상태였던 GameplayEffect가 요구 사항을 다시 충족하면, GameplayEffect는 활성화되며 그 Modifier를 다시 적용합니다. 이 기능은 DurationInfinite GameplayEffect에서만 작동합니다.
Application Tag RequirementsGameplayEffect가 타겟에 적용될 수 있는지 여부를 결정하는 태그입니다. 요구 사항이 충족되지 않으면 GameplayEffect는 적용되지 않습니다.
Remove Gameplay Effects with TagsGameplayEffect가 성공적으로 적용되면 타겟의 Asset TagsGranted Tags에 해당 태그를 가진 다른 GameplayEffect가 제거됩니다.

⬆ 위로 가기

4.5.8 면역

GameplayEffectGameplayTags를 기반으로 다른 GameplayEffect의 적용을 차단하는 면역(Immunity)을 부여할 수 있습니다. 면역은 Application Tag Requirement와 같은 다른 수단을 통해서도 효과적으로 달성할 수 있지만, 해당 시스템을 사용하면 UAbilitySystemComponent::OnImmunityBlockGameplayEffectDelegate 델리게이트를 통해 면역으로 인해 GameplayEffect가 차단되었을 때 알림을 받을 수 있습니다.

GrantedApplicationImmunityTag Source ASC(Source에 Ability가 있었던 경우 해당 Abilty의 AbilityTag도 포함)에 지정된 태그가 있는지를 검사합니다. 이를 통해 특정 캐릭터나 Source에 기반한 태그로부터 오는 GameplayEffect를 모두 차단할 수 있습니다.

Granted Application Immunity Query는 들어오는 GameplayEffectSpec이 지정된 쿼리 중 하나와 일치하는지를 확인하여 적용을 차단하거나 허용합니다.

해당 쿼리들은 GameplayEffect 블루프린트에서 마우스를 올리면 유용한 툴팁으로 설명을 제공해 줍니다.

⬆ 위로 가기


4.5.9 Gameplay Effect Spec

GameplayEffectSpec (GESpec)은 GameplayEffect의 인스턴스화된 버전으로 생각할 수 있습니다. GESpec은 이를 대표하는 GameplayEffect 클래스에 대한 참조, 생성 시점의 레벨, 그리고 이를 생성한 주체를 포함합니다. GameplayEffect는 디자이너가 런타임 이전에 만들어야 하는 반면, GameplayEffectSpec은 런타임에 자유롭게 생성 및 수정될 수 있습니다. GameplayEffect를 적용할 때, GameplayEffectSpecGameplayEffect로부터 생성되며 실제로 Target에 적용되는 것이 바로 이 GESpec입니다.

GameplayEffectSpecGameplayEffects에서 UAbilitySystemComponent::MakeOutgoingSpec()을 사용해 생성되며, 해당 함수는 BlueprintCallable입니다. GameplayEffectSpecs은 즉시 적용될 필요는 없습니다. 일반적으로 GameplayEffectSpecs을 Ability에서 생성된 프로젝트타일에 전달하고, 해당 프로젝트타일이 나중에 맞은 대상에게 이를 적용하는 방식으로 사용됩니다. GameplayEffectSpecs가 성공적으로 적용되면 FActiveGameplayEffect라는 새로운 구조체가 반환됩니다.

참고해두면 좋은 GameplayEffectSpec의 주요 내용:

  • 해당 GameplayEffectSpec가 생성된 GameplayEffect 클래스
  • GameplayEffectSpec의 레벨. 보통 GameplayEffectSpec를 생성한 Ability의 레벨과 같지만 다를 수도 있음.
  • GameplayEffectSpec의 지속 시간. 기본적으로 원본 GameplayEffect의 지속 시간이지만 다르게 설정될 수 있음.
  • GameplayEffectSpec의 Period(Period Effect의 경우). 기본적으로 원본 GameplayEffect의 Period지만 변경될 수 있음.
  • GameplayEffectSpec의 현재 스택 수. 스택 한계는 원본 GameplayEffect에 설정되어 있음.
  • GameplayEffectContextHandle은 해당 GameplayEffectSpec를 생성한 주체를 나타냄.
  • 스냅샷팅(Snapshotting)에 의해 GameplayEffectSpec 생성 시점에 캡처된 Attribute.
  • GameplayEffect가 부여하는 GameplayTag 외에 Target에게 GameplayEffectSpec가 추가로 부여되는 DynamicGrantedTag.
  • GameplayEffect가 가지는 AssetTag 외에 GameplayEffectSpec가 추가로 가지는 DynamicAssetTag.
  • SetByCaller TMaps.

⬆ 위로 가기

4.5.9.1 SetByCallers

SetByCallerGameplayEffectSpec이 GameplayTag 또는 FName에 연결된 float 값을 운반하도록 허용합니다. 이 값들은 각각의 TMap에 저장됩니다:

  • TMap<FGameplayTag, float>
  • TMap<FName, float>

SetByCaller는 GameplayEffectModifier로 사용되거나, 일반적으로 float 값을 다른 시스템으로 전달하는 수단으로 사용될 수 있습니다. 보통 Ability 내부에서 생성된 수치 데이터를 GameplayEffectExecutionCalculationsModifierMagnitudeCalculations에 전달할 때 SetByCaller가 사용됩니다.

SetByCaller 사용처사용 방법
ModifiersGameplayEffect 클래스에서 미리 정의되어야 하며, GameplayTag 버전만 사용할 수 있습니다. 만약 GameplayEffectSpec이 일치하는 태그와 float 값 쌍을 가지지 않는다면, 게임은 런타임 오류를 발생시키고 해당 GameplayEffectSpec의 값은 0이 반환됩니다. 이는 나눗셈(Divide) 연산 시 잠재적 문제를 일으킬 수 있습니다. 자세한 내용은 Modifiers를 참고하세요.
기타미리 정의될 필요가 없으며, 어디서든 사용할 수 있습니다. GameplayEffectSpec에 존재하지 않는 SetByCaller 값을 읽으면 개발자가 정의한 기본 값(Default Value)을 반환할 수 있으며, 경고 메시지를 선택적으로 출력할 수도 있습니다.

블루프린트에서 SetByCaller 값을 할당하려면, 필요한 버전에 대한 블루프린트 노드(GameplayTag 또는 FName)를 사용합니다.

Assigning SetByCaller

블루프린트에서 SetByCaller 값을 읽으려면, Blueprint Library에 커스텀 노드를 만들어야 합니다.

C++에서 SetByCaller 값을 할당하려면 필요한 함수 버전(GameplayTag 또는 FName)을 사용하세요:

float GetSetByCallerMagnitude(FName DataName, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;
float GetSetByCallerMagnitude(FGameplayTag DataTag, bool WarnIfNotFound = true, float DefaultIfNotFound = 0.f) const;

FName 버전보다 GameplayTag 버전을 사용하는 것이 좋습니다. 이렇게 하면 블루프린트에서 철자 오류를 방지할 수 있습니다.

⬆ 위로 가기

4.5.10 Gameplay Effect Context

GameplayEffectContext 구조체는 GameplayEffectSpec의 시작자(Instigator)와 TargetData에 대한 정보를 보유합니다. 해당 구조체는 또한 ModifierMagnitudeCalculations, GameplayEffectExecutionCalculations, AttributeSets, GameplayCues 등과 같은 다양한 장소에서 임의의 데이터를 전달할 때 서브클래싱하여 사용하는 데 유용합니다.

GameplayEffectContext를 서브클래싱하려면 다음 단계를 따르세요:

  1. FGameplayEffectContext 서브클래스 생성
  2. FGameplayEffectContext::GetScriptStruct() 재정의
  3. FGameplayEffectContext::Duplicate() 재정의
  4. 새로운 데이터가 리플리케이트되어야 하는 경우 FGameplayEffectContext::NetSerialize() 재정의
  5. 부모 구조체인 FGameplayEffectContext가 사용하는 것처럼 서브클래스를 위해 TStructOpsTypeTrait 구현
  6. AbilitySystemGlobals 클래스에서 AllocGameplayEffectContext()를 재정의하여 서브클래스의 새 객체를 반환하도록 설정.

GASShooter는 서브클래싱된 GameplayEffectContext를 사용하여 TargetData를 추가하고, 이를 GameplayCue에서 접근할 수 있도록 합니다. 특히 산탄총과 같이 여러 적을 한 번에 맞힐 수 있는 경우에 유용합니다.

⬆ 위로 가기

4.5.11 Modifier Magnitude Calculation

ModifierMagnitudeCalculations (ModMagCalc 또는 MMC)는 GameplayEffect에서 Modifiers로 사용되는 강력한 클래스입니다. 이들은 GameplayEffectExecutionCalculations와 유사하게 작동하지만, 기능이 덜 강력하고 가장 중요한 점은 predicted이 가능하다는 것입니다. 이들의 주요 목적은 CalculateBaseMagnitude_Implementation()에서 float 값을 반환하는 것입니다. 이 함수를 Blueprint와 C++에서 서브클래싱하고 재정의할 수 있습니다.

MMCInstant, Duration, Infinite, Periodic 등 어떤 종류의 GameplayEffect에도 사용할 수 있습니다.

MMC의 강점은 GameplayEffectSpec에 대한 전체 액세스를 통해 Source 또는 Target의 여러 Attribute 값을 캡처할 수 있다는 점입니다. 이로 인해 GameplayTagSetByCaller를 읽을 수 있습니다. Attribute는 스냅샷 방식으로 캡처할 수 있으며, 그렇지 않은 경우에도 캡처할 수 있습니다. 스냅샷된 AttributeGameplayEffectSpec이 생성될 때 캡처되고, 비스냅샷 AttributeGameplayEffectSpec이 적용될 때 캡처되며, InfiniteDuration 효과에 대해 Attribute가 변경될 때 자동으로 업데이트됩니다. Attribute 캡처는 해당 AttributeCurrentValue를 ASC의 기존 모드에서 다시 계산합니다.

note 이 재계산은 AbilitySetPreAttributeChange()를 실행하지 않으므로, 모든 클램핑은 여기에서 다시 수행해야 합니다.

SnapshotSource or TargetGameplayEffectSpec의 캡처 시점Infinite 또는 Duration GEAttribute가 변경될 경우 자동 업데이트 여부.
YesSourceCreationNo
YesTargetApplicationNo
NoSourceApplicationYes
NoTargetApplicationYes

MMC에서 나온 float 값은 GameplayEffectModifier에서 계수(coefficient)와 전후 계수 추가에 의해 더 수정될 수 있습니다.`

예시로, 타겟의 mana 속성을 캡처하여 독 효과로부터 이를 감소시키는 MMC가 있을 수 있습니다. 해당 감소량은 타겟이 가진 mana 양과 타겟이 가지고 있을 수 있는 태그에 따라 달라집니다.

UPAMMC_PoisonMana::UPAMMC_PoisonMana()
{

	//ManaDef defined in header FGameplayEffectAttributeCaptureDefinition ManaDef;
	ManaDef.AttributeToCapture = UPAAttributeSetBase::GetManaAttribute();
	ManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	ManaDef.bSnapshot = false;

	//MaxManaDef defined in header FGameplayEffectAttributeCaptureDefinition MaxManaDef;
	MaxManaDef.AttributeToCapture = UPAAttributeSetBase::GetMaxManaAttribute();
	MaxManaDef.AttributeSource = EGameplayEffectAttributeCaptureSource::Target;
	MaxManaDef.bSnapshot = false;

	RelevantAttributesToCapture.Add(ManaDef);
	RelevantAttributesToCapture.Add(MaxManaDef);
}

float UPAMMC_PoisonMana::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	// Gather the tags from the source and target as that can affect which buffs should be used
	const FGameplayTagContainer* SourceTags = Spec.CapturedSourceTags.GetAggregatedTags();
	const FGameplayTagContainer* TargetTags = Spec.CapturedTargetTags.GetAggregatedTags();

	FAggregatorEvaluateParameters EvaluationParameters;
	EvaluationParameters.SourceTags = SourceTags;
	EvaluationParameters.TargetTags = TargetTags;

	float Mana = 0.f;
	GetCapturedAttributeMagnitude(ManaDef, Spec, EvaluationParameters, Mana);
	Mana = FMath::Max<float>(Mana, 0.0f);

	float MaxMana = 0.f;
	GetCapturedAttributeMagnitude(MaxManaDef, Spec, EvaluationParameters, MaxMana);
	MaxMana = FMath::Max<float>(MaxMana, 1.0f); // Avoid divide by zero

	float Reduction = -20.0f;
	if (Mana / MaxMana > 0.5f)
	{
		// Double the effect if the target has more than half their mana
		Reduction *= 2;
	}
	
	if (TargetTags->HasTagExact(FGameplayTag::RequestGameplayTag(FName("Status.WeakToPoisonMana"))))
	{
		// Double the effect if the target is weak to PoisonMana
		Reduction *= 2;
	}
	
	return Reduction;
}

MMC 의 생성자에서 RelevantAttributesToCaptureFGameplayEffectAttributeCaptureDefinition 을 추가하지 않고 Attributes 캡처를 시도하면 캡처 도중 스펙이 없다는 오류가 발생합니다. Attributes을 캡처할 필요가 없는 경우 RelevantAttributesToCapture에 아무 것도 추가할 필요가 없습니다.

⬆ 위로 가기

4.5.12 Gameplay Effect Execution Calculation

GameplayEffectExecutionCalculations (ExecutionCalculation, Execution(해당 용어는 플러그인의 소스 코드에서 자주 보임), 또는 ExecCalc)은 GameplayEffectASC에 변화를 주는 가장 강력한 방법입니다. ModifierMagnitudeCalculations와 유사하게, ExecutionCalculationAttribute를 캡처할 수 있으며, 이를 선택적으로 스냅샷 방식으로 캡처할 수 있습니다. MMC와는 달리, ExecutionCalculation은 하나 이상의 Attribute를 변경할 수 있으며, 본질적으로 프로그래머가 원하는 모든 작업을 수행할 수 있습니다. 그러나 이 강력한 기능과 유연성의 단점은 predicted이 불가능하고 C++로 구현해야 한다는 것입니다.

ExecutionCalculationInstantPeriodic GameplayEffect에서만 사용 가능합니다. Execute라는 단어가 포함된 것은 일반적으로 이 두 종류의 GameplayEffect를 의미합니다.

스냅샷은 GameplayEffectSpec이 생성될 때 Attribute를 캡처하며, 비스냅샷은 GameplayEffectSpec이 적용될 때 Attribute를 캡처합니다. Attribute 캡처는 해당 Attribute의 CurrentValueASC의 기존 모드에서 다시 계산합니다.

note 이 재계산은 AbilitySetPreAttributeChange()를 실행하지 않으므로, 모든 클램핑은 여기에서 다시 수행해야 합니다.

SnapshotSource or TargetGameplayEffectSpec 캡처 시점
YesSourceCreation
YesTargetApplication
NoSourceApplication
NoTargetApplication

Attribute 캡처를 설정하려면, 에픽 게임즈의 ActionRPG Sample Project에서 설정한 패턴을 따르며, Attributes를 캡처하는 방법을 정의하는 구조체를 정의하고, 구조체의 생성자에서 해당 구조체의 복사본을 생성해야 합니다. ExecCalc마다 이런 구조체가 필요합니다.

Note: 구조체 이름은 고유해야 합니다. 동일한 이름을 사용하면 Attribute 캡처에서 잘못된 동작이 발생할 수 있습니다(주로 잘못된 Attribute의 값이 캡처됨).

Local Predicted, Server Only, Server Initiated GameplayAbilities의 경우, ExecCalc는 서버에서만 호출됩니다.

SourceTarget에서 여러 Attribute를 읽어 복잡한 공식에 따라 피해를 계산하는 것이 ExecCalc의 가장 일반적인 예입니다. 포함된 Sample Project에는 GameplayEffectSpecSetByCaller에서 피해 값을 읽고, Target에서 캡처된 방어구 Attribute를 기준으로 그 값을 완화하는 간단한 ExecCalc가 포함되어 있습니다. 이 예시는 GDDamageExecCalculation.cpp/.h에서 확인할 수 있습니다.

⬆ 위로 가기

4.5.12.1 Sending Data to Execution Calculations

ExecutionCalculationAttribute를 캡처하는 것 외에도 데이터를 전달하는 몇 가지 방법이 있습니다.

4.5.12.1.1 SetByCaller

GameplayEffectSpec에 설정된 모든 SetByCaller값은 ExecutionCalculation에서 직접 읽을 수 있습니다.

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
float Damage = FMath::Max<float>(Spec.GetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName("Data.Damage")), false, -1.0f), 0.0f);

4.5.12.1.2 Backing Data Attribute Calculation Modifier

값을 GameplayEffect에 하드코딩하려면, 캡처된 Attribute 중 하나를 백업 데이터로 사용하는 CalculationModifier를 사용하여 값을 전달할 수 있습니다.

아래의 스크린샷 예제에서는 캡처된 Damage Attribute에 50을 추가하고 있습니다. 또한, 값을 Override로 설정하여 하드코딩된 값만 사용할 수도 있습니다.

Backing Data Attribute Calculation Modifier

ExecutionCalculationAttribute를 캡처할 때 이 값을 읽습니다.

float Damage = 0.0f;
// ExecutionCalculation에서 CalculationModifier로 설정된 옵션성 피해 값을 GameplayEffect의 Damage GE에 캡처합니다.
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(DamageStatics().DamageDef, EvaluationParameters, Damage);

4.5.12.1.4 Gameplay Effect Context

ExecutionCalculation으로 데이터를 보내려면, GameplayEffectContext on the GameplayEffectSpec를 커스텀하여 전달할 수 있습니다.

ExecutionCalculation에서 FGameplayEffectCustomExecutionParameter를 통해 EffectContext에 접근할 수 있습니다.

const FGameplayEffectSpec& Spec = ExecutionParams.GetOwningSpec();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(Spec.GetContext().Get());

GameplayEffectSpec 또는 EffectContext의 내용을 변경해야 하는 경우:

FGameplayEffectSpec* MutableSpec = ExecutionParams.GetOwningSpecForPreExecuteMod();
FGSGameplayEffectContext* ContextHandle = static_cast<FGSGameplayEffectContext*>(MutableSpec->GetContext().Get());

ExecutionCalculation에서 GameplayEffectSpec을 수정할 때는 주의해야 합니다.
GetOwningSpecForPreExecuteMod()에 대한 주석을 참고하십시오.

/** Const 접근이 아닙니다. 특히 Attribute 캡처 후 Spec을 수정할 때 주의하세요. */
FGameplayEffectSpec* GetOwningSpecForPreExecuteMod() const;

⬆ 위로 가기

4.5.13 Custom Application Requirement

CustomApplicationRequirement(CAR) 클래스는 GameplayEffect가 적용될 수 있는지에 대한 고급 제어를 디자이너에게 제공합니다. 이는 GameplayEffect의 단순한 GameplayTag 검사보다 더 복잡한 조건을 설정할 수 있게 해줍니다. CAR은 CanApplyGameplayEffect()를 오버라이드하여 블루프린트에서 구현할 수 있으며, C++에서는 CanApplyGameplayEffect_Implementation()을 오버라이드하여 구현할 수 있습니다.

CAR의 사용 예시:

  • Target이 특정 Attribute의 값을 일정 수준 이상 가지고 있어야 하는 경우
  • Target이 특정 GameplayEffect의 스택을 일정 수 이상 가지고 있어야 하는 경우

CAR은 더 많은 고급 기능을 수행할 수 있습니다. 예를 들어, 해당 대상에게 이미 GameplayEffect의 인스턴스가 적용되어 있는지 확인하고, 새 인스턴스를 적용하는 대신 기존 인스턴스의 changing the duration할 수 있습니다(이 경우 CanApplyGameplayEffect()에서 false를 반환).

⬆ 위로 가기

4.5.14 Cost Gameplay Effect

GameplayAbilities에는 선택적으로 Ability의 Cost(비용)로 사용할 수 있도록 설계된 GameplayEffect가 존재합니다. Cost란, AbilitySystemComponent (ASC)가 GameplayAbility를 활성화하기 위해 필요한 Attribute의 양을 의미합니다. 만약 GameplayAbility가 Cost에 해당하는 GameplayEffect를 감당할 수 없다면 활성화되지 않습니다.

해당 Cost GameplayEffectInstant 타입이어야 하며, 하나 이상의 Modifier를 통해 Attribute에서 값을 차감합니다. 기본적으로 Cost GameplayEffect는 예측(Prediction)을 지원해야 하므로 ExecutionCalculation을 사용하지 않는 것이 좋습니다. 복잡한 Cost 계산이 필요하다면 MMC(GameplayModMagnitudeCalculation)를 사용하는 것이 허용되며 권장됩니다.

처음 시작할 때는 대부분 GameplayAbility마다 고유한 Cost GameplayEffect를 설정하게 될 것입니다. 하지만 더 고급 기법으로는 여러 개의 GameplayAbility에서 하나의 Cost GameplayEffect를 재사용할 수 있습니다. 이때는 Cost 값이 각 GameplayAbility에 정의되어야 하며, 생성된 GameplayEffectSpecGameplayAbility별 데이터를 추가로 설정합니다. 이 방법은 인스턴스화된(Instanced) GameplayAbility에서만 작동합니다.

Cost GameplayEffect를 재사용하는 두 가지 방법:

  1. MMC 사용하기 (가장 쉬운 방법)
    MMC를 만들고, GameplayEffectSpec에서 GameplayAbility 인스턴스로부터 Cost 값을 가져옵니다.
float UPGMMC_HeroAbilityCost::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());

	if (!Ability)
	{
		return 0.0f;
	}

	return Ability->Cost.GetValueAtLevel(Ability->GetAbilityLevel());
}

이 예제에서 Cost 값은 GameplayAbility 자식 클래스에 추가된 FScalableFloat 타입의 변수입니다.

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cost")
FScalableFloat Cost;

Cost GE With MMC

  1. UGameplayAbility::GetCostGameplayEffect() 오버라이드하기
    이 함수를 오버라이드하면 GameplayAbility의 Cost 값을 기반으로 GameplayEffect를 런타임에 생성할 수 있습니다.

⬆ 위로 가기

4.5.15 Cooldown Gameplay Effect

GameplayAbilities에는 Cooldown(쿨타임) 용도로 사용할 수 있도록 설계된 GameplayEffect가 있습니다. Cooldown은 GameplayAbility를 활성화한 후 다시 사용할 수 있을 때까지의 시간을 결정합니다. 만약 GameplayAbility가 아직 Cooldown 상태라면 활성화할 수 없습니다. 이 Cooldown GameplayEffectDuration 타입이어야 하며 Modifier가 없어야 합니다. 또한 GameplayEffectGrantedTagGameplayAbility별로 고유한 GameplayTag(“Cooldown Tag”)를 할당해야 합니다. 만약 게임에 슬롯 개념이 존재하고, 슬롯 간에 Cooldown을 공유한다면 슬롯당 고유한 GameplayTag를 사용할 수도 있습니다.
GameplayAbility는 실제로 Cooldown Tag의 존재 여부를 확인하지, Cooldown GameplayEffect 자체를 확인하지는 않습니다. 기본적으로 Cooldown GameplayEffect는 예측을 지원해야 하므로 ExecutionCalculation를 사용하지 않는 것이 좋습니다. 대신, 복잡한 Cooldown 계산에는 MMC를 사용하는 것이 허용되며 권장됩니다.

처음에는 GameplayAbility마다 고유한 Cooldown GameplayEffect를 설정하게 됩니다. 하지만 더 고급 기법으로는 여러 개의 GameplayAbility에서 하나의 Cooldown GameplayEffect를 재사용할 수 있습니다. 이 경우 Cooldown 시간과 Cooldown Tag는 각 GameplayAbility에서 정의해야 하며, 생성된 GameplayEffectSpec에 해당 데이터를 동적으로 설정합니다. 이 방법은 인스턴스화된(Instanced)GameplayAbility에서만 작동합니다.

Cooldown GameplayEffect를 재사용하는 두 가지 방법:

  1. SetByCaller를 활용한 방법(가장 쉬운 방법)

공유 Cooldown GameplayEffect(GE)의 Duration을 GameplayTag와 함께 SetByCaller로 설정합니다. GameplayAbility 서브클래스에서 다음을 정의합니다. GameplayAbility 서브클래스에 Duration에 대한 float / FScalableFloat, 고유 Cooldown 태그에 대한 FGameplayTagContainer, Cooldown TagCooldown GE의 태그를 합친 반환 포인터로 사용할 임시 FGameplayTagContainer를 정의합니다.

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;

// GetCooldownTags()에서 반환할 포인터를 사용할 임시 컨테이너입니다.
// 이것은CooldownTag와 Cooldown GE의 CoolDown 태그를 합친 값입니다.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

그런 다음, UGameplayAbility::GetCooldownTags()를 오버라이드하여 Cooldown Tag와 기존 cooldown GameplayEffect의 Tag를 합친 값을 반환하도록 합니다.

const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
	FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
	MutableTags->Reset(); // MutableTags는 CDO의 TempCooldownTags에 기록되므로, GameplayAbility의 Cooldown 태그가 변경될 경우(다른 슬롯으로 이동) 이를 지우기 위해 초기화.
	const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
	if (ParentTags)
	{
		MutableTags->AppendTags(*ParentTags);
	}
	MutableTags->AppendTags(CooldownTags);
	return MutableTags;
}

마지막으로, UGameplayAbility::ApplyCooldown()을 오버라이드하여 Cooldown Tag를 주입하고, Cooldown GameplayEffectSpecSetByCaller를 추가합니다.

void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
	UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
	if (CooldownGE)
	{
		FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
		SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
		SpecHandle.Data.Get()->SetSetByCallerMagnitude(FGameplayTag::RequestGameplayTag(FName(  OurSetByCallerTag  )), CooldownDuration.GetValueAtLevel(GetAbilityLevel()));
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
	}
}

이 그림에서 Cooldown의 Duration ModifierSetByCaller로 설정되며, Data.Cooldown이라는 data Tag를 사용합니다. Data.Cooldown은 위 코드에서 OurSetByCallerTag에 해당합니다.

Cooldown GE with SetByCaller

  1. MMC를 활용한 방법

해당 방법은 위의 방법(ApplyCooldown)과 동일한 설정을 사용하지만, Cooldown GE의 지속 시간을 SetByCaller로 설정하는 대신, Duration을 Custom Calculation Class로 설정하고, 새로 만들 MMC를 가리키도록 합니다.

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FScalableFloat CooldownDuration;

UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Cooldown")
FGameplayTagContainer CooldownTags;

// GetCooldownTags()에서 반환할 포인터로 사용할 임시 컨테이너입니다.
// CooldownTags와 Cooldown GE의 Cooldown 태그를 합친 값입니다.
UPROPERTY(Transient)
FGameplayTagContainer TempCooldownTags;

그런 다음, UGameplayAbility::GetCooldownTags()를 오버라이드하여 Cooldown Tag와 기존 Cooldown GE의 태그를 합친 값을 반환하도록 합니다.

const FGameplayTagContainer * UPGGameplayAbility::GetCooldownTags() const
{
	FGameplayTagContainer* MutableTags = const_cast<FGameplayTagContainer*>(&TempCooldownTags);
	MutableTags->Reset(); // MutableTags writes to the TempCooldownTags on the CDO so clear it in case the ability cooldown tags change (moved to a different slot)
	const FGameplayTagContainer* ParentTags = Super::GetCooldownTags();
	if (ParentTags)
	{
		MutableTags->AppendTags(*ParentTags);
	}
	MutableTags->AppendTags(CooldownTags);
	return MutableTags;
}

마지막으로, UGameplayAbility::ApplyCooldown()을 오버라이드하여 Cooldown Tag를 Cooldown GameplayEffectSpec에 주입합니다.

void UPGGameplayAbility::ApplyCooldown(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
	UGameplayEffect* CooldownGE = GetCooldownGameplayEffect();
	if (CooldownGE)
	{
		FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(CooldownGE->GetClass(), GetAbilityLevel());
		SpecHandle.Data.Get()->DynamicGrantedTags.AppendTags(CooldownTags);
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, SpecHandle);
	}
}
float UPGMMC_HeroAbilityCooldown::CalculateBaseMagnitude_Implementation(const FGameplayEffectSpec & Spec) const
{
	const UPGGameplayAbility* Ability = Cast<UPGGameplayAbility>(Spec.GetContext().GetAbilityInstance_NotReplicated());

	if (!Ability)
	{
		return 0.0f;
	}

	return Ability->CooldownDuration.GetValueAtLevel(Ability->GetAbilityLevel());
}

Cooldown GE with MMC

⬆ 위로 가기

4.5.15.1 Cooldown GameplayEffect의 남은 시간 얻어내기
bool APGPlayerState::GetCooldownRemainingForTag(FGameplayTagContainer CooldownTags, float & TimeRemaining, float & CooldownDuration)
{
	if (AbilitySystemComponent && CooldownTags.Num() > 0)
	{
		TimeRemaining = 0.f;
		CooldownDuration = 0.f;

		FGameplayEffectQuery const Query = FGameplayEffectQuery::MakeQuery_MatchAnyOwningTags(CooldownTags);
		TArray< TPair<float, float> > DurationAndTimeRemaining = AbilitySystemComponent->GetActiveEffectsTimeRemainingAndDuration(Query);
		if (DurationAndTimeRemaining.Num() > 0)
		{
			int32 BestIdx = 0;
			float LongestTime = DurationAndTimeRemaining[0].Key;
			for (int32 Idx = 1; Idx < DurationAndTimeRemaining.Num(); ++Idx)
			{
				if (DurationAndTimeRemaining[Idx].Key > LongestTime)
				{
					LongestTime = DurationAndTimeRemaining[Idx].Key;
					BestIdx = Idx;
				}
			}

			TimeRemaining = DurationAndTimeRemaining[BestIdx].Key;
			CooldownDuration = DurationAndTimeRemaining[BestIdx].Value;

			return true;
		}
	}

	return false;
}

Note: 클라이언트에서 Cooldown의 남은 시간을 Query(조회)하려면 리플리케이트된 GameplayEffect를 수신할 수 있어야 합니다. 이는 ASCreplication mode에 따라 달라집니다.

4.5.15.2 Cooldown 시작 및 종료 청취(Listening)

Cooldown이 시작되는 시점을 수신하려면, AbilitySystemComponent->OnActiveGameplayEffectAddedDelegateToSelf에 바인딩하여 Cooldown GE가 적용될 때 응답하거나, AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved)에 바인딩하여 Cooldown Tag가 추가될 때 응답할 수 있습니다. Cooldown GE가 언제 추가되었는지 확인하는 것이 좋은데, Cooldown GE를 적용한 GameplayEffectSpec에도 접근할 수 있기 때문입니다. 이를 통해 Cooldown GE가 로컬에서 예측한 것인지 서버에서 수정한 것인지를 확인할 수 있습니다.

Cooldown이 언제 끝나는지 수신하려면, AbilitySystemComponent->OnAnyGameplayEffectRemovedDelegate()에 바인딩하여 Cooldown GE가 제거되는 시점에 응답하거나, AbilitySystemComponent->RegisterGameplayTagEvent(CooldownTag, EGameplayTagEventType::NewOrRemoved)에 바인딩하여 Cooldown Tag가 제거되는 시점에 응답하면 됩니다. 서버의 수정된 Cooldown GE가 들어오면 로컬에서 예측한 Cooldown이 제거되어 Cooldown이 진행 중임에도 불구하고 OnAnyGameplayEffectRemovedDelegate()가 발동되므로 Cooldown Tag가 제거되는 시점을 잘 살펴볼 것을 권장합니다. 예측된 Cooldown GE를 제거하고 서버의 수정된 Cooldown GE를 적용하는 동안 Cooldown Tag는 변경되지 않습니다.

Note: 클라이언트에서 GameplayEffect가 추가되거나 제거되는 것을 듣기 위해서는 클라이언트가 리플리케이트된 GameplayEffect를 받을 수 있어야 합니다. 이는 ASCreplication mode에 따라 달라집니다.

샘플 프로젝트에는 Cooldown이 시작되고 끝나는 것을 듣는 Costom Blueprint 노드가 포함되어 있습니다. HUD UMG Widget은 이를 사용하여 메테오의 Cooldown의 남은 시간을 업데이트합니다. 해당 AsyncTaskEndTask()가 수동으로 호출될 때까지 계속 살아 있습니다. UMG Widget의 Destruct 이벤트에서 이를 처리합니다. AsyncTaskCooldownChanged.h/cpp를 참고하세요.

Listen for Cooldown Change BP Node

4.5.15.3 Predicting Cooldowns

현재 Cooldown을 실제로 예측할 수 없습니다. 로컬에서 예측된 Cooldown GE가 적용될 때 UI Cooldown 타이머를 시작할 수 있지만, GameplayAbility의 실제 Cooldown은 서버의 Cooldown의 남은 시간에 연결되어 있습니다. 플레이어의 지연 시간에 따라, 로컬에서 예측된 Cooldown이 만료되었을 수 있지만, GameplayAbility는 여전히 서버에서 Cooldown 중일 수 있으며, 이로 인해 서버의 Cooldown이 만료될 때까지 GameplayAbility를 즉시 재활성화할 수 없습니다.

샘플 프로젝트에서는 로컬에서 예측된 Cooldown이 시작될 때 메테오 Ability의 UI 아이콘을 회색으로 처리하고, 서버에서 수정된 Cooldown GE가 들어오면 Cooldown 타이머를 시작하는 방식으로 이를 처리합니다.

게임 플레이 결과로, 지연 시간이 높은 플레이어는 짧은 Cooldown 능력에 대해 낮은 발사 속도를 가지게 되어 지연 시간이 낮은 플레이어에 비해 불리한 상황에 처하게 됩니다. Fortnite는 이를 피하기 위해 무기들이 Cooldown GameplayEffect를 사용하지 않고 맞춤형 기록 방식을 사용합니다.

진정한 예측된 Cooldown을 허용하는(플레이어가 로컬 Cooldown이 만료되었을 때 GameplayAbility를 활성화할 수 있지만 서버에서는 여전히 Cooldown 중인 상태) 것은 에픽 게임즈가 향후 GAS의 다음 버전에서 구현하고자 하는 기능입니다.

⬆ 위로 가기

4.5.16 활성화된 GameplayEffect 지속 시간 변경

Cooldown GE 또는 Duration GameplayEffect의 남은 시간을 변경하려면, GameplayEffectSpecDuration을 변경하고, StartServerWorldTime을 업데이트하고, CachedStartServerWorldTime을 업데이트하고, StartWorldTime을 업데이트한 다음 CheckDuration()으로 지속시간 검사를 다시 실행해야 합니다. 서버에서 이 작업을 수행하고 FActiveGameplayEffect를 더티로 표시하면 클라이언트에 변경사항이 리플리케이트됩니다.

Cooldown GE나 다른 Duration GameplayEffect의 남은 시간을 변경하려면, GameplayEffectSpecDuration을 변경하고, StartServerWorldTime, CachedStartServerWorldTime, StartWorldTime을 업데이트한 후, CheckDuration()으로 Duration 검사를 다시 실행해야 합니다. 서버에서 이를 수행하고 FActiveGameplayEffect를 더티 마킹하면, 변경 사항이 클라이언트에 리플리케이트됩니다.

Note: 이것은 const_cast가 필요하며 에픽 게임즈가 의도한 Duration 변경 방식이 아닐 수도 있지만, 지금까지는 잘 작동하는 것 같습니다.

bool UPAAbilitySystemComponent::SetGameplayEffectDurationHandle(FActiveGameplayEffectHandle Handle, float NewDuration)
{
	if (!Handle.IsValid())
	{
		return false;
	}

	const FActiveGameplayEffect* ActiveGameplayEffect = GetActiveGameplayEffect(Handle);
	if (!ActiveGameplayEffect)
	{
		return false;
	}

	FActiveGameplayEffect* AGE = const_cast<FActiveGameplayEffect*>(ActiveGameplayEffect);
	if (NewDuration > 0)
	{
		AGE->Spec.Duration = NewDuration;
	}
	else
	{
		AGE->Spec.Duration = 0.01f;
	}

	AGE->StartServerWorldTime = ActiveGameplayEffects.GetServerWorldTime();
	AGE->CachedStartServerWorldTime = AGE->StartServerWorldTime;
	AGE->StartWorldTime = ActiveGameplayEffects.GetWorldTime();
	ActiveGameplayEffects.MarkItemDirty(*AGE);
	ActiveGameplayEffects.CheckDuration(Handle);

	AGE->EventSet.OnTimeChanged.Broadcast(AGE->Handle, AGE->StartWorldTime, AGE->GetDuration());
	OnGameplayEffectDurationChange(*AGE);

	return true;
}

⬆ 위로 가기

4.5.17 런타임에서 GameplayEffect 동적 생성하기

런타임에서 동적 GameplayEffect를 생성하는 것은 심화 주제입니다. 이 작업은 자주 할 필요는 없습니다.

Instant GameplayEffect만 C++에서 런타임에 처음부터 생성할 수 있습니다. DurationInfinite GameplayEffect는 런타임에서 동적으로 생성할 수 없습니다. 왜냐하면, 이를 리플리케이트할 때 해당 GameplayEffect 클래스 정의를 찾게 되는데, 이는 존재하지 않기 때문입니다. 이 기능을 구현하려면, 대신 에디터에서 하던 것처럼 Archetype GameplayEffect 클래스를 만들고, 런타임에서 GameplayEffectSpec 인스턴스를 필요한 대로 커스터마이즈하는 방식으로 접근해야 합니다.

런타임에서 생성된 Instant GameplayEffectlocal predicted GameplayAbility내에서 호출될 수 있습니다. 그러나 동적 생성이 부작용을 일으킬 수 있는지는 아직 불확실합니다.

Examples

샘플 프로젝트에서는 캐릭터가 치명적인 타격을 입혔을 때, 그 캐릭터를 처치한 플레이어에게 골드와 경험치를 보내기 위해 GameplayEffect를 생성합니다.

// 보상을 주기 위해 동적 Instant GameplayEffect를 생성
UGameplayEffect* GEBounty = NewObject<UGameplayEffect>(GetTransientPackage(), FName(TEXT("Bounty")));
GEBounty->DurationPolicy = EGameplayEffectDurationType::Instant;

int32 Idx = GEBounty->Modifiers.Num();
GEBounty->Modifiers.SetNum(Idx + 2);

FGameplayModifierInfo& InfoXP = GEBounty->Modifiers[Idx];
InfoXP.ModifierMagnitude = FScalableFloat(GetXPBounty());
InfoXP.ModifierOp = EGameplayModOp::Additive;
InfoXP.Attribute = UGDAttributeSetBase::GetXPAttribute();

FGameplayModifierInfo& InfoGold = GEBounty->Modifiers[Idx + 1];
InfoGold.ModifierMagnitude = FScalableFloat(GetGoldBounty());
InfoGold.ModifierOp = EGameplayModOp::Additive;
InfoGold.Attribute = UGDAttributeSetBase::GetGoldAttribute();

Source->ApplyGameplayEffectToSelf(GEBounty, 1.0f, Source->MakeEffectContext());

두 번째 예시는 로컬 예측 GameplayAbility 내에서 생성된 런타임 GameplayEffect를 보여줍니다. 코드 내 주석을 참조하여 사용할 때 주의하세요!

UGameplayAbilityRuntimeGE::UGameplayAbilityRuntimeGE()
{
	NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted;
}

void UGameplayAbilityRuntimeGE::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	if (HasAuthorityOrPredictionKey(ActorInfo, &ActivationInfo))
	{
		if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
		{
			EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
		}

		// 런타임 중 GE 생성.
		UGameplayEffect* GameplayEffect = NewObject<UGameplayEffect>(GetTransientPackage(), TEXT("RuntimeInstantGE"));
		GameplayEffect->DurationPolicy = EGameplayEffectDurationType::Instant; // 런타임 GE는 인스턴트만 작동합니다.

		// 간단한 스케일러블 float Modifier를 추가하여 MyAttribute를 42로 덮어씁니다.
        // 실제 애플리케이션에서는 TriggerEventData를 통해 전달된 정보를 소비합니다.
		const int32 Idx = GameplayEffect->Modifiers.Num();
		GameplayEffect->Modifiers.SetNum(Idx + 1);
		FGameplayModifierInfo& ModifierInfo = GameplayEffect->Modifiers[Idx];
		ModifierInfo.Attribute.SetUProperty(UMyAttributeSet::GetMyModifiedAttribute());
		ModifierInfo.ModifierMagnitude = FScalableFloat(42.f);
		ModifierInfo.ModifierOp = EGameplayModOp::Override;

		// GE 적용.

        // 여기서 GESpec을 생성하여 ASC가 GE 클래스 기본 객체에서 GESpec을 생성하는 동작을 피합니다.
        // 동적 GE가 있을 때 기본 GameplayEffect 클래스로 GESpec을 생성하면 Modifier가 손실되기 때문에 이렇게 처리합니다. 
        // 주의: 이 해킹이 문제가 될 수 있는지 불확실합니다!
        // GESpec에서 GE가 UPROPERTY로 참조되므로 GE 객체가 GarbageCollector에 의해 수거되는 것을 방지합니다.
		FGameplayEffectSpec* GESpec = new FGameplayEffectSpec(GameplayEffect, {}, 0.f); // new, handle 내에서 shared ptr로 수명이 관리되기 때문입니다.
		ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, FGameplayEffectSpecHandle(GESpec));
	}
	EndAbility(Handle, ActorInfo, ActivationInfo, false, false);
}

⬆ 위로 가기

4.5.18 Gameplay Effect Containers

에픽 게임즈의 Action RPG Sample ProjectFGameplayEffectContainer라는 구조체를 구현합니다. 이는 기본 GAS에는 없지만 GameplayEffectTargetData를 담는 데 매우 유용합니다. 이 구조체는 GameplayEffectSpec를 생성하고 기본 값을 설정하는 등의 작업을 자동화합니다. GameplayAbility에서 GameplayEffectContainer를 생성하고 이를 발사된 투사체에 전달하는 것은 매우 쉽고 직관적입니다. 저는 포함된 샘플 프로젝트에서 GameplayEffectContainer를 구현하지 않았는데, 이는 기본 GAS에서 이를 사용하지 않고 작업하는 방법을 보여주기 위함입니다. 하지만 이 구조체를 프로젝트에 추가하는 것을 고려해보는 것이 좋습니다.

GameplayEffectContainer 안의 GESpec에 접근하여 SetByCaller를 추가하는 등의 작업을 하려면, FGameplayEffectContainer를 분해하고 GESpec의 인덱스를 사용하여 GESpec 참조에 접근해야 합니다. 이를 위해서는 액세스하려는 GESpec의 인덱스를 미리 알아야 합니다.

SetByCaller with a GameplayEffectContainer

GameplayEffectContainer는 효율적인 targeting.을 위한 선택적인 수단도 포함하고 있습니다.

⬆ 위로 가기

4.6 Gameplay Ability

4.6.1 Gameplay Ability 정의

GameplayAbilities (GA)는 게임에서 Actor가 수행할 수 있는 모든 행동이나 스킬입니다. 예를 들어, 질주하면서 총을 쏘는 것처럼 둘 이상의 GameplayAbility를 동시에 활성화할 수도 있습니다. GameplayAbility는 블루프린트 또는 C++에서 구현할 수 있습니다.

GameplayAbility의 예시:

  • 점프
  • 질주
  • 총 발사
  • 특정 초마다 수동으로 공격 막기
  • 포션 사용
  • 문 열기
  • 자원 수집
  • 건물 건설

GameplayAbility로 구현해서는 안 되는 것들:

  • 기본적인 움직임 입력
  • UI와의 상호작용 - GameplayAbility를 사용하여 상점 아이템 구매

이는 규칙이 아니라 권장 사항일 뿐입니다. 설계와 구현은 다를 수 있습니다.

GameplayAbility는 Attribute의 변동량을 조절하거나 기능을 변경하기 위한 레벨(Level) 기능을 기본적으로 제공합니다.

GameplayAbility는 소유하는 클라이언트 또는 서버에서 실행되며, Simulated Proxy에서는 실행되지 않습니다. Net Execution Policy에 따라 GameplayAbility가 로컬에서 predicted 실행될지 결정됩니다. 이 정책에는 Cost 및 Cooldown을 적용할 수 있는 GameplayEffect의 기본 동작이 포함되어 있습니다.
GameplayAbility는 일정 시간 동안 발생하는 이벤트(예: 이벤트 대기, Attribute 변화 대기, 타겟 선택 대기, Root Motion Source를 활용한 Character 이동)를 관리하는 AbilityTasks를 사용합니다. 시뮬레이션된 클라이언트는 GameplayAbility를 실행하지 않습니다. 대신 서버에서 Ability가 실행될 때, 애니메이션 몽타주와 같은 시각적 효과는 AbilityTask 또는 사운드 및 파티클과 같은 시각적 요소를 처리하는 GameplayCues를 통해 리플리케이트되거나 RPC를 통해 전달됩니다.

모든 GameplayAbilityActivateAbility() 함수를 오버라이드하여 게임플레이 로직을 구현해야 합니다. 추가적으로 EndAbility()에 완료되거나 취소될 때 실행될 로직을 추가할 수 있습니다.

간단한 GameplayAbility 흐름도:

Simple GameplayAbility Flowchart

조금 더 복잡한 GameplayAbility 흐름도:

Complex GameplayAbility Flowchart

복잡한 Ability는 여러 GameplayAbility를 사용해 서로 활성화하거나 취소하는 등의 방식으로 상호작용하게 구현할 수 있습니다.

4.6.1.1 Replication Policy(리플리케이션 정책)

이 옵션은 사용하지 않는 것이 좋습니다. 이름이 오해를 불러일으키며 실제로 필요하지도 않습니다. GameplayAbilitySpecs은 기본적으로 서버에서 소유하는 클라이언트로 복제됩니다. 앞서 언급했듯이, GameplayAbility는 Simulated Proxy에서 실행되지 않습니다. 대신 AbilityTaskGameplayCue를 사용해 시각적 변경 사항을 리플리케이트하거나 RPC로 전달합니다. 에픽 게임즈의 Dave Ratti는 이 옵션을 향후 제거할 계획임을 언급한 바 있습니다.

4.6.1.2 Server Respects Remote Ability Cancellation

이 옵션은 대부분의 경우 문제를 일으킵니다. 이 옵션을 활성화하면 클라이언트의 GameplayAbility가 취소되거나 자연스럽게 완료될 경우, 서버에서 실행 중인 Ability도 강제로 종료됩니다. 여기서 중요한 문제는 서버의 Ability가 완료되지 않은 상태에서도 강제로 종료될 수 있다는 점입니다. 이 문제는 특히 로컬 예측(Local Prediction) GameplayAbility를 사용하는 플레이어가 높은 지연 시간(High Latency)을 겪을 때 심각하게 나타납니다. 일반적으로 이 옵션은 비활성화하는 것이 좋습니다.

4.6.1.3 Replicate Input Directly(입력 직접 복제)

이 옵션을 활성화하면 입력 누름(Press) 및 해제(Release) 이벤트가 항상 서버로 리플리케이트됩니다. 하지만 에픽 게임즈에서는 이 옵션을 사용하지 말고 AbilityTasks에 내장된 Generic Replicated Event를 사용하는 것을 권장합니다. 이는 입력이 ASC(Ability System Component)에 바인딩되어 있을 때 더욱 적절합니다.

Epic Games's comment:

/** 직접 입력 상태 리플리케이트. 이 함수들은 Ability에 `bReplicateInputDirectly`가 true로 설정된 경우 호출되지만, 일반적으로 사용하지 않는 것이 좋습니다.(대신 Generic Replicated Event를 사용하는 것이 더 좋습니다) */
UAbilitySystemComponent::ServerSetInputPressed()

⬆ 위로 가기

4.6.2 ASC에 입력 바인딩

ASC(Ability System Component)를 사용하면 입력 액션을 ASC에 직접 바인딩하고, 그 입력을 GameplayAbility에 할당할 수 있습니다. 할당된 입력 액션은 GameplayTag 요구 사항이 충족되면 입력이 눌릴 때 자동으로 GameplayAbility를 활성화합니다. 할당된 입력 액션은 입력에 반응하는 AbilityTask를 사용할 때 필수입니다.

GameplayAbility를 활성화하는 입력 액션 외에도 ASC확인(Confirm)취소(Cancel)와 같은 일반 입력도 수용합니다. 이러한 특수 입력은 Target Actors를 확인하거나 취소하는 AbilityTask에서 사용됩니다.

ASC에 입력을 바인딩하려면 먼저 입력 액션 이름을 byte로 변환하는 열거형(Enum)을 생성해야 합니다. 해당 열거형의 이름은 프로젝트 설정에서 사용된 입력 액션의 이름과 정확히 일치해야 합니다. 하지만 DisplayName은 중요하지 않습니다.

샘플 프로젝트에서의 예시:

UENUM(BlueprintType)
enum class EGDAbilityInputID : uint8
{
	// 0 None
	None			UMETA(DisplayName = "None"),
	// 1 Confirm
	Confirm			UMETA(DisplayName = "Confirm"),
	// 2 Cancel
	Cancel			UMETA(DisplayName = "Cancel"),
	// 3 LMB
	Ability1		UMETA(DisplayName = "Ability1"),
	// 4 RMB
	Ability2		UMETA(DisplayName = "Ability2"),
	// 5 Q
	Ability3		UMETA(DisplayName = "Ability3"),
	// 6 E
	Ability4		UMETA(DisplayName = "Ability4"),
	// 7 R
	Ability5		UMETA(DisplayName = "Ability5"),
	// 8 Sprint
	Sprint			UMETA(DisplayName = "Sprint"),
	// 9 Jump
	Jump			UMETA(DisplayName = "Jump")
};

ASC가 캐릭터에 존재하는 경우, SetupPlayerInputComponent()에서 ASC에 입력을 바인딩하는 함수를 포함시킵니다:

// Bind to AbilitySystemComponent
FTopLevelAssetPath AbilityEnumAssetPath = FTopLevelAssetPath(FName("/Script/GASDocumentation"), FName("EGDAbilityInputID"));
AbilitySystemComponent->BindAbilityActivationToInputComponent(PlayerInputComponent, FGameplayAbilityInputBinds(FString("ConfirmTarget"),
	FString("CancelTarget"), AbilityEnumAssetPath, static_cast<int32>(EGDAbilityInputID::Confirm), static_cast<int32>(EGDAbilityInputID::Cancel)));

ASCPlayerState에 존재하는 경우, SetupPlayerInputComponent() 내부에서 경쟁 조건(Race condition)이 발생할 수 있습니다. 이는 PlayerState가 클라이언트에 아직 리플리케이트되지 않았을 수 있기 때문입니다. 따라서 SetupPlayerInputComponent()OnRep_PlayerState() 모두에서 입력을 바인딩하도록 시도하는 것을 권장합니다.
OnRep_PlayerState()만으로는 충분하지 않은 이유는, PlayerState가 리플리케이트될 때 PlayerControllerClientRestart()를 호출해 InputComponent를 생성하기 전에 ActorInputComponent가 null일 수 있기 때문입니다. 샘플 프로젝트는 이 두 위치 모두에서 입력 바인딩을 시도하면서, 입력이 단 한 번만 바인딩되도록 불리언 변수를 사용해 과정을 제어하는 방법을 보여줍니다.

Note: 샘플 프로젝트에서 열거형(Enum)의 ConfirmCancel은 프로젝트 설정에 정의된 입력 액션 이름(ConfirmTargetCancelTarget)과 일치하지 않습니다. 그러나 BindAbilityActivationToInputComponent()에서 이 둘 사이의 매핑을 제공합니다. 이러한 입력은 특별하므로 이름이 일치할 필요는 없지만 일치시킬 수도 있습니다. 반면, 열거형에 포함된 다른 입력들은 프로젝트 설정의 입력 액션 이름과 반드시 일치해야 합니다.

하나의 입력으로만 활성화될 GameplayAbility(MOBA처럼 항상 같은 "슬롯"에 존재하는 어빌리티)의 경우, 저는 UGameplayAbility 서브클래스에 입력을 정의할 수 있는 변수를 추가하는 것을 선호합니다. 그런 다음 Ability를 부여할 때 ClassDefaultObject에서 이 값을 읽어 사용할 수 있습니다.

4.6.2.1 GameplayAbility를 활성화하지 않고 입력 바인딩

입력이 눌렸을 때 GameplayAbility가 자동으로 활성화되는 것을 원하지 않지만, AbilityTask에서 사용할 수 있도록 입력에 바인딩하고 싶다면, UGameplayAbility 서브클래스에 새로운 Boolean 변수 bActivateOnInput을 추가할 수 있습니다. 이 변수는 기본값을 true로 설정한 후, UAbilitySystemComponent::AbilityLocalInputPressed()를 오버라이드하면 됩니다.

void UGSAbilitySystemComponent::AbilityLocalInputPressed(int32 InputID)
{
	// 입력이 GenericConfirm/Cancel에 오버로드되어 있고
    // GenericConfirm/Cancel 콜백이 바인딩된 경우 입력을 소모합니다.
	if (IsGenericConfirmInputBound(InputID))
	{
		LocalInputConfirm();
		return;
	}

	if (IsGenericCancelInputBound(InputID))
	{
		LocalInputCancel();
		return;
	}

	// ---------------------------------------------------------

	ABILITYLIST_SCOPE_LOCK();
	for (FGameplayAbilitySpec& Spec : ActivatableAbilities.Items)
	{
		if (Spec.InputID == InputID)
		{
			if (Spec.Ability)
			{
				Spec.InputPressed = true;
				if (Spec.IsActive())
				{
					if (Spec.Ability->bReplicateInputDirectly && IsOwnerActorAuthoritative() == false)
					{
						ServerSetInputPressed(Spec.Handle);
					}

					AbilitySpecInputPressed(Spec);

					// InputPressed 이벤트를 호출합니다. 
                    // 여기서는 리플리케이트되지 않습니다.  
                    // 누군가 감지하고 있다면 InputPressed 이벤트를 
                    // 서버로 리플리케이트할 수 있습니다.
					InvokeReplicatedEvent(EAbilityGenericReplicatedEvent::InputPressed, Spec.Handle, Spec.ActivationInfo.GetActivationPredictionKey());
				}
				else
				{
					UGSGameplayAbility* GA = Cast<UGSGameplayAbility>(Spec.Ability);
					if (GA && GA->bActivateOnInput)
					{
						// Ability가 활성화되지 않았으므로, 이를 활성화하려 시도합니다.
						TryActivateAbility(Spec.Handle);
					}
				}
			}
		}
	}
}

⬆ 위로 가기

4.6.3 Granting Abilities(Ability 부여)

GameplayAbilityASC에 부여하면, 해당 Ability가 ASCActivatableAbilities 목록에 추가됩니다. 이를 통해 GameplayTag의 요구사항을 충족하면 원하는 대로 GameplayAbility를 활성화할 수 있습니다.

GameplayAbility는 서버에서 부여되며, 이후 GameplayAbilitySpec이 자동으로 소유 클라이언트에 복제됩니다. 다른 클라이언트나 Simulated Proxy는 해당 GameplayAbilitySpec을 받지 않습니다.

샘플 프로젝트에서는 Character 클래스에 TArray<TSubclassOf<UGDGameplayAbility>>를 저장하여 게임 시작 시 이를 읽고 Ability를 부여하는 방식으로 구현되어 있습니다:

void AGDCharacterBase::AddCharacterAbilities()
{
	// Ability 부여합니다. 단, 서버에서만 실행됩니다.	
	if (Role != ROLE_Authority || !AbilitySystemComponent.IsValid() || AbilitySystemComponent->bCharacterAbilitiesGiven)
	{
		return;
	}

	for (TSubclassOf<UGDGameplayAbility>& StartupAbility : CharacterAbilities)
	{
		AbilitySystemComponent->GiveAbility(
			FGameplayAbilitySpec(StartupAbility, GetAbilityLevel(StartupAbility.GetDefaultObject()->AbilityID), static_cast<int32>(StartupAbility.GetDefaultObject()->AbilityInputID), this));
	}

	AbilitySystemComponent->bCharacterAbilitiesGiven = true;
}

이처럼 GameplayAbilities를 부여할 때, UGameplayAbility 클래스, Ability 레벨, 바인딩된 입력, 그리고 해당 GameplayAbilityASC에 부여한 SourceObject(제공자)를 사용하여 GameplayAbilitySpec을 생성합니다.

⬆ 위로 가기

4.6.4 Ability 활성화

GameplayAbility에 입력 액션이 할당되면, 입력이 눌리고 GameplayTag 요구사항을 충족하면 자동으로 활성화됩니다. 하지만 이는 항상 원하는 방식으로 GameplayAbility를 활성화하는 방법은 아닐 수 있습니다. ASCGameplayTag 혹은 GameplayAbility 클래스나 GameplayAbilitySpec Handle 그리고 이벤트를 통한 총 네 가지 방법으로 GameplayAbility를 활성화할 수 있습니다. 이벤트를 통해 GameplayAbility를 활성화하면, 이벤트와 함께 데이터를 포함한 페이로드를 전달할 수 있습니다.

UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilitiesByTag(const FGameplayTagContainer& GameplayTagContainer, bool bAllowRemoteActivation = true);

UFUNCTION(BlueprintCallable, Category = "Abilities")
bool TryActivateAbilityByClass(TSubclassOf<UGameplayAbility> InAbilityToActivate, bool bAllowRemoteActivation = true);

bool TryActivateAbility(FGameplayAbilitySpecHandle AbilityToActivate, bool bAllowRemoteActivation = true);

bool TriggerAbilityFromGameplayEvent(FGameplayAbilitySpecHandle AbilityToTrigger, FGameplayAbilityActorInfo* ActorInfo, FGameplayTag Tag, const FGameplayEventData* Payload, UAbilitySystemComponent& Component);

FGameplayAbilitySpecHandle GiveAbilityAndActivateOnce(const FGameplayAbilitySpec& AbilitySpec, const FGameplayEventData* GameplayEventData);

이벤트로 GameplayAbility를 활성화하려면, 해당 GameplayAbilityTrigger를 설정해야 합니다. GameplayTag를 지정하고 GameplayEvent 옵션을 선택합니다. 이벤트를 전송하려면 UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload) 함수를 사용합니다. 이벤트를 통해 GameplayAbility를 활성화하면, 데이터를 포함한 페이로드를 전달할 수 있습니다.

또한, GameplayAbility Trigger를 사용하면 GameplayTag가 추가되거나 제거될 때도 GameplayAbility를 활성화할 수 있습니다.

Note: Blueprint에서 이벤트로 GameplayAbility를 활성화할 때는 반드시 ActivateAbilityFromEvent 노드를 사용해야 합니다.

Note: GameplayAbility를 종료해야 할 시점이 되면 반드시 EndAbility()를 호출해야 합니다. 단, 항상 실행되는 패시브 Ability 같은 경우에는 호출할 필요가 없습니다.

locally predicted GameplayAbility의 활성화 순서:
1. Owning clientTryActivateAbility()를 호출합니다.
1. InternalTryActivateAbility()를 호출합니다.
1. CanActivateAbility()를 호출하여 GameplayTag 요건 충족 여부, ASC가 Cost를 감당할 수 있는지, GameplayAbility가 Cooldown 상태가 아닌지, 현재 활성화된 다른 인스턴스가 없는지 반환합니다.
1. 클라이언트가 CallServerTryActivateAbility()를 호출하며, 생성된 Prediction key를 서버로 전달합니다.
1. CallActivateAbility()를 호출합니다.
1. PreActivate()를 호출합니다. (에픽 게임즈는 이를 boilerplate init stuff라고 부릅니다.)
1. ActivateAbility()를 호출하여 최종적으로 Ability를 활성화합니다.

ServerCallServerTryActivateAbility() 수신:
1. ServerTryActivateAbility()를 호출합니다.
1. InternalServerTryActivateAbility()을 호출합니다.
1. InternalTryActivateAbility()를 호출합니다.
1. CanActivateAbility()를 호출하여 GameplayTag 요건 충족 여부, ASC가 Cost를 감당할 수 있는지, GameplayAbility가 Cooldown 상태가 아닌지, 현재 활성화된 다른 인스턴스가 없는지 반환합니다.
1. 성공하면 ClientActivateAbilitySucceed()를 호출하여 클라이언트에게 활성화가 서버에 의해 확인되었음을 알리고, ActivationInfo를 업데이트하도록 지시합니다. 또한 OnConfirmDelegate 대리자를 브로드캐스트합니다. 이는 입력 확인과는 다릅니다.
1. CallActivateAbility()를 호출합니다.
1. PreActivate()를 호출합니다. (에픽 게임즈는 이를 boilerplate init stuff라고 부릅니다.)
1. ActivateAbility()를 호출하여 최종적으로 Ability를 활성화합니다.

서버가 활성화에 실패하면, ClientActivateAbilityFailed()를 호출하여 클라이언트의 GameplayAbility를 즉시 종료하고, 모든 예측된 변경 사항을 되돌립니다.

4.6.4.1 패시브 Ability

자동으로 활성화되어 지속적으로 실행되는 패시브 GameplayAbility를 구현하려면, UGameplayAbility::OnAvatarSet()을 재정의해야 합니다. 해당 함수는 GameplayAbility가 부여되고 AvatarActor가 설정될 때 자동으로 호출됩니다. 이후 TryActivateAbility()를 호출하여 Ability를 활성화할 수 있습니다.

또한, 커스텀 UGameplayAbility 클래스에 GameplayAbility가 부여될 때 자동으로 활성화해야 하는지 여부를 나타내는 bool 변수를 추가하는 것이 좋습니다. 샘플 프로젝트에서는 방어구 중첩 패시브 Ability에 이 방식을 적용하고 있습니다.

패시브 GameplayAbility는 일반적으로 Net Execution PolicyServer Only로 설정됩니다.

void UGDGameplayAbility::OnAvatarSet(const FGameplayAbilityActorInfo * ActorInfo, const FGameplayAbilitySpec & Spec)
{
	Super::OnAvatarSet(ActorInfo, Spec);

	if (bActivateAbilityOnGranted)
	{
		ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle, false);
	}
}

에픽 게임즈는 해당 함수가 패시브 Ability를 초기화하고 BeginPlay와 같은 작업을 수행하기에 적합한 위치라고 설명합니다.

⬆ 위로 가기

4.6.4.2 Activation Failed Tags

Ability에는 Ability 활성화 실패 이유를 알려주는 기본 로직이 있습니다. 이를 활성화하려면 기본 실패 케이스에 해당하는 GameplayTag를 설정해야 합니다.

다음 태그(또는 자신만의 명명 규칙)를 프로젝트에 추가하세요:

+GameplayTagList=(Tag="Activation.Fail.BlockedByTags",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.CantAffordCost",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.IsDead",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.MissingTags",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.Networking",DevComment="")
+GameplayTagList=(Tag="Activation.Fail.OnCooldown",DevComment="")

그 다음, 이 태그들을 GASDocumentation\Config\DefaultGame.ini에 추가하세요:

[/Script/GameplayAbilities.AbilitySystemGlobals]
ActivateFailIsDeadName=Activation.Fail.IsDead
ActivateFailCooldownName=Activation.Fail.OnCooldown
ActivateFailCostName=Activation.Fail.CantAffordCost
ActivateFailTagsBlockedName=Activation.Fail.BlockedByTags
ActivateFailTagsMissingName=Activation.Fail.MissingTags
ActivateFailNetworkingName=Activation.Fail.Networking

이제 Ability 활성화가 실패할 때마다, 해당 GameplayTag가 출력 로그 메시지에 포함되거나 showdebug AbilitySystem HUD에서 표시됩니다.

LogAbilitySystem: Display: InternalServerTryActivateAbility. Rejecting ClientActivation of Default__GA_FireGun_C. InternalTryActivateAbility failed: Activation.Fail.BlockedByTags
LogAbilitySystem: Display: ClientActivateAbilityFailed_Implementation. PredictionKey :109 Ability: Default__GA_FireGun_C

Activation Failed Tags Displayed in showdebug AbilitySystem

⬆ 위로 가기

GameplayAbility를 내부에서 취소하려면 CancelAbility()를 호출합니다. 이 함수는 EndAbility()를 호출하고, 그 파라미터 중 WasCancelled를 true로 설정합니다.

외부에서 GameplayAbility를 취소하려면, ASC는 몇 가지 함수를 제공합니다:

/** 지정된 Ability CDO를 취소합니다. */
void CancelAbility(UGameplayAbility* Ability);	

/** 전달된 Spec Handle로 표시된 Ability를 취소합니다. Handle이 재활성화된 Ability 목록에 없으면 아무 일도 일어나지 않습니다. */
void CancelAbilityHandle(const FGameplayAbilitySpecHandle& AbilityHandle);

/** 지정된 태그로 모든 Ability를 취소합니다. Ignore 인스턴스는 취소하지 않습니다. */
void CancelAbilities(const FGameplayTagContainer* WithTags=nullptr, const FGameplayTagContainer* WithoutTags=nullptr, UGameplayAbility* Ignore=nullptr);

/** 태그와 관계없이 모든 Ability를 취소합니다. Ignore 인스턴스는 취소하지 않습니다. */
void CancelAllAbilities(UGameplayAbility* Ignore=nullptr);

/** 모든 Ability를 취소하고 남아있는 인스턴스된 Ability를 종료합니다. */
virtual void DestroyActiveState();

Note: CancelAllAbilitiesNon-Instanced GameplayAbility가 있을 경우 제대로 작동하지 않는 것 같습니다. Non-Instanced GameplayAbility를 처리하고 멈추는 경우가 발생하는 것 같습니다. CancelAbilitiesNon-Instanced GameplayAbility를 더 잘 처리할 수 있으며, 이는 샘플 프로젝트에서 사용되는 방식입니다 (Jump는 Non-Instanced GameplayAbility입니다). 결과는 환경에 따라 달라질 수 있습니다.

⬆ 위로 가기

4.6.6 활성화된 Ability 얻기

초보자들은 종종 활성화된 Ability를 어떻게 얻을 수 있나요?라고 묻습니다. 이는 Ability의 변수 값을 설정하거나 Ability를 취소하기 위해서일 수 있습니다. 한 번에 여러 개의 GameplayAbility가 활성화될 수 있기 때문에, 단 하나의 활성화된 Ability는 존재하지 않습니다. 대신, ASCActivatableAbilities(ASC가 소유한 부여된 GameplayAbility) 목록을 검색하여 원하는 Asset또는 부여된 GameplayTag와 일치하는 Ability를 찾아야 합니다.

UAbilitySystemComponent::GetActivatableAbilities()는 순회할 수 있는 TArray<FGameplayAbilitySpec>를 반환합니다.

ASC는 또한 GameplayTagContainer를 매개변수로 받아 GameplayAbilitySpecs 목록을 직접 순회하는 대신 검색을 도와주는 다른 헬퍼 함수를 제공합니다. bOnlyAbilitiesThatSatisfyTagRequirements 파라미터는 GameplayTag 요구사항을 충족하고 지금 당장 활성화될 수 있는 GameplayAbilitySpec만 반환합니다. 예를 들어, 무기를 가진 기본 공격 능력과 맨손 기본 공격 GameplayAbility가 있을 경우, 무기가 장착되어 있는지에 따라 해당 GameplayTag 요구 사항을 설정하고 올바른 능력이 활성화됩니다. 이 함수에 대한 에픽 게임즈의 주석에서 더 많은 정보를 확인할 수 있습니다.

UAbilitySystemComponent::GetActivatableGameplayAbilitySpecsByAllMatchingTags(const FGameplayTagContainer& GameplayTagContainer, TArray < struct FGameplayAbilitySpec* >& MatchingGameplayAbilities, bool bOnlyAbilitiesThatSatisfyTagRequirements = true)

원하는 FGameplayAbilitySpec을 찾았다면, 그 위에서 IsActive()를 호출할 수 있습니다.

⬆ 위로 가기

4.6.7 Instancing Policy

GameplayAbilityInstancing Policy는 Ability가 활성화될 때 어떻게 인스턴스화되는지를 결정합니다.

Instancing Policy설명사용 예시
Instanced Per ActorASC는 활성화 간에 재사용되는 하나의 GameplayAbility 인스턴스를 가집니다.가장 자주 사용되는 Instancing Policy입니다. 모든 Ability에 사용할 수 있으며, 활성화 간에 지속성을 제공합니다. 디자이너는 필요시 변수들을 수동으로 리셋해야 합니다.
Instanced Per ExecutionGameplayAbility가 활성화될 때마다 새로운 인스턴스가 생성됩니다.변수들이 매번 리셋되므로 해당 GameplayAbility는 활성화할 때마다 새로 생성됩니다. 성능은 Instanced Per Actor보다 나쁘지만, 변수 리셋이 필요할 때 유용합니다. 샘플 프로젝트에서는 이 방식을 사용하지 않습니다.
Non-InstancedGameplayAbilityClassDefaultObject에서 작동하며 인스턴스가 생성되지 않습니다.성능이 가장 좋지만 기능적으로 제한적입니다. Non-Instanced GameplayAbility는 상태를 저장할 수 없고, 동적 변수를 사용할 수 없으며, AbilityTask 델리게이트와 바인딩할 수 없습니다. 주로 MOBA나 RTS에서 자주 사용되는 간단한 Ability(예: 미니언 기본 공격)에 적합합니다. 샘플 프로젝트의 Jump GameplayAbilityNon-Instanced입니다.

⬆ 위로 가기

4.6.8 Net Execution Policy

GameplayAbilityNet Execution PolicyGameplayAbility를 누가 실행하는지와 그 실행 순서를 결정합니다.

Net Execution Policy설명
Local OnlyGameplayAbility는 소유한 클라이언트에서만 실행됩니다. 로컬에서만 시각적 효과를 변경하는 Ability에 유용할 수 있습니다. 싱글 플레이어 게임에서는 Server Only를 사용해야 합니다.
Local PredictedLocal Predicted GameplayAbility는 먼저 소유한 클라이언트에서 활성화되고, 그 후 서버에서 실행됩니다. 서버는 클라이언트가 예측한 내용을 수정합니다. 예측에 대한 자세한 내용은 Prediction을 참조해주세요.
Server OnlyGameplayAbility는 오직 서버에서만 실행됩니다. Passive GameplayAbility는 보통 Server Only입니다. 싱글 플레이어 게임에서는 이 방식을 사용해야 합니다.
Server InitiatedServer Initiated GameplayAbility는 먼저 서버에서 활성화되고, 그 후 소유한 클라이언트에서 실행됩니다. 개인적으로는 이 방법은 많이 사용하지 않았습니다.

⬆ 위로 가기

4.6.9 Ability Tags

GameplayAbility는 내장된 로직을 가진 GameplayTagContainer와 함께 제공됩니다. 이 GameplayTag는 복제되지 않습니다.

GameplayTag Container설명
Ability TagsGameplayAbility가 소유한 GameplayTag입니다. 이들은 GameplayAbility를 설명하는 데 사용됩니다.
Cancel Abilities with Tag해당 GameplayAbility가 활성화될 때, 해당 Ability Tag에 포함된 GameplayTag를 가진 다른 GameplayAbility는 취소됩니다.
Block Abilities with Tag해당 GameplayAbility가 활성화되는 동안, 해당 Ability Tag에 포함된 GameplayTag를 가진 다른 GameplayAbility는 활성화될 수 없습니다.
Activation Owned Tags해당 GameplayAbility가 활성화되는 동안 소유자에게 주어지는 GameplayTags입니다. 단, 이들은 리플리케이되지 않습니다.
Activation Required Tags해당 GameplayAbility는 소유자가 모든 해당 GameplayTag를 가지고 있어야만 활성화될 수 있습니다.
Activation Blocked Tags해당 GameplayAbility는 소유자가 어떤 해당 GameplayTag를 가진 경우 활성화될 수 없습니다.
Source Required Tags해당 GameplayAbility는 소스가 모든 해당 GameplayTag를 가지고 있어야만 활성화될 수 있습니다. SourceGameplayTagGameplayAbility가 이벤트로 트리거될 때만 설정됩니다.
Source Blocked Tags해당 GameplayAbility는 소스가 어떤 해당 GameplayTag를 가진 경우 활성화될 수 없습니다. SourceGameplayTagGameplayAbility가 이벤트로 트리거될 때만 설정됩니다.
Target Required Tags해당 GameplayAbility는 타겟이 모든 해당 GameplayTag를 가지고 있어야만 활성화될 수 있습니다. TargetGameplayTagGameplayAbility가 이벤트로 트리거될 때만 설정됩니다.
Target Blocked Tags해당 GameplayAbility는 타겟이 어떤 해당 GameplayTag를 가진 경우 활성화될 수 없습니다. TargetGameplayTagGameplayAbility가 이벤트로 트리거될 때만 설정됩니다.

⬆ 위로 가기

4.6.10 Gameplay Ability Spec

GameplayAbilitySpecGameplayAbility가 부여된 후 ASC에 존재하며, 활성화 가능한 GameplayAbility - GameplayAbility 클래스, 레벨, 입력 바인딩, 그리고 GameplayAbility 클래스와 분리하여 유지해야 하는 런타임 상태를 정의합니다.

GameplayAbility가 서버에서 부여되면, 서버는 GameplayAbilitySpec을 소유하는 클라이언트에게 복제하여 해당 클라이언트가 이를 활성화할 수 있도록 합니다.

GameplayAbilitySpec을 활성화하면, Instancing Policy에 따라 GameplayAbility의 인스턴스를 생성합니다.(Non-Instanced GameplayAbility는 생성하지 않음)

⬆ 위로 가기

4.6.11 Ability에 데이터 전달하기

GameplayAbility의 일반적인 패러다임은 Activate->Generate Data->Apply->End입니다. 때때로 기존 데이터를 처리해야 할 때가 있습니다. GAS는 외부 데이터를 GameplayAbility에 전달하는 몇 가지 방법을 제공합니다:

Method설명
Activate GameplayAbility by Event이벤트를 사용하여 데이터 페이로드가 포함된 GameplayAbility를 활성화합니다. 이벤트의 페이로드는 로컬 예측(Local Predicted)된 GameplayAbility의 경우 클라이언트에서 서버로 복제됩니다. Optional Object 또는 TargetData 변수는 기존 변수에 맞지 않는 임의의 데이터를 위한 변수로 사용됩니다. 단점은 입력 바인드를 통해 Ability를 활성화할 수 없다는 점입니다. GameplayAbility를 이벤트로 활성화하려면, GameplayAbility에서 Trigger를 설정해야 합니다. GameplayTag를 할당하고 GameplayEvent 옵션을 선택합니다. 이벤트를 보내려면, 함수 UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(AActor* Actor, FGameplayTag EventTag, FGameplayEventData Payload)를 사용합니다.
Use WaitGameplayEvent AbilityTaskWaitGameplayEvent AbilityTask를 사용하여 GameplayAbility가 활성화된 후 이벤트를 기다리도록 할 수 있습니다. 이벤트 페이로드와 이를 보내는 과정은 GameplayAbility를 이벤트로 활성화하는 것과 동일합니다. 단점은 AbilityTask에 의해 이벤트가 복제되지 않으므로, Local OnlyServer Only GameplayAbilities에서만 사용해야 한다는 점입니다. 이벤트 페이로드를 복제하는 자체 AbilityTask를 작성할 수 있는 가능성도 있습니다.
Use TargetData커스텀 TargetData 구조체는 클라이언트와 서버 간에 임의의 데이터를 전달하는 좋은 방법입니다.
Store Data on the OwnerActor or AvatarActor소유자(OwnerActor), 아바타(AvatarActor), 또는 참조를 얻을 수 있는 다른 객체에 저장된 리플리케이트된 변수를 사용하십시오. 해당 방법은 가장 유연하며 입력 바인딩으로 활성화된 GameplayAbility와 함께 작동합니다. 그러나 해당 방법은 데이터가 사용할 때 리플리케이트를 통해 동기화될 것인지를 보장하지 않습니다. 이를 미리 보장해야 합니다. 즉, 리플리케이트된 변수를 설정한 후 바로 GameplayAbility를 활성화하면 패킷 손실로 인해 수신자에서 발생하는 순서를 보장할 수 없습니다.

⬆ 위로 가기

4.6.12 Ability Cost and Cooldown

GameplayAbility는 선택적 Cost와 Cooldown 기능을 제공합니다. Cost는 ASCGameplayAbility를 활성화하기 위해 가져야 하는 미리 정의된 Attribute 값이며, 이는 Instant GameplayEffect (Cost GE)로 구현됩니다. Cooldown은 GameplayAbility가 만료될 때까지 재활성화를 방지하는 타이머로, Duration GameplayEffect (Cooldown GE)로 구현됩니다.

GameplayAbilityUGameplayAbility::Activate()를 호출하기 전에, 먼저 UGameplayAbility::CanActivateAbility()를 호출합니다. 이 함수는 소유한 ASC가 Cost를 감당할 수 있는지 확인(UGameplayAbility::CheckCost())하고, GameplayAbility가 Cooldown 상태가 아닌지 확인(UGameplayAbility::CheckCooldown())합니다.

GameplayAbilityActivate()를 호출한 후에는 언제든지 UGameplayAbility::CommitAbility()를 사용하여 Cost와 Cooldown을 커밋할 수 있습니다. 해당 함수는 UGameplayAbility::CommitCost()UGameplayAbility::CommitCooldown()을 호출합니다. 디자이너는 Cost와 Cooldown이 동시에 커밋되지 않아야 한다면 이를 별도로 호출할 수 있습니다. Cost와 Cooldown을 커밋하는 것은 CheckCost()CheckCooldown()을 다시 한 번 호출하며, 이는 해당 항목과 관련하여 GameplayAbility가 실패할 수 있는 마지막 기회입니다. GameplayAbility가 활성화된 후 소유한 ASCAttribute가 변경될 수 있으므로, 커밋 시점에서 Cost를 충족하지 못할 수 있습니다. Cost와 Cooldown을 커밋하는 것은prediction key가 유효한 경우 locally predicted가 가능합니다.

구현에 대한 자세한 내용은 CostGECooldownGE를 참조하세요.

⬆ 위로 가기

4.6.13 Leveling Up Abilities

Ability를 레벨 업하는 두 가지 일반적인 방법이 있습니다:

레벨업 방법설명
Ungrant and Regrant at the New LevelASC에서 GameplayAbility를 제거(Ungrant)한 후, 서버에서 다음 레벨로 다시 부여(Regrant)합니다. 이때 활성화 상태의 GameplayAbility는 종료됩니다.
Increase the GameplayAbilitySpec's Level서버에서 해당 GameplayAbilitySpec을 찾아 레벨을 증가시키고, 이를 Dirty로 표시하여 소유 클라이언트로 리플리케이트되도록 합니다. 활성화 상태의 GameplayAbility는 종료하지 않습니다.

위 두 방법의 주요 차이는 레벨 업 시 활성 상태인 GameplayAbility를 취소할지 여부입니다. 사용하는 GameplayAbilities에 따라 두 가지 방법을 모두 활용해야 할 가능성이 높습니다. 이를 위해 UGameplayAbility 서브클래스에 bool 변수를 추가하여 어느 방법을 사용할지 지정하는 것을 추천합니다.

⬆ 위로 가기

4.6.14 Ability Sets

GameplayAbilitySetGameplayAbility를 부여하는 로직이 있는 캐릭터의 시작 GameplayAbility의 입력 바인딩과 목록을 보관하기 위한 편의성 UDataAsset 클래스입니다. 서브클래스에는 추가 로직이나 프로퍼티를 포함할 수도 있습니다. 파라곤에는 영웅마다 주어진 모든 GameplayAbility를 포함하는 GameplayAbilitySet이 있었습니다.
이 클래스는 적어도 지금까지 살펴본 바에 따르면 불필요한 클래스입니다. 샘플 프로젝트는 GDCharacterBase와 그 서브클래스 내부에서 GameplayAbilitySet의 모든 기능을 처리합니다.

⬆ 위로 가기

4.6.15 Ability Batching

기존 GameplayAbility의 수명 주기에는 클라이언트에서 서버까지 최소 2~3개의 RPC(Remote Procedure Call)가 포함됩니다.

  1. CallServerTryActivateAbility()
  2. ServerSetReplicatedTargetData() (선택 사항)
  3. ServerEndAbility()

Gameplay Ability가 이 모든 작업을 한 프레임 내에서 원자적 그룹으로 수행하는 경우, 이 워크플로를 최적화하여 모든 두세 개의 RPC를 하나로 Batch(결합)할 수 있습니다. GAS에서는 이 RPC 최적화를 Ability Batching이라고 부릅니다.
대표적인 예로 히트스캔(instant hit) 총을 들 수 있습니다. 히트스캔 총은 활성화, 라인 트레이스, TargetData를 서버에 전송, Ability 종료를 한 프레임 내의 원자적 그룹으로 처리합니다. GASShooter 샘플 프로젝트는 히트스캔 총에 이 기술을 활용하는 방법을 보여줍니다.

반자동 총기는 CallServerTryActivateAbility(), ServerSetReplicatedTargetData() (총알 명중 데이터), ServerEndAbility()를 하나의 RPC로 배치하여 세 개의 RPC를 하나로 줄이는 최상의 시나리오입니다.

자동/연발 총기는 첫 번째 총알의 CallServerTryActivateAbility()ServerSetReplicatedTargetData()를 하나의 RPC로 배칭합니다. 이후의 각 총알은 ServerSetReplicatedTargetData()가 별도의 RPC로 전송됩니다. 마지막으로, 총이 사격을 멈출 경우 ServerEndAbility()가 별도의 RPC로 전송됩니다. 이는 첫 번째 총알 발사 시에만 두 개의 RPC를 배칭해 하나로 줄이고 이후에는 더 이상 최적화할 수 없는 최악의 시나리오입니다. 이 시나리오는 Gameplay Event를 통해 Ability를 활성화하고 TargetDataEventPayload에 포함하여 클라이언트에서 서버로 전송하는 방식으로도 구현할 수 있습니다. 그러나 이 방식의 단점은 TargetData를 Ability 외부에서 생성해야 한다는 점이며, 반면 Batching 접근법은 Ability 내부에서 TargetData를 생성합니다.

Ability Batching은 기본적으로 ASC에서 비활성화되어 있습니다.
이를 활성화하려면, ShouldDoServerAbilityRPCBatch()를 재정의하여 true를 반환하도록 설정합니다.

virtual bool ShouldDoServerAbilityRPCBatch() const override { return true; }

이제 Ability Batching이 활성화되었으므로, 배칭하려는 Ability를 활성화하기 전에 FScopedServerAbilityRPCBatcher 구조체를 생성해야 합니다. 이 특별한 구조체는 범위 내의 모든 발생하는 모든 Ability를 배칭하려고 시도합니다. FScopedServerAbilityRPCBatcher가 범위를 벗어나면 이후에 할성화되는 Ability들은 더 이상 배칭을 시도하지 않습니다.
FScopedServerAbilityRPCBatcher는 배칭이 가능한 각 함수에 있는 특수 코드를 사용하여 RPC 호출을 가로채고, 해당 메시지를 Barch 구조체에 대신 패킹합니다. 그리고 FScopedServerAbilityRPCBatcher가 범위를 벗어나면, 자동으로 해당 Batch 구조체를 서버에 RPC로 전송합니다. 이 작업은 UAbilitySystemComponent::EndServerAbilityRPCBatch()에서 이루어집니다. 서버는 이 Batch RPC를 UAbilitySystemComponent::ServerAbilityRPCBatch_Internal(FServerAbilityRPCBatch& BatchInfo) 함수에서 수신합니다. BatchInfo 매개변수에는 Ability 종료해야 하는지 여부에 대한 flag, 활성화 시 입력 여부에 대한 flag, TargetData가 포함되어 있는 경우 TargetData가 포함됩니다. 해당 함수는 배칭이 올바르게 작동하는지 확인하기 위해 중단점을 설정하기 적합한 곳입니다. 또는, cvar AbilitySystem.ServerRPCBatching.Log 1을 사용하여 특수 Ability Batching 로그를 활성화할 수도 있습니다.

이 메커니즘은 C++에서만 가능하며, FGameplayAbilitySpecHandle을 통해서만 Ability를 활성화할 수 있습니다.

bool UGSAbilitySystemComponent::BatchRPCTryActivateAbility(FGameplayAbilitySpecHandle InAbilityHandle, bool EndAbilityImmediately)
{
	bool AbilityActivated = false;
	if (InAbilityHandle.IsValid())
	{
		FScopedServerAbilityRPCBatcher GSAbilityRPCBatcher(this, InAbilityHandle);
		AbilityActivated = TryActivateAbility(InAbilityHandle, true);

		if (EndAbilityImmediately)
		{
			FGameplayAbilitySpec* AbilitySpec = FindAbilitySpecFromHandle(InAbilityHandle);
			if (AbilitySpec)
			{
				UGSGameplayAbility* GSAbility = Cast<UGSGameplayAbility>(AbilitySpec->GetPrimaryInstance());
				GSAbility->ExternalEndAbility();
			}
		}

		return AbilityActivated;
	}

	return AbilityActivated;
}

GASShooter는 반자동 및 자동 총기에 대해 배칭된 GameplayAbility를 동일하게 사용하며, EndAbility()는 Ability 내에서 직접 호출되지 않습니다. 대신, EndAbility()는 플레이어 입력을 관리하고 현재 발사 모드에 따라 배치된 Ability 호출을 처리하는 로컬 전용 Ability에서 처리됩니다. 모든 RPC가 FScopedServerAbilityRPCBatcher 범위 내에서 발생해야 하므로, EndAbilityImmediately 파라미터를 제공하여 로컬 전용 Ability가 해당 Ability가 EndAbility() 호출을 배칭해야 하는지(반자동), 아니면 배칭하지 않아야 하는지(자동) 지정할 수 있게 합니다. EndAbility() 호출은 나중에 별도의 RPC로 발생하게 됩니다.

GASShooter는 배칭된 Ability를 트리거하는 로컬 전용 Ability에서 사용하는 Blueprint 노드를 노출하여 Ability 배칭을 허용합니다.

Activate Batched Ability

⬆ 위로 가기

4.6.16 Net Security Policy

GameplayAbilityNetSecurityPolicy는 Ability가 네트워크에서 실행될 위치를 결정하며, 제한된 Ability를 실행하려는 클라이언트로부터 보호합니다.

Net Security Policy설명
ClientOrServer보안 요구 사항이 없습니다. 클라이언트와 서버 모두 자유롭게 Ability를 실행하고 종료할 수 있습니다.
ServerOnlyExecution클라이언트가 Ability의 실행을 요청하면 서버에서 이를 무시합니다. 클라이언트는 여전히 서버에게 Ability를 취소하거나 종료하도록 요청할 수 있습니다.
ServerOnlyTermination클라이언트가 Ability의 취소나 종료를 요청하면 서버에서 이를 무시합니다. 클라이언트는 여전히 Ability의 실행을 요청할 수 있습니다.
ServerOnly서버가 Ability의 실행과 종료를 모두 제어합니다. 클라이언트가 어떤 요청을 하더라도 무시됩니다.

⬆ 위로 가기

4.7 Ability Tasks

4.7.1 Ability Task 정의

GameplayAbility는 한 프레임에서만 실행됩니다. 이로 인해 유연성이 제한됩니다. 시간이 지남에 따라 발생하는 작업이나 특정 시점에 호출되는 델리게이트에 반응해야 하는 작업을 수행하기 위해 우리는 AbilityTask라는 지연 작업을 사용합니다.

GAS는 기본적으로 여러 종류의 AbilityTask를 제공합니다:

  • RootMotionSource로 캐릭터 이동을 위한 작업
  • 애니메이션 몽타주를 재생하는 작업
  • Attribute 변경에 반응하는 작업
  • GameplayEffect 변경에 반응하는 작업
  • 플레이어 입력에 반응하는 작업
  • 그 외의 작업들

UAbilityTask 생성자는 게임 전역에서 동시에 실행할 수 있는 최대 1000개의 AbilityTask만을 허용합니다. 이는 수백 명의 캐릭터가 동시에 존재하는 게임(예: RTS 게임)에서 GameplayAbility를 설계할 때 유의해야 합니다.

⬆ 위로 가기

4.7.2 커스텀 AbilityTask

여러분은 종종 자신만의 커스텀 AbilityTask(C++)를 만들게 될 것입니다.

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

  1. PlayMontageAndWaitForEvent: 기본 PlayMontageAndWaitWaitGameplayEvent AbilityTask를 결합한 것입니다. 이 AbilityTask는 애니메이션 몽타주가 AnimNotify에서 발생한 GameplayEvent를 GameplayAbility로 다시 전달하도록 합니다. 애니메이션 몽타주 중 특정 시점에 행동을 트리거하는 데 사용합니다.
  2. WaitReceiveDamage: 해당 AbilityTaskOwnerActor가 피해를 받을 때를 감지합니다. 패시브 갑옷 스택 능력은 영웅이 피해를 입을 때마다 갑옷 스택을 제거합니다.

AbilityTask는 다음과 같은 구성 요소로 이루어집니다:

  • AbilityTask의 새 인스턴스를 생성하는 정적 함수
  • AbilityTask가 완료되었을 때 방송되는 델리게이트
  • 주요 작업을 시작하고 외부 델리게이트에 바인딩하는 Activate() 함수
  • 외부 델리게이트와의 바인딩을 해제하는 등 정리를 위한 OnDestroy() 함수
  • 바인딩된 외부 델리게이트에 대한 콜백 함수
  • 멤버 변수와 내부 헬퍼 함수들

Note: AbilityTask는 한 가지 유형의 출력 델리게이트만 선언할 수 있습니다. 매개변수 사용 여부에 관계없이 모든 출력 델리게이트는 이 유형이어야 합니다. 사용하지 않는 델리게이트 매개변수에는 기본값을 전달해야 합니다.

AbilityTask는 해당 GameplayAbility를 실행하는 클라이언트나 서버에서만 실행됩니다. 하지만 AbilityTaskbSimulatedTask = true;AbilityTask 생성자에 설정하고, virtual void 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 WaitTargetData AbilityTask

Blueprint에서 AbilityTask를 수동으로 취소하려면, AbilityTask 객체(Async Task Proxy)에서 EndTask()를 호출하거나 C++에서 동일하게 호출하면 됩니다.

⬆ 위로 가기

4.7.4 Root Motion Source Ability Tasks

GAS에는 CharacterMovementComponent에 연결된 Root Motion Source를 사용하여 넉백, 복잡한 점프, 당기기, 돌진 등 시간 경과에 따라 Character를 움직일 수 있는 AbilityTask가 포함되어 있습니다.

Note: RootMotionSource AbilityTask의 예측은 엔진 버전 4.19 및 4.25 이상에서는 정상 작동합니다. 하지만 엔진 4.20~4.24 버전에서는 예측에 버그가 있어, 멀티플레이어에서 네트워크 수정이 필요하며 싱글 플레이에서는 완벽하게 작동합니다. 4.25의 prediction fix 사항을 4.20-4.24 버전의 엔진에 적용하는 것도 가능합니다.

⬆ 위로 가기

4.8 Gameplay Cues

4.8.1 Gameplay Cue 정의

GameplayCue(GC)는 게임플레이와 관련되지 않은 작업들을 실행하는데 사용됩니다. 예를 들어, 사운드 효과, 파티클 효과, 카메라 흔들기 등입니다. GameplayCue는 일반적으로 리플리케이트 되어 실행되며(명시적으로 로컬에서만 실행, 추가 또는 제거되지 않는 한) 예측됩니다.

GameplayCue반드시 GameplayCue라는 부모 GameplayTag와 이벤트 유형(Execute, Add, Remove)을 함께 ASC를 통해 GameplayCueManager로 보내어 트리거됩니다. GameplayCueNotify 객체와 IGameplayCueInterface를 구현한 다른 액터들은 GameplayCueGameplayTag(GameplayCueTag)에 따라 이 이벤트를 구독할 수 있습니다.

Note: 다시 한 번 말씀드리자면, GameplayCueGameplayTag는 반드시 GameplayCue라는 부모 GameplayTag로 시작해야 합니다. 예를 들어, 유효한 GameplayCue GameplayTagGameplayCue.A.B.C와 같이 생성됩니다.

GameplayCueNotify에는 StaticActor라는 두 가지 종류가 있습니다. 각각은 서로 다른 이벤트에 응답하고, 다른 유형의 GameplayEffect가 이들을 트리거할 수 있습니다. 해당 이벤트를 오버라이드하여 필요한 로직을 구현하면 됩니다.

GameplayCue 클래스이벤트GameplayEffect 타입설명
GameplayCueNotify_StaticExecuteInstant or PeriodicStatic GameplayCueNotifyClassDefaultObject에서 작동하며(인스턴스가 없음을 의미) 타격 임팩트와 같은 일회성 효과에 적합합니다.
GameplayCueNotify_ActorAdd or RemoveDuration or InfiniteActor GameplayCueNotify가 추가되면 새 인스턴스를 스폰합니다. 인스턴스화되어 있기 때문에 제거될 때까지 계속 동작을 할 수 있습니다. backing Duration 또는 Infinite GameplayEffect가 제거되거나 수동으로 remove를 호출하면 제거되는 사운드 및 파티클 이펙트를 루핑하는 데 좋습니다. 또한 동시에 추가할 수 있는 개수를 관리할 수 있는 옵션도 제공되므로 동일한 효과를 여러 번 적용할 때 사운드나 파티클이 한 번만 시작되도록 할 수 있습니다.

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

Note: GameplayCueNotify_Actor를 사용할 때, Auto Destroy on Remove를 체크하지 않으면 이후 동일한 GameplayCueTag에 대한 Add 호출이 작동하지 않을 수 있습니다.

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

샘플 프로젝트에는 스턴과 스프린트 효과를 위한 GameplayCueNotify_Actor와 FireGun의 발사체 충돌을 위한 GameplayCueNotify_Static이 포함되어 있습니다. 이러한 GCGE를 통해 리플리케이트하는 대신 로컬에서 트리거하여 최적화할 수 있습니다. 샘플 프로젝트에서는 초보자에게 적합한 방법으로 이를 보여주기로 했습니다.

⬆ 위로 가기

4.8.2 Triggering Gameplay Cues

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

GameplayCue Triggered from a GameplayEffect

UGameplayAbilityGameplayCueExecute, Add 또는 Remove하는 블루프린트 노드를 제공합니다.

GameplayCue Triggered from a GameplayAbility

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

/** GameplayCue는 독립적으로 올 수 있습니다. 이들은 EffectContext를 전달하여 히트 결과 등을 처리할 수 있습니다. */
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void ExecuteGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** persistent GameplayCue를 추가합니다. */
void AddGameplayCue(const FGameplayTag GameplayCueTag, FGameplayEffectContextHandle EffectContext = FGameplayEffectContextHandle());
void AddGameplayCue(const FGameplayTag GameplayCueTag, const FGameplayCueParameters& GameplayCueParameters);

/** persistent GameplayCue를 제거합니다. */
void RemoveGameplayCue(const FGameplayTag GameplayCueTag);
	
/** 자체적으로 추가된 GameplayCue를 제거합니다. 즉, GameplayEffect의 일부로 추가되지 않은 경우입니다. */
void RemoveAllGameplayCues();

⬆ 위로 가기

4.8.3 Local Gameplay Cues

GameplayAbilityASC에서 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 Gameplay Cue Parameters

GameplayCueFGameplayCueParameters 구조체를 받아 해당 GameplayCue에 대한 추가 정보를 전달합니다. 만약 GameplayCueGameplayAbilityASC의 함수에서 수동으로 트리거된다면, GameplayCue에 전달되는 FGameplayCueParameters 구조체를 수동으로 채워야 합니다. 만약 GameplayCueGameplayEffect에 의해 트리거된다면, 다음과 같은 변수들이 FGameplayCueParameters 구조체에 자동으로 채워집니다:

  • AggregatedSourceTags
  • AggregatedTargetTags
  • GameplayEffectLevel
  • AbilityLevel
  • EffectContext
  • Magnitude (만약 GameplayEffectGameplayCue tag container의 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 Gameplay Cue Manager

기본적으로 GameplayCueManager는 게임 디렉토리 전체를 스캔하여 GameplayCueNotify를 찾고, 게임 실행 시 이를 메모리에 로드합니다. 이 경로를 변경하려면 DefaultGame.ini에서 GameplayCueManager가 스캔하는 경로를 설정할 수 있습니다.

[/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에 연결된 피격 임팩트를 재생하지 않거나 대신 커스텀 피격 임팩트를 재생하고 싶을 수 있습니다. 이 경우 GameplayEffectExecutionCalculations 내에서 OutExecutionOutput.MarkGameplayCuesHandledManually()를 호출하고, 이후 ASCTarget이나 Source에 수동으로 GameplayCue 이벤트를 전송할 수 있습니다.

특정 ASC에서 GameplayCue가 전혀 실행되지 않게 하려면, AbilitySystemComponent->bSuppressGameplayCues = true;로 설정할 수 있습니다.

⬆ 위로 가기

4.8.7 Gameplay Cue Batching(일괄 처리)

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

4.8.7.1 수동 RPC

예를 들어, 샷건이 8개의 총알을 발사한다고 가정해 보겠습니다. 그러면 8개의 트레이스와 임팩트 GameplayCue가 발생합니다. GASShooter는 이를 하나의 RPC로 합치는 간단한 방법을 사용하여 모든 트레이스 정보를 EffectContextTargetData로 저장합니다. 이렇게 하면 RPC 수가 8에서 1로 줄어들지만, 여전히 그 하나의 RPC에서 약 500바이트의 데이터가 전송됩니다. 더 최적화된 방법은 임팩트 위치를 효율적으로 인코딩하는 커스텀 구조체를 사용하여 RPC를 보내거나, 랜덤 시드 번호를 사용해 수신 측에서 임팩트 위치를 재생성하거나 근사화하는 방법입니다. 클라이언트는 이 커스텀 구조체를 언팩하여 로컬에서 실행되는 GameplayCue로 변환합니다.

이 방법은 다음과 같이 작동합니다:

  1. FScopedGameplayCueSendContext를 선언합니다. 이것은 UGameplayCueManager::FlushPendingCues()의 호출을 범위 밖으로 나올 때까지 억제하여, 모든 GameplayCueFScopedGameplayCueSendContext 범위 밖으로 나올 때까지 큐에 저장됩니다.
  2. UGameplayCueManager::FlushPendingCues()를 오버라이드하여, 일부 GameplayTag에 따라 배치할 수 있는 GameplayCue들을 커스텀 구조체에 병합하고 이를 클라이언트로 RPC로 전송합니다.
  3. 클라이언트는 커스텀 구조체를 수신하고 이를 로컬에서 실행되는 GameplayCue로 언팩합니다.

이 방법은 FGameplayCueParameters에 맞지 않는 특정 GameplayCue 파라미터가 필요할 때, 예를 들어 피해 수치, 치명타 표시, 방어구가 파괴된 표시, 치명적인 타격 표시 등과 같은 EffectContext를 추가하려고 할 때 유용하게 사용할 수 있습니다.

https://forums.unrealengine.com/development-discussion/c-gameplay-programming/1711546-fscopedgameplaycuesendcontext-gameplaycuemanager

4.8.7.2 하나의 GE에 여러 개의 GC

GameplayEffect에 있는 모든 GameplayCue는 이미 하나의 RPC로 전송됩니다. 기본적으로 UGameplayCueManager::InvokeGameplayCueAddedAndWhileActive_FromSpec()는 전체 GameplayEffectSpec(그러나 FGameplayEffectSpecForRPC로 변환된 형태)을 NetMulticast로 전송합니다. 이는 ASCReplication Mode와 관계없이 신뢰할 수 없는 방식으로 전송됩니다. 이 방식은 GameplayEffectSpec에 포함된 데이터에 따라 많은 대역폭을 차지할 수 있습니다. 이를 최적화하려면 cvar AbilitySystem.AlwaysConvertGESpecToGCParams 1을 설정할 수 있습니다. 이 설정은 GameplayEffectSpecFGameplayCueParameters 구조체로 변환하여, FGameplayEffectSpecForRPC 대신 이 구조체만 RPC로 전송하게 합니다. 이 방식은 잠재적으로 대역폭을 절약할 수 있지만, GESpecFGameplayCueParameters로 변환되는 과정에서 정보가 일부 손실될 수 있으며, 이는 각 GameplayCue가 요구하는 정보에 따라 다를 수 있습니다.

⬆ 위로 가기

4.8.8 Gameplay Cue Events

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

EGameplayCueEvent설명
OnActiveGameplayCue가 활성화(추가)될 때 호출됩니다.
WhileActiveGameplayCue가 활성 상태일 때 호출되며, 실제로 바로 적용되지 않았더라도 진행 중인 경우에 호출됩니다(진행 중 조인 등). 이는 Tick이 아니며, GameplayCueNotify_Actor가 추가되거나 OnActive일 때 한 번만 호출됩니다. Tick()이 필요하면 GameplayCueNotify_ActorTick()을 사용해야 합니다. 결국 이것은 AActor입니다.
RemovedGameplayCue가 제거될 때 호출됩니다. 이 이벤트에 응답하는 블루프린트 GameplayCue 함수는 OnRemove입니다.
ExecutedGameplayCue가 실행될 때 호출됩니다: Instant Effect나 Periodic Tick(). 이 이벤트에 응답하는 블루프린트 GameplayCue 함수는 OnExecute입니다.

GameplayCue 시작 시 발생하는 GameplayCue의 모든 이펙트에 OnActive를 사용하되, 늦게 참여하는 플레이어가 놓쳐도 괜찮은 경우 사용합니다. WhileActiveGameplayCue에서 지속적으로 발생하는 효과에 사용하며, 늦게 합류한 플레이어도 볼 수 있도록 해야 합니다. 예를 들어 MOBA에서 타워 구조물이 폭발하는 GameplayCue가 있을 때, 초기 폭발 파티클 시스템과 폭발 사운드는 OnActive에 넣고 폭발 후 지속적으로 발생하는 불꽃 파티클이나 사운드는 WhileActive에 넣을 수 있습니다. 이 시나리오에서는 뒤늦게 합류한 플레이어가 초기 폭발을 OnActive에서 재생하는 것은 의미가 없지만, 폭발이 발생한 후 지면에 지속적이고 반복되는 불꽃 이펙트를 WhileActive에서 볼 수 있게 하려는 것입니다. OnRemoveOnActiveWhileActive에 추가된 모든 항목을 정리해야 합니다.

  • WhileActive는 액터가 GameplayCueNotify_Actor의 연관성 범위에 들어올 때마다 호출됩니다.
  • OnRemove는 액터가 GameplayCueNotify_Actor의 연관성 범위를 벗어날 때마다 호출됩니다.

⬆ 위로 가기

4.8.9 Gameplay Cue Reliability(신뢰성)

GameplayCue는 일반적으로 비신뢰성을 가지므로, 직접적으로 게임 플레이에 영향을 미치는 요소에는 적합하지 않습니다.

실행된 GameplayCue: 비신뢰성 멀티캐스트(Unreliable Multicast)를 통해 적용되며 항상 신뢰성이 보장되지 않습니다.

GameplayEffect에서 적용되는 GameplayCue:

  • Autonomous Proxy는 OnActive, WhileActive, OnRemove 이벤트를 신뢰성 있게 수신합니다. FActiveGameplayEffectsContainer::NetDeltaSerialize()OnActiveWhileActive를 호출하기 위해 UAbilitySystemComponent::HandleDeferredGameplayCues()를 실행합니다. FActiveGameplayEffectsContainer::RemoveActiveGameplayEffectGrantedTagsAndModifiers()OnRemoved를 호출합니다.
  • Simulated Proxy는 WhileActiveOnRemove를 신뢰성 있게 수신합니다. UAbilitySystemComponent::MinimalReplicationGameplayCues의 리플리케이션은 WhileActiveOnRemove를 호출합니다. OnActive 이벤트는 비신뢰성 멀티캐스트에 의해 호출됩니다.

GameplayEffect없이 적용되는 GameplayCue:

  • Autonomous Proxy는 OnRemove를 신뢰성 있게 수신합니다. OnActiveWhileActive 이벤트는 비신뢰성 멀티캐스트로 호출됩니다.
  • Simulated Proxy는 WhileActiveOnRemove를 신뢰성 있게 수신합니다. UAbilitySystemComponent::MinimalReplicationGameplayCues의 리플리케이션은 WhileActiveOnRemove를 호출합니다. OnActive 이벤트는 비신뢰성 멀티캐스트에 의해 호출됩니다.

GameplayCue에서 신뢰성이 필요한 경우, 해당 GameplayCueGameplayEffect를 통해 적용하고, WhileActive에서 FX를 추가하며 OnRemove에서 FX를 제거하도록 설정하세요.

⬆ 위로 가기

4.9 Ability System Globals

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와 관련된 오류가 발생하고 클라이언트가 서버와 연결이 끊길 수 있습니다. 이 함수는 프로젝트에서 한 번만 호출하면 됩니다. 이 함수를 포트나이트에서는 UAssetManager::StartInitialLoading()에서 호출했고, Paragon은 UEngine::Init()에서 호출했습니다. 샘플 프로젝트에서는 이를 UAssetManager::StartInitialLoading()에 배치하는 것이 좋은 방법으로 제시됩니다. 이 코드는 TargetData 문제를 방지하기 위해 프로젝트에 복사해 사용하는 기본 코드로 간주할 수 있습니다. UE 5.3부터는 이 함수가 자동으로 호출됩니다.

만약 AbilitySystemGlobalsGlobalAttributeSetDefaultsTableNames를 사용하는 도중 충돌이 발생한다면, Fortnite처럼 AssetManagerGameInstance에서 UAbilitySystemGlobals::Get().InitGlobalData()를 나중에 호출해야 할 수도 있습니다.

⬆ 위로 가기

4.10 Prediction

GAS는 클라이언트 측 예측을 기본적으로 지원하지만, 모든 것을 예측하지는 않습니다. GAS에서의 클라이언트 측 예측은 클라이언트가 서버의 승인을 기다리지 않고 GameplayAbility를 활성화하고 GameplayEffect를 적용할 수 있다는 의미입니다. 클라이언트는 서버가 이를 허용할 것이라고 예측하고, 예측한 대로 타겟에 GameplayEffect를 적용합니다. 그 후 서버는 GameplayAbility network latency-time이 지난 후 클라이언트가 예측한 것이 맞았는지 여부를 알려줍니다. 만약 클라이언트가 잘못 예측했다면, 서버와 일치하도록 변경 사항을 롤백합니다.

GAS 관련 예측의 결정적인 출처는 GameplayPrediction.h에 있는 플러그인 소스 코드입니다.

에픽 게임즈의 마인드셋은 할 수 있는 것만 예측하라입니다. 예를 들어, Paragon과 Fortnite는 피해를 예측하지 않습니다. 대부분 ExecutionCalculations를 사용하여 피해를 처리하며, 이는 예측할 수 없습니다. 그렇다고 해서 피해 같은 것을 예측할 수 없다는 것은 아닙니다. 만약 예측이 잘 된다면, 그렇게 하는 것도 좋습니다.

"모든 것을 완벽하게 자동으로 예측하는" 솔루션에 올인하는 것도 아닙니다. 저희는 여전히 플레이어 예측을 최소한으로 유지하는 것이 가장 좋다고 생각합니다(즉, 플레이어가 피할 수 있는 최소한의 것만 예측하는 것이 좋습니다).

Network Prediction Plugin에 대한 에픽 게임즈의 데이브 라티의 코멘트.

predicted되는 것:

  • Ability 활성화
  • 트리거된 이벤트
  • GameplayEffect 적용
    • Attribute 수정(예외: Execution은 전혀 예측되지 않으며, Attribute Modifier에서 예측됩니다.)
    • GameplayTag 수정
  • GameplayCue 이벤트(예측된 GameplayEffect와 그 자체로도 가능합니다.)
  • 몽타주
  • 움직임 (UE5 UCharacterMovement 내장)

predicted되지 않는 것:

  • 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: Instant GameplayEffect(예: Cost GE)는 자신에게 Attribute를 예측하는 데 원활하게 예측할 수 있습니다. 그러나 다른 캐릭터에 대한 Instant Attribute 변경을 예측하면, 그들의 Attribute에 잠시 간격이 생기거나 깜빡임(Blip)이 나타날 수 있습니다. 예측된 Instant GameplayEffectInfinite GameplayEffect처럼 처리되어 잘못 예측되었을 경우 롤백할 수 있습니다. 서버의 GameplayEffect가 적용되면 두 개의 동일한 GameplayEffect가 존재하게 되어 잠시 동안 Modifier가 두 번 적용되거나 적용되지 않을 수 있습니다. 결국 수정되지만, 이 깜빡임은 플레이어에게 눈에 띄는 경우가 있을 수 있습니다.

GAS의 prediction 구현이 해결하려는 문제들:

  1. "해도 되는가?" 예측을 위한 기본 프로토콜
  2. "실행 취소" 예측이 실패했을 때 부작용을 되돌리는 방법
  3. "재실행" 클라이언트가 예측한 부작용을 서버에서 리플리케이트하여 다시 실행하지 않도록 하는 방법
  4. "완전성" 모든 부작용을 진정으로 예측했는지 확인하는 방법
  5. "종속성" 예측된 이벤트와 의존적인 이벤트의 관리 방법
  6. "재정의" 서버에서 리플리케이되거나 소유된 상태를 예측적으로 오버라이드하는 방법

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에서 새로운 Prediction Windows 만들기

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를 생성할 수 있습니다.

자신의 custom 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 AbilityTaskOnlyServerWait 옵션으로 배치하여 새로운 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가 게임플레이에 영향을 미치는 경우(예: 투사체처럼 피해 예측이 필요한 경우), 고급 로직이 필요하며 이는 이 문서의 범위를 벗어납니다. 예를 들어, Unreal Tournament에서는 투사체를 소유 클라이언트에서만 더미로 생성하고, 서버에서 리플리케이트된 투사체와 동기화하는 방법을 사용하고 있습니다. 이 방법은 에픽 게임즈의 GitHub에서 예측적으로 투사체를 생성하는 방법을 확인할 수 있습니다.

⬆ 위로 가기

4.10.4 GAS Prediction의 미래

GameplayPrediction.h에서는 향후 GameplayEffect 제거 및 주기적인 GameplayEffect의 예측을 추가할 수 있는 기능을 제공할 수 있다고 명시되어 있습니다.

에픽 게임즈의 데이브 라티는 Cooldown 예측에서 발생하는 latency reconciliation(지연 시간 불일치) 문제를 해결하려는 관심을 표명했으며, 이 문제는 높은 지연 시간을 가진 플레이어가 낮은 지연 시간을 가진 플레이어보다 불리한 상황을 초래할 수 있습니다.

에픽 게임즈의 새로운 Network Prediction plugin 플러그인은 이전의 CharacterMovementComponent와 마찬가지로 GAS와 완전히 호환될 것으로 예상됩니다. 이 플러그인은 GAS의 예측 기능을 개선하고, 예측을 보다 원활하게 처리할 수 있도록 도와줄 것입니다.

⬆ 위로 가기

4.10.5 Network Prediction Plugin

에픽 게임즈는 CharacterMovementComponent를 새로운 Network Prediction 플러그인으로 대체하는 프로젝트를 시작했습니다. 해당 플러그인은 아직 초기 단계에 있지만 언리얼 엔진 깃허브에서 얼리 액세스로 이용할 수 있습니다. 향후 언리얼 엔진의 어떤 버전에서 실험적 베타 버전으로 출시될지는 아직 알 수 없습니다.

⬆ 위로 가기

4.11 Targeting

4.11.1 Target Data

FGameplayAbilityTargetData는 네트워크를 통해 전달될 Target Data용으로 설계된 일반 구조체입니다. Target Data에는 보통 AActor/UObject 레퍼런스, FHitResults, 그 이외의 위치/방향/원점 정보 등이 포함됩니다. 또한, 해당 구조체를 서브클래싱하면 클라이언트와 서버 간에 데이터를 전달하는 간단한 수단으로 원하는 모든 것을 GameplayAbilities에 넣을 수 있습니다. FGameplayAbilityTargetData는 기본적으로 직접 사용되기보다는 서브클래싱하여 사용하는 것을 목적으로 합니다. GAS는 이미 몇 가지 서브클래싱된 FGameplayAbilityTargetData 구조체를 제공하며, 이는 GameplayAbilityTargetTypes.h에 정의되어 있습니다.

TargetData는 일반적으로 Target Actors에 의해 생성되거나 수동으로 만들어지며, AbilityTasksGameplayEffects에서 EffectContext를 통해 소비됩니다. EffectContext에 포함된 덕분에 Executions, MMCs, GameplayCues, 그리고 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 // FGameplayAbilityTargetDataHandle net serialization이 작동하려면 필수입니다.
	};
};

핸들에 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 Actors

GameplayAbilityWaitTargetData AbilityTask와 함께 TargetActors를 생성하여, World에서 타겟 정보를 시각화하고 캡처할 수 있습니다. TargetActor는 선택적으로 GameplayAbilityWorldReticles을 사용하여 현재 타겟을 표시할 수 있습니다. 타겟 정보가 확인되면, 해당 정보는 TargetData로 반환되어 GameplayEffect에 전달될 수 있습니다.

TargetActorAActor를 기반으로 하므로 타겟이 어디에 있는지, 어떻게 타겟팅하는지를 나타내기 위해 정적 메시나 데칼과 같은 시각적 컴포넌트를 가질 수 있습니다. 정적 메시를 사용하여 캐릭터가 생성할 물체의 배치 위치를 시각화하거나, 데칼을 사용하여 땅에 영향을 미치는 영역을 표시할 수 있습니다. 샘플 프로젝트에서는 Meteor 능력의 피해 범위를 나타내기 위해 땅에 데칼을 사용하는 AGameplayAbilityTargetActor_GroundTrace를 사용합니다. 그러나 어떤 경우에는 표시할 것이 없을 수도 있습니다. 예를 들어, GASShooter에서처럼 즉시 타겟을 추적하는 히트스캔 총의 경우, 아무것도 표시할 필요가 없을 수 있습니다.

TargetActor는 기본적으로 트레이스나 충돌 오버랩을 사용하여 타겟팅 정보를 캡처하고, 그 결과를 FHitResult 또는 AActor 배열로 변환하여 TargetData로 전달합니다. WaitTargetData AbilityTaskTEnumAsByteEGameplayTargetingConfirmation::Type ConfirmationType 매개변수를 통해 타겟이 확인될 시점을 결정합니다. TEnumAsByteEGameplayTargetingConfirmation::Type::Instant사용하지 않는 경우, TargetActor는 일반적으로 Tick()에서 트레이스/오버랩을 수행하고 구현에 따라 FHitResult에 위치를 업데이트합니다. 이 방법은 Tick()에서 트레이스/오버랩을 수행하지만, 복제되지 않으며 보통 동시에 실행되는 TargetActor가 하나뿐이므로 성능에 큰 영향을 미치지 않습니다. 다만, 복잡한 TargetActorTick()에서 많은 작업을 할 수 있으므로 성능에 부담이 될 수 있습니다. Tick()에서 트레이스를 하는 것은 클라이언트에서 매우 반응성이 좋지만, 성능 저하가 너무 크다면 TargetActor의 틱 속도를 낮추는 것을 고려할 수 있습니다. TEnumAsByteEGameplayTargetingConfirmation::Type::Instant을 사용하는 경우, TargetActor는 즉시 생성되어 TargetData를 생성한 후 바로 파괴됩니다. 이 경우 Tick()은 호출되지 않습니다.

EGameplayTargetingConfirmation::Type타겟이 확인되는 시점
Instant타겟팅은 즉시 발생하며 특별한 로직이나 사용자 입력이 필요하지 않습니다.
UserConfirmed타겟팅은 사용자가 Ability에 바인딩된 Confirm 입력을 통해 또는 UAbilitySystemComponent::TargetConfirm()을 호출하여 확인되었을 때 발생합니다. TargetActor는 바인딩된 Cancel 입력 또는 UAbilitySystemComponent::TargetCancel() 호출에 의해 타겟팅을 취소할 수도 있습니다.
CustomGameplayTargeting Ability가 UGameplayAbility::ConfirmTaskByInstanceName()을 호출하여 타겟팅 데이터가 준비된 시점을 결정합니다. TargetActor는 또한 UGameplayAbility::CancelTaskByInstanceName()을 호출하여 타겟팅을 취소할 수 있습니다.
CustomMultiGameplayTargeting Ability가 UGameplayAbility::ConfirmTaskByInstanceName()을 호출하여 타겟팅 데이터가 준비된 시점을 결정합니다. TargetActor는 또한 UGameplayAbility::CancelTaskByInstanceName()을 호출하여 타겟팅을 취소할 수 있습니다. 데이터 생성 시 AbilityTask를 종료하지 않아야 합니다.

모든 EGameplayTargetingConfirmation::Type이 모든 TargetActor에서 지원되는 것은 아닙니다. 예를 들어, AGameplayAbilityTargetActor_GroundTrace는 Instant 확인을 지원하지 않습니다.

WaitTargetData AbilityTaskAGameplayAbilityTargetActor 클래스를 매개변수로 받아, AbilityTask가 활성화될 때마다 인스턴스를 생성하고 AbilityTask가 종료되면 TargetActor를 파괴합니다. WaitTargetDataUsingActor AbilityTask는 이미 생성된 TargetActor를 매개변수로 받지만, 여전히 AbilityTask가 종료될 때 TargetActor를 파괴합니다. 이 두 AbilityTask는 새로운 TargetActor를 생성하거나 요구하므로 비효율적일 수 있습니다. 이는 프로토타입에 적합하지만, 자동 소총처럼 지속적으로 TargetData를 생성해야 하는 경우, 최적화를 고려할 수 있습니다. GASShooter는 AGameplayAbilityTargetActor의 사용자 정의 서브클래스와 새로 작성된 WaitTargetDataWithReusableActor AbilityTask를 사용하여 TargetActor를 재사용하고 파괴하지 않습니다.

TargetActor는 기본적으로 복제되지 않지만, 게임에서 다른 플레이어에게 로컬 플레이어의 타겟팅 위치를 보여줄 필요가 있다면 복제할 수 있습니다. TargetActorWaitTargetData AbilityTask를 통해 서버와 통신하는 기본 기능을 포함하고 있습니다.

만약 TargetActorShouldProduceTargetDataOnServer 속성이 false로 설정되어 있다면, 클라이언트는 타겟팅이 확인되면 TargetDataUAbilityTask_WaitTargetData::OnTargetDataReadyCallback()CallServerSetReplicatedTargetData()통해 서버에 RPC로 확인을 전송합니다.

만약 ShouldProduceTargetDataOnServertrue라면, 클라이언트는 UAbilityTask_WaitTargetData::OnTargetDataReadyCallback()EAbilityGenericReplicatedEvent::GenericConfirm을 RPC로 서버에 보내고, 서버는 이를 받아 트레이스 또는 오버랩을 수행하여 서버에서 데이터를 생성합니다.

클라이언트가 타겟팅을 취소하면, UAbilityTask_WaitTargetData::OnTargetDataCancelledCallbackEAbilityGenericReplicatedEvent::GenericCancel을 RPC로 서버에 보내고, 서버는 이를 받아 타겟팅을 취소합니다. 이처럼 TargetActorWaitTargetData AbilityTask는 많은 델리게이트를 사용합니다. TargetActor는 타겟팅 데이터를 준비, 확인, 또는 취소하는 델리게이트를 방송하고, WaitTargetData는 이를 듣고 GameplayAbility 및 서버로 전달합니다. 서버로 TargetData를 보낼 때는 부정행위를 방지하기 위해 서버에서 데이터가 합리적인지 검증하는 것이 좋습니다. 서버에서 직접 TargetData를 생성하면 이 문제를 완전히 피할 수 있지만, 클라이언트에서 예측 오류가 발생할 수 있습니다.

사용하는 AGameplayAbilityTargetActor의 서브클래스에 따라, WaitTargetData AbilityTask 노드에서 여러 ExposeOnSpawn 매개변수가 노출됩니다. 일부 일반적인 매개변수는 다음과 같습니다:

Common TargetActor Parameters정의
Debugtrue일 경우, TargetActor가 트레이스를 수행할 때마다 디버그 트레이싱/오버랩 정보를 그려냅니다. Shipping이 아닌 빌드에서만 표시됩니다. 일반적으로 non-Instant TargetActorTick()에서 트레이스를 수행하므로 이 디버그 드로우 호출도 Tick()에서 발생합니다.
Filter[선택] 트레이스/오버랩이 발생할 때 Actor를 필터링하는 특수 구조체입니다. 일반적으로 플레이어의 Pawn을 제외하거나 특정 클래스만 타겟팅하려는 경우 사용됩니다. 더 고급 사용 사례는 Target Data Filters을 참조하세요.
Reticle Class[선택] TargetActor가 생성할 AGameplayAbilityWorldReticle의 서브클래스입니다.
Reticle Parameters[Optional] Reticle을 설정합니다. Reticles을 참조하세요.
Start Location트레이싱이 시작될 위치를 설정하는 특수 구조체입니다. 보통 플레이어의 시점, 무기 총구, 또는 Pawn의 위치입니다.

기본 TargetActor 클래스에서는 Actor가 트레이스/오버랩 내에 있을 때만 유효한 타겟으로 간주됩니다. 트레이스/오버랩을 벗어나면 더 이상 유효하지 않습니다. TargetActor가 마지막 유효 타겟을 기억하도록 하려면 커스텀 TargetActor 클래스에서 이 기능을 추가해야 합니다. 이를 Persistent Target(지속 타겟)이라고 부르며, TargetActor가 확인 또는 취소를 받기 전까지, 새로운 유효 타겟을 찾기 전까지, 또는 타겟이 더 이상 유효하지 않으면 계속 유지됩니다. GASShooter는 로켓 발사기의 보조 능력에서 지속 타겟을 사용하여 유도 로켓 타겟팅을 구현합니다.

⬆ 위로 가기

4.11.3 Target Data Filters

Make GameplayTargetDataFilterMake 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 Reticles

AGameplayAbilityWorldReticles(Reticle)는 즉시 확인되지 않은 TargetActors를 사용할 때 타겟팅 중인 대상을 시각화합니다. TargetActor는 모든 Reticle의 생성과 소멸 수명을 담당합니다. ReticleAActor이므로 모든 종류의 시각적 컴포넌트를 사용하여 표현할 수 있습니다. 이는 GASShooter 화면 공간에서 항상 플레이어의 카메라를 향해 보이는 UMG 위젯을 표시하는 WidgetComponent와 같은 시각적 컴포넌트를 사용할 수 있습니다. Reticle은 자신이 어떤 AActor에 있는지 알지 못하지만, 커스텀 TargetActor에서 이 기능을 서브클래싱하여 추가할 수 있습니다. 일반적으로 TargetActor는 매 Tick()마다 Reticle의 위치를 타겟의 위치로 업데이트합니다.

GASShooter에서는 Reticle을 사용하여 로켓 발사기의 보조 능력인 유도 미사일의 잠금된 타겟을 표시합니다. 적의 빨간 표시가 Reticle이고, 유사한 흰색 이미지는 로켓 발사기의 조준선입니다.

Reticles in GASShooter

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가 제공하는 FWorldReticleParameters를 선택적으로 사용할 수 있습니다. 기본 구조체는 하나의 변수인 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 Gameplay Effect Containers Targeting

GameplayEffectContainersTargetData를 생성하는 효율적인 방법을 옵션으로 제공합니다. 해당 타겟팅은 EffectContainer가 클라이언트와 서버에서 적용될 때 즉시 발생합니다. 이는 TargetActors보다 효율적이며, 타겟팅 객체의 CDO에서 실행되므로(액터를 생성하거나 파괴하지 않음) 성능이 뛰어납니다. 그러나 플레이어 입력이 없고, 확인 없이 즉시 발생하며, 취소할 수 없고, 클라이언트에서 서버로 데이터를 전송할 수 없습니다(두 곳에서 데이터가 생성). 이 방식은 인스턴트 트레이스와 충돌 오버랩에 잘 작동합니다. 에픽 게임즈의 Action RPG 샘플 프로젝트는 두 가지 예시 타겟팅 방법을 GameplayEffectContainer와 함께 제공합니다. 하나는 Ability 소유자를 타겟으로 하고, 다른 하나는 이벤트에서 TargetData를 가져오는 방식입니다. 또한, Blueprint에서 플레이어로부터 일정 오프셋(자식 Blueprint 클래스에서 설정)을 두고 인스턴트 구체 트레이스를 수행하는 예시도 구현되어 있습니다. URPGTargetType을 C++ 또는 Blueprint에서 서브클래스하여 자신만의 타겟팅 유형을 만들 수 있습니다.

⬆ 위로 가기

profile
언리얼 엔진 주니어(신입) 개발자 | 소설 쓰는 취준 개발자

0개의 댓글