Skip to content

Realtime Multiplayer

This tutorial shows how to create a real-time multiplayer version of the Tic Tac Toe game using the Photon PUN SDK, Unity and ChilliConnect.

Just like the Async Multiplayer Tic Tac Toe example the full Unity project for this tutorial is available on the ChilliConnect GitHub samples repository under the folder "TicTacToeRealtime".

 Download Sample Code

Configure Settings

There is some configuration to do before this project will run under your own game defined in ChilliConnect, most of which is covered in our Realtime Multiplayer Guide. Follow this guide to setup both Authentication and the WebHooks. The WebHook path we will be using will be the PathEvent so that we can store a score against the Player on winning a game.

The Photon App ID that you get from the Photon Dashboard will need to be placed into the Sample project in two places; Under Assets > Photon Unity Networking > Resources > PhotonServerSettings.asset there is an AppId configuration option, copy your AppId into this field first.

This AppId will also need to be put into the m_photonApplicationId private variable in the PhotonController.cs file. As with other tutorials you will also need to change the GAME_TOKEN constant in SceneController.cs to your ChilliConnect Game Token.

Connecting to Photon

The PhotonController.cs file handles the initial authorisation and connection to the Photon Cloud when the game is started. When the game has finished the initial setup process and has a logged in Player stored, a time limited access token is generated on ChilliConnect and passed back to Tic Tac Toe.

public void LoadPhotonInstance()
    {
        UnityEngine.Debug.Log ("Photon Multiplayer - Starting Photon Access Token Generation");

        m_chilliConnect.Multiplayer.GeneratePhotonAccessToken(m_photonApplicationId, (request, response) => OnPhotonAccessTokenRetrieved(response), (request, createError) => Debug.LogError(createError.ErrorDescription));
    }

When this token has successfully been returned it is immediately used to connect to Photon Cloud (the token lasts for 5 minutes so it's best to connect as soon as possible).

private void OnPhotonAccessTokenRetrieved(GeneratePhotonAccessTokenResponse photonAccessTokenResponse)
{
    UnityEngine.Debug.Log ("Photon Multiplayer - Retrieved Initial Access Token: " + photonAccessTokenResponse.PhotonAccessToken);

    PhotonNetwork.AuthValues = new AuthenticationValues();
    PhotonNetwork.AuthValues.AuthType = CustomAuthenticationType.Custom;
    PhotonNetwork.AuthValues.AddAuthParameter("PhotonAccessToken", photonAccessTokenResponse.PhotonAccessToken);
    PhotonNetwork.ConnectUsingSettings("1.0");  
}

ChilliConnect uses a Custom Authenticator to validate the Application attempting to connect, which is why we define PhotonNetwork.AuthValues.AuthType = CustomAuthenticationType.Custom; and use the manual connection method defined by Photon PhotonNetwork.ConnectUsingSettings("1.0");. The value that is provided to ConnectUsingSettings is the GameVersion. A vital part to using Photon within Unity is the use of Callbacks that are called when certain operations are complete within Photon. The first instance used of this is the OnConnectedToMaster Method:

void OnConnectedToMaster ()
{
    UnityEngine.Debug.Log ("Photon Multiplayer - Connected: " + PhotonNetwork.connected);

    PhotonNetwork.JoinRandomRoom();
}

When the Photon Servers have processed the connection request and everything is successful, this OnConnectedToMaster method is called, and will immediately try to join a random room. We expect this to fail the first time actually calling this method as there will not be any rooms to join, which is fine as we use another Callback method to handle this.

void OnPhotonRandomJoinFailed(object[] codeAndMsg)
{
    Debug.Log("Photon Multiplayer - No Rooms Available, Creating New Room");
    PhotonNetwork.CreateRoom(null, new RoomOptions() { MaxPlayers = 2, PlayerTtl = 20000 }, null);
}

The last method that is called in the controller, this line simply tells photon to create a room with a timeout of 20 seconds and to have a maximum of 2 Players at any time.

The room is now set up and ready for Players to join. The RoomController.cs file handles any operations that happen in a room.

Joining the Room

When the a Player first starts the Tic Tac Toe application they attempt to join a room, and when that operation fails they create a room and wait for an opponent to join. There are two methods that control what happens when a Player that exist in RoomController.cs, one that handles the Player that has just joined the room and a callback that handles the Player that initially created the room.

When Player 2 finds and joins the room the OnJoinedRoom Photon Callback is triggered;

void OnJoinedRoom()
{
    UnityEngine.Debug.Log ("Photon Multiplayer - Current Player Count: " + PhotonNetwork.room.PlayerCount);

    if (PhotonNetwork.room.PlayerCount == 2) {
        Debug.Log ("Photon Multiplayer - Room Found, Connected.");

        OnRoomJoin ();
    }
}

This method is also called whenPhotonNetwork.CreateRoom is used, as the Player who creates the room also has to join, which is why we only take an action when there are two Players in a room. When Player 2 does join the room OnRoomJoin is triggered in the SceneController.cs and Player 2 is set to the 'O' side of the board, and the UI elements are changed to display that it's Player 1's turn.

Player 1 still needs to be informed that Player 2 has arrived and have their game activated. There is another Photon Callback we use when another Player connects to the room:

void OnPhotonPlayerConnected(PhotonPlayer newPlayer)
{
    UnityEngine.Debug.Log ("Photon Multiplayer - Current Player Count: " + PhotonNetwork.room.PlayerCount);

    if (PhotonNetwork.room.PlayerCount == 2)
    {
        UnityEngine.Debug.Log("Photon Multiplayer - Starting Turn for player X");

        OnGameStart ();
    }
}

OnPhotonPlayerConnected is called, validates that 2 Players are currently in the room then calls OnGameStart in SceneController.cs. Player 1 is set to Player 'X' and the UI elements are updated to reflect this, as well at the buttons on the board being activated. The first turn can now be taken.

Taking a Turn

Player 'X' can now select an empty position on the board to take a turn, and locally this will update their board view to reflect the selection, but Player 2 does not know this new board state yet. In order to inform the other Player in the room of a turn being taken we use Photon's RaiseEvent method. OnTurnEnded is called in SceneController.cs when a selection has been made, which in turn activates the TriggerEvent method;

public void TriggerEvent(byte evCode, string payload, bool setWebFlag)
{
    RaiseEventOptions options = new RaiseEventOptions ();

    if(setWebFlag) {
        options.ForwardToWebhook = true;
    };

    PhotonNetwork.RaiseEvent(evCode, payload, true, options);
}

OnTurnEnded has a quick check to see if a Player has won yet, and if not will take the current state of the board, in string form and pass it into RaiseEvent along with the Event Code 0. We use this Event Code to determine the state of the game later. There is another Photon Callback method we use to catch when a Player has taken a turn;.

private void OnEvent(byte EventCode, object BoardState, int reliable)
{
    string boardState = (string)BoardState;

    if (EventCode == (byte) 0) {
        UnityEngine.Debug.Log ("Photon Multiplayer - Player Has just taken a turn, New Board: " + boardState);

        OnNextTurn (boardState);
    } else if (EventCode == (byte) 1) {
        UnityEngine.Debug.Log ("Photon Multiplayer - Game Over, Player 2 wins");

        OnGameEnded (boardState);
    }
}

OnEvent is called when Player 'X' takes their turn, and Player 'O' now has the updated state of the board. OnNextTurn takes this string, applies it to the current game and activates the UI again so that Player 'O' can take their turn. This will continue until one Player meets the win condition, which is where the Event Code comes into play. Having the Event Code set to 1 will tell the receiving player that the game is over and to display the victor on screen.

Storing the Result

We now want to store the result in a leaderboard when the game ends, we can do this using a combination of Cloud Code and Photon Webhook functionality. When the game is won, another event is fired and sent using RaiseEvent with the Event Code set to 1, but there is another difference here. When we're telling the application to send the event to the other Player, we are now setting a flag on the method to tell Photon that we also want it to trigger a Webhook when called.

We set the flag during the turn end inside a conditional;

public void OnTurnEnded(string boardState)
{
    CurrentMatch.Board = boardState;

    if (!gameController.IsGameOver())
    {
        gameController.ShowChilliInfoPanel(MESSAGE_OPPONENT_TURN);
        roomController.TriggerEvent (0, boardState, false);
    }
    else 
    {
        roomController.TriggerEvent (1, boardState, true);
        CurrentMatch.MatchState = Match.MATCHSTATE_GAMEOVER;
    }
}

This in turn calls the TriggerEvent method with the indicator that RaiseEvent must also have it's ForwardToWebhook flag set.

public void TriggerEvent(byte evCode, string payload, bool setWebFlag)
{
    RaiseEventOptions options = new RaiseEventOptions ();

    if(setWebFlag) {
        options.ForwardToWebhook = true;
    };

    PhotonNetwork.RaiseEvent(evCode, payload, true, options);
}

This will let Photon know that we want to trigger the PathEvent WebHook when RaiseEvent is called.

You will need to set up the webhook configuration on the Photon Dashboard (as discussed previously) for it to properly call the 'saveVictorScore' script, as shown below and in the previous Multiplayer section.

We need to set up the Leaderboard on the ChilliConnect Dashboard to hold the final scores. As we want wins to stack up over time the scoring type will be cumulative.

A Cloud Code script is set up to add 1 to the currently logged in ChilliConnect Player who initiated the PathEvent WebHook. This is added under the new PhotonWebhook script type with the "PathEvent" option selected.

The script contents are quite simple:

ChilliConnect.Logger.info("Start: saveVictorScore.");


try {
    var leaderboardKey = "TICTACTOEWINS"

    ChilliConnect.Logger.info("Saving score against user.");

    ChilliConnect.Leaderboards.addScore( 1, leaderboardKey );
}
catch( e)
{
    ChilliConnect.Logger.warn("An Error has occured when saving TicTacToe victory data.");
    ChilliConnect.Logger.error("Error:" + e );
}

ChilliConnect.Logger.info("End: saveVictorScore.");

return ChilliConnect.Photon.Response.setMessage("Successfully saved score");

Now ChilliConnect will increment a Player`s score every time a game is won.

Dealing with Disconnects

Due to the nature of mobile gaming, network issues and unexpected disconnects are to be expected. In our case it is very simple to handle these cases though, and Photon has another Callback that is triggered when a player leaves the room.

void OnPhotonPlayerDisconnected(PhotonPlayer newPlayer)
{
    UnityEngine.Debug.Log ("Photon Multiplayer - Other Player has Left");
    UnityEngine.Debug.Log ("Photon Multiplayer - New Room Count: " + PhotonNetwork.room.PlayerCount);

    OnPlayerQuit ();
}

When an opposing Player disconnects or closes a game in progress, OnPlayerQuit is called to reset the state of the game board, display an appropriate message to the Player, and then wait for another opponent to enter the room to start another game.