UT³ retrospective
Ultimate Tic Tac Toe (UTTT) is a fantastic game in it's own right, besides being the simply superior form of Tic Tac Toe. I'll presume you already know what Tic Tac Toe is, and just get to the "Ultimate" part, and then explain how that became UT³. A brief overview is that this is UTTT, but digitised via the Unity3D game engine. I didn't find a suitably simple version of it online, so I just made my own, as we were tired of drawing on maths paper.
What is Ultimate Tic Tac Toe?
Normal Tic Tac Toe (TTT) is so boring, good players will just about always end up in a draw. The only way to improve complexity and interest in the game is to expand the game board... except then that's just Connect4 or something similarly lame. So instead of that, let's not increase the size of the board for an individual game, and instead we'll simply add more games! We'll copy the 3x3 layout of squares, with a 3x3 layout of classic 3x3 TTT games.
And now for one last little touch... You play all the games simultaneously, because they're all linked, and a part of the "big" overall TTT game.
For example, Purple plays in the top-left square of the central TTT game. Orange must now play somewhere in the top-left TTT game (often referred to as "sending" or "being sent" to play in that small TTT game).
The overall goal of the game is to win the "big" game, by winning the smaller TTT games. Once a small TTT game is won, that whole game is coloured in as a square for the "big" game. If a player is "sent" to that game/square, they get to play in any TTT game on the board. Once a player gets three small TTT games in a row, they win the game of UTTT.
UTTT -> UT³
This was back in physics extension class, Year 12 of my schooling. We made very productive use of our study time to play UTTT, of course. But even we got tired of drawing out all those squares over and over (we'd get a game done every 15-20 minutes during an hour class), thus I started contemplating digitising the process. I had previous experience with the Unity3D game engine, and as it had been some time since I'd done anything game dev-related, I wanted to give it a proper go.
I was also pretty committed to doing this properly, as opposed to most of my other projects at the time, which ended up only getting to the "it kinda works, I suppose" stage. In addition to that, this was the first real project I'd both completed in Unity3D, and also the first project that had real purpose, to be used by more than just me. Thus, there were extra rigors placed to ensure it didn't crash and do generally stupid stuff if the user was a monkey.
The actual how/what
A reasonably new feature to me, and also recently updated in Unity3D, was the prefab workflow. Everything (gameobject) in the game is actually a prefab, instantiated as necessary.
Before the game starts, the only pre-existing gameobjects are a camera, a canvas, and the event system for the canvas.
Once the game starts, a simple menu is instantiated. It containts a little bit of text for the title, and then three buttons. The only difference between the buttons are their positioning, their text, and what number they send along with the OnClick()
function call. And even then, the latter is set via code.
GameObject startPanel = Instantiate(StartPanel, canvas.transform, false);
Button two_button = startPanel.transform.GetChild(1).GetComponent<Button>();
UnityAction two_action = new UnityAction(delegate { buildBoard(2); });
two_button.onClick.AddListener(two_action);
Button three_button = startPanel.transform.GetChild(2).GetComponent<Button>();
UnityAction three_action = new UnityAction(delegate { buildBoard(3); });
three_button.onClick.AddListener(three_action);
Button four_button = startPanel.transform.GetChild(3).GetComponent<Button>();
UnityAction four_action = new UnityAction(delegate { buildBoard(4); });
four_button.onClick.AddListener(four_action);
Yeah, but how does it all get constructed?
Quite simply, actually. There is a simple prefab for the whole board background, with a "vertical layout group." That just means any children are sorted into rows. Which naturally leads on to the children: row prefabs.They consist of a horizontal layout group, of course. Thus, the last instantiated object are the squares themselves.
You might have spotted one inconsistency there: why use a vertical layout group for rows, and then immediately put a horizontal layout group to create a grid, when the grid layout exists as a single component?
Well, one simple reason: GetChild()
. That's the Unity3D way of grabbing the nth child of a particular gameobject. Except, with a grid, every single cell is a direct child. Splitting it the way I have done means that I can index via row and then column with two separate GetChild()
calls.
There's one more question left: I only mentioned one set of rows and columns, to instantiate the "squares." Squares are not TTT games. Or are they?
In this case, yeah they are actually. I just repeat this process again, using each big square as the parent. I even use the exact same squareController
script for the large squares as for the small ones. It is literally a perfectly duplicated and nested structure.
This makes all the code to handle things pretty neat and easy, and also, expandable. All the instantiation, nesting, spacing, layout, etc. is controlled via the code while creating the board...
So I can literally pass any number I want for board creation, and it'll just work™. Yeah nah I'm serious about that, I only built it to do 3x3, and when I got that working, I thought, "what happens if I put in a 2 or a 4 instead?" And it worked, literally first time.
But how does it work?
Fine, fine, I didn't do 100% of it myself. I copy-pasted some code from StackOverflow to check the various winning conditions for TTT. The rest of the logic is a bit too complex to dissect in a blog post though, so I'll touch on the highlights. Each small square reports back to a single function in the main gameManager
script. Then the logic is handled pretty much as you'd presume, by analysing where that square exists in it's own TTT game heirarchy, and applying that to the overall heirarchy of the big squares for the next turn, by the opposing player.
There's a bit of extra work to do to handle some awkward edge cases and states, such as when the next player will have to "play" in a TTT game that has already been won. Winning a small game is also surprisingly simple in contrast: use the squareController
script to set the colour and mark it as won, and delete all children (small squares).
By far the trickiest part was handling: highlighting. One of the key features to improve ease of use and gameplay over the pen and paper version is to highlight both where the player can play, and where the other player will play next. And do it all before you actually play your move. This gets handled when the cursor hovers over any single square, and has accompanying logic to highlight the bigger square accordingly (and the actual hovered square, if it is valid to be played on). Naturally, it must handle all the edge cases and states too, such as when a game has been won, but is due to be "played" on via the highlight, or when a player can play anywhere.
Serialisation and misappropriation
How do you save game state?
This is the one bit I might change, if I redid it. First question: what is the board state? Well, it's a bunch of squares, that can be invalid, valid, not won, won, and exist in some coordinate system. Back when I was doing it, the first thing that sprung to mind is, "they're all just numbers!" And more than that: integers. And I knew that Vector3
existed, and even Vector4
because Unity3D can be made 4D. There are also integer versions of those, as they are for floats.
So let's make it happen: for the large squares/games, we need to store X, Y, "won" state, and validity. Easy peasy!
Let's check for the smaller squares: X, Y, x, y, "won" state, and validity. Uh oh, that's six!
Unfortunately, a Vector6Int
doesn't exist.However... the internet does! A quick search later revealed that someone had done the work to extend Unity3D's Vector4
to be Vector5
. Thus, I copied it in, and set about butchering it to be a Vector5Int
. I didn't immediately realise I needed to save square validity as well.
When I did realise, it was reasonably easy to just extend it to be the desired Vector6Int
, and safely store all the info I needed. These could then be accumulated into a list, and serialised through a binary formatter into a file. Reversing the process was then pretty straightforward, and equally easy to set the game state once the board had been created.
Thus concludes my foray into poor serialisation, and the retrospective on UT³.
Where can I get this wonder?
All the source code is of course available on my GitHub, like all my projects.
Here's a direct link for you lazy folk: https://github.com/theonlytechnohead/ut3
And lastly, it's also available in WebGL form: /webgl/ut³