Deeper Learning

[UE5] 메신저 핵심 기능 개발 완료 (ProjectVT 개발기) 본문

Game Development/Devlog

[UE5] 메신저 핵심 기능 개발 완료 (ProjectVT 개발기)

Dlaiml 2024. 7. 23. 11:53

이전 포스팅에서 메신저 기능을 위해 WBP와 클래스 구조를 설계하고 기본적인 레이아웃을 개발한 것까지 소개하였다.  

 

[UE5] 메신저 기능 데이터 구조 설계, Widget Reflector (ProjectVT 개발기)

출시 이전 크라우드 펀딩에 공개할 데모를 만들기 위해 주요 기능을 우선으로 개발하고 있는데, 그 중 하나가 이전 포스팅에서 언리얼 엔진 WBP 기능에 대해 설명하면서 예시로 든 메신저 형태의

dlaiml.tistory.com

 

현재는 핵심 피쳐 개발을 모두 마쳤기 때문에 이번 포스팅에서는 지난 포스팅 이후 추가로 개발한 것들, 겪었던 문제들을 개발기 형태로 간단하게 작성하려 한다.

 

채팅탭, 채팅방 목록

 

채팅방 목록은 모든 메신저에 존재하는 필수 기능으로 채팅 상대이름, 프로필, 마지막 채팅 시각, 마지막 채팅 내용 등의 정보를 포함하고 있다.

 

메신저 기능 구조는 현재 프로필 클래스의 멤버로 채팅창 객체가 있는 형태이고 채팅창은 Scroll Box 위젯 컴포넌트에 채팅 객체(좌측 채팅, 우측 채팅)를 Child로 가지고 있다.

 

채팅방 또한 채팅창에서의 상대방 프로필과 동일한 대상을 참조하도록 Owner를 프로필 클래스의 객체로 지정하였고 마찬가지로 프로필 클래스도 멤버로 채팅방 객체를 갖도록 하였다. 

 

  • FriendProfile (User Widget, 프로필)
    • Profile Image (Texture 2D)
    • Profile Name (Text)
    • ChattingBox (User Widget, 채팅창)
      • ScrollBox
        • LeftChat (User Widget, 상대방 채팅)
        • RightChat (User Widget, 플레이어 채팅)
    • ChattingTab (User Widget, 채팅요약)
      • ScrollBox 
        • ChattingRoom (User Widget, 채팅방 객체)

이번 포스팅에서 설명할 주요 클래스들의 구조를 요약하면 위와 같다.

메신저 메인 클래스에서는 FriendProfile을 배열 행태로 가지고 있어 이를 순회하며 친구탭, 채팅탭에 Widget을 배치한다.

 

 

채팅탭에는 위 사진처럼 채팅을 나눴던 친구와의 대화 정보들이 기록되어 있어야 한다.

채팅탭 객체는 FriendProfile에 의존하고 있기 때문에 Get Parent로 쉽게 FriendProfile에 접근이 가능하다. 현재 대화하고 있는 상대방의 이름, 프로필 사진도 가져올 수 있으며 Scroll Box의 Child를 탐색하여 마지막 대화 내용과 시각도 가져올 수 있다.

 

가져온 정보들을 아래와 같은 모양의 ChattingRoom(User Widget)에 채워 넣고 이를 채팅탭의 Scroll Box에 Child로 추가하면 친구마다 채팅방을 하나씩 만들 수 있게 된다.

 

각 ChattingRoom은 OnClick 이벤트에 프로필 사진을 클릭하였을 때 대화창이 열리도록 구현하기 위해 선언한 OnFriendProfileClickedDelegate를 호출하도록 하여 마찬가지로 클릭한 Profile에 알맞은 대화창이 열리도록 구현하였다.

 

이렇게 이전 포스팅까지 친구 프로필 / 친구 목록 / 대화창 / 채팅방 목록 등 메신저 기능을 위한 기초 클래스에 대해 모두 다루었다.

 

이후 구현한 기능들은 간단하게 각 기능의 구현 방식, 겪었던 어려움에 대해서만 설명하려 한다. 

 

기능 구현

 

단체 대화방

메신저 기능 기획 전에 결정된 기능이지만 중요도, 우선도에서 토의가 진행중이어서 일단 배제하고 전체적인 구조 설계를 마쳤는데 이후 추가된 기능이다. FriendProfile이 곧 대화하는 상대방이고, 대화방의 주인이며, 친구 1명인 모두 1대 1 대응 관계인 현재 구조에서 바로 도입이 어려운 기능이었다. 

 

메시지를 보내는 대상의 이름은 DataTable의 Sender라는 필드에 들어있어서 이를 활용해서 플레이어의 채팅과, 상대방의 채팅을 구분하고 있었는데, 추가로 채팅하는 상대방의 프로필을 가져오는데 Sender 필드를 사용하였다.

 

메신저 기능은 주요 챕터 사이에 들어가는 독립적인 레벨에 포함된 스펙으로 내부 클래스들도 모두 의도적으로 메인 게임의 클래스들과 의존하지 않도록 설계하였다. (게임에 NPC3이 나온다고 메신저 기능에 NPC3이 자동으로 추가되지 않음, DataTable을 기준으로 친구, 대화가 진행)

 

채팅을 보내는 사람의 이름이 "친구1"이면 "친구 1"을 Key값으로 FriendProfile 객체를 탐색하여 가져와 프로필 사진, 친구 이름 등을 설정하였다. FriendProfile 객체를 그대로 가져왔기 때문에 프로필 클릭, 채팅방 목록등 다른 기능도 모두 잘 작동한다.

 

다음으로 해결할 문제는 채팅창이 친구하나와 연관되어 있다는 것이다. 채팅창에 연결된 Profile을 배열로 관리하게 되면 FriendProfile의 멤버로 하나의 채팅방, 채팅창 설계를 뒤집어야 되는 불편함이 있다.

 

앞서 말한것처럼 메인 게임과 독립적인 시스템이기 때문에 친구의 이름을 Delimiter(@)로 묶고 DataTable에 저장하였다.

친구목록에 단체 대화방은 나타나지 않기 때문에 Delimiter를 포함하고 있는지 확인 후 Visibility를 조정하는 방식으로 친구목록에서 임의로 만든 친구 프로필객체가 나타나지 않도록 하였다.

 

대화창에서 나오는 친구 이름은 따로 Field를 만들어 대체할 수 있도록 처리하였다.

 

읽지 않은 메시지 알림

 

아직 확인하지 않은 메시지나 메신저를 실행하기 전 전송받은 메시지를 표현하기 위한 메시지 알림 기능이다. 자연스럽게 처음 메신저 기능을 접한 플레이어가 대화를 확인할 수 있도록 추가한 기능으로 마찬가지로 시작부터 스펙에 명확히 포함되어 있지 않던 기능이라서 구현에 어려움이 있었다.

 

우선 클릭 이벤트를 트리거로 (좌측/우측) 채팅 객체를 생성하고 이를 Scroll Box에 Child로 추가하는 형태로 대화를 주고받는 기능을 구현하였는데, 추가로 미리 클릭하기 전 생성되어야 하는 채팅 객체가 생긴 것이다.

 

메신저 실행 전 메시지 생성을 위해 DataTable에 Column을 추가하여 실행 전 로드가 필요한 메시지인지 표시하였고

채팅창의 NativeConstruct 시점에 아래처럼 블루프린트로 구현한 Preload 함수를 호출하여 미리 채팅 Scroll Box에 채팅 들이 추가될 수 있도록 하였다.

 

 

확인하지 않은 메시지의 숫자를 표시하기 위해 채팅방의 정보를 대화가 진행될 때마다 바로 업데이트하기 위해 만든 Delegate인 Chat Progress Delegates에 Bind 한 Set Chat Room Data 함수에서 1씩 메시지 카운트를 증가시켰다.

 

 

Unread Message Counter가 0이면 읽지 않은 메시지 알림 Overlay가 사라지도록 블루프린트에서 Visibility 프로퍼티에 아래 함수를 바인드 해두었다.

 

void UChatRoom::NativeConstruct()
{
    Super::NativeConstruct();

   // ...
   // Bind at BP
   // ...
}

void UChatRoom::UpdateVisibilityByCount(ESlateVisibility NewVisibility)
{
   if (UnreadMessageCounter >= 1)
   {
       UnreadMessageCounterOverlay->SetVisibility(ESlateVisibility::Visible); 
   }
}

 

타이머로 인해 클릭에 대한 피드백이 없어 상대방의 채팅이 입력중이라는 안내를 추가하였다.

 

 

채팅창을 확인한 후에는 읽지 않은 메시지 카운트를 0으로 바꾼다. 앞서 설명한 Profile Click Delagete에 메신저 메인 클래스의 함수를 Bind 하였고 메인 클래스는 모든 채팅방에 접근이 가능하기 때문에 클릭으로 인해 채팅창이 Active 되고 나서 현재 Widget Switcher의 Active Widget을 확인하고 해당 채팅창에 연결된 프로필에 접근하고 채팅방의 카운트를 0으로 만드는 방식으로 구현하였다.

 

UI에 변화가 있는 부분은 해당 UI에 연결된 UserWidget에서 처리하도록 하였으며, 위처럼 전체 Widget을 필요로 하거나 위젯 생성, 소멸과 관련된 코드는 메인 클래스에 작성하였다.

 

채팅 타이머

더 실감나는 메신저를 만들기 위해 상대방의 입력을 1초 정도 지연시키는 기능을 추가 구현 하였다.

Delay를 사용하여 지연시켰고 그동안 플레이어의 입력을 막기 위해 입력이 가능한지 여부를 결정하는 Bool 값을 하나 추가하였다.

 

 

 

플레이어가 인지할 수 있도록 아래에는 입력중이라는 메시지와 애니메이션을 추가하였다.

상대방이 입력도중 플레이어가 다른 채팅방으로 이동하게 되면 읽지 않은 메시지로 처리가 되어야 한다. 이를 처리하기 위해 현재 Widget Switcher에서 Active 되고 있는 Widget이 무엇인지 확인하도록 하였다.

 

UWidgetSwitcher는 Index로도 Active Widget에 접근할 수 있고 Widget을 Getter로 가져오는 것도 가능하다.

int32 UWidgetSwitcher::GetActiveWidgetIndex() const
{
	if ( MyWidgetSwitcher.IsValid() )
	{
		return MyWidgetSwitcher->GetActiveWidgetIndex();
	}
PRAGMA_DISABLE_DEPRECATION_WARNINGS
	return ActiveWidgetIndex;
PRAGMA_ENABLE_DEPRECATION_WARNINGS
}

UWidget* UWidgetSwitcher::GetActiveWidget()const
{
	return GetWidgetAtIndex(GetActiveWidgetIndex());
}

 

GetActiveWidget으로 가져온 채팅창 위젯과 현재 대화가 진행중인 채팅창 위젯이 동일하면 읽지 않은 메시지 카운트를 증가시키지 않도록 하였다.

 

사용자가 보고있지 않은 채팅창에서 입력 중이던 채팅이 진행되면 읽지 않은 메시지로 표시가 된다.

 

 

채팅 선택 기능

플레이어가 전송할 채팅을 직접 선택할 수 있도록 하는 기능이다. 마찬가지로 DataTable에 선택지인지 여부와 선택할 채팅의 텍스트, 선택하면 전송할 텍스트를 입력할 필드를 추가하였다. 

 

선택지가 나온 상황에서 채팅이 진행되지 않도록 입력 가능 여부 Bool 멤버 변수를 추가하고 Mouse Down, Up 함수를 Override 한 채팅 진행함수에서 입력 가능 여부를 확인 후 채팅을 넘기도록 하였다.

 

DataTable에서 채팅 데이터를 가져오는 Chat Index를 하나씩 증가시키면서 대화가 진행되는데 선택지의 경우 대신 특정 Index로 뛰어넘도록 하여 마치 게임북과 같은 방식으로 선택지 기능을 구현하였다.

 

 

[UE5] UI Design - Widget Blueprint를 Color Palette로 활용하기 (ProjectVT 개발기)

Widget Blueprint 내부의 여러 다양한 Component에 사용할 여러 색상들을 웹 디자인 할 때처럼 언리얼 엔진에서도 하나의 객체(또는 전역변수)로 관리할 필요성이 느껴졌다. 제작하고 있는 게임의 UI 시

dlaiml.tistory.com

UI가 아직 확정되지 않아 이를 완료하고, 여러 테스트를 마치면 데모 버전에 사용할 수 있을 정도로 완성되었다.

 

 

Comments