[UMG/CommonUI] InputRouting

suyoung·2025년 12월 15일

UE5

목록 보기
11/12

CommonUI의 Input Routing

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/input-fundamentals-for-commonui-in-unreal-engine

CommonUI는 Slate/UMG 프레임 워크를 확장한 시스템으로, 입력 라우팅을 자체적인 방법으로 구현하지만, 기본적으로 Slate에 이미 존재하는 입력 처리 시스템의 로직을 의존한다.

Input Config을 사용하여 애플리케이션의 UI 입력 처리 방식 변경

현재 활성화된 위젯에 따라 애플리케이션의 입력 처리 방식을 변경하고 싶을 수도 있다.

예를 들어, 일시 정지 메뉴가 열려 있을 때는 플레이어가 게임 월드에서 이동하지 못하도록 하고 싶을 수도 있는데, 이를 위해 CommonUI는 Activatable Widget에 대해 선택적으로 InputConfig를 지원한다.

  • CommonActivatableWidget
	/**
	 * 이 위젯이 활성화되어 입력을 받을 수 있게 될 때 ( 즉, 모든 부모 위젯 또한 활성화 상태일 때 )
	 * 적용하고자 하는 입력 설정을 반환한다.
	 * 이 설정은 이전에 활성화된 다른 Activatable Widget에 의해 설정되어 있던 기존 입력 설정을
	 * 덮어쓰며, 위젯이 비활성화될 때 해당 설정이 유효하다면 원래 설정으로 복원된다.
	 */
	virtual TOptional<FUIInputConfig> GetDesiredInputConfig() const;
  • Lyra Project - ULyraActivatableWidget.cpp
UENUM(BlueprintType)
enum class ECommonInputMode : uint8
{
	// UI Only - 인게임으로 포커싱이 안감
	Menu	UMETA(Tooltip = "Input is received by the UI only"),
	// Game Onlye - UI로 포커싱이 안감
	Game	UMETA(Tooltip = "Input is received by the Game only"),
	// UI/Game 모두 포커싱 감. 클릭된 위치에 따라서 달라짐 (UI 존재 여부)
	All		UMETA(Tooltip = "Input is received by UI and the Game"),

	MAX UMETA(Hidden)
};

Widgets에서 Input Configs 사용하기

Input Configs는 FUIInputConfig 구조체(UIActionBindingHandle.h)로 표현된다. 각 Input Config는 다음을 포함한 여러 입력 방식의 상태를 추적한다.
Activatable Widget이 활성화될 때, CommonUI는 UCommonActivatableWidget::GetDesiredInputConfig를 호출하여 해당 위젯에 적용할 InputConfig를 가져온다.

이 함수는 기본적으로 아래 이미지 InputConfig가 기본으로 설정된다. 그리고, 원한다면 Lyra 프로젝트 처럼 임의의 로직으로 오버라이드 할 수 있다.

CommonUI는 마지막으로 사용된 유효한 Input Config를 그대로 사용하며, Leaf Node의 Input Config를 적용한다.

이 동작은 CommonInputSettings.h 에 정의된 bEnableDefaultInputConfig 변수를 통해 비활성화할 수 있다.

	/**
	* 활성화된 CommonActivatableWidget에서 별도의 Input Config를 지정하지 않았을 때
	* 기본 Input Config를 적용할지 여부를 제어한다.
	* 입력 모드를 다른 매커니즘으로 관리하는 경우에 해당 옵션을 끌 수 있다.
	*/
	// false일 때 Slate와 Viewport/PlayerController로 각자 판단해 입력 처리 진행 -> 확인할 필요가 있음.
	UPROPERTY(config, EditAnywhere, Category = "Input")
	bool bEnableDefaultInputConfig = true;
  • Project Settings… → Enable Default Input Config = false 처리시 CommonUI InputConfig 비활성화 가능
  • 비활성화시 ApplyLeafmostNodeConfig 함수에서 UIInputConfig 처리하는 부분이 실행되지 않음.

    위젯이 비활성화 될 때, CommonUI는 현재 지원할 수 있는 InputConfig가 없는 상태에 빠지는 것을 방지하기 위해서 이전에 사용하던 InputConfig를 복원한다.
// ApplyLeafmostNodeConfig 함수 내부
if (FActivatableTreeNodePtr PinnedLeafmostNode = LeafmostActiveNode.Pin())
	{
		GetActionRouter().SetActiveActivationMetadata(PinnedLeafmostNode->FindActivationMetadata());

		if (ensure(PinnedLeafmostNode->IsReceivingInput()))
		{
			UE_LOG(LogUIActionRouter, Display, TEXT("Applying input config for leaf-most node [%s]"), *PinnedLeafmostNode->GetWidget()->GetName());

			TOptional<FUIInputConfig> DesiredConfig = PinnedLeafmostNode->FindDesiredInputConfig();
			
			// GetEnableDefaultInputConfig -> bEnableDefaultInputConfig = true 일 때 사용!!!!
			// false라면, 아래 동작 자체가 안될 것
			if(DesiredConfig.IsSet())
			{
				GetActionRouter().SetActiveUIInputConfig(DesiredConfig.GetValue(), PinnedLeafmostNode->GetWidget());
			}
			else if(ICommonInputModule::GetSettings().GetEnableDefaultInputConfig())
			{
				// 전체 트리에서 Input Config를 요구하는 위젯이 없으면, 기본 InputConfig를 적용한다.
				GetActionRouter().SetActiveUIInputConfig(FUIInputConfig());
			}

			FocusLeafmostNode();
		}
  • 예)

ESC를 눌러 메뉴 창 → ESC를 눌러 다시 인게임 HUD로 돌아가는 과정

  • W_LyraGameMenu_C → FindDesiredInputConfig 로, TOptional DesiredConf용
  • 즉, 가장 최하단의 Leaf Node 위젯의 Input Config 사용

권장 사용 방식

InputConfig 을 사용하고 있다면, UI에서 표준 입력 설정 방식(기존 UE 입력 설정 함수들)을 함께 사용하는 것은 피하는 것이 좋다.

UCommonUIActionRouterBase::ApplyUIInputConfig 가상 함수의 기본 구현은 입력 설정 과정에서 다음과 같은 언리얼 엔진의 표준 입력 설정 함수들을 호출한다.

  • APlayerController::SetIgnoreMoveInput

  • UGameViewportClient::SetMouseCaptureMode

  • UGameViewportClient::SetHideCursorDuringCapture

  • UCommonUIActionRouterBase::ApplyUIInputConfig

    • CommonUI를 사용하지 않을 때(InputConfig 구조체), 직접 Input Mode를 변경해주었던 걸 하나의 구조체로 해결할 수 있도록 만들어놓은것.

    • 그래서, 구조체 정보에 따라서 ApplyUIInputConfig 함수에서 PlayerController/마우스 커서/Viewport 등을 한꺼번에 관리하고 있음. (즉, 중앙처리적임.)

      void UCommonUIActionRouterBase::ApplyUIInputConfig(const FUIInputConfig& NewConfig, bool bForceRefresh)
      {
      	// 강제로 재로딩할 필요가 있거나, 현재 활성화된 InputConfig가 새로 들어온 InputConfig와 다를 경우에 업데이트 진행
      	// 매 호출되는 것은 아님, 값 비교를 수행해서 다를 경우를 체크함. 
      	// (값 비교인가는 FUIInputConfig ==operator/!=operator 정의를 보면된다.)
      	if (bForceRefresh || NewConfig != ActiveInputConfig.GetValue())
      	{
      		UE_LOG(LogUIActionRouter, Display, TEXT("UIInputConfig being changed. bForceRefresh: %d"), bForceRefresh ? 1 : 0);
      		UE_LOG(LogUIActionRouter, Display, TEXT("\tInputMode: Previous (%s), New (%s)"),
      			ActiveInputConfig.IsSet() ? *StaticEnum<ECommonInputMode>()->GetValueAsString(ActiveInputConfig->GetInputMode()) : TEXT("None"), *StaticEnum<ECommonInputMode>()->GetValueAsString(NewConfig.GetInputMode()));
      		
      		// 이전 값을 함수 내부의 지역 변수로 보관하고, 
      		// 현재 새로운 입력 값으로 덮어쓰기 진행
      		const ECommonInputMode PreviousInputMode = GetActiveInputMode();
      
      		TOptional<FUIInputConfig> OldConfig = ActiveInputConfig;
      		ActiveInputConfig = NewConfig;
      
      		ULocalPlayer& LocalPlayer = *GetLocalPlayerChecked();
      
      		// SlitScreen 환경에서는 정상적으로 동작하지 않을 수 있음.
      		// 마우스 캡처를 위해서는 플레이어별(Viewport 기준)설정이 필요하다.
      		// 아래 if문 부터는 실제로, CommonUI의 InputConfig 파일을 설정하지 않았을때(비활성화했을때)
      		// APlayerController/UGameViewportClient/UGameViewportClient -> 각자 설정해야할 것을
      		// 하나로 묶어서 이전에 정의된 대로 처리해주고 있음
      		if (UGameViewportClient* GameViewportClient = LocalPlayer.ViewportClient)
      		{
      			if (TSharedPtr<SViewport> ViewportWidget = GameViewportClient->GetGameViewportWidget())
      			{
      				if (APlayerController* PC = LocalPlayer.GetPlayerController(GetWorld()))
      				{
      					if (!OldConfig.IsSet() || OldConfig.GetValue().bIgnoreMoveInput != NewConfig.bIgnoreMoveInput)
      					{
      						PC->SetIgnoreMoveInput(NewConfig.bIgnoreMoveInput);						
      					}
      					
      					if (!OldConfig.IsSet() || OldConfig.GetValue().bIgnoreLookInput != NewConfig.bIgnoreLookInput)
      					{
      						PC->SetIgnoreLookInput(NewConfig.bIgnoreLookInput);						
      					}
      
      					if (bAutoFlushPressedKeys && NewConfig.GetInputMode() == ECommonInputMode::Menu && PreviousInputMode != NewConfig.GetInputMode())
      					{
      						// Menu InputMode 전환 이후 눌려 있던 키 입력을 초기화한다.
      						// 이를 통해 입력이 인위적으로 "계속 눌린 상태"로 인식되는 것을 방지한다.
      						// 이 처리는 현재 프레임의 끝에서 캡쳐된 입력을 정상적으로 제거하기 위해 
      						// 한 프레임 지연시켜야 한다.
      						
      						// 예시)
      						// W키를 눌러서 캐릭터가 이동 중인데, ESC를 눌러서 메뉴 UMG가 뜨면 인게임 내부에 Released 이벤트를 상실해버림
      						// 왜냐하면, Game -> Menu로 변경되었기 때문임. 
      						// W Released 이벤트는 game Input으로 도달하지 못하고, 그래서 엔진은 W를 계속 눌린 상태로 기억한다.
      						// 이를 막기 위해서 UI 전환 시 입력 상태를 강제로 초기화한다.
      						GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::FlushPressedKeys);
      					}
      					
      					// 커서 상태 반영
      					const bool bWasCursorHidden = !PC->ShouldShowMouseCursor();
      
      					GameViewportClient->SetMouseCaptureMode(NewConfig.GetMouseCaptureMode());
      					GameViewportClient->SetHideCursorDuringCapture(NewConfig.HideCursorDuringViewportCapture() && !ShouldAlwaysShowCursor());
      					GameViewportClient->SetMouseLockMode(NewConfig.GetMouseLockMode());
      
      					FReply& SlateOperations = LocalPlayer.GetSlateOperations();
      					const EMouseCaptureMode CaptureMode = NewConfig.GetMouseCaptureMode();
      					switch (CaptureMode)
      					{
      					case EMouseCaptureMode::CapturePermanently:
      					case EMouseCaptureMode::CapturePermanently_IncludingInitialMouseDown:
      					{
      						PC->SetShowMouseCursor(ShouldAlwaysShowCursor() || !NewConfig.HideCursorDuringViewportCapture());
      
      						TSharedRef<SViewport> ViewportWidgetRef = ViewportWidget.ToSharedRef();
      						SlateOperations.UseHighPrecisionMouseMovement(ViewportWidgetRef);
      						SlateOperations.SetUserFocus(ViewportWidgetRef);
      						SlateOperations.CaptureMouse(ViewportWidgetRef);
      
      						if (GameViewportClient->ShouldAlwaysLockMouse() || GameViewportClient->LockDuringCapture() || !PC->ShouldShowMouseCursor())
      						{
      							SlateOperations.LockMouseToWidget(ViewportWidget.ToSharedRef());
      						}
      						else
      						{
      							SlateOperations.ReleaseMouseLock();
      						}
      					}
      					break;
      					case EMouseCaptureMode::NoCapture:
      					case EMouseCaptureMode::CaptureDuringMouseDown:
      					case EMouseCaptureMode::CaptureDuringRightMouseDown:
      					{
      						PC->SetShowMouseCursor(true);
      
      						SlateOperations.ReleaseMouseCapture();
      
      						if (GameViewportClient->ShouldAlwaysLockMouse())
      						{
      							SlateOperations.LockMouseToWidget(ViewportWidget.ToSharedRef());
      						}
      						else
      						{
      							SlateOperations.ReleaseMouseLock();
      						}
      					}
      					break;
      					}
      
      					// 이전에 마우스 커서가 숨겨져 있다면,
      					// 다시 표시하는 시점에 커서를 뷰포트 중앙으로 되돌린다.
      					if (!bForceRefresh && bWasCursorHidden && PC->ShouldShowMouseCursor())
      					{
      						const ECommonInputType CurrentInputType = GetInputSubsystem().GetCurrentInputType();
      						
      						bool bCenterCursor = true;
      						switch (CurrentInputType)
      						{
      							// 터치 - 처리하지 않는다. (커서 개념이 의미가 없기 때문)
      							case ECommonInputType::Touch:
      								bCenterCursor = false;
      								break;
      							// 게임패드 - 중앙으로 이동시킬지 여부는 설정에서 판단
      							case ECommonInputType::Gamepad:
      								break;
      						}
      
      						if (bCenterCursor)
      						{
      							// 커서를 중앙으로 이동시키는 코드
      							TSharedPtr<FSlateUser> SlateUser = LocalPlayer.GetSlateUser();
      							TSharedPtr<IGameLayerManager> GameLayerManager = GameViewportClient->GetGameLayerManager();
      							if (ensure(SlateUser) && ensure(GameLayerManager))
      							{
      								FGeometry PlayerViewGeometry = GameLayerManager->GetPlayerWidgetHostGeometry(&LocalPlayer);
      								const FVector2D AbsoluteViewCenter = PlayerViewGeometry.GetAbsolutePositionAtCoordinates(FVector2D(0.5f, 0.5f));
      								SlateUser->SetCursorPosition(AbsoluteViewCenter);
      
      								UE_LOG(LogUIActionRouter, Verbose, TEXT("Moving the cursor to the viewport center."));
      							}
      						}
      					}
      				}
      				else
      				{
      					UE_LOG(LogUIActionRouter, Warning, TEXT("\tFailed to commit change! Local player controller was null."));
      				}
      			}
      			else
      			{
      				UE_LOG(LogUIActionRouter, Warning, TEXT("\tFailed to commit change! ViewportWidget was null."));
      			}
      		}
      		else
      		{
      			UE_LOG(LogUIActionRouter, Warning, TEXT("\tFailed to commit change! GameViewportClient was null."));
      		}
      
      		if (PreviousInputMode != NewConfig.GetInputMode())
      		{
      			// 게임 모드 변경에 대한 브로드 캐스트
      			OnActiveInputModeChanged().Broadcast(NewConfig.GetInputMode());
      		}
      	}
      }
      

입력 처리 상태 레퍼런스

FInputConfig는 여러 입력 상태를 하나의 번들(묶음)로 처리한다. UCommonActivatableWidget::GetDesiredInputConfig 에서 Input Config를 설정하면, 해당 위젯이 포커스를 가질 때 입력이 어떻게 동작해야 하는지에 대한 완전한 구성을 제공한다.

변수Type설명
InputModeEnum/ECommonInputModeCommonUI의 내부 InputMode를 설정
  • Game : 입력은 게임에만 전달

  • Menu : 입력은 UI에만 전달

  • All : 입력이 UI/게임 모두에 전달 |
    | MouseCaptureMode | Enum/EMouseCaptureMode | CommonUI의 마우스 모드를 설정

  • CapturePermanently : 뷰포트를 클릭하면 마우스를 영구적으로 캡쳐하며, 캡처를 발생시킨 최초의 마우스 버튼 입력은 소비하여 플레이어 입력으로 처리되지 않도록 한다.

  • CapturePermanently_IncludingInitialMouseDown : CapturePermently와 동일하게 작동하지만, 캡처를 발생시킨 마우스 버튼 입력이 게임 플레이 입력으로도 전달된다.

  • CaptureDuringMouseDown : 마우스 버튼이 눌려있는 동안 마우스를 캡처하고, 버튼을 떼면 캡처를 해제한다.

  • CaptureDuringRightMouseDown : 마우스 오른쪽 버튼이 눌려져 있을 때만 캡처하며, 다른 마우스 버튼들은 캡처하지 않는다. |
    | bHideCursorDuringViewportCapture | Bool | true일 경우, 마우스 캡처 중 뷰포트에서 커서를 숨긴다. |
    | bIgnoreMoveInput | Bool | true일 경우, 플레이어 컨트롤러가 이동 입력을 무시한다. |
    | bIgnoreLookInput | Bool | true일 경우, 플레이어 컨트롤러가 시점 입력을 무시한다. |

FReply를 사용하여 위젯의 입력 반응 방식 변경하기

FReply는 입력 이벤트가 처리되었는지(Handled) 또는 처리되지 않았는지(Unhandled) 상태를 추적한다.

Slate의 대부분의 입력 처리 함수는 FReply::Handled 또는 FReply::Unhandled 중 하나를 반환한다.

  • FReply::Handled : 해당 입력이 처리되었음을 의미하며, 일반적으로 다른 위젯이나 입력 시스템으로 전달되지 않아야 함
  • FReply::Unhandled : 입력 방식으로든 사용되었더라도, 추가 처리를 위해 다른 위젯이나 입력 시스템으로 계속 전달되어야 함을 의미 함.

자주 사용되는 SWidget 입력 이벤트 함수들

FReply OnKeyUp(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent);
FReply OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& InAnalogInputEvent);
FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent);
void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent);

⇒ 전부는 아니지만, 많은 함수들은 FReply 를 반환한다.

이 반환 값은 블루프린트에서도 설정하거나 오버라이드할 수 있으므로, 특정 유형의 입력 처리를 차단하거나 허용하고 싶을 때 원하는 FReply를 반환하는 방식으로 제어할 수 있다.

다만, 대부분의 경우에는 기본 FReply 반환값으로도 잘 설계된 위젯 또는 위젯 집합에서는 충분하다. 커스텀 FReply를 직접 다루는 일은 주로 Slate 위젯을 직접 작성할 필요가 있을 때 사용된다.

  • FReply 입력 라우팅 흐름을 나타내는 다이어그램으로, 입력은 먼저 플랫폼 자체의 입력 이벤트에서 시작되며, 이후 Slate Application으로 전달된다. Slate Application은 해당 입력을 위젯들에게 전달하고, 각 위젯은 FReply를 사용해 입력이 Handled(처리됨) 인지 Unhandled(처리되지 않음)인지를 결정한다.
  • 이 과정은 입력이 Handled로 처리되거나, 또는 모든 위젯이 검사될 때까지 반복된다.

FReply는 입력 이벤트가 처리되었는지(Handled) 또는 처리되지 않았는지(Unhandled) 상태를 추적한다. 또한, FReply 에는 이러한 처리 상태 외에도, 다음과 같은 추가 데이터들을 함께 설정하고 추적할 수 있다.

CaptureMouse모든 마우스 이벤트를 특정 위젯으로 전달하도록 시스템에 요청한다.
ClearUserFocus시스템에 사용자 포커스 초기화를 요청한다.
ReleaseMouseCapture마우스 캡처를 해제하도록 시스템에 요청한다.
SetUserFocus지정된 위젯으로 사용자 포커스를 설정하도록 시스템에 요청한다.
SetNavigation지정된 목적지로 네비게이션을 시도하도록 시스템에 요청한다.

FReply::CaptureMouse나 FReply::SetUserFocus와 같은 일부 이벤트들은 대상 위젯을 포함한 추가 인자를 받는다. 이 함수들은 FReply 네임스페이스에 속해 있으며, 이는 Slate가 FReply를 처리하는 과정에서 발생하는 동작을 직접 수정할 수 있음을 의미한다.

FReply 안에서 이러한 함수들을 호출하면, FReply 외부에서 동일한 함수를 호출했을 때와는 완전히 동일하게 재현하기 어려운, 약간 다른 동작을 얻을 수 있다.

⇒ 외부에서 동작의 의미 :

FSlateApplication::Get().SetUserFocus(MyWidget);
// or
MyWidget->SetFocus();
// 입력 이벤트 처리와 다르게 동작할 수 있음.

언제 FReply를 설정해야 할까?

FReply를 사용해야 하는 상황의 예로, 키 입력 시 위젯 포커스를 설정하거나 해제해야 하는 경우를 볼 수 있다.

일반적으로는 키 입력 핸들러 안에서 FSlateApplication의 관련 함수를 직접 호출하여 위젯 포커스를 변경하려고 시도할 수 있다. ( 위 코드 방식 )

하지만, 이러한 방식은 모든 상황에서 정상적으로 동작하지 않을 수 있으며, 특히 Input Routing을 사용하고 있는 경우에 문제가 발생할 수 있다. 이는 현재 위젯에서 입력이 아직 처리 중인 상태에서 포커스를 변경하거나 해제하려고 시도하기 때문이다. 이러한 입력 흐름은 원하지 않는 동작으로 이어질 수 있다.

대신 Slate가 입력을 완전히 처리하도록 맡긴 뒤, 포커스 변경과 같은 작업은 입력 이벤트의 응답값인 FReply를 통해 처리하는 것을 권장합니다.

⇒ 이해한 바로는 이벤트 함수 내부에서 처리하는 것을 권장한다는 뜻인 것 같다. 그 이유는 직접적으로 변경할 경우에 타이밍에 따라 원하는 동작대로 흘러가지 않을 수 있기 때문이다.

⇒ Slate가 입력 처리를 완료 한 이후에 해당 변경을 적용되도록 하는 방식이다.

UI 네비게이션 커스터마이징

CommonUI에서 네비게이션을 커스터마이징하기 위한 가이드라인과 다양한 옵션을 제공한다.

네비게이션 설정은 CommonUI와 직접적인 관련은 없지만, 입력 처리 방식을 이해하는 데 도움이 된다.

Slate는 CommonUI 활성화 여부와 관계없이 기본적인 4 방향 네비게이션을 기본적으로 제공한다. FNavigationConfig를 통해 각 방향에 대응하는 입력 키를 설정 할 수 있다.

Navigation Config를 설정하려면 FSlateApplication::SetNavigationConfig를 호출하면 된다. 일반적으로 FNavigationConfig를 상속한 커스텀 네비게이션 설정 클래스를 사용하여 이 할수를 호출한다.

예를 들어, 사용자가 WASD 키를 사용해 UI를 조작하도록 하고 싶다면, 이 지점이 설정을 시작하기 가장 적합한 위치이다.

  • 예시인데, 동작 여부를 확인하지 못함. 확인해보기.
class FZNavigationConfig : public FNavigationConfig
{
public:
	FZNavigationConfig();
}

FZNavigationConfig ::FZNavigationConfig ()
	: bTabNavigation(true)
	, bKeyNavigation(true)
	, bAnalogNavigation(true)
	, bIgnoreModifiersForNavigationActions(true)
	, AnalogNavigationHorizontalThreshold(0.50f)
	, AnalogNavigationVerticalThreshold(0.50f)
{
	AnalogHorizontalKey = EKeys::Gamepad_LeftX;
	AnalogVerticalKey = EKeys::Gamepad_LeftY;

	KeyEventRules.Emplace(EKeys::D, EUINavigation::Left);
	KeyEventRules.Emplace(EKeys::Gamepad_DPad_Left, EUINavigation::Left);

	KeyEventRules.Emplace(EKeys::A, EUINavigation::Right);
	KeyEventRules.Emplace(EKeys::Gamepad_DPad_Right, EUINavigation::Right);

	KeyEventRules.Emplace(EKeys::W, EUINavigation::Up);
	KeyEventRules.Emplace(EKeys::Gamepad_DPad_Up, EUINavigation::Up);

	KeyEventRules.Emplace(EKeys::S, EUINavigation::Down);
	KeyEventRules.Emplace(EKeys::Gamepad_DPad_Down, EUINavigation::Down);
}

TSharedPtr<FSlateUser> User = FSlateApplication::Get().GetUser(UserIndex);
User->SetNavigationConfig(Config);
// or
FSlateApplication::Get().SetNavigationConfig(NewConfig);

네비게이션 수동 제어

네비게이션 이벤트가 발생했을 때의 동작을 수동으로 설정하려면, UMG에서 위젯을 선택한 뒤 Details 패널의 Navigation 섹션을 찾는다. 이 섹션에서는 상하좌우 각 방향에 대한 옵션들이 포함되어져 있다.

| 네비게이션 컨트롤 옵션 | 설명 |
| --- | --- |
| Escape | 해당 방향으로의 이동을 허용하며, 다음으로 네비게이션 가능한 위젯을 자동으로 탐색한다. |
| Explicit | 특정 위젯으로 이동한다. |
| Wrap | 네비게이션이 컨테이너를 벗어날 경우, 반대편으로 순환되도록 이동을 래핑한다. |
| Stop | 해당 방향으로의 이동을 중단한다. |
| Custom | 네비게이션 동작을 사용자 정의 코드에서 직접 처리한다. |
| CustomBoundary | 네비게이션이 경계에 도달하면, 해당 동작을 사용자 정의 코드로 처리한다. |

	/**
	 * 이 위젯에 대한 네비게이션 객체는,
	 * 위젯 디자이너에서 사용자가 커스텀 네비게이션 규칙을 설정한 경우에 한해
	 * 선택적으로 생성된다.
	 * 
	 * 이러한 규칙들은 위젯 간에 네비게이션 전환이
	 * 어떻게 이루어질지를 결정한다.
	 */
	UPROPERTY(Instanced, EditAnywhere, BlueprintReadOnly, Category="Navigation")
	TObjectPtr<class UWidgetNavigation> Navigation;
	
 /**
 * 특정 방향에 대한 위젯 네비게이션 규칙을 설정합니다.
 * 이 함수는 위젯 트리에 포함된 위젯에서만 호출할 수 있으며,
 * Explicit 규칙에서만 동작합니다.
 *
 * @param Direction 네비게이션이 발생하는 방향
 * @param InWidget 해당 방향 입력 시 포커스를 이동시킬 위젯 인스턴스
 */
	UFUNCTION(BlueprintCallable, Category = "Widget")
	UMG_API void SetNavigationRuleExplicit(EUINavigation Direction, UWidget* InWidget);

Activatable 위젯 활성화 시 포커스 설정하기

Activatable Widget이 활성화될 때마다, UCommonActivatableWidget::GetDesiredFocusWidgetTarget 함수가 호출되어 CommonUI가 사용자의 입력 포커스를 어떤 위젯에 둘지 결정한다.

  • 블루프린트 내부에서는 DesiredFocusWidget을 설정하여, 초기에 포커싱될 위젯을 설정할 수 있음.
/**
 * 이 위젯이 주요 활성 위젯이 되었을 때
 * 사용자 입력 포커스를 받을 위젯을 반환하도록 오버라이드합니다.
 *
 * bAutoRestoreFocus가 true라면,
 * 이전 포커스 복원 대상이 없을 경우에만
 * 기본 포커스 대상을 제공하기 위해 호출됩니다.
 */
	virtual UWidget* NativeGetDesiredFocusTarget() const; //아래 위젯 이용
	
	/** @returns 이 UserWidget이 포커스를 획득할 때
	 *          포커스를 받을 대상 위젯을 반환합니다.
	 */
	UWidget* GetDesiredFocusWidget() const;
	

NativeGetDesiredFocusTarget 의 커스텀 구현을 제공하지 않으면, 위젯이 활성화되거나 비활성화 될 때 CommonUI가 어디에 포커스를 두어야 할지 판단하는데 어려움을 겪을 수 있다. 이러한 이유로 Activatable Widget에서 이 함수를 반드시 구현할 것을 강력히 권장한다.

  • Lyra 프로젝트 내부에서
// GameSettingPanel을 감싸고 있는 GameSettingScreen
UWidget* UGameSettingScreen::NativeGetDesiredFocusTarget() const
{
	if (UWidget* Target = BP_GetDesiredFocusTarget())
	{
		return Target;
	}

	// Target이 정해져 있지 않으면, Settings_Panel을 기본 포커싱으로 정함
	// 포커싱을 정할때 NativeOnFocusReceived 함수 호출
	return Settings_Panel;
}

// 하위 클래스 내부에서 포커싱 잡혔을 때 호출
FReply UGameSettingPanel::NativeOnFocusReceived(const FGeometry& InGeometry, const FFocusEvent& InFocusEvent)
{
	const UCommonInputSubsystem* InputSubsystem = GetInputSubsystem();
	if (InputSubsystem && InputSubsystem->GetCurrentInputType() == ECommonInputType::Gamepad)
	{
		if (TSharedPtr<SWidget> PrimarySlateWidget = ListView_Settings->GetCachedWidget())
		{
			// 리스트 뷰의 가장 첫 위젯에게 네비게이션 갈 수 있도록 설정
			ListView_Settings->NavigateToIndex(0);
			ListView_Settings->SetSelectedIndex(0);

			return FReply::Handled();
		}
	}

	return FReply::Unhandled();
}
  • 블루프린트 내부에서도 아래처럼 정의해 놓았음.
  • 그리고, Lyra 프로젝트 내부에서 반드시 구현하도록, 경고를 출력할 수 있는 함수 구현
// 해당 함수를 상속받았지만, BP_GetDesiredFocusTarget을 구현하지 않았을 때 오류를 출력
void ULyraActivatableWidget::ValidateCompiledWidgetTree(const UWidgetTree& BlueprintWidgetTree, class IWidgetCompilerLog& CompileLog) const
{
	Super::ValidateCompiledWidgetTree(BlueprintWidgetTree, CompileLog);

	if (!GetClass()->IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(ULyraActivatableWidget, BP_GetDesiredFocusTarget)))
	{
		if (GetParentNativeClass(GetClass()) == ULyraActivatableWidget::StaticClass())
		{
			CompileLog.Warning(LOCTEXT("ValidateGetDesiredFocusTarget_Warning", "GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen."));
		}
		else
		{
			//TODO - Note for now, because we can't guarantee it isn't implemented in a native subclass of this one.
			CompileLog.Note(LOCTEXT("ValidateGetDesiredFocusTarget_Note", "GetDesiredFocusTarget wasn't implemented, you're going to have trouble using gamepads on this screen.  If it was implemented in the native base class you can ignore this message."));
		}
	}
}

입력 액션이 트리거되는 시점 변경하기

액션 바인딩을 위해 FBindUIActionArgs 를 생성할 때, FBindUIActionArgs::KeyEvent를 설정하여 어떤 입력 이벤트 타입에서 액션이 트리거 될지 지정할 수 있다.

profile
게임 클라이언트 프로그래머

0개의 댓글