Slate, Simple C++ Chat System
Contents
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
.
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 File > 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)
-
Extending the AGameMode class in Editor.
-
Extending the SCompoundWidget class with Show All Classes enabled.
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<AMyHUD> hudclassobj(TEXT("Blueprint'/MyHUD.MyHUD_C'"));
if (hudclassobj.Class != NULL)
HUDClass = hudclassobj.Class;
static ConstructorHelpers::FClassFinder<AMyPlayerState> 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<class SMyChatWidget> 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<AMyPlayerState>(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<APlayerController>(*Iterator);
if (MyCon)
{
MyHud = Cast<AMyHUD>(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<class AMyHUD>, OwnerHUD)
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs);
TSharedRef<ITableRow> OnGenerateRowForList(TSharedPtr<FSChatMsg> Item, const TSharedRef<STableViewBase>& OwnerTable); // the function that is called for each chat element to be displayed in the chatbox
TArray<TSharedPtr<FSChatMsg>> Items; // array of all the current items in this players chat box
TSharedPtr< SListView< TSharedPtr<FSChatMsg> > > 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<class AMyHUD> OwnerHUD;
TSharedPtr< SVerticalBox > ChatBox;
TSharedPtr< SEditableText > ChatInput;
void OnChatTextChanged(const FText& InText);
void OnChatTextCommitted(const FText& InText, ETextCommit::Type CommitMethod);
void AddMessage(const FSChatMsg& newmessage); // the final stage, this function takes the input and does the final placement in the chatbox
void Tick(const FGeometry& 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& 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(&Items) //The Items array is the source of this listview
.OnGenerateRow(this, &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, &SMyChatWidget::OnChatTextCommitted) // function to call when text is entered
.OnTextChanged(this, &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<ITableRow> SMyChatWidget::OnGenerateRowForList(TSharedPtr< FSChatMsg > Item, const TSharedRef<STableViewBase>& 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& 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& 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<AMyPlayerState>(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& 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& 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.
thanks to !