클라이언트 - 서버 연동 작업 중에 Delegate를 한 번에 관리하는 방법을 동기가 쓰고 계시길래 신기해서 배우게 되었다.
클라이언트 / 서버 간의 다음과 같은 메세지를 전달해야하고, 서버로 부터의 응답은 FPacketBase라는 구조체를 받는다.
Client Send | Server Anser |
CS_Ping() CS_Connect() CS_Login(const int UserId) CS_GetAdress(const int UserId) CS_JoinRoom(const int UserId, const int RoomId) |
SA_Pong(const FPacketBase& PacketObj) SA_Connect(const FPacketBase& PacketObj) SA_Login(const FPacketBase& PacketObj) SA_GetAdress(const FPacketBase& PacketObj) SA_JoinRoom(const FPacketBase& PacketObj) SA_JoinUser(const FPacketBase& PacketObj) SA_LeaveUser(const FPacketBase& PacketObj) |
서버에 전송할 패킷을 만들어 전송한다. 대부분 UserId가 포함되어있는 작은 패킷 FCQ_Login FCQ_RoomAddress FCQ_JoinRoom 등.. |
각각 패킷은 이러한 형식으로 함수 안에서 다운캐스트하여 받는다. FSA_Login FSA_RoomAddress FSA_UserJoin FSA_UserLeave |
빌드 일정에 좀 쫒겨서 클라에서 사용할 함수를 아래와 같이 Delegate를 남발하면서 initialize에서 바인딩 해주고, 해당 패킷이 온 함수에 Broadcast 하여 함수를 실행하였다.
// .h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnGetRoomAddressSuccessDelegate, const FSA_GetRoomAddress&);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnGetRoomAddressFailedDelegate, const FSA_GetRoomAddress&);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnJoinRoomSuccessDelegate, const FSA_JoinRoom&);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnJoinRoomFailedDelegate, const FSA_JoinRoom&);
FOnWSGetRoomAddressSuccessDelegate OnGetRoomAddressSuccessDelegate;
FOnWSGetRoomAddressFailedDelegate OnGetRoomAddressFailedDelegate;
FOnWSJoinRoomSuccessDelegate OnJoinRoomSuccessDelegate;
FOnWSJoinRoomFailedDelegate OnJoinRoomFailedDelegate;
// .cpp
// 클라이언트 Send
void UWorldServerClient::CQ_GetRoomAddress(const int32 UserId, const int32 RoomId)
{
FCQ_GetRoomAddress CqGetRoomAddr;
CqGetRoomAddr.SetUserId(UserId);
CqGetRoomAddr.SetRoomId(RoomId);
SendMessage(CqGetRoomAddr);
}
void UWorldServerClient::CQ_JoinRoom(const int32 UserId, const int32 RoomId)
{
FCQ_JoinRoom Packet = FCQ_JoinRoom();
Packet.SetUserId(UserId);
Packet.SetRoomId(RoomId);
SendMessage(Packet);
}
// 서버 Answer
void UWorldServerClient::SA_JoinRoom(const FPacketBase& PacketObj)
{
const FSA_JoinRoom& Packet = static_cast<const FSA_JoinRoom&>(PacketObj);
(처리 로직 생략)...
OnJoinRoomSuccessDelegate.Broadcast(Packet);
}
void UWorldServerClient::SA_GetRoomAddress(const FPacketBase& PacketObj)
{
const FSA_GetRoomAddress& Packet = static_cast<const FSA_GetRoomAddress&>(PacketObj);
(처리 로직 생략)...
OnGetRoomAddressSuccessDelegate.Broadcast(Packet);
}
그렇지만 패킷을 처리할 함수가 10개정도를 바인딩 하고 나서야 너무 Delegate가 남발된다는 생각을 하게되었고
같은 패킷에 대한 Delegate는 Array에 담아서 하나의 Subsystem에서 관리하도록 다음과 같이 선언하고
// .h
DECLARE_DELEGATE_OneParam(FOnReceivedPacketEvent, const FPacketBase&);
TMap<EPacketType, TArray<FOnReceivedPacketEvent>> BindedDelegateMap;
// 서버에서 응답이 왔을 때 바인딩된 Delegate 실행
void UWorldServerClient::SA_GetCharacterSlots(const FPacketBase& PacketObj)
{
ExecuteBindedFunction(EPacketType::WS_CQ_GetCharacterSlots, PacketObj);
}
Delegate TMap에 대한 함수는 다음과 같이 바인딩 / 언바인딩 / 실행 한다.
// .cpp
void UWorldServerClient::BindFunction(const EPacketType& PackageType, const FOnReceivedPacketEvent& Delegate)
{
if (!BindedDelegateMap.Contains(PackageType))
BindedDelegateMap.Add(PackageType);
BindedDelegateMap[PackageType].Add(Delegate);
}
void UWorldServerClient::UnBindFunction(const EPacketType& PackageType, const FDelegateHandle& Handle)
{
if (BindedDelegateMap.Contains(PackageType))
{
for (int Index = 0; Index < BindedDelegateMap[PackageType].Num(); ++Index)
{
if (BindedDelegateMap[PackageType][Index].GetHandle() == Handle)
{
BindedDelegateMap[PackageType].RemoveAt(Index);
}
}
}
}
void UWorldServerClient::ExecuteBindedFunction(const EPacketType& PackageType, const FPacketBase& PacketObj)
{
if (BindedDelegateMap.Contains(PackageType))
{
for (const FOnReceivedPacketEvent& Delegate : BindedDelegateMap[PackageType])
{
Delegate.Execute(PacketObj);
}
}
}
Delegate를 Multicast로 안하였는데, 순서대로 TArray에 차곡차곡 쌓은다음에 Array를 순회하여 실행을 하게 되면
이렇게하면 순서가 보장되지 않는 Delegate 특성을 무시하고 순서대로 실행할 수 있다.
또한 Delegate가 있는지 확인해 주어야하는 것과, 이 Delegate를 바인딩 한 곳에서 소멸될 때 언바인딩 해주어야 오류가 나지 않을 것 이다. (확인필요)
아래의 예제에서는 Delegate 완료 시, 언바인딩 하였는데 만약 소멸자가 불릴 때 Delegate가 바인딩 되어있으면 언바인딩 할 수 있도록 해야 안전할것이다.
// .h
FOnReceivedPacketEvent OnGetCharacterSlots;
// .cpp
void UCharacterStateComponent::~CharacterStateComponent()
{
WorldServerClient->UnBindFunction(EPacketType::CQ_GetCharacterSlots, OnGetCharacterSlots.GetHandle());
}
void UCharacterStateComponent::InitializeCharacterData()
{
(생략...)
OnGetCharacterSlots = FOnReceivedPacketEvent::CreateUObject(this, &CharacterStateComponent::OnGetAvatarParts);
WorldServerClient->BindFunction(EPacketType::CQ_GetCharacterSlots, OnGetCharacterSlots);
WorldServerClient->CQ_GetCharacterSlots(WorldConnectSubsystem->GetConnectUserId());
}
void UCharacterStateComponent::OnGetAvatarParts(const FPacketBase& PacketObj)
{
const FSA_GetCharacterSlots& Packet = static_cast<const FSA_GetCharacterSlots&>(PacketObj);
(생략...)
WorldServerClient->UnBindFunction(EPacketType::CQ_GetCharacterSlots, OnGetCharacterSlots.GetHandle());
}
기존에 Delegate를 여러개 사용하여 바인딩 / 언바인딩 하는 것 보다는 for문을 순회하는 면에서 성능은 조금 떨어질 지라도 개발자가 읽기 편하고, 한 곳에 관리할 수 있다는 점에서 훨신 편하다.
'언리얼 > 언리얼 기능' 카테고리의 다른 글
언리얼 보간 (Interpolation) (1) | 2023.10.19 |
---|---|
UStruct static_cast (0) | 2023.09.04 |
Unreal Editor Mode (0) | 2023.07.15 |
FMath / SpringArm / Camera (0) | 2023.07.08 |
UE5 PSO 캐시 적용 (0) | 2023.02.19 |