Gamedev Grievances #33: Pain with Primitives

A long time ago, on a computer far far away… there once was a development build of Ambience which introduced a neat swirling light pattern in the prison-based dungeons.

Throwback to the good old days… September 2016, to be exact. Wow, this really is old.

More recently, I decided that the light swirl was in need of some updating. It hadn’t actually changed much since the GIF above (except I might have changed the colour a bit – I’m not exactly sure). In particular, I didn’t like seeing the sharp edges of the light swirl and decided to make a smoother fade effect at the edges. How to do this, you ask?

Enter the primitives…

I’ve never liked primitives much. Not because they’re actually not useful – they really are – but mostly because I’ve always seen them as more of a 3D thing rather than a 2D thing. It’s kinda like that obnoxious relative you always hear about at family gatherings but never really meet, and when the time comes to meet them, you’re not super keen on it. If that makes sense…

Anyway, here’s my own simplistic understanding. A primitive is kinda like a basic shape (say a point, line, polygon, 3D blob…) which you define by setting the vertices/corners and letting the game engine draw it. Say you want to draw, I dunno, a triangle. No problem! Just set three vertex positions and tell the game to fill it in. How about a segment (like a pie slice) for our swirling light? Also easy-peasy! Just draw a bunch of really thin triangles squashed up next to each other, and if they’re thin enough, no-one will notice that your pie slice is actually kinda blocky at the ends.

Fading the edges

In the GIF above, I just made a single segment primitive, drew it and rotated it around and around to make the swirling light effect. But to fade out the edges, I couldn’t just rely on a single primitive. Instead, I needed to add some more primitives (think: thin pie slices) with gradually increasing opacities at the edges to surround the bright “main” segment.

My first thought was, “Why don’t I make twenty separate primitives for the segment and just fade out the ones at the ends?” But I figured that would be inefficient. I only needed separate primitives for the edges where the opacity was changing, not for the middle segment which had constant opacity. So, I had a plan: First draw the primitives at the leading edge fading in, then a single primitive for the middle segment, and then finally some primitives on the trailing edge fading out. Easy, right?

Well… sort of. There’s a price to pay for efficiency, it turns out. I wanted to make my light swirl code as elegant and efficient as possible, so I thought up a basic algorithm to do the job using a single for-loop and minimal different cases:

  • Set the initial opacity to zero.
  • Check where we are on the light swirl segment. If we’re near the leading edge, increase the opacity a bit. If we’re near the trailing edge, decrease it a bit. If we’re in the middle, don’t do anything.
  • Check if the opacity’s changed. If it hasn’t, then fine – just add another vertex to the same primitive (this keeps drawing the middle segment as a single primitive).
  • If the opacity has changed, then we stop drawing the primitive we were drawing and start drawing a new one with a different opacity.
  • If the opacity’s about to go below zero, then don’t draw any more primitives. We’re done! (The limits of the for-loop take care of this for us.)

The outtakes…

Because stuff always messes up when you’re doing these things.

The first form of the algorithm I tried drew the light swirl perfectly – with the exception of a single segment just before the trailing edge, which it refused to draw at all. You can see this pretty clearly in the screenie below:

Note that the light swirl (centered on the player here for debugging purposes) is rotating counter-clockwise – so the “leading edge” here is below the player and the “trailing edge” is to the player’s left.

Even when I changed the number of segments, the angle of each segment, and the number of fade in/out segments, the same thing happened. The segment two positions before the start of the fading-out segments simply refused to be drawn.

Since I had set the peak opacity to be 1 (that is, the middle segment was completely opaque), I couldn’t really see clearly what was going on. So I halved the peak opacity and had another look at the leading and trailing segments.

Aha! So while the leading edge segments are drawn normally, the two visible trailing edge segments are drawn with these weird gradients! Something was going on with the trailing edge segments overall. I’m not sure exactly what it was in the end, but eventually I gave up trying to salvage my code and just re-wrote it following the algorithm above. And it worked!

In this case, the arc draws out a 120-degree segment with 12 segments (making it nice and blocky), including 1 leading-edge and 1 trailing-edge segment.

Finally, I made the light swirl sit at a random corner of the view and draw the arc from there, so that the arc would “flash” periodically across the player’s field of view, as per the GIF at the top. (I also made the colour a bit “redder” compared to the top GIF.)

This is how it turned out in the end with 30 segments. It looks a tad choppy here (you can see the individual segments kinda clearly), but that’s just the GIF recording software – it’s much smoother in-game when running at 60 fps. I could make it draw more segments for an even smoother fade, but figured it probably wasn’t worth it.

Looks good!

The Learning Curve

There’s always lessons to be learned from solving these kinds of problems, so here they are:

  • As I mentioned above, in-game efficiency comes at a cost – which is usually time spent actually implementing the system (and tearing your hair out trying to squash those bugs). Generally, I’ve found that the more sophisticated and/or efficient your algorithm is, the longer it takes to make it work. But when it does work, it’s worth it.
  • This might be a no-brainer to some, but I find it helpful to break systems down into simpler ones that are easier to deal with when trying to identify and fix problems. In this case, it would have been really hard to fix the problems I had, if I had kept the swirling light flashing at full speed in the corner of the room. Instead, I made the light smaller, rotated it slower, centered it on the player, made it blocky by changing the number of segments, and adjusted the peak opacity and total angle swept out as needed until I (finally) solved the problem. Whatever it takes, I guess.

Leave a Reply