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
- ESpawnClass, This is a list of Spawn Groups. Useful when you need characters and vehicles in different locations.
- EQueuePrivileges, Server Join Queue privileges.
- ESpawnQueueAddStatus
- 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.
- EReservedAuthorityBranch, reserved slots enter authority status.
- 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).
- FUTCTimeDifference, for UTC clock time sync feature. This will store information about server and client time difference.
- FPlayerKick, Player kick information holder.
- 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, &DM_Gamemode::InitPlayerLoop, 0.1f, true);
GetWorld()->GetTimerManager().SetTimer(MasterTimer_B, this, &DM_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.
[…] Part 2 – Deathmatch Gamemode […]