본문 바로가기
Unreal Engine 5

언리얼 엔진 - 향상된 입력 시스템(Enhanced Input System)

by 니키티스 2025. 12. 4.

언리얼 엔진 5의 향상된 입력 시스템(Enhanced Input System)에 관해 공부하고 정리한 글입니다.

공식 문서의 내용이 이해가 안 돼서 제 마음대로 해석을 많이 끼워 넣었기 때문에 오류가 많을 수 있습니다. 참고해 주시고 이상한 점이 있다면 지적해 주시면 감사하겠습니다!


유니티의 Input System과 유사하게, Mapping Contexts를 추가하고 거기에 액션들을 등록하는 식으로 동작한다(즉, 명령 패턴을 통한 느슨한 연결을 추구함).
Add > Input 안에 있는 옵션들로 이를 다룰 수 있다.
 

핵심 콘셉트

향상된 입력 시스템은 입력 액션(Input Actions), 입력 매핑 컨텍스트(Input Mapping Contexts), 입력 모디파이어(Input Modifiers), 입력 트리거(Input Triggers)로 구성되어 있다.
 

입력 액션(Input Action)

입력 액션은 향상된 입력 시스템과 프로젝트 코드 사이의 통신 링크 역할을 한다. 달리기, 앉기, 점프 등의 액션들을 지정하면, 코드나 블루프린트에서 여기에 입력 리스너(Input Listeners)를 추가해서 연결하는 식이다.
  • 액션 타입에 따라 액션 동작이 결정된다. Axis1D는 float, Axis2D는 FVector2D, Axis3D는 FVector로 정의된다.
  • on/off state는 bool Action을 써야 한다.
  • 트리거 스테이트(Trigger State): Started, Ongoing, Triggered, Completed, Canceled 등 액션의 현재 스테이트를 의미한다.
  • 코드로 작업할 땐 이런 식임.
  • void AFooBar::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
    	{
    		UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
    		// 여기에서 'ETriggerEvent' 열거형 값을 변경하여 원하는 트리거 이벤트를 바인딩할 수 있습니다.
    		Input->BindAction(AimingInputAction, ETriggerEvent::Triggered, this, &AFooBar::SomeCallbackFunc);
    	}
    	 
    	void AFooBar::SomeCallbackFunc(const FInputActionInstance& Instance)
    	{
    		// 여기에서 원하는 타입의 입력 액션 값을 가져옵니다...
    		FVector VectorValue = Instance.GetValue().Get<FVector>();
    		FVector2D 2DAxisValue = Instance.GetValue().Get<FVector2D>();
    		float FloatValue = Instance.GetValue().Get<float>(); 
    		bool BoolValue = Instance.GetValue().Get<bool>();
    	 
    		// 여기서 멋진 작업을 수행하세요!
    	}
    (UEnhancedInputComponent*)->BindAction 로 액션을 바인딩하고 입력 리스너 함수에서는 const FInputActionInstance& 를 인자로 받아 GetValue() 로 값을 가져오는 식.

 

입력 매핑 컨텍스트(Input Mapping Context)

입력 매핑 컨텍스트는 플레이어의 입력 액션 컬렉션이다. 실제 입력이 들어오면 입력 매핑 컨텍스트에 의해 적절한 액션에 전달해 주는 방식이다.
  • 입력 액션마다 실제 키를 지정해준다. 그 이외에도 액션의 추가 트리거나 모디파이어(길게 눌러야 한다 등의 옵션)를 지정할 수 있다.
  • 사용자마다 매핑 컨텍스트를 동적으로 추가, 제거할 수도 있고, 우선순위를 지정해서 조작이 충돌하는 상황을 해결할 수 있다(키가 겹친다면 우선순위 높은 컨텍스트만 우선적으로 고려되며 나머지 컨텍스트는 무시).
  • 로컬 플레이어에 적용할 땐 별도의 서브시스템을 사용한다(=EnhancedInputLocalPlayerSubSystem ).
  • 예를 들어, 차량에 탑승할 때 로컬 플레이어에게 차량용 입력 매핑 컨텍스트를 추가하고, 내릴 때 제거하는 식으로 처리한다.
  • 실제로 사용할 땐 다음과 같이 플레이어의 서브시스템을 얻어와서 거기에 매핑 컨텍스트를 추가하는 식으로 활용한다.
  • 	// 매핑 컨텍스트를 헤더 파일에 프로퍼티로 노출합니다...
    	UPROPERTY(EditAnywhere, Category="Input")
    	TSoftObjectPtr<UInputMappingContext> InputMapping;
    	 
    	 
    	// cpp에서...
    	if (ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(Player))
    	{
    		if (UEnhancedInputLocalPlayerSubsystem* InputSystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
    		{
    			if (!InputMapping.IsNull())
    			{
    				InputSystem->AddMappingContext(InputMapping.LoadSynchronous(), Priority);
    			}
    		}
    	}
 

입력 모디파이어(Input Modifiers)

입력을 받았을 때, 이걸 트리거에 보내기 이전에 변경하는 작업을 해주는 장치가 ‘모디파이어’다.
  • 축 순서를 바꾸거나 ‘데드존’을 구현하는 등을 수행한다.
  • PlayerState에 따라 입력 동작 방식도 바꿀 수 있고, 블루프린트로 작업하는 게 한계가 있다면 아예 자체 모디파이어를 구현해 UPlayerInput 에 접근할 수도 있다.
  • 상속받을 땐 UInputModifier 의 서브 클래스를 만들어 ModifyRaw_Implementation  함수를 오버라이드하면 됨.
  • 게임 세팅 값에 따라 축 반전을 구현하는 등에 이런 옵션이 유용하다(뭐, 여기 옵션에 필요한 게 없으면 InputModifier를 상속받아 나만의 모디파이어를 만들어도 된다).
  • 이 입력 모디파이어가 필요한 예시가 바로 ‘방향 입력’이다. 방향키나 WASD는 기본적으로 1차원 소스의 입력인데, 입력 모디파이어를 잘 쓰면 WASD를 X-Y축으로 구성된 2차원 입력으로 변환할 수 있다. 
  • 이렇게 하면 WS 입력이 Y축 입력으로 변환되는 걸 볼 수 있다!
 
 

입력 트리거(Input Triggers)

입력 모디파이어를 통과한 것에 대해, 입력 매핑 컨텍스트에서 입력을 분석해서 액션을 활성화할지 말지를 결정한다.
  • 말이 어려운데, 원래는 그냥 눌러도 동작하는 걸 ‘탭’하거나 ‘홀드’했을 때만 액션을 동작시키도록 변형할 수 있다.
  • 즉, ‘입력 패턴’을 통해 액션을 발동할 통과 조건을 만드는 셈.
  • 다만 한 가지 예외로, 다른 입력 액션을 통해서만 트리거되는 ‘조화된 액션’ 입력 트리거라는 것도 있다.
  • 입력 트리거는 세 가지로 구성된다.
  • 사용자 입력 처리 후, 입력 트리거는 세 가지 상태 중 하나를 반환한다.
  • 베이스 입력 트리거(InputTrigger) 클래스나 입력 트리거 시간 베이스(InputTriggerTimeBase)를 상속받아 필요한 자신만의 입력 트리거를 생성할 수 있다.
// UInputTriggerHold.h
/** UInputTriggerHold
    입력이 HoldTimeThreshold 초 동안 계속 작동되면 트리거가 발동됩니다.
    선택적으로 트리거가 한 번 발동될 수도 있고, 반복해서 발동될 수도 있습니다.
*/
UCLASS(NotBlueprintable, MinimalAPI, meta = (DisplayName = "Hold"))
class UInputTriggerHold final : public UInputTriggerTimedBase
{
	GENERATED_BODY()

	bool bTriggered = false;

protected:

	virtual ETriggerState UpdateState_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue ModifiedValue, float DeltaTime) override;

public:
	virtual ETriggerEventsSupported GetSupportedTriggerEvents() const override { return ETriggerEventsSupported::Ongoing; }
	
	// 트리거를 발동하려면 입력을 누른 채로 있어야 하는 시간입니다.
	UPROPERTY(EditAnywhere, Config, BlueprintReadWrite, Category = "Trigger Settings", meta = (ClampMin = "0"))
	float HoldTimeThreshold = 1.0f;
 
	// 길게 누르기 시간 한계치를 충족하는 경우 이 트리거를 한 번만 발동할지 아니면 프레임마다 발동할지 여부입니다.
	UPROPERTY(EditAnywhere, Config, BlueprintReadWrite, Category = "Trigger Settings")
	bool bIsOneShot = false;

	virtual FString GetDebugState() const override { return HeldDuration ? FString::Printf(TEXT("Hold:%.2f/%.2f"), HeldDuration, HoldTimeThreshold) : FString(); }
	};

// UInputTriggerHold.cpp
ETriggerState UInputTriggerHold::UpdateState_Implementation(const UEnhancedPlayerInput* PlayerInput, FInputActionValue ModifiedValue, float DeltaTime)
{
	// HeldDuration을 업데이트하고 기본 상태를 파생합니다
	ETriggerState State = Super::UpdateState_Implementation(PlayerInput, ModifiedValue, DeltaTime);

	// HeldDuration이 한계치에 다다르면 트리거됩니다
	bool bIsFirstTrigger = !bTriggered;
	bTriggered = HeldDuration >= HoldTimeThreshold;
	if (bTriggered)
	{
		return (bIsFirstTrigger || !bIsOneShot) ? ETriggerState::Triggered : ETriggerState::None;
	}

	return State;
}
위 코드 자체는 단순한데, UInputTriggerTimedBase는 HeldDuration 멤버를 통해 몇 초간 입력 트리거를 누르고 있었는지를 알 수 있다.
이를 통해 홀드했을 때 액션을 발동하는 것을 구현할 수 있는데, UpdateState_Implementation 에서 이전 프레임에서 트리거 여부(bIsFirstTrigger)와 현재 프레임에서의 트리거 여부(bTriggered)를 비교하여 결과를 계산한다.
여기에서는 최대한 유연성을 위해, bIsOneShot 이라는 멤버를 두어, 홀드한 시간이 임계 값을 넘는 순간 한 번만 발동할지 말지도 함께 고려할 수 있게 구현했다.
 

Player Mappable Input Config(PMI) - Deprecated

조금 있다가 말하겠지만, 실제로 살펴보니 여기 있는 PMI라는 기능은 Unreal Engine 5에서는 Deprecated된 기능으로 보인다. 다만 사용 자체는 가능하니 참고만 하자.
(PMI를 생성해서 보면, 이런 식으로 여러 개의 입력 매핑 컨텍스트(IMC)를 할당할 수 있는 걸 볼 수 있다. IMC마다 적혀 있는 숫자는 우선순위이다.)
매핑 가능 환경설정(PMI)은 입력 매핑 컨텍스트들을 모아놓는 컬렉션이다. 왜 이렇게 여러 개를 모아놓냐 하면, ‘프리셋’처럼 그 중 하나를 자유롭게 바꿀 수 있게 하기 위해서다.
예를 들어, 왼손잡이와 오른손잡이는 조작 방법이 다른 만큼 입력 매핑 컨텍스트도 따로 만들어야 한다. 이때 기존에는 ‘왼손잡이용 입력 매핑 컨텍스트’와 ‘오른손잡이용 입력 매핑 컨텍스트’를 따로 배열 등으로 수동으로 관리해야 했다. 여기에서 PMI를 사용하면, 컨텍스트 세트를 만들고, 우선순위를 미리 정의해 놓을 수도 있다고 한다.
 
Subsystem->AddPlayerMappableConfig(Pair.Config.LoadSynchronous(), Options);
5.5 버전 이전에서는 위와 같이 AddPlayerMappableConfig 를 통해 PMI를 추가할 수 있었는데, UE 5.5 버전 이후부터는 이게 안 된다고 한다.
그 이유는 PlayerMappableInputConfig 자체가 Deprecated되었기 때문으로 보인다. 기존에는 아래와 같이 Player Mappable Input Config 데이터 에셋을 생성했는데, 현재에는 Deprecated로 표시되는 걸 볼 수 있다.
그래서 다음과 같이  AddMappingContext 라는 원래 함수를 호출하여 추가해야 한다고 한다.
사실 이렇게 되면, 모든 컨텍스트를 수동으로 추가하는 것과 그리 다르진 않아 보이긴 한다. 어쨌거나, 여러 개의 컨텍스트를 추가할 때 PMI를 참조하여 이렇게 추가하는 것도 가능하다.
for (const FLyraCloneMappableConfigPair& Pair : DefaultInputConfigs)
{
    if (Pair.bShouldActivateAutomatically)
    {
       FModifyContextOptions Options = {};
       Options.bIgnoreAllPressedKeysUntilRelease = false;

       // 내부적으로 Input Mapping Context를 추가한다:
       // - AddPlayerMappableConfig를 간단히 보는 것을 추천
       for (const auto ConfigObject = Pair.Config.LoadSynchronous(); const auto& MappingContextPair : ConfigObject->GetMappingContexts())
       {
          const UInputMappingContext* MappingContext = MappingContextPair.Key;
          const int32 Priority = MappingContextPair.Value; // 우선순위 값도 가져옵니다
          Subsystem->AddMappingContext(MappingContext, Priority, Options);
       }
    }
}
 
 
 

입력 주입(Injecting Input)

향상된 입력 시스템에는 입력 주입(Injecting Input)이라는 개념도 제공한다. 블루프린트든 C++이든 콘솔 명령어에서든 함수를 호출해서 플레이어의 입력을 시뮬레이션할 수 있다는 개념이다. Input.+key  콘솔 명령어로 입력 시뮬레이션을 시작할 수 있다.
Input.+key Gamepad_Left2D X=0.7 Y=0.5

Input.-key Gamepad_Left2D
위가 바로 Gamepad_Left2D 에 대한 예시인데, 이렇게 하면 Gamepad_Left2D라는 키의 액션에 X=0.7, Y=0.5의 입력이 들어가는 셈.
여기서의 Gamepad_Left2D라는 키에는 실제 FKey 이름이면 뭐든 들어갈 수 있다. 똑같은 일을 블루프린트나 C++에서도 똑같이 할 수 있다.
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer());
	 
UEnhancedPlayerInput* PlayerInput = Subsystem->GetPlayerInput();
	 
FInputActionValue ActionValue(1.0f); // This can be a bool, float, FVector2D, or FVector
PlayerInput->InjectInputForAction(InputAction, ActionValue);
위 두 개가 각각 블루프린트에서의 입력 주입(Injecting Input)에 해당하는데, 콘솔 명령어와 같은 기능을 한다.
 

플랫폼 세팅

의외로 게임을 개발하다 보면, 수동으로 빌드하면 매번 다른 환경에서 빌드하면서 실수하기 쉽다. 그 때문에 플랫폼을 바꿀 때 세팅해야 하는 것들을 자동으로 설정하도록 CI/CD를 많이들 구축하게 된다.
언리얼 엔진에서도 마찬가지로, 플랫폼마다 사용 가능한 액션이 달라 입력 세팅을 바꾸어 주어야 하는 경우가 있다. 향상된 입력 시스템에서는 이를 위해 매핑 컨텍스트 리디렉트(MappingContextRedirect)를 제공한다.
향상된 입력 플랫폼 데이터(EnhancedInputPlatformData)를 상속받아 블루프린트를 만들면 아래와 같은 클래스를 볼 수 있다. 게임에 플랫폼별 옵션을 추가하기 위해 빌드할 수 있는 클래스인데, 입력 매핑 컨텍스트를 1:1로 매핑해서 리디렉션해준다.
이때 이 EnhancedInputPlatformData는 다른 입력 관련 에셋과 달리 Input 탭에서 찾을 수 없는데, 블루프린트 생성에서 직접 Parent 클래스를 찾아주어 생성해야 한다.
이를 생성하려면 이런 식으로 EnhancedInputPlatformData 클래스를 상속받아 블루프린트 클래스를 만들어야 한다(C++도 가능하지만 그건 너무 큰 고생이다).
여러 개의 IMC를 매핑시켜준다.
내부적으로는 이렇게 1:1로 리디렉션이 가능한데, 이렇게 되면 특정 플랫폼에서 빌드할 때 왼쪽에 있는 입력 매핑 컨텍스트(IMC_Player)의 값을 오른쪽의 입력 매핑 컨텍스트로 대체해 주게 된다.
이 리디렉트를 적용하려면 프로젝트 설정(Project Setting) > 향상된 입력(Enhanced Input) > 플랫폼 설정(Platform Settings) > 입력 데이터(Input Data)에 EnhancedInputPlatformData의 자식 클래스를 넣어주면 된다.
실제 설정에 들어오면, 빌드 가능한 플랫폼별로 Input Data를 적용시켜줄 수 있다.
이 프로젝트 세팅은 DefaultInput.ini에 추가되니, 핫픽스를 적용할 때 아주 간단히 수정할 수 있다는 장점이 있다(ini 파일만 수정하면 되기 때문!).
 

참고 자료

향상된 입력 시스템에 대한 블로그 글https://bokki0117.tistory.com/75

댓글