ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] GAS를 활용한 캐릭터의 입력 처리
    UE5 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"));
    }
Designed by Tistory.