
上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:
- We will do all this by first creating another script like our
SquareState
script. Create the newGameState
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 }
- We now need to update our
TicTacToeControl
script. For starters, because we want to be able to play multiple games, add theNewGame
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]; }
- But first, we need to update our
OnGUI
function. To do that, start by moving all of the current contents ofOnGUI
to a new function calledDrawGameBoard
. - 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. Aswitch
statement works the same as a bunch ofif
andelse 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 toGameState.MultiPlayer
, we will call theDrawGameBoard
function, which should now contain what used to be in theOnGUI
function.public void OnGUI() { switch(gameState) { case GameState.Opening: DrawOpening(); break; case GameState.MultiPlayer: DrawGameBoard(); break; case GameState.GameOver: DrawGameOver(); break; } }
- 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 ourTicTacToeControl
class, right above where we defined our game board:public GameState gameState = GameState.Opening;
- 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 toGUI.Label
. By passing it aRect
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");
- The following line of code defines the
Rect
class used by ourNew 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 to75
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);
- 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 toMultiPlayer
that will start our game. TheNewGame
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; } }
- 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 theSquareState
enumeration. If the winner variable is equal toClear
, nobody won the game. If it is equal toXControl
orOControl
, 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;
- 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, theRect
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 toOpening
, 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; }
- 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 ourxTurn
Boolean to determine what to write about whose turn it is. Finally, it is theGUI.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);
- 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 newif
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);
- 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 ofOnGUI
, which is already called every frame? The reason is that theOnGUI
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 byOnGUI
. That is whatUpdate
andLateUpdate
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. - Add the following
LateUpdate
function to ourTicTacToeControl
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 caseMultiPlayer
, exit here and go no further.public void LateUpdate() { if(gameState != GameState.MultiPlayer) return;
- 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 notClear
, 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. Theelse 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 toSquareState.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; } }
- 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 theLateUpdate
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; }
- 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 isClear
, the game has yet to finish and we exit the function. But, if we make it through the entire loop without finding aClear
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); }
- Finally, we'll create the
SetWinner
function that is called repeatedly in ourLateUpdate
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 toGameOver
.public void SetWinner(SquareState toWin) { winner = toWin; gameState = GameState.GameOver; }
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.