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.
To use states to manage object behavior, follow these steps:
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); } } }
OnClick
action to call the BUTTON_CLICK_ACTION_WIN_GAME()
method from the GameManager component in the Main Camera GameObject.OnClick
action to call the BUTTON_CLICK_ACTION_LOSE_GAME()
method from the GameManager component in the Main Camera GameObject.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.
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() m
ethod, 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.
18.189.171.125