Turn Based Multiplayer – Part 5

We’re on the home stretch for this mini-project! It’s finally time to modify the Game Controller so that input is dependent on the players joined in our match, and to make sure that moves made by one player are seen by the other.

Game Controller

A finite state machine can be a very nice way to chop the logic of a game into bite-sized bits. Even for a game as simple as Tic Tac Toe, we can easily identify a variety of important states:

  1. Loading – while waiting for both players to join the match
  2. Active – while it is the local player’s turn
  3. Passive – while it is the remote player’s turn
  4. End – when the game has ended due to a victory or stalemate

Open the GameController script for editing. Modify the class definition so that it inherits from StateMachine instead of MonoBehaviour:

public class GameController : StateMachine

I frequently cache important references in my game, including the game elements, UI, other controllers, etc. for convenience. This way I am not required to either find them or have singletons all over. To finish our little project, we need to provide links to our three UI labels (note that this requires you to add the UnityEngine.UI namespace), and to the Match Controller:

public Text localPlayerLabel;
public Text remotePlayerLabel;
public Text gameStateLabel;
public MatchController matchController;

Go ahead and connect the references to these in the editor. While we are there, we can go ahead and manually connect the board reference as well.

Remove the implementation of the Start method. Then add this Awake method in its place. The CheckState method will cause the GameController to enter its Loading phase, but we will get to that in a bit.

void Awake ()
{
	CheckState();
}

We’re also going listen to all the rest of the notifications we added. There were a few we didn’t use from the Game class, and now we can also observe the PlayerController and MatchController notifications as well. You can remove the Board click notification though, because I will want each state to be able to respond differently. Here are the full OnEnable and OnDisable methods:

void OnEnable ()
{
	this.AddObserver(OnMatchReady, MatchController.MatchReady);
	this.AddObserver(OnDidBeginGame, Game.DidBeginGameNotification);
	this.AddObserver(OnDidMarkSquare, Game.DidMarkSquareNotification);
	this.AddObserver(OnDidChangeControl, Game.DidChangeControlNotification);
	this.AddObserver(OnDidEndGame, Game.DidEndGameNotification);
	this.AddObserver(OnCoinToss, PlayerController.CoinToss);
	this.AddObserver(OnRequestMarkSquare, PlayerController.RequestMarkSquare);
}

void OnDisable ()
{
	this.RemoveObserver(OnMatchReady, MatchController.MatchReady);
	this.RemoveObserver(OnDidBeginGame, Game.DidBeginGameNotification);
	this.RemoveObserver(OnDidMarkSquare, Game.DidMarkSquareNotification);
	this.RemoveObserver(OnDidChangeControl, Game.DidChangeControlNotification);
	this.RemoveObserver(OnDidEndGame, Game.DidEndGameNotification);
	this.RemoveObserver(OnCoinToss, PlayerController.CoinToss);
	this.RemoveObserver(OnRequestMarkSquare, PlayerController.RequestMarkSquare);
}

Let’s take a look at the handler methods for each:

void OnMatchReady (object sender, object args)
{
	if (matchController.clientPlayer.isLocalPlayer)
		matchController.clientPlayer.CmdCoinToss();
}

When a player joins a match, it registers with the host (server) before finalizing itself on the client which created it. So to make sure that the match was fully configured on both instances of the game before proceeding, I waited for the “Ready” notification where the “client” player was also the “local” player.

void OnCoinToss (object sender, object args)
{
	bool coinToss = (bool)args;
	matchController.hostPlayer.mark = coinToss ? TicTacToe.Mark.X : TicTacToe.Mark.O;
	matchController.clientPlayer.mark = coinToss ? TicTacToe.Mark.O : TicTacToe.Mark.X;
	game.Reset();
}

The coin flip is used to decide who goes first. In my implementation the ‘X’ mark is also tied to the player which goes first, much like the white pieces always move first in Chess. It is important to make sure that both instances are synched in their knowledge of which player should be in control. It was convenient to think of the players in terms of “host” and “client” here because those identifiers remain consistent regardless of the game instance they run on, but the “remote” and “local” identifiers are subject to each instance’s perspective.

void OnRequestMarkSquare (object sender, object args)
{
	game.Place((int)args);
}

Here we have listened to a notification which should be posted on each game instance thanks to an ClientRpc method call. This makes sure that the move is attempted on each game.

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

We had this handler already, but I added another call to the CheckState method. This will cause the game to either go into an Active or Passive state, depending on whether it is the local player’s turn.

void OnDidChangeControl (object sender, object args)
{
	CheckState ();
}

void OnDidEndGame (object sender, object args)
{
	CheckState ();
}

We will also call the CheckState method at other important times such as each time that the game changes control to a different player, or when a game ends.

void CheckState ()
{
	if (!matchController.IsReady)
		ChangeState<LoadGameState>();
	else if (game.control == TicTacToe.Mark.None)
		ChangeState<EndGameState>();
	else if (game.control == matchController.localPlayer.mark)
		ChangeState<ActiveGameState>();
	else
		ChangeState<PassiveGameState>();
}

Finally we have the implementation which determines what state we are in. The first and most important check is to make sure that we have both players in the match – until that point we should stay in the “Load” state. Next we want to see if the game is over, and if so, trigger the “EndGame” state. Otherwise, when a game is still in play we will toggle between the “Active” and “Passive” states depending on whether or not it is the local player’s turn.

Base Game State

Create a new sub-folder in the Scripts/Controller named Game Controller States. Then, create a new script in that folder called BaseGameState. Replace the template code with the following:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using TicTacToe;

public abstract class BaseGameState : State 
{
	public GameController owner;

	public Board Board { get { return owner.board; }}
	public Text LocalPlayerLabel { get { return owner.localPlayerLabel; }}
	public Text RemotePlayerLabel { get { return owner.remotePlayerLabel; }}
	public Text GameStateLabel { get { return owner.gameStateLabel; }}
	public Game Game { get { return owner.game; }}
	public PlayerController LocalPlayer { get { return owner.matchController.localPlayer; }}
	public PlayerController RemotePlayer { get { return owner.matchController.remotePlayer; }}

	protected virtual void Awake ()
	{
		owner = GetComponent<GameController>();
	}

	protected void RefreshPlayerLabels ()
	{
		LocalPlayerLabel.text = string.Format("You: {0}\nWins: {1}", LocalPlayer.mark, LocalPlayer.score);
		RemotePlayerLabel.text = string.Format("Opponent: {0}\nWins: {1}", RemotePlayer.mark, RemotePlayer.score);
	}
}

It is a simple abstract base class from which we will derive the four states I mentioned earlier. I want each state to cache a reference to its owner, so I handle that in the Awake method, and I also want to wrap a bunch of the properties found on the owner so I don’t have to fully specify the references. Those convenience properties weren’t necessary, but I feel like they make the other code more readable.

I also added a method to update the player labels since several of the subclassed states will use the same formatting.

Load Game State

Create another script named LoadGameState in the same folder as the last one. Replace the default code with the following:

using UnityEngine;
using System.Collections;

public class LoadGameState : BaseGameState 
{
	public override void Enter ()
	{
		base.Enter ();
		GameStateLabel.text = "Waiting For Players";
		LocalPlayerLabel.text = "";
		RemotePlayerLabel.text = "";
	}

	public override void Exit ()
	{
		base.Exit ();
		LocalPlayer.score = 0;
		RemotePlayer.score = 0;
		RefreshPlayerLabels();
	}
}

When this state enters, I update the GameStateLabel with an informative message so that users will realize why they are waiting. I clear the player labels since they wont have been assigned a mark yet.

When the state exits, it will be because the players have been assigned their marks and the game has begun. I can go ahead and reset their scores and call the RefreshPlayerLabels method in the base class to update the player labels.

Active Game State

Create another script named ActiveGameState in the same folder as the last one. Replace the default code with the following:

using UnityEngine;
using System.Collections;

public class ActiveGameState : BaseGameState 
{
	public override void Enter ()
	{
		base.Enter ();
		GameStateLabel.text = "Your Turn!";
		RefreshPlayerLabels();
	}

	protected override void AddListeners ()
	{
		base.AddListeners ();
		this.AddObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
	}

	protected override void RemoveListeners ()
	{
		base.RemoveListeners ();
		this.RemoveObserver(OnBoardSquareClicked, Board.SquareClickedNotification);
	}

	void OnBoardSquareClicked (object sender, object args)
	{
		LocalPlayer.CmdMarkSquare((int)args);
	}
}

Like before I provide a handy message when this state enters so that a user will realize it is time for them to make a move. We use the Add and Remove Listeners methods defined in the base State class in order to listen to the board square clicked notification. The handler method triggers an attempted move for the game. Note that we wont have to worry about the player entering moves on the opponents turn because this handler method will be unregistered when the “Active” state exits.

Passive Game State

Create another script named PassiveGameState in the same folder as the last one. Replace the default code with the following:

using UnityEngine;
using System.Collections;

public class PassiveGameState : BaseGameState 
{
	public override void Enter ()
	{
		base.Enter ();
		GameStateLabel.text = "Opponent's Turn!";
		RefreshPlayerLabels();
	}
}

All we need to do here is update the labels so the player knows it isn’t their turn yet. I could register to listen for the board clicked events and use that opportunity to play some sort of sound effect to reinforce that its not time that player’s turn. Doing nothing is fine for now.

End Game State

Create another script named EndGameState in the same folder as the last one. Replace the default code with the following:

using UnityEngine;
using System.Collections;
using TicTacToe;

public class EndGameState : BaseGameState 
{
	public override void Enter ()
	{
		base.Enter ();

		if (Game.winner == Mark.None)
		{
			GameStateLabel.text = "Tie Game!";
		}
		else if (Game.winner == LocalPlayer.mark)
		{
			GameStateLabel.text = "You Win!";
			LocalPlayer.score++;
		}
		else
		{
			GameStateLabel.text = "You Lose!";
			RemotePlayer.score++;
		}

		RefreshPlayerLabels();

		if (!LocalPlayer.isServer)
			StartCoroutine(Restart());
	}

	IEnumerator Restart ()
	{
		yield return new WaitForSeconds(5);
		LocalPlayer.CmdCoinToss();
	}
}

This “Enter” method looks a little more complicated, but really I am simply showing a message relevant to the result of the game. Also, I increment the score of whichever player won.

I chose a player which would be unique among the two game instances to serve as a marker so that I would only trigger a new game one time. Only the host game instance will run the “Restart” method.

Summary

In this lesson we made great use of a Finite State Machine to help group the abstract concept of a game state into logical chunks. These states help simplify what would otherwise probably have been implemented as relatively complicated branches of “if” statements and organize them into easily readable code. In other words, we showed things like how to only allow game input when it is your turn, and how to make sure that help labels are showing something appropriate.

We have completed a networked multiplayer turn based game, but it’s only the first step on a long journey. There is a lot more to learn, such as implementing your own HUD to drive the Network Manager. Don’t forget about managing other events such as what happens if a player gets disconnected from a match, is it possible to rejoin? I am hoping to see more examples and tutorials by Unity on these types of topics, but if anyone finds other tutorials please share.

Also, as I mentioned before, I have very little experience with Networking and no experience with Unity’s Networking outside of this first attempt, so if anyone has any suggestions or critiques I would love to hear them!

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

17 thoughts on “Turn Based Multiplayer – Part 5

  1. After I completed the tutorial, I can no longer click the board to add a piece of the game. Any possibility you know why off-hand?

    1. Also this:
      NullReferenceException: Object reference not set to an instance of an object
      GameController.CheckState () (at Assets/Scripts/Controller/GameController.cs:93)
      GameController.Awake () (at Assets/Scripts/Controller/GameController.cs:20)

      1. The error shows that there is a null reference found on line 93 of the Game Controller script. Did you remember to connect the reference to the “Match Controller” in the inspector?

  2. Hello!

    I’ve completed this project following every step and I got one question for you.

    How can I make the Board look different on each client?

    I want for example to simulate that the board is between both players so that they are in front of each other. The handling of the clicks is easy, but I don’t know how to change the scene on each client.

    Thanks in advance.

    Javi.

  3. I just downloaded and tried your game.
    Thank you very much. this was exactly what i was looking for.
    And your tutorial gave me a very clear image about how networking works.

  4. Thanks so much for the tutorial, it’s fun, clear and easy to follow.

    I apologize if this is a obvious question but I’m very new to networking: how can I set up the game to actually run on separate machines?

    1. You’re welcome, I’m glad you enjoyed it. The short answer is that Unity sets up the code to allow the game to run on separate machines. The code that worked on the Simulator vs Executable is the same code that will run on distributed executables. How you handle matchmaking is another topic (multiplayer lobbies etc) that I haven’t dabbled in, but hopefully the Unity manual can give you a general idea.
      https://docs.unity3d.com/Manual/UNet.html

  5. Hi, I have another question.

    I’d like to adapt the game to be asynchronous, as in the two players don’t have to both be connected to the server simultaneously, but instead could take their turns at different times. Basically like Words with Friends.

    How can I go about doing that?

    Thanks again 🙂

    1. I am not sure if Unity is designed to handle turn based network games like this. It might be, but I have no experience with it, and the only examples I saw were real-time games. Because of a lack of experience and (IMHO) poor documentation on Unity’s side, I would probably resort to another option depending on my target platform. For example if I were going mobile I could use “Google Play Games Services” (Android) and/or GameKit (iPhone). I might also try things like Firebase or Photon, there are actually a ton of options outside of Unity if you look for them. Unfortunately I have almost no experience with any of those either.

  6. Thanks for your great tutorial.Really appreciate it.
    I planing to modify the game a little bit . Say, if i wanted to have a separate camera for each player and and their rotation will be 0 and 180 deg along the ‘y’ axis (like a card card game where each player sit opposite to each other).I already achieved this by making the camera as a prefab of the player and by using ‘Network Start Position’ component for spawn pos. The problem I have now is that the click hit position is always flipped for one player.Can you give me solution for that..
    Thanks again for the great tutorial..

    1. It sounds like you have modified things, so I don’t know how much will apply to your configuration. The code in my project is using the world position from a raycast, which means that clicking the same place on a surface would result in the same coordinate regardless of your camera’s position and rotation. This is also true even if you moved or rotated the board itself. If you wanted to work in a space local to the board there are other methods that might help you such as “Transform.InverseTransformPoint” which can convert a point from world space to local space.

Leave a Reply

Your email address will not be published. Required fields are marked *