We may encounter the problem of managing multiple player types when developing a game. Various types of players, such as offline players, bots, and online players, exist in the game. Each player has their own way of handling the same job.

Approach A: Classic Method

For example, let us consider the task of choosing a card to throw in a card game. This will be handled by different player types:

  • The offline player will get a card from user input.
  • Bots will run some type of logic to find the right card
  • Similarly, an online player may need to wait for a message from the server to know which card to throw.

Let us take an example game and implement the player system in it. This will be a turn-based game where the player needs to play a number in their turn.

Start by making a Game class that will handle the player turn. When a player plays a number, this class will find the next player and start his turn.

We will make a Game class as follows:

public class Game:MonoBehaviour{

	public List<Player> players;//list of players in game
	public Player currentPlayer;//player who is currently playing

	//this method will be called when player plays a number
	public void PlayNumber(int number){
		Debug.Log(number);
		currentPlayer.EndTurn();//end current player’s turn

		Player nextPlayer;
		//find next player
		nextPlayer.StartTurn();//start next player turn
	}
}
Example of a Game Class

Next, we have to make a player class. This player class should be able to handle all kinds of players.

So, we create a player class without inheritance. We will handle different player types using if() statements and perform the action accordingly. This method has issues that will be discussed below.

public class Player:MonoBehavior{

	public Game game;//reference to game class
	public string playerType;

	void StartTurn(){
		int number=0;
		if(playerType==“offlinePlayer”){
			//enable input for user and wait for user to enter a number. print number after receiving user input
			game.PlayNumber(number);
		}else if(playerType==“bot”){
			//do some bot logic and produce a number.print this number
			game.PlayNumber(number);
		}else if(playerType==“onlinePlayer”){
			//wait for number from server. print that number after receiving it
			game.PlayNumber(number);
		}
		// check for all player types and perform action…
	}

	void EndTurn(){
		if(playerType==“offlinePlayer”){
			//disable input for user
		}else if(playerType==“bot”){
			//reset logic status and other bot related stuff
		}else if(playerType==“onlinePlayer”){
			//stop listening message from server
		}
		// check for all player types and perform action…
	}
}
Player:MonoBehaviour Class

Problem in Approach A

The code is not scalable with this approach. Suppose we want to add a new type of bot player for tutorial levels. Using this approach, we will have to add a new if() statement in start turn as:

if(playerType==“tutorialBot”){
	//do tutorial bot logic and produce a number
	game.PlayNumber(number);
}

Considering our player script has multiple methods that do player type checks, we will have to change all those places.

The next issue we will encounter using this approach is that all player types handle problems differently. For example, the human player waits for user input in the Update() method to get numbers, bots may implement their logic inside the coroutine, the online player may wait for messages from the server in the callback function, and so on.

With this approach, we must implement all those codes in a single class and check player types everywhere in the code. The if(playerType==“something”) code checks unnecessarily long, unreadable, and unscalable codes.

This problem can be solved using inheritance. We need to break the functionality of each type of player in the player class into individual child classes of a player class.

Approach B: Solution Using Inheritance

To implement inheritance in the player system, we need a base class.

Make a Player class that will be inherited by the child class to make different types of players. This base class will have methods and variables that are common for all types of players. We can use the previous Game class to implement this class.

public abstract class Player:MonoBehavior{

	//common variables
	public Game game;//reference to game class
	public string name;
	public int playerId;
	protected bool isPlayerTurn;
	//..and other common variables

	//common methods
	public virtual void StartTurn(){
		isPlayerTurn=true;//set player’s turn status
	}

	public virtual void EndTurn(){
		isPlayerTurn=false;//set player’s turn status
	}

	protected void Play(int number){
		//do desired action. in our case print number
		game.PlayNumber(number);
	}

	//..and other methods
}
Player:Monobehaviour Class as a Solution

StartTurn and EndTurn methods are called by the Game class when the player's turn starts and ends. Notice that they both are virtual methods, so they can be overridden by the child class. This means each type of player can override these in their way to perform player type-specific action.

The TakeAction(int) method is called by the child class when it gets a number to play. This method is a protected type. This method can be called from class and its child class only. Also, our base class is of abstract type. This means the object of this class cannot be made directly. It must first be inherited by the child class, and the object of that child class can then be made.

We finished setting up our base class. Now let's start making different types of players for our game.

public class OfflinePlayer:Player{

	//offline player specific variables
	public string userName;
	public string userLevel;
	//and other offline player specific variables

	//offline player will check user input to get number to play
	void Update(){
		if(isPlayerTurn){
			//wait for player input and play number after getting it
			Play(number);
		}
	}

	//overriding StartTurn method to take action at start of turn
	public override void StartTurn(){
		base.StartTurn();//call base method(run parent’s method)

		//do player specific actions
		//show player’s turn in UI
		//enable player’s input
		//other stuffs..
	}

	//overriding EndTurn method to take action at end of turn
	public override void EndTurn(){
		base.EndTurn();//call base method

		//do player specific actions
		//show player’s end turn in UI
		//notify player his turn has ended
		//disable player’s input
		// and other things..
	}

	//other offline player related methods
	//...
}
OfflinePlayer Class
public class BotPlayer:Player{

	//bot player specific variables
	int difficultyLevel;
	//and other bot specific variables

	public override void StartTurn(){
		base.StartTurn();//call base method(run parent’s method)

		StartCroutine(StartBotLogic());
		//do bot specific actions
		//make UI changes
		//other stuffs..
	}

	public override void EndTurn(){
		base.EndTurn();//call base method

		//do bot specific actions
		//make UI changes
		// and other things..
	}

	//Algorithm to choose number will run in coroutine
	IEnumerator StartBotLogic(){
		//generate number from bot logic
		Play(generatedNumber);//call protected method to play number
	}
    
	//..and other bot player related methods
}
BotPlayer Class

We can similarly add other player types. As all the player-specific code remains in a separate class, implementing complex player types or other types of players doesn't complicate game logic.

As the base class for every player type is the same, every player type is referenced using Player type, and the game uses base methods without needing to know the type of player. We can also simply drag and drop players into game class without taking care of player type.

Types of Player

The example above shows that our game system can handle multiple player types without including code in a single class.

This pattern is not only limited to the player system. It can be used for making systems such as multiple types of animation, player class and variants in-game, enemy types, and weapon systems.

Make a base class and derive child class from producing its multiple types. This same pattern can be used using an interface, but Unity doesn't allow drag and drop, so inheritance is preferable.

Thank you for reading this blog! Please leave a comment below