UE5

[UE5] GAS를 활용한 캐릭터의 입력 처리

검정색필통 2025. 1. 2. 12:30

플레이어 캐릭터의 ASC 설정

  • 분수대 액터와 같이 플레이어 캐릭터에 설정하는 것이 가능
  • 하지만 네트워크 멀티플레이를 감안했을 때, 서버에서 클라이언트로 배포되는 액터가 보다 적합
  • 이 때 많이 사용하는 액터가 주기적으로 플레이어 정보를 배포하는 PlayerState 액터
  • 따라서 Owner를 PlayerState로 설정하고, Avatar를 Character로 설정하는 것이 일반적인 방법
// ABGASCharacterPlayer.cpp

AABGASPlayerState* GASPS = GetPlayerState<AABGASPlayerState>();
ASC = GASPS->GetAbilitySystemComponent();

// Owner는 PlayerState, Avatar는 Character(this)
ASC->InitAbilityActorInfo(GASPS, this);

 

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

  • 게임 어빌리티 스펙에는 입력 값을 설정하는 필드 InputID가 제공됨
  • ASC에 등록된 스펙을 검사해 입력에 매핑된 GA를 찾을 수 있음 : FindAbilitySpecFromInputID
  • 사용자 입력이 들어오면 ASC에서 입력에 관련된 GA를 검색함
  • 해당 GA를 발견하면, 현재 발동 중인지를 판별
    • GA가 발동 중이면 입력이 왔다는 신호를 전달 : AbilitySpecInputPressed
    • GA가 발동하지 않았으면 새롭게 발동시킴 : TryActivateAbility
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);
		}
	}
}
  • 입력이 떨어지면 동일하게 처리
    • GA에게 입력이 떨어졌다는 신호를 전달 : AbilitySpecInputReleased
void AABGASCharacterPlayer::GASInputReleased(int32 InputId)
{
	FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
	if (Spec)
	{
		Spec->InputPressed = false;
		if (Spec->IsActive())
		{
			ASC->AbilitySpecInputReleased(*Spec);
		}
	}
}
  • EnhancedInputComponent의 BindAction 함수를 활용하면 범용적인 입력 처리가 가능해짐
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);
	}
}

 

 

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

  • 상황에 따라 다양한 인스턴스 정책을 지정할 수 있음
  • NonInstanced : 인스턴싱 없이 CDO에서 일괄 처리
    • 메모리 사용량이 낮고, 능력의 상태를 따로 관리할 필요가 없을 때 적합
    • 상태를 저장할 공간이 없으므로 능력 실행 간 데이터를 공유하거나 독립적으로 관리할 수 없음
  • InstancedPerActor : 액터마다 하나의 어빌리티 인스턴스를 만들어 처리 ( Primary Instance )
    • 능력 실행 간 필요한 데이터를 인스턴스 변수로 저장 가능
    • 서버와 클라이언트에서 동일한 애겉에 대해 독립적인 능력 인스턴스를 유지하므로 상태 동기화가 간단
    • 능력을 반복적으로 실행하거나 복잡한 상태를 유지해야 하는 경우 적합
    • 액터당 하나의 인스턴스를 공유하므로 불필요한 메모리 사용을 줄임
  • InstancedPerExecution : 발동시 인스턴스를 생산함
    • 완벽히 독립적인 상태 관리 : 여러 번 능력을 발동해도 서로 상태가 충돌하지 않음
    • 능력이 비결정적이거나 실행마다 고유한 상태를 필요로 하는 경우 적합 ( 예: 투사체 발사 )
    • 발동할 때마다 새로운 인스턴스를 생성하므로 메모리와 성능 오버헤드가 발생
    • 네트워크에서 각 실행마다 상태를 복제 및 동기화해야 하므로 복잡도가 증가
    • 능력 실행 횟수가 많을 경우 부하가 커짐
  • 네트워크 리플리케이션까지 고려했을 때 InstnacedPerActor가 무난한 선택지
UABGA_Attack::UABGA_Attack()
{
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

 

 

어빌리티 태스크(AT)의 활용

  • 어빌리티 태스크는 줄여서 AT라고 함
  • 게임플레이 어빌리티(GA)의 실행(Activation)은 한 프레임에서 이루어짐
  • 게임플레이 어빌리티(GA)가 시작되면 EndAbility함수가 호출되기까지는 끝나지 않음
  • 애니메이션 재생 같이 시간이 소요되고 상태를 관리해야 하는 어빌리티의 구현 방법
    • 비동기적으로 작업을 수행하고 끝나면 결과를 통보받는 형태로 구현
    • 이를 위해 GAS는 어빌리티 태스크를 제공하고 있음
  • 어빌리티 태스크(AT)의 활용 패턴
    1. 어빌리티 태스크에 작업이 끝나면 브로드캐스팅되는 종료 델리게이트를 선언함
    2. GA는 AT를 생성한 후 바로 종료 델리게이트를 구독함
    3. GA의 구독 설정이 완료되면 AT를 구동 : AT의 ReadyForActivation 함수 호출
    4. AT의 작업이 끝나면 델리게이트를 구독한 GA의 콜백 함수가 호출됨
    5. GA의 콜백함수가 호출되면 GA의 EndAbility 함수를 호출해 GA를 종료
void UABGA_Attack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

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

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

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

 

  • GA는 필요에 따라 다수의 AT를 사용해 복잡한 액션 로직을 설계할 수 있음
void UMyStrongAttackAbility::ActivateAbility(
    const FGameplayAbilitySpecHandle Handle,
    const FGameplayAbilityActorInfo* ActorInfo,
    const FGameplayAbilityActivationInfo ActivationInfo,
    const FGameplayEventData* TriggerEventData)
{
    Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

    // 강공격 애니메이션 재생
    UAbilityTask_PlayMontageAndWait* PlayMontageTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(
        this, FName("StrongAttackMontage"), StrongAttackMontage);
    PlayMontageTask->OnCompleted.AddDynamic(this, &UMyStrongAttackAbility::OnMontageCompleted);
    PlayMontageTask->Activate();
}

void UMyStrongAttackAbility::OnMontageCompleted()
{
    // 일정 시간 대기 후 폭발 생성
    UAbilityTask_WaitDelay* WaitDelayTask = UAbilityTask_WaitDelay::WaitDelay(this, ExplosionDelay);
    WaitDelayTask->OnFinish.AddDynamic(this, &UMyStrongAttackAbility::SpawnExplosion);
    WaitDelayTask->Activate();
}

void UMyStrongAttackAbility::SpawnExplosion()
{
    // 폭발 이펙트 생성
    UAbilityTask_SpawnActor* SpawnExplosionTask = UAbilityTask_SpawnActor::SpawnActor(
        this, ExplosionActorClass, ExplosionLocation, FRotator::ZeroRotator);
    SpawnExplosionTask->OnSuccess.AddDynamic(this, &UMyStrongAttackAbility::ApplyExplosionDamage);
    SpawnExplosionTask->Activate();
}

void UMyStrongAttackAbility::ApplyExplosionDamage(AActor* SpawnedActor)
{
    // 폭발 범위 내 데미지 적용
    UAbilityTask_ApplyRadialDamage* ApplyDamageTask = UAbilityTask_ApplyRadialDamage::ApplyRadialDamage(
        this, DamageAmount, ExplosionLocation, DamageRadius, nullptr, {}, GetAvatarActorFromActorInfo());
    ApplyDamageTask->Activate();

    // 능력 종료
    EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}

 

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

  • 꼭 필요한 상황이 아니라면 GA와 AT는 가급적 자기 역할만 충실하게 구현하는 것이 좋음
  • 게임플레이 태그를 C++에서 설정하는 경우 기획 변경때마다 소스코드 컴파일을 수행해야 함
  • 게임플레이 태그 설정은 블루프린트에서 설정하는 것이 의존성 분리에 도움이 됨
  • 게임플레이 태그 설정 기획
    • 점프 GA의 ActivationOwnedTags에 Character.State.IsJumping 게임플레이 태그 설정
    • 공격 GA의 ActivationOwnedTags에 Character.State.IsAttacking 게임플레이 태그 설정
  • GAS 디버깅을 사용해 현재 상황의 확인 가능
void AABGASCharacterPlayer::PossessedBy(AController* NewController)
{
    APlayerController* PlayerController = CastChecked<APlayerController>(NewController);
    PlayerController->ConsoleCommand(TEXT("showdebug abilitysystem"));
}