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

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

Starting to get somewhere πŸ™‚

Featured on Hashnode

Context

In my last article, I mentioned that I wanted to practice more multiplayer development, so I'd like to make an online Tic-tac-toe game, hopefully with exaggerated amount of particles and VFX. I'm having fun with this experiment!

I've just started though, so here's how it currently looks when running the game:

Unity_o3g0RH6WeX.gif

I've only setup a basic scene, and the ability to highlight a game spot when you hover the mouse over it.

So let's continue the development journey of this experiment, and see what we can make happen. Since I've managed to highlight the open game spots in the game, I'd like to add the functionality of selecting an open spot and claiming it.

First Prefab

I created a new folder under the pre-made Assets folder called Prefabs. Then I dragged one of the OpenSpot GOs from the Scene Hierarchy into the Prefabs folder to create my 1st prefab:

image

Now looking into the prefab, we have a way to mark which open spot we're highlighting with the mouse, but we don't have a way to mark an open spot. Let's add a new script called GameSpotBehavior to handle that:

Adding some gameplay logic

I wanted to go ahead and add some code, but I felt like I needed to stop for a minute and think about the current problem.

Separating responsibilities

Let's pause for a bit and think about the flow of what needs to happen for a player to claim an open spot on the game board during their turn. Here's a quick diagram I made to help me think. It might not be the best or final way I build this process, but it's a working algorithm:

image.png

Also, shout out to the miro team for creating such an awesome tool! πŸŽ‰

In this diagram, we can see that there are 3 main action steps:

  • Searching for a game spot

  • Highlighting a game spot

  • Claiming a game spot

Small code cleanup

If you remember from the last article, the OpenSpotSearcher C# class handles searching for an open game spot and highlighting the spot when the mouse hovers over it. We can, (and should), separate these two actions into two separate classes, so let's do that.

Let's create a new file called GameSpotRaycastSearcher:

using UnityEngine;

static class GameSpotRaycastSearcher
{
    static private readonly float k_maxRayCastDistance = 500f;
    static private readonly int k_rayCastLayerMask = 1 << 3;

    static public bool SearchForGameSpotsUsingCamera(
        Camera sceneCamera,
        out RaycastHit raycastHit)
    {
        Ray rayFromMousePosition = sceneCamera.ScreenPointToRay(Input.mousePosition);

        return Physics.Raycast(
            rayFromMousePosition,
            out raycastHit,
            k_maxRayCastDistance,
            k_rayCastLayerMask);
    }
}

By structuring our code like this, we can now raycast into the scene looking for GameObjects that are part of the OpenSpot layer whenever we need. Note, I include the word Raycast into the class name because I'm using that UnityEngine specific feature.

Now, let's add code to our GameSpotBehavior script file that we added to the prefab earlier:

using UnityEngine;

public class GameSpotBehavior : MonoBehaviour
{
    public bool isSpotClaimed { get; private set; } = false;

    private Color m_currentOpenSpotFoundColor = new Color();

    [SerializeField]
    private MeshRenderer m_meshRenderer;

    public void HighlightOpenGameSpot()
    {
        m_currentOpenSpotFoundColor = m_meshRenderer.material.color;

        m_meshRenderer.material.color = Color.blue;
    }

    public void ResetGameSpotToOriginalLook()
    {
        Debug.Log("reverting to color..." + m_currentOpenSpotFoundColor);

        m_meshRenderer.material.color = m_currentOpenSpotFoundColor;
    }

    public void ChangeToClaimedSpot()
    {
        Debug.Log("Spot claimed!");

        isSpotClaimed = true;

        // TODO:
        // if X turn
            //ChangeVisualStyleToClaimedAs_X
        // else //O turn
            //ChangeVisualStyleToClaimedAs_O
    }

    private void ChangeVisualStyleToClaimedAs_X()
    {
        // TODO
    }

    private void ChangeVisualStyleToClaimedAs_O()
    {
        // TODO
    }
}

This class will now hold the logic of the game spot behavior, mainly when claiming the open spot. This means for an object to be highlighted when the mouse cursor hovers over it and changing it to claimed as either X or O.

Also, please note that the reference I use of the object's MeshRenderer component is now serialized:

    [SerializeField]
    private MeshRenderer m_meshRenderer;

So I set this reference in the prefab editor itself:

image.png

With this small change, I don't have to use the Start method, or make an unnecessary call to gameObject.GetComponent<MeshRenderer>();

And last but not least, let's update our OpenSpotSearcher script. It will now use the GameSpotRaycastSearcher class for searching in the game world, and it will communicate with the object's GameSpotBehavior component, to signal when to change claim the spot, and change its visual style. It will not define or care about what those two actions mean:

using UnityEngine;

public class OpenSpotSearcher : MonoBehaviour
{
    public Camera sceneCamera;

    private GameSpotBehavior m_currentGameSpotBehaviorFound = null;

    private void FixedUpdate()
    {
        if (GameSpotRaycastSearcher.SearchForGameSpotsUsingCamera(
            sceneCamera,
            out RaycastHit raycastHit))
        {
            var objectHit = raycastHit.transform.gameObject;
            var objectHitGameSpotBehavior = 
                objectHit.GetComponent<GameSpotBehavior>();

            if (objectHitGameSpotBehavior != m_currentGameSpotBehaviorFound)
            {
                ResetCurrentGameSpotBehaviorFound();
            }

            m_currentGameSpotBehaviorFound = objectHitGameSpotBehavior;

            if (m_currentGameSpotBehaviorFound.isSpotClaimed == false)
            {
                m_currentGameSpotBehaviorFound.HighlightOpenGameSpot();

                if (Input.GetMouseButtonDown(0))
                {
                    m_currentGameSpotBehaviorFound.ChangeToClaimedSpot();
                }
            }
        }
        else //no open spot found
        {
            ResetCurrentGameSpotBehaviorFound();

            m_currentGameSpotBehaviorFound = null;
        }
    }

    private void ResetCurrentGameSpotBehaviorFound()
    {
        if (m_currentGameSpotBehaviorFound != null)
        {
            m_currentGameSpotBehaviorFound.ResetGameSpotToOriginalLook();
        }
    }
}

πŸ›‘ First fail of the day! πŸŽ‰πŸŽ‰

Whoops! I found a critical error in my logic, check it out!

Unity_W9SUXPnHFl.gif

😨 It leaves all highlighted spots in blue. This is NOT what should be shown to players❗

The problem is that on every frame we tell the found spot to get highlighted, it calls the HighlightOpenGameSpot function like this:

public class GameSpotBehavior : MonoBehaviour
{
    ...

    public void HighlightOpenGameSpot()
    {
        m_currentOpenSpotFoundColor = m_meshRenderer.material.color;

        m_meshRenderer.material.color = Color.blue;
         //^^ how do we know it's not already highlighted?
    }

This means that it first gets the original color of the material (correct), but once the mouse is still hovering over the spot, this function gets called again, and the m_currentOpenSpotFoundColor changes to the new value of the material which is Color.blue! So the ACTUAL original value of the material gets lost!

To currently fix this error, I added a new isHighlighted property.

using UnityEngine;

public class GameSpotBehavior : MonoBehaviour
{
    public bool isSpotClaimed { get; private set; } = false;

    private bool isHighlighted = false;

    ...

    public void HighlightOpenGameSpot()
    {
        if (isHighlighted)
            return;

        m_currentOpenSpotFoundColor = m_meshRenderer.material.color;

        m_meshRenderer.material.color = Color.blue;

        isHighlighted = true;
    }

    public void ResetGameSpotToOriginalLook()
    {
        m_meshRenderer.material.color = m_currentOpenSpotFoundColor;

        isHighlighted = false;
    }
    ...
}

And there we go! We're back to the desired result:

Unity_o3g0RH6WeX.gif

Note: Some of you might be already thinking that I could handle this with a C# event, or perhaps a Unity Event...And you're right!

πŸ” I might look into it in the next article!

Claiming a Spot!

So now that we have these 3 important classes working together, let's add some small changes to mark the game spot as claimed.

On GameSpotBehavior, this is just some initial logic to show that a spot has been claimed:

public class GameSpotBehavior : MonoBehaviour
{
    ...

    public void ChangeToClaimedSpot()
    {
        Debug.Log("Claiming spot!");
        isSpotClaimed = true;

        m_meshRenderer.material.color = Color.green;

        ...
    }

    ...
}

Just for now, I'll stick to simply changing an open spot to green 🟒. This will visually mean that the spot is now claimed.

And on OpenSpotSearcher, update this function:

public class OpenSpotSearcher : MonoBehaviour
{
    ...

    private void ResetCurrentGameSpotBehaviorFound()
    {
        if (m_currentGameSpotBehaviorFound != null &&
            m_currentGameSpotBehaviorFound.isSpotClaimed == false)
        {
            m_currentGameSpotBehaviorFound.ResetGameSpotToOriginalLook();
        }
    }
}

And just like that, we now have the base logic for highlighting an open spot and also claiming it. Now it's a matter of differentiating between claiming as X or O:

Unity_l2Gp8OIJVg.gif

Back to using regular Update πŸ˜…

There's a principle out there called the YAGNI principle (You Aren't Gonna Need It). It states that at times people add unnecessary checks in their code, or start optimizing code before it's even needed.

I noticed that when I used the FixedUpdate function, the game did NOT pick up on the mouse click as quickly as it should. I changed to using the regular Update function for now. Even though raycasting is an expensive operation, the scene is still small enough that it doesn't affect the experience in the game so far:

public class OpenSpotSearcher : MonoBehaviour
{
    ...

    private void Update()
    {
        ...
    }
}

Back to the regular update. So essentially, YAGNI = "if it ain't broke, don't fix it!"

Also, you can think of this as the 2nd fail of this experiment 😎

Completed goals

  • Improved code structure to change the visual aspect of game spots

  • The code now supports claiming a game spot

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!

  • When has using the FixedUpdate worked best for you?

  • Can you start identifying which logic will need to be reworked for a multiplayer experience?

  • How else would you have written the logic to claim an open spot in the game, and change the visual style?

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

Β