Blocks of Code


Blocks of Code


Thematically, puzzles in Gitch respresent blocks of source code. The 'block' is composed of several smaller pieces which can each be removed and manipulated independently. To solve a puzzle, players have to arrange the pieces to connect together traces on the surface of the cube. It's been a really interesting problem to work with, so I thought I'd take the chance to talk about it.

Naive approach

For the original prototype, I simply broke apart a 3D model of a puzzle Charlie had given me, and considered the puzzle solved if all the pieces were in their original positions and orientations. This was fine for developing other game systems but it had some flaws of its own. Since every piece *had* to go where it started, duplicate pieces or those with rotational symmetry would break the system. Not only that, but it would require a custom 3D model for each and every puzzle! Clearly, there was some work to do.

A Custom File Format

Each face of a Gitch puzzle can be blank, an end piece, a corner or a straight line. When all different available rotations are taken into account, this makes 12 possible faces, or 13 with blank faces included. Moreover, each of a face's four edges can either have or not have a trace on it. By agreeing on a winding order, we labelled a face's edges 1, 2, 4 and 8. Now we were able to respresent a face with half a byte (a nybble!) of data.

While we could store individual faces, we were now left with the problem of reading our encoded data and creating a puzzle out of it. We looked at several cube nets and picked a sideways cross, as it gave us the most space in contiguous chunks - fewer special cases to work on. With this, we wrote an engine to map positions from a flat byte array to a cube. For this engine it was critical to consider which edges joined to each other, taking into account relative rotation. Looking at a flattened cube net, some pairs of edges require a 90 or 180 degree rotation to match up to each other.

This rotation issue proved to persist through various stages of the transformation between 1D and 3D, and required novel solutions. While it was easy enough to get the faces for each side of the puzzle - simply split the flat array into 6 - there was no easy way to figure out the rotations of the pieces, or in which order to place them down. To get the faces in the right place, you'd have to specify which corner of the side to place the first face, whether to fill row-first or column-first and how many 90-degree increments to rotate faces in such that they lined up not only with other faces on the same side, but also with faces on neighbouring sides.

There may have been a purely mathematical solution to this problem, but to this day I haven't found one. Instead, upon realising the nature of the problem I decided to encode the various decisions into a byte and then place and rotate faces based on that. This meant that while I couldn't say for sure what the transformations for a particular side of the puzzle needed to be, I could brute-force the solution relatively quickly by incrementing a byte. Somewhere deep in Gitch there are some hard-coded, random-looking byte arrays, and they are critical to the function of the system.

Arts and Crafts

Now we had a data format for our puzzle, and an engine that could process it and calculate the solution state of a particular puzzle instance. The problem still remained of actually creating puzzles, and adding them to the game. The critical features of this process would be speed an accuracy, as we intended on having several tens of puzzles in the final release and therefore needed to build them reliably and without relegating someone to the task of puzzle builder for the remainder of the project. We set about cutting up graph paper into actual cube nets and drawing lines on them in order to produce a flattened solved puzzle. We could check if edges lined up by physically folding up the net into a cube. With this paper respresention, we could unfold the net and read off encoded values to one of the 13 states identified earlier. We named our format ".pzl" and set about making some custom data files with a hex editor.

This approach worked, albeit very slowly. The workflow to create a puzzle started with arts and crafts, before progressing to manual and error-prone data entry, and finally invoking a custom asset importer to turn our nybbles into a byte array, and serialized in a way Unity could understand. The serialized asset contained not only the flat byte data, but also some code for checking and validating said data. Our engine could check if the byte array was 'solved', so proved essential for finding encoding errors. When finding one of these errors, we'd have to carefully check the 3D puzzle in Unity versus our paper nets to find the discrepancy, and then change the corresponding nybble in the pzl file before re-importing. Certainly an improvement over custom 3D models for each level, but still not a workflow as quick and pain-free as we'd have liked.

The Future is 3D

The next iteration of the puzzle construction process was to be a 3D tool within Unity to create serialized assets. Up until now I'd not had much experience with ScriptableObjects, and so was eager to leverage them to speed up development. The benefit of a 3D tool over bits of graph paper and hex editor is that it's substantially faster to iterate, can validate a puzzle on-the-fly and most importantly doesn't require any understanding of the byte representation of a puzzle, or the engine that drives it. This means that non programming team members can use it, spreading the workload and leaving us time to focus on building new features and improving on years-old system designs.

With the puzzle builder came a rethink of the game's data structure as a whole. Instead of setting array of values in MonoBehaviours the level scene to be indexed by a level number, we moved the arrays to ScriptableObjects where they were safely outside any level concerns. This meant the level data was also available at compile-time, so we added another feature to the puzzle builder to automatically place built puzzles into the level structure at the click of a button.

So, by developing our understanding of the problem and developing tooling instead of focusing purely on game features we've taken the time and effort requirements of adding puzzles to the game to almost nothing. A year ago, adding a new 2x2 puzzle would have taken somewhere on the order of an hour - first cutting out a shape on graph paper, then drawing on it while folding and unfolding it to check, before encoding it as hex values and finally importing it - possibly with errors - into the game. Now, we can add a new small puzzle in under a minute, without having to know anything about how it works under the hood. I'm pretty happy with that.

My work on the puzzle builder probably isn't done - it's still a little rough around the edges. For example, it can't load and edit existing puzzles, and will also happily obliterate any saved puzzles you've created if you try to save another one with the same name. On top of these quality of life aspects, the larger levels still take some time to put together. A 5x5 puzzle has 150 individual faces, all of which have to be set manually, so we can expect to take about a thousand clicks per puzzle when working at that scale. A feature that may be worth adding is a randomiser to set most of the puzzle's faces to junk, and then go in and tweak them manually to make the puzzle valid. Going further than that, it should be possible to use pathfinding algorithms to generate totally solved puzzles, and even tweak the difficulty of the procedural result. Those are topics for later posts, though, and I think it's time to get back to working on the game proper for a little while before going back in my workflow optimisation cage.

I don't have much of a conclusion to share for this post, besides it being a great little anecdote for how much you can gain by working smart instead of hard. The first 3 puzzles in Gitch took about a year to add, the following 7 took an evening.

-Cat

Get Gitch

Leave a comment

Log in with itch.io to leave a comment.