State-driven behavior using the State Design pattern

The previous pattern illustrated not only the usefulness of modeling game states, but also how a game manager class can grow in size and become unmanageable. To manage the complexity of many states and complex behaviors of states, the State pattern has been proposed in the software development community. Design patterns are general purpose software component architectures that have been tried and tested and found to be good solutions to commonly occurring software system features. The key features of the State pattern are that each state is modeled by its own class and that all states inherit (are subclassed) from a single parent state class. The states need to know about each other in order to tell the game manager to change the current state. This is a small price to pay for the division of the complexity of the overall game behaviors into separate state classes.

Note

NOTE: Many thanks to the contribution from Bryan Griffiths which has helped improve this recipe.

Getting ready

This recipe builds upon the previous recipe. So, make a copy of that project, open it, and then follow the steps for this recipe.

How to do it...

To manage an object's behavior using the state pattern architecture, perform the following steps:

  1. Replace the contents of C# script class GameManager with the following:
    using UnityEngine;
    using System.Collections;
    using UnityEngine.UI;
    
    public class GameManager : MonoBehaviour {
      public Text textGameStateName;
      public Button buttonWinGame;
      public Button buttonLoseGame;
    
      public StateGamePlaying stateGamePlaying{get; set;}
      public StateGameWon stateGameWon{get; set;}
      public StateGameLost stateGameLost{get; set;}
    
      private GameState currentState;
    
      private void Awake () {
        stateGamePlaying = new StateGamePlaying(this);
        stateGameWon = new StateGameWon(this);
        stateGameLost = new StateGameLost(this);
      }
    
      private void Start () {
        NewGameState( stateGamePlaying );
      }
    
      private void Update () {
        if (currentState != null)
          currentState.StateUpdate();
      }
    
      public void NewGameState(GameState newState)
      {
        if( null != currentState)
          currentState.OnMyStateExit();
    
        currentState = newState;
        currentState.OnMyStateEntered();
      }
    
      public void DisplayStateEnteredMessage(string stateEnteredMessage){
        textGameStateName.text = stateEnteredMessage;
      }
    
      public void BUTTON_CLICK_ACTION_WIN_GAME(){
        if( null != currentState){
    currentState.OnButtonClick(GameState.ButtonType.ButtonWinGame);
          DestroyButtons();
        }
      }
    
      public void BUTTON_CLICK_ACTION_LOSE_GAME(){
        if( null != currentState){
    currentState.OnButtonClick(GameState.ButtonType.ButtonLoseGame);
          DestroyButtons();
        }
      }
    
      private void DestroyButtons(){
        Destroy (buttonWinGame.gameObject);
        Destroy (buttonLoseGame.gameObject);
      }
    }
  2. Create a new C# script class called GameState:
    using UnityEngine;
    using System.Collections;
    
    public abstract class GameState {
      public enum ButtonType {
        ButtonWinGame,
        ButtonLoseGame
      }
    
      protected GameManager gameManager;
      public GameState(GameManager manager) {
        gameManager = manager;
      }
    
      public abstract void OnMyStateEntered();
      public abstract void OnMyStateExit();
      public abstract void StateUpdate();
      public abstract void OnButtonClick(ButtonType button);
    }
  3. Create a new C# script class called StateGamePlaying:
    using UnityEngine;
    using System.Collections;
    
    public class StateGamePlaying : GameState {
      public StateGamePlaying(GameManager manager):base(manager){}
    
      public override void OnMyStateEntered(){
        string stateEnteredMessage = "ENTER state: StateGamePlaying";
        gameManager.DisplayStateEnteredMessage(stateEnteredMessage);
        Debug.Log(stateEnteredMessage);
      }
      public override void OnMyStateExit(){}
      public override void StateUpdate() {}
    
    
      public override void OnButtonClick(ButtonType button){
        if( ButtonType.ButtonWinGame == button )
          gameManager.NewGameState(gameManager.stateGameWon);
    
        if( ButtonType.ButtonLoseGame == button )
          gameManager.NewGameState(gameManager.stateGameLost);
      }
    }
  4. Create a new C# script class called StateGameWon:
    using UnityEngine;
    using System.Collections;
    
    public class StateGameWon : GameState {
      public StateGameWon(GameManager manager):base(manager){}
    
      public override void OnMyStateEntered(){
        string stateEnteredMessage = "ENTER state: StateGameWon";
    gameManager.DisplayStateEnteredMessage(stateEnteredMessage);
        Debug.Log(stateEnteredMessage);
      }
      public override void OnMyStateExit(){}
      public override void StateUpdate() {}
      public override void OnButtonClick(ButtonType button){}
    }
  5. Create a new C# script class called StateGameLost:
    using UnityEngine;
    using System.Collections;
    
    public class StateGameLost : GameState {
      public StateGameLost(GameManager manager):base(manager){}
    
      public override void OnMyStateEntered(){
        string stateEnteredMessage = "ENTER state: StateGameLost";
    gameManager.DisplayStateEnteredMessage(stateEnteredMessage);
        Debug.Log(stateEnteredMessage);
      }
      public override void OnMyStateExit(){}
      public override void StateUpdate() {}
      public override void OnButtonClick(ButtonType button){}
    }
  6. In the Hierarchy, select the Button-win button, and for its Button (Script) component, add an OnClick action to call the BUTTON_CLICK_ACTION_WIN_GAME() method from the GameManager component in the Main Camera GameObject.
  7. In the Hierarchy, select the Button-lose button, and for its Button (Script) component, add an OnClick action to call the BUTTON_CLICK_ACTION_LOSE_GAME() method from the GameManager component in the Main Camera GameObject.
  8. In the Hierarchy, select the Main Camera GameObject. Next, drag into the Inspector to ensure that all three GameManager (Script) public variables, Text State Messages, Button Win Game, and Button Lose Game, have the corresponding Canvas GameObjects dragged into them (the two buttons and the UI text GameObject).

How it works...

The scene is very straightforward for this recipe. There is the single Main Camera GameObject that has the GameManager script object component attached to it.

A C# scripted class is defined for each state that the game needs to manage—for this example, the three states StateGamePlaying, StateGameWon, and StateGameLost. Each of these state classes is a subclass of GameState. GameState defines properties and methods that all subclass states will possess:

  • An enumerated type ButtonType, which defines the two possible button clicks that the game might generate: ButtonWinGame and ButtonLoseGame.
  • The gameManager variable: so that each state object has a link to the game manager.
  • The constructor method that accepts a reference to the GameManager: that automatically makes the gameManager variable refer to the passed in GameManager object.
  • The four abstract methods OnMyStateEntered(), OnMyStateExit(), OnButtonClick(…), and StateUpdate(). Note that abstract methods must have their own implementation for each subclass.

When the GameManager class' Awake() method is executed, three state objects are created, one for each of the playing/win/lose classes. These state objects are stored in their corresponding variables: stateGamePlaying, stateGameWon, and stateGameLost.

The GameManager class has a variable called currentState, which is a reference to the current state object at any time while the game runs (initially, it will be null). Since it is of the GameState class (the parent of all state classes), it can refer to any of the different state objects.

After Awake(), GameManager will receive a Start() message. This method initializes the currentState to be the stateGamePlaying object.

For each frame, the GameManager will receive Update()messages. Upon receiving these messages, GameManager sends a StateUpdate()messages to the currentState object. So, for each frame, the object for the current state of the game will execute those methods. For example, when the currentState is set to game playing, for each frame, the gamePlayingObject will calls its (in this case, empty) StateUpdate() method.

The StateGamePlaying class implements statements in its OnButtonClick() method so that when the user clicks on a button, the gamePlayingObject will call the GameManager instance's NewState() method, passing it the object corresponding to the new state. So, if the user clicks on Button-win, the NewState() method is passed to gameManager.stateGameWon.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.108.175