isaac.ac

UE5 custom multi-user events

I couldn't find any information anywhere about simple communication between clients in a UE5 multi-user session, however after some digging I did discover a mostly undocumented event system in the multi-user code. I thought it was worth sharing the basics here.

This post will cover how to send and receive events between specific clients in an Unreal Engine multi-user session. It assumes general C++, Unreal, and multi-user editing knowledge.

To illustrate the event system, I will build an editor plugin to "ping" other users in the multi-user session. Source code for the final plugin is available here.

Note: Multi-user used to be called Concert, and Concert is still used for naming in most of Epic's code. Throughout this post they're used interchangeably.


Create an event

Firstly there needs to be a custom event type. As I want to send messages, mine looks like this:

USTRUCT()
struct FMultiUserPingEvent
{
	GENERATED_BODY()

	UPROPERTY()
	FString Message;

	UPROPERTY()
	FString Sender;
};

Message will be the message body, and Sender will be the display name of the client that sent the message.

Your event should be a USTRUCT, and any properties you want to send should be UPROPERTYs.

Next you will need a place to create, listen for, and act on the events. For simplicity I will be doing this in my example plugins module, however you can do it wherever you like.


Declare functions

Add two new functions:

void OnConcertSessionStartup(TSharedRef<IConcertClientSession> ConcertClientSession);

void HandlePingEvent(const FConcertSessionContext&, const FMultiUserPingEvent& Event);

and a property:

FDelegateHandle StartupHandle;

HandlePingEvent will be called by the multi-user event code, and thus it's signature is important - you should substitute FMultiUserPingEvent for your event type here.


Implement functions

Now in the C++ file, you will want to bind the OnConcertSessionStartup function you just added to the actual startup of the Concert session. I'm doing this in my module's StartupModule, but you can do it anywhere that feels appropriate. (You may also wish to use a different delegate Add depending on your class).

void FMultiUserPingModule::StartupModule()
{
	if (TSharedPtr<IConcertSyncClient> ConcertSyncClient = IConcertSyncClientModule::Get().GetClient(TEXT("MultiUser")))
	{
		const IConcertClientRef ConcertClient = ConcertSyncClient->GetConcertClient();
		StartupHandle = ConcertClient->OnSessionStartup().AddRaw(this, &FMultiUserPingModule::OnConcertSessionStartup);

		if (const TSharedPtr<IConcertClientSession> Session = ConcertClient->GetCurrentSession())
		{
			OnConcertSessionStartup(Session.ToSharedRef());
		}
	}
}

And you'll want to make sure to unbind towards the end of your objects lifetime:

void FMultiUserPingModule::ShutdownModule()
{
	if (TSharedPtr<IConcertSyncClient> ConcertSyncClient = IConcertSyncClientModule::Get().GetClient(TEXT("MultiUser")))
	{
		ConcertSyncClient->GetConcertClient()->OnSessionStartup().Remove(StartupHandle);
	}
}

Now this is out of the way, you can register a function to handle the custom events sent by other clients (or, tangentially, this client, because thats possible too!). Do this in the function bound to the Concert session startup:

void FMultiUserPingModule::OnConcertSessionStartup(TSharedRef<IConcertClientSession> ConcertClientSession)
{
	if (TSharedPtr<IConcertClientSession> PinnedSession = ConcertClientSession.ToSharedPtr())
	{
		ConcertClientSession->RegisterCustomEventHandler<FMultiUserPingEvent>(
			this, &FMultiUserPingModule::HandlePingEvent);
	}
}

The key function here is RegisterCustomEventHandler. It's templated, and from what I can tell this is the only way to distinguish between custom events - i.e. it isn't possible to send different events that use same type to different places.


Handle events

In my case the function to handle events is very simple. I just want to add a notification to the editor that received the event:

void FMultiUserPingModule::HandlePingEvent(const FConcertSessionContext&, const FMultiUserPingEvent& Event)
{
	FNotificationInfo Info(FText::FromString(Event.Message));
	Info.SubText = FText::FromString(Event.Sender);
	FSlateNotificationManager::Get().AddNotification(Info);
}

Send events

Now that the event handling is done, we can move onto creating and sending the events.

In my case I want to add a new console command called muping that sends messages using the format muping username message. To do this I've used FAutoConsoleCommandWithWorldAndArgs with a lambda that will run when the command is executed (yes, this would be much simpler with a UFUNCTION(Exec), however I didn't use a UObject for this example):

static FAutoConsoleCommandWithWorldAndArgs GMultiUserPing(
	TEXT("muping"),
	TEXT("Send messages to other clients in a multi-user session"),
	FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld*)
	{ ... }));

You can put your code for sending events wherever you like.

I'm going to skip my code to parse out the username and message from the command arguments, leaving the relevant part of the lambda body:

for (const FConcertSessionClientInfo& Client : Session->GetSessionClients())
{
	if (Client.ClientInfo.DisplayName.Equals(Username, ESearchCase::IgnoreCase))
	{
		FMultiUserPingEvent Event;
		Event.Message = Message;
		Event.Sender = Session->GetLocalClientInfo().DisplayName;

		UE_LOG(LogMultiUserPing, Verbose, TEXT("sending message %s to %s"),
			*Event.Message, *Client.ClientEndpointId.ToString());

		Session->SendCustomEvent(Event, Client.ClientEndpointId, EConcertMessageFlags::None);

		return;
	}
}

I'm iterating over the available clients, until I find the one with the DisplayName I want. You can filter the available clients by endpoint GUID, username, machine name, etc. When I have the client I want, I create my custom event type and populate the relevant fields. I use GetLocalClientInfo() to get the display name of the sender - this function will return the client info of the machine running the code.

Finally, I send the message using SendCustomEvent(). This function takes any USTRUCT, an endpoint (or array of endpoints), flags, and an optional sequence. For the endpoint, supply the GUID(s) of client(s) you want to send the event to.

That's it!

I can now ping other users in a multi-user session from the UE command line.

image

image