State-driven behavior Do-It-Yourself states

Games as a whole, and individual objects or characters, can often be thought of (or modeled as) passing through different states or modes. Modeling states and changes of state (due to events or game conditions) is a very common way to manage the complexity of games and game components. In this recipe, we create a simple three-state game (game playing/game won/game lost) using a single GameManager class.

How to do it...

To use states to manage object behavior, follow these steps:

  1. Create two UI buttons at the top middle of the screen. Name one Button-win and edit its text to read Win Game. Name the second Button-lose and edit its text to read Lose Game.
  2. Create a UI text object at the top left of the screen. Name this Text-state-messages, and set its Rect Transform height property to 300 and its Text (Script) Paragraph Vertical Overflow property to Overflow.
    How to do it...
  3. Add the following C# script class GameManager to Main Camera:
    using UnityEngine;
    using System.Collections;
    using System;
    using UnityEngine.UI;
    
    public class GameManager : MonoBehaviour {
      public Text textStateMessages;
      public Button buttonWinGame;
      public Button buttonLoseGame;
    
      private enum GameStateType {
        Other,
        GamePlaying,
        GameWon,
        GameLost,
      }
    
      private GameStateType currentState = GameStateType.Other;
      private float timeGamePlayingStarted;
      private float timeToPressAButton = 5;
    
      void Start () {
        NewGameState( GameStateType.GamePlaying );
      }
    
      private void NewGameState(GameStateType newState) {
        // (1) state EXIT actions
        OnMyStateExit(currentState);
    
        // (2) change current state
        currentState = newState;
    
        // (3) state ENTER actions
        OnMyStateEnter(currentState);
    
        PostMessageDivider();
      }
    
      public void PostMessageDivider(){
        string newLine = "
    ";
        string divider = "--------------------------------";
        textStateMessages.text += newLine + divider;
      }
    
      public void PostMessage(string message){
        string newLine = "
    ";
        string timeTo2DecimalPlaces = String.Format("{0:0.00}", Time.time);
        textStateMessages.text += newLine + timeTo2DecimalPlaces + " :: " + message;
      }
    
      public void BUTTON_CLICK_ACTION_WIN_GAME(){
        string message = "Win Game BUTTON clicked";
        PostMessage(message);
        NewGameState( GameStateType.GameWon );
      }
    
      public void BUTTON_CLICK_ACTION_LOSE_GAME(){
        string message = "Lose Game BUTTON clicked";
        PostMessage(message);
        NewGameState( GameStateType.GameLost );
      }
    
      private void DestroyButtons(){
        Destroy (buttonWinGame.gameObject);
        Destroy (buttonLoseGame.gameObject);
      }
    
      //--------- OnMyStateEnter[ S ] - state specific actions
      private void OnMyStateEnter(GameStateType state){
        string enterMessage = "ENTER state: " + state.ToString();
        PostMessage(enterMessage);
    
        switch (state){
        case GameStateType.GamePlaying:
          OnMyStateEnterGamePlaying();
          break;
        case GameStateType.GameWon:
          // do nothing
          break;
        case GameStateType.GameLost:
          // do nothing
          break;
        }
      }
    
      private void OnMyStateEnterGamePlaying(){
        // record time we enter state
        timeGamePlayingStarted = Time.time;
      }
    
      //--------- OnMyStateExit[ S ] - state specific actions
      private void OnMyStateExit(GameStateType state){
        string exitMessage = "EXIT state: " + state.ToString();
        PostMessage(exitMessage);
    
        switch (state){
        case GameStateType.GamePlaying:
          OnMyStateExitGamePlaying();
          break;
        case GameStateType.GameWon:
          // do nothing
          break;
        case GameStateType.GameLost:
          // do nothing
          break;
        case GameStateType.Other:
          // cope with game starting in state 'Other'
          // do nothing
          break;
        }
      }
    
      private void OnMyStateExitGamePlaying(){
    // if leaving gamePlaying state then destroy the 2 buttons
        DestroyButtons();
      }
    
      //--------- Update[ S ] - state specific actions
      void Update () {
        switch (currentState){
        case GameStateType.GamePlaying:
          UpdateStateGamePlaying();
          break;
        case GameStateType.GameWon:
          // do nothing
          break;
        case GameStateType.GameLost:
          // do nothing
          break;
        }
      }
    
      private void UpdateStateGamePlaying(){
        float timeSinceGamePlayingStarted = Time.time - timeGamePlayingStarted;
        if(timeSinceGamePlayingStarted > timeToPressAButton){
          string message = "User waited too long - automatically going to Game LOST state";
          PostMessage(message);
          NewGameState(GameStateType.GameLost);
        }
      }
    }
  4. 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.
  5. 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.
  6. 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...

As can be seen in the following state chart figure, this recipe models a simple game, which starts in the GAME PLAYING state; then, depending on the button clicked by the user, the game moves either into the GAME WON state or the GAME LOST state. Also, if the user waits too long to click on a button, the game moves into the GAME LOST state.

The possible states of the system are defined using the enumerated type GameStateType, and the current state of the system at any point in time is stored in the currentState variable.

How it works...

A fourth state is defined (Other) to allow us to explicitly set the desired GamePlaying state in our Start() method. When we wish the game state to be changed, we call the NewGameState(…)method, passing the new state the game is to change into. The NewGameState(…)method first calls the OnMyStateExit(…)method with the current state, since there may be actions to be performed when a particular state is exited; for example, when the GamePlaying state is exited, it destroys the two buttons. Next, the NewGameState(…)method sets the currentState variable to be assigned the new state. Next, the OnMyStateEnter(…) method is called, since there may be actions to be performed immediately when a new state is entered. Finally, a message divider is posted to the UI Text box, with a call to the PostMessageDivider()method.

When the GameManager object receives messages (for example, every frame for Update()), its behavior must be appropriate for the current state. So, we see in this method a Switch statement, which calls state-specific methods. For example, if the current state is GamePlaying, then when an Update() message is received, the UpdateStateGamePlaying()method will be called.

The BUTTON_CLICK_ACTION_WIN_GAME() and BUTTON_CLICK_ACTION_LOSE_GAME() methods are executed if their corresponding buttons have been clicked. They move the game into the corresponding WIN or LOSE state.

Logic has been written in the UpdateStateGamePlaying() method, so once the GameManager has been in the GamePlaying state for more than a certain time (defined in variable timeToPressAButton), the game will automatically change into the GameLost state.

So, for each state, we may need to write methods for state exit, state entry, and update events, and also a main method for each event with a Switch statement to determine which state method should be called (or not). As can be imagined, the size of our methods and the number of methods in our GameManager class will grow significantly as more states and a more complex game logic are needed for non-trivial games. The next recipe takes a more sophisticated approach to state-driven games, where each state has its own class.

See also

Refer to the next recipe in this chapter for more information on how to manage the complexity of states with class inheritance and the State Design Pattern.

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

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