Understanding State Design Pattern By Implementing Finite State Machine In Super Mario Game

In object oriented programming State Pattern is one of the way to implement Finite State Machines. This pattern falls under Behavioral Design Patterns.

State Design Pattern - Game Character States

In object-oriented programming, State Pattern is one of the ways to implement Finite State Machines. This pattern falls under Behavioral Design Patterns.

When in our software, an object can change between multiple possible states and change its behavior according to the state, then, this type of problem can be easily solved using Finite State Machines, and this pattern helps us to achieve the same.


A glance at Mario’s State/Behaviors in Game

Here, I’m taking the example of the Super Mario game. Most people are already aware of this nostalgic game. In this game, Mario changes his states and behaviour based on the events which occurred, which you can see in the below image which I got from Mario Wiki.

Super Mario States

 

Let’s observe states/behavior and events in above image.

States

  1. Mario (We will refer as Small Mario hereafter)
  2. Super Mario
  3. Fire Mario
  4. Cape Mario
  5. Lost Life (Apart from the image considering this state)

Events

  1. Got Mushroom
  2. Got Fire Flower
  3. Got Feather
  4. Met Monster (Not shown in image)

State Transition on Event Occurrence & Earning Coins

The following table demonstrates how the state changes with different events. Apart from the state change, coins are also earned on the occurrence of events.

Current StateEvent OccurredNew StateCoins Earned
Small MarioGot MushroomSuper Mario100
Small MarioGot Fire FlowerFire Mario200
Small MarioGot FeatherCape Mario300
Small MarioMet MonsterLost Life0
Super MarioGot MushroomSuper Mario100
Super MarioGot Fire FlowerFire Mario200
Super MarioGot FeatherCape Mario300
Super MarioMet MonsterSmall Mario0
Fire MarioGot MushroomFire Mario100
Fire MarioGot Fire FlowerFire Mario200
Fire MarioGot FeatherCape Mario300
Fire MarioMet MonsterSmall Mario0
Cape MarioGot MushroomCape Mario100
Cape MarioGot Fire FlowerFire Mario200
Cape MarioGot FeatherCape Mario300
Cape MarioMet MonsterSmall Mario0

Earning Life

On every 5000 coins collected, one life will be awarded.

Implementing in Code

Just to make it clear, Nintendo hasn’t open sourced the Super Mario source code yet. I am just taking the example to help you understand State Design Pattern, like in other articles in the series we will start programming with some code and will be refactoring it gradually.

Approach 1 - Creating Method for Every Events Occured

We created enum (internalState) with the name of all the states, for each event we have methods, where after validating conditions we are setting State property value which is of internalState type and represents the current state of object/Mario. Refer to the below code,

Source Code

State Pattern / Mario / Approach1

  1. public class Mario {  
  2.     enum internalState {  
  3.         SmallMario,  
  4.         SuperMario,  
  5.         FireMario,  
  6.         CapeMario  
  7.     }  
  8.   
  9.     public int LifeCount { getprivate set; }  
  10.     public int CoinCount { getprivate set; }  
  11.     private internalState State { getset; }  
  12.   
  13.     public Mario() {  
  14.         LifeCount = 1;  
  15.         CoinCount = 0;  
  16.         State = internalState.SmallMario;  
  17.     }  
  18.   
  19.     public void GotMushroom() {  
  20.         WriteLine("Got Mushroom!");  
  21.         if (State == internalState.SmallMario)  
  22.             State = internalState.SuperMario;  
  23.   
  24.         GotCoins(100);  
  25.     }  
  26.   
  27.     public void GotFireFlower() {  
  28.         WriteLine("Got FireFlower!");  
  29.         State = internalState.FireMario;  
  30.         GotCoins(200);  
  31.     }  
  32.   
  33.     public void GotFeather() {  
  34.         WriteLine("Got Feather!");  
  35.         State = internalState.CapeMario;  
  36.         GotCoins(300);  
  37.     }  
  38.   
  39.     public void GotCoins(int numberOfCoins) {  
  40.         WriteLine($"Got {numberOfCoins} Coin(s)!");  
  41.         CoinCount += numberOfCoins;  
  42.         if (CoinCount >= 5000)  
  43.         {  
  44.             GotLife();  
  45.             CoinCount -= 5000;  
  46.         }  
  47.     }  
  48.   
  49.     private void GotLife() {  
  50.         WriteLine("Got Life!");  
  51.         LifeCount += 1;  
  52.     }  
  53.   
  54.     private void LostLife() {  
  55.         WriteLine("Lost Life!");  
  56.         LifeCount -= 1;  
  57.         if (LifeCount <= 0)  
  58.             GameOver();  
  59.     }  
  60.   
  61.     public void MetMonster() {  
  62.         WriteLine("Met Monster!");  
  63.         if (State == internalState.SmallMario)  
  64.             LostLife();  
  65.         else  
  66.             State = internalState.SmallMario;  
  67.     }  
  68.   
  69.     public void GameOver() {  
  70.         LifeCount = 0;  
  71.         CoinCount = 0;  
  72.         WriteLine("Game Over!");  
  73.     }  
  74.   
  75.     public override string ToString() {  
  76.         return $"State: {State} | LifeCount: {LifeCount} | CoinsCount: {CoinCount} \n";  
  77.     }  
  78. }  
  79.   
  80. class MainClass {  
  81.     static void Main(string[] args) {  
  82.         Mario mario = new Mario();  
  83.         WriteLine(mario);  
  84.   
  85.         mario.GotMushroom();  
  86.         WriteLine(mario);  
  87.   
  88.         mario.GotFireFlower();  
  89.         WriteLine(mario);  
  90.   
  91.         mario.GotFeather();  
  92.         WriteLine(mario);  
  93.   
  94.         mario.GotCoins(4800);  
  95.         WriteLine(mario);  
  96.   
  97.         mario.MetMonster();  
  98.         WriteLine(mario);  
  99.   
  100.         mario.MetMonster();  
  101.         WriteLine(mario);  
  102.   
  103.         mario.MetMonster();  
  104.         WriteLine(mario);  
  105.     }  
  106. }  
As you see in the output it is changing state on the occurrence of different events.

Approach1_Output

 

Reviewing Approach 1

On the occurrence of each event, a different operation can be executed based on current state of the object. For example, GotMushroom event -- if it occurs for SmallMario, the character would be changed to SuperMario, but if the same event occurs for SuperMario, it will remain the same. It may lead to confusion to write the same conditions in each method.

Approach 2 - Moving All State Related Code To Respective Class

To address the problem of approach 1, here I created a separate class for each State, which all are inherited from IState interface. This interface contains respective methods for all our four events; i.e GotMushroom(), GotFireFlower(), GotFeather() & MetMonster(). All State classes are inheriting this. Now, before writing state specific code, we don’t need to check condition, because it’s being written for specific states. All 4 state classes are mimicking our State transition table shown above. Refer to the code.

  1. public interface IState {  
  2.     void GotMushroom();  
  3.     void GotFireFlower();  
  4.     void GotFeather();  
  5.     void MetMonster();  
  6. };  
  7.   
  8. public class SmallMario : IState {  
  9.     private Mario mario;  
  10.   
  11.     public SmallMario(Mario mario) {  
  12.         this.mario = mario;  
  13.     }  
  14.   
  15.     public void GotMushroom() {  
  16.         WriteLine("Got Mushroom!");  
  17.         mario.state = mario.GetState("superMario");  
  18.         mario.GotCoins(100);  
  19.     }  
  20.   
  21.     public void GotFireFlower() {  
  22.         WriteLine("Got FireFlower!");  
  23.         mario.state = mario.GetState("fireMario");  
  24.         mario.GotCoins(200);  
  25.     }  
  26.   
  27.     public void GotFeather() {  
  28.         WriteLine("Got Feather!");  
  29.         mario.state = mario.GetState("capeMario");  
  30.         mario.GotCoins(300);  
  31.     }  
  32.   
  33.     public void MetMonster() {  
  34.         WriteLine("Met Monster!");  
  35.         mario.state = mario.GetState("smallMario");  
  36.         mario.LostLife();  
  37.     }  
  38. }  
  39.   
  40. public class SuperMario : IState {  
  41.     private Mario mario;  
  42.   
  43.     public SuperMario(Mario mario) {  
  44.         this.mario = mario;  
  45.     }  
  46.   
  47.     public void GotMushroom() {  
  48.         WriteLine("Got Mushroom!");  
  49.         mario.GotCoins(100);  
  50.     }  
  51.   
  52.     public void GotFireFlower() {  
  53.         WriteLine("Got FireFlower!");  
  54.         mario.state = mario.GetState("fireMario");  
  55.         mario.GotCoins(200);  
  56.     }  
  57.   
  58.     public void GotFeather() {  
  59.         WriteLine("Got Feather!");  
  60.         mario.state = mario.GetState("capeMario");  
  61.         mario.GotCoins(300);  
  62.     }  
  63.   
  64.     public void MetMonster() {  
  65.         WriteLine("Met Monster!");  
  66.         mario.state = mario.GetState("smallMario");  
  67.     }  
  68. }  
  69.   
  70. public class FireMario : IState {  
  71.     private Mario mario;  
  72.   
  73.     public FireMario(Mario mario) {  
  74.         this.mario = mario;  
  75.     }  
  76.   
  77.     public void GotMushroom() {  
  78.         WriteLine("Got Mushroom!");  
  79.         mario.GotCoins(100);  
  80.     }  
  81.   
  82.     public void GotFireFlower() {  
  83.         WriteLine("Got FireFlower!");  
  84.         mario.GotCoins(200);  
  85.     }  
  86.   
  87.     public void GotFeather() {  
  88.         WriteLine("Got Feather!");  
  89.         mario.state = mario.GetState("capeMario");  
  90.         mario.GotCoins(300);  
  91.     }  
  92.   
  93.     public void MetMonster() {  
  94.         WriteLine("Met Monster!");  
  95.         mario.state = mario.GetState("smallMario");  
  96.     }  
  97. }  
  98.   
  99. public class CapeMario : IState {  
  100.     private Mario mario;  
  101.   
  102.     public CapeMario(Mario mario) {  
  103.         this.mario = mario;  
  104.     }  
  105.   
  106.     public void GotMushroom() {  
  107.         WriteLine("Got Mushroom!");  
  108.         mario.GotCoins(100);  
  109.     }  
  110.   
  111.     public void GotFireFlower() {  
  112.         WriteLine("Got FireFlower!");  
  113.         mario.state = mario.GetState("fireMario");  
  114.         mario.GotCoins(200);  
  115.     }  
  116.   
  117.     public void GotFeather() {  
  118.         WriteLine("Got Feather!");  
  119.         mario.GotCoins(300);  
  120.     }  
  121.   
  122.     public void MetMonster() {  
  123.         WriteLine("Met Monster!");  
  124.         mario.state = mario.GetState("smallMario");  
  125.     }  
  126. }  
  127.   
  128. public class Mario {  
  129.     public int LifeCount { getprivate set; }  
  130.     public int CoinCount { getprivate set; }  
  131.     public IState state;  
  132.   
  133.     private SmallMario smallMario;  
  134.     private SuperMario superMario;  
  135.     private FireMario fireMario;  
  136.     private CapeMario capeMario;  
  137.   
  138.     public Mario() {  
  139.         LifeCount = 1;  
  140.         CoinCount = 0;  
  141.   
  142.         smallMario = new SmallMario(this);  
  143.         superMario = new SuperMario(this);  
  144.         fireMario = new FireMario(this);  
  145.         capeMario = new CapeMario(this);  
  146.   
  147.         state = smallMario;  
  148.     }  
  149.   
  150.     public IState GetState(string stateId) {  
  151.         switch (stateId) {  
  152.             case "smallMario":  
  153.                 return smallMario;  
  154.             case "superMario":  
  155.                 return superMario;  
  156.             case "fireMario":  
  157.                 return fireMario;  
  158.             case "capeMario":  
  159.                 return capeMario;  
  160.             default:  
  161.                 return null;  
  162.         }  
  163.     }  
  164.      
  165.     public void GotMushroom() {  
  166.         state.GotMushroom();  
  167.     }  
  168.   
  169.     public void GotFireFlower() {  
  170.         state.GotFireFlower();  
  171.     }  
  172.   
  173.     public void GotFeather() {  
  174.         state.GotFeather();  
  175.     }  
  176.   
  177.     public void MetMonster() {  
  178.         state.MetMonster();  
  179.     }  
  180.       
  181.     public void GotCoins(int numberOfCoins) {  
  182.         WriteLine($"Got {numberOfCoins} Coin(s)!");  
  183.         CoinCount += numberOfCoins;  
  184.         if (CoinCount >= 5000) {  
  185.             GotLife();  
  186.             CoinCount -= 5000;  
  187.         }  
  188.     }  
  189.   
  190.     public void GotLife() {  
  191.         WriteLine("Got Life!");  
  192.         LifeCount += 1;  
  193.     }  
  194.   
  195.     public void LostLife() {  
  196.         WriteLine("Lost Life!");  
  197.         LifeCount -= 1;  
  198.         if (LifeCount <= 0)  
  199.             GameOver();  
  200.     }  
  201.   
  202.     public void GameOver() {  
  203.         LifeCount = 0;  
  204.         CoinCount = 0;  
  205.         WriteLine("Game Over!");  
  206.     }  
  207.   
  208.     public override string ToString() {  
  209.         return $"State: {state} | LifeCount: {LifeCount} | CoinsCount: {CoinCount} \n";  
  210.     }  
  211. }  
  212.   
  213. class MainClass {  
  214.     static void Main(string[] args) {  
  215.         Mario mario = new Mario();  
  216.         WriteLine(mario);  
  217.   
  218.         mario.GotMushroom();  
  219.         WriteLine(mario);  
  220.   
  221.         mario.GotFireFlower();  
  222.         WriteLine(mario);  
  223.   
  224.         mario.GotFeather();  
  225.         WriteLine(mario);  
  226.   
  227.         mario.GotCoins(4800);  
  228.         WriteLine(mario);  
  229.   
  230.         mario.MetMonster();  
  231.         WriteLine(mario);  
  232.   
  233.         mario.MetMonster();  
  234.         WriteLine(mario);  
  235.   
  236.         mario.MetMonster();  
  237.         WriteLine(mario);  
  238.     }  
  239. }   

Reviewing Approach 2

Everything related to states is within state classes now, but responsibility to create its object is still outside.

Approach 3 - Making State Classes Singleton

Since our state classes having no variable/properties, all those are maintained in Mario class, so we can make state classes singleton. In event methods of all singleton classes, we will be passing current Mario object so states can be switched. Here, IState interface is changed accordingly to pass the Mario class object as parameter and inside Mario class there is no need to initialize all the objects.

  1. public interface IState {  
  2.     void GotMushroom(Mario mario);  
  3.     void GotFireFlower(Mario mario);  
  4.     void GotFeather(Mario mario);  
  5.     void MetMonster(Mario mario);  
  6. };  
  7.   
  8. public class SmallMario : IState {  
  9.     private static SmallMario instance = new SmallMario();  
  10.   
  11.     private SmallMario() { }  
  12.   
  13.     public static SmallMario GetInstance {  
  14.         get { return instance; }  
  15.     }  
  16.   
  17.     public void GotMushroom(Mario mario) {  
  18.         WriteLine("Got Mushroom!");  
  19.         mario.State = SuperMario.GetInstance;  
  20.         mario.GotCoins(100);  
  21.     }  
  22.   
  23.     public void GotFireFlower(Mario mario) {  
  24.         WriteLine("Got FireFlower!");  
  25.         mario.State = FireMario.GetInstance;   
  26.         mario.GotCoins(200);  
  27.     }  
  28.   
  29.     public void GotFeather(Mario mario) {  
  30.         WriteLine("Got Feather!");  
  31.         mario.State = CapeMario.GetInstance;  
  32.         mario.GotCoins(300);  
  33.     }  
  34.   
  35.     public void MetMonster(Mario mario) {  
  36.         WriteLine("Met Monster!");  
  37.         mario.State = SmallMario.GetInstance;  
  38.         mario.LostLife();  
  39.     }  
  40. }  
  41.   
  42. public class SuperMario : IState {  
  43.     private static SuperMario instance = new SuperMario();  
  44.   
  45.     private SuperMario() { }  
  46.   
  47.     public static SuperMario GetInstance {  
  48.         get { return instance; }  
  49.     }  
  50.   
  51.     public void GotMushroom(Mario mario) {  
  52.         WriteLine("Got Mushroom!");  
  53.         mario.GotCoins(100);  
  54.     }  
  55.   
  56.     public void GotFireFlower(Mario mario) {  
  57.         WriteLine("Got FireFlower!");  
  58.         mario.State = FireMario.GetInstance;  
  59.         mario.GotCoins(200);  
  60.     }  
  61.   
  62.     public void GotFeather(Mario mario) {  
  63.         WriteLine("Got Feather!");  
  64.         mario.State = CapeMario.GetInstance;  
  65.         mario.GotCoins(300);  
  66.     }  
  67.   
  68.     public void MetMonster(Mario mario) {  
  69.         WriteLine("Met Monster!");  
  70.         mario.State = SmallMario.GetInstance;  
  71.     }  
  72. }  
  73.   
  74. public class FireMario : IState {  
  75.     private static FireMario instance = new FireMario();  
  76.   
  77.     private FireMario() { }  
  78.   
  79.     public static FireMario GetInstance {  
  80.         get { return instance; }  
  81.     }  
  82.   
  83.     public void GotMushroom(Mario mario) {  
  84.         WriteLine("Got Mushroom!");  
  85.         mario.GotCoins(100);  
  86.     }  
  87.   
  88.     public void GotFireFlower(Mario mario) {  
  89.         WriteLine("Got FireFlower!");  
  90.         mario.GotCoins(200);  
  91.     }  
  92.   
  93.     public void GotFeather(Mario mario) {  
  94.         WriteLine("Got Feather!");  
  95.         mario.State = CapeMario.GetInstance;  
  96.         mario.GotCoins(300);  
  97.     }  
  98.   
  99.     public void MetMonster(Mario mario) {  
  100.         WriteLine("Met Monster!");  
  101.         mario.State = SmallMario.GetInstance;  
  102.     }  
  103. }  
  104.   
  105. public class CapeMario : IState {  
  106.     private static CapeMario instance = new CapeMario();  
  107.   
  108.     private CapeMario() { }  
  109.   
  110.     public static CapeMario GetInstance {  
  111.         get { return instance; }  
  112.     }  
  113.   
  114.     public void GotMushroom(Mario mario) {  
  115.         WriteLine("Got Mushroom!");  
  116.         mario.GotCoins(100);  
  117.     }  
  118.   
  119.     public void GotFireFlower(Mario mario) {  
  120.         WriteLine("Got FireFlower!");  
  121.         mario.State = FireMario.GetInstance;  
  122.         mario.GotCoins(200);  
  123.     }  
  124.   
  125.     public void GotFeather(Mario mario) {  
  126.         WriteLine("Got Feather!");  
  127.         mario.GotCoins(300);  
  128.     }  
  129.   
  130.     public void MetMonster(Mario mario) {  
  131.         WriteLine("Met Monster!");  
  132.         mario.State = SmallMario.GetInstance;   
  133.     }  
  134. }  
  135.   
  136. public class Mario  
  137. {  
  138.     public int LifeCount { getprivate set; }  
  139.     public int CoinCount { getprivate set; }  
  140.     private IState state;  
  141.   
  142.     public IState State {  
  143.         set { state = value; }  
  144.     }  
  145.   
  146.     public Mario() {  
  147.         LifeCount = 1;  
  148.         CoinCount = 0;  
  149.   
  150.         state = SmallMario.GetInstance;  
  151.     }  
  152.   
  153.     public void GotMushroom() {  
  154.         state.GotMushroom(this);  
  155.     }  
  156.   
  157.     public void GotFireFlower() {  
  158.         state.GotFireFlower(this);  
  159.     }  
  160.   
  161.     public void GotFeather() {  
  162.         state.GotFeather(this);  
  163.     }  
  164.   
  165.     public void MetMonster() {  
  166.         state.MetMonster(this);  
  167.     }  
  168.   
  169.     public void GotCoins(int numberOfCoins) {  
  170.         WriteLine($"Got {numberOfCoins} Coin(s)!");  
  171.         CoinCount += numberOfCoins;  
  172.         if (CoinCount >= 5000) {  
  173.             GotLife();  
  174.             CoinCount -= 5000;  
  175.         }  
  176.     }  
  177.   
  178.     public void GotLife() {  
  179.         WriteLine("Got Life!");  
  180.         LifeCount += 1;  
  181.     }  
  182.   
  183.     public void LostLife() {  
  184.         WriteLine("Lost Life!");  
  185.         LifeCount -= 1;  
  186.         if (LifeCount <= 0)  
  187.             GameOver();  
  188.     }  
  189.   
  190.     public void GameOver() {  
  191.         LifeCount = 0;  
  192.         CoinCount = 0;  
  193.         WriteLine("Game Over!");  
  194.     }  
  195.   
  196.     public override string ToString() {  
  197.         return $"State: {state} | LifeCount: {LifeCount} | CoinsCount: {CoinCount} \n";  
  198.     }  
  199. }  
  200.   
  201. class MainClass {  
  202.     static void Main(string[] args) {  
  203.         Mario mario = new Mario();  
  204.         WriteLine(mario);  
  205.   
  206.         mario.GotMushroom();  
  207.         WriteLine(mario);  
  208.   
  209.         mario.GotFireFlower();  
  210.         WriteLine(mario);  
  211.   
  212.         mario.GotFeather();  
  213.         WriteLine(mario);  
  214.   
  215.         mario.GotCoins(4800);  
  216.         WriteLine(mario);  
  217.   
  218.         mario.MetMonster();  
  219.         WriteLine(mario);  
  220.   
  221.         mario.MetMonster();  
  222.         WriteLine(mario);  
  223.   
  224.         mario.MetMonster();  
  225.         WriteLine(mario);  
  226.     }  
  227. }   
Conclusion

All state related logic is maintained within state classes now, and in the final approach, a singleton is used, which can be implemented in various better ways. Here, we have removed conditional duplicacy & new states can be easily added. Existing state logic can be easily extended without changing any other class. In game programming, this pattern is frequently used.

Thanks for reading, let the suggestions/discussions/queries go in the comments.