Spectator pawn on client side

On a player death, I try to set the controller state to Spectating. I have made my own BP_SpectatorPawn Inherit from ASpectatorPawn and configure the game mode for use it.

In the player character death function, I call a controller function SetSpectatorState just after a DetachFromControllerPendingDestroy():


void ASPlayerController::SetSpectatorState()
{
    PlayerState->bIsSpectator = true;
    ChangeState(NAME_Spectating);
    ClientGotoState(NAME_Spectating);
}

It work fine on the server player death. But the SpectatorPawn seams to be the default SpectatorPawn on the client and it’s created at 0,0,0 instead of the current view location. The game state seams to replicate correctly the Reference to the SpectatorPawn class. I don’t understand why the client have this comportment.

I’ve found a clue. bIsSpectator is replicated after the change state to spectating.
A really dirty solution is to add a delay before change state to spectating after set bIsSpectator to true.


void ASPlayerController::SetSpectatorState()
{
    PlayerState->bIsSpectator = true;
    // Waiting IsSpectator replication before set state spectating
    FTimerHandle TimerHandle_Tmp;
    GetWorldTimerManager().SetTimer(TimerHandle_Tmp, this, &ASPlayerController::WaitingForChangeState, 3.f, false);
}

void ASPlayerController::WaitingForChangeState()
{
    ChangeState(NAME_Spectating);
    ClientGotoState(NAME_Spectating);
}

I don’t like this solution. That cause some ploblemes if the player respawn during the delay for exemple.

Another solution would be, in the OnRep_IsSpectator, the client tell the server to set him state spectating. I haven’t test it at this time. But I’m not sure the replication of bIsSpectator is the real or only cause.

I found that this issue was caused by the way a spectator pawn is spawned by APlayerController::SpawnSpectatorPawn().


                SpawnParams.ObjectFlags |= RF_Transient;    // We never want to save spectator pawns into a map
                SpawnedSpectator = World->SpawnActor<ASpectatorPawn>(SpectatorClass, GetSpawnLocation(), GetControlRotation(), SpawnParams);
                if (SpawnedSpectator)

GetSpawnLocation() may or may not be what you expect.

In our case, it meant anyone becoming a spectator spawned in a completely unrelated part of the map.
To fix this, I created a new function to send over a spectator location and rotation:



.h
    UFUNCTION(Unreliable, Client)
    void ClientSetSpectatorLocationAndRotation(FVector NewSpectatorLocation, FRotator NewSpectatorRotation);
    virtual void ClientSetSpectatorLocationAndRotation_Implementation(FVector NewSpectatorLocation, FRotator NewSpectatorRotation);

.cpp
void AXXPlayerController::ClientSetSpectatorLocationAndRotation_Implementation(FVector NewSpectatorLocation, FRotator NewSpectatorRotation)
{
    SpectatorLocation = NewSpectatorLocation;
    SpectatorRotation = NewSpectatorRotation;

    if (GetSpectatorPawn())
    {
        GetSpectatorPawn()->TeleportTo(SpectatorLocation, SpectatorRotation, false, true);

        if (PlayerCameraManager)
        {
            PlayerCameraManager->UpdateCamera(0.0f);
        }
    }
}

In case the RPC beats the spectator being spawned, you also need to override SpawnSpectatorPawn():


                SpawnParams.ObjectFlags |= RF_Transient;    // We never want to save spectator pawns into a map

                SpawnedSpectator = World->SpawnActor<ASpectatorPawn>(SpectatorClass, SpectatorLocation, SpectatorRotation, SpawnParams);

                if (SpawnedSpectator)

Its a little brute force, but it does the trick.

I used to have the same problem and fixed it using



ChangeState(NAME_Spectating);
ClientGotoState(NAME_Spectating);


I’ve analyzed engine code and I don’t think bIsSpectator makes a difference. I moved on and started working on other features and at some point the problem came back. I must have changed something at some point that stopped it from working. When I try your solution of adding a delay, it works but as you’ve pointed out, it’s not ideal.

I’ve spent a few hours debugging and it seems like the function DestroySpectatorPawn() inside PlayerController is called AFTER changing state to spectator, so it creates (when changing states) and then destroys itself. Here’s the stack trace:



    UE4Editor-Engine.dll!APlayerController::DestroySpectatorPawn() Line 4549    C++
     UE4Editor-Engine.dll!APlayerController::ChangeState(FName NewState) Line 4591    C++
     UE4Editor-Engine.dll!AController::PawnPendingDestroy(APawn * inPawn) Line 336    C++
     UE4Editor-Engine.dll!APawn::DetachFromControllerPendingDestroy() Line 863    C++
     UE4Editor-Engine.dll!APawn::Destroyed() Line 388    C++
     UE4Editor-Engine.dll!UWorld::DestroyActor(AActor * ThisActor, bool bNetForce, bool bShouldModifyLevel) Line 586    C++
     UE4Editor-Engine.dll!AActor::Destroy(bool bNetForce, bool bShouldModifyLevel) Line 3565    C++
     UE4Editor-Engine.dll!UActorChannel::CleanUp(const bool bForDestroy) Line 1770    C++
     UE4Editor-Engine.dll!UChannel::ConditionalCleanUp(const bool bForDestroy) Line 135    C++
     UE4Editor-Engine.dll!UChannel::ReceivedSequencedBunch(FInBunch & Bunch) Line 313    C++
     UE4Editor-Engine.dll!UChannel::ReceivedNextBunch(FInBunch & Bunch, bool & bOutSkipAck) Line 667    C++
     UE4Editor-Engine.dll!UChannel::ReceivedRawBunch(FInBunch & Bunch, bool & bOutSkipAck) Line 388    C++
     UE4Editor-Engine.dll!UNetConnection::ReceivedPacket(FBitReader & Reader) Line 1540    C++
     UE4Editor-Engine.dll!UNetConnection::ReceivedRawPacket(void * InData, int Count) Line 933    C++
     UE4Editor-OnlineSubsystemUtils.dll!UIpNetDriver::TickDispatch(float DeltaTime) Line 237    C++
     UE4Editor-Engine.dll!TBaseUObjectMethodDelegateInstance<0,UNetDriver,void __cdecl(float)>::ExecuteIfSafe(float <Params_0>) Line 659    C++
     UE4Editor-Engine.dll!TBaseMulticastDelegate<void,float>::Broadcast(float <Params_0>) Line 937    C++
     UE4Editor-Engine.dll!UWorld::Tick(ELevelTick TickType, float DeltaSeconds) Line 1317    C++
     UE4Editor-UnrealEd.dll!UEditorEngine::Tick(float DeltaSeconds, bool bIdleMode) Line 1693    C++
     UE4Editor-UnrealEd.dll!UUnrealEdEngine::Tick(float DeltaSeconds, bool bIdleMode) Line 401    C++
     UE4Editor.exe!FEngineLoop::Tick() Line 3339    C++
     UE4Editor.exe!GuardedMain(const wchar_t * CmdLine, HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, int nCmdShow) Line 166    C++
     UE4Editor.exe!GuardedMainWrapper(const wchar_t * CmdLine, HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, int nCmdShow) Line 144    C++
     UE4Editor.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow) Line 223    C++
     [External Code]  


I’ve tried the solution posted above and unfortunately it doesn’t change anything for me as the spectator is not even spawning to begin with (or maybe it is but it’s being immediately destroyed for some reason)

@Kris Thank for for your trick, that will help me to make a better patch. But my main problem is not the location and rotation, it’s seems to be be the default spectator pawn instead of my custom spectator pawn wich spawn. I has added a spring arm and a camera on my custom spectator pawn and I lock all rotations and position on Z axis. Without the delay, The spectator movements are free, with the delay I have my contraintes and the right camera.

@Moshy Indeed, bIsSpectator make no difference. I print a message on begin play and on end play of the Spectator Pawn and the end play message is not diplayed until the player respawn. The end play message should be called at the moment of the destroy no? This messages think to me the spectator pawn is created but not correctly setted in the client side controller. Or It’s created too late, and the default spectator pawn if affected instead of the custom spectator pawn. I will try to explore this ways.

I found something interesting last night but didn’t have time to explore.

I switch to spectator when my main pawn dies. So in the Pawn’s class I have a function called on the server to kill the player. If I didn’t destroy the pawn, it would switch to spectator fine.

In code it looked something like this:



Server_KillPlayer()
{
    PC = GetPlayerController
    PC->ChangeToSpectator()

    this->Destroy()
}


So as I was saying in my last post, maybe my spectator was spawning but being destroyed because of this->Destroy() I don’t understand why calling Destroy() in the Pawn class would also destroy the spectator or if this information is of any use to you but I think the main point here is, we might need to look elsewhere in our code for the cause.

[USER=“534601”]Le Schmurtz[/USER] Have you confirmed that your spectator class is being replicated/received by the player controller correctly?

See AGameStateBase::ReceivedGameModeClass() & APlayerController::ReceivedSpectatorClass().

@Moshy When I remove the DetachFromControllerPendingDestroy() and SetLifeSpan from my character, there is no difference. I don’t destroy the pawn as fast as you.

@Kris The spectator class is correctly replicate. I’ve tracking the spectator creation with some logs in functions ReceivedSpectatorClass, SetSpectatorPawn, SpawnSpectatorPawn and DestroySpectatorPawn. At the player death here is my logs:

Everything seems to be okay. Perhaps my spectator pawn is not correctly configured for the client.

I’ll try to reproduce that problem in a more simple projet, from a template with just the player death and the spectator pawn spawning. It’s probably help me to undestand where is my error.

Not sure whats going on then :frowning:
Please let us know what you find.
And good luck! :slight_smile:

I’m back with news.

My problem seems to be much less inexplicable than at first.

In fact, I had two problems in the same time.

The first is a fake problem, because my Spectator Pawn reacts correctly in the client after packaging the game. The constraintes doesn’t work only in the PIE window of the client player. I will survive to that.

However, the view is not setted on my Spectator Pawn Camera. It Stay at location zero and rotation zero. I have implement your trick Kris, the location is updated but not the rotation. My rotation have a value of -60,0,0 (or 300,0,0) but the view stay irremediably horizontal. Perhaps the constraints lock the possible rotations. But in this case there is no reason it work for the server.

I have to clean my code and blueprints before continue to search a solution to that little problem ^^.

I’ve also fixed mine but I don’t really understand why.

I moved the spectator change after the pawn’s destroy like this:



Server_KillPlayer()
{      
     this->Destroy();
     PC = GetPlayerController    
     PC->ChangeToSpectator()
}


This makes no sense because in the past I’ve tried to call functions AFTER calling this->Destroy() and it just broke so I thought once you call Destroy() you can’t call anymore functions. Also, I don’t know why it used to work because I didn’t change that part in my code.

Can confirm that code works for pitch & yaw.



            FVector NewSpectatorLocation;
            FRotator NewSpectatorRotation;

            Player->GetActorEyesViewPoint(NewSpectatorLocation, NewSpectatorRotation);

            // Ensure roll is cleared.
            NewSpectatorRotation.Roll = 0.0f;
..................
                // If we have a character that is still alive, make it not so.
                if (GBCharacter->IsAlive())
                {
                    GBCharacter->Suicide();
                }
                else if (Player->IsFirstPerson())
                {
                    NewSpectatorRotation = FRotator(-30.0f, NewSpectatorRotation.Yaw, 0.0f);
                    NewSpectatorLocation += (NewSpectatorRotation.Vector() * -100.0f);
                }
            }

            Player->ClientSetSpectatorLocationAndRotation(NewSpectatorLocation, NewSpectatorRotation);


Still no idea whats going on your end ><

How are you updating the rotation?

This is how I do it


  
        ChangeState(NAME_Spectating);
        ClientGotoState(NAME_Spectating);

        Client_UpdateSpectatorCameraRotation(CameraRotation);




Client_UpdateSpectatorCameraRotation_Implementation(FRotator CameraRotation)
{
    ASpectatorPawn* SpectatorPawn = GetSpectatorPawn();

    if (SpectatorPawn)
    {
        SetControlRotation(CameraRotation);
    }
}


My rotation is updated in the client’s version of the PlayerController

Erf, I have the same problem as Moshy at the begining. My character is destroy after 10 seconds, so my spectator pawn works fine on server until the character is destroyed. After 10 seconds, it’s broken as on the client, with an horizontal camera and wrong constraints.

I will leave this problem aside for the moment, for saving my brain and my time. I’ll looking back later with clear spirit and new ideas.

I temporarly use the dirty solution for my prototype, but I don’t like this.

Here is my code if that can help somebody to make a dirty patch:

in my character pawn .cpp file, when the player die:


GetMovementComponent()->StopMovementImmediately();

// Set spectator state after detach from controller
ASPlayerController* PC = Cast<ASPlayerController>(GetController());

DetachFromControllerPendingDestroy();

if (PC)
{
    PC->SetSpectatorState(this);
}

SetLifeSpan(10.f);

.h of the controller


public:
    void SetSpectatorState(APawn* OldPawn = nullptr);

protected:
    void WaitingForChangeState();

.cpp of the controller


void ASPlayerController::SetSpectatorState(APawn* OldPawn)
{
    if (!HasAuthority()) { return; }

    if (OldPawn)
    {
        ClientSetViewTarget(OldPawn); // Rotation is broken on client until change state to spectating
    }

    // TODO remove this horible patch:
    // Waiting for initialize correctly the spectator pawn (don't know why.)
    FTimerHandle TimerHandle_Tmp;
    GetWorldTimerManager().SetTimer(TimerHandle_Tmp, this, &ASPlayerController::WaitingForChangeState, 4.f, false);
}

void ASPlayerController::WaitingForChangeState()
{
    ASGameState* GS = GetWorld()->GetGameState<ASGameState>();
    // Change to spectator state only during the playing phases
    if (GS && (GS->GetWaveState() == EWaveState::WaitingToComplete || GS->GetWaveState() == EWaveState::WaveInProgress))
    {
        ChangeState(NAME_Spectating);
        ClientGotoState(NAME_Spectating);
    }
}

@Kris @Moshy thanks for your help. Even if the problem still present, I understand a little more all this.

I’ve been having some trouble with this issue for a while and came up with the solution after extensive breakpoints.

If you’re SpectatorPawn is not spawning properly, it’s because you’re not calling ChangeState(NAME_Spectating) on the server side.

If you’re SpectatorPawn spawns correctly and everything is working except the camera position, you need to set the ViewTarget back to SpectatorPawn. What basically happens is after the SpectatorPawn is spawned and set as a ViewTarget on the client side, a delayed Controller::OnRep_Pawn(null) calls PlayerController::SetPawn(null), which then invokes AutoManageActiveTarget(this), changing the PlayerController location as the ViewTarget.

I’ve made a pull request to the Unreal Source engine which fixes the issue 5 days ago, but for now, you can easily change the ViewTarget issue by overriding the OnRep_Pawn() on your controller class, calling its base, then setting the ViewTarget Back to the Spectator Pawn if it’s in a spectating state.

void AMyPlayerController::OnRep_Pawn()
{
Super::OnRep_Pawn();

if (GetStateName() == NAME_Spectating)
{
    AutoManageActiveCameraTarget(GetSpectatorPawn());
}

}

Thanks Alex.

Another update: If you’re overriding SetPawn(APawn* InPawn) don’t check for nullptr. I just ran into another case where the spectator pawn was created, but the client was not possessing it.

This is what I had:



void ABasePlayerController::SetPawn(APawn* InPawn)
{
    if (InPawn == nullptr) // remove this
          return;

    Super::SetPawn(InPawn);
    // ...
}


Thanks this solved my issue, any news on when this gets fixed?
I’m using 4.22 and I seem to still have this problem.

The simplest solution to the problem that I found (5.1 version):

// called both on client and server by some multicast function in my character class
void ABlasterPlayerController::PlayerDeath(AController* Killer)
{
	if (HasAuthority())
	{
		ChangeState(NAME_Spectating);
		// avoid repeated calls on the server
		if (GetRemoteRole() == ROLE_AutonomousProxy)
		{
			ClientGotoState(NAME_Spectating);
		}
	}
	else // there is no need to remember the location on the server
	{
		const auto BlasterCharacter = Cast<ABlasterCharacter>(GetPawn());
		if (BlasterCharacter && BlasterCharacter->GetCameraComponent())
		{
			SetSpawnLocation(BlasterCharacter->GetCameraComponent()->GetComponentLocation());
		}
	}
}