Multiplayer Experiment: Tit-tac-toe - Pt. 3

Multiplayer Experiment: Tit-tac-toe - Pt. 3

Starting to add Networking!

Context

In my last article, I was able to add the feature of selecting an open spot on the field, to mark it as claimed with the click of the left-click mouse button:

Unity_l2Gp8OIJVg.gif

The next step is to differentiate between claiming as either X or O.

2nd Prefab

To keep things organized, I made a prefab out of the main game field:

image.png

The created prefab is shown at the bottom, in the Project window

Additionally, I decided to name all of the game spots to be in the order of 1 to 9:

Screen-Recording-2022-11-23-at-10.00.34-PM (1).gif

1st use of Multiplayer!

In my first post, I added the Netcode for GameObjects plugin (also referred to as NGO) which is Unity's multiplayer library that lets you synchronize data between connected clients in a networked session.

Let's start to add networking functionality. We need a GameObject to be responsible for starting a networked session, and we'll call it the NetworkManager:

image.png

Let's now add the very first NGO component which is also called NetworkManager:

image.png

This new component comes with many editable fields, but let's start with the most important one, the method that we'll use to send data in-between connected clients:

image.png

We selected Unity Transport, which will automatically add that same component to the NetworkManager object. Notice the connection details that get filled up by default:

image.png

This means that by default, the networked session will be running on the very same computer that I'm using to build my project. This is the best way of testing your multiplayer games when you're first creating them.

Planning The Game Flow

Before I continued, I needed to stop for a bit, and think things through. I went back to the Miro drawing board to think of the high-level application flow:

Screen Shot 2022-11-25 at 12.50.58 AM.png

This seems simple, but it's good enough to get us started. But let's expand on the Main Game Loop step:

Screen Shot 2022-11-25 at 12.53.20 AM.png

So now I that have drawn out an action plan, let's see if it gives us the desired results.

Let's Start to Build it!

Singleton for the win!

I need to cover Introducing the Singleton, which is a commonly used programming pattern. The Singleton ensures a class has one instance and provides a global point of access to it. Let's go ahead and create the C# script:

Screen Shot 2022-12-05 at 9.59.52 PM.png

And here are the contents of the file:

using Unity.Netcode;
using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : Component
{
    public static T Instance { get; private set; }

    public virtual void Awake()
    {
        if (Instance == null)
        {
            Instance = this as T;
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

As you can see, this class is a generic class that will ensure there's one instance of a class that inherits from MonoBehaviour, in other words, a GameObject component.

Game Turn Manager

Our first use of the Singleton class is to have 1 instance of a class that keeps track of whose turn it is to play:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum TicTacToeTurn
{
    none,
    X_Turn,
    O_Turn
}

public class TicTacToeTurnManager : Singleton<TicTacToeTurnManager>
{
    // for now, starting with O symbol
    public TicTacToeTurn currentTicTacToeTurn { get; private set; }
        = TicTacToeTurn.O_Turn;

    public void ChangeTurn()
    {
        if(currentTicTacToeTurn == TicTacToeTurn.O_Turn)
        {
            currentTicTacToeTurn = TicTacToeTurn.X_Turn;
        }
        else
        {
            currentTicTacToeTurn = TicTacToeTurn.O_Turn;
        }
    }
}

Let's now update the GameSpotBehavior class to use this new single TicTacToeTurnManager instance:

public class GameSpotBehavior : MonoBehaviour
{
    ...

    public void ChangeToClaimedSpot()
    {
        isSpotClaimed = true;

        if(TicTacToeTurnManager.Instance.currentTicTacToeTurn
            == TicTacToeTurn.X_Turn)
        {
            ChangeVisualStyleToClaimedAs_X();
        }
        else if (TicTacToeTurnManager.Instance.currentTicTacToeTurn
            == TicTacToeTurn.O_Turn)
        {
            ChangeVisualStyleToClaimedAs_O();
        }

        TicTacToeTurnManager.Instance.ChangeTurn();
    }

    private void ChangeVisualStyleToClaimedAs_X()
    {
        m_meshRenderer.material.color = Color.green;
    }

    private void ChangeVisualStyleToClaimedAs_O()
    {
        m_meshRenderer.material.color = Color.yellow;
    }
}

And check it out, we now alter the look in-between X and O turns:

Screen-Recording-2022-12-05-at-10.26.52-PM.gif

Synchronizing what we have

Starting a Network session

What NGO offers by default

When we go into play mode, you can see that the Network Manager is moved to this additional scene. This is a built-in behavior of the Unity editor:

Screen Shot 2022-12-09 at 8.57.58 PM.png

When you select the NM and look in the Inspector window, we now have 3 clickable buttons to choose from:

Screen Shot 2022-12-09 at 8.55.33 PM.png

These buttons do exactly what they say. With these, you can:

  • Start a network session as a host (server + client)

  • Start a network session as a standalone server

  • Start a network session as a client, who joins an active session

πŸ“For more information about this terminology, please visit Unity's multiplayer documentation site.

Let's add some test UI

Let's add a game object that will show a menu, called TestStartMenu:

Screen Shot 2022-12-09 at 8.59.36 PM.png

And now, grabbing a lesson from NGO's Golden Path Tutorial, let's add a quick menu that will help to start the network session.

Let's add a new script called TestStartMenu, and add it to the new game object:

using Unity.Netcode;
using UnityEngine;

public class TestStartMenu : MonoBehaviour
{
    void OnGUI()
    {
        GUILayout.BeginArea(new Rect(10, 10, 300, 300));

        if (!NetworkManager.Singleton.IsClient &&
            !NetworkManager.Singleton.IsServer)
        {
            StartButtons();
        }

        else
        {
            StatusLabels();
        }

        GUILayout.EndArea();

    }

    void StartButtons()
    {
        if (GUILayout.Button("Host"))
            NetworkManager.Singleton.StartHost();

        if (GUILayout.Button("Client"))
            NetworkManager.Singleton.StartClient();

        if (GUILayout.Button("Server"))
            NetworkManager.Singleton.StartServer();
    }

    void StatusLabels()
    {
        GUILayout.Label("Transport: " +
            NetworkManager.Singleton.
                NetworkConfig.NetworkTransport.GetType().Name);

        string mode = NetworkManager.Singleton.IsHost ?
            "Host" : NetworkManager.Singleton.IsServer ?
                "Server" : "Client";

        GUILayout.Label("Session Mode: " + mode);
    }
}

And look at that, now we have some basic UI:

Screen Shot 2022-12-09 at 9.07.50 PM.png

1st Networking Test

Let's change the Project Settings so that when we build the game it doesn't go to full screen. Change the Fullscreen mode to Windowed:

Screen Shot 2022-12-09 at 9.18.51 PM.png

Let's build the current scene. You can access the build settings under File > Build Settings:

Screen Shot 2022-12-09 at 9.10.19 PM.png

After hitting the Build button, the editor will create that build of the current scene. We can test this built game so that it connects to an active networking session:

1stNetworkTest.gif

As you can see, technically I'm starting a networking session with the editor, and then joining the network session with the built instance. There's no synchronization happening for now, but this is currently expected.

Change the Game Spot to Network

I need to add a few changes to the GameSpotBehavior script to make it access networking functionality from NGO.

We'll need to make the script inherit the NetworkBehaviour class. This is NGO's default scripting class to inherit when you want to access networking functionality. And the first feature that we're going to use is NetworkVariable. As the name mentions, these are variables that have built-in synchronization features for multiplayer games:

public class GameSpotBehavior : NetworkBehaviour
{
    public bool isSpotClaimedGetter => isSpotClaimed.Value;

    readonly NetworkVariable<bool> isSpotClaimed = new NetworkVariable<bool>(
        false,
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Server);

    readonly NetworkVariable<bool> isHighlighted = new NetworkVariable<bool>(
        false,
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Server);

    ...
}

But now we need to fix something. NGO is giving us the following error message because we have to make some important changes to the OpenSpot prefab, which was previously NOT made to access NGO functionality:

Screen Shot 2022-12-09 at 10.03.05 PM.png

Let's open the prefab, and add a very important component, the NetworkObject component. This component marks game objects as ones that should access functions and features from NGO.

πŸ‘€I'll show the status of the project with these changes in the next post!

Completed goals

  • Started to use the Netcode for GameObjects plugin, to start a networking session

  • Tested that a network connection is made between a host and a client

  • Started to change existing prefabs to access NGO's features

Lessons learned so far

  • Proper use of layer masks in ray casting

  • There's a reason there are only 32 possible layers in a Unity project

  • Again, YAGNI. Do NOT optimize before you need to! πŸ˜‚

Please let me know what you think!

  • Are these changes I'm doing making sense?

  • Have you used the Singleton pattern before?

  • What do you think of the current design of the game flow?

πŸŽ‰πŸŽ‰πŸŽ‰ Happy building! πŸŽ‰πŸŽ‰πŸŽ‰

References

Β