Context
It's incredibly likely that you know the game Tic-tac-toe, almost impossible to not play it at least once in your life:
So while writing this I found out that if you google "tictactoe", you get this cool mini game! ๐
As my multiplayer development journey grows, I'd like to keep practicing and making more experiments. I wanna make an online tic-tac-toe, but with exaggerated VFX. I'd wanted to do this for a while now, because it's a very easy game to make, but still a challenge to convert to an online experience when you're still starting to shift your mindset to multiplayer game development.
And when I say to add an exaggerated amount of VFX and particles to the game board, I mean something like an actual asteroid would come in from the skies and explode on the board, marking the player's move (X
or O
):
And this is the look/feel I'd like to hopefully show in the demo, hopefully something like old ruins of an ancient temple:
Awesome concept art by: Wanxing Wang
And who knows? I might change my mind down the line. Nothing set in stone yet. But why add such level of exaggerated visual detail? Because it seems like fun, and I'd like to show how on a multiplayer game, you should only synchronize the important core data of a game(game events, game data, player stats, etc.), which is independent from the visual style of the game.
Preparing the Unity project
I created a new 3D Unity project, using the Universal Render Pipeline (URP). I'd like to take advantage of using shader graphs and also their visual effect graph technology, to practice with them.
And although I won't deal with any multiplayer code or features yet, I'd like to install the Netcode for GameObjects library, which lets you synchronize data between connected clients in a networking session:
The package can be found in the Package Manager window
Selecting playable spot with mouse
I know players will need to detect a playable spot with the mouse, so I wanted to figure out that logic first. I'll first create a scene to test how to do this. I created a new scene called TicTacToe_Sample
:
Now with this new scene open, I'll use the built-in feature to create a 3D cube...
...to create a floor object, and a test cube in the middle of the floor:
Raycasting test
I'll be creating a new empty object called OpenSpotSearcher
to detect what would be an open game board spot:
Let's add a new layer to the OpenSpot
, so that it has a unique layer mask value:
ATM, the number 3 spot was open, so I added the new layer to it:
I'll now add a new C# script also called OpenSpotSearcher
:
And the proper code for this file:
using UnityEngine;
public class OpenSpotSearcher : MonoBehaviour
{
public Camera sceneCamera;
private const float k_maxRayCastDistance = 500f;
private const int k_rayCastLayerMask = 1 << 3;
private void FixedUpdate()
{
Ray ray = sceneCamera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(
ray,
out RaycastHit raycastHit,
k_maxRayCastDistance,
k_rayCastLayerMask))
{
var objectHit = raycastHit.transform.gameObject;
var meshRenderer = objectHit.GetComponent<MeshRenderer>();
//just for testing...
meshRenderer.material.color = Color.blue;
Debug.Log("an open spot was found!");
}
}
}
Note: Since raycasting is an expensive operation, I decided to use the FixedUpdate function which does not happen every frame. The default time for the fixed update is
0.02
seconds, which is50 FPS
. Fortunately, you can modify this in the project settings.
And make sure to set the camera value!
So you might've noticed that I use a layer mask field, for ray casting, and then set the value to 1 << 3
. In C# (and other programming languages), there's a total of 32 bits in an int
type of variable, a total of 4 bytes. This is why there's a total of 32 possible layers, and every bit is used to determine in which layer does a GameObject
belong to.
Unity's own manual page on layers mentions:
The
Physics.Raycast
function uses a bitmask, and each bit determines if a layer is ignored by rays or not. If all bits in the layerMask are on, the ray collides against all colliders . If the layerMask = 0, there are no collisions.
We use the 1 << 3
operation to move that 1
three spots to the left, starting in the 1st bit out of the possible 32. In fact, 1 << 3
results in 8, which in bits is 1000
in base 2, with just one bit with the value of 1
. This operator(<<) is called the left-shift operator, and you can read more about it on Microsoft's own C# documentation.
First Experiment
Let's test this code out! I've placed 2 additional cubes on the floor, but they're not in the OpenSpot
layer, so they won't change to blue, including the floor as well:
Now let's extend this file a bit, because at the moment the open spot that's found does not get reset. It remains blue, and I'd like for that color change to be undone once the player is not hovering the mouse over the open spot. Let's modify the file so that we react to when the raycast finds another open spot, or finds nothing:
using UnityEngine;
public class OpenSpotSearcher : MonoBehaviour
{
...
private GameObject m_currentOpenSpotFound = null;
private Color m_currentOpenSpotFoundColor = new();
private void FixedUpdate()
{
...
if ( Physics.Raycast(...) )
{
var objectHit = raycastHit.transform.gameObject;
if(objectHit != m_currentOpenSpotFound)
{
ResetObjectFoundBackToOriginalColor();
UpdateFoundOpenSpot(objectHit);
Debug.Log("A new open spot was found!");
}
Debug.Log("Same open spot was found!");
}
else //no open spot found
{
ResetObjectFoundBackToOriginalColor();
m_currentOpenSpotFound = null;
}
}
private void ResetObjectFoundBackToOriginalColor()
{
if (m_currentOpenSpotFound != null)
{
var meshRenderer = m_currentOpenSpotFound.GetComponent<MeshRenderer>();
meshRenderer.material.color = m_currentOpenSpotFoundColor;
}
}
private void UpdateFoundOpenSpot(GameObject foundOpenSpot)
{
m_currentOpenSpotFound = foundOpenSpot;
var newHitMeshRenderer = m_currentOpenSpotFound.GetComponent<MeshRenderer>();
m_currentOpenSpotFoundColor = newHitMeshRenderer.material.color;
newHitMeshRenderer.material.color = Color.blue;
}
}
And now look! With this simple change, I've managed to undo the visual change to the previously highlighted object in the OpenSpot
layer:
We'll be able to use this logic later on for when we create the core logic of the game.
Setting up the board
Now at this point, the open spot can be re-used, and spread around the floor. I created 8 additional cubes to form a 3x3 grid:
I also modified the Transform
values of the scene camera to get a better view of the new grid, when running the game:
And here's how it currently looks when running the game:
Note: This is not the actual visual style I'll use to mark the current highlighted open spot during the game, but at least now I have some of base logic done.
Completed goals
- Can detect a playable spot in a tic-tac-toe game using the mouse
- Avoid checking unnecessary geometry in ray casting, by using different layers
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
Please let me know what you think!
- Are you working on any multiplayer games or demos?
- What challenges have you faced when using 3D VFX in an online game?
- What other change would you add to this classic game?
๐๐๐ Happy building! ๐๐๐