How Can I prevent Controlled Pawns from Being Deleted On Reset?

I’m working a local multiplayer game. At the end of the round, I’d like to reset it so players are ready to play again. I’m using the Reset Level node and the OnReset event.

output

I’m seeing two things happen when I reset the level:

  1. Any pawn possessed by a player controller is deleted
  2. The camera changes

output

For example, if I have 4 player controllers that each possess a pawn, all 4 pawns are deleted OnReset

In Mathew Wadstein’s video on this, he calls out a similar, but subtly different behavior:
He notes the issue that any player pawn spawned in by the player start will be deleted. However, my pawns are not spawned in by player starts - they are in the level directly.

In the comments section to that video, devs say they’ve worked around this by spawning a default pawn, having the player controller possess that, resetting the level, then restoring the original connection. So far, that hasn’t worked for me - the berry is still deleted when this blueprint code is run.

Any advice on how I can keep these pawns from being deleted OnReset?

Hm… in this video they show pairing Reset Level with Restart Player at Player Start and also a Delay.

I’m not using Player Starts in my game, so I swapped it for the “Restart Player at Transform.”

However, I’m not seeing any change in the behavior - the pawn is deleted, the camera changes, and there isn’t a pawn that replaces it.

Screenshot 2023-05-05 071724

Checking out the C++…

Simply enough, looks like the behavior is different if the controller is set on the pawn. Still not sure why unpossessing the pawn isn’t working for me…


Interesting - when I select the camera in the Outliner, I can see the view is still correct. So the new view is something different happening.

PlayerController_Reset
Yeah, looks like in the C++ the view target is getting reset.

Ok, this resetting behavior is built in at a few places in C++:

  1. APawn::Reset()
  2. APlayerController::Reset()
  3. APlayerController::ClientReset_Implementation()

The cleanest solution I find is to make subclasses and override the default behaviors. I made 3:

ResettablePawn.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "ResettablePawn.generated.h"

UCLASS()
class BUMPERCROP_API AResettablePawn : public APawn
{
	GENERATED_BODY()

public:	

	AResettablePawn(); //constructor

	virtual void Reset() override;

};

ResettablePawn.cpp

#include "ResettablePawn.h"
#include "Engine/GameEngine.h"

// constructor
AResettablePawn::AResettablePawn()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AResettablePawn::Reset()
{
	K2_OnReset();
}

(K2_OnReset() is the function that triggers the Blueprint OnReset event)

ResettablePlayerController.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "ResettablePlayerController.generated.h"

UCLASS()
class BUMPERCROP_API AResettablePlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:

	AResettablePlayerController(); //constructor

	virtual void Reset() override;

};

ResettablePlayerController.cpp

#include "ResettablePlayerController.h"

AResettablePlayerController::AResettablePlayerController()
{
	PrimaryActorTick.bCanEverTick = true;

}


void AResettablePlayerController::Reset()
{

	K2_OnReset();

}

That takes care of the first two functions, but the third one isn’t virtual. For that one, we can override the GameModeBase::ResetLevel() function, keep most of the code the same, but remove the client reset call.

The client reset would need to be replaced with a different function if the game were networked, but that’s out of scope for now.

ResettableGameMode.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "ResettableGameMode.generated.h"

/**
 * 
 */
UCLASS()
class BUMPERCROP_API AResettableGameMode : public AGameMode
{
	GENERATED_BODY()
	
public:
	virtual void ResetLevel() override;
};

ResettableGameMode.cpp

#include "ResettableGameMode.h"
#include "EngineUtils.h"
#include "Engine/LevelScriptActor.h"

void AResettableGameMode::ResetLevel()
{
	UE_LOG(LogGameMode, Verbose, TEXT("Reset %s"), *GetName());

	// Reset ALL controllers first
	for (FConstControllerIterator Iterator = GetWorld()->GetControllerIterator(); Iterator; ++Iterator)
	{
		AController* Controller = Iterator->Get();
		Controller->Reset();
	}

	// Reset all actors (except controllers, the GameMode, and any other actors specified by ShouldReset())
	for (FActorIterator It(GetWorld()); It; ++It)
	{
		AActor* A = *It;
		if (IsValid(A) && A != this && !A->IsA<AController>() && ShouldReset(A))
		{
			A->Reset();
		}
	}

	// Reset the GameMode
	Reset();

	// Notify the level script that the level has been reset
	ALevelScriptActor* LevelScript = GetWorld()->GetLevelScriptActor();
	if (LevelScript)
	{
		LevelScript->LevelReset();
	}
}

Then I just switched the parent classes of my custom Blueprint pawn, player controller, and game mode to these new versions.


This is perhaps a bit overkill - another option is just creating a new type of Reset that didn’t have these built-in behaviors might be easier, but at least it was good getting practice digging into the engine.