First-Person Shooter: Player’s respawn
This is Elympics First-Person Shooter tutorial: part 8. In this part we’ll be handling player’s respawn. See: Part 7.
Players provider class
Players can damage each other with different types of weapons, but to create a full gameplay loop, you need to be able to respawn them. To do this, you’ll have to create a PlayerSpawner
object that will be responsible for placing players in the appropriate positions at the start of the game and resetting them after their death.
Before moving on to the proper implementation, let’s look at the assumptions for our PlayersSpawner
:
- it has several positions (spawn points) defining where player can be moved to;
- it’s executed at the start of the game (assigns each player a starting position) and it also assigns a new starting position to a given player on the
DeathController
's request (once the player's respawn time has expired); - it’s performed only on the server (it’s related to the randomization of each spawn point).
The PlayerSpawner
will be a script placed on a separate Empty Game Object in the game scene. To allow it to access information about all the players in the game, you’ll have to create a special PlayersProvider
class that will provide a reference to all the players in the scene as well as a reference to the player currently controlled by the client. Thanks to this solution, you won’t have to manually assign references to each player (this way, you eliminate the risk of error resulting from e.g. a missing reference), and you'll also be able to universally use this class in other scripts that will focus on controlling the game and will require information about e.g. individual players.
So, create a new PlayersProvider.cs
class:
public class PlayersProvider : ElympicsMonoBehaviour, IInitializable
{
public PlayerData ClientPlayer { get; private set; } = null;
public PlayerData[] AllPlayersInScene { get; private set; } = null;
public bool IsReady { get; private set; } = false;
public event Action IsReadyChanged = null;
public void Initialize()
{
FindAllPlayersInScene();
FindClientPlayerInScene();
IsReady = true;
IsReadyChanged?.Invoke();
}
private void FindAllPlayersInScene()
{
this.AllPlayersInScene = FindObjectsOfType<PlayerData>().OrderBy(x => x.PlayerId).ToArray();
}
private void FindClientPlayerInScene()
{
foreach (PlayerData player in AllPlayersInScene)
{
if ((int)Elympics.Player == player.PlayerId)
{
ClientPlayer = player;
return;
}
}
//Fix for server side.
ClientPlayer = AllPlayersInScene[0];
}
public PlayerData GetPlayerById(int id)
{
return AllPlayersInScene.FirstOrDefault(x => x.PlayerId == id);
}
}
This class has a reference to all the players in the scene, as well as to the specific player controlled by the client in the game. The Initialize method searches for all player objects in the scene using FindObjectsOfType<PlayerData>()
.
The PlayersProvider
class also has an appropriate IsReady
variable as well as an event related to this variable, so that other classes that want to use PlayersProvider
in the Initialize
method can be sure that all the references are properly fetched and assigned.
While iterating through all the player references found in FindAllPlayersInScene
, the FindClientPlayerInScene
method compares and saves the player currently controlled by the client. For the server, ClientPlayer
will always be null because the server side doesn’t have a valid value for Elympics.Player
. For this reason, at the end of the method, if no player matching the ClientPlayer
variable is found, it’s assigned the first player in the scene.
Spawner
Once you create the PlayersProvider
class, you can move on to creating the proper spawner class:
public class PlayersSpawner : ElympicsMonoBehaviour, IInitializable
{
[SerializeField] private PlayersProvider playersProvider = null;
[SerializeField] private Transform[] spawnPoints = null;
private System.Random random = null;
public static PlayersSpawner Instance = null;
private void Awake()
{
if (PlayersSpawner.Instance == null)
PlayersSpawner.Instance = this;
else
Destroy(this);
}
public void Initialize()
{
if (!Elympics.IsServer)
return;
random = new System.Random();
if (playersProvider.IsReady)
InitialSpawnPlayers();
else
playersProvider.IsReadyChanged += InitialSpawnPlayers;
}
private void InitialSpawnPlayers()
{
var preparedSpawnPoints = GetRandomizedSpawnPoints().Take(playersProvider.AllPlayersInScene.Length).ToArray();
for (int i = 0; i < playersProvider.AllPlayersInScene.Length; i++)
{
playersProvider.AllPlayersInScene[i].transform.position = preparedSpawnPoints[i].position;
}
}
public void SpawnPlayer(PlayerData player)
{
Vector3 spawnPoint = GetSpawnPointWithoutPlayersInRange().position;
player.transform.position = spawnPoint;
}
private Transform GetSpawnPointWithoutPlayersInRange()
{
var randomizedSpawnPoints = GetRandomizedSpawnPoints();
Transform chosenSpawnPoint = null;
foreach (Transform spawnPoint in randomizedSpawnPoints)
{
chosenSpawnPoint = spawnPoint;
Collider[] objectsInRange = Physics.OverlapSphere(chosenSpawnPoint.position, 3.0f);
if (!objectsInRange.Any(x => x.transform.root.gameObject.TryGetComponent<PlayerData>(out _)))
break;
}
return chosenSpawnPoint;
}
private IOrderedEnumerable<Transform> GetRandomizedSpawnPoints()
{
return spawnPoints.OrderBy(x => random.Next());
}
}
The class needs the following references to work properly:
PlayersProvider
which will give it access and information about all the players in the game;- Table of
Transform[]
spawn points which is the positions that will be assigned to players during spawn / respawn.
You’ll also need to store a System.Random
reference that you use when selecting a new random spawn point.
The PlayersSpawner
class is a singleton (Awake
method) due to the fact that selected part of its code will be called by DeathController.cs
of the individual players.
In the Initialize
function, you start by checking whether your game is a server (according to the previously saved assumptions). Depending on whether PlayersProvider
is already initialized, you call the InitialSpawnPlayers
method. If not, you execute it by subscription once PlayersProvider
is ready.
The InitialSpawnPlayers
function selects as many random spawn points as there are players in the scene using the GetRandomizedSpawnPoints
method (this method returns all the available spawn points in random order). Then, each player's position (transform.position) is changed to the position of their spawn point. Thanks to it, at the beginning of the game, each player will start the game in a different, random place.
In the case of this method, it’s not necessary to perform any additional actions: as this method is performed at the start of the game, we’re sure that there will be no other player near the drawn spawn point.
The situation is different when you want to reset the player's position after their death: the drawn point may already be occupied by another player during the game. For this reason, in the SpawnPlayer
method (called ultimately by DeathController.cs
), you’ll need to use the GetSpawnPointWithoutPlayersInRange
method that returns a random spawn point after making sure that there’s no other player in its given area. The SpawnPlayer
method itself works very similarly to the InitialSpawnPlayers
method except that it assigns a spawn point to only one player passed as its PlayerData
argument.
Once you prepare the scripts, you can proceed to attaching them to your scene. You’ll need to create an Empty Game Object with the PlayersProvider
script in it:
Then, add another Empty Game Object and add the PlayersSpawner
script to it:
For the above script to work properly, you’ll need to supplement it with appropriate references. Drag the PlayersProvider
object and create several Empty Game Objects that will act as Spawn Points.
The last step is to modify the DeathController.cs
script so that the player will be respawned once a certain amount of time has elapsed after their death:
[...]
public void ElympicsUpdate()
{
if (!IsDead || !Elympics.IsServer)
return;
CurrentDeathTime.Value -= Elympics.TickDuration;
if (CurrentDeathTime.Value <= 0)
{
RespawnPlayer();
}
}
private void RespawnPlayer()
{
PlayersSpawner.Instance.SpawnPlayer(playerData);
PlayerRespawned?.Invoke();
IsDead.Value = false;
}
It's deathmatch with no score limit!
From now on, your players will start the game from a random spawn point, and, after their death, they will be properly moved and respawn at a random spawn point.
In the next part we'll create health bar and death screen ❤️❤️🖤