Unreal Engine is an awesome piece of technology making it easy to do almost anything you might want.
When using the Top Down view however, there is a hurdle to get over when trying to get multiplayer to work. This is a C++ project solution to this problem based on a BluePrints solution.
The basic problem stems from the fact that
“SimpleMoveToLocation was never intended to be used in a network environment. It’s simple after all 😉 Currently there’s no dedicated engine way of making player pawn follow a path. ” (from the same page)
To be able to get a working version of SimpleMoveToLocation, we need to do the following:
- Create a proxy player class (BP_WarriorProxy is BP solution)
- Set the proxy class as the default player controller class
- Move the camera to the proxy (Since the actual player class is on the server, we can’t put a camera on it to display on the client)
The BP solution talks about four classes – our counterparts are as follows:
- BP_WarriorProxy: ADemoPlayerProxy
- BP_WarriorController: ADemoPlayerController (Auto-created when creating a c++ top down project)
- BP_Warrior: ADemoCharacter (Auto-created when creating a C++ top down project)
- BP_WarriorAI: AAIController – we have no reason to subclass it.
So, from a standard c++ top down project, the only class we need to add is the ADemoPlayerProxy – so go ahead and do that first.
The first thing we’ll do is rewire the ADemoGameMode class to initialise the proxy class instead of the default MyCharacter Blueprint.
ADemoGameMode::ADemoGameMode(const class FPostConstructInitializeProperties& PCIP) : Super(PCIP)
{
// use our custom PlayerController class
PlayerControllerClass = ADemoPlayerController::StaticClass();
// set default pawn class to our Blueprinted character
/* static ConstructorHelpers::FClassFinder<apawn> PlayerPawnBPClass(TEXT("/Game/Blueprints/MyCharacter"));
if (PlayerPawnBPClass.Class != NULL)
{
DefaultPawnClass = PlayerPawnBPClass.Class;
}*/
DefaultPawnClass = ADemoPlayerProxy::StaticClass(); }
Our Player Proxy class declaration
/* This class will work as a proxy on the client - tracking the movements of the
* real Character on the server side and sending back controls. */
UCLASS() class Demo_API ADemoPlayerProxy : public APawn
{
GENERATED_UCLASS_BODY()
/** Top down camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera) TSubobjectPtr<class ucameracomponent=""> TopDownCameraComponent;
/** Camera boom positioning the camera above the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera) TSubobjectPtr<class uspringarmcomponent=""> CameraBoom;
// Needed so we can pick up the class in the constructor and spawn it elsewhere
TSubclassOf<aactor> CharacterClass;
// Pointer to the actual character. We replicate it so we know its location for the camera on the client
UPROPERTY(Replicated) ADemoCharacter* Character;
// The AI Controller we will use to auto-navigate the player
AAIController* PlayerAI;
// We spawn the real player character and other such elements here
virtual void BeginPlay() override;
// Use do keep this actor in sync with the real one
void Tick(float DeltaTime);
// Used by the controller to get moving to work
void MoveToLocation(const ADemoPlayerController* controller, const FVector& vector);
};
and the definition:
#include "Demo.h"
#include "DemoCharacter.h"
#include "AIController.h"
#include "DemoPlayerProxy.h"
#include "UnrealNetwork.h"
ADemoPlayerProxy::ADemoPlayerProxy(const class FPostConstructInitializeProperties& PCIP)
: Super(PCIP)
{
// Don't rotate character to camera direction
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// It seems that without a RootComponent, we can't place the Actual Character easily
TSubobjectPtr<UCapsuleComponent> TouchCapsule = PCIP.CreateDefaultSubobject<ucapsulecomponent>(this, TEXT("dummy"));
TouchCapsule->InitCapsuleSize(1.0f, 1.0f);
TouchCapsule->SetCollisionEnabled(ECollisionEnabled::NoCollision);
TouchCapsule->SetCollisionResponseToAllChannels(ECR_Ignore);
RootComponent = TouchCapsule;
// Create a camera boom...
CameraBoom = PCIP.CreateDefaultSubobject<USpringArmComponent>(this, TEXT("CameraBoom"));
CameraBoom->AttachTo(RootComponent);
CameraBoom->bAbsoluteRotation = true; // Don't want arm to rotate when character does
CameraBoom->TargetArmLength = 800.f;
CameraBoom->RelativeRotation = FRotator(-60.f, 0.f, 0.f);
CameraBoom->bDoCollisionTest = false; // Don't want to pull camera in when it collides with level
// Create a camera...
TopDownCameraComponent = PCIP.CreateDefaultSubobject<UCameraComponent>(this, TEXT("TopDownCamera"));
TopDownCameraComponent->AttachTo(CameraBoom, USpringArmComponent::SocketName);
TopDownCameraComponent->bUseControllerViewRotation = false; // Camera does not rotate relative to arm
if (Role == ROLE_Authority)
{
static ConstructorHelpers::FObjectFinder<UClass> PlayerPawnBPClass(TEXT("/Game/Blueprints/MyCharacter.MyCharacter_C"));
CharacterClass = PlayerPawnBPClass.Object;
}
}
void ADemoPlayerProxy::BeginPlay()
{
Super::BeginPlay();
if (Role == ROLE_Authority)
{
// Get current location of the Player Proxy
FVector Location = GetActorLocation();
FRotator Rotation = GetActorRotation();
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = Instigator;
SpawnParams.bNoCollisionFail = true;
// Spawn the actual player character at the same location as the Proxy
Character = Cast<ADemoCharacter>(GetWorld()->SpawnActor(CharacterClass, &Location, &Rotation, SpawnParams));
// We use the PlayerAI to control the Player Character for Navigation
PlayerAI = GetWorld()->SpawnActor<AAIController>(GetActorLocation(), GetActorRotation());
PlayerAI->Possess(Character);
}
}
void ADemoPlayerProxy::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (Character)
{
// Keep the Proxy in sync with the real character
FTransform CharTransform = Character->GetTransform();
FTransform MyTransform = GetTransform();
FTransform Transform;
Transform.LerpTranslationScale3D(CharTransform, MyTransform, ScalarRegister(0.5f));
SetActorTransform(Transform);
}
}
void ADemoPlayerProxy::MoveToLocation(const ADemoPlayerController* controller, const FVector& DestLocation)
{
/** Looks easy - doesn't it.
* What this does is to engage the AI to pathfind.
* The AI will then "route" the character correctly.
* The Proxy (and with it the camera), on each tick, moves to the location of the real character
*
* And thus, we get the illusion of moving with the Player Character
*/
PlayerAI->MoveToLocation(DestLocation);
}
void ADemoPlayerProxy::GetLifetimeReplicatedProps(TArray< class FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// Replicate to Everyone
DOREPLIFETIME(ADemoPlayerProxy, Character);
}</ucapsulecomponent>
We’ll now cover changes to the Player Controller. We’ll rewire it here to ask the proxy to move, which will in turn ask the AIController to find a path and move the real player character.
This involves changing the SetMoveDestination method to call a server side method to move the character. When the character moves, the player Proxy will automatically mirror the movements.
In the header file, add the following function
/** Navigate player to the given world location (Server Version) */
UFUNCTION(reliable, server, WithValidation)
void ServerSetNewMoveDestination(const FVector DestLocation);
Now let’s implement it (DemoPlayerController.cpp):
bool ADemoPlayerController::ServerSetNewMoveDestination_Validate(const FVector DestLocation)
{
return true;
}
/* Actual implementation of the ServerSetMoveDestination method */
void ADemoPlayerController::ServerSetNewMoveDestination_Implementation(const FVector DestLocation)
{
ADemoPlayerProxy* Pawn = Cast<ademoplayerproxy>(GetPawn());
if (Pawn)
{
UNavigationSystem* const NaDemoys = GetWorld()->GetNavigationSystem();
float const Distance = FVector::Dist(DestLocation, Pawn->GetActorLocation());
// We need to issue move command only if far enough in order for walk animation to play correctly
if (NaDemoys && (Distance > 120.0f))
{
//NaDemoys->SimpleMoveToLocation(this, DestLocation);
Pawn->MoveToLocation(this, DestLocation);
}
}
}
And finally, the rewiring of the original method:
void ADemoPlayerController::SetNewMoveDestination(const FVector DestLocation)
{
ServerSetNewMoveDestination(DestLocation);
}
Finally, in terms of the character class, the only change is really to remove the camera components that we moved to the Player Proxy which I shall leave to you :-p