Controlling the game

確認済のバージョン: 5.3

-

難易度: 初級

We now need to turn this foundation behavior into a game.

To do this we will need to move the control into a central place by creating a Game Controller. The Game Controller will set the starting player's side, either "X" or "O", keep track of whose turn it is and when a button is clicked, send the current player's side, check for a winner and either change sides or end the game.

We will need a new script to do this, so let’s create one attached to its own GameObject.

  • Create a new Empty GameObject using Create > Create Empty.
  • Rename this GameObject "Game Controller".
  • With the Game Controller selected,
    • ... reset the Game Controller’s Transform component.
    • ... add a new script called "GameController".
  • File the GameController script in the Scripts folder.
  • Open the GameController script for editing.

Again, we are using the UI toolset, so we need the appropriate namespace.

  • Add the UI Namespace to the top of the script.
using UnityEngine.UI;

The GameController script will need to know about all of the buttons in the game so it can check their state and determine if that has been a win. For this, the script should hold a collection of all of the Grid Space buttons. We will want to check the button’s Text component for the Player Side to see if there are 3 matching values in a row, so let’s make an array of the type Text.

  • Remove all of the sample code from the class.
  • Create a public Text array called "buttonList".
public Text[] buttonList;

The square brackets [] after the type indicate that this variable is an array.

The final script should look like this:

GameController

Code snippet

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
public class GameController : MonoBehaviour {
    public Text[] buttonList;
}
  • Save this script.
  • Return to Unity.

Now that we have created this Button List array, we must populate it with the Text components of our Grid Space buttons. Remember, order here is important. It is also important that we link the child Text GameObjects to the Button List array, not the parent Grid Space GameObjects.

One way of populating the Button List array on the Game Controller in the Inspector is to set the size of the array to 9 and then drag each Text GameObject, one at a time, into the array. This is a bit tedious and time consuming. There is, however, an easier way to do this, and it involves "locking" the Inspector Window and dragging all GameObjects at once into the array.

  • Select the Game Controller GameObject in the Hierarchy.
  • In the upper right of the Inspector Window, click the Lock button.

description

What this does is prevent the Inspector Window from changing focus from current selection, in this case the Game Controller. If we didn't do this, when, in the next step, we select the child Text GameObjects, the focus of the Inspector would change to these child Text GameObjects and we wouldn't be able to drag them into the array on the Game Controller.

It's also worth noting that we can open multiple Inspector Windows, locking only the ones we want. This allows for complex views of various GameObjects in the Hierarchy and Project Windows.

  • Open the 9 Grid Space buttons in the Inspector using the turn down arrow.
  • Using + click on Windows or + click on the Mac, select only the child Text GameObjects from every Grid Space GameObject.

description

  • Drag these onto the locked Inspector for the Game Controller GameObject and drop them on the Button List name above the Size field.

description

When the dragging cursor is in the correct position, a small "+" icon will appear next to the cursor. If you miss a the location, the cursor will change to show a circle-and-slash icon indicating the drop will fail.

Successfully dropping the 9 Text GameObjects will automatically size the Button List array and add all of the items to the array.

description

In this particular case, it is important to check the order now. These should be in order automatically, but it’s worth checking to make sure.

  • Select each element in order by clicking on the element field.

This should highlight the GameObject linked as a reference.

description

Each element in the Button List array should correspond in order to the correct GameObject. Element 0 should be associated with Grid Space, Element 1 should be associated with Grid Space (1), Element 2 should correspond with Grid Space (2) and so on.

If this is correct,

  • Unlock the Inspector Window.
  • Save the scene.

Now, for the player to take a turn, the Grid Space button needs to inform the Game Controller that a move has been made and win conditions need to be checked. This means that the Grid Space button needs to have a reference to the GameController component. This can be held in a local variable with of the type GameController.

One way to associate the GameController component with each of the Grid Space buttons in the scene would be to make this variable public and populate this public property in the Inspector for each Grid Space button instance in the scene. We can’t make the association between the Grid Space prefab in the Project View and the instance of the Game Controller in the scene, as the assets cannot hold references instances. All this dragging can be a bit tedious, but it also opposes one of the basic workflow related to using a prefab: we can’t simply drop the prefab into the scene and have it "just work".

Another way to associate the GameController component with each of the Grid Space buttons in the scene is to do it in code.

Now, if we do this in code, we could have each Grid Space button search the scene's Hierarchy for the Game Controller when they are first created at the start of the game. The Game Controller, however, already has a list of all of the Grid Space buttons in the buttonList array. In our case, I feel it would be better for the Game Controller to push the reference to itself to the buttons directly.

To push the references to the Grid Space buttons, the GridSpace script will need a local variable with of the type GameController and a public function to set it.

  • Open the GridSpace script for editing.
  • Add a new private variable to hold a reference to the GameController.
private GameController gameController;
  • Create a new public function which returns void that can take a GameController as a parameter and assign it to the gameController variable as a reference.
public void SetGameControllerReference (GameController controller)
{
    gameController = controller;
}

The final script should look like this:

GridSpace

Code snippet

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class GridSpace : MonoBehaviour {
    
    public Button button;
    public Text buttonText;
    public string playerSide;

    private GameController gameController;

    public void SetGameControllerReference (GameController controller)
    {
        gameController = controller;
    }

    public void SetSpace ()
    {
        buttonText.text = playerSide;
        button.interactable = false;
    }
}
  • Save this script.
  • Open the GameController script for editing.
  • Create a new function that returns void called "SetGameControllerReferenceOnButtons".
void SetGameControllerReferenceOnButtons ()
{

}
  • In SetGameControllerReferenceOnButtons write code that loops through all of the buttons. This is best done with a for loop that iterates through all of the elements in the buttonList array.
for (int i = 0; i < buttonList.Length; i++)
{

}

The loop is easy. Loop through the entire length of the Button List... but the Button List is referencing the child Text GameObjects and we need to get a hold of the GridSpace component on the parent of the Text GameObjects. How can we do this?

There is a convenient call we can make to GetComponentInParent. With this we can give GetComponentInParent a type, in our case GridSpace, and it will return that component if it exists.

  • Add code to check each item in the Button List and set the GameController reference in the GridSpace component on the parent GameObject.
buttonList[i].GetComponentInParent< GridSpace>().SetGameControllerReference(this);

The keyword this refers to this class - or the class that the code is written in. By passing in this to SetGameControllerReference we are passing in a reference to this instance of the class. Each GridSpace instance will use this to set their reference to the GameController.

The function SetGameControllerReferenceOnButtons now needs to be called when the game starts.

  • Create an Awake function that returns void.
  • Call SetGameControllerReferenceOnButtons.
void Awake ()
{
    SetGameControllerReferenceOnButtons();
}

Now that the buttons know about the GameController and have a proper reference to it, we want the buttons to do two things: Get the Player Side from the Game Controller before the buttons set the Text value so the buttons know what symbol to place in their grid space and, once they’ve done that, inform the Game Controller that the turn is now over so that the Game Controller can check the win conditions and either end the game or change the side taking the turn.

To do this, we need to set up additional functionality in the GameController script.

  • Open the GameController script for editing.
  • Create a new empty public function that returns string called "GetPlayerSide".
public string GetPlayerSide ()
{
    return "?";
}

To successfully compile, GetPlayerSide must return some string value, so we will add a dummy line here for now returning "?".

  • Create a new empty public function that returns void called "EndTurn".
public void EndTurn ()
{
    Debug.Log("EndTurn is not implemented!");
}

Note how, at this stage in our development, we have created two essentially empty functions. We know the basic idea of what we want these functions to do but we have not yet developed the content. We can "fill these out" later, but by creating these empty but viable functions, we can continue our development in another part of our game without getting lost in a tangle of other code we are not yet ready to think about in detail. Both of these functions do have indicators that they are not complete if we ever run the game to test. GetPlayerSide returns "?" and EndTurn prints a line to the console warning us that "EndTurn is not implemented!". This way it will be clear that we will need to go back and work on these functions later.

The final script should look like this:

GameController

Code snippet

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class GameController : MonoBehaviour {

    public Text[] buttonList;

    void Awake ()
    {
        SetGameControllerReferenceOnButtons();
    }

    void SetGameControllerReferenceOnButtons ()
    {
        for (int i = 0; i < buttonList.Length; i++)
        {            buttonList[i].GetComponentInParent<GridSpace>().SetGameControllerReference(this);
        }
    }

    public string GetPlayerSide ()
    {
        return "?";
    }

    public void EndTurn ()
    {
        Debug.Log("EndTurn is not implemented!");
    }
}
  • Save the script.

With these two new public functions in GameController we need to make use of them in GridSpace.

  • Open the GridSpace script for editing.
  • In the function SetSpace,
    • ... change the first line so that buttonText.text is assigned the value of GetPlayerSide directly from the current value in GameController.
buttonText.text = gameController.GetPlayerSide();
  • In the function SetSpace,
    • ... as the last line in the function, add a line calling EndTurn.
gameController.EndTurn();

At this point, it is worth remembering that code in a function is executed in order, from top to bottom, so by placing the call to EndTurn at the end of the function, we know it will be called last, after all of the other business in SetSpace is done.

We now no longer need a local variable for playerSide. This value is now taken directly from the Game Controller.

  • Remove the line defining the variable playerSide.
public string playerSide;

The final script should look like this:

GridSpace

Code snippet

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class GridSpace : MonoBehaviour {

    public Button button;
    public Text buttonText;

    private GameController gameController;

    public void SetGameControllerReference (GameController controller)
    {
        gameController = controller;
    }

    public void SetSpace ()
    {
        buttonText.text = gameController.GetPlayerSide();
        button.interactable = false;
        gameController.EndTurn();
    }
}
  • Save the script.
  • Return to Unity.
  • Enter Play Mode.
  • Test by clicking any of the spaces.

We should see that everything now works, at least technically. It may look bad that the spaces are getting a "?" from the Game Controller and every turn our console tells us "EndTurn is not implemented!", but the Player Side value is now coming from the Game Controller and when the player takes their move by clicking a button, control of the game is handed back to the Game Controller to process the turn. We have now taken our foundation game play behavior away from the button elements themselves and expanded it so we now have overall control from a central point.

In the next lesson we will test the game for a win.