Enhanced Input과 Gameplay Tags를 활용한 확장 가능한 입력 시스템

Stupidiot·2025년 6월 5일

Unreal & C++

목록 보기
4/5
post-thumbnail

개요


기존 방식의 입력을 향상된 입력을 이용하여 인풋액션을 통해 사용자의 입력을 받게되었습니다.

또한 아래처럼 향상된 입력을 바인딩하여 사용했으나,

/* Move Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* MoveAction;

/* Look Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* LookAction;

이는 바인딩할 입력이 많은 큰 규모의 프로젝트에서는 관리가 힘들어집니다.
즉 바인딩은 새로운 입력, 능력이 추가될 수록 해야하는 작업이며, 심하면 개발이 불가능해질 수도 있습니다.

이를 해결하고 더 쉽게 구현할 수 있도록 Gameplay Ability System의 Tag를 이용, 인풋 태그를 사용하여 바인딩하는 방식으로 처리해보겠습니다.

Input Binding Process


1. 먼저 Native Gameplay Tag를 여러개 만들어서 이를 인풋 태그로 사용하겠습니다.

💡 GameplayTag
게임플레이 태그는 X.Y 형태의 계층적인 이름입니다.
-> eg.Player.Attack
-> eg.Player.Attack.Melee

2. 모든 게임플레이 태그를 생성 후 Input Config Data Asset을 생성

쉽게 말해 이 에셋을 통해 입력 태그를 고유한 입력 액션에 매핑하는 것입니다.

3. Custom Input Component

언리얼에 기본으로 제공되는 입력 컴포넌트는 이런 방식의 바인딩 입력을 지원하지 않기에 커스텀 인풋 컴포넌트를 생성해햐 합니다.

4. Binding Inputs

실제로 입력을 바인딩하고, 입력 콜백들을 생성합니다.

5. Assign assets

입력 바인딩의 마무리 단계이며, 에디터 내부에 필요한 모든 에셋들을 할당합니다.

구현


1. TestGameplayTags


//.h
#pragma once  
  
#include "NativeGameplayTags.h"  
  
namespace  TestGameplayTags  
{  
    /* Input Tags */  
    UNREALSTUDY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_Move)  
    UNREALSTUDY_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(InputTag_Look)  
}

다른 파일에서 사용 가능하도록 외부 선언

//.cpp
#include "TestGameplayTags.h"  
  
namespace  TestGameplayTags  
{  
    /* Input Tags */  
    UE_DEFINE_GAMEPLAY_TAG(InputTag_Move, "InputTag.Move");  
    UE_DEFINE_GAMEPLAY_TAG(InputTag_Look, "InputTag.Look");  
}

제 메모리에 변수 생성하고 태그 시스템에 등록

위의 코드를 추가하면 프로젝트 세팅에서 태그가 추가되었다는 것을 확인할 수 있습니다.

왜 이렇게 사용하나?

기존 방식 (나쁜 예시)

// 하드코딩된 문자열 - 오타 위험, 자동완성 없음
if (ActionName == "Move") { /* 이동 처리 */ }
if (ActionName == "Look") { /* 시점 처리 */ }

// 오타 발생 위험!
if (ActionName == "Mov") { /* 작동 안함! */ }

Gameplay Tags 방식 (좋은 예시)

// 컴파일 타임 검증, 자동완성, 오타 방지
if (ActionTag == TestGameplayTags::InputTag_Move) { /* 이동 처리 */ }
if (ActionTag == TestGameplayTags::InputTag_Look) { /* 시점 처리 */ }

// 오타시 컴파일 에러로 즉시 발견!

2. Input Config Data Asset


Data Asset를 상속받는 C++ 클래스를 생성합니다.
-> 이름은 DataAsset_InputConfig으로 설정했습니다.

//.h
#pragma once  
  
#include "CoreMinimal.h"  
#include "Engine/DataAsset.h"  
#include "DataAsset_InputConfig.generated.h"  
  
USTRUCT()  
struct FTestInputActionConfig  
{  
    GENERATED_BODY()  
    };  
  
/**  
 * */  
UCLASS()  
class UNREALSTUDY_API UDataAsset_InputConfig : public UDataAsset  
{  
    GENERATED_BODY()  
};

이후 입력 태그와 입력 액션을 매핑하는 구조를 만들기 위해 FTestInputActionConfig라는 구조체를 하나 정의합니다.

2-1. 구조체 구현

USTRUCT(BlueprintType)  // 블루프린트에서도 이 구조체를 사용할 수 있게 만듦
struct FTestInputActionConfig  
{  
    GENERATED_BODY()  
public:  
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (Categories = "InputTag"))  
    FGameplayTag InputTag;  
  
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)  
    class UInputAction* InputAction;  
};

구조체를 위와 같이 구현합니다.

  • 데이터 멤버
    - FGameplayTag InputTag : 입력 태그 (예: InputTag.Move)
    - UInputAction* InputAction : 실제 입력 액션 (예: IA_Move)

이 구조체는 "이 입력 태그는 이 입력 액션과 연결된다" 라는 매핑 정보를 저장하는 것입니다.

나중에 Data Asset에서 이런 매핑들의 배열을 만들어서 한 곳에서 모든 입력 바인딩을 관리하게 됩니다.

👓 Categories
meta = (Categories = "InputTag"): 태그 선택할 때 InputTag 카테고리만 보이게 필터링

2-2. 클래스 구현

UDataAsset_InputConfig 클래스는 모든 입력 설정을 한 곳에서 관리하는 Data Asset입니다.

UCLASS()  
class UNREALSTUDY_API UDataAsset_InputConfig : public UDataAsset  
{  
    GENERATED_BODY()  
  
public:  
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")  
    class UInputMappingContext* DefaultMappingContext;  
  
    UPROPERTY(EditDefaultsOnly,BlueprintReadOnly, meta = (TitleProperty = "InputAction"))  
    TArray<FTestInputActionConfig> NativeInputActions;  
};

먼저 다음과 같은 데이터 필드를 만들어 줍니다.
여기서 DefaultMappingContext는 IMC이며 기본 입력 매핑 컨텍스트로 키보드/마우스 등의 실제 입력을 정의합니다.

👓 TitleProperty
meta = (TitleProperty = "InputAction"): 에디터에서 배열을 볼 때 InputAction 이름으로 표시됨

2-3 클래스 도우미 함수 구현

다음으로는 아래의 함수를 정의하고 구현합니다.

UInputAction* FindNativeInputActionByTag(const FGameplayTag& InInputTag) const;
UInputAction* UDataAsset_InputConfig::FindNativeInputActionByTag(const FGameplayTag& InInputTag) const 
{  
    for (const FTestInputActionConfig& InputActionConfig : NativeInputActions)  
    {       
	    if (InputActionConfig.InputTag == InInputTag)  
       {          
	       return InputActionConfig.InputAction;  
       }    
    }  
    return nullptr;  
}

이후 빌드를 진행하고 데이터 에셋을 만들면 아래와 같이 구성한 Data Input Config를 생성 가능합니다.

생성 후 Input Action과 Input Mapping context를 할당해주면 됩니다.

3. Custom Input Component


기본적으로 제공되는 입력 컴포넌트로는 tags로 바인딩하는 기능을 구현하기 힘들기에 EnhancedInputComponent를 상속받은 C++ 클래스를 생성하겠습니다.

이후 태그를 통해 인풋과 함수를 바인드할 수 있는 함수를 구현하겠습니다.

#pragma once  
  
#include "CoreMinimal.h"  
#include "DataAsset_InputConfig.h"  
#include "EnhancedInputComponent.h"  
#include "TsetInputComponent.generated.h"  
  
UCLASS()  
class UNREALSTUDY_API UTsetInputComponent : public UEnhancedInputComponent  
{  
    GENERATED_BODY()  
  
public:  
    template<class UserObject, typename CallbackFunc>  
    void BindNativeInputAction(const UDataAsset_InputConfig* InInputConfig,  
       const FGameplayTag& InInputTag, ETriggerEvent TriggerEvent,  
       UserObject* ContextObject, CallbackFunc Func);  
};  
  
template <class UserObject, typename CallbackFunc>  
void UTsetInputComponent::BindNativeInputAction(const UDataAsset_InputConfig* InInputConfig,  
    const FGameplayTag& InInputTag, ETriggerEvent TriggerEvent, UserObject* ContextObject, CallbackFunc Func)  
{  
    checkf(InInputConfig, TEXT("Input Config data asset is nullptr."));  
  
    if (UInputAction* FoundAction = InInputConfig->FindNativeInputActionByTag(InInputTag))  
    {       
	    BindAction(FoundAction, TriggerEvent, ContextObject, Func);  
    }
}
  • UserObject: 콜백 함수를 가진 객체 타입
  • CallbackFunc: 콜백 함수의 타입

동작 과정:
1. InInputTag로 Data Asset에서 해당하는 UInputAction 찾기
2. 찾은 액션을 기존 BindAction으로 바인딩
3. 컴파일 타임에 타입 체크로 안전성 보장

💡 checkf
동작 방식

  • 조건 체크: InInputConfig가 유효한지 확인
  • 실패시: 게임을 크래시시키고 지정된 메시지 출력
  • 릴리즈 빌드: 자동으로 제거됨 (성능에 영향 없음)

이후 기본 입력 컴포넌트 클래스를 프로젝트 세팅에서 커스텀 컴포넌트로 교체합니다.

4. Binding Inputs


이제 인풋이 필요한 캐릭터 같은 클래스에 UDataAsset_InputConfig를 넣을 수 있게끔 멤버로 추가해줍니다.

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input", meta = (AllowPrivateAccess = "true"))  
UDataAsset_InputConfig* InputConfig;

그리고 SetupPlayerInputComponent를 구현해야 하는데, 기존 방식과 달리 아래처럼 사용하게 되는 것입니다.

void ABaseCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)  
{  
    Super::SetupPlayerInputComponent(PlayerInputComponent);  
  
    if (UTsetInputComponent* TestInputComponent = Cast<UTsetInputComponent>(PlayerInputComponent))   
    { 
        if (!InputConfig)  
        {            
	        UE_LOG(LogTemp, Error, TEXT("InputConfig is not set! Please assign it in the editor."));  
            return;  
        }  
        TestInputComponent->BindNativeInputAction(  
            InputConfig,  
            TestGameplayTags::InputTag_Move,  
            ETriggerEvent::Triggered,  
            this,  
            &ABaseCharacter::HandleMovementInput  
        );  
  
        TestInputComponent->BindNativeInputAction(  
            InputConfig,  
            TestGameplayTags::InputTag_Move,  
            ETriggerEvent::Completed,  
            this,  
            &ABaseCharacter::HandleMovementInputEnd  
        );  
  
        TestInputComponent->BindNativeInputAction(  
            InputConfig,  
            TestGameplayTags::InputTag_Look,  
            ETriggerEvent::Triggered,  
            this,  
            &ABaseCharacter::HandleLookInput  
        );  
  
        UE_LOG(LogTemp, Warning, TEXT("Successfully bound input actions using custom input component!"));  
    }    
    else  
    {  
        UE_LOG(LogTemp, Error, TEXT("Failed to cast to UTsetInputComponent! Make sure the input component class is set correctly."));  
    }
}

결론


이런 방식이 꽤 복잡하고 귀찮게 느껴질 수 있으나, 이는 많은 장점들이 있기에 한번 구현해두면 편할 것으로 예상됩니다.

단순하게 생각이 드는 장점은 아래와 같습니다.

  • 중앙집중식 관리: Data Asset에서 태그-액션 매핑 관리
  • 입력이 100개여도 UPROPERTY 1개
  • 타입 안전성: 컴파일 타임 태그 검증
  • 에디터 편의성: Data Asset에서 비주얼하게 매핑 관리
  • 런타임 유연성: 동적으로 Input Config 교체 가능
profile
행복하세요

0개의 댓글