카테고리 없음

0819 인게임 UI 입력과 Common UI 트러블슈팅

iosers0564 2026. 8. 19. 22:08

구현한 기능

기능구현 내용
티어 업 UI 버튼 클릭부터 HUD 요청 전달까지 델리게이트 연결
선택 시스템 HUD 선택 대상과 PlayerController 명령 대상 동기화
체력 UI HealthComponent 델리게이트를 이용한 실시간 갱신
미니맵 좌클릭 카메라 이동, 드래그 이동, 우클릭 지휘관 명령
설정 메뉴 ESC로 열기와 닫기, 마우스 화면 고정
계속하기 설정 메뉴 비활성화 후 게임 복귀
나가기 현재 게임을 정리하고 로비 레벨로 이동
마우스 커서 게임 스타일의 커스텀 Software Cursor 적용
입력 피드백 선택 표시와 우클릭 이동 표시 구현 방식 설계

1. 티어 업 버튼 Dynamic Delegate 연결

티어 업 버튼 클릭부터 HUD까지 다음과 같은 전달 구조를 만들었다.

TierUpButton
→ USFSelectionPanelWidget
→ OnTierUpgradeRequested
→ USFInGameHUDWidget
→ 선택된 건물의 티어 업 요청
 

발생한 문제

구분내용
증상 실행 중 Dynamic Delegate 바인딩 구간에서 중단점 발생
오류 Unable to bind delegate to 'HandleTierUpgradeButtonClicked'
원인 함수가 리플렉션에 등록되지 않았거나 함수 이름을 정상적으로 찾지 못함
해결 핸들러에 UFUNCTION()을 추가하고 함수 포인터를 한 줄로 작성
추가 조치 에디터 종료 후 전체 빌드로 UHT 정보 갱신

버튼 델리게이트는 다음과 같이 연결했다.

 
TierUpButton->OnClicked.AddUniqueDynamic(this, &USFSelectionPanelWidget::HandleTierUpgradeButtonClicked);
 

선택 패널에서 발생한 요청은 HUD가 받도록 연결했다.

 
SelectionPanel->OnTierUpgradeRequested.AddUniqueDynamic(this, &USFInGameHUDWidget::HandleTierUpgradeRequested);
 

Dynamic Delegate는 일반 C++ 함수 포인터와 달리 리플렉션 시스템을 사용한다. 따라서 함수 선언, UFUNCTION() 여부, 빌드 상태를 함께 확인해야 한다.

2. UI 선택 대상과 실제 명령 대상 동기화

지휘관을 선택하면 UI에는 지휘관 정보가 표시됐지만, 이후 건물을 선택해도 우클릭 시 이전 지휘관이 이동하는 문제가 발생했다.

문제 분석

구분HUDPlayerController
저장 값 UI에 표시할 SelectedActor 명령을 수행할 SelectedActor
기존 상태 새로운 선택 대상으로 갱신됨 이전 선택 대상이 남아 있음
결과 건물 정보 표시 이전 지휘관에게 이동 명령 전달

원인은 HUD와 PlayerController가 서로 다른 선택 상태를 가지고 있었기 때문이다.

해결

HUD에서 선택을 적용할 때 PlayerController에도 동일한 액터를 전달했다.

 
PlayerController->SetSelectedActor(NewSelectedActor);
 

선택이 해제되거나 선택된 액터가 파괴됐을 때도 함께 초기화했다.

 
PlayerController->SetSelectedActor(nullptr);
 

선택 처리는 다음 흐름으로 통일했다.

마우스 선택 입력
→ HUD 라인트레이스
→ 새로운 액터 선택
→ HUD SelectedActor 갱신
→ PlayerController SelectedActor 갱신
→ 선택 정보 UI 갱신
 

UI 표시와 게임 명령이 같은 대상을 사용하려면 선택 상태의 기준을 하나로 통일하거나, 상태 변경 시 모든 관련 객체를 함께 갱신해야 한다.

3. 선택된 액터의 실시간 체력 갱신

기존 선택 정보는 액터를 처음 클릭했을 때만 체력을 가져왔다. 선택 이후 체력이 변경돼도 UI에는 이전 값이 남아 있었다.

이를 해결하기 위해 선택된 액터의 UHealthComponent가 제공하는 체력 변경 델리게이트를 HUD에 연결했다.

체력 바인딩 흐름

액터 선택
→ HealthComponent 탐색
→ OnHealthValueChanged 바인딩
→ 체력 변경 이벤트 발생
→ SelectedInfo 갱신
→ SelectionPanel 갱신
 

체력이 변경되면 다음 값을 업데이트했다.

데이터사용 목적
BeforeHP 변경 전 체력 확인 및 로그
CurrentHP 현재 체력 UI 표시
MaxHP 프로그레스 바 비율 계산

핸들러에서는 선택 정보 구조체를 갱신한 후 패널을 다시 표시했다.

 
SelectedInfo.CurrentHealth = CurrentHP;
SelectedInfo.MaximumHealth = MaxHP;
SelectionPanel->ShowSelection(SelectedInfo);
 

선택 대상이 바뀔 때는 이전 HealthComponent의 델리게이트를 제거하고 새로운 대상에 바인딩했다.

기존 대상 Unbind
→ 새로운 대상 저장
→ 새로운 HealthComponent Bind
 

Tick에서 지속적으로 체력을 조회하는 방식보다 값이 변경될 때만 UI를 갱신하는 델리게이트 방식이 효율적이다.

4. 미니맵 입력 기능 분리

미니맵에서 사용하는 마우스 버튼에 따라 서로 다른 명령을 수행하도록 구현했다.

입력동작카메라 이동
좌클릭 해당 월드 위치로 카메라 이동 O
좌클릭 드래그 마우스 위치를 따라 카메라 연속 이동 O
우클릭 선택된 지휘관에게 이동 명령 전달 X

초기에는 좌클릭 시 카메라가 한 번만 이동했다. 이후 마우스 버튼을 누르고 있는 동안 위치를 계속 계산하도록 수정해 드래그 이동을 구현했다.

우클릭은 카메라 위치를 변경하지 않고 기존 이동 델리게이트를 사용했다.

 
OnMoveInput.Broadcast(Destination);
 

미니맵 좌표는 다음 과정을 통해 월드 좌표로 변환했다.

마우스 절대 좌표
→ 미니맵 로컬 좌표
→ 0~1 범위 정규화
→ 월드 경계 좌표로 변환
 

카메라 이동과 유닛 명령은 같은 미니맵 좌표를 사용하지만, 입력 목적이 다르므로 처리 경로를 분리했다.

5. ESC 설정 메뉴 구현

ESC 입력은 Enhanced Input의 IA_SFToggleSettings를 사용했다.

ESC 입력
→ ASFInGamePlayerController
→ USFInGameHUDWidget::ToggleSettings()
→ SettingsUIStack에 WBP_SFSettings 추가
 

설정 메뉴는 열고 닫히며 입력과 포커스를 가져가는 화면이므로 CommonActivatableWidget으로 구현했다.

설정 메뉴 구성

구성 요소역할
SettingsUIStack 설정 메뉴를 관리하는 Common UI 스택
WBP_SFSettings 계속하기, 설정, 나가기 버튼 표시
ActiveSettingsWidget 현재 활성화된 설정 메뉴 저장
ToggleSettings() ESC 입력에 따라 열기 또는 닫기
HandleSettingsDeactivated() 메뉴가 닫힌 후 참조 정리

메뉴가 열린 상태에서 ESC를 다시 눌렀을 때 닫히도록 OnKeyDown을 오버라이드했다.

OnKeyDown
→ 입력 키가 ESC인지 확인
→ DeactivateWidget()
→ Handled 반환
 

마우스는 설정 메뉴가 열려도 게임 화면 밖으로 빠져나가지 않도록 다음 입력 설정을 유지했다.

 
FInputModeGameAndUI InputMode;
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways);
SetInputMode(InputMode);
 

6. 계속하기와 로비 나가기

설정 메뉴의 계속하기와 나가기 버튼은 USFSettingsWidget에서 관리했다.

버튼 기능

버튼실행 내용
계속하기 현재 설정 위젯 비활성화
설정 다른 담당자의 설정 WBP가 준비되면 연결 예정
나가기 USFGameInstance::MoveToLobby() 호출

계속하기는 현재 게임 상태를 유지한 채 메뉴만 닫는다.

 
void USFSettingsWidget::HandleContinueButtonClicked()
{
DeactivateWidget();
}
 

나가기는 WBP_Lobby를 직접 생성하지 않고 로비 위젯을 사용하는 로비 레벨로 이동한다.

 
void USFSettingsWidget::HandleExitButtonClicked()
{
USFGameInstance* GameInstance = Cast<USFGameInstance>(GetGameInstance());
 
if (!IsValid(GameInstance))
{
return;
}
 
GameInstance->MoveToLobby();
}
 

레벨이 전환되면서 현재 게임 월드에 존재하던 액터와 HUD가 정리된다. GameInstance는 유지되므로 공통 데이터와 레벨 전환 기능을 계속 사용할 수 있다.

7. 설정 메뉴 버튼이 한 번만 동작하는 문제

처음 설정 메뉴를 열었을 때는 계속하기와 나가기 버튼이 동작했지만, 메뉴를 다시 열거나 로비에서 스테이지에 재입장하면 버튼이 반응하지 않았다.

트러블슈팅

구분내용
증상 첫 번째 설정 메뉴에서는 버튼 동작, 두 번째부터 반응 없음
기존 바인딩 NativeOnInitialized()에서 버튼 바인딩
기존 해제 NativeDestruct()에서 버튼 바인딩 제거
원인 Common UI가 위젯을 재사용하면 초기화 함수가 다시 호출되지 않을 수 있음
해결 바인딩 시점을 NativeConstruct()로 변경
결과 설정 메뉴를 반복해서 열어도 버튼 정상 동작

수정한 생명주기 구조는 다음과 같다.

NativeConstruct
→ 버튼 델리게이트 연결
 
NativeDestruct
→ 버튼 델리게이트 제거
 
 
void USFSettingsWidget::NativeConstruct()
{
Super::NativeConstruct();
 
ContinueButton->OnClicked.AddUniqueDynamic(this, &USFSettingsWidget::HandleContinueButtonClicked);
ExitButton->OnClicked.AddUniqueDynamic(this, &USFSettingsWidget::HandleExitButtonClicked);
}
 

Common UI는 위젯을 단순히 생성하고 제거하는 것이 아니라 활성화, 비활성화, 재사용할 수 있다. 따라서 바인딩과 해제 시점을 대응되는 생명주기 함수에 배치해야 한다.

8. RTS 커스텀 마우스 커서 적용

게임 건물에서 사용하는 재질과 색상을 참고해 커스텀 마우스 커서를 제작했다.

요소커서에 적용한 표현
비스트 건물 상아 뿔과 짙은 석재
에테르 건물 청록 수정
공통 장식 황동 테두리
전체 스타일 귀여운 로우폴리 미니어처

커서는 PNG 텍스처를 사용하는 Software Cursor 방식으로 적용했다.

커서 적용 구조

커서 PNG
→ UI Texture로 임포트
→ WBP_SFDefaultCursor의 Image에 적용
→ Project Settings의 Software Cursors에 등록
 

텍스처에는 다음 설정을 사용했다.

옵션설정값
Compression Settings UserInterface2D (RGBA)
Texture Group UI
Mip Gen Settings NoMipmaps
Filter Bilinear

9. 커서 외형과 실제 클릭 위치가 다른 문제

커서 외형은 정상적으로 변경됐지만 실제 클릭은 화살표 끝이 아니라 커서 위젯 중앙에서 발생했다.

트러블슈팅

구분내용
증상 커서 끝과 실제 클릭 위치가 일치하지 않음
실제 클릭 위치 Software Cursor 위젯의 중앙
이미지 시작 위치 기존 위젯의 좌측 상단
원인 클릭 기준점과 화살표 끝의 위치가 서로 다름
해결 커서 이미지의 좌측 상단을 루트 위젯 중앙에 배치

수정한 위젯 구조는 다음과 같다.

SizeBox 96 × 96
└─ CanvasPanel
└─ Image
Position: 48, 48
Size: 48, 48
 

위젯 중앙과 이미지 시작점을 (48, 48)로 일치시켰다.

SizeBox 중앙
= 실제 마우스 클릭 위치
= 커서 이미지의 화살표 끝
 

커스텀 커서는 이미지 교체만으로 끝나지 않는다. 실제 클릭 좌표가 커서 끝부분과 일치하는지 Hot Spot을 반드시 확인해야 한다.

다음 작업

유닛 선택 표시와 우클릭 이동 지점 표시는 이미지 기반 UI 대신 월드 이펙트로 구현할 예정이다.

기능구현 기술동작
선택 표시 UDecalComponent와 절차적 머티리얼 선택된 액터 발밑에 얇은 원 표시
선택 해제 컴포넌트 제거 또는 숨김 이전 선택 표시 제거
이동 지점 표시 Niagara System 네 방향 화살표가 중심으로 모임
이펙트 종료 Normalized Age와 Auto Destroy 축소 및 페이드 후 자동 제거

선택 표시는 이미지 없이 SphereMask 또는 RadialGradientExponential을 이용해 제작한다.

우클릭 이동 표시는 OnMoveInput으로 전달된 목적지에 Niagara를 생성하는 방식으로 연결할 예정이다.

임무 UI는 GameState에 임무 데이터와 진행도 델리게이트가 추가된 이후 진행한다.

오늘의 핵심 정리

주제배운 점
Dynamic Delegate UFUNCTION과 리플렉션 정보가 필요하다
선택 시스템 UI 선택 대상과 실제 명령 대상을 동기화해야 한다
체력 UI Tick보다 상태 변경 델리게이트가 효율적이다
미니맵 동일한 좌표라도 입력 목적에 따라 처리 경로를 분리해야 한다
Common UI 위젯 재사용을 고려해 생명주기와 바인딩을 설계해야 한다
레벨 전환 위젯이 아니라 해당 위젯을 생성하는 레벨로 이동해야 한다
Software Cursor 외형뿐만 아니라 실제 클릭 Hot Spot 정렬이 중요하다