原文:
[UE4] Getting Multiplayer Player Pawn AI Navigation to work (C++)
http://droneah.com/content/ue4-getting-multiplayer-player-pawn-ai-navigation-work-c
下面方案的思路是:
每个Actor,为其定义一个代理(ActorProxy),真实的Actor放在服务端,代理ActorProxy放在客户端,移动Actor时,实际是移动服务端上的Actor,然后对客户端ActorProxy的位置进行同步。
摄像机绑定的是ActorProxy,服务端的真实Actor不用摄像机;而AIController实际控制的是服务端Actor,客户端其实没有AIController。
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); }
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