
Enhanced Input 키 리맵 UI 구현 흐름 정리
이번 글에서는 키 변경 UI가 실제로 어떤 흐름으로 동작하는지 정리해보겠습니다.
예시는 UWidget_ListEntry_KeyRemap, UWidget_KeyRemapScreen, 그리고 IInputProcessor를 사용한 구조입니다.
전체 흐름은 아래처럼 보면 이해하기 쉽습니다.
- 리스트 엔트리에서 변경 / 리셋 버튼 클릭
- 키 입력을 받을 전용 화면 표시
- 화면 활성화 시 Input Preprocessor 등록
- 사용자 입력을 먼저 감지
- 유효한 입력이면 실제 키 매핑 변경
- 설정 저장 후 화면 종료
- 비활성화 시점에 Input Preprocessor 해제
즉, 단순히 버튼을 눌러 키를 바꾸는 구조가 아니라 입력을 기다리는 전용 화면을 띄우고, 그 안에서 입력을 먼저 받아 처리한 뒤, 안전하게 닫고 저장하는 구조입니다.

1. 리스트 엔트리에서 버튼 이벤트 바인딩
UWidget_ListEntry_KeyRemap에서는 변경 버튼과 리셋 버튼의 클릭 이벤트를 바인딩합니다.
CommonButton_RemapKey->OnClicked().AddUObject(this, &ThisClass::OnRemapKeyButtonClicked);
CommonButton_ResetKeyBinding->OnClicked().AddUObject(this, &ThisClass::OnResetKeyBindingButtonClicked);
여기서 핵심은 변경 버튼을 눌렀다고 바로 키를 바꾸는 것이 아니라, 먼저 새로운 입력을 받을 전용 화면을 띄운다는 점입니다.
2. 키 입력을 받을 전용 화면 생성
변경 버튼을 누르면 키 입력을 받을 위젯인 UWidget_KeyRemapScreen을 생성하고, 입력 성공 / 취소 시 호출될 함수를 바인딩합니다.
UWidget_KeyRemapScreen* CreatedKeyRemapScreen = CastChecked<UWidget_KeyRemapScreen>(PushedWidget);
if (CachedOwningKeyRemapDataObject)
{
CreatedKeyRemapScreen->SetDesiredInputTypeToFilter(CachedOwningKeyRemapDataObject->GetDesiredInputKeyType());
CreatedKeyRemapScreen->OnKeyRemapScreenKeyPressed.BindUObject(this, &ThisClass::OnKeyToRemapPressed);
CreatedKeyRemapScreen->OnKeyRemapScreenKeySelectCanceled.BindUObject(this, &ThisClass::OnKeyRemapCanceled);
}
이 단계에서 하는 일은 크게 두 가지입니다.
- 어떤 종류의 입력을 받을지 필터링
- 키 입력 성공 / 취소 시 호출할 함수 연결
즉, UWidget_KeyRemapScreen은 입력을 실제로 받아오는 화면이고, UWidget_ListEntry_KeyRemap은 받아온 결과를 실제 설정 변경에 반영하는 역할을 합니다.
3. 유효한 키 입력이 들어오면 실제 매핑 변경
입력 화면에서 유효한 키가 감지되면 최종적으로 아래 함수가 호출됩니다.
void UWidget_ListEntry_KeyRemap::OnKeyToRemapPressed(const FKey& PressedKey)
{
SelectThisEntryWidget();
if (CachedOwningKeyRemapDataObject)
{
CachedOwningKeyRemapDataObject->BindNewInputKey(PressedKey);
}
}
이 함수는 선택된 키를 받아서 DataObject에 전달하는 역할만 하고, 실제 입력 매핑 변경은 아래 함수에서 수행합니다.
void UListDataObject_KeyRemap::BindNewInputKey(const FKey& InNewKey)
{
check(CachedOwningInputUserSettings);
FMapPlayerKeyArgs KeyArgs;
KeyArgs.MappingName = CachedOwningMappingName;
KeyArgs.Slot = CachedOwningMappableKeySlot;
KeyArgs.NewKey = InNewKey;
FGameplayTagContainer Container;
CachedOwningInputUserSettings->MapPlayerKey(KeyArgs, Container);
CachedOwningInputUserSettings->SaveSettings();
NotifyListDataModified(this);
}
🔸FMapPlayerKeyArgs KeyArgs
어떤 액션을 어떤 슬롯에서 어떤 키로 바꿀 것인지 전달하는 입력 데이터입니다.
🔸FGameplayTagContainer Container
입력 매핑 결과와 관련된 태그 정보를 받을 수 있는 컨테이너입니다.
🔸CachedOwningInputUserSettings->MapPlayerKey(KeyArgs, Container)
플레이어 입력 설정에서 특정 액션의 키를 새 키로 변경합니다. 즉, 사용자가 보고 있는 A -> B 변경 작업이 여기서 실제로 일어납니다.
🔸CachedOwningInputUserSettings->SaveSettings()
변경된 입력 설정을 디스크에 저장합니다. 이 과정을 거쳐야 다음 실행 시에도 바뀐 설정이 유지됩니다.
🔸NotifyListDataModified(this)
변경된 내용을 리스트 UI에 다시 반영하기 위해 데이터가 수정되었음을 알립니다.
4. 입력 전처리용 인터페이스 IInputProcessor
IInputProcessor는 Slate 입력 시스템이 키보드, 마우스, 아날로그 입력 등을 위젯에 넘기기 전에, 중간에서 먼저 받아서 처리할 수 있게 해주는 타입입니다.
공식 API 기준으로 HandleKeyDownEvent, HandleKeyUpEvent, HandleMouseMoveEvent, HandleMouseButtonDownEvent, HandleMouseWheelOrGestureEvent, HandleAnalogInputEvent, Tick, GetDebugName 같은 함수를 오버라이드할 수 있습니다.
또한 FSlateApplication::RegisterInputPreProcessor로 등록해서 사용하고, 필요하면 UnregisterInputPreProcessor로 해제합니다.
이번 경우에는 마우스와 키보드 입력만 받으면 되기 때문에 아래 두 개만 오버라이드하면 충분합니다.
- HandleKeyDownEvent
- HandleMouseButtonDownEvent
이 구조를 쓰는 이유는 단순합니다. 키 리맵 화면이 떠 있는 동안에는 일반 위젯 이벤트보다 지금 눌린 입력이 무엇인지 먼저 감지하는 것이 더 중요하기 때문입니다.

5. UWidget_KeyRemapScreen::NativeOnActivated()
입력 화면이 활성화될 때 Input Preprocessor를 생성하고, 키 입력 / 취소 이벤트를 바인딩한 뒤 Slate에 등록합니다.
CachedInputPreprocessor = MakeShared<FKeyRemapScreenInputPreprocessor>(CachedInputType);
CachedInputPreprocessor->OnInputPreProcessorKeyPressed.BindUObject(this, &ThisClass::OnValidKeyPressedDetected);
CachedInputPreprocessor->OnInputPreProcessorKeySelectCanceled.BindUObject(this, &ThisClass::OnKeySelectCanceled);
FSlateApplication::Get().RegisterInputPreProcessor(CachedInputPreprocessor, -1);
왜 MakeShared를 사용하는가?
Input Preprocessor는 생성 후 Slate에 등록되고, 나중에 해제할 때까지 살아 있어야 합니다. 지역 변수로 만들면 함수가 끝날 때 사라질 수 있기 때문에 TSharedPtr로 관리합니다.
6. 입력이 들어오면 바로 닫지 않고 다음 틱에 비활성화
유효한 키가 눌리거나 취소가 발생하면 아래 함수가 호출됩니다.
void UWidget_KeyRemapScreen::OnValidKeyPressedDetected(const FKey& PressedKey)
{
RequestDeactivateWidget(
[this, PressedKey]()
{
//Debug::Print(TEXT("Pressed Key: ") + PressedKey.GetDisplayName().ToString());
OnKeyRemapScreenKeyPressed.ExecuteIfBound(PressedKey);
}
);
}
void UWidget_KeyRemapScreen::OnKeySelectCanceled(const FString& CanceledReason)
{
RequestDeactivateWidget(
[this, CanceledReason]()
{
Debug::Print(CanceledReason);
OnKeyRemapScreenKeySelectCanceled.ExecuteIfBound(CanceledReason);
}
);
}
void UWidget_KeyRemapScreen::RequestDeactivateWidget(TFunction<void()> PreDeactivateCallback)
{
FTSTicker::GetCoreTicker().AddTicker(
FTickerDelegate::CreateLambda(
[PreDeactivateCallback, this](float DeltaTime)->bool
{
PreDeactivateCallback();
DeactivateWidget();
return false;
}
)
);
}
AddTicker를 하는 이유는?
핵심은 병렬 처리 때문이 아니라, 현재 입력 이벤트의 호출 스택이 아직 끝나지 않았기 때문입니다.
즉 코드는 직렬 실행이 맞지만, Slate가 아직 현재 입력 이벤트를 처리 중인 상태에서 위젯을 바로 닫아버리면 내부 상태가 꼬일 수 있습니다. 그래서 AddTicker를 사용해 다음 틱에 안전하게 위젯을 닫도록 한 것입니다.
키보드 키를 하나 눌렀을 때의 내부 흐름을 단순화하면 아래와 같습니다.
- OS 입력
- FSlateApplication
- InputProcessor (선처리)
- Slate Widget
- UMG Widget
- 게임 로직
즉 InputProcessor는 위젯이 입력을 받기 전에 먼저 가로채는 시스템입니다.
- 사용자 입력
- FSlateApplication
- InputProcessor가 입력 먼저 감지
- Widget 처리
Slate는 입력 처리 중 아래 같은 흐름을 이어서 수행합니다.
FSlateApplication::ProcessKeyDownEvent()
{
RouteInputToProcessors(); // InputProcessor 실행
RouteInputToWidgets(); // 위젯에 전달
UpdateFocus();
}
즉, 아래 시점에서 중요한 점은 ProcessKeyDownEvent 함수가 아직 끝나지 않았다는 것입니다.
FSlateApplication::ProcessKeyDownEvent
→ InputProcessor::HandleKeyDownEvent
→ ProcessPressedKey
→ OnValidKeyPressedDetected
→ DeactivateWidget()
여기서 위젯을 즉시 제거하면 Slate 입장에서는 "지금 처리 중인 입력 대상이 도중에 사라진 상태"가 되어 내부 포커스나 위젯 상태가 꼬일 수 있습니다.
그래서 지금 프레임의 입력 처리를 먼저 끝내고, 다음 틱에 콜백을 실행한 뒤 위젯을 비활성화하는 구조가 더 안전합니다.
정리하면, 이것은 병렬 수행 문제가 아니라 입력 이벤트 처리 도중에 객체를 제거하지 않기 위한 안전장치라고 이해하면 됩니다.
7. UWidget_KeyRemapScreen::NativeOnDeactivated()
입력 화면이 비활성화될 때는 등록했던 Input Preprocessor를 반드시 해제해줍니다.
if (CachedInputPreprocessor)
{
FSlateApplication::Get().UnregisterInputPreProcessor(CachedInputPreprocessor);
CachedInputPreprocessor.Reset();
}
이 정리 과정이 중요한 이유는 두 가지입니다.
- 화면이 닫힌 뒤에도 입력을 계속 가로채는 문제 방지
- Shared Pointer를 해제해 수명 정리
즉, 활성화 시 등록하고 비활성화 시 해제하는 구조로 입력 처리 범위를 명확하게 관리하는 것입니다.
8. 전체 흐름 다시 정리
- 리스트 엔트리에서 변경 버튼 클릭
- UWidget_KeyRemapScreen 표시
- NativeOnActivated()에서 IInputProcessor 등록
- 사용자가 키 또는 마우스 입력
- Input Preprocessor가 입력을 먼저 감지
- 유효한 키면 OnKeyToRemapPressed 호출
- MapPlayerKey로 입력 매핑 변경
- SaveSettings()로 저장
- 다음 틱에서 위젯 비활성화
- NativeOnDeactivated()에서 Input Preprocessor 해제
'🎯 game engine > ◽ 언리얼(unreal)' 카테고리의 다른 글
| [Unreal / C++] Game Ability System 정리 (입력 바인딩부터 쿨타임 처리까지) (1) | 2026.03.03 |
|---|---|
| [Unreal] Behavior Tree 이해하면서 몬스터 AI 만들기 (0) | 2025.12.02 |
| [Unreal / C++] AAA Frontend UI/Menu 만들기 (프레임 워크 정리 + 델리게이트, 서브시스템) (1) | 2025.11.20 |
| [Unreal] UI 세팅/구조 분석 (Set Up Common UI) (7) | 2025.09.16 |
| [Unreal] Frontend UI Subsystem (2) | 2025.09.14 |
존잘 프로그래머가 되고싶어