First-Person Shooter: Player’s respawn

Tutorial: First-Person Shooter in Unity - 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:

First-Person Shooter

Then, add another Empty Game Object and add the PlayersSpawner script to it:

First-Person Shooter

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.

First-Person Shooter First-Person Shooter

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.

First-Person Shooter

In the next part we’ll create health bar and death screen ❤️❤️🖤


Last modified December 14, 2022: Part 9 and part 10 (35603f8)