Turn Based Multiplayer – Part 3

Now that we have created a game model, as well as some 3D assets to display, we can tie them together and have something to interact with. We will create a couple more scripts, one to serve as the view component on the board and one to serve as the game controller. By the end of this lesson, you should be able to play a sample single player experience of Tic Tac Toe.

Board

  1. Create a new project folder named “View” as a subfolder of the “Scripts” folder
  2. Create a new C# script named “Board” in the “View” folder
  3. Select the “Board” prefab in the Project pane
  4. Add the “Board” component to the prefab
  5. Open the script for editing and replace the template code with the following:
using UnityEngine;
using UnityEngine.EventSystems;
using System.Collections;
using TicTacToe;

public class Board : MonoBehaviour, IPointerClickHandler
{
	// Add Code Here
}

Notice that I imported the “EventSystems” namespace so that our “Board” class could implement the “IPointerClickHandler” interface. Remember that we added a Physics Raycaster component to the camera, so taps or clicks on the collider for the board will now tie into Unity’s event systems much like a click on a canvas button.

public const string SquareClickedNotification = "Board.SquareClickedNotification";

Our board view will use the notification system to notify listeners anytime it has been clicked on. This notification will include an argument which indicates the index of a square that was clicked.

[SerializeField] SetPooler xPooler;
[SerializeField] SetPooler oPooler;

These are convenient references to the poolers on the same “Board” prefab. You wont actually be able to connect the references at this time, because our script wont be able to compile until we implement the interface.

public void Show (int index, Mark mark)
{
	SetPooler pooler = mark == Mark.X ? xPooler : oPooler;
	GameObject instance = pooler.Dequeue().gameObject;

	int x = index % 3;
	int z = index / 3;

	instance.transform.localPosition = new Vector3( x + 0.5f, 0, z + 0.5f );
	instance.SetActive(true);
}

When our “model” (the TicTacToe class) sends a notification that a mark has been placed on the screen, a “controller” class should handle the notification and tell a “view” to be updated and show what has happened. Our “Board” script is operating as the “view” and this public method will be how the controller will tell the view what to show and when to show it.

Using the passed “mark” value I can grab one of my two poolers according to the one that matches. Then I can dequeue a new mark and position it based on where the position of the given index would appear. Finally, because the pooler system provides instances in a disabled state, I must activate the object so it will become visible.

Note that I could have the view “listen” to the model notification itself, and sometimes I do, but the use of a controller allows for greater flexibility. For example, if I had an A.I. then it might create its own copy of the game, and I wouldn’t want notifications from that “dummy” game to cause the view to update. Of course, my notification system is powerful enough that the board could choose to only listen to notifications from a particular game instance, but even still there might be special rules for when or how to show something. As one example, maybe you want to animate the “winning” move appearing in a special way. The controller would be an ideal place to handle this logic.

public void Clear ()
{
	xPooler.EnqueueAll();
	oPooler.EnqueueAll();
}

Of course we will need a way to clear the board when a new game begins, so this method allows both poolers to reclaim their pooled instances.

void IPointerClickHandler.OnPointerClick (PointerEventData eventData)
{
	Vector3 pos = eventData.pointerCurrentRaycast.worldPosition;
	int x = Mathf.FloorToInt(pos.x);
	int z = Mathf.FloorToInt(pos.z);

	if (x < 0 || z < 0 || x > 2 || z > 2)
		return;

	int index = z * 3 + x;
	this.PostNotification(SquareClickedNotification, index);
}

When I made the board model, I created it with a 3 unit square surface. I also positioned it so that its lower left corner would appear at the origin of the scene’s coordinate space. This allows me to use the world position of the click and determine which square on the board was targeted. For example, if I click near the right edge of the board I might get an “x” position of around “2.9…” which when floored is “2”. This value is exactly what I want because arrays are zero-based, so the indices I would care about would be 0, 1 and 2.

As a precaution I abort the method early if something has allowed me to click on the board and get a coordinate that is out of bounds. Otherwise, I calculate the index and then post a notification so that a game controller knows I attempted input.

Now that the script is complete, don’t forget to head back to Unity and connect the pooler references. If you connect them on the prefab in the Project pane, then it will automatically update the instance in our scene. Don’t forget you will need to save the project in order to save the changes to the prefab.

Game Controller

We’ve done a fair amount of work and haven’t gotten to really see or test anything. Let’s create a quick little demo of what the game might be like if it were only a single player game.

  1. Create a new Empty GameObject called “Game Controller” in the scene
  2. For organization sake, I decided to parent the “Board” and “Canvas”, to the “Game Controller”. Sometimes it can be convenient to be able to search in a hierarchy from one to the other, but it’s also nice to be able to collapse things in the scene hierarchy pane so you can find what you need more quickly. Note that I will NOT create a prefab out of this hierarchy because Unity doesn’t understand nested prefabs
  3. Create a new project folder named “Controller” as a subfolder of the “Scripts” folder
  4. Create a new C# script named “GameController” in the “Controller” folder
  5. Add the “GameController” component to the “Game Controller” GameObject
  6. Open the script for editing and replace the template code with the following:
using UnityEngine;
using System.Collections;
using TicTacToe;

public class GameController : MonoBehaviour 
{
	// Add Code Here
}

There is nothing special about this class – just a normal MonoBehaviour which will be used to connect our “model” and “view” together.

public Game game = new Game();
public Board board;

This class will handle the creation of the game “model” and will also maintain a reference to the board “view”.

void OnEnable ()
{
	this.AddObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
	this.AddObserver(OnDidBeginGame, TicTacToe.DidBeginGameNotification);
	this.AddObserver(OnDidMarkSquare, TicTacToe.DidMarkSquareNotification);
}

void OnDisable ()
{
	this.RemoveObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
	this.AddObserver(OnDidBeginGame, TicTacToe.DidBeginGameNotification);
	this.AddObserver(OnDidMarkSquare, TicTacToe.DidMarkSquareNotification);
}

Here I show how to register and unregister for a subset of the notifications that we will need to implement the final version of this project. These few are enough to show a simple demo for now though.

void Start ()
{
	board = GetComponentInChildren<Board>();
	game.Reset();
}

Since the “Start” method runs after “OnEnable” I will know that I have already registered for all of the relevent notifications. When I tell the game to “Reset” so that it starts a new game, my notification observer method will be ready to respond to it.

void OnBoardSquareClicked (object sender, object args)
{
	if (game.control == TicTacToe.Mark.None)
		game.Reset();
	else
		game.Place((int)args);
}

When the board posts a notification that I clicked on it, I will want to do one of two things. If the game has already ended, then I will use that input as the trigger to start a new game. Otherwise, I will tell the game to attempt to take a turn based on the input.

Note that I am not updating the board “view” at this time, because it is possible that the attempted move is invalid. For example, a user may have clicked on a square which was already occupied. By simply waiting for the notification that a move was actually made I don’t have to add any duplicate validation logic.

void OnDidBeginGame (object sender, object args)
{
	board.Clear();
}

Here I make sure that the board “view” stays in sync with the game “model” by clearing the marks whenever a new game begins.

void OnDidMarkSquare (object sender, object args)
{
	int index = (int)args;
	Mark mark = game.board[index];
	board.Show(index, mark);
}

In this method I grab the index of the square that was updated, and then use that index to figure out which mark was placed. Using those two bits of data, I can tell the view all it will need to show the current state of the game.

Demo Time

Now would be a good time to save the scene and project. Then go ahead and run the scene and see that single-player TicTacToe was quite easily accomplished! You play both sides of the match, just keep clicking on empty squares until a game ends, and optionally click again to reset and play another round.

Next we just have to figure out how to play over a network – that’s the real challenge of this project.

Summary

In this lesson we began by making the board interactive. We used Unity’s EventSystems to register for clicks on the board, and were able to determine the square based on the position data it provided. We also connected it to the poolers so that it could correctly mark the squares with an appropriate prefab. Once we completed the code to drive the board, we made a sample implementation of a Game Controller which observed events from the game model and updated the view accordingly. We have a fully playable game, but only locally. We’ll start working toward multiplayer over a network next!

Don’t forget that if you get stuck on something, you can always check the repository for a working version here.

Advertisements

11 thoughts on “Turn Based Multiplayer – Part 3

    1. The SetPooler is a component which can be added to your objects via the Inspector pane in Unity. If you don’t have it, go back and check the “Project Setup” step in Part 1. That lesson also discusses how to configure the poolers. All you need to do in this lesson is connect a reference to the ones that were created in that first lesson.

      Like

  1. For the Game Controller script. It’s worth noting that the code-blocks on this page are incorrect. This tripped me up until I tracked it down.

    On this page the code-blocks show:

    void OnEnable ()
    {
    this.AddObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
    this.AddObserver(OnDidBeginGame, TicTacToe.DidBeginGameNotification);
    this.AddObserver(OnDidMarkSquare, TicTacToe.DidMarkSquareNotification);
    }

    void OnDisable ()
    {
    this.RemoveObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
    this.AddObserver(OnDidBeginGame, TicTacToe.DidBeginGameNotification);
    this.AddObserver(OnDidMarkSquare, TicTacToe.DidMarkSquareNotification);
    }

    In the repository you have the corrected version, adding “Game” to the TicTacToe namespace path:

    void OnEnable ()
    {
    this.AddObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
    this.AddObserver(OnDidBeginGame, TicTacToe.Game.DidBeginGameNotification);
    this.AddObserver(OnDidMarkSquare, TicTacToe.Game.DidMarkSquareNotification);
    }

    void OnDisable ()
    {
    this.RemoveObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
    this.AddObserver(OnDidBeginGame, TicTacToe.Game.DidBeginGameNotification);
    this.AddObserver(OnDidMarkSquare, TicTacToe.GameDidMarkSquareNotification);
    }

    Thank you for the tutorial, it’s awesome!

    Like

    1. Thanks for pointing that out. I notice another problem now that you mention it. In the “OnDisable” method the last two statements should be “RemoveObserver” calls instead of “AddObserver” calls.

      Like

  2. Ive done everything the same as you till now and when I execute the game it gives me this error

    “NullReferenceException: Object reference not set to an instance of an object
    GameController.OnDidBeginGame (System.Object sender, System.Object args) (at Assets/Scripts/Controller/GameController.cs:38)
    NotificationCenter.PostNotification (System.String notificationName, System.Object sender, System.Object e) (at Assets/Scripts/Common/NotificationCenter/NotificationCenter.cs:181)
    NotificationCenter.PostNotification (System.String notificationName, System.Object sender) (at Assets/Scripts/Common/NotificationCenter/NotificationCenter.cs:149)
    NotificationExtensions.PostNotification (System.Object obj, System.String notificationName) (at Assets/Scripts/Common/NotificationCenter/NotificationExtensions.cs:10)
    TicTacToe.Game.Reset () (at Assets/Scripts/Model/Game.cs:53)
    GameController.Start () (at Assets/Scripts/Controller/GameController.cs:27)”

    I don’t really get the error so i would like if you could help me please

    Like

    1. This is called a stack trace and is something you should take the time to understand. It tells you the order of events that led to something unexpected. The most recent line (at the top) is where a problem occurred. A NullReferenceException means that you tried to do something with a reference to an object, but instead your reference was pointing to nothing (null). They clarify that with the bit that says “Object reference not set to an instance of an object”. The log even contains the full path to the scripts, method names and even line numbers of where the chain of events happened, so it is very helpful if you learn it.

      To begin with, I would go look at the “GameController” script at line 38. What objects are referenced there? Your code may have been put in a different order than mine, but if it is the “OnDidBeginGame” method then I am guessing the statement causing a problem is “board.Clear();”, and if so, then your “board” reference probably hasn’t been assigned. You should look at the Inspector pane for the Game Controller scene object in Unity and verify that you have connected everything.

      Like

      1. I have everything in the same order as you and I have the board inside the “Board” Thing on the script in the inspector it just gives me that error when begining the game and the same on line 44 when I try to play those are the only errors so far and ive followed you till now

        Like

      2. Most of the time that someone has encountered a Null-ref while following along with my projects, it has had something to do with an incorrect setup on the Unity side. In order to help you figure out “what” is the problem, you can put a Debug.Log statement just before the line that is causing the issue. Something like “Debug.Log(board == null);”

        If that still doesn’t help, don’t forget that you can download a working copy from the repo and compare it against your own.

        Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s