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
}
}
Game
ClassNext, 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
ClassProblem 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 SolutionStartTurn
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
Classpublic 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
ClassWe 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.

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