Thursday, 31 July 2025

Coding a 2 player arcade game in 2 days.

During some lessons, my friend and I got so bored that we created a variant of Tic Tac Toe.

The largest issue with tic-tac-toe is that it is too easy to not have a winner. There are 3 outcomes to any game: draw, nought         win, or crosses win. Unless you are playing a sped-up version, where each player has a set amount of time to think, usually a second, this game always ends up in a draw. The variant we created was that when the game inevitably ends in this draw, we extend the lines and expand the grid. You then must carry on from where you left of, leading to a new strategy where you must plan ahead so that you have a better chance of winning after the grid expands. 

To make this game, I used UE5. This may seem overkill for such a simple concept, but I wanted to have some nice visuals to make the gameplay cosier. This early in the development stage, I'm not sure what this could entail, but some nice graphics would be great since this game can take a long time to beat if played with someone with relatively high intelligence and enough time.

 

I began by loading the default Unreal Engine infinite map. This comes with a sky, an atmosphere, the sun and some landscapes. By the end of this project, none of these will be the same. 

I added a player, which is simply a camera in an empty body. The cameras settings are set to Film for a more cinematic feel, instead of a flat lifeless one. The camera, at this stage, looked down at an angle, but this changes further down the project.

There would need to be a central system of controlling the game, which was a central blueprint called the Game Controller. All the tiles were their own blueprint, as I wanted them to be interactive, so that when you clicked them, they would react. This meant every tile had to be connected to the central system when the game was started: 

This was the initial code for when you clicked the tile. The system would first check if the tile has been interacted with. If the variable 'has been clicked?' is false, nothing happens - you can't change a square in tic tac toe after it is set. If the tile hasn't been interacted with, it runs a function called PlayerClicked. This essentially tells the control blueprint that the tile has been interacted with by a player. Then the variable 'has been clicked?' is set to true, so that the tile cannot be changed again.

This function adds one to a variable called total clicks. When this number reaches the number of tiles, the board would be full, so more tiles would need to be added around the current play area to make the game truly infinite. 

To do this, I squared the grid length by multiplying it by itself to give the number of tiles on the board (a 3 by 3 board has 9 tiles, a 7 by 7 board has 49 tiles etc).  

The code above shows that every time a tile is clicked, it increases the total click number, then compares the total click number to the number of tiles on the board at a given time. If this number is equal, given by the "==" function, the code continues to the screenshot below:


Here, the variable grid length is increased by 2, as we are going to add 2 more rows to the board in every direction. It then runs the spawn function, which actually manages how these tiles are spawned and placed every time:




This function is arguably one of the most conceptually complex and difficult to understand functions in the game; at least that's how it felt while making it. 

For now, let's ignore why we subtract 1 from the grid length, and try and understand how the function works.

The spawn function takes in a variable known as GridLengthNonFunction. The reason this variable is called this will be clear soon, but the variable is just an odd number, such as 1,3,5 etc. It defines how wide the ring of squares spawned will be - a value of 1 would just spawn one square in the centre, a value of 3 would spawn 8 squares in a circle etc:


 Note how each square fits inside the hole of the one next to it perfectly - 1 fits inside 3's hole, while 3 fits inside 5's hole, etc. The normal tic tac toe grid would be a combination of 1 and 3. For my game, every time the grid fills up, a ring of 5, 7, 9 etc would be added every time.

From the previous code, we can now add an offset that is calculated by taking the grid length, stored in a different variable, and dividing it by 2. The reason for this is easier to explain if we imagine spawning the tiles on a coordinate grid.

If we have a coordinate system with origin (0,0), if we want to spawn a square at the middle, this is easy enough - just spawn a square at (0,0). The issue is if we want to spawn a ring around it, its origin cannot be (0,0), as it will spawn in like this, with the pink square being the first square spawned in the middle:

Clearly, this is not what a tic-tac-toe grid looks like. The reason for this is that the first square that the grid places is the bottom left one. If this is placed at the origin, the centre of the ring won't be where the origin is, and we have a misalignment. We could change the first square placed to be the bottom right square, or any square on the ring, but the issue would remain that the centre of the ring would not be centred on the axes at (0,0). For this reason, we set the origin to be half of the width of the new ring to the left, and half the height of the new ring downwards. 


 

Now we can see that the ring we added is centred around (0,0). If we apply this algorithm to every ring, no matter what the size of the ring is, it will be centred around the same point. 

Since the ring is a square, the width and the height are equal, so we can have one variable to store this offset, and make it equal to half the width/height of the ring.

 This is the most difficult part to understand. We need to run a loop that places one tile down, then the next tile, and so on and so forth until the ring is made. 

The way I chose to do this is by far not the most efficient way, but it was the only way I could come up with at the time.

My idea was to repeatedly check every possible value of X a set number of times. The number of times this would be is Grid Length - when the grid length is 3, there are 3 possible X positions.


Inside this loop, I could loop again for all the possible Y values, again, doing this the Grid Length amount of times. This way, I now have a grid of squares, all with their designated X and Y 'coordinates. 


 If I were to draw this out, I would get a square of squares, like this:

If we start from (0,0), we can see that we only end up at (2,2) at the other corner, despite this being a 3x3 square. This is why we subtracted 1 from the grid length at the start of this function: if we input 3 into the spawn function, the loop would run until it got a coordinate of (3,3), but this actually generates a ring that's 4 by 4, not 3 by 3.

Looking at this grid, we can see that the only square that stops it from being the ring we want is the central one. The coordinates of this square are (1,1). The significance of this square is that it is the only square that doesn't have a 0 or a 2 in the coordinate - every other square that we want has a 0 in the coordinate somewhere, or a 2 in the coordinate somewhere.

This is a pattern present no matter how large the grid is - here is a 5x5 grid:

Once again, we can see that the squares we need to keep all have either a 0 or a 4 in the coordinate. From here, it should be clear that to keep a square, it must have 0 in the coordinate somewhere, or 1 less than the desired size of the grid, in the coordinate somewhere, where the desired size of the grid is the number we input into the function. If we discard every other square, we are left with a ring.
 
The red lines are 'booleans', which is a fancy way to say True or False. If the two numbers inputted into the "==" box are equal, then that box is marked as true, and vice versa. This goes into an OR gate - if any one of the red lines is true, the OR box is also true.

From this, we can see that we check if the X value is equal to 0 or the grid length, and then also check if the Y value is equal to 0 or the grid length. If any one of these statements is true, the code continues, as shown by the branch on the top right of the screenshot. If none of these conditions are met, nothing happens, and the square is never spawned.

Now that we have a set of X and Y values that form the ring we want, we can actually start spawning in tiles in the positions they should be at. This script, from left to right, takes the X and Y values and subtracts the Offset Centre value from them, to centre the ring around (0,0), as mentioned before. These values are then multiplied by a constant value named separation. This just defines how far apart each tile is from its neighbouring tiles. These values then make up the X and Y coordinates of where we need to spawn each tile. The Z coordinate, or how high each tile is spawned, is simply tied above the height of the camera, so that the cubes will always spawn behind the camera. 

Now, we can spawn in a ring of however many squares we want. Here is a ring of 3:

And a ring of 13, with one cube spawned in the centre to show the origin:
 

If we spawn in a central cube, and a ring of 3 around it, we get the familiar tic-tac-toe grid:

From here, my initial idea was to use different coloured squares instead of noughts or crosses, as shown below:

But this idea looked horrendous when the grid expands, and gave weakness to the game's possible player base, with issues about colour blindness and such. To display the nought or cross, I had a decal attached to each tile, which is essentially just an image mapped out onto the surface of an object, like a sticker.

When the game starts, these are hidden by default. When the player clicks on a tile, depending on whose turn it is, one of the decals is unhidden and made visible to the player. In reality, every cube has both the nought and the cross on it, just one is invisible. For cubes that haven't been interacted with, both are hidden.
 

From here, I just made a simple UI, with 2 buttons that control the Z coordinate, or height, of the camera, and counters to display the current score on screen. There is no auto win detection algorithm, as I wanted to keep this as true to life as possible - if you don't spot your win, you don't get it!

The game's link can be found here: