본문 바로가기
Unreal/GAS 토이프로젝트

[GameplayAbilitySystem 찍먹 프로젝트 - 2] Gameplay Ability System

by GameStudy 2023. 3. 26.

 

1. 준비사항

1.1 uproject 파일 수정

  - Edit > Plugins > Gameplay Ability System 플러그인 체크 후 .uproject 파일 수정

<hide/>

{
	"FileVersion": 3,
	"EngineAssociation": "5.1",
	"Category": "",
	"Description": "",
	"Modules": [
		{
			"Name": "TPSGAS",
			"Type": "Runtime",
			"LoadingPhase": "Default",
			"AdditionalDependencies": [
				"Engine",
				"GameplayAbilities"
			]
		}
	],
	"Plugins": [
		{
			"Name": "ModelingToolsEditorMode",
			"Enabled": true,
			"TargetAllowList": [
				"Editor"
			]
		},
		{
			"Name": "GameplayAbilities",
			"Enabled": true
		}   
	]
}

 

1.2 Build.cs 파일 수정

<hide/>

// GAS.Build.cs

using UnrealBuildTool;

public class GAS : ModuleRules
{
	public GAS(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
		
		PublicIncludePaths.Add("GAS/Public");
		PrivateIncludePaths.Add("GAS/Private");

		PublicDependencyModuleNames.AddRange(new string[]
		{
			"Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay",
			
			// Input
			"EnhancedInput",
			
			// GameplayAbilitySystem
			"GameplayAbilities", "GameplayTags", "GameplayTasks",
		});
	}
}

 

1.3 GASGlobal 헤더파일 수정

<hide/>

// GASGlobal.h

...

#define LIMIT (99999.f)

UENUM(BlueprintType)
enum class EGASAbilityInputID : uint8
{
	None,
	Confirm,
	Cancel,
	Idle,
	Jump,
	Move,
};

UENUM(BlueprintType)
enum class EGASAbilityID : uint8
{
	None,
	Idle,
	Walk,
	Jump,
	Run,
	Sprint,
};

 

 

2 GameAbilitySystem

2.1 AbilitySystem 컴포넌트 생성

  - 새로운 C++ 클래스 > AbilitySystemComponent 클래스 > "GASAbilitySystemComponent" 클래스

    AbilitySystem도 결국 캐릭터나 원하는 액터에 컴포넌트로 부착하는 방식.

 

2.2 AttributeSet

  - 새로운 C++ 클래스 > AttributeSet 클래스 > "GASCharacterAttributeSet" 클래스

  - 해당 액터에 부여될 속성들

    해당 속성들이 멀티플레이 환경에서도 동작할 수 있도록 코드가 작성되어 있음.

    또한 델리게이트를 활용해서 해당 속성이 변경되거나 소진되었을 경우를 확인 가능.

<hide/>

// UGASCharacterAttributeSet.h

#pragma once

#include "CoreMinimal.h"
#include "AttributeSet.h"
#include "AbilitySystemComponent.h"
#include "GASCharacterAttributeSet.generated.h"

/*
 * 이 매크로 함수 하나만 작성하면, 자동으로 해당 속성에대한 게터/세터가 generated.h 파일에 작성됨.
 * 덕분에 런타임 중, 블루 프린트에서도 해당 속성을 접근할 수 있게 됨.
 */
#define ATTRIBUTE_ACCESSORS(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_PROPERTY_GETTER(ClassName, PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_GETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_SETTER(PropertyName) \
	GAMEPLAYATTRIBUTE_VALUE_INITTER(PropertyName)

/* 속성의 변화를 알리기 위한 델리게이트들. 필요에 따라 추가. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAttributeChangeDelegate, float, CurrentAttribute);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnOutOfAttributeDlegate);

UCLASS()
class GAS_API UGASCharacterAttributeSet : public UAttributeSet
{
	GENERATED_BODY()

public:
	UPROPERTY(BlueprintReadOnly, Category = Attribute, ReplicatedUsing = OnRep_MaxHP)
	FGameplayAttributeData MaxHP;
	ATTRIBUTE_ACCESSORS(UGASCharacterAttributeSet, MaxHP);
	
	UPROPERTY(BlueprintReadOnly, Category = Attribute, ReplicatedUsing = OnRep_CurrentHP)
	FGameplayAttributeData CurrentHP;
	ATTRIBUTE_ACCESSORS(UGASCharacterAttributeSet, CurrentHP);
	
	UPROPERTY(BlueprintReadOnly, Category = Attribute, ReplicatedUsing = OnRep_MaxSP)
	FGameplayAttributeData MaxSP;
	ATTRIBUTE_ACCESSORS(UGASCharacterAttributeSet, MaxSP);
	
	UPROPERTY(BlueprintReadOnly, Category = Attribute, ReplicatedUsing = OnRep_CurrentSP)
	FGameplayAttributeData CurrentSP;
	ATTRIBUTE_ACCESSORS(UGASCharacterAttributeSet, CurrentSP);
	
	FOnAttributeChangeDelegate OnCurrentSPChanged;

	FOnOutOfAttributeDlegate OnOutOfCurrentSP;

public:
	UGASCharacterAttributeSet();

	/* 이펙트가 적용된 후에 자동으로 호출되는 함수 */
	void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override;

	/* 속성이 변경된 후에 자동으로 호출되는 함수 */
	void PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue) override;

	/* 이하는 레플리케이션을 위한 함수들 */
	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

	UFUNCTION()
	virtual void OnRep_MaxHP(const FGameplayAttributeData& OldMaxHP);
	
	UFUNCTION()
	virtual void OnRep_CurrentHP(const FGameplayAttributeData& OldCurrentHP);

	UFUNCTION()
	virtual void OnRep_MaxSP(const FGameplayAttributeData& OldMaxSP);

	UFUNCTION()
	virtual void OnRep_CurrentSP(const FGameplayAttributeData& OldCurrentSP);
	
};
<hide/>

// UGASCharacterAttributeSet.cpp

#include "GASCharacterAttributeSet.h"
#include "GameplayEffectExtension.h"
#include "GASGlobal.h"
#include "Net/UnrealNetwork.h"

UGASCharacterAttributeSet::UGASCharacterAttributeSet()
{
}

void UGASCharacterAttributeSet::PostAttributeChange(const FGameplayAttribute& Attribute, float OldValue, float NewValue)
{
	Super::PostAttributeChange(Attribute, OldValue, NewValue);

	/* 당연하지만, 어트리뷰트의 세터 함수들을 PostAttributeChange()에서 호출하면 안됨. */
}

void UGASCharacterAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	Super::PostGameplayEffectExecute(Data);
    
	if (Attribute == GetMaxHPAttribute())
	{
		SetMaxHP(FMath::Clamp(GetMaxHP(), 0.f, LIMIT));
	}
	if (Attribute == GetCurrentHPAttribute())
	{
		SetCurrentHP(FMath::Clamp(GetCurrentHP(), 0.f, GetMaxHP()));
	}
	if (Attribute == GetMaxSPAttribute())
	{
		SetMaxSP(FMath::Clamp(GetMaxSP(), 0.f, LIMIT));
	}
	if (Attribute == GetCurrentSPAttribute())
	{
		SetCurrentSP(FMath::Clamp(GetCurrentSP(), 0.f, GetMaxSP()));          /* 제한 코드 */

		OnCurrentSPChanged.Broadcast(GetCurrentSP());                         /* 변경시 알림 */

		if (KINDA_SMALL_NUMBER < OldValue &&  NewValue < KINDA_SMALL_NUMBER)  /* 소진시 알림 */
		{
			if (OnOutOfCurrentSP.IsBound())
			{
				OnOutOfCurrentSP.Broadcast();
			}
		}
	}
}

void UGASCharacterAttributeSet::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	
	DOREPLIFETIME_CONDITION_NOTIFY(UGASCharacterAttributeSet, MaxHP, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UGASCharacterAttributeSet, CurrentHP, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UGASCharacterAttributeSet, MaxSP, COND_None, REPNOTIFY_Always);
	DOREPLIFETIME_CONDITION_NOTIFY(UGASCharacterAttributeSet, CurrentSP, COND_None, REPNOTIFY_Always);
}

void UGASCharacterAttributeSet::OnRep_MaxHP(const FGameplayAttributeData& OldMaxHP)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCharacterAttributeSet, MaxHP, OldMaxHP);
}

void UGASCharacterAttributeSet::OnRep_CurrentHP(const FGameplayAttributeData& OldCurrentHP)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCharacterAttributeSet, MaxHP, OldCurrentHP);
}

void UGASCharacterAttributeSet::OnRep_MaxSP(const FGameplayAttributeData& OldMaxSP)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCharacterAttributeSet, MaxHP, OldMaxSP);
}

void UGASCharacterAttributeSet::OnRep_CurrentSP(const FGameplayAttributeData& OldCurrentSP)
{
	GAMEPLAYATTRIBUTE_REPNOTIFY(UGASCharacterAttributeSet, MaxHP, OldCurrentSP);
}

 

2.3 GameplayAbility 만들기

  - 새로운 C++ 클래스 > GameplayAbility 클래스 > "GASGameplayAbility" 클래스

    이번 실습 내의 모든 어빌리티들이 상속 받게될 클래스.

  - AbilityInputID는 액션 매핑시에 사용하게 됨. Enhanced Input을 바인드할 때도 사용됨.

    해당 ID를 가진 어빌리티가 인풋 액션이 들어오면 자동으로 활성화 되는 방식.

  - EffectHandle은 어빌리티의 여러 함수들에 자주 사용되기 때문에 따로 캐시해둠.

<hide/>

// GASGameplayAbility.h

#pragma once

#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "GASGlobal.h"
#include "GASGameplayAbility.generated.h"

UCLASS()
class GAS_API UGASGameplayAbility : public UGameplayAbility
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = InputID)
	EGASAbilityInputID AbilityInputID;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ID)
	EGASAbilityID AbilityID;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Handles)
	mutable FActiveGameplayEffectHandle EffectHandle;

public:
	UGASGameplayAbility();
	
};
<hide/>

// GASGameplayAbility.cpp

#include "GASGameplayAbility.h"

UGASGameplayAbility::UGASGameplayAbility()
	: AbilityInputID(EGASAbilityInputID::None)
	, AbilityID(EGASAbilityID::None)
{
}

 

2.4 Character 클래스와 GameAbilitySystem

<hide/>

// GASCharacter.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputActionValue.h"
#include "GameplayAbilities/Public/AbilitySystemInterface.h"
#include "GASCharacter.generated.h"

UCLASS()
class AGASCharacter : public ACharacter, public IAbilitySystemInterface
{
	GENERATED_BODY()

#pragma region DefaultCharacterVariables
	...
#pragma endregion

#pragma region GAS
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = GAS, meta = (AllowPrivateAccess = "true"))
	class UGASAbilitySystemComponent* AbilitySystemComponent;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = GAS, meta = (AllowPrivateAccess = "true"))
	TArray<TSubclassOf<class UGASGameplayAbility>> Abilities;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = GAS, meta = (AllowPrivateAccess = "true"))
	class UGASCharacterAttributeSet* AttributeSet;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = GAS, meta = (AllowPrivateAccess = "true"))
	TSubclassOf<class UGameplayEffect> DefaultAttributeEffect;	
#pragma endregion 
	
public:
	...

	virtual void PossessedBy(AController* NewController) override;
	
public:
	...

	virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;

private:
	...

	void InitializeAttributes();
	void GiveAbilities();
	
};
<hide/>

// GASCharacter.cpp

#include "GASCharacter.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/Controller.h"
#include "GASGlobal.h"

#include "EnhancedInputSubsystems.h"
#include "EnhancedInputComponent.h"
#include "Components/InputComponent.h"
#include "GASInputConfigData.h"

#include "AbilitySystemComponent.h"
#include "GASAbilitySystemComponent.h"
#include "GASGameplayAbility.h"
#include "GASCharacterAttributeSet.h"
#include "GameplayEffectTypes.h"

AGASCharacter::AGASCharacter()
{
	...

#pragma region InitializeAbilitySystem

	AbilitySystemComponent = CreateDefaultSubobject<UGASAbilitySystemComponent>(TEXT("AbilitySystemComponent"));
	AbilitySystemComponent->SetIsReplicated(true);
	AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
	/*
	 *  EGameplayEffectReplicationMode::Minimal -> Only replicate minimal gameplay effect info.
	 *  EGameplayEffectReplicationMode::Mixed   -> Only replicate minimal gameplay effect info to simulated proxies but full info to owners and autonomous proxies.
	 *                                             simulated proxies란, 네트워크 반대편에서 예측을 통해 시뮬레이션하는 액터를 말함.(정확하지 않음. 필자 생각.) ex. 총알 액터의 정보는 출발점 정보와 속도 정보 정도의 Minimal gameplayeffect만 보내면 알아서 행동을 시뮬레이션하는 것과 같은듯.
	 *                                             autonomous(자발적인) proxies는 자발적으로 움직인다는 뜻이기에 플레이어 자신을 뜻함. 플레이어 자신에게는 모든 정보를 넘김.
	 *                                             owners는 해당 액터를 가지고 있는 액터들(ex. controller, ...)을 뜻함. 이들에게는 모든 정보를 넘김.
	 *  EGameplayEffectReplicationMode::Full    -> Replicate full gameplay info to all.
	 */

	AttributeSet = CreateDefaultSubobject<UGASCharacterAttributeSet>(TEXT("AttributeSet"));

#pragma endregion 
	
}

...

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

	AbilitySystemComponent->InitAbilityActorInfo(this, this);

	InitializeAttributes();
	GiveAbilities();
}

UAbilitySystemComponent* AGASCharacter::GetAbilitySystemComponent() const
{
	return AbilitySystemComponent;
}

void AGASCharacter::InitializeAttributes()
{
	/* 이펙트를 적용해서 자신의 어트리뷰트들을 초기화하는 코드. */
	if (AbilitySystemComponent && DefaultAttributeEffect)
	{
		FGameplayEffectContextHandle EffectContextHandle = AbilitySystemComponent->MakeEffectContext();
		EffectContextHandle.AddSourceObject(this);

		FGameplayEffectSpecHandle EffectSpecHandle = AbilitySystemComponent->MakeOutgoingSpec(DefaultAttributeEffect, 1, EffectContextHandle);
		if (EffectSpecHandle.IsValid())
		{
			FActiveGameplayEffectHandle EffectHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*EffectSpecHandle.Data.Get());
		}
	}
}

void AGASCharacter::GiveAbilities()
{
	/* 기본 어빌리티들은 블루프린트 클래스에서 지정해줌. 해당 기본 어빌리티들이 캐릭터에게 주어지게끔 하는 코드. */
	if (HasAuthority() && AbilitySystemComponent)
	{
		for (TSubclassOf<UGASGameplayAbility>& Ability : DefaultAbilities)
		{
			/*
			 *  각 어빌리티마다 AbilityInputID가 있음. 주의해야할 것은 AbilityInputID != AbilityID. 
			 *  AbilityLocalInputPressed(AbilityInputID) 함수가 호출되면, AbilityInputID 어빌리티의 InputPressed() 함수가 호출되게끔 함.
			 *  AbilityLocalInputReleased() 함수도 마찬가지.
			 */
			AbilitySystemComponent->GiveAbility(FGameplayAbilitySpec(Ability, 1, static_cast<int32>(Ability.GetDefaultObject()->AbilityInputID), this));			
		}
	}
}

...

 

2.5 GameplayEffect 블루프린트 클래스 만들기

  - Content > GAS > Blueprints > 새폴더 "GameplayEffects" 생성

    새 블루프린트 클래스 > GameplayEffect 부모 클래스 > "BPGE_GASCharacterDefaults"

    컴파일 버튼 옆 세 개 동그라미 기호 클릭 > Save on Compile > On Success Only 체크 후 컴파일.

  - BP_GASCharacter > Default Attribute Effect에 BPGE_GASCharacterDefaults 지정.

  - 플레이 > 뷰포트 클릭 > 틸트키(`) > showdebug abilitysystem > 어트리뷰트 초기화된거 확인.

GE_ GASCharacterDefaults의 Details

 

3. 어빌리티 만들기

3.1 Jump ability 구현

3.1-1 Jump 어빌리티 클래스 생성

  - 새 C++ 클래스 > GASGameplayAbility 부모 클래스 > "GASGameplayAbility_Jump" 클래스 생성

  - 당연히, GASGameplayAbility를 상속 받는 블루프린트 클래스로 작성해도 됨.

<hide/>

// GASGameplayAbility_Jump.h

#pragma once

#include "CoreMinimal.h"
#include "GASGameplayAbility.h"
#include "GASGameplayAbility_Jump.generated.h"

UCLASS()
class GAS_API UGASGameplayAbility_Jump : public UGASGameplayAbility
{
	GENERATED_BODY()

public:
	UGASGameplayAbility_Jump();

	virtual void InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) override;
	virtual void InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) override;

	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;

	/* CommitCost()는 어빌리티가 활성되기 전에 비용 지불이 가능한지 확인하고 지불함. 반면에 ApplyCost()는 어빌리티가 활성된 후 지불이 가능한지 확인하고 지불함. */
	virtual void ApplyCost(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const override;
	
};
<hide/>

// GASGameplayAbility_Jump.cpp

#include "GASGameplayAbility_Jump.h"

#include "Kismet/KismetSystemLibrary.h"

UGASGameplayAbility_Jump::UGASGameplayAbility_Jump()
{
	AbilityID = EGASAbilityID::Jump;
	AbilityInputID = EGASAbilityInputID::Jump;

	// 이렇게 태그를 지정해두면, 이후에 해당 태그를 가진 어빌리티들을 비활성할 수도 있음. ex. 마나가 없으니 마나를 사용하는 스킬은 모두 꺼라.
	AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("GASAbilities.UseSP")));
	AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("GASAbilities.UseSP.Jump")));
}

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

	// 멀티플레이 환경에서 이 함수를 활용해서 디버깅 가능.
	UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("InputPressed: %s"), *GetName()));
}

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

	UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("InputReleased: %s"), *GetName()));
}

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

	UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("ActivateAbility: %s"), *GetName()));
}

void UGASGameplayAbility_Jump::EndAbility(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
	bool bReplicateEndAbility, bool bWasCancelled)
{
	Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);

	UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("EndAbility: %s"), *GetName()));
}

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

	UKismetSystemLibrary::PrintString(GetWorld(), FString::Printf(TEXT("ApplyCost: %s"), *GetName()));
}

Project Settings > GameplayTags > Gameplay Tag List에서 태그 추가.

 

 

3.1-2 어빌리티와 입력의 바인딩 준비

  - GASInputConfigData 클래스 수정.

  - DA_GASInputConfigData 에셋 수정.

<hide/>

// GASInputConfigData.h

...

UCLASS()
class GAS_API UGASInputConfigData : public UDataAsset
{
	GENERATED_BODY()

public:
	const UInputAction* FindNativeInputActionByTag(const FGameplayTag& NewInputTag, bool bLogNotFound = true) const;
	const UInputAction* FindAbilityInputActionByTag(const FGameplayTag& NewInputTag, bool bLogNotFound = true) const;

public:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (TitleProperty = "InputActions"))
	TArray<FTagBindingInputAction> NativeInputActions;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (TitleProperty = "InputActions"))
	TArray<FTagBindingInputAction> AbilityInputActions;

};
<hide/>

// GASInputConfigData.cpp

...

const UInputAction* UGASInputConfigData::FindAbilityInputActionByTag(const FGameplayTag& NewInputTag,
	bool bLogNotFound) const
{
	for (const FTagBindingInputAction& Action : AbilityInputActions)
	{
		if (Action.InputAction && Action.InputTag.MatchesTagExact(NewInputTag))
		{
			return Action.InputAction;
		}
	}

	if (bLogNotFound)
	{
		UE_LOG(LogTemp, Error, TEXT("Can't find AbilityInputAction for InputTag [%s] on InputConfig [%s]."), *NewInputTag.ToString(), *GetNameSafe(this));
	}

	return nullptr;
}

DA_GASInputConfigData의 Details

 

3.1-3 GASCharacter 클래스 수정

  - 컴파일 후 플레이 하면 확인 가능.

<hide/>

// TPSCharacter.h

...
class TPSGAS_API ATPSCharacter : public ACharacter, public IAbilitySystemInterface
{
	...

	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = AbilitySystem, meta = (AllowPrivateAccess = "true"))
	float CurrentSP;
	
public:
	...

private:
	...

	virtual void Jump() override;
	
};
<hide/>

// GASCharacter.cpp

...

void AGASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	...
	
	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
#pragma region BindNativeInputActions
		...
#pragma endregion

#pragma region BindAbilityInputAction
		/* 바인딩 설명은 GiveAbilities() 함수 참고 */
		EnhancedInputComponent->BindAction(InputActions->FindAbilityInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
											ETriggerEvent::Triggered, AbilitySystemComponent, &UAbilitySystemComponent::AbilityLocalInputPressed, static_cast<int32>(EGASAbilityInputID::Jump));
		EnhancedInputComponent->BindAction(InputActions->FindAbilityInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
											ETriggerEvent::Completed, AbilitySystemComponent, &UAbilitySystemComponent::AbilityLocalInputReleased, static_cast<int32>(EGASAbilityInputID::Jump));

#pragma endregion 
	}
}

...

BP_GASCharacter 블루프린트 클래스의 Details. Default Abilities에 Jump 어빌리티를 추가해줘야함.

 

3.1-4 이펙트 만들기

  - 점프할 때마다 10 SP씩 소모되게끔 구현하고자 함.

    어트리뷰트를 수정하려면 GameplayEffect의 도움을 받아야함.

  - GAS > Blueprints > GameplayEffects

    새 블루프린트 클래스 > GameplayEffect 부모 클래스 > "BPGE_Jump" 클래스

  - 점프 1회마다 10 SP씩 소모되는 방식이라, Duration Policy는 Instant로 지정함.

  - GAS > Blurprints > 새 폴더 "GameplayAbilities" 생성

    새 블루프린트 클래스 > GASGameplayAbility_Jump 부모 클래스 > "BPGE_Jump" 클래스 생성

  - BPGE_Jump 블루프린트 클래스 > 아래와 같이 설정

BPGE_Jump 블루프린트 클래스의 Details

 

3.1-5 InputPressed 

  - Pressed를 falling edge에 1번만 작동하게끔 하고싶다.

    IA_GASJump > Details를 아래와 같이 설정

 

3.1-6 이펙트 적용

  - 점프를 할 때마다 이펙트를 적용해서 CurrentSP가 줄어들게끔 구현하고자 함.

    아래 코드를 작성하고 컴파일.

  - Content > GAS > 새 폴더 "GameplayAbilities"

    새 블루프린트 클래스 > GASGameplayAbility_Jump 부모 클래스 > "BPGA_Jump" 생성.

    아래와 같이 Details 설정.

  - BP_GASCharacter > Default Abilities > 기존 C++ 클래스 대신 BPGA_Jump로 변경.

<hide/>

// GASGameplayAbility_Jump.cpp

#include "GASGameplayAbility_Jump.h"
#include "AbilitySystemComponent.h"
#include "Kismet/KismetSystemLibrary.h"

UGASGameplayAbility_Jump::UGASGameplayAbility_Jump()
{
	AbilityID = EGASAbilityID::Jump;
	AbilityInputID = EGASAbilityInputID::Jump;
	
	AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("GASAbilities.UseSP.Jump")));
}

void UGASGameplayAbility_Jump::InputPressed(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
}

void UGASGameplayAbility_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
}

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

void UGASGameplayAbility_Jump::EndAbility(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
	bool bReplicateEndAbility, bool bWasCancelled)
{
	if (IsEndAbilityValid(Handle, ActorInfo))
	{
		UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
		if (ASC)
		{
			if (EffectHandle.IsValid())
			{
				if (ASC->RemoveActiveGameplayEffect(EffectHandle))
				{
					EffectHandle.Invalidate();
				}
			}
		}

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

void UGASGameplayAbility_Jump::ApplyCost(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) const
{
	UGameplayEffect* GECost = GetCostGameplayEffect();
	if (GECost)
	{
		EffectHandle = ApplyGameplayEffectToOwner(Handle, ActorInfo, ActivationInfo, GECost, GetAbilityLevel(Handle, ActorInfo));
	}
}

BPGA_Jump 블루프린트 클래스의 Details

 

3.1-7 문제점

  - 체공 중에 스페이스바를 한 번 더 누르면 SP가 또 감소함.

    체공 중에는 SP가 감소하지 않게끔 해야함. [여기서부터 다시]

<hide/>

// GASCharacter.cpp

...

void AGASCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	...
	
	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
#pragma region BindNativeInputActions
		// EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
		// 									ETriggerEvent::Triggered, this, &ACharacter::Jump);
		// EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Jump"))),
		// 									ETriggerEvent::Completed, this, &ACharacter::StopJumping);
		EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Move"))),
											ETriggerEvent::Triggered, this, &ThisClass::Move);
		EnhancedInputComponent->BindAction(InputActions->FindNativeInputActionByTag(FGameplayTag::RequestGameplayTag(FName("EnhancedInput.Look"))),
											ETriggerEvent::Triggered, this, &ThisClass::Look);
#pragma endregion

...
	}
}

...

 

 

 

 

3.2 Idle 상태에서 SP 회복

3.2-1 패시브 스킬 어빌리티

  - 패시브 스킬은 결국 무한히 적용되는 스킬. 

    Project Settings > GameplayTags > 새 태그 "GASAbilities.NoCost.RegenSP" 추가.

  - 새 C++ 클래스 > GASGameplayAbility 부모 클래스 > "GASGameplayAbility_RegenSP" 클래스

    아래와 같이 작성 후 컴파일.

<hide/>

// GASGameplayAbility_RegenSP.h

#pragma once

#include "CoreMinimal.h"
#include "GASGameplayAbility.h"
#include "GASGameplayAbility_RegenSP.generated.h"

UCLASS()
class GAS_API UGASGameplayAbility_RegenSP : public UGASGameplayAbility
{
	GENERATED_BODY()

public:
	UGASGameplayAbility_RegenSP();

	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;

};
<hide/>

// GASGameplayAbility_RegenSP.cpp

#include "GASGameplayAbility_RegenSP.h"

#include "AbilitySystemComponent.h"

UGASGameplayAbility_RegenSP::UGASGameplayAbility_RegenSP()
{
	AbilityID = EGASAbilityID::Idle;
	AbilityInputID = EGASAbilityInputID::None;

	AbilityTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("GASAbilities.NoCost.RegenSP")));
}

void UGASGameplayAbility_RegenSP::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
	const FGameplayEventData* TriggerEventData)
{
	if (CommitAbility(Handle, ActorInfo, ActivationInfo))
	{
		
	}
	else
	{
		EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
	}
}

void UGASGameplayAbility_RegenSP::EndAbility(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
	bool bReplicateEndAbility, bool bWasCancelled)
{
	if (IsEndAbilityValid(Handle, ActorInfo))
	{
		UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
		if (ASC)
		{
			if (EffectHandle.IsValid())
			{
				if (ASC->RemoveActiveGameplayEffect(EffectHandle))
				{
					EffectHandle.Invalidate();
				}
			}
		}
		Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
	}
}

 

3.2-2 패시브 스킬 이펙트

  - Content > GAS > Blueprints > GameplayEffect

    새 블루프린트 클래스 > GameplayEffect 부모 클래스 > "BPGE_RegenSP" 생성

    아래와 같이 Details 설정.

  - Content > GAS > Blueprints > GameplayAbilities

    새 블루프린트 클래스 > 

  - Content > GAS > Blueprints > GameplayAbilities

    새 블루프린트 클래스 > GASGameplayAbility_RegenSP 부모 클래스 > "BPGA_RegenSP" 클래스

    아래와 같이 Details 설정.

BPGE_RegenSP의 Details 설정.
BPGA_RegenSP의 Details 설정
BP_GASCharacter의 Event Graph
BP_GASCharacter의 Details 설정.

 

 

댓글