Synthrally Feature Breakdown: Hexagon Lattice

 

This is a post explaining how I implemented the menu transition visuals seen in this video:

In This Post:

  • What my process was in implementing these visuals, and how I converted the original idea to reality
  • Explanation of the complex 3D math involved in the menu visuals
  • Breakdowns of the menu transitions and idle animations
  • Brief videos showcasing both of the above
  • Possible improvements that could be made / features that could be added

Let’s get right into it!


Part 1: The Idea

My vision for Synthrally’s visuals is to invoke the feeling of a futuristic arcade machine.  Since the very beginning of my visual overhaul, I wanted the menus to have presence; as if they were actually there on a physical machine, and not just UI that were sprites just billboarded to the screen. To that effect, I had this idea of the buttons being embedded into this background grid that had horizontal wipes whenever there were menu changes, as if the buttons were being created and destroyed as the menu switched.

I was inspired a bit by the transitions in Heroes of the Storm; when you get to the hero select screen in that game, a shockwave travels through a grid from the bottom of the screen to the middle, and the tiny bases that the heroes stand on flip over in that direction as they materialize on the grid. I felt that this gave those stands and the background pieces around them a degree of physicality, since you can see that they are clearly 3d objects and not just an infinite plane with a tiling texture on it. Realizing that I could merge this concept with the hexagon lattice I had experimented with as a level background, I took some time to break down the separate features that I would need to accomplish this into separate items on Trello cards, seen below:

With the feature broken down into parts, I immediately started work on getting the base parts functioning first.


Part 2: The Math

Getting the lattice arranged wasn’t too difficult; I already had some arranged hexagon meshes, so I just took those and played with them a bit until they were all neatly arranged. (This was probably the least fun part of this whole feature). The base grid object looks like this:

After this, the next part was to get the hexes to flip over, in an expanding wave starting from an arbitrary point. The arbitrary point part wasn’t too difficult; it just meant that the rotation direction was defined by the distance from each hex to that point. Something like this:

Code for this part:

for (int i = 0; i < hexTransforms.Length; i++)
 {
 // the current hex's position
 Vector3 curPos = hexTransforms[i].position;

// get the distance from the center of the flip to the hexagon
 Vector3 hexDist = curPos - flipCenter;
 hexDist.y = 0; // ignore any difference in height

That’s not actually what I want, though; if each hex were to rotate along that vector , they would be spinning around the direction of the start point, not flipping away from the start point. I needed them to instead rotate around the vector perpendicular to the direction vector, seen here:

The direction vectors  are on the right, and the perpendicular vectors are on the left. The axis of rotation is in yellow, and the rotation direction is in blue. Notice how, to have the hexes rotate away  from the start point, the rotation axis has to be perpendicular to the direction vector.  Code here, continued from above:

Vector3 hexNorm = hexDist.normalized;

// get the perpendicular axis
 Vector3 perpAxis = Vector3.zero;
 perpAxis.x = hexNorm.z;
 perpAxis.z = -hexNorm.x;

So with that cleared up, the next part is the expanding wave. To do this, I determined the max possible distance from the center of any flip, (referred to as the “far distance”), and multiplied that with a timer to get a constantly-expanding wave radius that I could compare the hexagon distances to. As the wave expands, it goes from 0 to the “far distance” over a short time, and the current time into the flip is used to get the current radius of the wave. Seen here:

Essentially, as that circle expands, it rotates the hexes it passes over. You may also notice that there is a bit of range to the rotation; this is intentional. I wanted there to be a bit of “width” to the wave, so it would have a more gradual effect over a larger range. With this, hexes that are in front of the current wave start to slowly rotate up, and hexes that it has just passed rotate back down. This is accomplished by subtracting the current wave radius from the hex’s distance to the center; if the flip range is 2 units and the resultant value is in-between -2 and 2, it starts to rotate. The magnitude of that rotation is defined by an AnimationCurve.

 

AnimationCurves are a cool Unity feature that basically allows you to translate from a (given in code) X value to a (defined in object) Y value. In this case, the curve is set up so that the X value is the distance from a hex to the radius, and the Y value is used as a magnitude multiplier for a rotation. This object’s curve is pictured to the right.

 

The final chunk of rotation code:

// compare this hexagon's distance to the center with the current wave radius
 float hexDistSqrMag = hexDist.sqrMagnitude;
 float positionOnCurve = (hexDistSqrMag - curDistSqrMag) / flipRange;

// rotation
 hexTransforms[i].localRotation = Quaternion.AngleAxis(flipCurve.Evaluate(positionOnCurve) * 360, perpAxis) * startRot;

(I’m using the squared magnitude instead of just magnitude because it’s faster to compute.) So all of that is *just* for the rotation alone! I used a similar technique to also move the hexes up as the wave crossed them, and additionally lighten their color as well, to make the wave stand out even more. All of that together gets you this:

If you look on the right side of the game window, you can see all the variables that I’m using to tweak the visuals of this transition; the time it takes for the transition, the (squared) range of the flipping around the radius, and the added height and white color. That’s obviously not the same thing as the finished product, though; there are a few more little things that go into it that I’ll cover lightly in the next section.


Part 3: The Polish

In addition to just the transitions, the other feature you’ll see on the original Trello card is this “floor opening up” concept. When I was adding the higher position and whitening to the transition wave, I realized that I might be able to use the wave to scale down anything that wasn’t behind a button once the wave passed by. To test this, I added a “bounding box” variable to all the menu prefabs that would essentially define a rectangle, and any hexes outside this rectangle would scale down to 0  as the wave passed. In addition to this, I also added a “decay” range (similar to the flipping range), that would produce a gradient of smaller-scale hexes at the edges of the bounding box. I was satisfied with that on its own (and I think the “behind a button” behavior would’ve been far more difficult). The code for that is here:

// DONE OUTSIDE THE FOR LOOP
float maxScaleDistMag = boundingBoxWeight.sqrMagnitude;
 Vector2 normBoundingBox = boundingBoxWeight.normalized;

/* .... */

// scale
 Vector3 hexDistFromGeoCenter = curPos - geoPositions[(int)curBGState];
 hexDistFromGeoCenter.y = 0;
 hexDistFromGeoCenter.x *= normBoundingBox.x;
 hexDistFromGeoCenter.z *= normBoundingBox.y;
 float hexMagFromGeoCenter = hexDistFromGeoCenter.sqrMagnitude;

 if (hexDistSqrMag < curDistSqrMag) // if we're behind the wave
 {
 float minusScale = ((maxScaleDistMag + scaleDecayRange) - hexMagFromGeoCenter) / scaleDecayRange;
 hexTransforms[i].localScale = startScale * (scaleDecayCurve.Evaluate(minusScale) + flexCurve.Evaluate(positionOnCurve) / 3.0f);
 }

Here is a video of me demonstrating the effect of the bounding box (it’s more like a bounding oval, honestly) and decay range (you can see me changing the values in the inspector window on the right:

The final piece of the puzzle is the idle animation that slowly moves and lightens the hexagons after the transition is done. The code for that is, again, incredibly similar to the main wave code; the only major difference being that the idle timer has to loop, and to produce more than one wave peak, you have to use multiple offsets from the timer as well (or float modulus each hexes’ distance from the center by some amount). Here’s a video showcasing the idle animation on its own, over a very large hex grid:

That’s pretty much everything that went into it! There’s also some additional shader work that goes into some of the menu backgrounds, but I wanted to keep this post to the cool math stuff I got to do; I’ll probably cover the depth buffer shader stuff in another post. I hope you enjoyed this long, 3D transformation-filled journey. Now I get to talk about things I might add/fix in the future!


Part 4: The Future

I was pretty happy with this when I finished it, but pobody’s nerfect, and there are a couple things I might change going into the future:

  • If you stare at the original video for long enough, you’ll notice there’s a bit of an initial “burst” of activity when a transition is first triggered. This is because the hex in the center (and all the hexes within the flip range) are suddenly snapping to their rotation, position, and bonus light values the moment the transition happens. An easy solution is to make the transition have a start-up, but then the transition takes longer, so there’s a tradeoff there. If I can find a solution to that that doesn’t make the transition take longer, I’ll probably do that, but as it stands, the “light burst” isn’t super bothersome.
  • Looking at Unity’s profiler, woo boy is this taxing to compute (especially the idle animation). It’s not too taxing that it’s lagging the game (frankly, I shouldn’t even be worrying about it) but there are ways to make it more efficient. One way is to pre-sort all the hexagons in order of their magnitude and only do the calculations on the hexagons that are within the range of the wave. Another way is to group some of them that have magnitudes within a small range of one another, so you only have to calculate the rotation/position/scale/lightness one time, and apply it to a large number of hexagons at once.
  • Beyond the lightness and slight moving, the hexes still aren’t that interesting. At some point, I’d like to add a texture and shader that allows me to apply a gradient pattern on the top of the hexes that transitions in sync with their movements, so it looks like there’s a “current” running through them, or something. Just something more to make the background a bit more attractive, even when nothing is happening.

If you have any ideas for potential improvements to this, or you’d like to see me make more posts like this, feel free to let me know via my email (izzyabdussabur@gmail.com) or my twitter, @BBQSteakTips. Here’s hoping that by documenting my own adventures in game development, I can help someone else come up with something even cooler! Cheers.