[Unreal Engine] Input System

Jangmanbo·2024년 8월 27일
1

이미 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도 동일하다😊)

1. IInputProcesser

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에서 성공적으로 키 입력을 처리했다면 이후 스텝을 거치지 않는다.

그 어떤 것보다 우선적으로 키 입력을 가져가야 하는 경우 여기서 처리해야 한다.
주석을 보면, 아날로그 커서 클릭 등을 여기서 처리할 것 같다.

참고로 IInputProcessorFSlateApplication::RegisterInputPreProcessor를 통해 직접 추가할 수 있다.

2. SWidget

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이 키 입력을 핸들하면 리턴.

3. APlayerController

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에 현재 들어온 키 입력을 기록한다.

4. UInputComponent

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에서 기록한 키 입력에 따라 미리 추가해두었던 델리게이트를 호출한다.

0개의 댓글