Clients spectating instead of playing (replication issues?)

I made some slight changes to my game, that should in no way influence the spectating state: I created a new c++ class based on HUD, which just adds a Widget to the PlayerScreen on BeginPlay. I selected that HUD class as my default HUD class in my BP_Gamemode and I modified my BirdsEyePawn, so it does not create a Widget anymore.

After I made the changes, when I play in editor with a dedicated server, the client does not spawn the default pawn anymore and spawns the SpectatorPawn instead. When playing with a listen server and a client, the listen server still spawns the default pawn.

These are the relevant logs, when I get the SpecatorPawn instead of my DefaultPawn (BP_BirdsEyePawn):

LogGameMode:Display: Match State Changed from WaitingToStart to InProgress
LogBaseGameMode:Verbose: Spawned Default Pawn 'BP_BirdsEyePawn_C_0' for 'BP_BasePlayerController_C_0'
LogGameState: Match State Changed from WaitingToStart to InProgress
LogBasePlayerController:Verbose: NULL GameState when trying to spawn spectator!
LogSpawn:Warning: SpawnActor failed because no class was specified
LogBasePlayerController:Verbose: Spawned spectator SpectatorPawn_0 [server:0]

The SpawnActor failure seems to be the PlayerCameraManager, because I do not have a PlayerCameraManager Class assigned in my BP_PlayerController, and that Log dissappears, when I assign a PlayerCameraManager class. Weirdly, when I go back to the previous version in Git, the PlayerCameraManager Class is still unassigned, but the SpawnActor failure does not seem to appear.

The other message missing from the logs, when I play with the previous version is that the SpectatorPawn_0 was spawned. So maybe the match state is not correctly replicated to the client?

I am really at a loss about what is going on. Any help or things I could try would be very much appreciated.

If someone from Epic chimes in, I would happily send you a download link to my project. I would prefer to send the download link to an email address or through some other medium, where I don’t have to share the link with the whole internet.

I found, that when SpawnSpectatorPawn() is called, that the MatchState is InProgress, which I don’t think should happen. This is the relevant part of the call stack:

LogOutputDevice:Error: === Handled ensure: ===
LogOutputDevice:Error: Ensure condition failed: GetWorld()->GameState->GetMatchState() != MatchState::InProgress [File:C:\Users\alljo\Documents\Unreal Projects\gameCPP\RobotDefenseCPP\Source\RobotDefenseCPP\Player\BasePlayerController.cpp] [Line: 60]
LogOutputDevice:Error: Stack: 
LogOutputDevice:Error: UE4Editor-Core.dll!FWindowsPlatformStackWalk::StackWalkAndDump() [d:\build\++ue4+release-4.13+compile\sync\engine\source\runtime\core\private\windows\windowsplatformstackwalk.cpp:183]
LogOutputDevice:Error: UE4Editor-Core.dll!FDebug::EnsureFailed() [d:\build\++ue4+release-4.13+compile\sync\engine\source\runtime\core\private\misc\outputdevice.cpp:297]
LogOutputDevice:Error: UE4Editor-Core.dll!FDebug::OptionallyLogFormattedEnsureMessageReturningFalse() [d:\build\++ue4+release-4.13+compile\sync\engine\source\runtime\core\private\misc\outputdevice.cpp:432]
LogOutputDevice:Error: UE4Editor-RobotDefenseCPP-3129.dll
LogOutputDevice:Error: UE4Editor-Engine.dll!APlayerController::BeginSpectatingState() [d:\build\++ue4+release-4.13+compile\sync\engine\source\runtime\engine\private\playercontroller.cpp:4085]
LogOutputDevice:Error: UE4Editor-Engine.dll!APlayerController::ReceivedSpectatorClass() [d:\build\++ue4+release-4.13+compile\sync\engine\source\runtime\engine\private\playercontroller.cpp:3726]
LogOutputDevice:Error: UE4Editor-Engine.dll!AGameState::ReceivedSpectatorClass() [d:\build\++ue4+release-4.13+compile\sync\engine\source\runtime\engine\private\gamestate.cpp:112]
LogOutputDevice:Error: UE4Editor-CoreUObject.dll!UFunction::Invoke() [d:\build\++ue4+release-4.13+compile\sync\engine\source\runtime\coreuobject\private\uobject\class.cpp:4280]

And this is the code, where I overrode SpawnSpectatorPawn() in my BasePlayerController.cpp:

ASpectatorPawn * ABasePlayerController::SpawnSpectatorPawn()
{
	if (GetWorld()->GameState)
	{
		UE_LOG(LogBasePlayerController, Verbose, TEXT("MatchState: %s"), *GetWorld()->GameState->GetMatchState().ToString());
		ensure(GetWorld()->GameState->GetMatchState() != MatchState::InProgress);
	}
    ...

So it seems, like some things are happening, when the following function is called:

void APlayerController::ReceivedSpectatorClass(TSubclassOf<ASpectatorPawn> SpectatorClass)
{
	if (IsInState(NAME_Spectating))
	{
		if (GetSpectatorPawn() == NULL)
		{
			BeginSpectatingState();
		}
	}
}

IsInState(NAME_Spectating) returns true, even though GameState->MatchState is ‘InProgress’. So the GameState does not pass the MatchState down to the client’s version of its PlayerController, because IsInState() just checks the local StateName variable.

Edit: Nevermind, what I wrote before, I just had a different SpawnActor() failure, because my default HUD class in my BP_Gamemode was ‘None’.

I investigated, when the change is changed. The Player Controller’s State Name is changed in 2 places: In APlayerController::PostInitializeComponents() the StateName is set to NAME_Spectating. And in APlayerController::ChangeState is changed normally. I set up breakpoints in Visual Studio and it looks like the PlayerController is created, setting the StateName to NAME_Spectating. Then ChangeState is called, changing it to the Playing state, but then the PlayerController is constructed again, which changes the StateName to NAME_Spectating, but it is never changed back to the Playing state.

for some reason, when APlayerController::ClientRestart_Implementation(APawn* NewPawn) is called, NewPawn is NULL, resulting in the Client’s Pawn being set to NULL, and the PlayerController not changing to Playing state.

I was able to find out what caused the change in behaviour. By adding the following functions back into my BirdsEyePawn, I was able to recreate the previous behaviour:

BirdsEyePawn.h
public:
	virtual void UnPossessed() override;
	virtual void PossessedBy(AController* NewController) override;
protected:
	UFUNCTION(client, reliable)
		virtual void Client_UnPossessed();
	UFUNCTION(client, reliable)
		virtual void Client_PossessedBy(AController* NewController);

BirdsEyePawn.cpp
void ABirdsEyePawn::UnPossessed()
{
	Super::UnPossessed();
	Client_UnPossessed();
}

void ABirdsEyePawn::Client_UnPossessed_Implementation()
{
}

void ABirdsEyePawn::PossessedBy(AController * NewController)
{
	Super::PossessedBy(NewController);
	Client_PossessedBy(NewController);
}

void ABirdsEyePawn::Client_PossessedBy_Implementation(AController* NewController)
{
}

That made me realize, what might actually be happening: My replicated function call inside ABirdsEyePawn::PossessedBy is delaying the APlayerController::ClientRestart() call just long enough, that the Client already has destroyed the placeholder PlayerController, which then receives the ClientRestart() call, resulting in the Client’s PlayerController performing a false Restart and acting as if it possessed my BirdsEyePawn for a couple of seconds, until the BirdsEyePawn is garbage collected.

So my actual problem is something else and I will probably create a new answerhub post, once I figured that out, because no one wants to read through all my crap.

Or at least I wouldn’t want to read through all of that :slight_smile:

EDIT: Nevermind, I found the solution. A while ago I overrode APawn::IsNetRelevantFor() like so:

bool ABirdsEyePawn::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
	// BirdsEyePawn is never relevant to the client controlling it, because the controller acts authoritative
	if (RealViewer == Controller || this == ViewTarget)
	{
		return false;
	}
	else return Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
}

That of course resulted in the Pawn being deleted on the owning client, which in turn resulted in the above dilemma.