[UE GAS] 캐릭터의 광역 스킬 구현
캐릭터에 다양한 기능을 가진 광역 공격 스킬을 부여하기
- 플레이어 캐릭터에 새로운 스킬 GA를 부여하고, 범위 공격을 추가하도록 설계를 확장하기
- 새로운 어트리뷰트 세트를 추가해 확장하기
- GA에 사용 비용과 쿨다운의 제약 사항 추가하기
- 게임플레이 이펙트 실행 계산 클래스를 사용해 거리에 따른 광역 데미지 감소 적용하기
인프런 이득우님의 '언리얼 프로그래밍 Part4 - 게임플레이 어빌리티 시스템' 강의를 참고하였습니다.
😎 [이득우의 언리얼 프로그래밍 Part4 - 게임플레이 어빌리티 시스템] 강의 들으러 가기!
목차
기획
플레이어 캐릭터에 신규 광역 스킬 부여
- 무기를획득하면 마우스 우클릭으로 스킬을 사용할 수 있는 기능을 추가
- 스킬 반경과 데미지 계산은 새로운 어트리뷰트 값을 사용 ( SkillRange, SkillAttackRate )
- 스킬은 광역스킬이며, 다수의 액터에게 피해를 전달하고 폭발장식 이펙트를 발생시킴
- 스킬에는 에너지를 소비하며, 에너지가 모자라면 발동이 안됨 ( SkillEnergy )
- 스킬 시전 후 쿨다운이 발동되며, 쿨다운이 끝날 때까지 발동이 안됨
- 스킬이 주는 데미지는공격자와의 거리에 따라 선형적으로 감소함.
핵심 컴포넌트
새로운 범위 공격 스킬 부여
ABGASCharacterPlayer - 몽타주 에셋 추가
ABGASCharacterPlayer.h
#pragma once
#include "CoreMinimal.h"
#include "Character/ABCharacterPlayer.h"
#include "AbilitySystemInterface.h"
#include "Abilities/GameplayAbilityTypes.h"
#include "ABGASCharacterPlayer.generated.h"
UCLASS()
class ARENABATTLEGAS_API AABGASCharacterPlayer : public AABCharacterPlayer, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AABGASCharacterPlayer();
FORCEINLINE virtual class UAnimMontage* GetSkillActionMontage() const { return SkillActionMontage; }
virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
virtual void PossessedBy(AController* NewController) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
protected:
void SetupGASInputComponent();
void GASInputPressed(int32 InputId);
void GASInputReleased(int32 InputId);
UFUNCTION()
virtual void OnOutOfHealth();
void EquipWeapon(const FGameplayEventData* EventData);
void UnequipWeapon(const FGameplayEventData* EventData);
UPROPERTY(EditAnywhere, Category = GAS)
TObjectPtr<class UAbilitySystemComponent> ASC;
UPROPERTY(EditAnywhere, Category = GAS)
TArray<TSubclassOf<class UGameplayAbility>> StartAbilities;
UPROPERTY(EditAnywhere, Category = GAS)
TMap<int32, TSubclassOf<class UGameplayAbility>> StartInputAbilities;
UPROPERTY(VisibleAnywhere)
TObjectPtr<class UABGASWidgetComponent> HpBar;
UPROPERTY(EditAnywhere, Category = Weapon)
TObjectPtr<class USkeletalMesh> WeaponMesh;
UPROPERTY(EditAnywhere, Category = Weapon)
float WeaponRange;
UPROPERTY(EditAnywhere, Category = Weapon)
float WeaponAttackRate;
UPROPERTY(EditAnywhere, Category = GAS)
TSubclassOf<class UGameplayAbility> SkillAbilityClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Animation)
TObjectPtr<class UAnimMontage> SkillActionMontage; // 스킬 몽타주
};
인라인 함수 추가
- FORCEINLINE virtual class UAnimMontage* GetSkillActionMontage() const { return SkillActionMontage; }
스킬 몽타주 변수 추가
- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Animation)
TObjectPtr<class UAnimMontage> SkillActionMontage;
ABGASCharacterPlayer.cpp
#include "Character/ABGASCharacterPlayer.h"
#include "AbilitySystemComponent.h"
#include "Player/ABGASPlayerState.h"
#include "EnhancedInputComponent.h"
#include "UI/ABGASWidgetComponent.h"
#include "UI/ABGASUserWidget.h"
#include "Attribute/ABCharacterAttributeSet.h"
#include "Tag/ABGameplayTag.h"
AABGASCharacterPlayer::AABGASCharacterPlayer()
{
ASC = nullptr;
static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_ComboAttack.AM_ComboAttack'"));
if (ComboActionMontageRef.Object)
{
ComboActionMontage = ComboActionMontageRef.Object;
}
HpBar = CreateDefaultSubobject<UABGASWidgetComponent>(TEXT("Widget"));
HpBar->SetupAttachment(GetMesh());
HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f));
static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_HpBar.WBP_HpBar_C"));
if (HpBarWidgetRef.Class)
{
HpBar->SetWidgetClass(HpBarWidgetRef.Class);
HpBar->SetWidgetSpace(EWidgetSpace::Screen);
HpBar->SetDrawSize(FVector2D(200.0f, 20.f));
HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
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;
static ConstructorHelpers::FObjectFinder<UAnimMontage> SKillActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_SkillAttack.AM_SkillAttack'"));
if (SKillActionMontageRef.Object)
{
SkillActionMontage = SKillActionMontageRef.Object;
}
}
UAbilitySystemComponent* AABGASCharacterPlayer::GetAbilitySystemComponent() const
{
return ASC;
}
void AABGASCharacterPlayer::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
AABGASPlayerState* GASPS = GetPlayerState<AABGASPlayerState>();
if (GASPS)
{
ASC = GASPS->GetAbilitySystemComponent();
ASC->InitAbilityActorInfo(GASPS, this);
ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONEQUIP).AddUObject(this, &AABGASCharacterPlayer::EquipWeapon);
ASC->GenericGameplayEventCallbacks.FindOrAdd(ABTAG_EVENT_CHARACTER_WEAPONUNEQUIP).AddUObject(this, &AABGASCharacterPlayer::UnequipWeapon);
const UABCharacterAttributeSet* CurrentAttributeSet = ASC->GetSet<UABCharacterAttributeSet>();
if (CurrentAttributeSet)
{
CurrentAttributeSet->OnOutOfHealth.AddDynamic(this, &ThisClass::OnOutOfHealth);
}
for (const auto& StartAbility : StartAbilities)
{
FGameplayAbilitySpec StartSpec(StartAbility);
ASC->GiveAbility(StartSpec);
}
for (const auto& StartInputAbility : StartInputAbilities)
{
FGameplayAbilitySpec StartSpec(StartInputAbility.Value);
StartSpec.InputID = StartInputAbility.Key;
ASC->GiveAbility(StartSpec);
}
SetupGASInputComponent();
APlayerController* PlayerController = CastChecked<APlayerController>(NewController);
PlayerController->ConsoleCommand(TEXT("showdebug abilitysystem"));
}
}
void AABGASCharacterPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
SetupGASInputComponent();
}
void AABGASCharacterPlayer::SetupGASInputComponent()
{
if (IsValid(ASC) && IsValid(InputComponent))
{
UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 0);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AABGASCharacterPlayer::GASInputReleased, 0);
EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 1);
EnhancedInputComponent->BindAction(SkillAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 2);
}
}
void AABGASCharacterPlayer::GASInputPressed(int32 InputId)
{
FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
if (Spec)
{
Spec->InputPressed = true;
if (Spec->IsActive())
{
ASC->AbilitySpecInputPressed(*Spec);
}
else
{
ASC->TryActivateAbility(Spec->Handle);
}
}
}
void AABGASCharacterPlayer::GASInputReleased(int32 InputId)
{
FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
if (Spec)
{
Spec->InputPressed = false;
if (Spec->IsActive())
{
ASC->AbilitySpecInputReleased(*Spec);
}
}
}
void AABGASCharacterPlayer::OnOutOfHealth()
{
SetDead(); // 사망 처리
}
void AABGASCharacterPlayer::EquipWeapon(const FGameplayEventData* EventData)
{
if (Weapon)
{
Weapon->SetSkeletalMesh(WeaponMesh);
FGameplayAbilitySpec NewSkillSpec(SkillAbilityClass);
NewSkillSpec.InputID = 2;
if (!ASC->FindAbilitySpecFromClass(SkillAbilityClass))
{
ASC->GiveAbility(NewSkillSpec);
}
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);
FGameplayAbilitySpec* SkillAbilitySpec = ASC->FindAbilitySpecFromClass(SkillAbilityClass);
if (SkillAbilitySpec)
{
ASC->ClearAbility(SkillAbilitySpec->Handle);
}
Weapon->SetSkeletalMesh(nullptr);
}
}
스킬 몽타주 에셋 설정
- AABGASCharacterPlayer::AABGASCharacterPlayer()
- static ConstructorHelpers::FObjectFinder<UAnimMontage> SKillActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_SkillAttack.AM_SkillAttack'"));
- if (SKillActionMontageRef.Object)
- SkillActionMontage = SKillActionMontageRef.Object;
무기 장착시 스킬 부여하기
- if (Weapon)
- FGameplayAbilitySpec NewSkillSpec(SkillAbilityClass);
- NewSkillSpec.InputID = 2;
- if (!ASC->FindAbilitySpecFromClass(SkillAbilityClass))
- ASC->GiveAbility(NewSkillSpec);
무기 탈착시 스킬 해제하기
- if (Weapon)
- FGameplayAbilitySpec* SkillAbilitySpec = ASC->FindAbilitySpecFromClass(SkillAbilityClass);
- if (SkillAbilitySpec)
- ASC->ClearAbility(SkillAbilitySpec->Handle);
BP_ABGASCharacterPlayer
ABGA_Skill + BPGA_Skill생성
GameAbility - ABGA_Skill 생성
ABGA_Skill.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "ABGA_Skill.generated.h"
UCLASS()
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;
};
ABGA_Skill.cpp
BPGA_Skill 생성
발동방지를 위해 Gameplay Tag 추가
BPGA_Attack / BPGA_CharacterJump
- Activation Blocked Tags : 지정한 태그일 때 해당 GA를 발동시키지 않는다
실행화면
애니메이션 노티파이 이벤트설정
AM_SkillAttack에 AnimNotify_GASAttackHitCheck 추가
Trigger Gameplay Tag
- Event.Character.Action.SkillHitCheck로 설정
BPGA_AttackHitCheck에 Ability Trigger 추가 +
BPGA_SkillHitCheck에 Ability Trigger 추가
각각의 Trigger - Ability Triggers
- Trigger Tag : Event.Character.Action.AttackHitCheck / Event.Character.Action.SkillHitCheck 태그 추가
BP_ABGASCharacterPlayer에 BPGA_SkillHitCheck 등록하
GAS - Start Abilities
- BPGA_SkillHitCheck 추가
실행화면
스킬 공격을 하면 아래에 SkillHitCheck 로그가 찍힌다.
범위 공격 감지하기
ABGA_AttackHitCheck
ABGA_AttackHitCheck.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "ABGA_AttackHitCheck.generated.h"
UCLASS()
class ARENABATTLEGAS_API UABGA_AttackHitCheck : public UGameplayAbility
{
GENERATED_BODY()
public:
UABGA_AttackHitCheck();
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
protected:
UFUNCTION()
void OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle);
UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class UGameplayEffect> AttackDamageEffect;
UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class UGameplayEffect> AttackBuffEffect;
float CurrentLevel; // CurveTable의 현재레벨 변수
UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class AABTA_Trace> TargetActorClass;
};
변수 추가
- UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class AABTA_Trace> TargetActorClass;
ABGA_AttackHitCheck.cpp
#include "GA/ABGA_AttackHitCheck.h"
#include "ArenaBattleGAS.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "GA/AT/ABAT_Trace.h"
#include "GA/TA/ABTA_Trace.h"
#include "Attribute/ABCharacterAttributeSet.h"
#include "ArenaBattleGAS.h"
#include "Tag/ABGameplayTag.h"
UABGA_AttackHitCheck::UABGA_AttackHitCheck()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
ABGAS_LOG(LogABGAS, Log, TEXT("%s"), *TriggerEventData->EventTag.GetTagName().ToString());
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
CurrentLevel = TriggerEventData->EventMagnitude;
UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass);
AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback);
AttackTraceTask->ReadyForActivation();
}
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
// TargetDataHandle는 배열로 들어오기 때문에 0번 인덱스(=첫번째 데이터)에 이것이 있는지 확인
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0)) // 있다면
{
FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));
UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked(); // 소스에서 타겟으로 넘겨야 하므로 체크로 확인한 AbilitySystemComponent를 가져옴
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor()); // 물리 판정을 감지한 타겟 액터
const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>();
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);
}
FGameplayEffectSpecHandle BuffEffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackBuffEffect);
if (BuffEffectSpecHandle.IsValid())
{
ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, BuffEffectSpecHandle);
}
}
bool bReplicatedEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}
void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
- 변경 전: UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
- 변경 후: UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass);
ABTA_SphereMultiTrace 생성
ABTA_Trace - ABTA_SphereMultiTrace 생성
ABTA_SphereMultiTrace.h
#pragma once
#include "CoreMinimal.h"
#include "GA/TA/ABTA_Trace.h"
#include "ABTA_SphereMultiTrace.generated.h"
UCLASS()
class ARENABATTLEGAS_API AABTA_SphereMultiTrace : public AABTA_Trace
{
GENERATED_BODY()
protected:
virtual FGameplayAbilityTargetDataHandle MakeTargetData() const override;
};
ABTA_SphereMultiTrace.cpp
#include "GA/TA/ABTA_SphereMultiTrace.h"
#include "ArenaBattleGAS.h"
#include "Abilities/GameplayAbility.h"
#include "GameFramework/Character.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "Physics/ABCollision.h"
#include "DrawDebugHelpers.h"
#include "Attribute/ABCharacterSkillAttributeSet.h"
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();
}
TArray<FOverlapResult> Overlaps; // Sphere와 겹친 액터들의 정보를 담는 배열
const float SkillRadius = 600.f; // 탐지할 Sphere의 반지름
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); // 겹침연산 후 결과가 Overlaps 배열 변수에 저장됨
TArray<TWeakObjectPtr<AActor>> HitActors; // 겹친 연산이 일어난 액터들을 담는 배열. 아래의 ActorData->SetActors()의 인자로 넘길 때 TWeakObjectPtr 형태여야 해서 TWeakObjectPtr를 사용하였다.
for (const FOverlapResult& Overlap : Overlaps)
{
AActor* HitActor = Overlap.OverlapObjectHandle.FetchActor<AActor>();
if (HitActor && !HitActors.Contains(HitActor))
{
HitActors.Add(HitActor);
}
}
// 다수의 액터를 보관하는 TargetData 타입인 FGameplayAbilityTargetData_ActorArray 변수 생성 후 HitActors를 담는다
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);
}
BPGA_SkillHitCheck에 ABTA_SphereMultiTrace 지정
실행화면
다수 NPC에 데미지 전달 및 장식 이펙트 부여
ABGA_AttackHitCheck
ABGA_AttackHitCheck.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "ABGA_AttackHitCheck.generated.h"
UCLASS()
class ARENABATTLEGAS_API UABGA_AttackHitCheck : public UGameplayAbility
{
GENERATED_BODY()
public:
UABGA_AttackHitCheck();
virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
protected:
UFUNCTION()
void OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle);
UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class UGameplayEffect> AttackDamageEffect;
UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class UGameplayEffect> AttackBuffEffect;
float CurrentLevel; // CurveTable의 현재레벨 변수
UPROPERTY(EditAnywhere, Category = "GAS")
TSubclassOf<class AABTA_Trace> TargetActorClass;
};
변경사항 없음
ABGA_AttackHitCheck.cpp
#include "GA/ABGA_AttackHitCheck.h"
#include "ArenaBattleGAS.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "GA/AT/ABAT_Trace.h"
#include "GA/TA/ABTA_Trace.h"
#include "Attribute/ABCharacterAttributeSet.h"
#include "ArenaBattleGAS.h"
#include "Tag/ABGameplayTag.h"
UABGA_AttackHitCheck::UABGA_AttackHitCheck()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
ABGAS_LOG(LogABGAS, Log, TEXT("%s"), *TriggerEventData->EventTag.GetTagName().ToString());
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
CurrentLevel = TriggerEventData->EventMagnitude;
UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, TargetActorClass);
AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback);
AttackTraceTask->ReadyForActivation();
}
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
// TargetDataHandle는 배열로 들어오기 때문에 0번 인덱스(=첫번째 데이터)에 이것이 있는지 확인
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0)) // 있다면
{
FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0);
ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));
UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked(); // 소스에서 타겟으로 넘겨야 하므로 체크로 확인한 AbilitySystemComponent를 가져옴
UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(HitResult.GetActor()); // 물리 판정을 감지한 타겟 액터
const UABCharacterAttributeSet* SourceAttribute = SourceASC->GetSet<UABCharacterAttributeSet>();
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);
}
FGameplayEffectSpecHandle BuffEffectSpecHandle = MakeOutgoingGameplayEffectSpec(AttackBuffEffect);
if (BuffEffectSpecHandle.IsValid())
{
ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, BuffEffectSpecHandle);
}
}
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);
}
}
bool bReplicatedEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
- 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);
- UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo_Checked();
ABGC_AttackHit
ABGC_AttackHit.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayCueNotify_Static.h"
#include "ABGC_AttackHit.generated.h"
UCLASS()
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;
};
변경사항 없음
ABGC_AttackHit.cpp
#include "GC/ABGC_AttackHit.h"
#include "Particles/ParticleSystem.h"
#include "Kismet/GameplayStatics.h"
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); // 충돌 위치에 스폰
}
else // HitResult가 없고, 다수의 액터 정보가 들어올 때(=범위 공격 피격들)
{
for (const auto& TargetActor : Parameters.EffectContext.Get()->GetActors()) // 피격된 모든 TargetActor들에 대해
{
if (TargetActor.Get())
{
UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, TargetActor.Get()->GetActorLocation(), FRotator::ZeroRotator, true); // TargetActor에 이펙트 스폰
}
}
}
return false;
}
bool UABGC_AttackHit::OnExecute_Implementation(AActor* Target, const FGameplayCueParameters& Parameters) const
- else // HitResult가 없고, 다수의 액터 정보가 들어올 때(=범위 공격 피격들)
- for (const auto& TargetActor : Parameters.EffectContext.Get()->GetActors())
- if (TargetActor.Get())
- UGameplayStatics::SpawnEmitterAtLocation(Target, ParticleSystem, TargetActor.Get()->GetActorLocation(), FRotator::ZeroRotator, true);
- if (TargetActor.Get())
- for (const auto& TargetActor : Parameters.EffectContext.Get()->GetActors())
실행화면
신규 어트리뷰트의 추가 및 활용
ABCharacterSkillAttributeSet 생성
AttributeSet - ABCharacterSkillAttributeSet 생성
ABCharacterSkillAttributeSet.h
#pragma once
#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "ABCharacterSkillAttributeSet.generated.h"
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)
UCLASS()
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;
};
ABCharacterSkillAttributeSet.cpp
#include "Attribute/ABCharacterSkillAttributeSet.h"
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());
}
}
ABGASPlayerState에 SkillAttributeSet 추가
ABGASPlayerState.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "AbilitySystemInterface.h"
#include "ABGASPlayerState.generated.h"
UCLASS()
class ARENABATTLEGAS_API AABGASPlayerState : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AABGASPlayerState();
virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
protected:
UPROPERTY(EditAnywhere, Category = GAS)
TObjectPtr<class UAbilitySystemComponent> ASC;
UPROPERTY()
TObjectPtr<class UABCharacterAttributeSet> AttributeSet;
UPROPERTY()
TObjectPtr<class UABCharacterSkillAttributeSet> SkillAttributeSet;
};
변수 추가
- UPROPERTY()
TObjectPtr<class UABCharacterSkillAttributeSet> SkillAttributeSet;
ABGASPlayerState.cpp
#include "Player/ABGASPlayerState.h"
#include "AbilitySystemComponent.h"
#include "Attribute/ABCharacterAttributeSet.h"
#include "Attribute/ABCharacterSkillAttributeSet.h"
AABGASPlayerState::AABGASPlayerState()
{
ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
//ASC->SetIsReplicated(true);
AttributeSet = CreateDefaultSubobject<UABCharacterAttributeSet>(TEXT("AttributeSet"));
SkillAttributeSet = CreateDefaultSubobject<UABCharacterSkillAttributeSet>(TEXT("SkillAttributeSet"));
}
UAbilitySystemComponent* AABGASPlayerState::GetAbilitySystemComponent() const
{
return ASC;
}
AABGASPlayerState::AABGASPlayerState()
- SkillAttributeSet = CreateDefaultSubobject<UABCharacterSkillAttributeSet>(TEXT("SkillAttributeSet"));
ABTA_SphereMultiTrace
ABTA_SphereMultiTrace.h
#pragma once
#include "CoreMinimal.h"
#include "GA/TA/ABTA_Trace.h"
#include "ABTA_SphereMultiTrace.generated.h"
UCLASS()
class ARENABATTLEGAS_API AABTA_SphereMultiTrace : public AABTA_Trace
{
GENERATED_BODY()
protected:
virtual FGameplayAbilityTargetDataHandle MakeTargetData() const override;
};
변경사항 없음
ABTA_SphereMultiTrace.cpp
#include "GA/TA/ABTA_SphereMultiTrace.h"
#include "ArenaBattleGAS.h"
#include "Abilities/GameplayAbility.h"
#include "GameFramework/Character.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "Physics/ABCollision.h"
#include "DrawDebugHelpers.h"
#include "Attribute/ABCharacterSkillAttributeSet.h"
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; // Sphere와 겹친 액터들의 정보를 담는 배열
const float SkillRadius = SkillAttribute->GetSkillRange(); // 탐지할 Sphere의 반지름을 SkillAttribute에서 가져옴
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); // 겹침연산 후 결과가 Overlaps 배열 변수에 저장됨
TArray<TWeakObjectPtr<AActor>> HitActors; // 겹친 연산이 일어난 액터들을 담는 배열. 아래의 ActorData->SetActors()의 인자로 넘길 때 TWeakObjectPtr 형태여야 해서 TWeakObjectPtr를 사용하였다.
for (const FOverlapResult& Overlap : Overlaps)
{
AActor* HitActor = Overlap.OverlapObjectHandle.FetchActor<AActor>();
if (HitActor && !HitActors.Contains(HitActor))
{
HitActors.Add(HitActor);
}
}
// 다수의 액터를 보관하는 TargetData 타입인 FGameplayAbilityTargetData_ActorArray 변수 생성 후 HitActors를 담는다
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);
}
FGameplayAbilityTargetDataHandle AABTA_SphereMultiTrace::MakeTargetData() const
- const UABCharacterSkillAttributeSet* SkillAttribute = ASC->GetSet<UABCharacterSkillAttributeSet>(); // SkillAttribute 가져오기
- const float SkillRadius = SkillAttribute->GetSkillRange();
BPGE_SkillCost 생성
스킬을 사용할 때마다 30씩 줄어든다
BPGE_SkillCooldown 생성
BPGA_Skill에 Cost와 Cooldown 지정
최종 데미지의 수동 계산
ABSkillDamageExecutionCalc 생성
GameplayEffectExecutionCalculation - ABSkillDamageExecutionCalc 생성
ABSkillDamageExecutionCalc.h
#pragma once
#include "CoreMinimal.h"
#include "GameplayEffectExecutionCalculation.h"
#include "ABSkillDamageExecutionCalc.generated.h"
UCLASS()
class ARENABATTLEGAS_API UABSkillDamageExecutionCalc : public UGameplayEffectExecutionCalculation
{
GENERATED_BODY()
public:
virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& ExecutionParams, FGameplayEffectCustomExecutionOutput& OutExecutionOutput) const override;
};
ABSkillDamageExecutionCalc.cpp
#include "GE/ABSkillDamageExecutionCalc.h"
#include "AbilitySystemComponent.h"
#include "Attribute/ABCharacterSkillAttributeSet.h"
#include "Attribute/ABCharacterAttributeSet.h"
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; // 최대 반경일수록 더 적은 데미지로 뒤집기위해 ratio 구하기
float Damage = InvDamageRatio * MaxDamage; // ratio를 곱해 최대 반경일수록 더 적고 중앙일수록 더 큰 데미지 적용
OutExecutionOutput.AddOutputModifier(FGameplayModifierEvaluatedData(UABCharacterAttributeSet::GetDamageAttribute(), EGameplayModOp::Additive, Damage)); // FGameplayModifierEvaluatedData 형태로 값을 넣어서 전달
}
}
}
BPGE_SkillDamage 수정
BPGA_SkillHitCheck 수정
실행화면
가까울수록 더 큰 데미지가 멀수록 더 작은 데미지가 적용된다.
정리
- 무기를 들 때 발동가능한, 마우스 우클릭으로 발동하는 범위 스킬의 구현
- 다중 충돌 감지 기능을 수행하는 타겟 액터의 구현과 활용
- 명중한 다수의 타겟에 데미지를 전달하고 장식 이펙트를 발동하는 기능 구현
- 스킬 전용 어트리뷰트 클래스를 활용한 스킬 사용 비용과 쿨다운 기능 구현
- 이펙트 실행 계산 클래스를 활용한 거리에 따른 데미지 감소 구현
'⭐ Unreal Engine > UE Game Ability System(GAS)' 카테고리의 다른 글
[UE GAS] 아이템 상자 구현 (0) | 2024.03.10 |
---|---|
[UE GAS] 어트리뷰트와 UI 연동 Integration with Attribute and UI (0) | 2024.03.09 |
[UE GAS] 게임플레이 이펙트의 활용 Applying Gameplay Effect (0) | 2024.03.08 |
[UE GAS] 캐릭터 어트리뷰트 설정 (0) | 2024.03.07 |
[UE GAS] 공격 판정 시스템의 구현 (0) | 2024.03.06 |
댓글
이 글 공유하기
다른 글
-
[UE GAS] 아이템 상자 구현
[UE GAS] 아이템 상자 구현
2024.03.10 -
[UE GAS] 어트리뷰트와 UI 연동 Integration with Attribute and UI
[UE GAS] 어트리뷰트와 UI 연동 Integration with Attribute and UI
2024.03.09 -
[UE GAS] 게임플레이 이펙트의 활용 Applying Gameplay Effect
[UE GAS] 게임플레이 이펙트의 활용 Applying Gameplay Effect
2024.03.08 -
[UE GAS] 캐릭터 어트리뷰트 설정
[UE GAS] 캐릭터 어트리뷰트 설정
2024.03.07