관리 메뉴

기억을 위한 기록들

[Enhanced Input System] Lyra 프로젝트의 향상된 입력시스템 프레임워크 살펴보기 본문

UnrealEngine

[Enhanced Input System] Lyra 프로젝트의 향상된 입력시스템 프레임워크 살펴보기

에드윈H 2024. 5. 27. 21:35

기존에는 언리얼엔진에서는 플레이어 입력관련 바인딩 이벤트를 프로젝트 설정을 통해서 매핑을 시켜줬었다.

하지만, 언리얼 엔진4부터 존재하다가 5부터 본격적으로 어필하고 있는 새로운 프레임워크 Enhanced Input System 직역하면  "향상된 입력시스템"을 도입했다. 그래서 언리얼엔진 5 부터는 기존 프로젝트 설정을 통한 입력 설정을  들어가서 살펴보면, 

 

이제 사용하지 않을거라는 안내메시지가 나오면서, InputActions(IA)InputMappingContext(IMC) 를 사용하라고 안내해주고 있다. 향상된 입력 시스템은 우선 플러그인부터 활성화 해줘야한다.

 

활성화를 해준뒤, 애셋 생성창에서 보면

애셋 생성시 보이는 InputAction과  InputMappingContext

향상된 입력 시스템은 입력 액션(Input Actions) , 입력 매핑 컨텍스트(Input Mapping Contexts) , 입력 모디파이어(Input Modifiers) , 그리고 입력 트리거(Input Triggers) 크게 이렇게 4가지 주요 콘셉으로 이루어져 있다.

 

1. 입력 액션(Input Action)

공식문서를 살펴보면, 

입력 액션(IA) 은 향상된 입력 시스템과 프로젝트 코드 간의 통신 링크입니다.
입력 액션(IA)은 데이터 에셋이라는 점만 다를 뿐, 액션 및 축(Axis) 매핑 이름과 개념적으로 같습니다.
각 입력 액션은 '웅크리기'나 '무기 발사' 같은 사용자가 할 수 있는 행동을 나타내야 합니다. 입력 액션(IA)의 스테이트가 변경될 때 블루프린트 또는 C++에 입력 리스너(Input Listeners) 를 추가할 수 있습니다.
입력 액션(IA)은 여러 타입이 될 수 있으며, 타입에 따라 액션의 동작이 결정됩니다. 단순한 부울 액션을 만들 수도 있고 더욱 복잡한 3D 축을 만들 수도 있습니다. 액션 타입이 그 값을 결정합니다. 부울 액션은 단순한 bool 값을 갖고, Axis1D 는 float 값을, Axis2D 는 FVector2D , 그리고 Axis3D 는 전부 FVector 입니다. ...

 

요약하자면, 향상된 인풋과 프로젝트의 C++ 코드 간의 통신 시스템으로, 유저가 할 수 있는 행동을 나타낸다고 한다.

IA의 상태가 변할때, BP 또는 C++에 리스너를 추가할 수 있다. 다양한 타입과 다양한 입력 값이 될수 있고, 그 타입에 따라 액션이 결정된다고 한다.

Lyra 프로젝트 내부의 여러개의 IA 애셋들

입력액션(이하 IA)이라는 애셋을 생성하고, 그 내부에서 그 IA 애셋내부에서 해당 액션이 어떤 value type으로 사용될지 등을 지정해주며 4가지의 타입이 존재한다.

 

- Digital(bool)은 단순히 true 혹은 false 로서 입력 했다 or 안했다 등에 단순한 입력에 사용된다.

- Axis1D(float)은 -1.0 ~1 .0의 입력값을 받아 전달해준다.

- Axis2D(Vector2D)은  x 축의 -1.0 ~1 .0의 입력값과 또 다른 y축의 1.0 ~1 .0의 입력값  두 축의 값을 받아 전달해준다.

- Axis3D(Vector)은 Axis2D에서 추가로 한  더 총 3개의 값을 전달해줄수 있다.

 

IA_Move 예시

Value Type외에도 Triggers나 Modifiers 등이 있는데 추후에 살펴보기로 하자.

 

2. 입력매핑컨텍스트 (InputMappingContext - IMC)

 

생성된 각각의 IA들을 플레이어의 어떤 입력 키로 사용할지 지정 해주는 IMC가 있다.  공식문서를 살펴보면..

입력 매핑 컨텍스트(IMC) 는 플레이어가 처할 수 있는 특정 컨텍스트를 나타내는 입력 액션 컬렉션으로, 주어진 입력 액션의 트리거 규칙을 설명합니다. 각 사용자에게 매핑 컨텍스트를 동적으로 추가하거나 제거하거나 우선순위를 지정할 수 있습니다. -공식 문서 중 일부..

 

IA를 모아놓는 일종의 컬렉션으로 각각의 IA들에게 트리거 규칙을 설정해줍니다. 그리고 IMC 자체는 플레이중 동적으로 추가하거나, 제거하고 우선순위등을 지정 할 수 있다고 한다.

 

 

이렇게 생성된 IMC 애셋은 블루프린트를 통해서도 사용할수도 있고, 코드를 통해서도 사용할 수있다. 어떻게 사용하는지에 대한 방법까지 나열하면 글이 너무 길어져서 공식문서에 자세하게 나와 있어 패스하도록하겠다.

 

3. 입력 모디파이어(Input Modifier)

입력 모디파이어 는 입력 트리거로 보내기 전에 UE 에서 받는 원시 입력 값을 변경하는 프리 프로세서입니다. 향상된 입력 플러그인에는 축의 순서를 변경하고, '데드존'을 구현하고, 축 입력을 월드 스페이스로 변환하는 등 다양한 작업을 수행하기 위한 다양한 입력 모디파이어가 있습니다.
입력 모디파이어는 민감도 세팅을 적용하거나 여러 프레임에 걸쳐 입력을 스무딩하거나 플레이어 스테이트에 따라 입력 동작 방식을 변경하는 데 유용합니다.
자체 모디파이어를 만들 때 UPlayerInput 클래스에 액세스하므로 보유한 플레이어 컨트롤러(Player Controller)에 액세스하고 원하는 게임 스테이트를 가져올 수 있습니다.

 

이런 입력값들에 대해 실행하기전에 원시 입력값을 수정해주는 입력모디파이어라는게 있고, 해당 기능은 UInputModifier클래스를 상속받는 블루프린트 애셋이나 C++클래스를 만든 뒤, 내부에 ModifyRaw 라는 함수를 통해서 입력된 값에대한 수정을 진행한다.

기본제공하는 Swizzle Input Axis Values 내부클래스(InputModifiers.h) 중 일부

 


 

어떤 상황에 사용하는지 좀 헷갈렸는데 Lyra프로젝트에서의 사용예시를 살펴보니,

보통 게임 설정을 통해서 마우스 민감도 등을 조정해서 마우스 커서를 더 빠르게 혹은 느리게 수정 해주는데 이렇게 수정 된 값들을 실제 입력되는 마우스 값을 해당 민감도등으로 수정된 값으로 보간해주는 식이거나,

LyraInputModifiers.cpp 파일 중 일부

혹은 유저의 게임 설정에 따른 마우스 입력 좌우,상하 반전등으로 사용해주고 있었다.

 

이렇게 되면 개발자의 원하는 의도대로 입력에 대한 값을 수정을 가할수 있게 되는 것이다.

4. 입력 트리거(Input Trigger)

입력 트리거(Input Trigger)는 선택적 입력 모디파이어 목록을 패스스루(pass-through)한 후 사용자 입력이 입력 매핑 컨텍스트 내에서 해당 입력 액션을 활성화해야 할지 여부를 결정합니다. 대부분의 입력 트리거는 입력 자체를 분석하여 최소 발동 값을 확인하고, 짧은 탭 동작이나 길게 누르기 또는 전형적인 '누르기'나 '놓기' 이벤트 같은 패턴을 검증합니다. 이 규칙의 한 가지 예외는 다른 입력 액션을 통해서만 트리거되는 '조화된 액션' 입력 트리거입니다. 기본적으로 입력에 대한 모든 사용자 활동은 틱마다 트리거됩니다.

해당 입력트리거를 사용하게 되면  트리거 발동에 대해 update로 확인하며 유저가 의도된 트리거 이벤트를 실행하는지 확인 할 수 있다.

 

 

Lyra에서의 적용방식

//....

void ULyraHeroComponent::InitializePlayerInput(UInputComponent* PlayerInputComponent)
{
	check(PlayerInputComponent);

	const APawn* Pawn = GetPawn<APawn>();
	if (!Pawn)
	{
		return;
	}

	const APlayerController* PC = GetController<APlayerController>();
	check(PC);

	const ULyraLocalPlayer* LP = Cast<ULyraLocalPlayer>(PC->GetLocalPlayer());
	check(LP);

	UEnhancedInputLocalPlayerSubsystem* Subsystem = LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
	check(Subsystem);

	Subsystem->ClearAllMappings();

	if (const ULyraPawnExtensionComponent* PawnExtComp = ULyraPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
	{
		if (const ULyraPawnData* PawnData = PawnExtComp->GetPawnData<ULyraPawnData>())
		{
			if (const ULyraInputConfig* InputConfig = PawnData->InputConfig)
			{
				for (const FInputMappingContextAndPriority& Mapping : DefaultInputMappings)
				{
					if (UInputMappingContext* IMC = Mapping.InputMapping.Get())
					{
						if (Mapping.bRegisterWithSettings)
						{
							if (UEnhancedInputUserSettings* Settings = Subsystem->GetUserSettings())
							{
								Settings->RegisterInputMappingContext(IMC);
							}
							
							FModifyContextOptions Options = {};
							Options.bIgnoreAllPressedKeysUntilRelease = false;
							// Actually add the config to the local player							
							Subsystem->AddMappingContext(IMC, Mapping.Priority, Options);
						}
					}
				}

				// The Lyra Input Component has some additional functions to map Gameplay Tags to an Input Action.
				// If you want this functionality but still want to change your input component class, make it a subclass
				// of the ULyraInputComponent or modify this component accordingly.
				ULyraInputComponent* LyraIC = Cast<ULyraInputComponent>(PlayerInputComponent);
				if (ensureMsgf(LyraIC, TEXT("Unexpected Input Component class! The Gameplay Abilities will not be bound to their inputs. Change the input component to ULyraInputComponent or a subclass of it.")))
				{
					// Add the key mappings that may have been set by the player
					LyraIC->AddInputMappings(InputConfig, Subsystem);

					// This is where we actually bind and input action to a gameplay tag, which means that Gameplay Ability Blueprints will
					// be triggered directly by these input actions Triggered events. 
					TArray<uint32> BindHandles;
					LyraIC->BindAbilityActions(InputConfig, this, &ThisClass::Input_AbilityInputTagPressed, &ThisClass::Input_AbilityInputTagReleased, /*out*/ BindHandles);

					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Move, ETriggerEvent::Triggered, this, &ThisClass::Input_Move, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Mouse, ETriggerEvent::Triggered, this, &ThisClass::Input_LookMouse, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Look_Stick, ETriggerEvent::Triggered, this, &ThisClass::Input_LookStick, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_Crouch, ETriggerEvent::Triggered, this, &ThisClass::Input_Crouch, /*bLogIfNotFound=*/ false);
					LyraIC->BindNativeAction(InputConfig, LyraGameplayTags::InputTag_AutoRun, ETriggerEvent::Triggered, this, &ThisClass::Input_AutoRun, /*bLogIfNotFound=*/ false);
				}
			}
		}
	}

	if (ensure(!bReadyToBindInputs))
	{
		bReadyToBindInputs = true;
	}
 
	UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APlayerController*>(PC), NAME_BindInputsNow);
	UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APawn*>(Pawn), NAME_BindInputsNow);
}


//....

 

코드를 살펴보면 ULyraHeroComponent라는 컴포넌트를 캐릭터 클래스에 붙이면서 IMC를 지정하는 DefaultInputMappings 라는 변수를 사용하고 있다.

 

그렇게 Init이 되면 먼저 RegisterInputMappingContext 함수를 통해서 지정해준뒤,

별도의 ULyraPawnExtensionComponent 이라는 클래스를 통해 지정된 PawnData라는 데이터애셋에 내부에 있는 InputConfig를 불러와서 바인딩 시켜주고 있다.

 

ULyraInputComponent 내부 함수

 

https://docs.unrealengine.com/5.1/ko/enhanced-input-in-unreal-engine/

 

향상된 입력

향상된 입력 플러그인을 간략하게 살펴봅니다.

docs.unrealengine.com