- CommonUI의 Input System은 Cross-Platform을 대응할 뿐 아니라 복잡한 다중 Layer Menu 관리에도 용이하다.
- 아래 내용은 Common UI Input System의 작동 방식에 대한 내용이다.
Gamepad Navigation using Synthetic Cursor
- Common UI에서 Gamepad Input은 보이지 않는 Synthetic Cursor를 기반으로 동작한다.
- 때문에 Mouse를 사용하도록 UI를 설정할 경우, Common UI가 동작함에 있어 필요한 것은 2가지 뿐이다.
- 보이지 않은 Cursor가 올바른 위치에 있을 것
- Mouse와 같은 Click 입력을 받을 것.
- 이 구조는 Cross-Platform에서의 모든 Input을 하나의 Input Path로 간소화 한다.
- 때문에 Mouse를 사용하도록 UI를 설정할 경우, Common UI가 동작함에 있어 필요한 것은 2가지 뿐이다.
Click with Synthetic Cursor/Gamepad
- Gamepad의 Accept 혹은 Default Click을 한다고 가정하자.
- Virtual Accept Key는 대체로 EKeys::Virtual_Accept로 Mapping 된다.
- 내부적으로 Input Flow는 GenericApplication으로부터 파생된 Platform Application에서 시작된다.
- 예를 들어 Windows의 경우 FWindowsApplication을 사용한다.
bool FSlateApplication::OnKeyDown( const int32 KeyCode, const uint32 CharacterCode, const bool IsRepeat )
{
FKey const Key = FInputKeyManager::Get().GetKeyFromCodes( KeyCode, CharacterCode );
FKeyEvent KeyEvent(Key, PlatformApplication->GetModifierKeys(), GetUserIndexForKeyboard(), IsRepeat, CharacterCode, KeyCode);
return ProcessKeyDownEvent( KeyEvent );
}
- 이 Input은 FSlateApplication::ProcessKeyDownEvent로 처리가 된다.
- ProcessKeyDownEvent는 Input 처리를 위해 IInputProcessor Interface를 Implement하고,
가능하면 Input을 처리하는 Input Processor를 사용한다. - Input이 처리되면, 추가 Input 처리가 막히게 됩니다.
- ProcessKeyDownEvent는 Input 처리를 위해 IInputProcessor Interface를 Implement하고,
더보기
bool FSlateApplication::ProcessKeyDownEvent( const FKeyEvent& InKeyEvent )
{
/*...*/
// Analog cursor gets first chance at the input
if (InputPreProcessors.HandleKeyDownEvent(*this, InKeyEvent))
{
return true;
}
/*...*/
}
- FCommonAnalogCursor는 FAnalogCursor와 마찬가지로 IInputProcessor의 구현체이다.
- FcommonAnalogCursor는 현재 Widget의 Bound Action으로 캡처되지 않은
Gamepad의 표준 Accept Action Input을 처리하지 않는다.
bool FCommonAnalogCursor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
{
if (IsRelevantInput(InKeyEvent))
{
/*....*/
if (bIsVirtualAccept && ActionRouter.ProcessInput(InKeyEvent.GetKey(), InputEventType) == ERouteUIInputResult::Handled)
{
return true;
}
else if (!bIsVirtualAccept || ShouldVirtualAcceptSimulateMouseButton(InKeyEvent, IE_Pressed))
{
// There is no awareness on a mouse event of whether it's real or not, so mark that here.
UCommonInputSubsystem& InputSubsytem = ActionRouter.GetInputSubsystem();
InputSubsytem.SetIsGamepadSimulatedClick(bIsVirtualAccept);
bool bReturnValue = FAnalogCursor::HandleKeyDownEvent(SlateApp, InKeyEvent);
/*....*/
}
}
return false;
}
- 대신 Input을 HandleKeyDownEvent로 전달한다.
bool FAnalogCursor::HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent)
{
if (IsRelevantInput(InKeyEvent))
{
FKey Key = InKeyEvent.GetKey();
/*...*/
// Bottom face button is a click
if (Key == EKeys::Virtual_Accept)
{
if (!InKeyEvent.IsRepeat())
{
if (TSharedPtr<FSlateUser> SlateUser = SlateApp.GetUser(InKeyEvent))
{
const bool bIsPrimaryUser = FSlateApplication::CursorUserIndex == SlateUser->GetUserIndex();
FPointerEvent MouseEvent(
SlateUser->GetUserIndex(),
FSlateApplication::CursorPointerIndex,
SlateUser->GetCursorPosition(),
SlateUser->GetPreviousCursorPosition(),
bIsPrimaryUser ? SlateApp.GetPressedMouseButtons() : TSet<FKey>(),
EKeys::LeftMouseButton,
0,
bIsPrimaryUser ? SlateApp.GetModifierKeys() : FModifierKeysState()
);
TSharedPtr<FGenericWindow> GenWindow;
return SlateApp.ProcessMouseButtonDownEvent(GenWindow, MouseEvent);
}
}
return true;
}
}
return false;
}
- 이후, FSlateApplication에서 처리할 Synthetic Mouse Click Event를 생성한다.
- 여기까지 진행되면 Mouse Event는 Regular Click과 유사한 Input Process를 거치고, 최종 Click을 Trigger한다.
- 좀 더 자세한 흐름을 보고 싶다면 SButton::OnMouseButtonDown에 중단점을 찍고 디버깅을 해보길 추천한다.
TroubleShooting
- Common UI의 Synthetic Cursor Click이 예상과 다른 경우, FPointerEvent의 문제일 가능성이 높다.
- FPointEvent에서 올바른 User Input을 처리하지 않은 경우
- Synthetic Cursor가 예상 위치와 멀리 떨어져 있는 경우
- Click이 FSlateApplication::ProcessMouseButtonDownEvent에서 처리될 때
Capture가 있는 다른 Widget이 FWidgetPath에 영향을 미친 경우- 이는 Input Config의 MouseCaptureMode를 기반으로 발생할 수 있다.
- FWidgetPath가 FSlateApplication::LocateWindowUnerMouse를 사용하는 위치를 기반으로 자연 생성
- FWidgetPath에는 Input이 Route되는 Widget List가 포함되어 있다.
How Synthetic Cursor/Gamdpad Navigate and Focus
- 단순 Navigation 측면에서는 Common UI와 UMG의 기본 구현 과정은 크게 다를게 없다.
- 늘 그렇든 Input의 시작은 특정 Platform Application에서 시작한다.
- 아래 예시는 화살표나 Analog 이동의 시나리오를 예시로 삼는다.
- Input Louter System은 이 UI Navigation Input을 UI 내 Widget으로 Route한다.
- Navigation Input은 보통 SWidget::OnKeydown이나 SWidget::OnAnalogValueChanged에 의해 처리된다.
- 하지만 이러한 기본 Method는 Widget Focus를 직접 변경하지 않고 아래와 같이 동작한다.
FReply SWidget::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent )
{
if (bCanSupportFocus && SupportsKeyboardFocus())
{
EUINavigation Direction = FSlateApplicationBase::Get().GetNavigationDirectionFromKey(InKeyEvent);
// It's the left stick return a navigation request of the correct direction
if (Direction != EUINavigation::Invalid)
{
const ENavigationGenesis Genesis = InKeyEvent.GetKey().IsGamepadKey() ? ENavigationGenesis::Controller : ENavigationGenesis::Keyboard;
return FReply::Handled().SetNavigation(Direction, Genesis);
}
}
return FReply::Unhandled();
}
FReply SWidget::OnAnalogValueChanged( const FGeometry& MyGeometry, const FAnalogInputEvent& InAnalogInputEvent )
{
if (bCanSupportFocus && SupportsKeyboardFocus())
{
EUINavigation Direction = FSlateApplicationBase::Get().GetNavigationDirectionFromAnalog(InAnalogInputEvent);
// It's the left stick return a navigation request of the correct direction
if (Direction != EUINavigation::Invalid)
{
return FReply::Handled().SetNavigation(Direction, ENavigationGenesis::Controller);
}
}
return FReply::Unhandled();
}
- FSlateApplication::GetNavigationDirectionFromKey나 FSlateApplication::GetNavigationDirectionFromAnalog를
이용해 Input을 Navigation Direction으로 변환
EUINavigation FSlateApplication::GetNavigationDirectionFromKey(const FKeyEvent& InKeyEvent) const
{
TSharedRef<FNavigationConfig> RelevantNavConfig = GetRelevantNavConfig(InKeyEvent.GetUserIndex());
return RelevantNavConfig->GetNavigationDirectionFromKey(InKeyEvent);
}
EUINavigation FSlateApplication::GetNavigationDirectionFromAnalog(const FAnalogInputEvent& InAnalogEvent)
{
TSharedRef<FNavigationConfig> RelevantNavConfig = GetRelevantNavConfig(InAnalogEvent.GetUserIndex());
return RelevantNavConfig->GetNavigationDirectionFromAnalog(InAnalogEvent);
}
- 이 과정에서 Widget에 대한 Navigation Config을 고려한다.
/** An event should return FReply::Handled().SetNavigation( NavigationType ) as a means of asking the system to attempt a navigation*/
FReply& SetNavigation(EUINavigation InNavigationType, const ENavigationGenesis InNavigationGenesis, const ENavigationSource InNavigationSource = ENavigationSource::FocusedWidget)
{
this->NavigationType = InNavigationType;
this->NavigationGenesis = InNavigationGenesis;
this->NavigationSource = InNavigationSource;
this->NavigationDestination = nullptr;
return Me();
}
- Navigation Direction은 Capture되어 FReply::Handled에 포함된다.
- FReply::Handled는 FReply::SetNavigation을 통해 전송된다.
void FSlateApplication::ProcessReply( const FWidgetPath& CurrentEventPath, const FReply& TheReply, const FWidgetPath* WidgetsUnderMouse, const FPointerEvent* InMouseEvent, const uint32 UserIndex )
{
/*...*/
// If we have a valid Navigation request attempt the navigation.
if (TheReply.GetNavigationDestination().IsValid() || TheReply.GetNavigationType() != EUINavigation::Invalid)
{
FWidgetPath NavigationSource;
/*...*/
if (NavigationSource.IsValid())
{
if (!GSlateEnableGamepadEditorNavigation && TheReply.GetNavigationGenesis() == ENavigationGenesis::Controller && !NavigationSource.GetLastWidget()->GetPersistentState().bIsInGameLayer)
{
// Gamepad navigation while not in a game layer, do nothing as specified by GSlateEnableGamepadEditorNavigation
}
else if (TheReply.GetNavigationDestination().IsValid())
{
const bool bAlwaysHandleNavigationAttempt = false;
ExecuteNavigation(NavigationSource, TheReply.GetNavigationDestination(), UserIndex, bAlwaysHandleNavigationAttempt);
}
else
{
TSharedRef<SWindow> NavigationWindow = NavigationSource.GetDeepestWindow();
FNavigationEvent NavigationEvent(PlatformApplication->GetModifierKeys(), UserIndex, TheReply.GetNavigationType(), TheReply.GetNavigationGenesis());
FNavigationReply NavigationReply = FNavigationReply::Escape();
for (int32 WidgetIndex = NavigationSource.Widgets.Num() - 1; WidgetIndex >= 0; --WidgetIndex)
{
FArrangedWidget& SomeWidgetGettingEvent = NavigationSource.Widgets[WidgetIndex];
if (SomeWidgetGettingEvent.Widget->IsEnabled())
{
NavigationReply = SomeWidgetGettingEvent.Widget->OnNavigation(SomeWidgetGettingEvent.Geometry, NavigationEvent).SetHandler(SomeWidgetGettingEvent.Widget);
if (NavigationReply.GetBoundaryRule() != EUINavigationRule::Escape || SomeWidgetGettingEvent.Widget == NavigationWindow || WidgetIndex == 0)
{
AttemptNavigation(NavigationSource, NavigationEvent, NavigationReply, SomeWidgetGettingEvent);
break;
}
}
}
}
}
}
/*...*/
// Set focus if requested.
TSharedPtr<SWidget> RequestedFocusRecepient = TheReply.GetUserFocusRecepient();
if (TheReply.ShouldSetUserFocus() && RequestedFocusRecepient.IsValid())
{
if (TheReply.AffectsAllUsers())
{
SetAllUserFocus(RequestedFocusRecepient, TheReply.GetFocusCause());
}
else
{
SlateUser->SetFocus(RequestedFocusRecepient.ToSharedRef(), TheReply.GetFocusCause());
}
}
}
- Slate가 FSlateApplication::ProcessREply를 사용하여 Reply를 처리한다.
- 이 과정에서 Navigation이 발생한다.
- Navigation Event가 Direction에 의해 대략 정의 된 경우, FSlateApplication::AttemptNavigation은 Navigation 할 Widget을 찾으려 한다.
- Navigation이 가능한 경우, FSlateExecuteNavigation이 대상 Widget으로 Navigation한다.
- 대상 Widget이 유효한 경우, FSlateApplication::SetUserFocus가 호출된다.
- 이는 대상 Widget이 지정 되었는지, 다른 Widget을 찾았는지와 무관하게 동작한다.
void FSlateApplication::FinishedInputThisFrame()
{
const float DeltaTime = GetDeltaTime();
PlatformApplication->FinishedInputThisFrame();
// Any preprocessors are given a chance to process accumulated values (or do whatever other tick things they want)
// after we've finished processing all of the input for the frame
if (PlatformApplication->Cursor.IsValid())
{
InputPreProcessors.Tick(DeltaTime, *this, PlatformApplication->Cursor.ToSharedRef());
}
/*....*/
}
void FSlateApplication::InputPreProcessorsHelper::Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor)
{
TGuardValue<bool> IteratingGuard(bIsIteratingPreProcessors, true);
for (const TSharedPtr<IInputProcessor>& Preprocessor : InputPreProcessorList)
{
if (Preprocessor)
{
Preprocessor->Tick(DeltaTime, SlateApp, Cursor);
}
}
}
- Slate Focus Navigation이 종료된 후,
FSlateAnalogCursor::Tick에서 자동으로 다음 Tick 도중 Synthetic Cursor를 Focused Widget의 중앙으로 이동시킨다.- 이를 통해 Gamepad에서 Hover Effect를 사용할 수 있다.
Customize Synthetic Cursor Behavior in Common UI
- Common UI를 이용하면 고유한 Analogue Cursor나 Synthetic Cursor를 제공할 수 있다.
- Common UI를 통해 고유 Cursor를 제공하는 것에는 몇 가지 이점이 있다.
- Gamepad와 유사하게 작동하는 Keyboard Navigation을 만들려 하는 경우,
Gamepad를 사용하지 않을 때에도 FCommonAnalogCursor::Tick이 Widget의 중앙에 Snap되도록 할 수 있다. - Synthetic Mouse를 보이도록 하고, Focus가 달라질 때 Tween을 구현할 수 있다.
- Tween: 그래픽 상으로 두 상태나 프레임 사이를 자동으로 생성하는 Process
- Gamepad와 유사하게 작동하는 Keyboard Navigation을 만들려 하는 경우,
- Custom cursor를 만드는 방법은 다음과 같다.
- UCommonUIActionRouterBase를 상속받은 Custom ActionRouter Class 생성
- FCommonAnalogCursor를 상속받은 Custom AnalogCursor Class 생성
- Custom ActionRouter Class에서 MakeAnalogCursor를 Override하여 Custom AnalogCursor Class를 반환
- Custom Router Class에서 ShouldCreateSubsystem은 Override 된 경우 Instance를 생성하지 않는다.
- 이러한 Input Processor를 Customize 할 때에는 한가지 주의할 점이 있다.
- Input Processor는 모든 Input에서 실행 된다.
- 이는 Editor도 포함한다.
- 아래 함수를 이용하면 Application에서 발생한 Input과 그렇지 않은 것을 구분하는데 도움이 될 수 있다.
- Input Processor는 모든 Input에서 실행 된다.
bool FCommonAnalogCursor::IsGameViewportInFocusPathWithoutCapture() const
{
if (const UGameViewportClient* ViewportClient = GetViewportClient())
{
if (TSharedPtr<SViewport> GameViewportWidget = ViewportClient->GetGameViewportWidget())
{
TSharedPtr<FSlateUser> SlateUser = FSlateApplication::Get().GetUser(GetOwnerUserIndex());
if (SlateUser && !SlateUser->DoesWidgetHaveCursorCapture(GameViewportWidget))
{
#if PLATFORM_DESKTOP
// Not captured - is it in the focus path?
return SlateUser->IsWidgetInFocusPath(GameViewportWidget);
#endif
// If we're not on desktop, focus on the viewport is irrelevant, as there aren't other windows around to care about
return true;
}
}
}
return false;
}
Input Routing
- 간략하게 정리하면 다음과 같다.
- Common UI는 ActivableWidget을 Navigation을 처리하는 Node Tree로 구성한다.
- Inactive Widget은 추가되지 않고, 그에 따라 Router 대상으로 고려되지 않는다.
- CommonGameviewportClient가 Input을 Capture하고, Hierarchy의 최상단에 표시되는 Node를 탐색한다.
- 탐색된 Node에서 사용 가능 한 Input Handler를 이용해 Input을 처리할 수 있는지 확인한다.
- Player Input과 일치한 Handler가 없는 경우 자손 Node로 전달한다.
- 적절한 Node를 찾을 때까지 탐색을 반복한다.
- Common UI는 ActivableWidget을 Navigation을 처리하는 Node Tree로 구성한다.
- 이 Process는 모든 Node를 탐색할 때까지 반복한다.
bool FActivatableTreeNode::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
if (IsReceivingInput())
{
for (const FActivatableTreeNodeRef& ChildNode : Children)
{
if (ChildNode->ProcessNormalInput(ActiveInputMode, Key, InputEvent))
{
return true;
}
}
return FActionRouterBindingCollection::ProcessNormalInput(ActiveInputMode, Key, InputEvent);
}
return false;
}
- 탐색 방식은 BFS에 가깝다.
Execution Flow
- Synthetic Cursor와 마찬가지로 Input Processor는 Input Routing 발생 전에 Event를 처리할 수 있다.
- Input Processor에서 처리하지 않는 경우, Input Event는 Common UI의 Input Routing을 진행한다.
Click Event Process
- 특정 Platform Application이 FSlateApplication::ProcessKeyDownEvent를 Trigger 할 때 Input이 시작된다.
bool FSlateApplication::ProcessKeyDownEvent( const FKeyEvent& InKeyEvent )
{
/*...*/
// Send out key down events.
if ( !Reply.IsEventHandled() )
{
Reply = FEventRouter::RouteAlongFocusPath(this, FEventRouter::FBubblePolicy(EventPath), InKeyEvent, [] (const FArrangedWidget& SomeWidgetGettingEvent, const FKeyEvent& Event)
{
if (SomeWidgetGettingEvent.Widget->IsEnabled())
{
const FReply TempReply = SomeWidgetGettingEvent.Widget->OnKeyDown(SomeWidgetGettingEvent.Geometry, Event);
return TempReply;
}
return FReply::Unhandled();
}, ESlateDebuggingInputEvent::KeyDown);
}
/*...*/
return Reply.IsEventHandled();
}
- Slate Application은 현재 Focus 경로를 기반으로 Widget에 Input Event를 전달한다.
FReply SViewport::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& KeyEvent )
{
return ViewportInterface.IsValid() ? ViewportInterface.Pin()->OnKeyDown(MyGeometry, KeyEvent) : FReply::Unhandled();
}
- Game 내에서는 보통 SViewport::OnKeyDown Input Event이다.
- 이 Input Event는 현재 Viewport Interface Implemented Class로 키 누름을 전달한다.
FReply FSceneViewport::OnKeyDown( const FGeometry& InGeometry, const FKeyEvent& InKeyEvent )
{
// Start a new reply state
CurrentReplyState = FReply::Handled();
FKey Key = InKeyEvent.GetKey();
if (Key.IsValid())
{
KeyStateMap.Add(Key, true);
//@todo Slate Viewports: FWindowsViewport checks for Alt+Enter or F11 and toggles fullscreen. Unknown if fullscreen via this method will be needed for slate viewports.
if (ViewportClient && GetSizeXY() != FIntPoint::ZeroValue)
{
// Switch to the viewport clients world before processing input
FScopedConditionalWorldSwitcher WorldSwitcher(ViewportClient);
if (!ViewportClient->InputKey(FInputKeyEventArgs(this, InKeyEvent.GetInputDeviceId(), Key, InKeyEvent.IsRepeat() ? IE_Repeat : IE_Pressed, 1.0f, false)))
{
CurrentReplyState = FReply::Unhandled();
}
}
}
else
{
CurrentReplyState = FReply::Unhandled();
}
return CurrentReplyState;
}
- 보통은 키 누름이 전달되면 FSceneViewPort::OnKeyDown이 트리거 된다.
- 마지막으로 SceneViewPort는 Input을 현재 GameViewportClient의 InputKey Method로 전달한다.
bool UCommonGameViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs)
{
FInputKeyEventArgs EventArgs = InEventArgs;
if (IsKeyPriorityAboveUI(EventArgs))
{
return true;
}
// Check override before UI
if (OnOverrideInputKey().IsBound())
{
if (OnOverrideInputKey().Execute(EventArgs))
{
return true;
}
}
// The input is fair game for handling - the UI gets first dibs
#if !UE_BUILD_SHIPPING
if (ViewportConsole && !ViewportConsole->ConsoleState.IsEqual(NAME_Typing) && !ViewportConsole->ConsoleState.IsEqual(NAME_Open))
#endif
{
FReply Result = FReply::Unhandled();
if (!OnRerouteInput().ExecuteIfBound(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result))
{
HandleRerouteInput(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result);
}
if (Result.IsEventHandled())
{
return true;
}
}
return Super::InputKey(EventArgs);
}
- Common UI를 사용할 때에는 이것이 UCommonGameViewportClient::InputKey이다.
- 때문에 Common UI를 사용하려면 GameViewportClass가 CommonViewportClient로 설정되어야 한다.
Action Router Process
bool UCommonGameViewportClient::InputKey(const FInputKeyEventArgs& InEventArgs)
{
FInputKeyEventArgs EventArgs = InEventArgs;
if (IsKeyPriorityAboveUI(EventArgs))
{
return true;
}
// Check override before UI
if (OnOverrideInputKey().IsBound())
{
if (OnOverrideInputKey().Execute(EventArgs))
{
return true;
}
}
// The input is fair game for handling - the UI gets first dibs
#if !UE_BUILD_SHIPPING
if (ViewportConsole && !ViewportConsole->ConsoleState.IsEqual(NAME_Typing) && !ViewportConsole->ConsoleState.IsEqual(NAME_Open))
#endif
{
FReply Result = FReply::Unhandled();
if (!OnRerouteInput().ExecuteIfBound(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result))
{
HandleRerouteInput(EventArgs.InputDevice, EventArgs.Key, EventArgs.Event, Result);
}
if (Result.IsEventHandled())
{
return true;
}
}
return Super::InputKey(EventArgs);
}
- Common UI의 Action Router Process는 Input이 CommonGameViewportClient에 전달되면서 시작합니다.
void UCommonGameViewportClient::HandleRerouteInput(FInputDeviceId DeviceId, FKey Key, EInputEvent EventType, FReply& Reply)
{
FPlatformUserId OwningPlatformUser = IPlatformInputDeviceMapper::Get().GetUserForInputDevice(DeviceId);
ULocalPlayer* LocalPlayer = GameInstance->FindLocalPlayerFromPlatformUserId(OwningPlatformUser);
Reply = FReply::Unhandled();
if (LocalPlayer)
{
UCommonUIActionRouterBase* ActionRouter = LocalPlayer->GetSubsystem<UCommonUIActionRouterBase>();
if (ensure(ActionRouter))
{
ERouteUIInputResult InputResult = ActionRouter->ProcessInput(Key, EventType);
if (InputResult == ERouteUIInputResult::BlockGameInput)
{
// We need to set the reply as handled otherwise the input won't actually be blocked from reaching the viewport.
Reply = FReply::Handled();
// Notify interested parties that we blocked the input.
OnRerouteBlockedInput().ExecuteIfBound(DeviceId, Key, EventType, Reply);
}
else if (InputResult == ERouteUIInputResult::Handled)
{
Reply = FReply::Handled();
}
}
}
}
- UCommonGaameViewportClient::InputKey는 ActionLouter에
UCommonGameViewportClient::HandleRerouteInput의 입력을 처리할 기회를 제공한다.
ERouteUIInputResult UCommonUIActionRouterBase::ProcessInput(FKey Key, EInputEvent InputEvent) const
{
/*....*/
const auto ProcessNormalInputFunc = [Key, ActiveMode, this](EInputEvent Event)
{
bool bHandled = PersistentActions->ProcessNormalInput(ActiveMode, Key, Event);
if (!bHandled)
{
if (bIsActivatableTreeEnabled && ActiveRootNode)
{
bHandled = ActiveRootNode->ProcessNormalInput(ActiveMode, Key, Event);
}
if (!bHandled)
{
bHandled = ProcessInputOnActionDomains(ActiveMode, Key, Event);
}
}
return bHandled;
};
bool bHandledInput = ProcessHoldResult == EProcessHoldActionResult::Handled;
if (!bHandledInput)
{
if (ProcessHoldResult == EProcessHoldActionResult::GeneratePress)
{
// A hold action was in progress but quickly aborted, so we want to generate a press action now for any normal bindings that are interested
ProcessNormalInputFunc(IE_Pressed);
}
// Even if no widget cares about this input, we don't want to let anything through to the actual game while we're in menu mode
bHandledInput = ProcessNormalInputFunc(InputEvent);
}
if (bHandledInput)
{
return ERouteUIInputResult::Handled;
}
return CanProcessNormalGameInput() ? ERouteUIInputResult::Unhandled : ERouteUIInputResult::BlockGameInput;
}
- 성공하면, UCommonUIActionRouterBase::ProcessInput을 호출한다.
- Action Router는 ActivatableWidget Tree에서 현재 Active 중인 Root Node를 유지합니다.
bool FActivatableTreeNode::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
if (IsReceivingInput())
{
for (const FActivatableTreeNodeRef& ChildNode : Children)
{
if (ChildNode->ProcessNormalInput(ActiveInputMode, Key, InputEvent))
{
return true;
}
}
return FActionRouterBindingCollection::ProcessNormalInput(ActiveInputMode, Key, InputEvent);
}
return false;
}
- ProcessInput은 Root Node에서 FActivatableTreeNode::ProcessNormalInput을 호출해
Input 처리 시도를 지시합니다.- 이는 반복적으로 모든 Child Node에 ProcessNormalInput을 지시한다.
bool FActionRouterBindingCollection::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
for (FUIActionBindingHandle BindingHandle : ActionBindings)
{
if (TSharedPtr<FUIActionBinding> Binding = FUIActionBinding::FindBinding(BindingHandle))
{
if (ActiveInputMode == ECommonInputMode::All || ActiveInputMode == Binding->InputMode)
{
auto TryConsumeInput = [&](const FKey& InKey, const UInputAction* InInputAction)
{
// A persistent displayed action skips the normal rules for reachability, since it'll always appear in a bound action bar
const bool bIsDisplayedPersistentAction = Binding->bIsPersistent && Binding->bDisplayInActionBar;
if (InKey == Key && Binding->InputEvent == InputEvent && (bIsDisplayedPersistentAction || IsWidgetReachableForInput(Binding->BoundWidget.Get())))
{
// Just in case this was in the middle of a hold process with a different key, reset now
Binding->CancelHold();
// If injecting enhanced input. don't fire 'OnExecuteAction' since that can be manually done if desired in BP
bool bEnhancedInputInjected = false;
if (InInputAction)
{
if (TObjectPtr<const UCommonInputMetadata> CommonInputMetadata = CommonUI::GetEnhancedInputActionMetadata(InInputAction))
{
// Non generic actions should inject enhanced input so users can bind to enhanced input events
if (!CommonInputMetadata->bIsGenericInputAction)
{
FInputActionValue RawValue = FInputActionValue(true);
CommonUI::InjectEnhancedInputForAction(GetActionRouter().GetLocalPlayerChecked(), InInputAction, RawValue);
bEnhancedInputInjected = true;
}
}
}
if (!bEnhancedInputInjected)
{
Binding->OnExecuteAction.ExecuteIfBound();
}
if (Binding->bConsumesInput)
{
return true;
}
}
return false;
};
if (CommonUI::IsEnhancedInputSupportEnabled() && Binding->InputAction.IsValid())
{
if (const UInputAction* InputAction = Binding->InputAction.Get())
{
TArray<FKey> InputActionKeys;
CommonUI::GetEnhancedInputActionKeys(GetActionRouter().GetLocalPlayerChecked(), InputAction, InputActionKeys);
for (const FKey& InputActionKey : InputActionKeys)
{
if (TryConsumeInput(InputActionKey, InputAction))
{
return true;
}
}
}
}
for (const FUIActionKeyMapping& KeyMapping : Binding->NormalMappings)
{
if (TryConsumeInput(KeyMapping.Key, nullptr))
{
return true;
}
}
}
}
}
return false;
}
- FActivatableTreeNode는 FActionRouterBindingCollection으로,
Node의 ActivatableWidget에 모든 ActionBinding List를 유지한다. - 현재 Node 내의 모든 Child Node에서 Input을 처리하지 못하면,
FActivatableTreeNode::ProcessNormalInput을 호출한다.- 이 때 FActivatableTreeNode는 Binding Collection으로서 현재 Node는 Widget의 모든 Action Binding을 확인한다.
- Action Binding이 해당 Key와 일치하는 경우, 관련 동작이 실행되고 Input이 처리된 것으로 간주된다.
Modify the Input Routing System
- UCommonGameViewportClient를 상속받고 모든 Input Handling Method를 Override 하라.
- 이후 Project Setting에서 이 Class를 GameVieportClss로 설정한다.
- UCommonUIActionRouterBase를 상속받고 모든 Virtual Function을 Override한다.
- 예를 들어, FUIInputconfig에서 ApplyUIInputConfig를 Override 할 수 있다.
Change UI Input Handling with Input config
- 현재 활성화 된 Widget을 기반으로 Application의 Input Handling이 변경되어야 하는 경우가 있다.
- 예를 들어 Social Sidebar 혹은 Pause Menu가 열려 있을 때 Player Input을 막는다던가.
- 이를 처리하기 위해 Common UI에서는 ActivatableWidget의 Inputconfig 옵션을 지원한다.
- 이는 Common UI 사용에 필수적인 사안은 아니다.
Use Input Config
/**
* Input Config that can be applied on widget activation. Allows for input setup (Mouse capture,
* UI-only input, move / look ignore, etc), to be controlled by widget activation.
*/
USTRUCT(BlueprintType)
struct COMMONUI_API FUIInputConfig
{
GENERATED_BODY()
ECommonInputMode GetInputMode() const { return InputMode; }
EMouseCaptureMode GetMouseCaptureMode() const { return MouseCaptureMode; }
EMouseLockMode GetMouseLockMode() const { return MouseLockMode; }
bool HideCursorDuringViewportCapture() const { return bHideCursorDuringViewportCapture; }
FUIInputConfig();
FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, bool bInHideCursorDuringViewportCapture = true);
FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, EMouseLockMode InMouseLockMode, bool bInHideCursorDuringViewportCapture = true);
bool operator==(const FUIInputConfig& Other) const
{
return bIgnoreMoveInput == Other.bIgnoreMoveInput
&& bIgnoreLookInput == Other.bIgnoreLookInput
&& InputMode == Other.InputMode
&& MouseCaptureMode == Other.MouseCaptureMode
&& MouseLockMode == Other.MouseLockMode
&& bHideCursorDuringViewportCapture == Other.bHideCursorDuringViewportCapture;
}
bool operator!=(const FUIInputConfig& Other) const
{
return !operator==(Other);
}
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
bool bIgnoreMoveInput = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
bool bIgnoreLookInput = false;
/** Simplification of config as string */
FString ToString() const;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
ECommonInputMode InputMode;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
EMouseCaptureMode MouseCaptureMode;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
EMouseLockMode MouseLockMode;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
bool bHideCursorDuringViewportCapture = true;
};
- 각 InputConfig는 여러 Input의 상태를 추적한다.
- Mouse Capture Option
- 이동 및 시야 축 처리
- Common UI 전체의 Input Mode등
/**
* Gets the desired input configuration to establish when this widget activates and can receive input (i.e. all parents are also active).
* This configuration will override the existing one established by any previous activatable widget and restore it (if valid) upon deactivation.
*/
TOptional<FUIInputConfig> UCommonActivatableWidget::GetDesiredInputConfig() const
{
// Check if there is a BP implementation for input configs
if (GetClass()->IsFunctionImplementedInScript(GET_FUNCTION_NAME_CHECKED(UCommonActivatableWidget, BP_GetDesiredInputConfig)))
{
return BP_GetDesiredInputConfig();
}
// No particular config is desired by default
return TOptional<FUIInputConfig>();
}
- ActivatableWidget을 활성화하면 GetDesiredInputConfig를 사용하여 InputConfig를 가져온다.
- 일반적으로는 null을 반환하지만 Override가 가능하다.
- GetDesiredInputConfig가 Null을 반환할 때마다 Common UI는 마지막으로 유효했던 InputConfig로 되돌아간다.
/**
* Controls whether a default Input Config will be set when the active CommonActivatableWidgets do not specify a desired one.
* Disable this if you want to control the Input Mode via alternative means.
*/
UPROPERTY(config, EditAnywhere, Category = "Input")
bool bEnableDefaultInputConfig = true;
- 기본적으로 Common UI는 ActivatableWidget으로 지정되지 않는 경우 예비로 InputConfig를 적용한다.
- 하지만 위 변수를 사용해 이를 비활성화 할 수 있다.
void FActivatableTreeRoot::ApplyLeafmostNodeConfig()
{
#if WITH_EDITOR
if (!IsViewportWindowInFocusPath(GetActionRouter()))
{
return;
}
#endif
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();
if(DesiredConfig.IsSet())
{
GetActionRouter().SetActiveUIInputConfig(DesiredConfig.GetValue(), PinnedLeafmostNode->GetWidget());
}
else if(ICommonInputModule::GetSettings().GetEnableDefaultInputConfig())
{
// Nobody in the entire tree cares about the config and the default is enabled so fall back to the default
GetActionRouter().SetActiveUIInputConfig(FUIInputConfig());
}
FocusLeafmostNode();
}
else
{
UE_LOG(LogUIActionRouter, Log, TEXT("Didn't apply input config for leaf-most node [%s] because it's not receiving input right now"), *PinnedLeafmostNode->GetWidget()->GetName());
}
}
}
- Widget이 비활성화 되면 Common UI는 이전 InputConfig를 복구한다.
- 이는 현재 Widget을 지원할 적절한 Inputconfig가 없이 중단되는 것을 방지하기 위함이다.
- 만약 UI의 모든 Widget이 비활성화 되면
CommonUI는 마지막으로 비활성화 된 Widget의 Default InputConfig를 사용한다.- 이 경우 마지막으로 비활성화 된 Widget이 Soft Lock 방지를 위해
합리적인 InputConfig 상태를 다시 적용하도록 해야 한다.
- 이 경우 마지막으로 비활성화 된 Widget이 Soft Lock 방지를 위해
Recommended Use
- InputConfig를 사용하는 경우 UI에서 표준 InputConfig를 사용하지 않아야 한다.
더보기
void UCommonUIActionRouterBase::ApplyUIInputConfig(const FUIInputConfig& NewConfig, bool bForceRefresh)
{
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();
// Note: may not work for splitscreen. We need per-player viewport client settings for mouse capture
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())
{
// Flushing pressed keys after switching to the Menu InputMode. This prevents the inputs from being artificially "held down".
// This needs to be delayed by one frame to successfully clear input captured at the end of this frame
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 the mouse was hidden previously, set it back to the center of the viewport now that we're showing it again
if (!bForceRefresh && bWasCursorHidden && PC->ShouldShowMouseCursor())
{
const ECommonInputType CurrentInputType = GetInputSubsystem().GetCurrentInputType();
bool bCenterCursor = true;
switch (CurrentInputType)
{
// Touch - Don't do it - the cursor isn't really relevant there.
case ECommonInputType::Touch:
bCenterCursor = false;
break;
// Gamepad - Let the settings tell us if we should center it.
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());
}
}
}
- UCommonUIActionRouterBase::ApplyInputConfig에서는 Config Process 과정에서
다음 Unreal의 표준 InputConfig들을 기능의 일부로 호출한다.
void AController::SetIgnoreMoveInput(bool bNewMoveInput)
{
IgnoreMoveInput = FMath::Max(IgnoreMoveInput + (bNewMoveInput ? +1 : -1), 0);
}
void UGameViewportClient::SetMouseCaptureMode(EMouseCaptureMode Mode)
{
if (MouseCaptureMode != Mode)
{
UE_LOG(LogViewport, Display, TEXT("Viewport MouseCaptureMode Changed, %s -> %s"),
*StaticEnum<EMouseCaptureMode>()->GetNameStringByValue((uint64)MouseCaptureMode),
*StaticEnum<EMouseCaptureMode>()->GetNameStringByValue((uint64)Mode)
);
MouseCaptureMode = Mode;
}
}
void UGameViewportClient::SetHideCursorDuringCapture(bool InHideCursorDuringCapture)
{
if (bHideCursorDuringCapture != InHideCursorDuringCapture)
{
UE_LOG(LogViewport, Display, TEXT("Viewport HideCursorDuringCapture Changed, %s -> %s"),
bHideCursorDuringCapture ? TEXT("True") : TEXT("False"),
InHideCursorDuringCapture ? TEXT("True") : TEXT("False")
);
bHideCursorDuringCapture = InHideCursorDuringCapture;
}
}
- 때문에 다른 곳에서 이런 함수를 호출하며 Common UI의 InputConfig와 혼용할 경우 서로를 Override하여 관리할 때 혼란이 발생할 수 있다.
- Input Config 관리를 간소화하기 위해
Widget의 Enum값을 기반으로 주로 사용하는 InputConfig를 할당하는 기본 구현을 생성할 수 있다.- 이는 Widget별로 몇 개의 고정적인 Async InputConfig만 필요한 경우에 유용하다.
Input Handling State Reference
FUIInputConfig
더보기
/**
* Input Config that can be applied on widget activation. Allows for input setup (Mouse capture,
* UI-only input, move / look ignore, etc), to be controlled by widget activation.
*/
USTRUCT(BlueprintType)
struct COMMONUI_API FUIInputConfig
{
GENERATED_BODY()
ECommonInputMode GetInputMode() const { return InputMode; }
EMouseCaptureMode GetMouseCaptureMode() const { return MouseCaptureMode; }
EMouseLockMode GetMouseLockMode() const { return MouseLockMode; }
bool HideCursorDuringViewportCapture() const { return bHideCursorDuringViewportCapture; }
FUIInputConfig();
FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, bool bInHideCursorDuringViewportCapture = true);
FUIInputConfig(ECommonInputMode InInputMode, EMouseCaptureMode InMouseCaptureMode, EMouseLockMode InMouseLockMode, bool bInHideCursorDuringViewportCapture = true);
bool operator==(const FUIInputConfig& Other) const
{
return bIgnoreMoveInput == Other.bIgnoreMoveInput
&& bIgnoreLookInput == Other.bIgnoreLookInput
&& InputMode == Other.InputMode
&& MouseCaptureMode == Other.MouseCaptureMode
&& MouseLockMode == Other.MouseLockMode
&& bHideCursorDuringViewportCapture == Other.bHideCursorDuringViewportCapture;
}
bool operator!=(const FUIInputConfig& Other) const
{
return !operator==(Other);
}
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
bool bIgnoreMoveInput = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
bool bIgnoreLookInput = false;
/** Simplification of config as string */
FString ToString() const;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
ECommonInputMode InputMode;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
EMouseCaptureMode MouseCaptureMode;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
EMouseLockMode MouseLockMode;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = InputConfig)
bool bHideCursorDuringViewportCapture = true;
};
ECommonInputMode
더보기
UENUM(BlueprintType)
enum class ECommonInputMode : uint8
{
Menu UMETA(Tooltip = "Input is received by the UI only"),
Game UMETA(Tooltip = "Input is received by the Game only"),
All UMETA(Tooltip = "Input is received by UI and the Game"),
MAX UMETA(Hidden)
};
EMouseCaptureMode
더보기
UENUM()
enum class EMouseCaptureMode : uint8
{
/** Do not capture the mouse at all */
NoCapture,
/** Capture the mouse permanently when the viewport is clicked, and consume the initial mouse down that caused the capture so it isn't processed by player input */
CapturePermanently,
/** Capture the mouse permanently when the viewport is clicked, and allow player input to process the mouse down that caused the capture */
CapturePermanently_IncludingInitialMouseDown,
/** Capture the mouse during a mouse down, releases on mouse up */
CaptureDuringMouseDown,
/** Capture only when the right mouse button is down, not any of the other mouse buttons */
CaptureDuringRightMouseDown,
};
EMouseLockMode
더보기
UENUM()
enum class EMouseLockMode : uint8
{
/** Do not lock the mouse cursor to the viewport */
DoNotLock,
/** Only lock the mouse cursor to the viewport when the mouse is captured */
LockOnCapture,
/** Always lock the mouse cursor to the viewport */
LockAlways,
/** Lock the cursor if we're in fullscreen */
LockInFullscreen,
};
Change Widget Input Respons Using FReply
- FReply는 InputEvent의 처리 상태를 추적한다.
/**
* An event should return a FReply::Handled() to let the system know that an event was handled.
*/
static FReply Handled( )
{
return FReply(true);
}
/**
* An event should return a FReply::Unhandled() to let the system know that an event was unhandled.
*/
static FReply Unhandled( )
{
return FReply(false);
}
- Slate의 Input은 대부분 FReply::Handled나 FReply::Unhandled를 결과로 반환한다.
- SWidget에서 사용되는 수 많은 Input Event들은 대부분 FReply를 반환한다.
- 이를 이용해 특정 FReply를 반환하여 원하는 결과를 얻게할 수 있다.
- 예를들어 특정 InputType의 처리를 허용 또는 중지한다던가.
- 하지만 대부분은 순정 FReply를 사용해도 크게 문제 없을 것이다.
- 이를 이용해 특정 FReply를 반환하여 원하는 결과를 얻게할 수 있다.
- FReply를 Customize 할 때에는 Slate Widget 작업에서 문제가 발생하는 경우가 많다.
FReply Settings
- 보통은 FReply의 Handled/UnHandled로만 상태를 추적하지만, 몇 가지 추가 데이터를 제공하기도 한다.
- 이러한 Method는 UMG나 Slate의 일부 Method와 비슷해 보일 수 있다.
- 하지만 기본적으로 FReply Namespace에 존재하며, Slate에서 FReply를 처리할 때 발생하는 동작을 수정한다.
- FReply에서 이런 Method를 호출하면 FReply 외부의 동등한 Method를 호출한다.
- 이는 쉽게 복제할 수 없는 조금 다른 동작이 나올 수 있다.
CaptureMouse
/** An event should return a FReply::Handled().CaptureMouse( SomeWidget ) as a means of asking the system to forward all mouse events to SomeWidget */
FReply& CaptureMouse( TSharedRef<SWidget> InMouseCaptor )
{
this->MouseCaptor = InMouseCaptor;
return Me();
}
- System에 특정 Widget으로 모든 MouseEvent를 전달하도록 요청
ClearUserFocus
/** An event should return a FReply::Handled().ClearUserFocus() to ask the system to clear user focus*/
FReply& ClearUserFocus(bool bInAllUsers = false)
{
return ClearUserFocus(EFocusCause::SetDirectly, bInAllUsers);
}
/** An event should return a FReply::Handled().ClearUserFocus() to ask the system to clear user focus*/
SLATECORE_API FReply& ClearUserFocus(EFocusCause ReasonFocusIsChanging, bool bInAllUsers = false)
{
this->FocusRecipient = nullptr;
this->FocusChangeReason = ReasonFocusIsChanging;
this->bReleaseUserFocus = true;
this->bSetUserFocus = false;
this->bAllUsers = bInAllUsers;
return Me();
}
- System에 User Focus를 지우기를 요청
ReleaseMouseCapture
/**
* An event should return a FReply::Handled().ReleaseMouse() to ask the system to release mouse capture
* NOTE: Deactivates high precision mouse movement if activated.
*/
FReply& ReleaseMouseCapture()
{
this->MouseCaptor.Reset();
this->bReleaseMouseCapture = true;
this->bUseHighPrecisionMouse = false;
return Me();
}
- System에 Mouse Capture 해제를 요청
SetUserFocus
/** An event should return FReply::Handled().SetUserFocus( SomeWidget ) as a means of asking the system to set users focus to the provided widget*/
SLATECORE_API FReply& SetUserFocus(TSharedRef<SWidget> GiveMeFocus, EFocusCause ReasonFocusIsChanging = EFocusCause::SetDirectly, bool bInAllUsers = false)
{
this->bSetUserFocus = true;
this->FocusRecipient = GiveMeFocus;
this->FocusChangeReason = ReasonFocusIsChanging;
this->bReleaseUserFocus = false;
this->bAllUsers = bInAllUsers;
return Me();
}
- System에 User의 Focus를 제공된 Widget으로 설정하도록 요청
SetNavigation
/** An event should return FReply::Handled().SetNavigation( NavigationType ) as a means of asking the system to attempt a navigation*/
FReply& SetNavigation(EUINavigation InNavigationType, const ENavigationGenesis InNavigationGenesis, const ENavigationSource InNavigationSource = ENavigationSource::FocusedWidget)
{
this->NavigationType = InNavigationType;
this->NavigationGenesis = InNavigationGenesis;
this->NavigationSource = InNavigationSource;
this->NavigationDestination = nullptr;
return Me();
}
/** An event should return FReply::Handled().SetNavigation( NavigationDestination ) as a means of asking the system to attempt a navigation to the specified destination*/
FReply& SetNavigation(TSharedRef<SWidget> InNavigationDestination, const ENavigationGenesis InNavigationGenesis, const ENavigationSource InNavigationSource = ENavigationSource::FocusedWidget)
{
this->NavigationType = EUINavigation::Invalid;
this->NavigationGenesis = InNavigationGenesis;
this->NavigationSource = InNavigationSource;
this->NavigationDestination = InNavigationDestination;
return Me();
}
- System에 지정된 대상으로의 Navigation을 시도하도록 요청
When would we set FReply?
- 예를 들어 특정 Key Input으로 Widget Focus를 설정하거나, 지워야 한다고 가정해보자.
- 보통은 FSlateInputApplication에서 Key Input Handler 관련 함수를 직접 호출할 것이다.
- 이 방식은 모든 경우에, 특히 Input Routing을 사용할 때 작동하지 않을 수 있다.
- 현재 Widget에서 Input이 처리되는 동안 Focus 변경/지우기가 발생할 수 있기 때문이다.
- 이를 대신하여 Input이 완전히 처리되고 나서 발생되는 FReply를 사용하는 것이 좋다.
- 과거에는 FReply API가 제어 또는 노출되는 상태일 때에는 FReply만 사용 하도록 하였다.
- 하지만 이 방식은 너무 제한적이라 위와 같이 언제든 사용 가능하도록 바뀌었다.
- 현재 버전에서 기존의 가이드 이상으로 강력히 권장되는 방식이다.
Customize Navigation
Navigation Config
/**
* Navigation context for event
*/
UENUM(BlueprintType)
enum class EUINavigation : uint8
{
/** Four cardinal directions*/
Left,
Right,
Up,
Down,
/** Conceptual next and previous*/
Next,
Previous,
/** Number of navigation types*/
Num UMETA(Hidden),
/** Denotes an invalid navigation, more important used to denote no specified navigation*/
Invalid,
};
- Slate는 CommonUI 사용 여부와 무관하게 기본 Navigation을 지원한다.
/**
* Sets the navigation config. If you need to control navigation config dynamically, you
* should subclass FNavigationConfig to be dynamically adjustable to your needs.
*/
SLATE_API void SetNavigationConfig(TSharedRef<FNavigationConfig> InNavigationConfig)
{
NavigationConfig->OnUnregister();
NavigationConfig = InNavigationConfig;
NavigationConfig->OnRegister();
#if WITH_SLATE_DEBUGGING
TryDumpNavigationConfig(NavigationConfig);
#endif // WITH_SLATE_DEBUGGING
}
- NavigationConfig를 설정하려면 FSlateApplication::NetNavigationConfig를 호출한다.
- 대체로 FNavigationConfig에서 파생된 Custom Navigation 구성을 할 때 이 함수를 호출한다.
- 예를 들어 WASD로 UI라 상호작용 한다면 이 부분에서부터 시작하는게 좋다.
void FSlateUser::SetUserNavigationConfig(TSharedPtr<FNavigationConfig> InNavigationConfig)
{
if (UserNavigationConfig)
{
UserNavigationConfig->OnUnregister();
}
UserNavigationConfig = InNavigationConfig;
if (InNavigationConfig)
{
InNavigationConfig->OnRegister();
}
#if WITH_SLATE_DEBUGGING
FSlateApplication::Get().TryDumpNavigationConfig(UserNavigationConfig);
#endif // WITH_SLATE_DEBUGGING
}
- 또한 FSlateUser::SetUserNavigationConfig를 호출해 User 기반으로 Navigation 구성을 설정할 수도 있다.
Manually Control Navigation
- UMG에서 Widget을 선택하고 Details -> Navigation을 선택하면
Navigation Event 발생 시 어떻게 될지 수동으로 지정할 수 있다.
UENUM(BlueprintType)
enum class EUINavigationRule : uint8
{
/** Allow the movement to continue in that direction, seeking the next navigable widget automatically. */
Escape,
/** Move to a specific widget. */
Explicit,
/**
* Wrap movement inside this container, causing the movement to cycle around from the opposite side,
* if the navigation attempt would have escaped.
*/
Wrap,
/** Stops movement in this direction */
Stop,
/** Custom navigation handled by user code. */
Custom,
/** Custom navigation handled by user code if the boundary is hit. */
CustomBoundary,
/** Invalid Rule */
Invalid
};
Activatable Widgets and Action Bind
Set Focus for Activatable Widget on Activation
UWidget* UCommonActivatableWidget::GetDesiredFocusTarget() const
{
return NativeGetDesiredFocusTarget();
}
UWidget* UCommonActivatableWidget::NativeGetDesiredFocusTarget() const
{
// Prioritize BP implementation of this function.
UWidget* DesiredFocusTarget = BP_GetDesiredFocusTarget();
if (!DesiredFocusTarget)
{
// BP didn't specify focus target, fallback to DesiredFocusWidget property on UserWidget.
DesiredFocusTarget = GetDesiredFocusWidget();
}
return DesiredFocusTarget;
}
UWidget* UUserWidget::GetDesiredFocusWidget() const
{
return DesiredFocusWidget.GetWidget();
}
- ActivatableWidget을 Activate할 때마다 GetDesiredFocusTarget이 호출된다.
- 이 함수는 User Input 발생 시 Common UI가 Focus해야 할 Widget을 선택한다.
- 해당 함수를 Customize 하지 않을 시 Common UI에서
Widget이 Active/Inactive 할 때마다 Focus 할 위치를 파악하는데 어려울 수 있다. - 그러니 반드시 Customize를 하자.
- Lyra에서는 ActivatableWidget에서 원하는 Focus Target을 가져오는데 사용할 Method를 Enum Type으로 결정한다.
- 이는 Focus 대상이 고정된 Async Method를 사용하는 경우에 권장된다.
- 대부분의 Menu에서라던가.
Change when Input Action Fire Triggered
- Action Bind를 위해 FBindUIActionArgs를 생성할 때 KeyEvent를
InputAction Event를 Trigger해야 할 Type으로 설정한다.
Console Variable Reference
Use With Enhanced Input
- Unreal Engine 5.2 이전 버전에서는 사용을 권장하지 않음.
- Enhanced Input이 5.2버전 기준으로 테스트가 충분히 거치지 않음
- 주요 기능인 EnhancedInput의 PlayerMappableKeySettings가 아직 Experimental 단계임.
- 적어도 Beta가 되기 전까지는 시도를 안 하는 편이 좋음.
필수 사항
- Common UI와 Enhanced Input을 모두 사용할 것
- ViewportClass가 CommonGameViewportClient나 그 하위 Class일 것
- CommonInputData에서 Accept/Back에 대한 Input Action이 등록되어 있을 것
설정법
- Project Settings -> Game -> Common Input Settings에서 Enable Enhanced Input Support를 체크
- Miscellaneous -> DataAsset을 선택해 CommonMappingContextMetadataInterface를 선택해 Asset 생성
- 해당 Asset은 Common UI Input Data를 설정할 수 있는 Metadata Object를 제공한다.
- 여기서 쓰이는 Nav Bar Priority는 Common UI와 동일한 개념이다.
- 만약 기능이 필요하다면 CommonInputMetaData를 상속받아 확장 가능하다.
- 위와 같이 설정
- Per Action Enhanced Input Metadata
- 대응 될 InputAction BP
- Action마다 Asset을 각자 만드는 대신,
Per Action Enhanced Input Metadata를 사용해 한 Asset에서 여러 Action을 처리할 수 있다.
- Is Generic Iput Action
- true로 설정한 이유는 Common UI가 Input Action을 Broadcast하는 것을 방지하기 위함이다.
- 만약 Generic InputAction이 아닌 InputAction에서 사용하고 싶다면 이 값을 해제하면 된다.
- 이 경우, 해당 InputAction에는 Input을 Bind할 수 있다.
- Per Action Enhanced Input Metadata
- InputAction에서 Player Mappable Key Settings에서 Metadata 필드를 설정한다.
- 여기서 사용하는 Metadata는 위에서 만든 Asset이다.
- 이렇게 만들어진 InputAction은 Input을 받을 수 있는 Common UI에 연결해 사용할 수 있다.
- CommonButtonBase
- CommonActionWidget
- UI가 아닌 InputAction의 Key를 표시할 수 있다.
- CommonUIInputData
- 여기서 Default Navigation Action이 정의된다.
- 만약 이 옵션이 보이지 않는다면 Project Settings에서 Enable Enhanced Input Support 옵션을 확인해보자.
- CommonActivatableWidget의 경우, Active 여부에 따라 적용/제거 될 IMC를 지정할 수 있다.
- 더 좋은 구조를 위해 게임 IMC를 적용 할 때 Common UI IMC를 같이 적용할 것을 권장한다.
'UE5 > UI' 카테고리의 다른 글
[회고] Common UI, UMG ViewModel 사용 후기 (0) | 2024.09.28 |
---|---|
[UI] Common UI FAQ (0) | 2024.07.10 |
[UI] Common UI Widget (0) | 2024.07.10 |
[UI] Common UI Introduction (0) | 2024.07.10 |
[UI] UMG ViewModel (0) | 2024.07.08 |