Skip to content

Multiplayer Series UE4 [2] – Deathmatch

C++ Classes

It is best to start from creating C++ classes. First we need Gamemode, Custom Player Start, Server Player info managing class and some enums and structures.

Goto File -> New C++ Class…

Create PlayerStart Class, we are going to add our own modifications to this.

Name it as CustomPlayerStart.

Create Game Mode Base Class.

Name it as MPDM_Gamemode.

Create a Blueprint Function Library and name it as ServerPlayers.

Create 3 None Classes, Teams, PlayerInfo and ServerInfo. Remove .cpp files from all 3.

Now everything should look like this:

Teams.h

This is a list of Teams it also contains Lobby, Spectator and None which will be used as default.

#pragma once

#include "CoreMinimal.h"
#include "Teams.generated.h"

UENUM(BlueprintType)
enum class ETeams : uint8
{
	None UMETA(DisplayName = "No Team"),
	Lobby UMETA(DisplayName = "Lobby"),
	Spectator UMETA(DisplayName = "Spectator"),
	Opfor UMETA(DisplayName = "Opfor"),
	Bluefor UMETA(DisplayName = "Bluefor"),
	Independent UMETA(DisplayName = "Independent"),
	Civilian UMETA(DisplayName = "Civilian")
};

ServerInfo.h

In Server info we have

  1. ESpawnClass, This is a list of Spawn Groups. Useful when you need characters and vehicles in different locations.
  2. EQueuePrivileges, Server Join Queue privileges.
  3. ESpawnQueueAddStatus
  4. EOccupiedStatus, status of spawn location. We won’t be adding this feature but you can add it yourself if you like. Simply just create a function in PlayerStart and access it in Gamemode.
  5. EReservedAuthorityBranch, reserved slots enter authority status.
  6. FSpawnQueueInfo, spawn queue information holder. Can’t be edited in blueprints in 4.26 because selecting UClass will cause an editor crash but can be managed in C++ (Was working fine in 4.25).
  7. FUTCTimeDifference, for UTC clock time sync feature. This will store information about server and client time difference.
  8. FPlayerKick, Player kick information holder.
  9. FServerSettings, Server Settings.
//Make sure enums are on top of file
#pragma once

#include "CoreMinimal.h"
#include "ServerInfo.generated.h"

/**
* Spawn Class types.
*/
UENUM(BlueprintType)
enum class ESpawnClass : uint8
{
	JoinSpawn UMETA(DisplayName = "Join Spawn"),
	RespawnSpawn UMETA(DisplayName = "Respawn Spawn"),
	Vehicle UMETA(DisplayName = "Vehicle"),
	VehicleSpecial UMETA(DisplayName = "Vehicle Special"),
	Special_A UMETA(DisplayName = "Special A"),
	Special_B UMETA(DisplayName = "Special B"),
	Special_C UMETA(DisplayName = "Special C"),
	Death UMETA(DisplayName = "Death"),
	Lobby UMETA(DisplayName = "Lobby"),
	None UMETA(DisplayName = "None")
};

/**
* Spawn Queue Ranks.
*/
UENUM(BlueprintType)
enum class EQueuePrivileges : uint8
{
	//Highest rank 0
	Admin UMETA(DisplayName = "Admin"),
	//Rank 1
	VIP UMETA(DisplayName = "VIP"),
	//Rank 2
	Special_Yellow UMETA(DisplayName = "Special Yellow"),
	//Rank 3
	Special_Purple UMETA(DisplayName = "Special Purple"),
	//Lowest rank 4
	Regular UMETA(DisplayName = "Regular")
};

/**
*
*/
UENUM(BlueprintType)
enum class ESpawnQueueAddStatus : uint8
{
	Success UMETA(DisplayName = "Succes"),
	UnknownFailure UMETA(DisplayName = "Unknown Failure"),
	JoinQueueIsFull UMETA(DisplayName = "Join Queue Is Full"),
	NoAuthority UMETA(DisplayName = "No Authority")
};

/**
* Spawn Point Occupied status.
*/
UENUM(BlueprintType)
enum class EOccupiedStatus : uint8
{
	Availible UMETA(DisplayName = "Availible"),
	Occupied UMETA(DisplayName = "Occupied")
};

/**
* Branches for ReservedSlotsLoginAuthorize.
*/
UENUM(BlueprintType)
enum class EReservedAuthorityBranch : uint8
{
	Authority UMETA(DisplayName = "Authority"),
	NoAuthority UMETA(DisplayName = "No Authority")
};

/**
* Main Spawn Queue holder.
*/

USTRUCT(BlueprintType)
struct FSpawnQueueInfo {
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite)
		APlayerController* Controller;

	UPROPERTY(BlueprintReadWrite)
		FString PlayerID;

	UPROPERTY(BlueprintReadWrite)
		FDateTime Time;

	UPROPERTY(BlueprintReadWrite)
		UClass* SpawnCharacter;

	UPROPERTY(BlueprintReadWrite)
		ESpawnClass SpawnGroup;
};


USTRUCT(BlueprintType)
struct FUTCTimeDifference {
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite)
		FTimespan TimeDifference;
	UPROPERTY(BlueprintReadWrite)
		bool SetByController;
};

/** Generate error message and initiate a player kick off a server.
*	Player Controller needs to be notified about this message then it will automatically picks up this structure and saves it in Game Instance,
*	where it can be read after client has left the server.
*/
USTRUCT(BlueprintType)
struct FPlayerKick {
	GENERATED_BODY()
public:
	UPROPERTY(BlueprintReadWrite)
		APlayerController* Player;
	UPROPERTY(BlueprintReadWrite)
		FDateTime Time;
	UPROPERTY(BlueprintReadWrite)
		FString KickMessage;
	UPROPERTY(BlueprintReadWrite)
		bool Kick;
	//Changing this parameter to true will perform an automated kick action. Make sure kick message is saved in clients Game Instance before changing this.
	UPROPERTY(BlueprintReadWrite)
		bool ReadByPlayerController;
};

/**
*
*/
USTRUCT(BlueprintType)
struct FServerSettings {
	GENERATED_BODY()
public:
	//List of privileges that grant access to reserved slots. Use Make Array.
	UPROPERTY(BlueprintReadWrite)
		TArray<EQueuePrivileges> PrivilegesForSlots;
	//Server Max players.
	UPROPERTY(BlueprintReadWrite)
		int32 ServerMaxPlayers;
	//Slots from max players.
	UPROPERTY(BlueprintReadWrite)
		int32 ReservedSlots;
	//Server join queue size
	UPROPERTY(BlueprintReadWrite)
		int32 ServerJoinQueueSize;
	//If server is full admins can join.
	UPROPERTY(BlueprintReadWrite)
		bool AllowAdminsIfServerFull;
	//Don't count admins in join queue
	UPROPERTY(BlueprintReadWrite)
		bool DontCountAdminsInJoinQueue;
	//If join queue is full these can be kicked, even if they are in PrivilegesForSlots. If this is empty everything that is not in PrivilegesForSlots will be kicked.
	UPROPERTY(BlueprintReadWrite)
		TArray<EQueuePrivileges> Kickable;
};

PlayerInfo.h

This is pretty self-explanatory. Server Controller means a copy of the Players Controller on the server.

#pragma once

#include "CoreMinimal.h"
#include "Teams.h"
#include "PlayerInfo.generated.h"

/**
 * Player Info for Server
 *
 * @param	PlayerID			Player id, 32 characters long.
 * @param	PlayerName			Public Player Name on server.
 * @param	ServerController	Player Controller on server.
 * @param	PlayerNumber		Player Controller identification number.
 * @param	JoinTime			Date Time of when player joined the server.
 * @param	PlayerIP			Player ip address.
 * @param	Team				Team of player.
 * @param	ServerCharacter		Player Pawn on server.
 */

USTRUCT(BlueprintType)
struct FPlayerInfo {
	GENERATED_BODY()
public:
	//Player id, 32 characters long.
	UPROPERTY(BlueprintReadWrite)
	FString PlayerID;

	//Public Player Name on server.
	UPROPERTY(BlueprintReadWrite)
	FString PlayerName;

	//Player Controller on server (from cast). You can also change APlayerController to AController.
	UPROPERTY(BlueprintReadWrite)
	APlayerController* ServerController;

	//Player Controller identification number.
	UPROPERTY(BlueprintReadWrite)
	int32 PlayerNumber;

	//Date Time of when player joined the server.
	UPROPERTY(BlueprintReadWrite)
	FDateTime JoinTime;

	//Player ip address.
	UPROPERTY(BlueprintReadWrite)
	FString PlayerIP;

	//Team of player.
	UPROPERTY(BlueprintReadWrite)
	ETeams Team;

	//Player Pawn on server.
	UPROPERTY(BlueprintReadWrite)
	APawn* ServerCharacter;
};

/*
*
*/
USTRUCT(BlueprintType)
struct FUpdatePlayerInfo {
	GENERATED_BODY()
		FUpdatePlayerInfo() : UpdateTeam(false), UpdateCharacter(false), UpdatePlayerNum(false), UpdatePlayerIP(false), UpdatePlayerController(false), UpdatePlayerName(false) {}
public:

	UPROPERTY(BlueprintReadWrite)
		bool UpdateTeam;

	UPROPERTY(BlueprintReadWrite)
		ETeams Team;

	UPROPERTY(BlueprintReadWrite)
		bool UpdateCharacter;

	UPROPERTY(BlueprintReadWrite)
		APawn* ServerCharacter;

	UPROPERTY(BlueprintReadWrite)
		bool UpdatePlayerNum;

	UPROPERTY(BlueprintReadWrite)
		int32 PlayerNumber;

	UPROPERTY(BlueprintReadWrite)
		bool UpdatePlayerIP;

	UPROPERTY(BlueprintReadWrite)
		FString PlayerIP;

	UPROPERTY(BlueprintReadWrite)
		bool UpdatePlayerController;

	UPROPERTY(BlueprintReadWrite)
		APlayerController* ServerController;

	UPROPERTY(BlueprintReadWrite)
		bool UpdatePlayerName;

	UPROPERTY(BlueprintReadWrite)
		FString PlayerName;
};



/**
* Number tracking holder for Player Controller numbers.
*/
USTRUCT(BlueprintType)
struct FPlayerNumber {
	GENERATED_BODY()
public:
	UPROPERTY(EditDefaultsOnly)
		int32 PlayerNumber;
	UPROPERTY(EditDefaultsOnly)
		FString PlayerID;
};

CustomPlayerStart.h

This is a child class of PlayerStart we are just creating our own version of it. So we just add Teams and Spawn Group variables to it and that’s it. You can delete the cpp file or leave it if you want to modify this more later on.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Teams.h"
#include "ServerInfo.h"
#include "GameFramework/PlayerStart.h"
#include "CustomPlayerStart.generated.h"

/**
 * 
 */
UCLASS()
class ACustomPlayerStart : public APlayerStart
{
	GENERATED_BODY()
public:
	/** Set Team */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Object)
	ETeams Team;

	/** Set Spawn Group */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Object)
	ESpawnClass SpawnGroup;
	
};

ServerPlayers.h

This is a static class. Accessing anything in this you need UServerPlayers::Function(); in front.

//

#pragma once

#include "CoreMinimal.h"
#include "Engine/NetConnection.h"
#include "PlayerInfo.h"
#include "TimerManager.h"
#include "Kismet/GameplayStatics.h"
#include "ServerPlayers.generated.h"

/**
 *
 */
UCLASS()
class UServerPlayers : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:

    UServerPlayers(const FObjectInitializer& ObjectInitializer);

    /**
    * Register a new player info on the server.
    *
    * @param     ServerController    Player Controller on server (from cast).
    * @param     PlayerName          Public Player Name in the game.
    * @param     PlayerIP            Player Ip address.
    * @param     Team                Player Team.
    * @param     ServerCharacter     Character Pawn on server.
    * @param     SetControllerID     Set Player number ID automatically on controller (SetPlayerControllerID Function). Otherwise it will be -1 for everyone, unless set manually.
    */
    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Add New Player", Keywords = "spawn queue player add new"), Category = "Multiplayer Players")
        static FPlayerInfo AddNewPlayer(APlayerController* ServerController = nullptr, FString PlayerName = FString(TEXT("")), FString PlayerIP = FString(TEXT("")), ETeams Team = ETeams::Bluefor, APawn* ServerCharacter = nullptr, bool SetControllerID = true);

    /**
    * Remove Player info using controller.
    *
    * @param    Controller          Player Controller (from cast).
    * @return                       Returns true if remove was a success.
    */
    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Remove Player by Controller", Keywords = "spawn queue player find controller"), Category = "Multiplayer Players")
        static bool RemovePlayerByController(APlayerController* Controller);

    /**
    * Remove Player info using player id.
    *
    * @param    PlayerID          Player ID.
    * @return                     Returns true if remove was a success.
    */
    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Remove Player By ID", Keywords = "spawn queue player remove id"), Category = "Multiplayer Players")
        static bool RemovePlayerByID(FString PlayerID);

    /**
    * Find player info using player id.
    *
    * @param    PlayerID          Player ID.
    * @param    PlayerInfo        Player info structure.
    */
    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Find Player by ID", Keywords = "spawn queue player find id"), Category = "Multiplayer Players")
        static void FindByID(FString PlayerID, FPlayerInfo& PlayerInfo);

    /**
    * Get count of all registered players.
    *
    * @param    PlayerCount         Player Count.
    * @param    ExcludeJoinQueue    Negate server join queue players from count.
    */
    UFUNCTION(BlueprintPure, meta = (DisplayName = "Player Count", Keywords = "spawn queue player count"), Category = "Multiplayer Players")
        static void PlayerCount(int32& PlayerCount);

    /**
    * Find player info using Player Controller.
    *
    * @param    Controller      Player Controller.
    * @param    PlayerInfo      Player info structure.
    */
    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Find Player by Controller", Keywords = "spawn queue player find controller"), Category = "Multiplayer Players")
        static void FindByController(APlayerController* Controller, FPlayerInfo& PlayerInfo);

    /**
    * Update player info.
    *
    * @param    PlayerID                Player ID of player to be edited.
    * @param    UpdateList              Structure of update list.
    * @param    Updated                 Structure of updated player info.
    */
    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Update Player Info", Keywords = "spawn queue player update"), Category = "Multiplayer Players")
        static void UpdatePlayer(FString PlayerID, FUpdatePlayerInfo UpdateList, FPlayerInfo& Updated);

    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Player IP Address", Keywords = "spawn queue player connection info ip"), Category = "Multiplayer Players")
        static void GetPlayerIPAddress(APlayerController* PlayerController, FString& IPAddress);

    UFUNCTION(BlueprintCallable, meta = (DisplayName = "Empty All", Keywords = "spawn queue delete players all"), Category = "Multiplayer Players")
        static void EmptyAll();

    UFUNCTION(BlueprintPure, meta = (DisplayName = "Random String Generator", Keywords = "random string generator"), Category = "Multiplayer Players")
    static void RandomStr(int32 Length, FString& Value);


    /**
    * Date Time to Unix Time Stamp.
    *
    * @param		Value		Date Time.
    * @return					Returns Unix Time Stamp.
    */
    UFUNCTION(BlueprintPure, Category = "UnixTimeLibrary")
       static int64 DateTimetoUnixStamp(struct FDateTime Value);

    /**
    * Unix Time Stamp to Date Time.
    *
    * @param		Value		Unix Time Stamp.
    * @param		TimeOut		Returns Date Time.
    */
    UFUNCTION(BlueprintPure, Category = "UnixTimeLibrary")
       static void UnixTimetoDateTime(int64 Value, FDateTime& TimeOut);

    /**
    * Add seconds to Date Time.
    *
    * @param		Date		Date Time.
    * @param		Seconds		Seconds to add.
    * @param		Result		Returns result.
    */
    UFUNCTION(BlueprintPure, Category = "UnixTimeLibrary")
       static void AddSecondsToDate(FDateTime Date, float Seconds, FDateTime& Result);

protected:

    static bool RemovePlayer(APlayerController* Controller = nullptr, FString PlayerID = FString(TEXT("")));

    static FPlayerInfo Find(APlayerController* Controller = nullptr, FString PlayerID = FString(TEXT("")));

    static TArray<FPlayerInfo> Players;

    static TArray<FPlayerNumber> PlayerNumbers;

    static int32 NextPlayerNum();

    static bool CheckIfPlayerNumIsAvailible(int32 Number);

    static int32 SolveNextPlayerNumber();

    static FPlayerInfo emptyInitReadyPI;
};

ServerPlayers.cpp

Because this is static class we have public variables after #include “ServerPlayers.h”, this is how you initialize them. Also there’s some unix timestamp functions too for widget stuff.

//


#include "ServerPlayers.h"

TArray<FPlayerInfo> UServerPlayers::Players;
TArray<FPlayerNumber> UServerPlayers::PlayerNumbers;
FPlayerInfo UServerPlayers::emptyInitReadyPI;

UServerPlayers::UServerPlayers(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	UServerPlayers::Players.Empty();

	//empty player info initialized for fail searches
	UServerPlayers::emptyInitReadyPI.JoinTime = FDateTime::UtcNow();
	UServerPlayers::emptyInitReadyPI.PlayerID = FString(TEXT(""));
	UServerPlayers::emptyInitReadyPI.PlayerIP = FString(TEXT(""));
	UServerPlayers::emptyInitReadyPI.PlayerName = FString(TEXT(""));
	UServerPlayers::emptyInitReadyPI.PlayerNumber = 0;
	UServerPlayers::emptyInitReadyPI.ServerCharacter = nullptr;
	UServerPlayers::emptyInitReadyPI.ServerController = nullptr;
	UServerPlayers::emptyInitReadyPI.Team = ETeams::Bluefor;

}


PRAGMA_ENABLE_OPTIMIZATION
FPlayerInfo UServerPlayers::AddNewPlayer(APlayerController* ServerController, FString PlayerName, FString PlayerIP, ETeams Team, APawn* ServerCharacter, bool SetControllerID)
{

	FPlayerInfo Player = UServerPlayers::emptyInitReadyPI;
	FPlayerInfo FindResult = UServerPlayers::emptyInitReadyPI;

	if (IsValid(ServerController))
	{
		FTransform Emptylocation = FTransform();
		FString RandomID;
		int32 playernumber = 0;
		UServerPlayers::RandomStr(32, RandomID);

		playernumber = UGameplayStatics::GetPlayerControllerID(ServerController);

		Player.PlayerID = RandomID;
		Player.JoinTime = FDateTime::UtcNow();
		if (!PlayerIP.IsEmpty()) { Player.PlayerIP = PlayerIP; }
		if (!PlayerName.IsEmpty()) { Player.PlayerName = PlayerName; }

		if (playernumber != -1)
		{
			Player.PlayerNumber = playernumber;
		}
		else
		{
			Player.PlayerNumber = UServerPlayers::SolveNextPlayerNumber();

			if (SetControllerID)
			{
				UGameplayStatics::SetPlayerControllerID(ServerController, Player.PlayerNumber);
			}

			FPlayerNumber PlayerNumStruct;
			PlayerNumStruct.PlayerID = RandomID;
			PlayerNumStruct.PlayerNumber = Player.PlayerNumber;

			UServerPlayers::PlayerNumbers.Add(PlayerNumStruct);
		}

		Player.ServerController = ServerController;
		Player.Team = Team;
		if (ServerCharacter != nullptr) { Player.ServerCharacter = ServerCharacter; }

		int32 index;

		index = UServerPlayers::Players.Add(Player);

		UServerPlayers::FindByID(RandomID, FindResult);
	}

	return FindResult;
}

void UServerPlayers::GetPlayerIPAddress(APlayerController* PlayerController, FString& IPAddress)
{
	IPAddress = FString(TEXT(""));

	if (PlayerController != nullptr)
	{
		UNetConnection* Info = PlayerController->GetNetConnection();

		if (Info != nullptr)
		{
			if (IsValid(Info))
			{
				IPAddress = Info->RemoteAddressToString();
			}
		}
	}

}

/*
* Returns next player number.
* Starting from zero.
*
* Player 1 = 0
* Player 2 = 1
*/
int32 UServerPlayers::NextPlayerNum()
{

	int32 index;

	index = UServerPlayers::Players.Num() - 1;

	if (index >= int32(0)) {
		return index + 1;
	}
	else {
		return int32(0);
	}
}

/*
* Check if player number is availible.
*
*/
bool UServerPlayers::CheckIfPlayerNumIsAvailible(int32 Number)
{

	for (auto& playerout : UServerPlayers::PlayerNumbers) {

		if (playerout.PlayerNumber == Number) {

			return false;
		}
	}

	return true;
}

/*
* When players start leaving the server, joining players can take availible number.
*/
int32 UServerPlayers::SolveNextPlayerNumber()
{

	int32 MaxPlayers = 100;
	int32 NumberTrack = 0;
	int32 FirstTry = UServerPlayers::NextPlayerNum();

	if (UServerPlayers::CheckIfPlayerNumIsAvailible(FirstTry))
	{
		return FirstTry;
	}
	else
	{

		for (auto& playerout : UServerPlayers::PlayerNumbers)
		{
			NumberTrack = playerout.PlayerNumber + 1;

			if (UServerPlayers::CheckIfPlayerNumIsAvailible(NumberTrack))
			{
				return NumberTrack;
			}
		}
	}

	return 0;
}

PRAGMA_ENABLE_OPTIMIZATION
bool UServerPlayers::RemovePlayerByController(APlayerController* Controller)
{
	return UServerPlayers::RemovePlayer(Controller, FString(TEXT("")));
}

PRAGMA_ENABLE_OPTIMIZATION
bool UServerPlayers::RemovePlayerByID(FString PlayerID)
{
	return UServerPlayers::RemovePlayer(nullptr, PlayerID);
}

PRAGMA_ENABLE_OPTIMIZATION
void UServerPlayers::PlayerCount(int32& PlayerCount)
{
	PlayerCount = UServerPlayers::Players.Num();
}

PRAGMA_ENABLE_OPTIMIZATION
void UServerPlayers::EmptyAll()
{
	UServerPlayers::Players.Empty();
}

bool UServerPlayers::RemovePlayer(APlayerController* Controller, FString PlayerID)
{

	//if both empty exit with false
	if (PlayerID.IsEmpty() && Controller == nullptr)
	{
		return false;
	}


	FPlayerInfo PlayerInfo = UServerPlayers::emptyInitReadyPI;

	TArray<FPlayerInfo> clone = UServerPlayers::Players;
	int32 indexcount = 0;
	int32 indextorem = 0;
	FString FoundPlayerID;
	bool found = false;

	for (auto& playeropen : clone)
	{
		if (!found && !playeropen.PlayerID.IsEmpty())
		{
			if (playeropen.PlayerID.Equals(PlayerID))
			{
				found = true;
				indextorem = indexcount;

				FoundPlayerID = playeropen.PlayerID;
			}

			if (playeropen.ServerController == Controller)
			{
				found = true;
				indextorem = indexcount;
				FoundPlayerID = playeropen.PlayerID;
			}
		}

		indexcount++;
	}

	if (found)
	{
		UServerPlayers::Players.RemoveAt(indextorem);

		if (!FoundPlayerID.IsEmpty())
		{
			//Find player number and remove
			int32 plnindexrm = 0;
			int32 countplindx = 0;
			bool plindfound = false;

			for (auto& playernout : UServerPlayers::PlayerNumbers)
			{
				if (!plindfound && playernout.PlayerID.Equals(FoundPlayerID))
				{
					plnindexrm = countplindx;
					plindfound = true;
				}

				countplindx++;
			}

			if (plindfound)
			{
				UServerPlayers::PlayerNumbers.RemoveAt(plnindexrm);
			}
		}
	}


	PlayerInfo = UServerPlayers::Find(Controller, PlayerID);

	if (PlayerInfo.PlayerID.IsEmpty())
	{
		return true;
	}
	else return false;

}

FPlayerInfo UServerPlayers::Find(APlayerController* Controller, FString PlayerID)
{

	FPlayerInfo PlayerInfo = UServerPlayers::emptyInitReadyPI;

	bool found = false;

	for (auto& playerout : UServerPlayers::Players)
	{
		if (!PlayerID.IsEmpty())
		{
			if (!found && PlayerID.Equals(playerout.PlayerID)) {

				PlayerInfo = playerout;
				found = true;
			}
		}
		if (Controller != nullptr)
		{
			if (!found && (Controller == playerout.ServerController)) {

				PlayerInfo = playerout;
				found = true;
			}
		}
	}


	return PlayerInfo;
}

PRAGMA_ENABLE_OPTIMIZATION
void UServerPlayers::FindByID(FString PlayerID, FPlayerInfo& PlayerInfo)
{
	PlayerInfo = UServerPlayers::Find(nullptr, PlayerID);
}

PRAGMA_ENABLE_OPTIMIZATION
void UServerPlayers::FindByController(APlayerController* Controller, FPlayerInfo& PlayerInfo)
{
	PlayerInfo = UServerPlayers::Find(Controller, FString(TEXT("")));
}

PRAGMA_ENABLE_OPTIMIZATION
void UServerPlayers::UpdatePlayer(FString PlayerID, FUpdatePlayerInfo UpdateList, FPlayerInfo& Updated)
{
	Updated = UServerPlayers::emptyInitReadyPI;

	FPlayerInfo New = UServerPlayers::emptyInitReadyPI;
	FPlayerInfo Old = UServerPlayers::emptyInitReadyPI;

	UServerPlayers::FindByID(PlayerID, Old);

	if (!Old.PlayerID.IsEmpty())
	{
		New = Old;

		//Update Team
		if (UpdateList.UpdateTeam)
		{
			New.Team = UpdateList.Team;
		}

		//Update Character
		if (UpdateList.UpdateCharacter)
		{
			if (UpdateList.ServerCharacter != nullptr) {
				New.ServerCharacter = UpdateList.ServerCharacter;
			}
		}

		//Update Player Number on Server
		if (UpdateList.UpdatePlayerNum)
		{
			int32 count = 0;
			int32 indexfound = 0;
			bool ifound = false;

			for (auto& playerout : UServerPlayers::PlayerNumbers)
			{
				if (playerout.PlayerID.Equals(PlayerID))
				{
					indexfound = count;
					ifound = true;
				}

				count++;
			}

			if (ifound)
			{
				New.PlayerNumber = UpdateList.PlayerNumber;
				UServerPlayers::PlayerNumbers[indexfound].PlayerNumber = UpdateList.PlayerNumber;
			}
		}

		if (!New.PlayerID.IsEmpty())
		{
			int32 item_index = -1;
			int32 Count = 0;

			for (auto& Item : UServerPlayers::Players)
			{
				if (PlayerID.Equals(Item.PlayerID))
				{
					item_index = Count;
				}

				Count++;
			}


			if (!UServerPlayers::Players[item_index].PlayerID.IsEmpty() && item_index >= 0)
			{
				UServerPlayers::Players[item_index] = New;

				Updated = UServerPlayers::Players[item_index];
			}
		}

	}
}

/*
* Generate Random String
*/
void UServerPlayers::RandomStr(int32 Length, FString& Value)
{
	FString buffer;
	int32 randomint = 0;
	FText ArrayReturnItem{};

	FString alphanum = FString(TEXT("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"));

	for (int32 i = 0; i < Length; ++i) {
		randomint = FMath::RandRange(0, alphanum.Len() - 1);
		buffer += alphanum[randomint];
		randomint = 0;
	}

	Value = buffer;
}

int64 UServerPlayers::DateTimetoUnixStamp(const FDateTime Value)
{
	return Value.ToUnixTimestamp();
}

void UServerPlayers::UnixTimetoDateTime(int64 Value, FDateTime& TimeOut)
{
	TimeOut = FDateTime::FromUnixTimestamp(Value);
}

void UServerPlayers::AddSecondsToDate(FDateTime Date, float Seconds, FDateTime& Result)
{
	float f_milliseconds = FMath::Frac(Seconds) * 10;

	int32 milliseconds = (int32)f_milliseconds;

	FTimespan TimespanAdd = FTimespan(0, 0, 0, Seconds, milliseconds * 1000 * 1000);

	FDateTime NewTime = Date + TimespanAdd;

	Result = NewTime;
}

MPDM_Gamemode.h

In gamemode is where everything comes together. This class must work independently.

In this class we have:

  • Time based spawn queue.
  • FindPlayerStart modification.
  • Player join modification (register player info).
  • StartPlayersInLobby feature.
  • Player kick feature.
  • And in progress of these new features we managed to ruin StartPlayersAsSpectator feature, but that’s alright we don’t need it.

#pragma once

#include "CoreMinimal.h"
#include "Teams.h"
#include "Kismet/KismetSystemLibrary.h"
#include "PlayerInfo.h"
#include "ServerInfo.h"
#include "ServerPlayers.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerStart.h"
#include "CustomPlayerStart.h"
#include "EngineUtils.h"
#include "UObject/ObjectMacros.h"
#include "Misc/Guid.h"
#include "Containers/Queue.h"
#include "Templates/SubclassOf.h"
#include "GameFramework/Info.h"
#include "Engine/ServerStatReplicator.h"
#include "UObject/CoreOnline.h"
#include "GameFramework/GameModeBase.h"
#include "GameFramework/GameSession.h"
#include "GameFramework/PlayerController.h"
#include "Engine/PlayerStartPIE.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/SpectatorPawn.h"
#include "MPDM_Gamemode.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPlayerKickInit, FPlayerKick, KickInfo);

UCLASS()
class AMPDM_Gamemode : public AGameModeBase
{
	GENERATED_BODY()
public:
	AMPDM_Gamemode(const FObjectInitializer& ObjectInitializer);

	/**** Spawn Queue ****/


		/**
	* Add Player to time based queue.
	*
	* @param	SpawnInfo			Spawn Info.
	* @param	Controller			Player Controller.
	* @param	PlayerID			Player id.
	* @param	Class				Class to be spawned (must be a Pawn or Character).
	* @param	SpawnGroup			Spawn Group.
	* @param	AdditionalDelay		Additional Delay.
	* @return						Returns true if add was a success.
	*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Spawn Queue Add", Keywords = "spawn queue list add player"), Category = "SpawnQueue")
		bool SpawnListAdd(FSpawnQueueInfo& SpawnInfo, APlayerController* Controller, FString PlayerID, UClass* Class, ESpawnClass SpawnGroup = ESpawnClass::Lobby, float AdditionalDelay = 0.0);

	/**
	* Get whole list of Spawn Queue. Meant for widget things.
	*
	* @param	SpawnQueue		Spawn Queue.
	*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Spawn Queue Give All", Keywords = "spawn queue give all players from queue"), Category = "SpawnQueue")
		void GiveAllQueue(TArray<FSpawnQueueInfo>& SpawnQueue);

	/**
* Remove player from spawn queue.
*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Spawn Queue Remove Player By Controller", Keywords = "spawn queue list remove player delete controller"), Category = "SpawnQueue")
		bool RemovePlayerByController(APlayerController* Controller);

	/**
	* Remove player from spawn queue.
	*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Spawn Queue Remove Player By ID", Keywords = "spawn queue list remove player delete id"), Category = "SpawnQueue")
		bool RemovePlayerByID(FString PlayerID = FString(TEXT("")));

	/**
	* Check if player is in spawn queue
	*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Is In Spawn Queue", Keywords = "spawn queue list is in"), Category = "SpawnQueue")
		void IsInSpawnQueue(APlayerController* Controller, FString PlayerID, bool& IsInQueue, FTimespan& SpawnTimeLeft, int& NumberInQueue);

	/**
	* Get total queue time length.
	*
	* @param	Length		Queue length in Time Spawn.
	*/
	UFUNCTION(BlueprintPure, meta = (DisplayName = "Queue Time Length", Keywords = "spawn queue length time"), Category = "SpawnQueue")
		void QueueTimeLength(FTimespan& Length);

	/**
	* Get player count of queue.
	*
	* @param	PlayersLeft		Count of Players in Queue.
	*/
	UFUNCTION(BlueprintPure, meta = (DisplayName = "Queue Player Count", Keywords = "spawn queue player count"), Category = "SpawnQueue")
		void QueuePlayerCount(int32& PlayersLeft);

	/**
	* Find Player by Player Controller.
	*
	* @param	Controller		Player Controller.
	* @param	Result			Found Player from spawn queue.
	*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Spawn Queue Find by Controller", Keywords = "spawn queue player find controller"), Category = "SpawnQueue")
		void FindByController(APlayerController* Controller, FSpawnQueueInfo& Result);

	/**
	* Find Player by Player id.
	*
	* @param	PlayerID		Player id.
	* @param	Result			Found Player from spawn queue.
	*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Spawn Queue Find by Player ID", Keywords = "spawn queue player find id"), Category = "SpawnQueue")
		void FindByID(FString PlayerID, FSpawnQueueInfo& Result);

	/**
	* Automated Player Spawner.
	*/
	void QueueSpawner();

	/**
	* Delete all records from spawn queue and join queue.
	*/
	UFUNCTION(BlueprintCallable, meta = (DisplayName = "Wipe", Keywords = "spawn queue wipe clean empty delete"), Category = "SpawnQueue")
		void Wipe();

	/**** Spawn Queue End ****/

	UFUNCTION(BlueprintPure, Category=Game, meta = (DisplayName = "FindPlayerStart"))
	AActor* FindPlayerStartCustom(AController* Player, ETeams Team = ETeams::None, ESpawnClass SpawnGroup = ESpawnClass::None);

	UFUNCTION(BlueprintCallable, Category = Game, meta = (DisplayName = "FindPlayerStart"))
	bool RegisterNewPlayer(APlayerController* Player);

	UFUNCTION(BlueprintCallable, Category = Game, meta = (DisplayName = "Ez Spawn"))
	void EzSpawn(TSubclassOf<APawn> SpawnClass, APlayerController* Controller, FTransform SpawnLocation);

	UFUNCTION(BlueprintCallable, Category = Game, meta = (DisplayName = "Kick Check"))
	void KickCheck();

	/** Confirm that kick message has been received */
	UFUNCTION(BlueprintCallable, Category = Game, meta = (DisplayName = "Kick Confirm"))
	void KickConfirm(APlayerController* Player);

	/** Lobby Spawn Pawn to be used */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayName = "LobbyPawn", Category = Classes))
	TSubclassOf<APawn> LobbyPawn;

	/** When Players enter the server spawn them at lobby location, possesed to lobby pawn */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayName = "Start Players In Lobby", Category = GameMode))
	bool StartPlayersInLobby;

	/** Player Controller on server side will tap into this at the start of game and replicates values to client side, 
	*	then client Player Controller notifies kick message as delivered and changes parameter ReadByPlayerController to true which will trigger a kick action on Server. 
	*/
	UPROPERTY(BlueprintAssignable)
	FPlayerKickInit SendPlayerKickInfo;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayName = "Server Settings", Category = GameMode))
	FServerSettings ServerSettings;

	/** Overrides */
	//UFUNCTION(BlueprintNativeEvent, Category = Game)
	virtual AActor* ChoosePlayerStart_Implementation(AController* Player) override;

	//UFUNCTION(BlueprintNativeEvent, Category = Game)
	virtual void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) override;

	//UFUNCTION(BlueprintNativeEvent, Category = Game)
	virtual AActor* FindPlayerStart_Implementation(AController* Player, const FString& IncomingName) override;

protected:

	/*** Variables ***/

	/** Spawn Queue Variables */
	TArray<FSpawnQueueInfo> spawnlist;
	bool InitTT;

	FSpawnQueueInfo emptyInitReadyPI;


	FTimerHandle MasterTimer_A;
	FTimerHandle MasterTimer_B;
	ETeams TeamFindPlayerStart;
	ESpawnClass FindSpawnGroup;
	TArray<FPlayerKick> KickPlayers;

	//List of players that have used lobby spawn
	TArray<AController*> LobbySpawned;
	bool IsLobbySpawned(AController* Player);

	/*** Functions ***/

	//Spawn queue functions
	TArray<FSpawnQueueInfo> SortByDate(TArray<FSpawnQueueInfo> List);
	bool IsEmpty(FSpawnQueueInfo PlayerQueueInfo);
	bool RemovePlayer(APlayerController* Controller, FString PlayerID);

	//void LobbySpawner(APlayerController* Player);

	virtual void BeginPlay() override;

	virtual void PostLogin(APlayerController* NewPlayer) override;
	
	//Master timers
	void StartMasterTimers();

	
	//This will create player information and keeps JoiningPlayers array clean.
	void InitPlayerLoop();

	//List of undealed players
	TArray<APlayerController*> JoiningPlayers;
};

MPDM_Gamemode.cpp

/*

	GEngine->AddOnScreenDebugMessage(-1, 4.f, FColor::Green, TEXT(""));
*/

#include "MPDM_Gamemode.h"


AMPDM_Gamemode::AMPDM_Gamemode(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.DoNotCreateDefaultSubobject(TEXT("Sprite")))
{

	InitTT = false;

	//empty FSpawnQueue structure for setting values and for comparison
	emptyInitReadyPI.Controller = nullptr;
	emptyInitReadyPI.SpawnCharacter = nullptr;
	emptyInitReadyPI.PlayerID = FString(TEXT(""));
	emptyInitReadyPI.SpawnGroup = ESpawnClass::Death;
	emptyInitReadyPI.Time = FDateTime::UtcNow();


	TeamFindPlayerStart = ETeams::None;
	FindSpawnGroup = ESpawnClass::None;

	LobbySpawned.Empty();

	/* Server Settings */
	ServerSettings.AllowAdminsIfServerFull = true;
	ServerSettings.DontCountAdminsInJoinQueue = true;
	//ServerSettings.Kickable = ;
	//ServerSettings.PrivilegesForSlots = ;
	ServerSettings.ReservedSlots = 2;
	ServerSettings.ServerJoinQueueSize = 10;
	ServerSettings.ServerMaxPlayers = 10;
}

void AMPDM_Gamemode::PostLogin(APlayerController* NewPlayer)
{
	Super::PostLogin(NewPlayer);

	/*
	* Add player into array. In this moment player is entering into a lobby therefore we need an ability to change player's pawn multiple times.
	* Also player can be put into a queue if there is one. This queue can be time based or limited by how many players can be in the gameworld or in teams.
	*/
	JoiningPlayers.Add(NewPlayer);
}

void AMPDM_Gamemode::BeginPlay()
{
	//Call parent
	Super::BeginPlay();

	StartMasterTimers();
}

void AMPDM_Gamemode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
	// If players should start as spectators, leave them in the spectator state
	if (!StartPlayersInLobby && !bStartPlayersAsSpectators && !MustSpectate(NewPlayer) && PlayerCanRestart(NewPlayer))
	{
		// Otherwise spawn their pawn immediately
		RestartPlayer(NewPlayer);
	}
}

AActor* AMPDM_Gamemode::ChoosePlayerStart_Implementation(AController* Player)
{
	//Spawn in lobby if haven't spawned
	if (!IsLobbySpawned(Player) && TeamFindPlayerStart == ETeams::None && FindSpawnGroup == ESpawnClass::None)
	{
		TeamFindPlayerStart = ETeams::Lobby;
		FindSpawnGroup = ESpawnClass::Lobby;

		LobbySpawned.Add(Player);
	}

	APlayerStart* FoundPlayerStart = nullptr;
	TArray<APlayerStart*> UnOccupiedStartPoints;
	TArray<APlayerStart*> OccupiedStartPoints;

	UClass* PawnClass = GetDefaultPawnClassForController(Player);
	APawn* PawnToFit = PawnClass ? PawnClass->GetDefaultObject<APawn>() : nullptr;

	UWorld* World = GetWorld();

	for (TActorIterator<APlayerStart> It(World); It; ++It)
	{
		APlayerStart* PlayerStart = *It;

		//Is Custom Player Start
		if (PlayerStart->IsA<ACustomPlayerStart>())
		{

			ACustomPlayerStart* CustomActor = (ACustomPlayerStart*)PlayerStart;

			if (CustomActor->SpawnGroup == FindSpawnGroup && CustomActor->Team == TeamFindPlayerStart)
			{

			//if (PlayerStart->IsA<APlayerStartPIE>())
			//{
				// Always prefer the first "Play from Here" PlayerStart, if we find one while in PIE mode
				//FoundPlayerStart = PlayerStart;
				//break;
			//}
			//else
			//{
				FVector ActorLocation = PlayerStart->GetActorLocation();
				const FRotator ActorRotation = PlayerStart->GetActorRotation();

				if (!World->EncroachingBlockingGeometry(PawnToFit, ActorLocation, ActorRotation))
				{
					UnOccupiedStartPoints.Add(PlayerStart);
				}
				else if (World->FindTeleportSpot(PawnToFit, ActorLocation, ActorRotation))
				{
					OccupiedStartPoints.Add(PlayerStart);
				}
			}
		}
	}

	if (FoundPlayerStart == nullptr)
	{
		if (UnOccupiedStartPoints.Num() > 0)
		{
			FoundPlayerStart = UnOccupiedStartPoints[FMath::RandRange(0, UnOccupiedStartPoints.Num() - 1)];
		}
		else if (OccupiedStartPoints.Num() > 0)
		{
			FoundPlayerStart = OccupiedStartPoints[FMath::RandRange(0, OccupiedStartPoints.Num() - 1)];
		}
	}

	UnOccupiedStartPoints.Empty();
	OccupiedStartPoints.Empty();

	//Reset
	TeamFindPlayerStart = ETeams::None;
	FindSpawnGroup = ESpawnClass::None;

	return FoundPlayerStart;
}

AActor* AMPDM_Gamemode::FindPlayerStart_Implementation(AController* Player, const FString& IncomingName)
{
	//TeamFindPlayerStart

	UWorld* World = GetWorld();

	// If incoming start is specified, then just use it
	if (!IncomingName.IsEmpty())
	{

	}

	// Always pick StartSpot at start of match
	if (ShouldSpawnAtStartSpot(Player))
	{
		if (AActor* PlayerStartSpot = Player->StartSpot.Get())
		{
			return PlayerStartSpot;
		}
		else
		{
			UE_LOG(LogGameMode, Error, TEXT("FindPlayerStart: ShouldSpawnAtStartSpot returned true but the Player StartSpot was null."));
		}
	}

	AActor* BestStart = ChoosePlayerStart(Player);
	if (BestStart == nullptr)
	{
		// No player start found
		UE_LOG(LogGameMode, Log, TEXT("FindPlayerStart: PATHS NOT DEFINED or NO PLAYERSTART with positive rating"));

		// This is a bit odd, but there was a complex chunk of code that in the end always resulted in this, so we may as well just 
		// short cut it down to this.  Basically we are saying spawn at 0,0,0 if we didn't find a proper player start
		BestStart = World->GetWorldSettings();
	}

	return BestStart;
}

void AMPDM_Gamemode::Wipe()
{
	spawnlist.Empty();
}

AActor* AMPDM_Gamemode::FindPlayerStartCustom(AController* Player, ETeams Team, ESpawnClass SpawnGroup)
{
	TeamFindPlayerStart = Team;
	FindSpawnGroup = SpawnGroup;

	return ChoosePlayerStart(Player);
}

bool AMPDM_Gamemode::RegisterNewPlayer(APlayerController* Player)
{
	FPlayerInfo PlayerInfo;
	FString IPAddress;
	FString debug;

	UServerPlayers::GetPlayerIPAddress(Player, IPAddress);
	PlayerInfo = UServerPlayers::AddNewPlayer(Player, TEXT(""), IPAddress, ETeams::Spectator);

if (!PlayerInfo.PlayerID.IsEmpty())
{

	return true;
}
else
{
	//kick and send error message
	FPlayerKick Error;

	Error.KickMessage = FString(TEXT("Can't generate player info, please try rejoining the Session."));
	Error.KickMessage = FString(debug);
	Error.Kick = true;
	Error.Player = Player;
	Error.Time = FDateTime::UtcNow();

	KickPlayers.Add(Error);
}

return false;
}

void AMPDM_Gamemode::EzSpawn(TSubclassOf<APawn> SpawnClass, APlayerController* Controller, FTransform SpawnLocation)
{
	FActorSpawnParameters SpawnParameters;
	SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;

	UWorld* World = GetWorld();

	if (World == nullptr)
	{
		return;
	}

	APawn* SPawn = World->SpawnActor<APawn>(SpawnClass, SpawnLocation, SpawnParameters);

	AActor* PreviousPawn = Controller->GetPawn();

	Controller->UnPossess();
	Controller->Possess(SPawn);
	
	/** Update Playerinfo */
	/* Doesn't save spectator pawns because spectator pawn doesn't exist in server */
	FPlayerInfo PlayerInfo;
	FPlayerInfo UpdatedPlayerInfo;
	FUpdatePlayerInfo UpdatePlayerList;

	UServerPlayers::FindByController(Controller, PlayerInfo);

	if (!PlayerInfo.PlayerID.IsEmpty())
	{
		UpdatePlayerList.UpdateCharacter = true;
		UpdatePlayerList.ServerCharacter = SPawn;

		UServerPlayers::UpdatePlayer(PlayerInfo.PlayerID, UpdatePlayerList, UpdatedPlayerInfo);
	}
	/**********************/

	if (PreviousPawn != nullptr)
	{
		PreviousPawn->Destroy();
	}
}

void AMPDM_Gamemode::KickCheck()
{
	TArray<int32> DeleteList;
	int32 Count = 0;

	if (KickPlayers.Num() > 0)
	{
		for (auto& Open : KickPlayers)
		{
			if (SendPlayerKickInfo.IsBound())
			{
				SendPlayerKickInfo.Broadcast(Open);

				//Do only one at a time
				//break;
			}

			if (Open.ReadByPlayerController)
			{
				DeleteList.Add(Count);

				//Kick action
				this->GameSession->KickPlayer(Open.Player, FText::FromString(Open.KickMessage));
			}

			Count++;
		}
	}

	if (DeleteList.Num() > 0)
	{
		for (auto& DelItem : DeleteList)
		{
			KickPlayers.RemoveAt(DelItem);
		}
	}

	DeleteList.Empty();
}

void AMPDM_Gamemode::KickConfirm(APlayerController* Player)
{
	int32 Count = 0;
	int32 index;
	bool found = false;

	if (KickPlayers.Num() > 0)
	{
		for (auto& Open : KickPlayers)
		{
			if(Open.Player == Player)
			{
				index = Count;
				found = true;
			}

			Count++;
		}

		if (found)
		{
			KickPlayers[index].ReadByPlayerController = true;
		}

	}

}

void AMPDM_Gamemode::StartMasterTimers()
{
	if (::IsValid(GetWorld()))
	{
		
		GetWorld()->GetTimerManager().SetTimer(MasterTimer_A, this, &AMPDM_Gamemode::InitPlayerLoop, 0.1f, true);
		GetWorld()->GetTimerManager().SetTimer(MasterTimer_B, this, &AMPDM_Gamemode::QueueSpawner, 0.05f, true);
	}
}

void AMPDM_Gamemode::InitPlayerLoop()
{
	TArray<int32> RemoveByIndex;

	TArray<APlayerController*> JoiningControllersRemove;

	for (auto& OpenJoiningItem : JoiningPlayers)
	{
		if (RegisterNewPlayer(OpenJoiningItem))
		{
			JoiningControllersRemove.Add(OpenJoiningItem);
		}
	}

	int32 count_a = 0;
	int32 foundindex = 0;

	for (auto& OpenRMItem : JoiningControllersRemove)
	{
		//Failed to remove
		if (JoiningPlayers.Remove(OpenRMItem) == 0)
		{
			//find and remove by index
			if (JoiningPlayers.Find(OpenRMItem, foundindex))
			{
				JoiningPlayers.RemoveAt(foundindex);
			}
		}

		count_a++;
	}

	int32 count_b = 0;

	//Check if someone left in the middle of joining process
	for (auto& OpenJoiningToRMItem : JoiningPlayers)
	{
		if (!IsValid(OpenJoiningToRMItem) || OpenJoiningToRMItem->IsPendingKill())
		{
			RemoveByIndex.Add(count_b);
		}
		count_b++;
	}

	for (auto& OpenRMindex : RemoveByIndex)
	{
		JoiningPlayers.RemoveAt(OpenRMindex);
	}

	//Buffers can be cleaned
	JoiningControllersRemove.Empty();
	RemoveByIndex.Empty();

	KickCheck();
}

bool AMPDM_Gamemode::RemovePlayer(APlayerController* Controller, FString PlayerID)
{
	//if both empty exit with false
	if (PlayerID.IsEmpty() &&
		Controller == nullptr) {
		return false;
	}

	int32 indextt = 0;
	int32 indextoremove = 0;
	bool found = false;

	for (auto& spawnopen : spawnlist)
	{

		if (!found)
		{
			if (!PlayerID.IsEmpty())
			{
				if (spawnopen.PlayerID.Equals(PlayerID))
				{
					indextoremove = indextt;
					found = true;
				}
			}

			if (Controller != nullptr)
			{
				if (spawnopen.Controller == Controller)
				{
					indextoremove = indextt;
					found = true;
				}
			}
		}

		indextt++;
	}

	if (found)
	{
		spawnlist.RemoveAt(indextoremove);
	}

	FSpawnQueueInfo PlayerRecheck;

	if (!PlayerID.IsEmpty())
	{
		FindByID(PlayerID, PlayerRecheck);
	}
	else
	{
		FindByController(Controller, PlayerRecheck);
	}

	if (IsEmpty(PlayerRecheck)) {
		return true;
	}
	return false;
}

bool AMPDM_Gamemode::SpawnListAdd(FSpawnQueueInfo& SpawnInfo, APlayerController* Controller, FString PlayerID, UClass* Class, ESpawnClass SpawnGroup, float AdditionalDelay)
{
	FDateTime TimeNow;

	//Clear spawn queue when game begins
	if (!InitTT)
	{
		spawnlist.Empty();
		InitTT = true;
	}

	//Separate milliseconds from seconds also add 10 millisecond delay so we get queue
	float f_milliseconds = FMath::Frac((AdditionalDelay + 0.1)) * 10;

	//Convert to int32
	int32 milliseconds = (int32)f_milliseconds;

	//Make timespan
	FTimespan TimeSpan = FTimespan(0, 0, 0, AdditionalDelay, milliseconds * 1000 * 1000);

	//Temporal spawn queue structure
	FSpawnQueueInfo MkPlayerSpawnQueue;

	MkPlayerSpawnQueue.Controller = Controller;
	MkPlayerSpawnQueue.PlayerID = PlayerID;
	MkPlayerSpawnQueue.Time = TimeNow.UtcNow() + TimeSpan;
	MkPlayerSpawnQueue.SpawnGroup = SpawnGroup;
	MkPlayerSpawnQueue.SpawnCharacter = Class;

	int32 return_index = 0;

	return_index = spawnlist.Add(MkPlayerSpawnQueue);


	FSpawnQueueInfo Player = emptyInitReadyPI;

	if (!PlayerID.IsEmpty())
	{
		FindByID(PlayerID, Player);
	}
	else
	{
		FindByController(Controller, Player);
	}
	//Return found
	SpawnInfo = Player;

	return true;
}

void AMPDM_Gamemode::GiveAllQueue(TArray<FSpawnQueueInfo>& SpawnQueue)
{
	SpawnQueue = spawnlist;
}

bool AMPDM_Gamemode::RemovePlayerByController(APlayerController* Controller)
{
	return RemovePlayer(Controller, TEXT(""));;
}

bool AMPDM_Gamemode::RemovePlayerByID(FString PlayerID)
{
	return RemovePlayer(nullptr, PlayerID);
}

void AMPDM_Gamemode::IsInSpawnQueue(APlayerController* Controller, FString PlayerID, bool& IsInQueue, FTimespan& SpawnTimeLeft, int& NumberInQueue)
{
	//initialize and reset returning values
	NumberInQueue = 9999;
	SpawnTimeLeft = FTimespan::Zero();
	IsInQueue = false;

	FSpawnQueueInfo Player = emptyInitReadyPI;
	FDateTime TimeNow = FDateTime::UtcNow();

	
	if (!PlayerID.IsEmpty())
	{
		FindByID(PlayerID, Player);
	}
	else
	{
		FindByController(Controller, Player);
	}

	//Find result is empty
	if (Player.PlayerID.IsEmpty())
	{
		IsInQueue = false;
	}
	//is in queue
	else
	{
		IsInQueue = true;
		/*
		now: 15:11
		spawn time 15:15
		spawn time - now =
		*/
		SpawnTimeLeft = Player.Time - TimeNow;
		TArray<FSpawnQueueInfo> Result = SortByDate(spawnlist);

		int32 item_index = 0;
		int32 item_count = 0;
		bool found = false;

		for (auto& spawnout : Result)
		{

			if (!found)
			{
				if (spawnout.PlayerID.Equals(Player.PlayerID))
				{
					item_index = item_count;
					found = true;
				}
			}

			item_count++;
		}

		//add one so zero is not first
		NumberInQueue = (item_index + 1);
	}
}

void AMPDM_Gamemode::QueueTimeLength(FTimespan& Length)
{
	Length = FTimespan();

	FDateTime First = FDateTime();
	FDateTime Last = FDateTime();

	int32 lastitem = (spawnlist.Num() - 1);
	int32 count = 0;

	for (auto& spawnout : spawnlist) {

		if (count == int32(0)) {
			spawnout.Time = First;
		}

		if (count == lastitem) {
			spawnout.Time = Last;
		}

		count++;
	}

	Length = Last - First;
}

void AMPDM_Gamemode::QueuePlayerCount(int32& PlayersLeft)
{
	PlayersLeft = spawnlist.Num();
}

void AMPDM_Gamemode::FindByController(APlayerController* Controller, FSpawnQueueInfo& Result)
{
	int32 count = 0;

	Result = emptyInitReadyPI;

	for (auto& spawnout : spawnlist)
	{
		if (Controller != nullptr) {
			if (Controller == spawnout.Controller)
			{
				Result = spawnout;
			}
		}

		count++;
	}
}

void AMPDM_Gamemode::FindByID(FString PlayerID, FSpawnQueueInfo& Result)
{
	int32 count = 0;

	Result = emptyInitReadyPI;

	for (auto& spawnout : spawnlist)
	{
		if (PlayerID.Equals(spawnout.PlayerID))
		{
			Result = spawnout;
		}

		count++;
	}
}

bool AMPDM_Gamemode::IsLobbySpawned(AController* Player)
{

	if (StartPlayersInLobby)
	{
		for (auto& LobbySPItem : LobbySpawned)
		{
			if (LobbySPItem == Player)
			{
				return true;
			}
		}
	}

	return false;
}

TArray<FSpawnQueueInfo> AMPDM_Gamemode::SortByDate(TArray<FSpawnQueueInfo> List)
{

	/*
	------- Sorting Swap Strategy -------

	Timelist:
	15:15 compare this and find bigger, when bigger value has been found, swap
	09:20 swap this with 17:55, second loop
	22:01 swap this with 15:15, first loop
	17:55

	to

	22:01
	17:55
	15:15
	09:20

	for() {
		if (a > b) {
			swap (replace_item, with_this)
		}
	}

	*/

	bool swapped;

	TArray<FSpawnQueueInfo> clone = spawnlist;

	do {
		swapped = false;

		for (int i = 0; i < (clone.Num() - 1); i++) {

			if (clone[i].Time > clone[i + 1].Time) {

				clone.Swap(i, (i + 1));
				swapped = true;
			}

		}
	} while (swapped);

	return clone;
}

bool AMPDM_Gamemode::IsEmpty(FSpawnQueueInfo PlayerQueueInfo)
{
	//empty player id or controller
	if (PlayerQueueInfo.PlayerID.IsEmpty() ||
		PlayerQueueInfo.Controller == nullptr) {
		return true;
	}

	return false;
}

void AMPDM_Gamemode::QueueSpawner()
{
	AActor* SpawnLocation;
	FPlayerInfo PlayerInfo;
	TArray<FString> RemovePlayersList;

	int32 PlayerCount = 0;
	UServerPlayers::PlayerCount(PlayerCount);

	//FServerSettings ServerSettings = UServerPlayers::ServerSettings;

	//This is still holding data from previous round
	RemovePlayersList.Empty();

	FDateTime Time;

	for (auto& spawnopen : spawnlist) {

		if (!spawnopen.PlayerID.IsEmpty())
		{
			//if time now is more then it's spawn time
			if (Time.UtcNow() >= spawnopen.Time)
			{
				UServerPlayers::FindByController(spawnopen.Controller, PlayerInfo);

				SpawnLocation = FindPlayerStartCustom(spawnopen.Controller, PlayerInfo.Team, spawnopen.SpawnGroup);
				
				if (SpawnLocation != nullptr)
				{
					EzSpawn(spawnopen.SpawnCharacter, spawnopen.Controller, SpawnLocation->GetActorTransform());

					RemovePlayersList.Add(PlayerInfo.PlayerID);
				}
				else
				{
					//Todo: add here your own solution for not found Spawn Point
					GEngine->AddOnScreenDebugMessage(-1, 4.f, FColor::Red, TEXT("Cannot Spawn Character. No Spawn Point Found. Please Add CustomPlayerStart in map and set Spawn Group."));
				}
			}

		}
	}

	for (auto& RMItem : RemovePlayersList)
	{
		RemovePlayerByID(RMItem);
	}
}

Once you have Compiled it, make a new blueprint gamemode class, search for: MPDM_Gamemode:

Also create SpectatorPawn and name it as NoMovementPawn:

Sometimes possessed pawns don’t spawn in correct rotation so we do something like this.

Gamemode class default settings.

Compiler Problems

  • Make sure all Enums Start with E Letter.
  • Same thing with Structures make sure they start with Letter F.
  • If everything fails find MyProject.uproject file and right click -> Generate visual studio project files…
  • Check the Output for errors.
Pages: 1 2 3 4 5
Subscribe
Notify of
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

[…] Part 2 – Deathmatch Gamemode […]

Level Paradox
1
0
Would love your thoughts, please comment.x
()
x