Devlog 6 - Basic Chat
So to keep the burnout away I decided to take a mini break today, and add a simple chat service for XYZ glhf me!
Ramblings of what I tried first and failed (feel free to skip)
My thought process was the less things the dedicated game server does the better, so why give it the lowly task of being a chat server. At first I had the comically bad idea to just make a flask webserver that the clients could make POST and GET requests for sending and retrieving messages. The issue is was that I was having to store all the sent messages, and I was having to constantly send GET requests for the stored messages on the server. This was just bad, so bad I won't even show the code :)
Then I remembered about SocketIO which easily let's you communicate between clients and a server, and it even has the added bonus of rooms which would make it easy to only send to clients that are matched with each other. I even found https://github.com/getnamo/SocketIOClient-Unreal a library made to communicate to socket io servers. Sadly after a few hours of troubleshooting my clients just did not want to connect to the SocketIO server. I'm still not sure if it was the ue library, or some server configuration. And so enough stalling I figured that we can just do all this on WebSockets, and UE5 conveniently enough has a library to communicate with WebSockets.
WebSocket Server Implementation
Nothing too fancy here we create a node.js app that we send messages through WebSockets we will pass in a LobbyId
and a MessageContent
and with a Map of <LobbyId, Clients>
we can keep track of what clients we should send the messages to.
When a message gets sent to the server we check if the client is in the lobby if not we add them to the lobby. We then propagate the messages to all clients in a lobby.
let lobbies = new Map();
wss.on('connection', ws => {
ws.on('message', message => {
let payload;
try {
payload = JSON.parse(message);
} catch (e) {
console.error('Invalid message received:', message);
return;
}
let clients = lobbies.get(payload.LobbyId) || [];
// If the client isn't in the lobby yet, add them
if (!clients.includes(ws)) {
clients.push(ws);
lobbies.set(payload.LobbyId, clients);
}
//Send each client in a lobby the message
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(payload.MessageContent);
console.log(`Message sent to clients in room ${payload.LobbyId}: ${payload.MessageContent}`);
}
});
console.log(`Message sent in room ${payload.LobbyId}: ${payload.MessageContent}`);
});
//Close connection code below...
I then pushed the app on gcloud App Engine and like magic our chat service is up and running!
Websockets Unreal Client Implementation
We need our client to do the following things.
- Connect to the chat service via WebSockets
- Join a lobby
- Receive Messages
- Send Messages
So let's create some sort of manager class that deals with the chat XYZChatManager
creative...
When connecting we can bind functions most importantly OnMessage so we can handle receiving chat messages
void ConnectToChatServer()
{
WebSocket = FWebSocketsModule::Get().CreateWebSocket(CHAT_WSS);
WebSocket->OnConnected().AddUObject(this, &UXYZChatManager::HandleWebSocketConnected);
WebSocket->OnConnectionError().AddUObject(this, &UXYZChatManager::HandleWebSocketConnectionError);
WebSocket->OnClosed().AddUObject(this, &UXYZChatManager::HandleWebSocketClosed);
WebSocket->OnMessage().AddUObject(this, &UXYZChatManager::HandleWebSocketMessageReceived);
if (WebSocket.IsValid())
{
WebSocket->Connect();
}
}
For sending messages we can pack our message and lobby id into a struct and send it over as a json string. Our lobby id is currently just a Guid generated by the dedicated server and replicated to our clients, for now it seems good enough to not cause duplicate lobbies.
void UXYZChatManager::SendMessage(FString LobbyId, FString PlayerMessage)
{
if (WebSocket.IsValid() && WebSocket->IsConnected())
{
FChatMessage ChatMessage;
ChatMessage.LobbyId = LobbyId;
ChatMessage.MessageContent = PlayerMessage;
WebSocket->Send(ChatMessage.ToJson());
}
}
To join a lobby we can just send a blank message once we are connected.
void UXYZChatManager::HandleWebSocketConnected()
{
UE_LOG(LogTemp, Warning, TEXT("WebSocket Connected!"));
SendMessage(GameState->ChatLobbyId, "");
}
We already binded our function to receive messages, and for now we can just append each message to a string that will eventually be used by a widget.
void UXYZChatManager::HandleWebSocketMessageReceived(const FString& Message)
{
UE_LOG(LogTemp, Warning, TEXT("Received WebSocket Message: %s"), *Message);
if(!Message.IsEmpty())
{
LobbyMessages += "\n" + Message;
OnReceivedChat.Broadcast(LobbyMessages);
}
}
Displaying the chat
ok now for the part that i suck at UI. I figured we can have a widget that has this hierarchy
VerticalBox->{ScrollBox->Text, InputTextBox}
We'll set the ScrollBoxText to LobbyMessages
with a custom event, and call SendMessage
once we press enter on our InputTextBox.
And the result is...
It's a blurry gif, but it's working as intended!
And for a last touch let's make it so the name is a different color based on what team you're on.
We'll make use of RichText and create a new DataTable where <Team_1>
will be blue and <Team2>
will be red.
We'll need to format our messages so that player's are tagged correctly. For now we'll just hard code 0 blue 1 red
FString UXYZChatManager::GetRichChatMessage(FString Message)
{
TArray<FString> ParsedMessage;
Message.ParseIntoArray(ParsedMessage, TEXT(":"))'
FString PlayerName = ParsedMessage[0].TrimEnd();
FString ChatContent = ParsedMessage[1].TrimStart();
for (int i = 0; i < GameState->UsernamesByTeamId.Num(); i++)
{
if (GameState->UsernamesByTeamId[i].Equals(PlayerName))
{
switch (i)
{
case 0:
PlayerName = FString::Printf(TEXT("<Team_1>%s</>"), *PlayerName);
break;
case 1:
PlayerName = FString::Printf(TEXT("<Team_2>%s</>"), *PlayerName);
break;
}
break;
}
}
FString RichMessage = FString::Printf(TEXT("%s: %s"), *PlayerName, *ChatContent);
After applying the new rich text to the chat box we get
Improvements to for the future
- Clear out inactive chat lobbies. This will need some sort of timestamp passed in or set in the chat service.
- Deploy to multiple regions? I'm not sure about this one as it is just chat, and there's not much done in rts anyways.
- Make it more interactive for example in sc2 the chat will fade out overtime.
- Pressing enter lets you start typing.
- Movable
- Settings to toggle it on it off
So yeah there's a bunch of small improvements that can be made, but for the moment a working chat is a nice accomplishment for the day!
Thank you for reading email me at jandro@xyzrts.com for any questions or if you're looking for a passionate game developer :)