Simple C++ Chat System

A lightweight simple chat system with Slate widgets and server RPCs.

Updated 5 months ago Edit Page Revisions

Overview

I made a lightweight simple chat system with Slate widgets and server RPCs.

The way it works is a widget is created then attached in the HUD class. The user presses enter to focus the inputbox and when he submits the message is passed to the player state then is replicated on the server and then to all players with a multicast server call.

You will have to enable slate and extend 4 classes.

  • MyGameMode extends AGameMode
  • MyHUD extends AHUD
  • MyPlayerState extends APlayerState
  • MyChatWidget extends SCompoundWidget

Getting Started

First thing you have to do is enable the Slate module.
It can be done with this tutorial Slate,_Hello. To enable slate you need to open your PROJECT.Build.cs in your source/PROJECT folder and uncomment the line:

PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

Adding Classes

Now you need to extend the 4 classes and add the code. You may already have these classes extended so you may have to add this code to your existing classes.
To extend a class go to Tools > New C++ Class...
The first 3 classes are extending main classes so you can find them in the main search area.
The Last class MyChatWidget extends SCompoundWidget add you will have to click the Show All Classes button and type in SCompoundWidget.

Once you have all the classes extended you need to add the code. You will have to rename the include files to your project name and possible some other class names if you chose different ones.

Note you will also have to change the Project name in each .h file to your project name. (ex CHATTUTORIAL_API to MYGAME_API)

The AGameMode class with all classes enabled. InheritAGameMode.png

The SCompoundWidget class in Editor. InheritSCompoundWidget.png

MyGameMode.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/GameMode.h"
#include "MyGameMode.generated.h"

UCLASS()
class CHATTUTORIAL_API AMyGameMode : public AGameMode
{
    GENERATED_BODY()
    
public:
    AMyGameMode();
    
};

MyGameMode.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "ChatTutorial.h"
#include "MyGameMode.h"
#include "MyHUD.h"
#include "MyPlayerState.h"

AMyGameMode::AMyGameMode()
{
    // assign our custom classes above their parents
    HUDClass = AMyHUD::StaticClass();
    PlayerStateClass = AMyPlayerState::StaticClass();

    /* use this is you wish to extend the c++ into a bp and assign the bp to the class
    static ConstructorHelpers::FClassFinder hudclassobj(TEXT("Blueprint'/MyHUD.MyHUD_C'"));
    if (hudclassobj.Class != NULL)
        HUDClass = hudclassobj.Class;

    static ConstructorHelpers::FClassFinder psclassobj(TEXT("Blueprint'/MyPlayerState.MyPlayerState_C'"));
    if (psclassobj.Class != NULL)
        PlayerStateClass = psclassobj.Class;
    */
}

MyHUD.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/HUD.h"
#include "MyHUD.generated.h"

USTRUCT()
struct FSChatMsg // Struct to hold the message data to be passed between classes
{
    GENERATED_BODY()

    UPROPERTY() // UProperty means this variable will be replicated
    int32 Type;

    UPROPERTY()
    FText Username;

    UPROPERTY()
    FText Text;

    FText Timestamp; // Dont replicate time because we can set it locally once we receive the struct

    double Created;

    void Init(int32 NewType, FText NewUsername, FText NewText) // Assign only the vars we wish to replicate
    {
        Type = NewType;
        Username = NewUsername;
        Text = NewText;
    }
    void SetTime(FText NewTimestamp, double NewCreated)
    {
        Timestamp = NewTimestamp;
        Created = NewCreated;
    }
    void Destroy()
    {
        Type = NULL;
        Username.GetEmpty();
        Text.GetEmpty();
        Timestamp.GetEmpty();
        Created = NULL;
    }
};


UCLASS()
class CHATTUTORIAL_API AMyHUD : public AHUD
{
    GENERATED_BODY()

public:

    AMyHUD();

    TSharedPtr MyUIWidget; // Reference to the main chat widget

    APlayerController* MyPC;

    UFUNCTION(BlueprintCallable, Category = "User")
    void AddMessageBP(const int32 Type, const FString& Username, const FString& Text, const bool Replicate); // A Blueprint function you can use to place messages in the chat box during runtime

protected:
    
    virtual void PostInitializeComponents() override; // All game elements are created, add our chat box

    virtual void DrawHUD() override; // The HUD is drawn on our screen
};

MyHUD.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "ChatTutorial.h"
#include "MyHUD.h"
#include "MyChatWidget.h"
#include "MyPlayerState.h"

AMyHUD::AMyHUD()
{

}

void AMyHUD::PostInitializeComponents()
{
    Super::PostInitializeComponents();

    if (GEngine && GEngine->GameViewport) // make sure our screen is ready for the widget
    {
        SAssignNew(MyUIWidget, SMyChatWidget).OwnerHUD(this); // add the widget and assign it to the var
        GEngine->GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(MyUIWidget.ToSharedRef()));
    }
}

void AMyHUD::DrawHUD()
{
    Super::DrawHUD();

    if (!MyPC)
    {
        MyPC = GetOwningPlayerController();
        AddMessageBP(2, TEXT(""), TEXT("Welcome. Press Enter to chat."), false); // random Welcome message shown to the local player. To be deleted. note type 2 is system message and username is blank
        return;
    }

    if (MyPC->WasInputKeyJustPressed(EKeys::Enter))
        if (MyUIWidget.IsValid() && MyUIWidget->ChatInput.IsValid())
            FSlateApplication::Get().SetKeyboardFocus(MyUIWidget->ChatInput); // When the user presses Enter he will focus his keypresses on the chat input bar
}

void AMyHUD::AddMessageBP(const int32 Type, const FString& Username, const FString& Text, const bool Replicate)
{
    if (!MyPC 

 !MyUIWidget.IsValid())
        return;

    FSChatMsg newmessage;
    newmessage.Init(Type, FText::FromString(Username), FText::FromString(Text)); // initialize our struct and prep the message
    if (newmessage.Type > 0)
        if (Replicate)
        {
            AMyPlayerState* MyPS = Cast(MyPC->PlayerState);
            if (MyPS)
                MyPS->UserChatRPC(newmessage); // Send the complete chat message to the PlayerState so it can be replicated then displayed
        }
        else
            MyUIWidget->AddMessage(newmessage); // Send a local message to this client only, no one else receives it
}

MyPlayerState.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/PlayerState.h"
#include "MyHUD.h"
#include "MyPlayerState.generated.h"

UCLASS()
class CHATTUTORIAL_API AMyPlayerState : public APlayerState
{
    GENERATED_BODY()
    
public:

    AMyPlayerState();

    UFUNCTION(Server, Reliable, WithValidation) // for player to player rpc you need to first call the message on the server
    virtual void UserChatRPC(const FSChatMsg& newmessage); // first rpc for the server
    UFUNCTION(NetMulticast, Reliable, WithValidation) // then the server calls the function with a multicast that executes on all clients and the server
    virtual void UserChat(const FSChatMsg& newmessage); // second rpc for all the clients
};

MyPlayerState.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "ChatTutorial.h"
#include "MyPlayerState.h"
#include "MyHUD.h"
#include "MyChatWidget.h"

AMyPlayerState::AMyPlayerState()
{

}

bool AMyPlayerState::UserChatRPC_Validate(const FSChatMsg& newmessage)
{
    return true;
}
void AMyPlayerState::UserChatRPC_Implementation(const FSChatMsg& newmessage)
{
    UserChat(newmessage);
}
bool AMyPlayerState::UserChat_Validate(const FSChatMsg& newmessage)
{
    return true;
}
void AMyPlayerState::UserChat_Implementation(const FSChatMsg& newmessage)
{
    APlayerController* MyCon;
    AMyHUD* MyHud;

    for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator) // find all controllers
    {
        MyCon = Cast(*Iterator);
        if (MyCon)
        {
            MyHud = Cast(MyCon->GetHUD());
            if (MyHud && MyHud->MyUIWidget.IsValid())
                MyHud->MyUIWidget->AddMessage(newmessage); // place the chat message on this player controller
        }
    }
}

MyChatWidget.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "MyHUD.h"
#include "SlateBasics.h"

/**
 * 
 */
class CHATTUTORIAL_API SMyChatWidget : public SCompoundWidget
{
    SLATE_BEGIN_ARGS(SMyChatWidget) : _OwnerHUD(){} // the OwnerHUD var is passed to the widget so the owner can be set.

    SLATE_ARGUMENT(TWeakObjectPtr, OwnerHUD)

    SLATE_END_ARGS()

public:

    void Construct(const FArguments& InArgs);

    TSharedRef OnGenerateRowForList(TSharedPtr Item, const TSharedRef& OwnerTable); // the function that is called for each chat element to be displayed in the chatbox
    TArray Items; // array of all the current items in this players chat box
    TSharedPtr< SListView< TSharedPtr > > ListViewWidget; // the acutall widgets for each chat element

    const FSlateFontInfo fontinfo = FSlateFontInfo(FPaths::EngineContentDir() / TEXT("UI/Fonts/Comfortaa-Regular.ttf"), 15); // Font, Font Size  for the chatbox

    TWeakObjectPtr OwnerHUD;

    TSharedPtr< SVerticalBox > ChatBox;
    TSharedPtr< SEditableText > ChatInput;

    void OnChatTextChanged(const FText&amp; InText);
    void OnChatTextCommitted(const FText&amp; InText, ETextCommit::Type CommitMethod);

    void AddMessage(const FSChatMsg&amp; newmessage); // the final stage, this function takes the input and does the final placement in the chatbox
    
    void Tick(const FGeometry&amp; AllottedGeometry, const double InCurrentTime, const float InDeltaTime); // The full widget ticks and deletes messages
};

MyChatWidget.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "ChatTutorial.h"
#include "MyChatWidget.h"
#include "MyHUD.h"
#include "MyPlayerState.h"

#define LOCTEXT_NAMESPACE "SMyChatWidget"

void SMyChatWidget::Construct(const FArguments&amp; InArgs)
{
    OwnerHUD = InArgs._OwnerHUD;

    ChildSlot // Build the base for the chatbox
        .VAlign(VAlign_Bottom)
        .HAlign(HAlign_Left)
        .Padding(15) // move the chat box out from the corner 
        [
            SNew(SVerticalBox) // outter container
            + SVerticalBox::Slot()
            .AutoHeight()
            .MaxHeight(408.f)
            .VAlign(VAlign_Bottom)
            [
                SAssignNew(ListViewWidget, SListView< TSharedPtr< FSChatMsg > >) // a ListView widget that takes the array of messages and draws them on the hud
                .ListItemsSource(&amp;Items) //The Items array is the source of this listview
                .OnGenerateRow(this, &amp;SMyChatWidget::OnGenerateRowForList) // The widget is trying to draw, give the elements
                .ScrollbarVisibility(EVisibility::Hidden)
            ]
            + SVerticalBox::Slot()
            .AutoHeight()
            .FillHeight(30.f)
            [
                SNew(SHorizontalBox)
                + SHorizontalBox::Slot()
                .AutoWidth()
                .MaxWidth(600.f)
                [
                    SAssignNew(ChatInput, SEditableText) // the widget for player input
                    .OnTextCommitted(this, &amp;SMyChatWidget::OnChatTextCommitted) // function to call when text is entered
                    .OnTextChanged(this, &amp;SMyChatWidget::OnChatTextChanged) // function to call when text is changed
                    .ClearKeyboardFocusOnCommit(true)
                    .Text(FText::FromString(""))
                    .Font(FSlateFontInfo(fontinfo.FontMaterial, fontinfo.Size + 2)) // set the font for the input and add 2 font size
                    .ColorAndOpacity(FLinearColor(1.f, 1.f, 1.f, 0.9f)) // send color and alpha R G B A
                    .HintText(FText::FromString("Send a message to everyone.")) // hint message (optional)
                ]
            ]
        ];
}

TSharedRef SMyChatWidget::OnGenerateRowForList(TSharedPtr< FSChatMsg > Item, const TSharedRef&amp; OwnerTable)
{
    if (!Items.IsValidIndex(0) 

 !Item.IsValid() 

 !Item.Get()) // Error catcher
        return
            SNew(STableRow< TSharedPtr< FSChatMsg > >, OwnerTable)
            [
                SNew(SBox)
            ];

    if (Item.Get()->Type === 1) // Type 1 is for player chat messages
    return
        SNew(STableRow< TSharedPtr< FSChatMsg > >, OwnerTable)
        [
            SNew(SWrapBox)
            .PreferredWidth(600.f)
            + SWrapBox::Slot()
            [
                SNew(STextBlock) // places the timestamp
                .Text(Item.Get()->Timestamp)
                .ColorAndOpacity(FLinearColor(0.25f, 0.25f, 0.25f, 1.f))
                .Font(fontinfo)
                .ShadowColorAndOpacity(FLinearColor::Black)
                .ShadowOffset(FIntPoint(1, 1))
            ]
            + SWrapBox::Slot()
            [
                SNew(STextBlock) // places the username
                .Text(Item.Get()->Username)
                .ColorAndOpacity(FLinearColor::White)
                .Font(fontinfo)
                .ShadowColorAndOpacity(FLinearColor::Black)
                .ShadowOffset(FIntPoint(1, 1))
            ]
            + SWrapBox::Slot()
            [
                SNew(STextBlock) // adds the : between the username and chat text
                .Text(FText::FromString(" :  "))
                .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f, 1.f))
                .Font(fontinfo)
                .ShadowColorAndOpacity(FLinearColor::Black)
                .ShadowOffset(FIntPoint(1, 1))
            ]
            + SWrapBox::Slot()
            [
                SNew(STextBlock) // places the user text
                .Text(Item.Get()->Text)
                .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f, 1.f))
                .Font(fontinfo)
                .ShadowColorAndOpacity(FLinearColor::Black)
                .ShadowOffset(FIntPoint(1, 1))
            ]
        ];
    else // 2 is for server messages, add more types for whispers friendslists etc
    return
        SNew(STableRow< TSharedPtr< FSChatMsg > >, OwnerTable)
        [
            SNew(SWrapBox)
            .PreferredWidth(600.f)
            + SWrapBox::Slot()
            [
                SNew(STextBlock)
                .Text(Item.Get()->Timestamp)
                .ColorAndOpacity(FLinearColor(0.25f, 0.25f, 0.25f, 1.f))
                .Font(fontinfo)
                .ShadowColorAndOpacity(FLinearColor::Black)
                .ShadowOffset(FIntPoint(1, 1))
            ]
            + SWrapBox::Slot()
            [
                SNew(STextBlock)
                .Text(Item.Get()->Text)
                .ColorAndOpacity(FLinearColor(0.75f, 0.75f, 0.75f, 1.f))
                .Font(fontinfo)
                .ShadowColorAndOpacity(FLinearColor::Black)
                .ShadowOffset(FIntPoint(1, 1))
            ]
        ];
}

void SMyChatWidget::OnChatTextChanged(const FText&amp; InText) // Called everytime the user presses a key on the input bar
{
    FString SText = InText.ToString();
    if (SText.Len() > 120) // if there are more that 120 characters in the char box, remove the rest
    {
        SText = SText.Left(120);
        if (ChatInput.IsValid())
            ChatInput->SetText(FText::FromString(SText));
    }
}

void SMyChatWidget::OnChatTextCommitted(const FText&amp; InText, ETextCommit::Type CommitMethod) // The chat box is submitted
{
    if (CommitMethod != ETextCommit::OnEnter) // only complete if the textbox was comitted with enter
        return;

    if (ChatInput.IsValid())
    {
        FText NFText = FText::TrimPrecedingAndTrailing(InText); // remove whitespace
        if (!NFText.IsEmpty())
        {
            AMyPlayerState* MyPS = Cast(OwnerHUD->MyPC->PlayerState); // cast to our player state that contains the rpc functions
            if (MyPS)
            {
                // Insert code here if you wish to have / commands
                FSChatMsg newmessage; // make a new struct to send for replication
                newmessage.Init(1, FText::FromString(MyPS->PlayerName), NFText); // initialize the message struct for replication
                if (newmessage.Type > 0)
                    MyPS->UserChatRPC(newmessage); // Send the complete chat message to the PlayerState so it can be replicated then displayed
            }
        }
        ChatInput->SetText(FText()); // clear the chat box now were done with it
    }

    FSlateApplication::Get().SetUserFocusToGameViewport(0, EFocusCause::SetDirectly); // set the players focus back to the gameport
}

void SMyChatWidget::AddMessage(const FSChatMsg&amp; newmessage) // this function is the last in line and does the actual placing of the message
{
    int32 index = Items.Add(MakeShareable(new FSChatMsg())); // add a new message to the chatbox array
    if (Items[index].IsValid())
    {
        Items[index]->Init(newmessage.Type, newmessage.Username, newmessage.Text); // intiate our new message with the passed message

        int32 Year, Month, Day, DayOfWeek, Hour, Minute, Second, Millisecond; // set the timestamp and decay timer
        FPlatformTime::SystemTime(Year, Month, DayOfWeek, Day, Hour, Minute, Second, Millisecond);
        Items[index]->SetTime(FText::FromString(FString::Printf(TEXT("[ %02d:%02d:%02d ] "), Hour, Minute, Second)), FPlatformTime::Seconds()); // Comment this line to remove timestamps or replace FPlatformTime::Seconds() with 0 to slow decay the messages

        ListViewWidget->RequestListRefresh(); // update the chatbox widget with our new array element
        ListViewWidget->ScrollToBottom(); // scroll the chatbox to the bottom so our new message pops up
    }
}

void SMyChatWidget::Tick(const FGeometry&amp; AllottedGeometry, const double InCurrentTime, const float InDeltaTime) // called everyframe and used for our gamelogic
{
    SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime);

    if (Items.Num()) // make sure there is atleast one element in the chatbox array
    {
        if (!Items[0]->Created) // this element doesnt have a creation time and will last forever so lets set the creation time now and it was start decaying
            Items[0]->Created = InCurrentTime;
        if (InCurrentTime - Items[0]->Created > 20) // the first message in the array is older that 20 seconds
        {
            Items[0]->Destroy(); // clear the vars and pointers
            Items.RemoveAt(0); // remove the item from the array
            Items.Shrink();
        }
    }
}

Once you compile all the classes. Make sure your game is using your new GameMode in the Project Settings Maps & Modes setting.