[UE5] 메신저 기능 데이터 구조 설계, Widget Reflector (ProjectVT 개발기)
출시 이전 크라우드 펀딩에 공개할 데모를 만들기 위해 주요 기능을 우선으로 개발하고 있는데, 그 중 하나가 이전 포스팅에서 언리얼 엔진 WBP 기능에 대해 설명하면서 예시로 든 메신저 형태의 대화 기능이다.
(해당 기능을 예시로 다룬 이전 포스팅)
메신저 기능에 쓰이는 데이터를 정리해보면 다음과 같다.
친구 목록, 친구 프로필, 각 친구별 대화 내용, 대화창에서의 친구 이름/프로필사진
그리고 현재 만들어둔 UserWidget을 크게 보면
- 좌측 친구목록, 대화목록으로 이동 가능한 Navigator가 포함된 NavigatorTab
- 중앙 친구목록(친구 프로필 사진, 친구 이름) / 대화창 목록 (최근 대화 내용, 이전 포스팅에서 미구현)이 있는 FriendListTab, ChatListTab
- 우측 대화내용, 대화상대 이름 프로필이 있는 ChatBoxTab
- 위 UserWidget을 담고 있는 MainWidget
4가지 정도가 있다.
살펴보면 공통적으로 연결되어 있는 정보들이 있는데
친구목록에 있는 친구와만 대화가 가능하여야 하고, 친구 프로필(이름+사진)은 대화창에 그대로 나타나야 한다.
대화목록에 있는 대화들도 마찬가지로 친구목록에 있는 친구들과의 대화만 저장되어야 한다.
친구 목록 탭 설계 및 구현
친구 목록 데이터는 하나만 존재해야 하고 이를 참고하여 모든 친구목록, 대화목록 등 기능이 구현되어야 앞으로 유지보수가 용이하다. (친구를 하나 추가한다고 하면 대화목록, 친구목록에서 하나하나 모두 추가하지 않고 친구 목록 데이터에만 추가)
파이썬으로 개발할 때는 dictionary를 주로 활용하거나 json, dataclass 형식의 config 파일로 구현을 하였겠지만 언리얼 엔진에서는 주로 Enum, Structure, DataTable을 사용하여 이를 다룬다.
우선 Key가 될 수 있는 필드가 무엇인지 생각해보자. 친구 이름, 프로필 사진, ID 등 여러 필드가 있지만 게임 속 NPC와의 대화이기 때문에 간단하게 친구이름을 unique한 key로 정했다.
Enumeration을 만들고 친구 이름을 입력한다.
친구 목록, 대화창에 모두 쓰이는 친구 프로필(친구 이름 + 프로필 사진)은 Structure로 만들어 관리한다. 친구 이름의 경우 위에서 만든 Enumeration을 자료형으로 지정한다.
프로필 사진의 경우 Texture2D를 참조로 받는다.
다음으로 위 Structure를 기반으로 DataTable을 만든다.
Enuemration에 넣어둔 친구 이름마다 프로필 이미지를 지정해준다.
친구 프로필 데이터를 나타낼 UserWidget인 FriendProfile 클래스를 만들었다.
위 UserWidget은 친구 탭에 노출시킬 친구 프로필이면서 친구 이름, 친구 프로필 사진을 담고 있는 Structure의 역할도 겸한다.
친구 프로필을 로드하는 간단한 로직을 블루프린트로 바로 작성하였다.
친구목록을 나열하는 기능이기 때문에 FriendTab에 구현하였고 NativeConstruct 시점에 친구 목록에 실제 친구 데이터를 채운다.
앞서 만든 DataTable의 Row를 순회하면서 FriendProfile Widget을 Create 한다. DataTable의 데이터인 프로필 이름, 프로필 사진을 Create한 Widget의 알맞은 요소에 넣어준다. 만든 프로필 위젯은 친구 목록의 레이아웃 위젯인 Horizontal Box에 Child로 추가한다.
친구목록 탭에 앞서 만든 프로필들이 세로로 차곡차곡 로드된 것을 확인할 수 있다.
대화창 설계 및 구현
이제 우측의 대화창 기능을 생각해보자.
친구목록에 있는 친구 프로필을 눌러 대화가 시작되고 해당 친구의 프로필 사진과 이름이 나타나야 한다.
그리고 친구마다 대화 내용이 다를테고 친구목록의 프로필을 누를 때 마다 그 친구와의 대화 내용으로 우측 탭이 전환되어야 한다.
우선 MainWidget에서 우측탭에 UserWidget 전환 기능을 위해 WidgetSwitcher를 추가하였고 아무 대화창이 없을 경우 표시할 이미지를 추가하였다.
대화 한 줄은 저번 포스팅에서 만든 상대방, 자신 대화 UserWidget을 사용할 예정이고 대화 내용을 담을 DataTable과 Structure의 정의가 필요하다.
위에서 만든 Enumuration(초록색)을 다시 활용하는데 대화 상대방의 경우 친구 목록에 있는 친구 중 한명이기 때문이다. 대화 내용 텍스트를 String 자료형으로 지정하였다. (나머지 필드는 선택지나 대화가 끝났는지 여부, 다음에 로드할 대화의 Index 등을 저장하기 위해 만들었다)
DataTable은 위와 같이 대화 상대방 / 메세지 전송자(Enumeration에 '나'를 추가) / 대사 / 이미지 필드를 입력하여 만들었다.
이제 지금까지 만든 데이터 구조와 클래스 간 연결 로직을 정리해보면
친구 탭에서 DataTable에 저장된 친구이름, 프로필 사진을 활용하여 FriendProfile UserWidget을 친구 별로 생성한다.
각 FriendProfile은 우측 탭인 대화창의 UserWidget을 멤버로 가지고 있다. (후에 Friend라는 Interface아래에 Profile / ChatBox 기능을 필수 구현하도록 대체하였음)
이렇게 친구목록 - 대화창 간의 연결이 완료되었고 친구 목록에서 '친구1'을 클릭하면 연결된 Delegate가 호출되어 WidgetSwitcher에서 '친구1'의 ChatBox(대화창)의 Visibility가 변경되어 대화창이 뜨게된다.
각 친구와의 대화가 모두 정의된 DataTable이 있으며 메신저 위젯 NativeConstruct 시점에 각 친구 이름에 따라 DataTable이 나뉜다. (TalkPartner가 '친구1'이면 '친구1' ChatBox의 DataTable로 지정)
대화창에서 마우스가 클릭 > 이벤트를 받은 대화창 위젯에서 DataTable에서 현재 대화 Index에 맞는 Row를 탐색 > 플레이어의 채팅이면 오른쪽채팅 UserWidget, 상대방의 채팅이면 왼쪽채팅 UserWidget을 생성 > Row의 데이터를 입력(채팅텍스트, > ChatBox의 ScrollBox에 Child로 추가한다.
이렇게 친구 목록 - 대화목록 - 친구 프로필 - 대화창 프로필 - 대화내용이 유기적으로 연결된 메신저 시스템의 기본 기능 구현을 완료하였다.
(전송 시각 표시 기능은 아직 추가하지 않았다)
Widget Reflector
언리얼 엔진 에디터의 런타임에서 한 눈에 UserWidget의 구조를 볼 수 있는 기능이다.
UserWidget의 Designer 탭에서 상단을 보면 Widget Reflector라는 버튼이 보일 것이다. (현재 사용하고 있는 엔진 버전은 5.3)
새 탭으로 Widget Reflector가 열리는데 Pick Hit-Testable Widget 버튼을 누르면 마우스커서가 가는 곳이 전부 위젯으로 분리되어 표시된다. (언리얼 엔진 에디터 자체 포함)
에디터에서 게임을 실행하고 나서도 작동하기 때문에 런타임에 Create Widget을 통해 생성되는 UserWidget을 디버깅하기 매우 편하다.
친구1의 대화창 내 프로필 사진을 누르고 Esc를 눌러 Picking을 멈춰보았다.
Widget의 전체 Hierarchy가 나오고 각 Widget의 Visibility, Focus, Clipping 어느 uasset에 있는지 까지 전부 확인할 수 있다.
Focus, Visibility의 경우 Widget이 모달창이 반복되며 중첩될 경우 신경쓰기가 매우 어려웠는데 런타임에 쉽게 확인이 가능할 수 있어 정말 유용하였다.
Filter에서 또는 우클릭후 "UMG as root" 설정을 키면 언리얼 에디터 자체에 있는 위젯 요소 전체 대신 직접 만든 User Widget이 root가 되어 훨씬 깔끔하게 볼 수 있다.