[UE5] Gameplay Ability와 Input 바인딩하기

kkado·2024년 9월 5일
post-thumbnail

Gameplay Ability란 게임 내에서 액터가 수행할 수 있는 모든 행동들을 의미한다.
게임 내에서 동적으로 키를 입력할 때마다 특정 어빌리티를 사용할 수 있도록 해보자.

Add vs Activate

먼저 두 가지 헷갈리는 개념인 추가(Add), 활성화(Activate)에 대해서 짚고 넘어가야 할 필요가 있다.

'추가' 는 어빌리티를 액터에 할당하거나 등록하는 과정을 의미한다. 이 과정은 주로 Ability System Component를 통해 수행된다.

어빌리티를 캐릭터에 추가하면, 어빌리티 목록에 해당 어빌리티가 등록된다. 어빌리티가 등록되었다고 해서 바로 '활성화' 시킬 수 있는 것은 아니다. 태그를 통해 어빌리티의 동작을 제어하고, 활성화 조건을 명시할 수 있다.

Ability System Component에서 GiveAbility 함수를 통해서 어빌리티를 추가할 수 있다.

Tag Property

태그 프로퍼티는 Gameplay Ability 블루프린트에서 확인할 수 있으며, 각 프로퍼티마다 태그를 바인딩할 수 있다.

Required

어빌리티가 활성화되기 위해서 '반드시 필요로 하는' 태그이다.
즉 캐릭터에게 이 태그가 없으면 어빌리티가 활성화되지 않는다. 사용하기 위해 어떤 상태 또는 능력 등이 요구되는 어빌리티에 유용하다.

Blocked

어빌리티의 활성화를 '막는' 태그이다.
즉 캐릭터에게 이 태그가 있으면 어빌리티가 활성화되지 않는다. 실행을 제어한다는 측면에서는 required와 비슷하지만, 반대 방향으로의 제어를 수행한다고 보면 될 것 같다.

예를 들어서 특정한 상태이상이 걸린 상태에서는 스킬을 사용할 수 없는 등의 제약조건을 구현할 때 유용하다.


'활성화' 는 '추가' 와 아예 다른 개념이다. 추가되었다고 해서 모두 활성화된 상태는 아니다. 어빌리티가 실제로 사용할 준비가 되었을 때 (사용 조건을 만족하고 있는 상태일 때) 어빌리티를 실행 및 발동시키는 과정을 의미한다.

TryActivateAbility 함수를 통해 활성화할 수 있으며, 이 과정에서 cost, requirement 등의 조건을 확인한다. 어빌리티가 활성화될 때는 GameplayAbility::ActivateAbility 함수가 호출되며, 여기에서 어빌리티가 활성화됐을 때의 로직을 추가하면 된다.


Custom Input Component

우리는 특정 키 입력에 대해서 어빌리티를 활성화하기 위해 인풋 컴포넌트에 게임플레이 태그를 연결해야 할 필요가 있다. Enhanced Input Component는 입력을 처리하는 데 강력한 기능을 제공하지만, 기본적으로는 입력에 대한 태그나 Ability와 직접 연결하는 기능은 제공하지 않는다.

따라서 특정한 InputAction과 특정한 태그를 서로 연결시켜 하나의 구조체로 만들고, 이 구조체를 data asset으로 만들어서 관리함으로써 코드의 모듈화, 관리의 편의 등의 이점을 누릴 수 있다.

EnhancedInputComponent를 상속하는 커스텀 인풋 컴포넌트 클래스를 만들고, TriggerEvent에 바인딩하는 작업이 필요하다.

먼저 커스텀 Data Asset 클래스를 하나 만들고, 아래와 같이 UInputActionUGameplayTag를 서로 묶을 수 있는 구조체를 만든다.

USTRUCT(BlueprintType)
struct FAuraInputAction
{
	GENERATED_BODY()
	
	UPROPERTY(EditDefaultsOnly)
	const UInputAction* InputAction = nullptr;

	UPROPERTY(EditDefaultsOnly)
	FGameplayTag InputTag = FGameplayTag();
};

이 클래스는 커스텀 구조체를 담고 있는 TArray와, 이 TArray에서 특정한 태그에 대응하는 구조체를 반환하는 lookup 함수를 포함할 것이다.

이후 위 사진과 같이 Data Asset을 만들고, Input Action과 태그를 만든 뒤 서로 연결한다.

그리고 Project Settings -> Input -> Default Classes 에서 Default Input Component Class를 꼭 직접 만든 클래스로 재지정해 줘야 한다.


AuraInputComponent 에서는 기존의 EnhancedInputComponent에서처럼 특정한 TriggerEvent에 함수를 바인딩하는 역할을 추가한다.

여기서는 여러 타입의 함수를 여러 타입의 액터에 바인딩할 수 있어야 하므로 템플릿 함수를 만든다.

UCLASS()
class AURA_API UAuraInputComponent : public UEnhancedInputComponent
{
	GENERATED_BODY()

public:
	template <class UserClass, typename PressedFuncType, typename ReleasedFuncType, typename HeldFuncType>
	void BindAbilityActions(const UAuraInputConfig* InputConfig, UserClass* Object, PressedFuncType PressedFunc,
	                        ReleasedFuncType ReleasedFunc, HeldFuncType HeldFunc);
};


// Data Asset에 존재하는 모든 InputAction에 대해 액션 바인딩을 한꺼번에 수행
template <class UserClass, typename PressedFuncType, typename ReleasedFuncType, typename HeldFuncType>
void UAuraInputComponent::BindAbilityActions(const UAuraInputConfig* InputConfig, UserClass* Object,
	PressedFuncType PressedFunc, ReleasedFuncType ReleasedFunc, HeldFuncType HeldFunc)
{
	check(InputConfig);
	for (const FAuraInputAction& Action : InputConfig->AbilityInputActions)
	{
		if (Action.InputAction && Action.InputTag.IsValid())
		{
			if (PressedFunc)
				BindAction(Action.InputAction, ETriggerEvent::Started, Object, PressedFunc, Action.InputTag);
			
			if (ReleasedFunc)
				BindAction(Action.InputAction, ETriggerEvent::Completed, Object, ReleasedFunc, Action.InputTag);
			
			if (HeldFunc)
				BindAction(Action.InputAction, ETriggerEvent::Triggered, Object, HeldFunc, Action.InputTag);
			
		}
	}
}

세 가지 주요 이벤트인 Hold, Pressed, Released에 대응하는 함수를 바인딩할 수 있도록 했다.

PlayerController 클래스도 일부 수정해야 한다.
먼저 BindAbilityActions 함수를 사용할 수 있게 하기 위해 AuraInputComponent로 캐스팅해야 하며, InputConfig와 함게 BindAbilityActions 함수를 호출한다.

먼저 바인딩할 함수 3가지를 정의한다.

	void AbilityInputTagPressed(FGameplayTag InputTag);
	void AbilityInputTagReleased(FGameplayTag InputTag);
	void AbilityInputTagHeld(FGameplayTag InputTag);

그리고 BindAbilityActions 함수를 호출.

void AAuraPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	UAuraInputComponent* AuraInputComponent = CastChecked<UAuraInputComponent>(InputComponent);
	AuraInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AAuraPlayerController::Move);
	AuraInputComponent->BindAbilityActions(InputConfig, this, &AAuraPlayerController::AbilityInputTagPressed, &AAuraPlayerController::AbilityInputTagReleased, &AAuraPlayerController::AbilityInputTagHeld);
}

Ability System Component에서 Ability Activation

어빌리티의 활성화는 ASC에서 수행하는 것이 일반적이다. Ability System Component에도 이 인풋 함수에 대응하는 함수를 만들어야 한다.

어빌리티를 활성화하기 위한 코드만 살펴보면 GetActivatableAbilities() 함수를 통해서 활성화 가능한 어빌리티 스펙들을 얻은 후, 이 중에서 인풋에 바인딩 된 태그와 일치하는 것을 찾는다. 찾은 어빌리티 스펙에 TryActivateAbility 함수와 함께 AbilitySpecInputPressed 함수를 호출해 준다.

void UAuraAbilitySystemComponent::AbilityInputTagHeld(const FGameplayTag& InputTag)
{
	if (!InputTag.IsValid()) return;
	for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
	{
		if (AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
		{
			AbilitySpecInputPressed(AbilitySpec);
			if (!AbilitySpec.IsActive())
			{
				TryActivateAbility(AbilitySpec.Handle);
			}
		}
	}
}

AbilitySpecInputPressed 함수는 입력이 눌렸을 때 해당 Ability를 활성화하는 함수이다. 이 함수에 어빌리티 스펙을 파라미터로 전달하면 된다.

0개의 댓글