Unity Android Game Development by Example Beginner's Guide
上QQ阅读APP看书,第一时间看更新

Time for action – finish creating the game

Let us finish the creation of our game by creating an opening screen. We will then add some checks to stop players from selecting squares more than once. Follow that with a check to see if anyone won and finally display a game over screen. With that, the game will be ready for us to make it look great.

Let's perform the following steps for finishing our game:

  1. We will do all this by first creating another script like our SquareState script. Create the new GameState script and clear out the default contents. Add the following code snippet and we will have the values needed to track the current state of our game:
    public enum GameState {
      Opening,
      MultiPlayer,
      GameOver
    }
  2. We now need to update our TicTacToeControl script. For starters, because we want to be able to play multiple games, add the NewGame function to the script. This function initializes our control variables so that we can start a fresh game with a clear board. It will not do very well for players to start a new game and have the board already filled in. This function will be used by our main menu, which we will be writing shortly.
    public void NewGame() {
      xTurn = true;
      board = new SquareState[9];
    }
  3. But first, we need to update our OnGUI function. To do that, start by moving all of the current contents of OnGUI to a new function called DrawGameBoard.
  4. Now, we need to change our cleared OnGUI function to the following code snippet in order to allow it to check and draw the proper screen based on the current game state. A switch statement works the same as a bunch of if and else if statements. In our case, we check the game state and call a different function based on what it is. For example, if the game state is equal to GameState.MultiPlayer, we will call the DrawGameBoard function, which should now contain what used to be in the OnGUI function.
    public void OnGUI() {
      switch(gameState) {
        case GameState.Opening:
          DrawOpening();
          break;
        case GameState.MultiPlayer:
          DrawGameBoard();
          break;
        case GameState.GameOver:
          DrawGameOver();
          break;
      }
    }
  5. By this point you are probably wondering where that game state variable is coming from. If you guessed that it was automatically provided by Unity, you are wrong. We have to track our own game state. That is why we created the GameState script earlier. Add the following line of code to the top of our TicTacToeControl class, right above where we defined our game board:
    public GameState gameState = GameState.Opening;
  6. Next, we need to create the other two game state screens. Let us start with the opening screen. When we draw our opening screen, we start by defining the Rect class used by our title. We follow that with a quick call to GUI.Label. By passing it a Rect class to position itself by and some text, the text is simply drawn on screen. This function is the best way to draw a section of text on the screen.
    public void DrawOpening() {
      Rect titleRect = new Rect(0, 0, 300, 75);
      GUI.Label(titleRect, "Tic-Tac-Toe");
  7. The following line of code defines the Rect class used by our New Game button. We want to be sure that it was right under the title, so it starts with the title's x position. We then combine the title's y position with its height to find the position right underneath it. Next, we used the width from the title so that our button will cover the entire position under it. Finally, the height is set to 75 because it is a good size for fingers and we don't want it to change based on the title. We could have just as easily used all the values from the title or just put in the numbers but our title will change later when we start styling everything.
      Rect multiRect = new Rect(titleRect.x, titleRect.y + titleRect.height, titleRect.width, 75);
  8. Finally, we make a call that will draw our button. You may remember our use of the GUI.Button function from when we drew the game board. If the button is pressed, the game state is set to MultiPlayer that will start our game. The NewGame function is also called, which will reset our game board. And of course, there is an extra curly brace to finish off the function.
      if(GUI.Button(multiRect, "New Game")) {
        NewGame();
        gameState = GameState.MultiPlayer;
      }
    }
  9. We have one screen left to draw, the game over screen. To do this, we will create the function referenced by our OnGUI function. However, in order for a game to end, there must be a winner, so add the following line of code right under our game state variable. We are making extended use of the SquareState enumeration. If the winner variable is equal to Clear, nobody won the game. If it is equal to XControl or OControl, the relevant player has won. Don't worry, it will make more sense when we create the game over screen next and the winner check system in a little bit.
    public SquareState winner = SquareState.Clear;
  10. There is nothing particularly new in the DrawGameOver function. First, we'll define where we are going to write who won the game. We'll then figure out who won, using our winner variable. After drawing the winner title, the Rect class used is shifted down by its height so it can be reused. Finally, we'll draw a button that changes our game state back to Opening, which is of course our main menu.
    public void DrawGameOver() {
      Rect winnerRect = new Rect(0, 0, 300, 75);
      string winnerTitle = winner == SquareState.XControl ? "X Wins!" : winner == SquareState.OControl ? "O Wins!" : "It's A Tie!";
      GUI.Label(winnerRect, winnerTitle);
    
      winnerRect.y += winnerRect.height;
      if(GUI.Button(winnerRect, "Main Menu"))
        gameState = GameState.Opening;
    }
  11. To make sure we are not overwriting squares that somebody already controls, we need to make a few changes to our DrawGameBoard function. First, it would be helpful if the players could easily tell whose turn it is. To do this, we'll add the following code snippet to the end of the function. This should start to become familiar. We'll first define where we want to draw. Then, we'll use our xTurn Boolean to determine what to write about whose turn it is. Finally, it is the GUI.Label function to draw it on screen.
    Rect turnRect = new Rect(300, 0, 100, 100);
    string turnTitle = xTurn ? "X's Turn!" : "O's Turn!";
    GUI.Label(turnRect, turnTitle);
  12. We now need to change the bit where we draw the board square, the GUI.Button function. We need to only draw that button if the square is clear. The following code snippet will do just that by moving the button inside of a new if statement. It checks whether the board square is clear. If it is, we draw the button. Otherwise, we use a label to write the owner to the button's location.
    if(board[boardIndex] == SquareState.Clear) {
      if(GUI.Button(square, owner))
        SetControl(boardIndex);
    }
    else GUI.Label(square, owner);
  13. The last thing we need to do is make a system that checks for a winner. We will do this in another function provided by the MonoBehaviour class. LateUpdate is called at the end of every frame, just before things are drawn on the screen. You might be wondering to yourself, why don't we just create a function that is called at the end of OnGUI, which is already called every frame? The reason is that the OnGUI function gets a little weird when drawing some of the GUI elements. It will sometimes be called more than once so that it can draw everything. So, for the most part, the functionality should never be controlled by OnGUI. That is what Update and LateUpdate are for. Update is the normal game loop where most of a game's functionality is called from. LateUpdate is for things that need to happen after the objects' update, such as our check for a game over.
  14. Add the following LateUpdate function to our TicTacToeControl class. We'll start with a check to make sure we should even be checking for a winner. If the game isn't in a state where we are playing, in this case MultiPlayer, exit here and go no further.
    public void LateUpdate() {
      if(gameState != GameState.MultiPlayer) return;
  15. Follow that with a short for loop. A victory in this game is a run of three matching squares. We start by checking the column that is marked by our loop. If the first square is not Clear, compare it to the square below; if they match, check it against the square below that. Our board is stored as a list but drawn as a grid, so we have to add three to go down a square. The else if statement follows checks of each row. By multiplying our loop value by three, we will skip down a row of each loop. We'll again compare the square to SquareState.Clear, then to the square one to its right, and finally two to the right. If either set of conditions is correct, we'll send the first square in the set out to another function to change our game state.
      for(int i=0;i<3;i++) {
        if(board[i] != SquareState.Clear && board[i] == board[i + 3] && board[i] == board[i + 6]) {
          SetWinner(board[i]);
          return;
        }
        else if(board[i * 3] != SquareState.Clear && board[i * 3] == board[(i * 3) + 1] && board[i * 3] == board[(i * 3) + 2]) {
          SetWinner(board[i * 3]);
          return;
        }
      }
  16. The following code snippet is largely the same as the if statements we just wrote previously. However, these lines of code check the diagonals. If the conditions are true, again send out to the other function to change game states. You have probably also noticed the returns after the function calls. If we have found a winner at any point, there is no need to check any more of the board. So, we'll exit the LateUpdate function early.
      if(board[0] != SquareState.Clear && board[0] == board[4] && board[0] == board[8]) {
        SetWinner(board[0]);
        return;
      }
      else if(board[2] != SquareState.Clear && board[2] == board[4] && board[2] == board[6]) {
        SetWinner(board[2]);
        return;
      }
  17. This is the last little bit for our LateUpdate function. If no one has won the game, as determined by the previous parts of this function, we have to check for a tie. This is done by checking all of the squares of the game board. If any one of them is Clear, the game has yet to finish and we exit the function. But, if we make it through the entire loop without finding a Clear square, we go set the winner but declare a tie.
      for(int i=0;i<board.Length;i++) {
        if(board[i] == SquareState.Clear)
          return;
      }
      SetWinner(SquareState.Clear);
    }

    Tip

    Do remember to close the last curly brace. It is needed to close off the LateUpdate function. If you forget it, some annoying errors will come your way.

  18. Finally, we'll create the SetWinner function that is called repeatedly in our LateUpdate function. Short and sweet, we'll pass to this function that is going to win. It sets our winner variable and changes our game state to GameOver.
    public void SetWinner(SquareState toWin) {
      winner = toWin;
      gameState = GameState.GameOver;
    }
    Time for action – finish creating the game

What just happened?

That is it. Congratulations! We now have a fully functioning Tic-tac-toe game and you survived the process. In the next sections, we will finally get to make it all look pretty. That is a good thing because, as the screenshot shows, the game does not look great right now.