클라이언트 - 서버 연동 작업 중에 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

+ Recent posts