다양한 기록

[Unreal GAS] Character Input Handling 본문

언리얼 엔진/Unreal Ability System

[Unreal GAS] Character Input Handling

라구넹 2025. 1. 12. 05:50

플레이어 캐릭터의 기획

- 기존 플레이어 캐릭터 상속

- 입력에 따라 정해진 게임플레이 어빌리티가 발동되도록 설정

- 점프 GA : 스페이스바를 누르면 점프 어빌리티가 발동

- 공격 GA : 마우스 왼쪽 클릭 시 공격 어빌리티가 발동

 

일단 블루프린트 게임모드 하나 만들고 컨트롤러 설정

변경

 

플레이어 캐릭터의 ASC 설정

- 분수대 액터처럼 플레이어 캐릭터에 설정하는 것도 가능

- 하지만 네트워크 멀티플레이 감안 시, 서버->클라이언트 배포되는 액터가 더 적합함

- 주기적으로 플레이어 정보를 배포하는 PlayerState 액터가 적합

- Owner를 PlayerState로 설정, Avatar로 Character로 설정하는 것이 일반적인 방법

 

ASC 설정은 PossessedBy에서 해주면 됨

그리고 게임 모드에서 PlayerState랑 DefaultPawn 설정

아직 뭐 바뀌진 않음

이제 입력으로 어빌리티 발동시킬 것

 

사실 점프 어빌리티 정도는 언리얼 엔진이 이미 만들어놨음

가져다 써보기

 

* 게임플레이 어빌리티 스펙

- 게임플레이 어빌리티에 대한 정보를 담고 있는 구조체

- ASC는 직접 어빌리티를 참조하지 않고 스펙 정보만 가지고 있음

- 스펙은 어빌리티의 현재 상태와 같은 다양한 정보를 가지고 있음

- ASC로부터 어빌리티를 다루고자 할 때엔 스펙의 Handle을 사용해 컨트롤

- 핸들값은 전역으로 설정되어 있으며 스펙 생성 시 자동으로 1씩 증가, 기본값 -1

- 어빌리티 정보: 스펙 / 어빌리티 인스턴스에 대한 레퍼런스 : 스펙 핸들

 

* 어빌리티 시스템 컴포넌트의 입력 처리

- 어빌리티 스펙에는 입력 값을 설정하는 필드 InpitID가 제공

- ASC에 등록된 스펙을 검사해 입력에 매핑된 GA를 찾을 수 있음(FindAbilitySpecFromInputID)

- 사용자 입력이 들어오면 ASC에서 입력에 관련된 GA를 검색

- 해당 GA를 발견 시, 현재 발동 중인지를 판별

    - GA가 발동 중이면 입력이 왔다는 신호 전달(AbilitySpecInputPressed)

    - GA가 발동하지 않았으면 새롭게 발동(TryActivateAbility)

- 입력이 떨어지면 동일하게 처리

    - GA에게 입력이 떨어졌다는 신호를 전달(AbilitySpecInputReleased)

 

`

// Fill out your copyright notice in the Description page of Project Settings.


#include "Character/ABGASCharacterPlayer.h"
#include "AbilitySystemComponent.h"
#include "Player/ABGASPlayerState.h"
#include "EnhancedInputComponent.h"

AABGASCharacterPlayer::AABGASCharacterPlayer()
{
	ASC = nullptr;
}

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

		int32 InputId = 0;
		for (const auto& StartAbility : StartAbilities) 
		{
			FGameplayAbilitySpec StartSpec(StartAbility); 
			StartSpec.InputID = InputId++;
			ASC->GiveAbility(StartSpec); 

			SetupGASInputComponent();
		}
	}
}

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

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

SetupGASInputComponent 두 번 호출하는 건 서버에서만 PossessedBy 호출되어서 그냥 안전하게 두 번 해준 거

코드 상 처음 어빌리티 부여할 때 0번부터 부여하는데, 준 건 점프밖에 없고 당연히 0번으로 설정됨

활성화 및 비활성화 시 Spec을 찾기 위해 InputID 사용해줌

* EnhancedInput은 바인딩할 때 인자 같이 넘겨줄 수 있음

점프 어빌리티 보면 InstancingPolicy라는게 존재

 

게임플레이 어빌리티의 인스턴싱 옵션

- 상황에 따라 다양한 인스턴스 정책 지정 가능

- NonInstanced : 인스턴싱 없이 CDO에서 일괄 처리

- InstancedPerActor : 액터마다 하나의 어빌리티를 만들어 처리(Primary Instance)

- InstancedPerExecution : 발동 시 인스턴스 생산

... 네트워크 리플리케이션 고려 시 InstancedPerActor가 무난

 

* 어빌리티 태스크

- 줄여서 AT

- GA의 실행(Activation)은 한 프레임에서 이루어짐

- GA가 시작되면 EndAbility 함수가 호출되기까지는 끝나지 않음

- 애니메이션 재생 같이 시간이 소요되고 상태를 관리해야 하는 어빌리티?

    - 비동기적으로 작업을 수행하고 끝나면 결과를 통보받기

    - 이를 위해 GAS는 어빌리티 태스크 제공

- AT의 활용 패턴

1. 어빌리티 태스크에 작업이 끝나면 브로드캐스팅되는 종료 델리게이트를 선언

2. GA는 AT를 생성한 후 바로 종료 델리게이트를 구독

3. GA의 구독 설정이 완료되면 AT를 구동 : AT의 ReadyForActivation 함수 호출

4. AT의 작업이 끝나면 델리게이트를 구독한 GA의 콜백 함수가 호출됨

5. GA의 콜백함수가 호출되면 GA의 EndAbility 함수를 호출해 GA를 종료

- GA는 필요에 따라 다수의 AT를 사용해 복잡한 액션 로직 설계 가능

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "ABGA_Attack.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLEGAS_API UABGA_Attack : public UGameplayAbility
{
	GENERATED_BODY()
	
public:
	UABGA_Attack();

	virtual void CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility) 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;
	virtual void InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) override;

protected:
	UFUNCTION()
	void OnCompleteCallback();

	UFUNCTION()
	void OnInterruptedCallback();
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "GA/ABGA_Attack.h"
#include "Character/ABCharacterBase.h"
#include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h"
#include "ArenaBattleGAS.h"
#include "GameFramework/CharacterMovementComponent.h"

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

void UABGA_Attack::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)
{
	Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);

}

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

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

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

	AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
	ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
}

void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
	ABGAS_LOG(LogABGAS, Log, TEXT("Begin"));
}

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

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

UAbilityTask_PlayMontageAndWait를 사용 (엔진에 이미 있는 거임)

끝날때 되면 EndAbility 불림

 

플레이어에선 바인드

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

	AABGASPlayerState* GASPS = GetPlayerState<AABGASPlayerState>();
	if (GASPS)
	{
		ASC = GASPS->GetAbilitySystemComponent();
		ASC->InitAbilityActorInfo(GASPS, this);

		int32 InputId = 0;
		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"));
	}
}

모든 어빌리티가 인풋을 요구하는게 아니니 분리해주는게 좋음

showdebug abilitysystem은 콘솔 커맨드인데, 어빌리티 시스템 상태를 볼 수 있음

 

태그 설정은 블루프린트에서 해줌

 

중앙 상단에 OwnedTags 보임

 

GA의 블루프린트 상속 및 게임플레이 태그 설정

- 꼭 필요한 상황이 아니라면 GA와 AT는 가급적 자기 역할만 충실하게 구현하는 것이 좋음

- 게임플레이 태그를 C++에서 설정하는 경우 기획 변경 때마다 소스코드 컴파일해야됨

- 게임플레이 태그 설정은 블루프린트에서 설정하는 것이 의존성 분리에 도움이 됨

 

 

'언리얼 엔진 > Unreal Ability System' 카테고리의 다른 글

[Unreal GAS] Implementing Combo Action  (0) 2025.01.12
[Unreal GAS] 개요  (0) 2025.01.11