언리얼 델리게이트에 바인딩할 때 생성자가 아닌 BeginPlay에서 수행

생성자에서는 객체나 델리게이트 초기화가 완료되지 않은 상태

 

Cast는 UObject 기반 클래스에만 사용할 수 있음

 

액터와 무관하게 동작해야 되는 다른 액터들을 가급적 약참조(TWeakObjectPtr)로 선언

강참조(TObjectPtr)로 선언하면 언리얼 엔진이 액터를 아직 사용하고 있다고 판단하고 메모리에서 소멸시키지 않을 수 있음

 

NewObject는 C++ New와 다름

UObjectBase는 FUObjectAllocator와 friend가 설정돼있고 FUObjectAllocator에서 UObjectBase를 생성하므로 public이 아닌 생성자여도 UE의 오브젝트 생성 시스템이 접근 가능

 

Dynamic 델리게이트에 바인딩하는 함수는 UFUNCTION 매크로 지정이 필수

리플렉션 시스템을 통해 런타임에 함수를 찾아야 함

 

라이브 코딩(Ctrl+Alt+F11)이 아닌 코드 빌드해야 하는 경우

생성자 혹은 헤더 변경

리플렉션 데이터나 CDO 데이터의 변경이 일어날 확률이 높은데 적용하려면 빌드를 새로 해야하기 때문

 

기존 엔진 함수는 엔진 동작에 있어 중요하므로 override할 때 기본 클래스의 함수를 실행할 것

void UMyGameInstance::Init()
{
	Super::Init();
}

게임플레이 큐(GC, Gameplay Cue)

시각 이펙트나 사운드와 같은 게임로직과 무관한 시각/청각 기능 담당

데디케이티드 서버에서는 사용할 필요가 없음

 

스태틱 게임플레이 큐 : 일시적으로 발생하는 특수효과 사용, Execute 이벤트 발동

액터 게임플레이 큐 : 일정 기간동안 발생하는 특수효과에 사용, Add/Remove 이벤트 발동

 

C++로 구현할 수 있지만 핵심기능만 구현하고 블루프린트로 제작하는 것이 더 생산적

 

게임플레이 이펙트에서 큐와 연동할 수 있도록 기능을 제공함

큐의 재생은 GameplayCueManager가 관리

게임플레이 태그를 사용해 쉽게 발동할 수 있는데, 태그는 반드시 GameplayCue로 시작해야 함

 

게임플레이 큐를 활용한 공격 이펙트 구현

void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentComboLevel);
		if (EffectSpecHandle.IsValid())
		{
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);

			FHitResult HitResult = *TargetDataHandle.Get(0)->GetHitResult();

			if (UAbilitySystemComponent* TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor()))
			{
				FGameplayEffectContextHandle EffectContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle);
				EffectContextHandle.AddHitResult(HitResult);
				FGameplayCueParameters CueParameters;
				CueParameters.EffectContext = EffectContextHandle;
				
				TargetAbilitySystemComponent->ExecuteGameplayCue(GAMEPLAYCUE_CHARCTER_ATTACKHIT, CueParameters);
			}
		}
	}
    
    ...
}

 

UABGC_AttackHit::UABGC_AttackHit()
{
	static ConstructorHelpers::FObjectFinder<UParticleSystem> ExplosionRef(TEXT("/Script/Engine.ParticleSystem'/Game/StarterContent/Particles/P_Explosion.P_Explosion'"));
	if (ExplosionRef.Object)
	{
		ParticleSystem = ExplosionRef.Object;
	}
}

bool UABGC_AttackHit::OnExecute_Implementation(AActor* MyTarget, const FGameplayCueParameters& Parameters) const
{
	if (const FHitResult* HitResult = Parameters.EffectContext.GetHitResult())
	{
		UGameplayStatics::SpawnEmitterAtLocation(MyTarget, ParticleSystem, HitResult->ImpactPoint, FRotator::ZeroRotator, true);
	}

	return false;
}

 

게임플레이 이펙트, 큐를 활용한 아이템 상자 구현

이펙트의 Period 옵션을 사용하면 Base Value를 변경할 수 있음

이펙트의 Gameplay Cue에 Tag를 지정하면 자동으로 큐가 실행됨

AABGASItemBox::AABGASItemBox()
{
	AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponent"));

	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("Trigger"));
	Trigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
	Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));
	RootComponent = Trigger;

	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	Mesh->SetupAttachment(Trigger);

	static ConstructorHelpers::FObjectFinder<UStaticMesh> BoxMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Env_Breakables_Box1.SM_Env_Breakables_Box1'"));
	if (BoxMeshRef.Object)
	{
		Mesh->SetStaticMesh(BoxMeshRef.Object);
	}
	Mesh->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
	Mesh->SetCollisionProfileName(TEXT("NoCollision"));
}

void AABGASItemBox::NotifyActorBeginOverlap(AActor* OtherActor)
{
	Super::NotifyActorBeginOverlap(OtherActor);

	FGameplayCueParameters CueParameters;
	CueParameters.SourceObject = this;
	CueParameters.Instigator = OtherActor;
	CueParameters.Location = GetActorLocation();
	
	AbilitySystemComponent->ExecuteGameplayCue(GameplayCueTag, CueParameters);
	if (UAbilitySystemComponent* TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
	{
		FGameplayEffectContextHandle EffectContext = TargetAbilitySystemComponent->MakeEffectContext();
		EffectContext.AddSourceObject(this);
		
		FGameplayEffectSpecHandle EffectSpecHandle = TargetAbilitySystemComponent->MakeOutgoingSpec(GameplayEffectClass, 1, EffectContext);
		if (EffectSpecHandle.IsValid())
		{
			TargetAbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
		}
	}

	Mesh->SetHiddenInGame(true);
	SetActorEnableCollision(false);
	SetLifeSpan(2.0f);
}

void AABGASItemBox::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	AbilitySystemComponent->InitAbilityActorInfo(this, this);
}

 

게임플레이 이펙트를 활용한 무기 상자 구현

void AABGASCharacterPlayer::EquipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		Weapon->SetSkeletalMesh(WeaponMesh);
		
		// 수동으로 어트리뷰트에 접근하는 것은 바람직하지 않지만 일부러 구현
		const float CurrentAttackRange = AbilitySystemComponent->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute());
		const float CurrentAttackRate = AbilitySystemComponent->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute());
		
		AbilitySystemComponent->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange + WeaponRange);
		AbilitySystemComponent->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate + WeaponAttackRate);
	}
}

void AABGASCharacterPlayer::UnEquipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		Weapon->SetSkeletalMesh(nullptr);
		
		// 수동으로 어트리뷰트에 접근하는 것은 바람직하지 않지만 일부러 구현
		const float CurrentAttackRange = AbilitySystemComponent->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute());
		const float CurrentAttackRate = AbilitySystemComponent->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute());
		
		AbilitySystemComponent->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange - WeaponRange);
		AbilitySystemComponent->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate - WeaponAttackRate);
	}
}

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

	if (AABGASPlayerState* CharacterPlayerState = GetPlayerState<AABGASPlayerState>())
	{
		AbilitySystemComponent = CharacterPlayerState->GetAbilitySystemComponent();
		AbilitySystemComponent->InitAbilityActorInfo(CharacterPlayerState, this);
		if (const UABCharacterAttributeSet* CurrentAttributeSet = AbilitySystemComponent->GetSet<UABCharacterAttributeSet>())
		{
			CurrentAttributeSet->OnOutOfHealth.AddDynamic(this, &ThisClass::OnOutOfHealth);
		}

		AbilitySystemComponent->GenericGameplayEventCallbacks.FindOrAdd(EVENT_CHARACTER_WEAPON_EQUIP).AddUObject(this, &ThisClass::EquipWeapon);
		AbilitySystemComponent->GenericGameplayEventCallbacks.FindOrAdd(EVENT_CHARACTER_WEAPON_UNEQUIP).AddUObject(this, &ThisClass::UnEquipWeapon);
        
        ...
}

 

void AABGASWeaponBox::NotifyActorBeginOverlap(AActor* OtherActor)
{
	Super::NotifyActorBeginOverlap(OtherActor);
	
	UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(OtherActor, WeaponEventTag, FGameplayEventData());
}

구현 사항

콤보 공격 히트시 뒤로 갈수록 전달하는 데미지가 증가하도록 설정

레벨에 따라 스폰된 캐릭터가 다른 체력 값을 가지도록 설정

 

게임플레이 이펙트(GE, Gameplay Effect)

GAS는 게임에 영향을 주는(게임 데이터 변경) 객체(GE)를 별도로 분리해서 관리하므로 대부분 게임플레이 이펙트와 어트리뷰트는 함께 동작하도록 구성됨

 

Instant : 어트리뷰트에 즉각적으로 적용되는 게임플레이 이펙트, 한 프레임에 실행됨

Duration : 지정한 시간 동안 동작하는 게임플레이 이펙트

Infinite : 명시적으로 종료하지 않으면 계속 동작하는 게임플레이 이펙트

 

다양하게 많은 옵션을 제공하여 다양하고 복잡한 작업의 수행이 가능함

 

게임플레이 이펙트 모디파이어(Modifier)

GE에서 어트리뷰트의 변경 방법을 설정

 

변경 방법

더하기, 곱하기, 나누기, 덮어쓰기

 

계산 방법

ScalableFloat : 실수(대입, 데이터테이블과 연동 가능)

AttributeBased : 특정 어트리뷰트 기반

CustomCalculationClass : 계산을 담당하는 전용 클래스 활용

SetByCaller : 데이터 태그를 활용한 데이터 전달

 

모디파이어 없이 GameplayEffectExecutionCalculation을 상속받아 자체 계산 로직을 만드는 것도 가능

 

블루프린트로 제작하는 것을 권장하는 이유

간단한 작업을 C++보다 블루프린트가 간단하고 생산성이 좋음

이펙트에서 어트리뷰트의 프로퍼티에 접근하기 위해 프렌드를 설정해야 함

void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect);
		if (EffectSpecHandle.IsValid())
		{
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
		}
	}

	bool bReplicateEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}

 

메타 어트리뷰트

어트리뷰트의 설정을 위해 사전에 미리 설정하는 임시 어트리뷰트

  • 체력을 바로 깎지 않고, 데미지를 통해 체력을 감소하도록 설정
    체력은 일반 어트리뷰트, 데미지는 메타 어트리뷰트

기획 추가에 유연한 대처가 가능

  • 무적 기능 추가 : 데미지를 0으로 처리
  • 실드 기능 추가 : 실드 값을 토대로 실드 값만큼 데미지를 처리
  • 콤보 진행시 공격력 보정 추가 : 콤보 증가시 데미지를 보정

메타 어트리뷰트는 적용 후 바로 0으로 값을 초기화되도록 설정

클라이언트에는 최종 체력 정보만 보내주면 되기 때문에 리플리케이션에서 제외시키는 것이 일반적

 

void UABCharacterAttributeSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
	constexpr float MinimumHealth = 0.0f;
	
	if (Data.EvaluatedData.Attribute == GetDamageAttribute()) 
	{
		SetHealth(FMath::Clamp(GetHealth() - GetDamage(), MinimumHealth, GetMaxHealth()));
		SetDamage(0.0f);
	}
}

 

레벨과 커브테이블

게임플레이 이펙트에 추가적으로 레벨 정보를 지정하여 커브테이블에서 특정 값을 가져올 수 있음

ScalableFloat 모디파이어 타입을 사용하여 가능

 

void UAnimNotify_GASAttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
	Super::Notify(MeshComp, Animation, EventReference);

	if (MeshComp)
	{
		if (AActor* Owner = MeshComp->GetOwner())
		{
			FGameplayEventData PayloadData;
			PayloadData.EventMagnitude = ComboAttackLevel;
			UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Owner, TriggerGameplayTag, PayloadData);
		}
	}
}

 

void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	CurrentComboLevel = TriggerEventData->EventMagnitude;
}

 

게임플레이 이펙트 생성 과정

게임플레이 이펙트 컨텍스트와 게임플레이 이펙트 스펙을 통해 생성할 수 있음

 

게임플레이 이펙트 컨텍스트

GE에서 계산에 필요한 데이터를 담은 객체

Instigator(가해자), Causer(가해수단), HitResult(판정정보) 등

 

게임플레이 이펙트 스펙

GE에 관련된 정보를 담고 있는 객체

레벨, 모디파이어, 태그에 대한 정보, 게임플레이 이펙트 컨텍스트 핸들

 

ASC는 각 데이터를 핸들 객체를 통해 간접적으로 관리하므로 이펙트 컨텍스트 핸들 -> 이펙트 스펙 핸들 순으로 생성해야 함

 

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

	AbilitySystemComponent->InitAbilityActorInfo(this, this);

	FGameplayEffectContextHandle EffectContextHandle = AbilitySystemComponent->MakeEffectContext();
	EffectContextHandle.AddSourceObject(this);

	FGameplayEffectSpecHandle EffectSpecHandle = AbilitySystemComponent->MakeOutgoingSpec(InitStatEffect, Level, EffectContextHandle);
	if (EffectSpecHandle.IsValid())
	{
		AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
	}
}

 

기간형 게임플레이 이펙트

인스턴트 타입이 아닌 게임플레이 이펙트

유효기간 동안 태그와 같은 상태를 가질 수 있음

중첩(Stack)이 가능하도록 설정이 가능

모디파이어를 설정하지 않아도 다양하게 활용 가능

인스턴트는 Base값을 변경하지만 기간형은 Current값을 변경하고 원래대로 돌려놓음

bool UABCharacterAttributeSet::PreGameplayEffectExecute(struct FGameplayEffectModCallbackData& Data)
{
	if (Data.EvaluatedData.Attribute == GetDamageAttribute())
	{
		if (Data.Target.HasMatchingGameplayTag(ABTAG_CHARCTER_INVINSIBLE))
		{
			return false;
		}
	}

	return true;
}

각 속성마다 오류를 피하기 위해 최대값을 지정하는 것도 좋은 방법

 

어트리뷰트 세트(Attribute Set)

단일 어트리뷰트 데이터인 GameplayAttributeData의 묶음

GameplayAttributeData는 하나의 값이 아닌 두 가지 값으로 구성됨

  • BaseValue : 기본 값, 영구적으로 적용되는 고정 스탯 값을 관리
  • CurrentValue : 변동 값, 버프 등으로 임시적으로 변동된 값을 관리

PreAttributeChange : 어트리뷰트 변경 전에 호출

PostAttributeChange : 어트리뷰트 변경 후에 호출

PreGameplayEffectExecute : 게임플레이 이펙트 적용 전에 호출

PostGameplayEffectExecute : 게임플레이 이펙트 적용 후에 호출

 

어트리뷰트 세트 접근자 매크로 : 많이 수행되는 기능에 대해 매크로를 만들어 제공하여 일일이 Get/Set 함수를 선언하지 않아도 됨

#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

 

ASC는 초기화될 때 같은 액터에 있는 UAttributeSet 타입 객체를 찾아서 등록하기 때문에 수동으로 등록할 필요가 없음

void UAbilitySystemComponent::InitializeComponent()
{
	...
    
	TArray<UObject*> ChildObjects;
	GetObjectsWithOuter(Owner, ChildObjects, false, RF_NoFlags, EInternalObjectFlags::Garbage);

	for (UObject* Obj : ChildObjects)
	{
		UAttributeSet* Set = Cast<UAttributeSet>(Obj);
		if (Set)  
		{
			SpawnedAttributes.AddUnique(Set);
			bIsNetDirty = true;
		}
	}

	...
}

 

 

다른 GAS 액터의 정보를 확인할 수 있는 디버그 시스템 설정 방법

DefaultGame.ini 파일에 해당 속성을 true로 변경해야 함

[/Script/GameplayAbilities.AbilitySystemGlobals]
bUseDebugTargetFromHud=True

 

어트리뷰트 세트를 활용한 공격 판정 관련 구현

FGameplayAbilityTargetDataHandle AABTA_Trace::MakeTargetData() const
{
	ACharacter* Character = CastChecked<ACharacter>(SourceActor);

	UAbilitySystemComponent* AbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
	if (AbilitySystemComponent == nullptr)
	{
		return FGameplayAbilityTargetDataHandle();
	}

	const UABCharacterAttributeSet* AttributeSet = AbilitySystemComponent->GetSet<UABCharacterAttributeSet>();
	if (AttributeSet == nullptr)
	{
		return FGameplayAbilityTargetDataHandle();
	}
	
	FHitResult OutHitResult;
	const float AttackRange = AttributeSet->GetAttackRange();
	const float AttackRadius = AttributeSet->GetAttackRadius();

	...
}

 

UABGA_AttackHitCheck::UABGA_AttackHitCheck()
{
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

	UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
	AttackTraceTask->OnComplete.AddDynamic(this, &ThisClass::OnTraceResultCallback);
	AttackTraceTask->ReadyForActivation();
}

void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0))
	{
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
		UAbilitySystemComponent* SourceAbilitySystemComponent = GetAbilitySystemComponentFromActorInfo();
		UAbilitySystemComponent* TargetAbilitySystemComponent = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());

		if (SourceAbilitySystemComponent == nullptr || TargetAbilitySystemComponent == nullptr)
		{
			return;
		}

		// 원래는 게임플레이 이펙트로 어트리뷰트를 변경해야 하므로 임시로 const_cast 사용
		const UABCharacterAttributeSet* SourceAttributeSet = SourceAbilitySystemComponent->GetSet<UABCharacterAttributeSet>();
		UABCharacterAttributeSet* TargetAttributeSet = const_cast<UABCharacterAttributeSet*>(TargetAbilitySystemComponent->GetSet<UABCharacterAttributeSet>());

		if (SourceAttributeSet == nullptr || TargetAttributeSet == nullptr)
		{
			return;
		}

		TargetAttributeSet->SetHealth(TargetAttributeSet->GetHealth() - SourceAttributeSet->GetAttackRate());
	}

	...
}

 

void UABCharacterAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	if (Attribute == GetHealthAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxHealth());
	}
}

구현 사항

공격 판정을 별도의 어빌리티 클래스로 분리해 의존성 감소

애니메이션 몽타주의 노티파이를 활용해 원하는 타이밍에 공격을 판정하는 기능 추가

애니메이션 노티파이가 발동되면 판정을 위한 GA를 트리거해 발동

새로운 GA가 발동되면 공격 판정을 위한 AT를 실행

GAS에서 제공하는 타겟 액터를 활용해 물리 공격 판정을 수행

판정 결과를 시각적으로 확인할 수 있도록 드로우 디버그 기능 제공

 

GA의 Ability Trigger의 Trigger Tag를 지정하면 ASC가 게임플레이 이벤트를 보낼 때 어빌리티를 활성화시켜줌

 

애니메이션 노티파이로 어빌리티 실행 코드

void UAnimNotify_GASAttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
	Super::Notify(MeshComp, Animation, EventReference);

	if (MeshComp)
	{
		if (AActor* Owner = MeshComp->GetOwner())
		{
			FGameplayEventData PayloadData;
			UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Owner, TriggerGameplayTag, PayloadData);
		}
	}
}

 

게임플레이 타겟 액터(TA)

게임플레이 어빌리티에서 대상에 대한 판정(주로 물리 판정)을 구현할 때 사용하는 특수 액터

 

타겟 액터가 필요한 이유는 즉각적으로 타겟을 판정하는 것이 아닌 사용자의 최종 확인을 한번 더 거치거나(예, 원거리 범위 공격) 공격 범위 확인을 위한 추가 시각화(WorldReticle)이 필요할 수 있음

 

StartTargeting : 타겟팅을 시작

ConfirmTargetingAndContinue : 타겟팅을 확정하고 이후 남은 프로세스를 진행

ConfirmTargeting : 태스크 진행 없이 타겟팅만 확정

CancelTargeting : 타겟팅 취소

 

게임플레이 어빌리티 타겟 데이터

타겟 액터에서 판정한 결과를 담은 데이터

HitResult, 판정된 다수 액터 포인터, 시작 지점, 끝지점을 포함

타겟 데이터 핸들로 타겟 데이터를 여러 개 묶어 전송

 

AbilityTask와 TargetActor 사이의 흐름

 

FGameplayAbilityTargetData_SingleTargetHit을 생성할 때 DataHandle.Add를 통해 TSharedPtr을 사용해 넣고 있기 때문에 안전

 

타겟 액터로 공격 판정 구현 코드

void UABAT_Trace::Activate()
{
	Super::Activate();

	SpawnAndInitializeTargetActor();
	FinalizeTargetActor();

	SetWaitingOnAvatar();
}

void UABAT_Trace::OnDestroy(bool bInOwnerFinished)
{
	if (SpawnedTargetActor)
	{
		SpawnedTargetActor->Destroy();
	}

	Super::OnDestroy(bInOwnerFinished);
}

UABAT_Trace* UABAT_Trace::CreateTask(UGameplayAbility* OwningAbility, TSubclassOf<AABTA_Trace> TargetActorClass)
{
	UABAT_Trace* NewTask = NewAbilityTask<UABAT_Trace>(OwningAbility);
	NewTask->TargetActorClass = TargetActorClass;
	return NewTask;
}

void UABAT_Trace::SpawnAndInitializeTargetActor()
{
	SpawnedTargetActor = Cast<AABTA_Trace>(Ability->GetWorld()->SpawnActorDeferred<AGameplayAbilityTargetActor>(TargetActorClass, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
	if (SpawnedTargetActor)
	{
		SpawnedTargetActor->SetDrawDebug(true);
		SpawnedTargetActor->TargetDataReadyDelegate.AddUObject(this, &ThisClass::OnTargetDataRedayCallback);
	}
}

void UABAT_Trace::FinalizeTargetActor()
{
	if (UAbilitySystemComponent* ASC = AbilitySystemComponent.Get())
	{
		const FTransform SpawnTransform = ASC->GetAvatarActor()->GetTransform();
		SpawnedTargetActor->FinishSpawning(SpawnTransform);

		ASC->SpawnedTargetActors.Push(SpawnedTargetActor);
		SpawnedTargetActor->StartTargeting(Ability);
		SpawnedTargetActor->ConfirmTargeting();
	}
}

void UABAT_Trace::OnTargetDataRedayCallback(const FGameplayAbilityTargetDataHandle& DataHandle)
{
	if (ShouldBroadcastAbilityTaskDelegates())
	{
		OnComplete.Broadcast(DataHandle);
	}

	EndTask();
}

 

void AABTA_Trace::StartTargeting(UGameplayAbility* Ability)
{
	Super::StartTargeting(Ability);

	SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
}

void AABTA_Trace::ConfirmTargetingAndContinue()
{
	if (SourceActor)
	{
		FGameplayAbilityTargetDataHandle DataHandle = MakeTargetData();
		TargetDataReadyDelegate.Broadcast(DataHandle);
	}
}

FGameplayAbilityTargetDataHandle AABTA_Trace::MakeTargetData() const
{
	ACharacter* Character = CastChecked<ACharacter>(SourceActor);

	FHitResult OutHitResult;
	// 현재 하드코딩이지만 Attribute를 통해 얻을 수 있음
	const float AttackRange = 100.0f;
	const float AttackRadius = 50.0f;

	FCollisionQueryParams Params(SCENE_QUERY_STAT(UABTA_Trace), false, Character);
	const FVector Forward = Character->GetActorForwardVector();
	const FVector Start = Character->GetActorLocation() + Forward * Character->GetCapsuleComponent()->GetScaledCapsuleRadius();
	const FVector End = Start + Forward * AttackRange;

	bool bHitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);

	FGameplayAbilityTargetDataHandle DataHandle;

	if (bHitDetected)
	{
		FGameplayAbilityTargetData_SingleTargetHit* TargetData = new FGameplayAbilityTargetData_SingleTargetHit(OutHitResult);
		DataHandle.Add(TargetData);
	}

#if ENABLE_DRAW_DEBUG
	if (bDrawDebug)
	{
		const FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
		const float CapsuleHalfHeight = AttackRange * 0.5f;
		const FColor DrawColor = bHitDetected ? FColor::Green : FColor::Red;

		DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(Forward).ToQuat(), DrawColor);
	}
#endif

	return DataHandle;
}

 

UABGA_AttackHitCheck::UABGA_AttackHitCheck() 
{
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
	
	UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
	AttackTraceTask->OnComplete.AddDynamic(this, &ThisClass::OnTraceResultCallback);
	AttackTraceTask->ReadyForActivation();
}

void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle) 
{
	bool bReplicateEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}

콤보 공격 구현

강의에서는 Prefix와 Index로 다음 Section Montage를 설정했지만, 어빌리티에 ComboActionData들을 저장하는 것이 좋아보임

void UABGA_Attack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
	
	AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
	ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

	CurrentComboData = ABCharacter->GetComboActionData();
	CurrentCombo = 0;
	HasNextComboInput = false;

	UAbilityTask_PlayMontageAndWait* PlayAttackTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("PlayAttack"), ABCharacter->GetComboActionMontage(), 1.0f, GetNextSection());
	PlayAttackTask->OnCompleted.AddDynamic(this, &UABGA_Attack::OnCompleteCallback);
	PlayAttackTask->OnInterrupted.AddDynamic(this, &UABGA_Attack::OnInterruptedCallback);
	PlayAttackTask->ReadyForActivation();
	
	StartComboTimer();
}

void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
	HasNextComboInput = true;
}

FName UABGA_Attack::GetNextSection()
{
	CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, CurrentComboData->MaxComboCount);

	return *FString::Printf(TEXT("%s%d"), *CurrentComboData->MontageSectionNamePrefix, CurrentCombo);
}

void UABGA_Attack::StartComboTimer()
{
	const uint8 ComboIndex = CurrentCombo - 1;
	ensure(CurrentComboData->EffectiveFrameCount.IsValidIndex(ComboIndex));

	const float ComboEffectiveTime = CurrentComboData->EffectiveFrameCount[ComboIndex] / CurrentComboData->FrameRate;
	if (ComboEffectiveTime > 0.0f)
	{
		GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &ThisClass::CheckComboInput, ComboEffectiveTime, false);
	}
}

void UABGA_Attack::CheckComboInput()
{
	ComboTimerHandle.Invalidate();

	if (HasNextComboInput)
	{
		MontageJumpToSection(GetNextSection());
		StartComboTimer();
		HasNextComboInput = false;
	}
}

 

어빌리티 태스크(AT)의 제작 팁

AT는 UAbilityTask 클래스를 상속 받아 제작

AT 인스턴스를 생성해 반환하는 static 함수를 선언해 구현

AT가 종료되면 GA에 알려줄 델리게이트를 선언하고 종료될 때 델리게이트를 브로드캐스팅

시작과 종료 처리를 위해 Activate와 OnDestroy 함수를 Override

일정 시간이 지난 후 AT를 종료하고자 한다면 활성화 시 SetWaitingOnAvatar 함수를 호출해 Waiting 상태로 설정

만약 Tick을 활성화하고 싶다면 bTickingTask 값을 true로 설정

 

GameplayAbility와 AbilityTask의 실행 흐름

아래 흐름 외에 어빌리티가 종료돼서 AT가 종료되기도 함

 

블루프린트에서 호출을 위한 제작 규칙

static 함수에 UFUNCTION(BlueprintCallable)을 지정

콜백을 위한 델리게이트는 Dynamic Delegate로 선언

AT의 델리게이트에 UPROPERTY(BlueprintAssingable)을 지정

 

UABGA_Jump::UABGA_Jump()
{
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

void UABGA_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
	
	// UABAT_JumpAndWaitForLanding* JumpAndWaitForLandingTask = UABAT_JumpAndWaitForLanding::CreateTask(this);
	// JumpAndWaitForLandingTask->OnComplete.AddDynamic(this, &ThisClass::OnLandedCallback);
	// JumpAndWaitForLandingTask->ReadyForActivation();
}

void UABGA_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
	Super::InputReleased(Handle, ActorInfo, ActivationInfo);

	ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
	Character->StopJumping();
}

bool UABGA_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const
{
	if (Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags) == false)
	{
		return false;
	}

	const ACharacter* Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
	return Character && Character->CanJump();
}

void UABGA_Jump::OnLandedCallback() 
{
	bool bReplicateEndAbility = false;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}

 

UABAT_JumpAndWaitForLanding::UABAT_JumpAndWaitForLanding()
{}

UABAT_JumpAndWaitForLanding* UABAT_JumpAndWaitForLanding::CreateTask(UGameplayAbility* OwningAbility)
{
	UABAT_JumpAndWaitForLanding* NewTask = NewAbilityTask<UABAT_JumpAndWaitForLanding>(OwningAbility);
	return NewTask;
}

void UABAT_JumpAndWaitForLanding::Activate()
{
	Super::Activate();

	ACharacter* Avatar = CastChecked<ACharacter>(GetAvatarActor());
	Avatar->LandedDelegate.AddDynamic(this, &ThisClass::OnLandedCallback);
	Avatar->Jump();

	SetWaitingOnAvatar();
}

void UABAT_JumpAndWaitForLanding::OnDestroy(bool bInOwnerFinished)
{
	ACharacter* Avatar = CastChecked<ACharacter>(GetAvatarActor());
	Avatar->LandedDelegate.RemoveDynamic(this, &ThisClass::OnLandedCallback);

	Super::OnDestroy(bInOwnerFinished);
}

void UABAT_JumpAndWaitForLanding::OnLandedCallback(const FHitResult& Hit)
{
	if (ShouldBroadcastAbilityTaskDelegates())
	{
		OnComplete.Broadcast();
	}
}

+ Recent posts

목차