Player system that handles multiple player types using inheritance

Various types of players such as offline players, bots, online players exist in the game. Each player has their own way of handling the same job.

While developing the game, we may have encountered the problem of managing multiple player types. Various types of players such as offline players, bots, online players exist in the game. Each player has their own way of handling the same job.

Approach 'A'
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 as:

  • 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 his turn.
Let us make a Game the class that will handle the turn of the player. When a player plays a number, this class will find the next player and start his turn. We will make a class Game 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
	}
}

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

Let us make 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…
	}
}

Problem with our Approach 'A'

With this approach code, it is not scalable. Say 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 make changes to all those places.

Next issue we will encounter using this approach is all player types handles problem differently. For example, the human player waits for user input in Update() a method to get numbers, bots may implement its logic inside the coroutine, OnlinePlayer 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 type everywhere in code. This makes code filled with if(playerType==“something”) checks, making code unnecessarily long, unreadable, unscalable. 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.

Solution using inheritance (Approach 'B')

To implement inheritance in the player system we need a base class. Let us 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
}

StartTurn and EndTurn methods are called by Game class when the player’s turn starts and end. 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.TakeAction(int) the method is called by child class when it gets number to play. This method is a protected type. It means 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
	//...
}
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
}

We can easily add other player types in a similar way. Even when implementing complex player type, as all the player specific code remains in a separate class it doesn’t complicate game logic or other types of player. As a base class for every player type is the same, every player type can be referenced using Player type and the game can just use 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.

As we can see from this example, 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, multiple enemy types, multiple weapon systems etc. Just make a base class and derive child class to produce its multiple types. This same pattern can be used using interface but unity doesn't allow drag and drops for the interface so it is preferable to use inheritance.