개발 이야기 안하는 개발자

언리얼5_4_3 : 게임플레이 어빌리티 시스템의 활용 방법 본문

Unreal/이득우의 언리얼 프로그래밍

언리얼5_4_3 : 게임플레이 어빌리티 시스템의 활용 방법

07e 2024. 3. 27. 20:06
반응형

게임 플레이 큐(Cue) _GC라고 부름

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

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

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

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

블루프린트로 제작하는 게 더 좋음.

게임플레이 이펙트에서 자동으로 GC와 연동할 수 있도록 기능을 제공하고 있음.

게임플레이 큐의 재생은 GameplayCueManager가 관리함(분리된 구조)

게임플레이 태그를 사용해 쉽게 발동시킬수 있음.

- 이때, 반드시 GameplayCue로 시작하는 게임플레이 태그를 사용해야 함.

 

공격에 성공했을때 이펙트가 터지도록 제작할 예정.

GameplayCueNotify_Static을 상속받아서 제작.

class ARENABATTLEGAS_API UABGC_AttackHit : public UGameplayCueNotify_Static
{
	GENERATED_BODY()
	
public:
	UABGC_AttackHit();

	virtual bool OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const override;

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=GameplayCue)
	TObjectPtr<class UParticleSystem> ParticleSystem;
	
};

터트릴 파티클을 받아와야함.

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* Target, const FGameplayCueParameters& Parameters) const
{
	const FHitResult* HitResult = Parameters.EffectContext.GetHitResult();
	if (HitResult)
	{
		UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, HitResult->ImpactPoint, FRotator::ZeroRotator, true);
	}

	return false;
}

OnExecute를 통해서 공격에 성공한 경우 실행할 예정이다.

파티클을 타격 위치에 배치한다.

 

태그를 사용할건데, 만약 GC에서 사용하는 태그라면 반드시 GameplayCue 하위에 존재해야 한다.

제작한건 AttackHit과 Dmaage, OPen으로 모두 Header에서 define을 한다.

#define ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT FGameplayTag::RequestGameplayTag(FName("GameplayCue.Character.AttackHit"))

#define ABTAG_EVENT_CHARACTER_WEAPONEQUIP FGameplayTag::RequestGameplayTag(FName("Event.Character.Weapon.Equip"))
#define ABTAG_EVENT_CHARACTER_WEAPONUNEQUIP FGameplayTag::RequestGameplayTag(FName("Event.Character.Weapon.Unequip"))
		UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor());
		FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
		if (EffectSpecHandle.IsValid())
		{
			//EffectSpecHandle.Data->SetSetByCallerMagnitude(ABTAG_DATA_DAMAGE, -SourceAttribute->GetAttackRate());			
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);

			FGameplayEffectContextHandle CueContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle);
			CueContextHandle.AddHitResult(HitResult);
			FGameplayCueParameters CueParam;
			CueParam.EffectContext = CueContextHandle;

			TargetASC->ExecuteGameplayCue(ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT, CueParam);
		}

대미지를 받을때 호출할 내용이라서 GA_AttackHitCheck에 해당 내용을 수정해 준다.

큐컨텍트핸들을 제작하고 여기에 HitResult 데이터를 넣는다. 그상태로 CueParam를 생성해서 EffectContext에 담는다.

TargetASC에서 큐를 실행해준다.

대미지를 받은 대상이 맞았다는 효과를 낼 것이다.

 

GC_AttackHit을 상속받는 블루프린트를 제작해준다.

간혈적으로 블루프린트가 아 닌경우에 언리얼이 못읽어올때가 있어서 블루프린트로 만드는 것이 좋다.

해당 큐는 어딘가에 넣는게 아니고 이상태로 종료인데,

GC는 독립적 실행이라서 이렇게 태그만 지정해 놓으면 에디터가 알아서 가져와 호출한다.

 

아이템상자를 만들어 상호작용기능을 제작한다.

Actor를 상속받고 Gas로 작업이 진행되게 제작할 것이다.

class ARENABATTLEGAS_API AABGASItemBox : public AActor, public IAbilitySystemInterface
{
	GENERATED_BODY()
	
public:	
	AABGASItemBox();

	virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
	virtual void NotifyActorBeginOverlap(class AActor* Other) override;

protected:
	virtual void PostInitializeComponents() override;

	void ApplyEffectToTarget(AActor* Target);
	void InvokeGameplayCue(AActor* Target);

protected:
	UPROPERTY()
	TObjectPtr<class UAbilitySystemComponent> ASC;

	UPROPERTY(VisibleAnywhere, Category=Box)
	TObjectPtr<class UBoxComponent> Trigger;

	UPROPERTY(VisibleAnywhere, Category = Box)
	TObjectPtr<class UStaticMeshComponent> Mesh;

	UPROPERTY(EditAnywhere, Category = GAS)
	TSubclassOf<class UGameplayEffect> GameplayEffectClass;

	UPROPERTY(EditAnywhere, Category = GAS, Meta=(Categories=GameplayCue))
	FGameplayTag GameplayCueTag;
};

GAS를 사용할 예정이라 ASC를 제작한다.

상자 상호작용 기능을 구현할 예정이라 Trggier BoxCollider를 제작한다.

상호작용했을때 나올 이펙트를 추가해주어야 하고 필요한 태그도 정의한다.

 

 

AABGASItemBox::AABGASItemBox()
{
	ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("Trigger"));
	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));

	RootComponent = Trigger;
	Mesh->SetupAttachment(Trigger);

	Trigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
	Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));

	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"));
}

UAbilitySystemComponent* AABGASItemBox::GetAbilitySystemComponent() const
{
	return ASC;
}
void AABGASItemBox::PostInitializeComponents()
{
	Super::PostInitializeComponents();
	ASC->InitAbilityActorInfo(this, this);
}

기본적은 콜라이더와 사용할 ASC를 정의한다.

 

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

	InvokeGameplayCue(Other);
	ApplyEffectToTarget(Other);

	Mesh->SetHiddenInGame(true);	//숨김
	SetActorEnableCollision(false);	//더이상 작동 안하게
	SetLifeSpan(2.0f);	//2초뒤에 파괴
}

다른 무언가와 충돌했을때 메소드들을 호출한다.

void AABGASItemBox::ApplyEffectToTarget(AActor* Target)
{
	UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(Target);
	if (TargetASC)
	{
		FGameplayEffectContextHandle EffectContext = TargetASC->MakeEffectContext();
		EffectContext.AddSourceObject(this);
		FGameplayEffectSpecHandle EffectSpecHandle = TargetASC->MakeOutgoingSpec(GameplayEffectClass, 1, EffectContext);
		if (EffectSpecHandle.IsValid())
		{
			TargetASC->BP_ApplyGameplayEffectSpecToSelf(EffectSpecHandle);
		}
	}
}

충돌한 대상이 ASC를 가지고 있는 경우라면 Effect를 호출한다.

 

void AABGASItemBox::InvokeGameplayCue(AActor* Target)
{
	FGameplayCueParameters Param;
	Param.SourceObject = this;
	Param.Instigator = Target;
	Param.Location = GetActorLocation();
	ASC->ExecuteGameplayCue(GameplayCueTag, Param);
}

ASC의 효과를 주기위해서 Param을 제작하고, SourceObject(Owner)를 지정하고, Instigator(대상)을 지정하고, Location을 지정하다.

이렇게 하고 Execute를 진행해서 해당 박스가 가지고 있는 큐를 실행한다.

태그를 통해 실행하기 때문에 언리얼 에디터에서 태그를 보유한 GC를 제작해야 한다.

 

상속받을 대상은 Gameplay Cue Notify Static 이다.

태그를 지정하고 부모 작업을 진행하도록 Parent :OnExecute를 지정한다.

Parameters에 데이터를 쪼개서 거기에서 Location 정보만 가져온다.

Location의 위치에 이펙트가 재생하게 된다.

이펙트는 P_TreasureChest_Open_Mesh이다.

 

해당 기능은 이제 큐이고, 이펙트는 따로 제작해야 한다.

대미지를 입는 GE를 제작할 건데, 대미지를 입을때마다 효과를 주는 GC를 만들것이다.

단순하게 해당 큐가 호출되면 해당 액터 위치에서 파티클이 생성되는 큐이다.

태그를 부착하면 해당 태그가 호출되면 어디에서도 재생이 된다.

 

해당 GE는 대미지를 입는다라는 내용의 Effect이다

Display칸을 보면 아래쪽에 Gameplay Cues에서 추가할 수 있는데,

여기에 호출하는 Tag를 넣으면 방금 제작했던 GC가 호출된

블루프린트에는 Cue도 바로 넣을 수 있어서 상자를 먹으면 바로 데미지를 받으며 이펙트가 터지는 것을 볼 수 있다.

 

이건 체력을 채우는 GE이다

 

이건 도트데미지를 입는 GE이다.

제작했던 무적기능도 추가한다.

Infinite로 무제한 기능을 지속하며, GrantedTags를 통해서 Invinsible태그를 획득하게 된다.

전에 Widget에서 만들었던 델리게이트 등록으로 해당 태그를 획득하게 되면 체력바다 파란색으로 변하게 될것이다.

 

이것도 무적상태인데, 3초의 시간이 정해진 무적이다.

아래는 무적기능을 해제하는 GE이다.

 

상자에 무기를 넣어놓고, 상자를 먹으면 무기를 장착하도록 제작한다.

class ARENABATTLEGAS_API AABGASWeaponBox : public AABGASItemBox
{
	GENERATED_BODY()
	
protected:
	virtual void NotifyActorBeginOverlap(AActor* Other) override;

protected:
	UPROPERTY(EditAnywhere, Category = GAS, Meta=(Categories=Event))
	FGameplayTag WeaponEventTag;
};

제작한 ItemBox를 상속받는다.

이벤트 태그를 제작해서 이벤트를 보내줄 것이다.

void AABGASWeaponBox::NotifyActorBeginOverlap(AActor* Other)
{
	Super::NotifyActorBeginOverlap(Other);

	UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(Other, WeaponEventTag, FGameplayEventData());
}

FGameplayEventData()는 비어있는 내용이다.

 

태그를 통해서 이벤트를 보내기 때문에 해당 이벤트를 받도록 등록을 해놔야 한다.

Equip과 Unequip을 추가한다.

CharacterPlayer.cpp - 생성자
...
	static ConstructorHelpers::FObjectFinder<USkeletalMesh> WeaponMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/InfinityBladeWeapons/Weapons/Blunt/Blunt_Hellhammer/SK_Blunt_HellHammer.SK_Blunt_HellHammer'"));
	if (WeaponMeshRef.Object)
	{
		WeaponMesh = WeaponMeshRef.Object;
	}

	WeaponRange = 75.f;
	WeaponAttackRate = 100.0f;
...


void AABGASCharacterPlayer::PossessedBy(AController* NewController)
{
		...
		ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONEQUIP).AddUObject(this, &AABGASCharacterPlayer::EquipWeapon);
		ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONUNEQUIP).AddUObject(this, &AABGASCharacterPlayer::UnequipWeapon);
		...
}

장착태그와 탈착 태그가 호출되면 다음 메소드들이 등록되어 호출될 것이다.

void AABGASCharacterPlayer::EquipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		Weapon->SetSkeletalMesh(WeaponMesh);

		const float CurrentAttackRange = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute());
		const float CurrentAttackRate = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute());

		ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange + WeaponRange);
		ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate + WeaponAttackRate);
	}
}

void AABGASCharacterPlayer::UnequipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		const float CurrentAttackRange = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute());
		const float CurrentAttackRate = ASC->GetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute());

		ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRangeAttribute(), CurrentAttackRange - WeaponRange);
		ASC->SetNumericAttributeBase(UABCharacterAttributeSet::GetAttackRateAttribute(), CurrentAttackRate - WeaponAttackRate);

		Weapon->SetSkeletalMesh(nullptr);
	}

}

장착을 할 경우 기본 데이터에 영향을 주도록 Get과 Set을 통해 데이터를 변경했다.

이제 상자를 만들고 여기에 태그를 등록해 준다.

다른상자에서는 UnEquip을 제작한다.

 

 

이제 다음 기능들이 정상작동하는지 확인해 보면 모두 정상작동하는 것을 볼 수 있다.

 

캐릭터의 광역 스킬 구현해본다

CharacterPlayer.h
...
	FORCEINLINE virtual class UAnimMontage* GetSkillActionMontage() const { return SkillActionMontage; }

...
	UPROPERTY(EditAnywhere, Category = GAS)
	TSubclassOf<class UGameplayAbility> SkillAbilityClass;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Animation)
	TObjectPtr<class UAnimMontage> SkillActionMontage;
...

.cpp

//생성자
	static ConstructorHelpers::FObjectFinder<UAnimMontage> SKillActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_SkillAttack.AM_SkillAttack'"));
	if (SKillActionMontageRef.Object)
	{
		SkillActionMontage = SKillActionMontageRef.Object;
	}

...
EnhancedInputComponent->BindAction(SkillAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 2);
...

void AABGASCharacterPlayer::EquipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		Weapon->SetSkeletalMesh(WeaponMesh);

		FGameplayAbilitySpec NewSkillSpec(SkillAbilityClass);
		NewSkillSpec.InputID = 2;

		if (!ASC->FindAbilitySpecFromClass(SkillAbilityClass))
		{
			ASC->GiveAbility(NewSkillSpec);
		}
		...
	}
}

void AABGASCharacterPlayer::UnequipWeapon(const FGameplayEventData* EventData)
{
	if (Weapon)
	{
		...
		FGameplayAbilitySpec* SkillAbilitySpec = ASC->FindAbilitySpecFromClass(SkillAbilityClass);
		if (SkillAbilitySpec)
		{
			ASC->ClearAbility(SkillAbilitySpec->Handle);
		}
        ...
	}

}

GA로 스킬 클래스를 제작한다. 

스킬 모션을 담고있는 몽타주도 들고온다.

무기를 장착할때 스펙을 제작하고 아이디를 2로 지정하는데, 이때 ASC에서 아이디2번이 없다면 추가한다.

무기를 버릴때도 같이 찾아서 있으면 제거한다.

 

SkillAction은 마우스 우클릭이다.

 

 

스킬에 사용될 스킬코스트를 GE로 제작한다.

스킬에 사용될 스킬쿨타임도 GE로 제작한다.

스킬 쿨다운 상태를 추가해준다.

 

이제 스킬 GA를 제작한다.

class ARENABATTLEGAS_API UABGA_Skill : public UGameplayAbility
{
	GENERATED_BODY()
	
public:
	UABGA_Skill();

public:
	virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
	virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override;

protected:
	UFUNCTION()
	void OnCompleteCallback();

	UFUNCTION()
	void OnInterruptedCallback();

protected:
	UPROPERTY()
	TObjectPtr<class UAnimMontage> ActiveSkillActionMontage;
};
void UABGA_Skill::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

	AABGASCharacterPlayer* TargetCharacter = Cast<AABGASCharacterPlayer>(ActorInfo->AvatarActor.Get());
	if (!TargetCharacter)
	{
		return;
	}

	ActiveSkillActionMontage = TargetCharacter->GetSkillActionMontage();
	if (!ActiveSkillActionMontage)
	{
		return;
	}

	TargetCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

	UAbilityTask_PlayMontageAndWait* PlayMontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("SkillMontage"), ActiveSkillActionMontage, 1.0f);
	PlayMontageTask->OnCompleted.AddDynamic(this, &UABGA_Skill::OnCompleteCallback);
	PlayMontageTask->OnInterrupted.AddDynamic(this, &UABGA_Skill::OnInterruptedCallback);

	PlayMontageTask->ReadyForActivation();
}

void UABGA_Skill::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
	AABGASCharacterPlayer* TargetCharacter = Cast<AABGASCharacterPlayer>(ActorInfo->AvatarActor.Get());
	if (TargetCharacter)
	{
		TargetCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
	}

	Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}

void UABGA_Skill::OnCompleteCallback()
{
	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

void UABGA_Skill::OnInterruptedCallback()
{
	bool bReplicatedEndAbility = true;
	bool bWasCancelled = true;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

기본적인 GA를 만들고 본 내용은 블루프린트에서 제작한다.

공격을 하지 못하는 경우들을 태그해 추가하고, Cost와 Colldowns를 제작해준 2개의 GE로 넣어준다.

 

이제 애니메이션 노티파이를 추가해준다.

Notify를 찍고 그곳에 SkillHitCheck 태그로 지정한다.

 

SkillHitCheck도 필요하기 때문에 AttackHitCheck를 상속받는 블루프린트를 제작한다.

그 전에 SkillHitCheck를 진행할 TA를 제작해야 한다.

기존에 제작했던 TA처럼 AttributeSet을 먼저 정의한다.

class ARENABATTLEGAS_API UABCharacterSkillAttributeSet : public UAttributeSet
{
	GENERATED_BODY()
	
public:
	UABCharacterSkillAttributeSet();

	ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, SkillRange);
	ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, MaxSkillRange);
	ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, SkillAttackRate);
	ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, MaxSkillAttackRate);
	ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, SkillEnergy);
	ATTRIBUTE_ACCESSORS(UABCharacterSkillAttributeSet, MaxSkillEnergy);

	virtual void PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue) override;
	
protected:
	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData SkillRange;

	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxSkillRange;

	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData SkillAttackRate;

	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxSkillAttackRate;

	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData SkillEnergy;

	UPROPERTY(BlueprintReadOnly, Category = "Attack", Meta = (AllowPrivateAccess = true))
	FGameplayAttributeData MaxSkillEnergy;

};


.cpp

UABCharacterSkillAttributeSet::UABCharacterSkillAttributeSet() :
	SkillRange(800.0f),
	MaxSkillRange(1200.0f),
	SkillAttackRate(150.0f),
	MaxSkillAttackRate(300.0f),
	SkillEnergy(100.0f),
	MaxSkillEnergy(100.0f)
{
}

void UABCharacterSkillAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	Super::PreAttributeChange(Attribute, NewValue);

	if (Attribute == GetSkillRangeAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0.1f, GetMaxSkillRange());
	}
	else if (Attribute == GetSkillAttackRateAttribute())
	{
		NewValue = FMath::Clamp(NewValue, 0.0f, GetMaxSkillAttackRate());
	}
}

 

 

TA를 제작하기 앞서, AttackHitCheck 클래스에서 생성하는 TA의 로직을 외부에서 받아와서 생성하도록 수정한다.

	UPROPERTY(EditAnywhere, Category = "GAS")
	TSubclassOf<class AABTA_Trace> TargetActorClass;
    
    ...
    
    생성자
	UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass);
	AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback);
	AttackTraceTask->ReadyForActivation();

 

분리를 시키기 위해 만든거고 SkillTraceCheck를 위해서 AttackTract를 상속받아 제작한다.

class ARENABATTLEGAS_API AABTA_SphereMultiTrace : public AABTA_Trace
{
	GENERATED_BODY()

protected:
	virtual FGameplayAbilityTargetDataHandle MakeTargetData() const override;
};
FGameplayAbilityTargetDataHandle AABTA_SphereMultiTrace::MakeTargetData() const
{
	ACharacter* Character = CastChecked<ACharacter>(SourceActor);

	UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
	if (!ASC)
	{
		ABGAS_LOG(LogABGAS, Error, TEXT("ASC not found!"));
		return FGameplayAbilityTargetDataHandle();
	}

	const UABCharacterSkillAttributeSet* SkillAttribute = ASC->GetSet<UABCharacterSkillAttributeSet>();
	if (!SkillAttribute)
	{
		ABGAS_LOG(LogABGAS, Error, TEXT("SkillAttribute not found!"));
		return FGameplayAbilityTargetDataHandle();
	}

	TArray<FOverlapResult> Overlaps;
	const float SkillRadius = SkillAttribute->GetSkillRange();

	FVector Origin = Character->GetActorLocation();
	FCollisionQueryParams Params(SCENE_QUERY_STAT(AABTA_SphereMultiTrace), false, Character);
	GetWorld()->OverlapMultiByChannel(Overlaps, Origin, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(SkillRadius), Params);

	TArray<TWeakObjectPtr<AActor>> HitActors;
	for (const FOverlapResult& Overlap : Overlaps)
	{
		AActor* HitActor = Overlap.OverlapObjectHandle.FetchActor<AActor>();
		if (HitActor && !HitActors.Contains(HitActor))
		{
			HitActors.Add(HitActor);
		}
	}

	FGameplayAbilityTargetData_ActorArray* ActorsData = new FGameplayAbilityTargetData_ActorArray();
	ActorsData->SetActors(HitActors);

#if ENABLE_DRAW_DEBUG

	if (bShowDebug)
	{
		FColor DrawColor = HitActors.Num() > 0 ? FColor::Green : FColor::Red;
		DrawDebugSphere(GetWorld(), Origin, SkillRadius, 16, DrawColor, false, 5.0f);
	}

#endif

    return FGameplayAbilityTargetDataHandle(ActorsData);
}

비슷한 코드이고, 변경된 점은 콜라이더 충돌을 Multi로 변경되었다는 점이다.

MakeSphere를 통해 넓은 범위를 검색하는 로직이라고 보면 된다.

	
    ...
    else if (UAbilitySystemBlueprintLibrary::TargetDataHasActor(TargetDataHandle, 0))
	{
		UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();

		FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackDamageEffect, CurrentLevel);
		if (EffectSpecHandle.IsValid())
		{
			ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);

			FGameplayEffectContextHandle CueContextHandle = UAbilitySystemBlueprintLibrary::GetEffectContext(EffectSpecHandle);
			CueContextHandle.AddActors(TargetDataHandle.Data[0].Get()->GetActors(), false);
			FGameplayCueParameters CueParam;
			CueParam.EffectContext = CueContextHandle;

			SourceASC->ExecuteGameplayCue(ABTAG_GAMEPLAYCUE_CHARACTER_ATTACKHIT, CueParam);
		}
	}
    ...

AttackHitCheck.cpp에서 공격을 체크했을때 결과적으로 나오는 결과가 Actor가 있다면 Cue를 실행하도록 로직을 추가했다.

 

이를 통해 GA skill hit check에 TA를 제작한 SphereMultiTrace를 지정하면 된다.

GE도 SKillDamage로 변경하고, Trigger Tag를 SkillHitCheck로 지정하면 된다.

이 skillhitcheck는 몽타주에서 notify에서 호출한다.

 

ChacaterPalyer의 블루프린트이다. SkillAbilityClass에 제작한 BPGA_Skill을 추가한다. 또한 Start Abilities에 SKillHitCheck를 추가한다.

 

이렇게 하면 범위에 들어오는 적들을 모두 공격할 수 있다.

 

새로운 Attribute를 제작해보자

class ARENABATTLEGAS_API UABSkillDamageExecutionCalc : public UGameplayEffectExecutionCalculation
{
	GENERATED_BODY()
	
public:
	virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
void UABSkillDamageExecutionCalc::Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const
{
	Super::Execute_Implementation(ExecutionParams, OutExecutionOutput);

	UAbilitySystemComponent* SourceASC = ExecutionParams.GetSourceAbilitySystemComponent();
	UAbilitySystemComponent* TargetASC = ExecutionParams.GetTargetAbilitySystemComponent();

	if (SourceASC && TargetASC)
	{
		AActor* SourceActor = SourceASC->GetAvatarActor();
		AActor* TargetActor = TargetASC->GetAvatarActor();

		if (SourceActor && TargetActor)
		{
			const float MaxDamageRange = SourceASC->GetNumericAttributeBase(UABCharacterSkillAttributeSet::GetSkillRangeAttribute());
			const float MaxDamage = SourceASC->GetNumericAttributeBase(UABCharacterSkillAttributeSet::GetSkillAttackRateAttribute());
			const float Distance = FMath::Clamp(SourceActor->GetDistanceTo(TargetActor), 0.0f, MaxDamageRange);
			const float InvDamageRatio = 1.0f - Distance / MaxDamageRange;
			float Damage = InvDamageRatio * MaxDamage;

			OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(UABCharacterAttributeSet::GetDamageAttribute(), EGameplayModOp::Additive, Damage));
		}
	}
}

거리에 따라서 대미지가 다르게 적용되도록 제작한다.

 

사용하는 스킬인 SkillDamage GE에서 주는 데미지 방식을 위에서 수정해서 만든 Execution으로 변경한다.

 

태그를 보면 쿨타임중엔 스킬을 못쓰게 된다.

디버그로 원이 그려지고 원 안에 있는 적들은 데미지를 입게 된다.

거리에 맞춰 데미지가 적용되기 때문에 멀어지면 데미지를 조금만 받게 된다.

반응형