ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Unreal] Unreal Engine 에서 RAII 패턴을 통해 자동으로 함수에서 EndAbility 하기
    쾌락없는 책임 (공부)/Unreal 2025. 8. 20. 12:20
    반응형

    개요

    사이드 프로젝트 내에서 아이템 종류에 따라 다른 어빌리티들을 활성화해주는 라우터 어빌리티가 필요하게 되었습니다. 해당 라우터 어빌리티의 역할은 ActivateAbility 에서 상황에 따라 어빌리티를 활성화 한 뒤 EndAbility를 해주면 됩니다. 

     

    다만 코드에서 Early Return 을 통해 여러 조건이 맞지 않으면 return을 미리 하도록 작성했는데 해당 코드에서 EndAbility를 계속 써야 하는 공수와 언제 내가 이걸 빼먹었을지 모른다는 공포가.... 있었습니다. 때문에 간단하게 RAII 패턴의 구조체를 만들어 ActivateAbility 함수(스코프)를 벗어나면 EndAbility를 자동으로 호출하도록 하였습니다. 

     

     

    코드

     

    아래 코드는 제작한 RAII 패턴의 구조체입니다. 생성자에서 어빌리티와 Cancel 을 호출할지의 옵션만 받도록 제작하였습니다.

    // All copyrights for this code are owned by Aster.
    
    #pragma once
    
    #include "CoreMinimal.h"
    #include "ProjectPA/Abilities/PAGameplayAbility.h"
    
    #define SCOPE_END_ABILITY() const FScopedAbilityEnd ANONYMOUS_VARIABLE(ScopedEnd)(this);
    
    /**
     * 아래 구조체는 RAII 패턴으로 만들어져 해당하는 스코프를 벗어나면 바로 EndAbility 를 호출하게 됩니다.
     * 어빌리티에서만 사용 가능하며 특수한 경우가 아니라면 사용하지 말도록 합니다.
     */
    struct FScopedAbilityEnd
    {
    	explicit FScopedAbilityEnd(UPAGameplayAbility* InAbility, bool bForceCancel = false)
    		: Ability(InAbility)
    		, bShouldCancel(bForceCancel)
    	{
    	}
    
    	~FScopedAbilityEnd()
    	{
    		if (Ability && Ability->IsActive())
    		{
    			if (bShouldCancel)
    			{
    				Ability->K2_CancelAbility();
    			}
    			else
    			{
    				Ability->SafeEndAbility();
    			}
    		}
    	}
    
    	FScopedAbilityEnd(const FScopedAbilityEnd&) = delete;
    	FScopedAbilityEnd& operator=(const FScopedAbilityEnd&) = delete;
    
    private:
    	UPAGameplayAbility* Ability = nullptr;
    	bool bShouldCancel = false;
    };

     

    여기서 주의해야 할 점은 EndAbility 와 K2_EndAbility 함수는 protected로 선언되어 있어 외부에서 어빌리티를 종료할 수 있는 함수를 제작해야 합니다. 외부에서 부를 때의 위험성이 있을 수 있어 해당 함수를 위 구조체에서만 만들 수 있도록 하던가 등의 조치가 있으면 더 좋을 거 같기는 합니다만, 일단 저렇게 제작하였습니다.  

     

    void UUseEquippingItemAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
    	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
    	const FGameplayEventData* TriggerEventData)
    {
    	// Auto-Call EndAbility
    	SCOPE_END_ABILITY();
    	
    	if (HasAuthority(&ActivationInfo) == false)
    	{
    		return;
    	}
    
    	const AActor* OwnerActor = ActorInfo->AvatarActor.Get();
    	UEquipmentComponent* EquipmentComponent = OwnerActor ? OwnerActor->FindComponentByClass<UEquipmentComponent>() : nullptr;
        
    	if (IsValid(EquipmentComponent) == false)
    	{
    		PALog_E(TEXT("EquipmentComponent is nullptr"));
    		return;
    	}
        
    	const FItemEncodedInfo& EquippingSlot = EquipmentComponent->GetEquippingItem();
    	if (EquippingSlot.IsValid() == false)
    	{
    		PALog_E(TEXT("EquippingSlot is not valid"));
    		return;
    	}
    	
    	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
    
    	if (FItemDataTable* ItemDataTable = EquipmentComponent->GetEquippingItemData())
    	{
    		EItemType ItemType = ItemDataTable->ItemType;
    		if (ItemTypeAbilityTags.Contains(ItemType))
    		{
    			if (UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get())
    			{
    				ASC->TryActivateAbilityByClass(ItemTypeAbilityTags[ItemType]);
    			}
    		}	
    	}
    }

     

    사용 자체는 간단합니다. 선언한 구조체를 사용하거나 매크로를 활용하면 해당 스코프를 벗어났을 때 어빌리티를 종료하도록 제작하였습니다.

     

    다만 해당 매크로를 활용하면 바로 어빌리티가 종료되어버리니 관련된 사이드 이펙트들을 주의해야 합니다. 저의 경우 다른 곳에서도 활용하려 했으나 EndAbility 가 활성화 되면 CurrentEventData 가 날아가버려 관련된 정보를 활용할 수 없다는 문제가 있었습니다. 이런 것들에 주의한다면 Early Return으로 코드를 짰을 때 EndAbility를 반복해서, 호출하지 않는 경우에 대한 문제를 고민하지 않아도 됩니다.

    반응형

    댓글

Designed by Tistory.