We’ve been working on the Quench pipeline tools for a couple of months now, and despite the apparently endless barrage of school assignments that keep slowing things down, we’ve managed to reach our first major milestone! As I mentioned in this post, our plan has been to closely integrate a hex-based pixel art editor called Hexels with Unity as a 2D map editor to let our map designers more easily do their work. We’ve got the initial stages of this process working and from this point forward we’ll be making more and more map features in Unity editable from Hexels.
I figured that I would run through the stages that we’ve passed through on the way to a working (but still pretty unstable) product.
When this integration process began, we planned to utilize Hexels’ XML output feature to pass data back and forth between it and Unity, but after a short email conversation with Ken Kopecky of Hex-Ray Studios (the developers behind Hexels) it was made clear to us that Hexels doesn’t actually read its own .XML output format.
With that in mind, we realized that we’d have to bite the bullet and decode the Hexels binary .HXL file format. Thankfully Ken is amazing and happily provided us with a specification to follow in the process of building a C# .HXL reader/writer (Hexels itself is written in C++). Ultimately the power of Hexels has been well worth the effort, as it provides us with a ton of map editing features that would have been a mountain of work to implement from scratch in Unity as Editor extensions.
This process of building up this reader/writer has been James’ work over the last while, and he has dug through a ton of binary data to get things working. Unfortunately the file format spec wasn’t as accurate as we might have hoped, so at times James just had to pick through the data by tracing out a big block of hexadecimal and reading it at face value. James is a stubborn fellow when it comes to debugging so he wanted to sort things out without asking Ken any unnecessary questions, and it was slow going, but it got done!
Meanwhile, I was busy writing most of the Unity Editor extensions responsible for connecting with this soon-to-be .HXL reader/writer and providing the facilities to render the hex map in 3D.
Creating Hex Maps
The first process that needed to be built was clear: We need to be able to automate the process of laying out thousands of hexes on the map. I don’t know too many people patient enough to do that manually. So that was my starting point.
Having perhaps WAY too great of a personal interest in hexagonal geometry, this actually didn’t take too long to get working. At least for a map containing no actual data. My first attempts looked something like this:
In order to provide everyone a clean experience in using the pipeline tools that I was starting to build, I built up a menu system to allow map designers easy access to all of the pipeline macros that do the heavy lifting (and to let me test more easily). Below is the for that this menu takes as of today:
With the bare basics working, the next big move was to implement the code to integrate with James’ Hexels reader so that I could load up a .HXL file and deform the hex grid based on the ground height values read in from the binary file. Our game designer and map designer Bill got to work on some basic map concepts to try out, and before long I had something basic to test.
The coding work to read a file in took a little longer, but lucky for me, James had the .HXL reader working around the same time that I finished up the integration and Bill’s map looked just like this:
The Hexels source data looked like this:
Pretty sweet progress for not a lot of work!
To begin with we thought that the process of using Hexels to build maps would involve selecting colours to define the terrain height and we would use a grayscale palette. In this way, we could read the RGB colour value directly, assuming that black was 0 height, and white was maximum height. We figured that we’d build a palette of some 16 grays to use and make maps using those distinct height levels.
However, as soon as we read this map into Unity, it became clear to us that this process wouldn’t suit the style we wanted for Quench, and that this process was much more work than it was worth. We took another stab at map importing using a process where map designers paint Hexels files with only a white brush, but using opacity. By multiplying opacity with white, I could determine the height of a given hex while making it much easier to paint terrain and getting a much more natural feel. Our 2nd try looked like this:
The Hexels source data looked like this:
With results in hand like this we’ve decided that this will define our production process for map design, since it’s much faster to work on heightmaps this way and you get much more visually interesting results.
Here’s a closer view of the shading effects on the terrain:
We’re not finished with the terrain shading effects by any means, but we’re definitely heading in the right direction.
Reality Strikes Back
While proudly showing off my work to my classmates who mostly didn’t have as much done toward their capstone projects as James and I had, it became clear that our plan was not very scaleable (karma?). Map sizes beyond 100×100 hexes took minutes to generate. A Unity scene saved containing a generated map as small as the 48×48 maps shown above would cause the Unity Editor to crash silently if the scene was loaded, or even if the user attempted to play the scene. Very bad news!
It was obvious that this was a pretty major setback, but I had always been suspicious that our plan to treat each hex as an individual GameObject wouldn’t scale. I always wondered when the decision in favour of simplicity would catch up to us and force us to do something more complicated. Turned out that we didn’t need to wait very long for reality to bite us.
Digging around in the Unity Editor log file, we discovered that the silent crash was caused by a StackOverflowException. Now, our scene files weren’t THAT large. There was no way that we should run out of memory while loading a scene, and we had no recursive processes of our own that might trigger such an error. Our best guess after some deep thinking, consultation with our capstone supervisor and a UnityAnswers question was that Unity’s .unity scene file loading process is recursive and that relationships between GameObjects cause this process to grow in memory cost exponentially.
Ultimately, the problem we were encountering was one of too many GameObjects. This really came as no surprise in hindsight, seeing as a map as small as 48×48 is still over 2300 GameObjects if each hex is represented by a GameObject. Nevermind the map sizes of 200×200 or greater that I had in mind for larger maps.
Fortunately, I already had a plan in mind already.
Similar to voxel engines that use “chunks” to clump large volumes of blocks together for processing, we can group large areas of hexes together for processing by batching them as 16×16 (256) hex pages that are represented as a single GameObject in Unity. There’s no question that this plan makes the process of building a mesh for the whole group of hexes more difficult and that interfacing with the data model gets more challenging, but it would solve our problem.
And so I spent a weekend building a paging system for grouping hex geometry together.
Pages and Stitches
Modifying the code I had already built to lay out the entire map, I created a class to lay out a single page of hexes as a group. It looked like this to start:
If you’re wondering, the subtle wave effect is Perlin noise that I injected into the ground height data that the map contouring process uses. It was a good test and it looks really pretty too.
A little more work to lay pages out next to one another and things took on a form very close to what I wanted, but with a problem that I knew was going to be awful to fix…
If you’re looking very carefully, you can see perforations running horizontally and vertically across and down the map. These perforations demarcate each page that comprises the whole map. This is a defect in the height contouring that emerged at the edges of each page because I missed a couple of cases when setting up a terribly huge if/switch block that decides which hex data structs need to be looked at to perform the contouring (thus why this was going to be awful to fix). Ultimately this amounts to the height being miscalculated by averaging values for the wrong hexes together at the edges of a page, causing the stitching between them to fail miserably.
Here’s a closer look at the problem:
And here’s a bit of the paperwork I did to actually FIND the defect…
Turns out that I missed 2 obscure cases (literally the corner cases!) that caused the edges of each page to not stitch together correctly.
A lot of writing, figuring and 2 single-line fixes later, I had fixed the problem:
So did my fix resolve our problem? Yeah. It did so rather well.
As a bit of a demonstration, this following image shows a 16×16 hex page, so a group of 256 hexes, selected on a map:
Zooming out a little bit…
And this is the whole map:
You are looking at a 960×960 hex (60×60 pages) map. That’s nearly 1,000,000 hexes. And Unity will load scenes and play them without any complaint. Of course, this particular map is gigantic and consumes more than 3GB of RAM to run in play mode, but it will run on my PC without overflowing the stack, and that’s a big win. In fact, if this map were any bigger I couldn’t deploy it as a 32-bit application anymore because I’d need a 64-bit address space to address all of the memory it’s using.
Our target hardware is an Android Tablet with as little as 1GB of RAM, so clearly we’ll be using map sizes much smaller than this for Quench, but it’s clear that our problem with too many GameObjects has been very thoroughly resolved.
Going forward with this technology, the system technically supports dynamically-loaded pages, meaning that we could provide an API to create endless maps with no loading screens for future projects.