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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  | 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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  | /* 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<UCameraComponent> TopDownCameraComponent;
        /* Camera boom positioning the camera above the character */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
    TSubobjectPtr<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:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
  | #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);
}
  | 
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
1
2
3
  | /*_ 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:
1
2
3
4
  | 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