First-Person Shooter: Players’ scores

Tutorial: First-Person Shooter in Unity - Controlling the gameplay: Players’ scores

This is Elympics First-Person Shooter tutorial: part 10. In this part we’ll be controling the gameplay by managing players’ scores. See: Part 9.

The gameplay you’ve created in your project will last forever: players will fight with each other, and when one of them dies, they’ll respawn after a given time. The next step is to add a script that will control the game, i.e. track the statistics of the players and end the match when one of them scores a defined number of points.

Player Scores Manager

Start by creating a new class, PlayersScoresManager.cs, which will be responsible for the gameplay aspects described above.

public class PlayerScoresManager : ElympicsMonoBehaviour, IInitializable
{
	[SerializeField] private PlayersProvider playersProvider = null;
	[SerializeField] private int pointsRequiredToWin = 10;

	private ElympicsArray<ElympicsInt> playerScores = null;
	public ElympicsInt WinnerPlayerId { get; } = new ElympicsInt(-1);

	public bool IsReady { get; private set; } = false;
	public event Action IsReadyChanged = null;

	public void Initialize()
	{
		if (playersProvider.IsReady)
			SetupManager();
		else
			playersProvider.IsReadyChanged += SetupManager;
	}

	private void SetupManager()
	{
		PreparePlayerScores();
		SubscribeToDeathControllers();

		IsReady = true;
		IsReadyChanged?.Invoke();
	}

	private void SubscribeToDeathControllers()
	{
		foreach (PlayerData playerData in playersProvider.AllPlayersInScene)
		{
			if (playerData.TryGetComponent(out DeathController deathController))
			{
				deathController.HasBeenKilled += ProcessPlayerDeath;
			}
		}
	}

	private void ProcessPlayerDeath(int victim, int killer)
	{
		if (victim == killer)
			playerScores.Values[killer].Value--;
		else
			playerScores.Values[killer].Value++;

		if (Elympics.IsServer && playerScores.Values[killer].Value >= pointsRequiredToWin)
		{
			WinnerPlayerId.Value = killer;
		}
	}

	private void PreparePlayerScores()
	{
		var numberOfPlayers = playersProvider.AllPlayersInScene.Length;

		ElympicsInt[] localPlayerScoresArray = new ElympicsInt[numberOfPlayers];

		for (int i = 0; i < numberOfPlayers; i++)
		{
			localPlayerScoresArray[i] = new ElympicsInt(0);
		}

		playerScores = new ElympicsArray<ElympicsInt>(localPlayerScoresArray);
	}
}

The task of this class is to control and manage the points of each player. To get information about all of them, you’ll need references to PlayersProvider once again. In addition, you’ll need to specify the maximum score leading to the end of the match. You’ll be able to modify in the editor.

The scores (ElympicsInt type) of individual players are stored in a synchronized ElympicsArray. This class also stores information about the id of the player who won the game. Thanks to the use of the ElympicsInt synchronized variable, in the future, the components responsible for displaying the summary screen will be able to use ValueChanged to display information about the winner.

Just like in the case of the PlayersProvider class, this class also has the appropriate IsReady flag and provides an event so that other classes that want to use PlayerScoresManager.cs can be sure that the script is fully initialized.

At the start of the game, in the initialize method, the SetupManager() method which requires a fully initialized PlayersProvider class is properly executed. The SetupManager() method prepares the entire manager accordingly:

First, the PreparePlayerScores() method is called to fully initialize the synchronized playerScores table.

Then, the SubscribeToDeathControllers() method is called. In this method, you iterate over all the players in the scene to be able to assign your manager’s main method, ProcessPlayerDeath, to the local event of each player’s DeathController component.

It is the ProcessPlayerDeath method that is the main pillar of your manager. It takes two arguments acting as the id of the players: the first id belonging to the defeated player and the second being the id of the player who scored the point.

Before you increase the score of a specific player, you’ll need to check whether the received arguments have the same value. If that’s the case, it means that the player self-inflicted damage leading to their death. Such a case should not generate a point. On the contrary: if the player causes their own death, they lose a point.

After modifying the points of this player, you check whether this player has reached the amount needed to end the game (in the case of increasing the points value). To avoid errors caused by incorrect prediction, this action is performed only on the server. If the player reaches the required number of points, their id is saved to the WinnerPlayerId variable, and clients synchronize this value with the next snapshot they receive.

Just like in the case of PlayersProvider and PlayersSpawner, the last step is to create an Empty Game Object in the scene and add the newly created script to it with references:

First-Person Shooter

If you want to test whether your class works correctly in its current state, you can add a simple Debug.Log to your code that will write on the server information about a possible modification of the WinnerPlayerId variable:

PlayersScoresManager.cs:

if (Elympics.IsServer && playerScores.Values[killer].Value >= pointsRequiredToWin)
{
	WinnerPlayerId.Value = killer;
	Debug.Log("Winner: " + WinnerPlayerId);
}

And the winner is…

After defeating one of the players a certain number of times, you should see the following message on the console:

First-Person Shooter

Now, your game monitors the gameplay. The script you’ve created will be a good starting point for subsequent scripts responsible for displaying the scoreboard and the proper end of the match.

In the next part we’ll expand controling the gameplay by adding start and end of the match. 🔧


Last modified December 14, 2022: Last chapters of fps tutorial added (57343b4)