UE5 - Project/개인 프로젝트 - FPS TPS
[UE5] - UI를 사용한 키리맵핑
KimGeon-U
2025. 2. 13. 02:55
기존의 DataAsset을 사용한 IMC 맵핑 후 UI통한 키리맵핑에 대해서 추가 작성하도록 하겠습니다.
키리맵핑 API 구현시 사용된 함수 및 관련 함수
UI 관련
NativeOnInitialized
- 위젯 생성 시 호출되는 UUserWidget내 함수
- Initialized()와 같이 위젯 생성시 초기화, AddDynamic과같은 바인딩 작업 할 시 사용
- Initialized는 리턴타입 bool, 내부적으로 초기화시 많은 내부적인 작업을 처리
- NativeOnInitialized 리턴타입 void, 언리얼 블루프린트와 관련된 OnInitialized 호출
- 기존의 Native가 아닌 함수들은 BlueprintImplementableEvent로 인한 C++클래스 내 재정의 불가
- Ex) PreConstruct, OnInitialized 등
bIsFocusable
- UI위젯이 키보드 입력을 받을 수 있도록 설정하는 변수
- 기본값 false
- Set Keyboard, OnKeyDown, OnKeyUp등의 키보드 관련 이벤트 함수 처리 가능
FReply NativeOnKeyDown(FGeometry,FKeyEvent)
- UI 위젯 내 키보드 키가 눌렸을 때 실행되는 함수
- ETriggerEvent::Started와 같은 개념
- bIsFocusable가 활성화 된 경우 UI를 통한 키 입력 처리 가능
- FGeometry
- UI 위젯의 위치, 크기, 화면 변환 정보를 포함하는 구조체
- 배치 정보 및 좌표 변환 시 사용
- UI 내부에서 마우스나 키보드 입력의 좌표 변환 가능
- FKeyEvent
- 키보드 이벤트 정보를 담고있는 구조체
- 어떤 키가 눌렸는지, 조합키(Shift,Ctrl,Alt)가 눌렸는지 확인 가능
- GetKey() : 눌린 키 (FKey 타입) 반환
- Is[Shitft/Control/Alt/Reapeat]()
- 각각 [Shitft/Ctrl/Alt] 키가 눌렸는지에 대한 여부
- IsRepeat
- 키가 반복 입력 중인지 확인
- 길게 누를시 True
- ETriggerEvent::Triggered같은 개념
- FReply
- 입력을 어떻게 처리할지에 대한 여부를 결정하는 구조체
- Handled() : UI에서 입력을 처리했음을 엔진에 알린다. (게임에 전달되지 않는다.)
- UnHandled() : UI에서 입력을 무시, (게임 혹은 다른위젯에 전달)
- 해당 위젯이 활성화 되어있음에도, 게임 내 캐릭터가 특정 FKey의 입력을 받을 때 등
- CaptureMouse(Widget) : 특정 위젯에서 마우스 입력 캡처
- ReleaseMouseCapure() : 마우스 입력 캡처 해제
- SetUserFocus(Widget) : 특정 위젯에 UI 포커스 설정
- SetKeyboardFocus(Widget) : 특정 위젯에서 키보드 입력 유지
- SetMousePos(Position) : 마우스 커서 위치 설정
OnClicked, Pressed,Hovered...
- UMG UButton버튼 관련 이벤트 처리 함수
- AddDynamic을 통해 C++에서 이벤트 바인딩
- OnClicked : 버튼이 클릭될 때 실행
- OnPressed : 버튼을 눌렀을 때 실행
- OnReleased : 버튼을 땟을 때 실행
- OnHovered : 버튼에 마우스가 올라갔을 때 실행
- OnUnhovered : 버튼에서 마우스가 벗어났을 때 실행
키리맵핑 관련
UInputMappintContext (IMC)
- 여러개의 키 바인딩을 저장하는 컨테이너
- 한번의 컨텍스트에 여러 입력 액션을 포함 가능
- 해당 컨텍스트를 UEnhancedInputLocalPlayerSubsystem*을 통한 입력 제어 변경 가능
FEnhancedActionKeyMapping
- UInputAction과 키를 연결하는 구조체
- IMC에 포함할 키들을 개별적으로 매핑을 담당
- 여러개의 키를 하나의 액션에 바인딩 가능
- FKey, Modifiers,Triggers등 추가 가능
UInputModifier
- 입력 값을 수정하는 역할
- IMC의 Modifiers
- 외 입력값 Value 반대로 변경하는 UInputModifierNegate와 같은 다양한 입력 값 관련 조절 가능
- IMC에 매핑시 배열형식의 TArray<UInputModifier*> modifiers를 사용한 모디파이어 추가 가능
키리맵핑 구현
- ActionName / Binding Action FKey 형식의 구조
- 버튼 클릭(OnClicked) 시 SetText()내 입력된 FKey값 " - " 변경
- 적용 버튼 클릭 시 변경된 값 적용, 되돌리기를 통해 기본값으로 복원
OptionMenuWidget.cpp
// 초기화
void UUserWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
// ... 유효성 검사
// UButton 이벤트 바인딩 과정
// IsBound : 이미 바인딩이 되어있는가?
if (ApplyButton && !ApplyButton->OnClicked.IsBound())
{
ApplyButton->OnClicked.AddDynamic(this, &UUserWidget::OnApplyButtonClicked);
}
if (ResetButton && !ResetButton->OnClicked.IsBound())
{
ResetButton->OnClicked.AddDynamic(this, &UUserWidget::OnResetButtonClicked);
}
}
// Apply Button OnClicked
void UUserWidget::OnApplyButtonClicked()
{
for (auto& KeyMapping : CurrentKeyDataAsset->KeyMappings)
{
// Enum to String
FString ActionName = StaticEnum<EPlayableInputAction>()->GetNameStringByValue(static_cast<int64>(KeyMapping.InputActionEnum));
// 적용을 누르기 전까지 DataAssets에 반영되지 않는다. TMap<FString,FKey> PendingKeyChanges를 통한 변경 값 임시 저장
// Apply시 임시 저장한 PendingKeyChanges를 연동한 DataAssets에 반영한다.
if (PendingKeyChanges.Contains(ActionName))
{
KeyMapping.CurrentKey = PendingKeyChanges[ActionName];
}
}
// 적용 후 초기화
PendingKeyChanges.Empty();
// PlayController에게 변경된 DataAsset을 통한 키 리맵핑 요청
if (APlayerController* PC = Cast<APlayerController>(GetOwningPlayer()))
{
PC->UpdateCurrentIMC(CurrentKeyDataAsset);
}
}
// Reset 로직... (생략)
// Apply와 같은 로직, CurrentKey를 DefaultKey로 변경 후 요청
// 임시저장 PendingKeyChanges 초기화
// 델리게이트 Dynamic_Multicast
void UUserWidget::HandleKeyBindingUpdated(UOptionKeyBindWidget* Widget, FKey NewKey)
{
if (!Widget) return;
PendingKeyChanges.Add(Widget->CurrentData.ActionName, NewKey);
}
PlayerController
void PlayerController::UpdateCurrentIMC(DataAsset* CurrentDataAsset)
{
// 동적 환경에서 빙의된 폰이 있는지에 대한 검사.
// 빙의가 된 경우, Subsystem 최신화, 아닌경우 변경된 DataAsset과 매핑한 IMC만 변경하기위해
bool bHasPawn = (GetPawn() != nullptr);
if (ULocalPlayer* LocalPlayer = GetLocalPlayer())
{
if (UEnhancedInputLocalPlayerSubsystem* SubSystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
{
// TMap ControlModeMap : 3인칭,1인칭,그외(자동차, 드론 등)을 구분하는 Key, IMC와DataAssets을 매핑한 Value
for (auto& ControlMap : ControlModeMap)
{
// UI에서 받아온 DataAsset과 일치한 경우를 모두 변경하기위한 조건문 추가
if (ControlMap.Value.DataAsset == CurrentDataAsset)
{
// IMC 인스턴스 생성
UInputMappingContext* NewIMC = NewObject<UInputMappingContext>(this);
// IMC 매핑할 Key들을 각각 하나씩 설정한다.
for (const FPlayerDefaultInputKeyMapping& Mapping : CurrentDataAsset->KeyMappings)
{
if (Mapping.InputAction && Mapping.CurrentKey.IsValid())
{
// 개별 키 FEnhancedActionKeyMapping를 통한 DataAsset내 IA, FKey 설정
FEnhancedActionKeyMapping& NewMapping = NewIMC->MapKey(Mapping.InputAction, Mapping.CurrentKey);
// 개발자가 미리 설정한 Modifier을 다시 적용한다.
// 해당 과정 누락 시 모디파이어를 통한 Ex) FKey A Value -1(Native) -> 1로 변경, 원하는 입력이 안된다.
for (UInputModifier* Modifier : Mapping.Modifiers)
{
if (Modifier)
{
NewMapping.Modifiers.Add(Modifier);
}
}
}
}
// DataAsset과 매핑한 IMC내 IA들을 변경된 NewIMC를 통해 변경
ControlMap.Value.IMC = NewIMC;
// 빙의된 Pawn/Character이 있을 경우, 실시간 반영을 위한 Subsystem을 통한 IMC 변경
// Ex) 인게임 내 조작키 변경 시 실시간 반영
if (bHasPawn && ControlMap.Key == CurrentMode)
{
SubSystem->ClearAllMappings();
SubSystem->AddMappingContext(NewIMC, 0);
}
}
}
}
}
}
구현 결과
https://www.youtube.com/watch?v=atH2eThw2NI
출처 및 참고내역
https://community.gamedev.tv/t/nativeoninitialized-may-replace-initialize-and-setup/152157
https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/UMG/Components/UButton