Download

[SOLVED] How do I spawn Players with different Pawns in Network games?

Perhaps someone can assist me with this, since this is proving to be a complete nightmare.

Our game is designed to work as part of an installation, with three PC’s and 20 Android tablets. Each one of these has a text file on it which configures it for the type of player it is. Android tablets are supposed to spawn into the game world as one type of pawn, whereas the PC’s are supposed to spawn in the world as a different type of pawn. I’ve been overriding a Gamemode function called “GetDefaultPawnClassForController”, which looks like it’s supposed to allow the end user to spawn a pawn for a player based on the Player Controller. By default, this function just returns the GameMode’s default Pawn, so I don’t understand why they would allow us to pass in a Controller if it wasn’t intended to be used this way.

After following this tutorial on the Wiki however I can confirm that it absolutely doesn’t work for Network games, because the pawn to spawn gets to the Server too late or whatever.

In order to determine which Pawn to spawn, the PlayerController reads from a text file to figure out which type of player it is (Tablet or Screen), and attempts to set a variable with the correct pawn class to spawn. Unfortunately it doesn’t seem to work at all. It seems to set the Default Pawn class for ALL player controllers, not the specific client controller.

Here’s the code, perhaps someone can make sense of it all. I’ve even tried calling ‘RestartPlayer’ on the PlayerController, but still not having any luck with it - It’s always reading which pawn to be from the Server.

GESGame_PlayerController.cpp



// Stormtide 2015

#include "GESGame.h"
#include "GESGame_PlayerController.h"

AGESGame_PlayerController::AGESGame_PlayerController(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
	GESServerPawn = NULL;
	GESClientPawn = NULL;

	MyPawnClass = NULL;

	bReplicates = true;
	GESCheckString = FString(TEXT("Tablet")); // Players are Tablets By Default
}

void AGESGame_PlayerController::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	if (GetWorld())
	{
		DeterminePawnClass();
	}
}

void AGESGame_PlayerController::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	DOREPLIFETIME(AGESGame_PlayerController, MyPawnClass);
}

UClass* AGESGame_PlayerController::GetGESPawnClass()
{
	return MyPawnClass;
}

void AGESGame_PlayerController::DeterminePawnClass()
{
	if (IsLocalController())
	{
		ASSERTV(GESClientPawn != NULL, *FString::Printf(TEXT("Player Controller: No Client Pawn Class Specified - Aborting")));
		ASSERTV(GESServerPawn != NULL, *FString::Printf(TEXT("Player Controller: No Server Pawn Class Specified - Aborting")));

		/* Load Text File Into String Array */
		TArray<FString> TextStrings;
		const FString FilePath = FPaths::GameDir() + "Textfiles/GESSettings.txt";

		/* Check if we successfully loaded the string */
		bool bDone = FFileHelper::LoadANSITextFileToStrings(*FilePath, NULL, TextStrings);
		ASSERTV(bDone, NULL, *FString::Printf(TEXT("Player Controller: Couldn't load GESSettings.txt - Aborting")));

		GESCheckString = TextStrings[0];

		if (GESCheckString == "Screen")
		{
			ServerSetPawn(GESServerPawn);
		}
		else if (GESCheckString == "Tablet")
		{
			ServerSetPawn(GESClientPawn);
		}
	}
}

bool AGESGame_PlayerController::ServerSetPawn_Validate(TSubclassOf<APawn> InPawnClass)
{
	return true;
}

void AGESGame_PlayerController::ServerSetPawn_Implementation(TSubclassOf<APawn> InPawnClass)
{
	MyPawnClass = InPawnClass;

	/* Restart Player */
	GetWorld()->GetAuthGameMode()->RestartPlayer(this);

	GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Yellow, FString::Printf(TEXT("Name = %s"), *MyPawnClass->GetDefaultObject()->GetName()));
}


GESGame_DefaultGameMode.cpp



UClass* AGESGame_DefaultGameMode::GetDefaultPawnClassForController(AController* InController)
{
	/* Override Functionality to get Pawn from PlayerController */
	AGESGame_PlayerController* GESController = Cast<AGESGame_PlayerController>(InController);
	if (GESController)
	{
		return GESController->GetGESPawnClass();;
	}

	return DefaultPawnClass;
}


Spawndefaultpawnfor is a place u can override. You can do a comparison operation there. Also you can spawn all characters as default pawn type and initialize the pawn type from the player controller itself after beginplay. Postlogin is another place, restartplayer is another place that’ll work. Getting the pawn class isnt going to do much, you want to spawn and possess.

Those functions you listed ALL call the function that I’ve overridden above, I traced back through them all.

The only way I can think to do it right now, is wait for the Player to spawn in the world, then have them ask the server to spawn a new pawn and possess that. Seems like a poor workaround to me, and I’ll have to wait until 4.8 for that to work, since Pawn Possession hasn’t been fixed until now.

Also, the data files on everyones computer could be modified by turning defaultpawn into a single id number to represent the pawn type and pass this as a parameter value into clienttravel or include it as part of the Game shortcut. The parameter will be passed in during the login process in the game mode and then you can use that in your comparison operation to spawn the appropriate pawn.

I think the issue here is that the server instance is responsible for determining the default pawn. File loading is done on the clients computer but need to be passed over. I cant say for certain because its been years since i dealt with this in UDK. Unless things have changed much you would use the parameter value to create a pawn. I had created a struct of 48 different pawn types and used the id to load the appropriate pawn. Its useful this way because then you dont need direct references to your assets.

http://www.osnapgames.com/2014/06/13/passing-and-reading-arguments-to-an-unreal-engine-4-server-upon-connection/

Okay so I can pass additional options in with that, but it still won’t let me determine what Pawn to spawn for the Player Controller, because InitNewPlayer and SpawnDefaultPawnFor are completely disjointed from each other, so I have no way to tell the Gamemode which specific player has to possess which specific pawn.

Also, ClientTravel in PlayerController isn’t a virtual function so I can’t override it to add my variables in code.

Basically speaking, how can I modify the URL in code, given that I can’t modify any functions.

Could you setup a default pawn that literally does nothing, then later on replace it with a new pawn that is actually the correct one. So basically a temporary “default” pawn with no functionality as a holding pawn?

I know it feels wrong to do that, but it does actually make sense. I’m doing a typical network play setup right now and I want players to setup stuff in a menu before they actually spawn into the game. So a “holding” pawn is not a bad idea there. You could do a similar thing.

That seems like the only way to do it right now. I can’t pass in parameters through the commandline as above, so the only way I can think of doing it is spawn everybody into the game as a spectator, blacken the screen for a while while it works out which is the correct pawn to spawn and possesses them.

Seems pretty **** silly to me.

zoombapup has a workable solution, most likely the switching to a new pawn would be within seconds you would hardly see a delay.

In your setup, the pawn to spawn is determined by the text file saved on the client’s device. The loading of the text file is done on the client side then passed over to the server either as a parameter or after the player has spawned.

Loading of the text file for the character to load is usually done on a map before the client connects to the server, then the pawn type is sent as a parameter using ClientTravel which can be done as a console command from the player controller. If you’re connecting to a server map this is going to have to happen anyways, how else would you travel to the server?


https://answers.unrealengine.com/questions/122565/browse-servertravel-and-clienttravel.html

The server needs to have some parameter sent to it in order to determine who owns which predetermined pawn type. Imagine you’re creating a battle royale game with 300+ players in a ring of all individually chosen player pawn types. The main server responsible for spawning all the players into the game world needs to know which client gets control of which player pawn type. The server also needs to know which player receives the score that is accumulated after attacks on which player pawn. Since the server spawns players in order of whoever connects first, and assigned the first available player id by default you run into issues identifying who is who. Passing your player’s unique playername or playerid that is consistent to their actual login id with the database server hosting the scores into ClientTravel you solve the issue of determining which player type to spawn and which score goes to which player in your remote score database.

From the options I can think of at the moment, the server can:

  1. use the unique playerid passed in with clienttravel to check a remote users database to determine which pawn to spawn and default attributes to apply to the player’s pawn.
  2. use an id for which pawn type to use or which default attributes to assign the player from within the parameters in clienttravel and keep the unique player id consistent with the users database only available on the client.
  3. Player pawns chosen at random.

I got it working, used the same method I was originally using, but had to add some extra checks to see if the player was local or not. Really was a ballache workaround, I hope the functionality for this is improved in the future, games where all the players spawn in identically are going to be pretty rare in the future.

Just for clarity, can you explain what you ended up doing?

Sure thing, I’ll post the code tomorrow!

Here you go Zoombapup, posted a tutorial on the Wiki! This method seems to be working very reliably for me, even in latent situations :slight_smile:

Hello, is there anyway I can do this with blueprint, I know nothing about c++??

I plan on updating that tutorial at some stage with a method that uses Player States or something similar instead. Haven’t got to implementing it in my own game yet though.

Really there are two options, either you store it in a playerstate and use seamless travel (which rarely seems to work for me at all), or you pack it into the options string when a player joins. I will post both a BP and C++ tutorial as soon as I’ve worked it out myself!

Patiently waiting for your tutorial :slight_smile: , I’m using possess in BP as a temporally for now, but possess causes a lot of complications :(.