In the previous post we’ve created a character creation screen and we’re able to log into the game.
Now we will aim to make the map a shared space, i.e. you can have multiple players connected and see each other move etc.
A lot of this post is based on the content from this video, which was made using C# and I ported over to Java for a starting point.
I already covered some of this before in posts 1-3 and post 3 contains some of what we’re going to implement specifically in this project.
Here’s a rundown of what’s covered in this post as well as in the next post which covers how to synchronize jump.
So let’s get straight into it.
Setting up connected character
First of all, open up your character blueprint and confirm its parent class
In most cases it should just be Character
.
What you will want to do is click Tools -> New C++ class
If your parent class was Character
, let’s put that here too, otherwise browse it in the All Classes
as necessary.
I will name this class ConnectedCharacter
and hit Create class
. You can name it as you like, but you may need to make modifications in code provided later to adjust for it.
Adding C++ code to support the motion
Ok now that you’ve created the ConnectedCharacter
class, you will want to populate it in C++.
There’s two files that you will need to work with: ConnectedCharacter.h
and ConnectedCharacter.cpp
Before we go into the code, let’s quickly discuss: What does the code do?
We’re going to have this be a parent class of any character blueprint in the UE game.
We will have the blueprints fetch the character position, rotation, velocity and if is falling and insert it into the functions we create here.
These functions will then take that data and feed it into our server to update.
We will also fetch all nearby players and return that data back to the client. With this extra data, we will render more characters around, so we can essentially interact with them.
ConnectedCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "MyUserAccountWidget.h"
#include "Runtime/Online/HTTP/Public/HttpModule.h"
#include "ConnectedCharacter.generated.h"
USTRUCT(BlueprintType)
struct FProxyCharacter
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
FString PlayerName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
FVector PlayerLocation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
FRotator PlayerRotation;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
FVector PlayerVelocity;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
bool PlayerFalling;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
FAccountCharacterBase CharacterBase;
};
struct FMotion
{
FVector Location;
FRotator Rotation;
FVector Velocity;
bool isFalling;
};
UCLASS()
class MMO_PROJECT_API AConnectedCharacter : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AConnectedCharacter();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
UClass* ProxyCharacterClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MMO")
TMap<FString, FProxyCharacter> ProxyCharacters;
UFUNCTION(BlueprintCallable, Category = "MMO")
void UpdateLocationOnServer();
void OnUpdateLocationOnServerResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
UFUNCTION(BlueprintImplementableEvent, Category = "MMO")
void NotifyNewCharacter(const FProxyCharacter& ProxyCharacter);
UFUNCTION(BlueprintImplementableEvent, Category = "MMO")
void NotifyUpdateCharacter(const FProxyCharacter& ProxyCharacter);
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UFUNCTION(BlueprintCallable, Category = "MMO")
void SetApiAccessToken(FString accessToken);
UFUNCTION(BlueprintCallable, Category = "MMO")
void SetAccountCharacterBase(FAccountCharacterBase CharacterBase);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
FString PathToAPI;
private:
FHttpModule* Http;
FString AccessToken;
FAccountCharacterBase CurrentCharacter;
};
If you’ve named the class the same as me, the only change you will need to make to above, is the project name. So find and replace the string MMO_PROJECT_API
with your project name as necessary.
ConnectedCharacter.cpp
This class holds the implementation details.
#include "ConnectedCharacter.h"
#include "Runtime/Online/HTTP/Public/Http.h"
#include "Runtime/Engine/Classes/GameFramework/PlayerController.h"
#include "Runtime/Engine/Classes/GameFramework/PlayerState.h"
#include "Runtime/Engine/Classes/GameFramework/CharacterMovementComponent.h"
#include "Runtime/Online/HTTP/Public/HttpModule.h"
// Sets default values
AConnectedCharacter::AConnectedCharacter()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
// configure your base API path
PathToAPI = "http://localhost:8081/";
}
// Called when the game starts or when spawned
void AConnectedCharacter::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AConnectedCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void AConnectedCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
void AConnectedCharacter::SetApiAccessToken(FString accessToken)
{
this->AccessToken = accessToken;
}
void AConnectedCharacter::SetAccountCharacterBase(FAccountCharacterBase CharacterBase)
{
this->CurrentCharacter = CharacterBase;
}
void AConnectedCharacter::UpdateLocationOnServer()
{
APlayerController* PC = Cast<APlayerController>(this->Controller);
if (PC && PC->PlayerState)
{
//FString playerName = PC->PlayerState->GetPlayerName();
FString playerName = this->CurrentCharacter.Name;
Http = &FHttpModule::Get();
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->OnProcessRequestComplete().BindUObject(this, &AConnectedCharacter::OnUpdateLocationOnServerResponseReceived);
// Set your input params - this is the PlayerMotion DTO in Java
TSharedPtr<FJsonObject> params = MakeShareable(new FJsonObject);
params->SetStringField(TEXT("playerName"), playerName);
TSharedPtr<FJsonObject> inputMotion = MakeShareable(new FJsonObject);
inputMotion->SetNumberField(TEXT("x"), GetActorLocation().X);
inputMotion->SetNumberField(TEXT("y"), GetActorLocation().Y);
inputMotion->SetNumberField(TEXT("z"), GetActorLocation().Z);
inputMotion->SetNumberField(FString(TEXT("roll")), GetActorRotation().Roll);
inputMotion->SetNumberField(FString(TEXT("pitch")), GetActorRotation().Pitch);
inputMotion->SetNumberField(FString(TEXT("yaw")), GetActorRotation().Yaw);
inputMotion->SetNumberField(FString(TEXT("vx")), GetCharacterMovement()->GetLastUpdateVelocity().X);
inputMotion->SetNumberField(FString(TEXT("vy")), GetCharacterMovement()->GetLastUpdateVelocity().Y);
inputMotion->SetNumberField(FString(TEXT("vz")), GetCharacterMovement()->GetLastUpdateVelocity().Z);
inputMotion->SetBoolField(FString(TEXT("isFalling")), GetCharacterMovement()->IsFalling());
params->SetObjectField(FString(TEXT("motion")), inputMotion);
FString paramsString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriter = TJsonWriterFactory<>::Create(¶msString);
FJsonSerializer::Serialize(params.ToSharedRef(), JsonWriter);
FString url = PathToAPI + FString(TEXT("player/update-motion"));
Request->SetURL(url);
Request->SetVerb("POST");
Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
Request->SetHeader("Content-Type", TEXT("application/json"));
Request->SetHeader("Accept", TEXT("application/json"));
if (AccessToken != "")
{
FString TokenString = FString(TEXT("Bearer ") + AccessToken);
Request->SetHeader("Authorization", TokenString);
}
Request->SetContentAsString(paramsString);
Request->ProcessRequest();
}
}
void AConnectedCharacter::OnUpdateLocationOnServerResponseReceived(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (bWasSuccessful)
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());
if (FJsonSerializer::Deserialize(Reader, JsonObject))
{
if (JsonObject->HasField("accountCharacters"))
{
TArray<TSharedPtr<FJsonValue>> Rows = JsonObject->GetArrayField("accountCharacters");
for (int i = 0; i < Rows.Num(); i++)
{
FProxyCharacter tempProxyCharacter;
TSharedPtr<FJsonObject> tempRow = Rows[i]->AsObject();
tempProxyCharacter.PlayerName = tempRow->GetStringField("playerName");
TSharedPtr<FJsonObject> motion = tempRow->GetObjectField("motion");
tempProxyCharacter.PlayerLocation.X = motion->GetNumberField("x");
tempProxyCharacter.PlayerLocation.Y = motion->GetNumberField("y");
tempProxyCharacter.PlayerLocation.Z = motion->GetNumberField("z");
tempProxyCharacter.PlayerRotation.Pitch = motion->GetNumberField("pitch");
tempProxyCharacter.PlayerRotation.Roll = motion->GetNumberField("roll");
tempProxyCharacter.PlayerRotation.Yaw = motion->GetNumberField("yaw");
tempProxyCharacter.PlayerVelocity.X = motion->GetNumberField("vx");
tempProxyCharacter.PlayerVelocity.Y = motion->GetNumberField("vy");
tempProxyCharacter.PlayerVelocity.Z = motion->GetNumberField("vz");
tempProxyCharacter.PlayerFalling = motion->GetBoolField("isFalling");
// Set Character Base information:
FAccountCharacterBase BaseCharacter;
// TSharedPtr<FJsonObject> tempRow = Rows[i]->AsObject();
BaseCharacter.AccountName = tempRow->GetStringField("accountName");
BaseCharacter.Name = tempRow->GetStringField("name");
BaseCharacter.Xp = tempRow->GetNumberField("xp");
TMap<FString, FString> appearanceInfo;
TSharedPtr<FJsonObject> appearanceItems = tempRow->GetObjectField("appearanceInfo");
// Iterate over Json Values
for (auto currJsonValue = appearanceItems->Values.CreateConstIterator(); currJsonValue; ++currJsonValue)
{
appearanceInfo.Add(currJsonValue->Key, appearanceItems->GetStringField(currJsonValue->Key));
}
BaseCharacter.AppearanceInfo = appearanceInfo;
tempProxyCharacter.CharacterBase = BaseCharacter;
if (ProxyCharacters.Contains(tempProxyCharacter.PlayerName)) // Update
{
ProxyCharacters[tempProxyCharacter.PlayerName].PlayerLocation = tempProxyCharacter.PlayerLocation;
ProxyCharacters[tempProxyCharacter.PlayerName].PlayerRotation = tempProxyCharacter.PlayerRotation;
ProxyCharacters[tempProxyCharacter.PlayerName].PlayerVelocity = tempProxyCharacter.PlayerVelocity;
ProxyCharacters[tempProxyCharacter.PlayerName].PlayerFalling = tempProxyCharacter.PlayerFalling;
NotifyUpdateCharacter(tempProxyCharacter);
}
else // Add
{
ProxyCharacters.Add(tempProxyCharacter.PlayerName, tempProxyCharacter);
NotifyNewCharacter(tempProxyCharacter);
}
}
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("OnUpdateLocationOnServerResponseReceived Server received no data"));
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("OnUpdateLocationOnServerResponseReceived Error accessing server"));
}
}
This class takes in the motion inputs, position, rotation, velocity and whether is falling and sending that information for storage to the server. It also receives back a list of character motions – essentially other players that are on the map. We can use this data to draw up other meshes around the player as required.
We expose several functions that can be called from our blueprint classes:
- Set API Access Token
- Set Account Character Base
- Event Notify New Character
- Event Notify Update Character
Set API Access token does as it says – we link the access token from login for our API authentication.
Set Account character base – this will link the logged in character into this class so when we do updates, we do it on correct character.
Event notify – both of these functions will be used to spawn/update the proxy characters around the player.
Linking C++ to Blueprints
Preparing Proxy character class
What is a proxy character? This is basically the class that we’re going to use which represents other players. It’s essentially like pawn class but is very similar to the character class that you’re playing (just that you’re not controlling it, its controlled from server).
Since it’s very similar to our StylisedThirdPerson
blueprint, we will copy it over to make the ProxyCharacter
class, but before that, let’s refactor one function out which will make things a little easier down the road.
Locate the function LoadCharacterFromSettings
in the blueprint that we created earlier.
The highlighted block can be refactored out – it’s simply setting local variables based on AppearanceInfo
contents.
So let’s create a new function, I’ll call it SyncAppearanceInfo
and let’s just copy that out and put into new function:
I don’t show all the function as I didn’t modify it – it’s the same as before, but now will be easier to call it.
So now if we go back to LoadCharacterFromSettings
we will see a much easier to read function:
This will allow us to sync appearance information based on appearance info data – this will help when synchronizing data runtime from server.
Now that we made this function, let’s make a copy of the StylizedThirdPerson
blueprint and make our ProxyCharacter
.
One quick thing is that few things can be removed from the ProxyCharacter
blueprint. Namely anything to do with main character being in control.
For example, any Gamepad input
or Mouse input
or Movement input
functionality etc – they can all be safely deleted from ProxyCharacter
.
Let’s add some smoothness – this is by no means enough for production-ready game, but without it game would look way too choppy.
Remember that our server will provide updates about the proxy character – so if there’s lag you may find the character ‘teleporting’ around.
To make it a bit better, you add a level of ‘prediction’ to it, i.e. basically the velocity that the character has should be added to the position every delta seconds (based on framerates etc).
You’d probably want to dampen this effect but let’s just add it directly for now.
This is now prepared enough for what we need so let’s get other blueprints connected.
Login widgets
From the login screen, we’re going to need to pass additional information to the player world. Namely is the character name (as we only passed appearance info before).
Remember, in order to add these as global variables, we need to add it to the GameInstanceSettings
blueprint. So let’s open that up and add the CharacterBase
variable.
Make sure that the eye is open (public) too.
Open up CharacterSelectionWidget
and here we will need to populate this variable when the user logs in.
Now that we’ve populated it, we could in fact remove the setting of appearance info as its included in the character base – we can do this kind of cleanup a little later though after everything is working as expected.
StylisedThirdPerson – Connected character BP
This is the main part of the work. Here we need to link the class to the C++ code that we created. This allows us to update the server with this players motion information and receive information about other players’ motion.
So first thing you want to do is click on File -> Reparent Blueprint
and select the ConnectedCharacter
that we created with C++.
Once this is done you should see Parent class: ConnectedCharacter
on the top right.
Let’s keep the next blueprints neatly separated, so let’s create a new Graph for this work:
Open up the newly created Connected
graph and let’s start adding some blueprints, first we will add the Event Notify New Character
.
This event is called from our C++ code. It notifies when there is a user nearby to your character and provides their appearance info + motion information.
With this information, we want to spawn a ProxyCharacter
class that we created earlier.
Once we spawn this ProxyCharacter
, we add it to a map
of characters
variable. So to do that, create a variable like this:
This provides easy access using character name to the character object.
So after setting the appearance info, we also call the newly created function SyncAppearanceInfo
and UpdateMesh
which will sync up the characters appearance info and update the mesh as expected.
The next function that we will add is the Event Notify Update Character
.
So after we created the characters, we will get events notifying their updates in motion.
The notify event provides information about their name, motion and appearance info (inside Character Base).
For now, we will just sync up their motion. First we load the reference to the character with the NearbyCharacters
variable, then we set the actor location and rotation, as well as getting the character movement and setting the velocity. There’s actually a small bug here relating to characters isFalling
state which I will investigate later – but let’s proceed with these motion settings for now.
Wiring it all up
We added the AccountCharacterBase
into the GameInstanceSettings
class earlier – so we want to pull this information in StylisedThirdPerson
when we LoadCharacterFromSettings
.
Let’s open up LoadCharacterFromSettings
and load this as a local variable now.
Before we used to only load in appearance information directly. Now we can pull the AccountCharacterBase
and get the AppearanceInfo
out of it.
Now we have all the data to connect things together so go to the main EventGraph
and at the end of EventBeginPlay
let’s add few things:
First we want to set the API Access token which we can fetch from Game Instance Settings object. We then Set Account Character Base. So note that these set the variables inside the C++ classes themselves.
We add a small delay (not really required) and set a timed event. This event will be called every 0.05 seconds
so its very frequent – and make sure that looping is enabled.
Inside this event we simply call the Update Location On Server
function from the C++ class.
Remember – if you don’t see these blueprint functions popping up, its probably because the class was not reparented correctly to ConnectedCharacter
class.
That’s it, you’re ready to start testing!
Note that it will still look a little choppy and stuff – check the YouTube video with results.
To make it better you’d need to add some custom smoothing