구현한 기능
| 티어 업 UI | 버튼 클릭부터 HUD 요청 전달까지 델리게이트 연결 |
| 선택 시스템 | HUD 선택 대상과 PlayerController 명령 대상 동기화 |
| 체력 UI | HealthComponent 델리게이트를 이용한 실시간 갱신 |
| 미니맵 | 좌클릭 카메라 이동, 드래그 이동, 우클릭 지휘관 명령 |
| 설정 메뉴 | ESC로 열기와 닫기, 마우스 화면 고정 |
| 계속하기 | 설정 메뉴 비활성화 후 게임 복귀 |
| 나가기 | 현재 게임을 정리하고 로비 레벨로 이동 |
| 마우스 커서 | 게임 스타일의 커스텀 Software Cursor 적용 |
| 입력 피드백 | 선택 표시와 우클릭 이동 표시 구현 방식 설계 |
1. 티어 업 버튼 Dynamic Delegate 연결
티어 업 버튼 클릭부터 HUD까지 다음과 같은 전달 구조를 만들었다.
발생한 문제
| 증상 | 실행 중 Dynamic Delegate 바인딩 구간에서 중단점 발생 |
| 오류 | Unable to bind delegate to 'HandleTierUpgradeButtonClicked' |
| 원인 | 함수가 리플렉션에 등록되지 않았거나 함수 이름을 정상적으로 찾지 못함 |
| 해결 | 핸들러에 UFUNCTION()을 추가하고 함수 포인터를 한 줄로 작성 |
| 추가 조치 | 에디터 종료 후 전체 빌드로 UHT 정보 갱신 |
버튼 델리게이트는 다음과 같이 연결했다.
선택 패널에서 발생한 요청은 HUD가 받도록 연결했다.
Dynamic Delegate는 일반 C++ 함수 포인터와 달리 리플렉션 시스템을 사용한다. 따라서 함수 선언, UFUNCTION() 여부, 빌드 상태를 함께 확인해야 한다.
2. UI 선택 대상과 실제 명령 대상 동기화
지휘관을 선택하면 UI에는 지휘관 정보가 표시됐지만, 이후 건물을 선택해도 우클릭 시 이전 지휘관이 이동하는 문제가 발생했다.
문제 분석
| 저장 값 | UI에 표시할 SelectedActor | 명령을 수행할 SelectedActor |
| 기존 상태 | 새로운 선택 대상으로 갱신됨 | 이전 선택 대상이 남아 있음 |
| 결과 | 건물 정보 표시 | 이전 지휘관에게 이동 명령 전달 |
원인은 HUD와 PlayerController가 서로 다른 선택 상태를 가지고 있었기 때문이다.
해결
HUD에서 선택을 적용할 때 PlayerController에도 동일한 액터를 전달했다.
선택이 해제되거나 선택된 액터가 파괴됐을 때도 함께 초기화했다.
선택 처리는 다음 흐름으로 통일했다.
UI 표시와 게임 명령이 같은 대상을 사용하려면 선택 상태의 기준을 하나로 통일하거나, 상태 변경 시 모든 관련 객체를 함께 갱신해야 한다.
3. 선택된 액터의 실시간 체력 갱신
기존 선택 정보는 액터를 처음 클릭했을 때만 체력을 가져왔다. 선택 이후 체력이 변경돼도 UI에는 이전 값이 남아 있었다.
이를 해결하기 위해 선택된 액터의 UHealthComponent가 제공하는 체력 변경 델리게이트를 HUD에 연결했다.
체력 바인딩 흐름
체력이 변경되면 다음 값을 업데이트했다.
| BeforeHP | 변경 전 체력 확인 및 로그 |
| CurrentHP | 현재 체력 UI 표시 |
| MaxHP | 프로그레스 바 비율 계산 |
핸들러에서는 선택 정보 구조체를 갱신한 후 패널을 다시 표시했다.
선택 대상이 바뀔 때는 이전 HealthComponent의 델리게이트를 제거하고 새로운 대상에 바인딩했다.
Tick에서 지속적으로 체력을 조회하는 방식보다 값이 변경될 때만 UI를 갱신하는 델리게이트 방식이 효율적이다.
4. 미니맵 입력 기능 분리
미니맵에서 사용하는 마우스 버튼에 따라 서로 다른 명령을 수행하도록 구현했다.
| 좌클릭 | 해당 월드 위치로 카메라 이동 | O |
| 좌클릭 드래그 | 마우스 위치를 따라 카메라 연속 이동 | O |
| 우클릭 | 선택된 지휘관에게 이동 명령 전달 | X |
초기에는 좌클릭 시 카메라가 한 번만 이동했다. 이후 마우스 버튼을 누르고 있는 동안 위치를 계속 계산하도록 수정해 드래그 이동을 구현했다.
우클릭은 카메라 위치를 변경하지 않고 기존 이동 델리게이트를 사용했다.
미니맵 좌표는 다음 과정을 통해 월드 좌표로 변환했다.
카메라 이동과 유닛 명령은 같은 미니맵 좌표를 사용하지만, 입력 목적이 다르므로 처리 경로를 분리했다.
5. ESC 설정 메뉴 구현
ESC 입력은 Enhanced Input의 IA_SFToggleSettings를 사용했다.
설정 메뉴는 열고 닫히며 입력과 포커스를 가져가는 화면이므로 CommonActivatableWidget으로 구현했다.
설정 메뉴 구성
| SettingsUIStack | 설정 메뉴를 관리하는 Common UI 스택 |
| WBP_SFSettings | 계속하기, 설정, 나가기 버튼 표시 |
| ActiveSettingsWidget | 현재 활성화된 설정 메뉴 저장 |
| ToggleSettings() | ESC 입력에 따라 열기 또는 닫기 |
| HandleSettingsDeactivated() | 메뉴가 닫힌 후 참조 정리 |
메뉴가 열린 상태에서 ESC를 다시 눌렀을 때 닫히도록 OnKeyDown을 오버라이드했다.
마우스는 설정 메뉴가 열려도 게임 화면 밖으로 빠져나가지 않도록 다음 입력 설정을 유지했다.
6. 계속하기와 로비 나가기
설정 메뉴의 계속하기와 나가기 버튼은 USFSettingsWidget에서 관리했다.
버튼 기능
| 계속하기 | 현재 설정 위젯 비활성화 |
| 설정 | 다른 담당자의 설정 WBP가 준비되면 연결 예정 |
| 나가기 | USFGameInstance::MoveToLobby() 호출 |
계속하기는 현재 게임 상태를 유지한 채 메뉴만 닫는다.
나가기는 WBP_Lobby를 직접 생성하지 않고 로비 위젯을 사용하는 로비 레벨로 이동한다.
레벨이 전환되면서 현재 게임 월드에 존재하던 액터와 HUD가 정리된다. GameInstance는 유지되므로 공통 데이터와 레벨 전환 기능을 계속 사용할 수 있다.
7. 설정 메뉴 버튼이 한 번만 동작하는 문제
처음 설정 메뉴를 열었을 때는 계속하기와 나가기 버튼이 동작했지만, 메뉴를 다시 열거나 로비에서 스테이지에 재입장하면 버튼이 반응하지 않았다.
트러블슈팅
| 증상 | 첫 번째 설정 메뉴에서는 버튼 동작, 두 번째부터 반응 없음 |
| 기존 바인딩 | NativeOnInitialized()에서 버튼 바인딩 |
| 기존 해제 | NativeDestruct()에서 버튼 바인딩 제거 |
| 원인 | Common UI가 위젯을 재사용하면 초기화 함수가 다시 호출되지 않을 수 있음 |
| 해결 | 바인딩 시점을 NativeConstruct()로 변경 |
| 결과 | 설정 메뉴를 반복해서 열어도 버튼 정상 동작 |
수정한 생명주기 구조는 다음과 같다.
Common UI는 위젯을 단순히 생성하고 제거하는 것이 아니라 활성화, 비활성화, 재사용할 수 있다. 따라서 바인딩과 해제 시점을 대응되는 생명주기 함수에 배치해야 한다.
8. RTS 커스텀 마우스 커서 적용
게임 건물에서 사용하는 재질과 색상을 참고해 커스텀 마우스 커서를 제작했다.
| 비스트 건물 | 상아 뿔과 짙은 석재 |
| 에테르 건물 | 청록 수정 |
| 공통 장식 | 황동 테두리 |
| 전체 스타일 | 귀여운 로우폴리 미니어처 |
커서는 PNG 텍스처를 사용하는 Software Cursor 방식으로 적용했다.
커서 적용 구조
텍스처에는 다음 설정을 사용했다.
| Compression Settings | UserInterface2D (RGBA) |
| Texture Group | UI |
| Mip Gen Settings | NoMipmaps |
| Filter | Bilinear |
9. 커서 외형과 실제 클릭 위치가 다른 문제
커서 외형은 정상적으로 변경됐지만 실제 클릭은 화살표 끝이 아니라 커서 위젯 중앙에서 발생했다.
트러블슈팅
| 증상 | 커서 끝과 실제 클릭 위치가 일치하지 않음 |
| 실제 클릭 위치 | Software Cursor 위젯의 중앙 |
| 이미지 시작 위치 | 기존 위젯의 좌측 상단 |
| 원인 | 클릭 기준점과 화살표 끝의 위치가 서로 다름 |
| 해결 | 커서 이미지의 좌측 상단을 루트 위젯 중앙에 배치 |
수정한 위젯 구조는 다음과 같다.
위젯 중앙과 이미지 시작점을 (48, 48)로 일치시켰다.
커스텀 커서는 이미지 교체만으로 끝나지 않는다. 실제 클릭 좌표가 커서 끝부분과 일치하는지 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 정렬이 중요하다 |