이미 5.1이 나온지도 한참이지만.. 내가 UE4밖에 못써봤기 때문에 UE4 기준으로 알게 된 Input System을 정리해봤다.
음.. Input System은 너무 거창하고 사용자 입력 처리 과정 정도이다ㅎㅎ
※UE5는 다를 수 있음. 내 경험으로 작성된 포스팅이므로 틀린 내용이 있을 수 있음.
사용자 입력은 FSlateApplication에서 처리한다.
사용자 Input 타입이 다양한 만큼 FSlateApplication의 함수도 다양하다.
FSlateApplication::ProcessMouseMoveEvent
FSlateApplication::ProcessMouseButtonDownEvent
FSlateApplication::ProcessKeyUpEvent
이번 포스팅에서는 내가 가장 많이 보게됐던 KeyInput 처리를 중점으로 알아보았다.
ProcessMouseMoveEvent
은 본문에 기재한 Step 1 이후 툴팁을 업데이트 하는 등 Input 타입마다 변주가 있다.
Key Input에는 Down, Up이 있는데, 본문에는 FSlateApplication::FProcessKeyUpEvent
을 기준으로 정리했다.
(Down도 동일하다😊)
bool FSlateApplication::ProcessKeyUpEvent( const FKeyEvent& InKeyEvent )
{
SCOPE_CYCLE_COUNTER(STAT_ProcessKeyUp);
#if WITH_SLATE_DEBUGGING
FSlateDebugging::FScopeProcessInputEvent Scope(ESlateDebuggingInputEvent::KeyUp, InKeyEvent);
#endif
TScopeCounter<int32> BeginInput(ProcessingInput);
// Analog cursor gets first chance at the input
if (InputPreProcessors.HandleKeyUpEvent(*this, InKeyEvent)) // Step 1. InputProcessor에서 키 입력 처리
{
return true;
}
// ...
}
등록된 IInputProcessor
리스트를 순회하며 IInputProcessor::HandleKeyUpEvent
를 호출한다.
InputProcesser가 HandleKeyUpEvent
에서 성공적으로 키 입력을 처리했다면 이후 스텝을 거치지 않는다.
그 어떤 것보다 우선적으로 키 입력을 가져가야 하는 경우 여기서 처리해야 한다.
주석을 보면, 아날로그 커서 클릭 등을 여기서 처리할 것 같다.
참고로 IInputProcessor
는 FSlateApplication::RegisterInputPreProcessor
를 통해 직접 추가할 수 있다.
bool FSlateApplication::ProcessKeyUpEvent( const FKeyEvent& InKeyEvent )
{
// Step 1.
FReply Reply = FReply::Unhandled();
SetLastUserInteractionTime(this->GetCurrentTime());
LastUserInteractionTimeForThrottling = LastUserInteractionTime;
// Bubble the key event
TSharedRef<FWidgetPath> EventPathRef = GetOrCreateUser(InKeyEvent)->GetFocusPath(); // Step 2-A. 현재 Focus Widget Path를 가져온다.
const FWidgetPath& EventPath = EventPathRef.Get();
// Switch worlds for widgets in the current path
FScopedSwitchWorldHack SwitchWorld(EventPath);
// Step 2-B. 현재 Focus Widget Path 상에 있는 SWidget에서 키 입력을 처리한다.
Reply = FEventRouter::RouteAlongFocusPath(this, FEventRouter::FBubblePolicy(EventPath), InKeyEvent, [](const FArrangedWidget& SomeWidgetGettingEvent, const FKeyEvent& Event)
{
if (SomeWidgetGettingEvent.Widget->IsEnabled())
{
const FReply TempReply = SomeWidgetGettingEvent.Widget->OnKeyUp(SomeWidgetGettingEvent.Geometry, Event);
#if WITH_SLATE_DEBUGGING
FSlateDebugging::BroadcastInputEvent(ESlateDebuggingInputEvent::KeyUp, TempReply, SomeWidgetGettingEvent.Widget, Event.GetKey().ToString());
#endif
return TempReply;
}
return FReply::Unhandled();
}, ESlateDebuggingInputEvent::KeyUp);
// If the key event was not processed by any widget...
if (!Reply.IsEventHandled() && UnhandledKeyUpEventHandler.IsBound())
{
Reply = UnhandledKeyUpEventHandler.Execute(InKeyEvent);
}
return Reply.IsEventHandled();
}
현재 Focus Widget Path 상의 하위 SWidget
부터 상위 SWidget
까지 OnKeyUp
을 호출하여 키 입력을 처리하도록 한다.
중간에 어떤 SWidget
이 키 입력을 핸들하면 리턴.
bool APlayerController::InputKey(FKey Key, EInputEvent EventType, float AmountDepressed, bool bGamepad)
{
// ...
bool bResult = false;
if (PlayerInput)
{
// Step 3. PlayerInput에 현재 들어온 키 입력을 기록
bResult = PlayerInput->InputKey(Key, EventType, AmountDepressed, bGamepad);
// ...
}
return bResult;
}
2번에서 현재 포커스된 위젯부터 상위 위젯까지 순차적으로 올라가며 OnKeyUp
을 호출했었다.
어떤 SWidget
도 키 입력을 핸들하지 않는다면, 가장 상위 SWidget
이 바로 SViewport
이므로 SViewport
에서 키 입력을 처리한다.
SViewport::OnKeyUp
에서는 현재 플레이어 컨트롤러의 PlayerInput
에 현재 들어온 키 입력을 기록한다.
UInputComponent::AddActionBinding
, UInputComponent::AddAxisBinding
등을 통해 InputComponent에 델리게이트를 추가했을 것이다.
void UPlayerInput::ProcessInputStack(const TArray<UInputComponent*>& InputComponentStack, const float DeltaTime, const bool bGamePaused)
{
// ...
// Step 4. 이미 InputComponent에 추가해두었던 Delegate Execute
// Dispatch the delegates in the order they occurred
NonAxisDelegates.Sort(FDelegateDispatchDetailsSorter());
for (const FDelegateDispatchDetails& Details : NonAxisDelegates)
{
if (Details.ActionDelegate.IsBound())
{
Details.ActionDelegate.Execute(Details.Chord.Key);
}
else if (Details.TouchDelegate.IsBound())
{
Details.TouchDelegate.Execute(Details.FingerIndex, Details.TouchLocation);
}
else if (Details.GestureDelegate.IsBound())
{
Details.GestureDelegate.Execute(Details.GestureValue);
}
}
// Now dispatch delegates for summed axes
for (const FAxisDelegateDetails& Details : AxisDelegates)
{
if (Details.Delegate.IsBound())
{
Details.Delegate.Execute(Details.Value);
}
}
for (const FVectorAxisDelegateDetails& Details : VectorAxisDelegates)
{
if (Details.Delegate.IsBound())
{
Details.Delegate.Execute(Details.Value);
}
}
}
UPlayerInput::ProcessInputStack
에서는 등록된 InputComponentStack과 Step 3에서 기록한 키 입력에 따라 미리 추가해두었던 델리게이트를 호출한다.