Join us! Nintendo Enthusiast Forums
dark        
Flegma blog header photo
Flegma's c-blog
Fronts 3Posts 807Blogs 84Following 0Followers 19


 
 

LONG BLOG

Fledgling retrodev: Sprite flickering and permutations

   0

My Bumper Ship Racing is practically done, but I finally decided to try adding thruster flames to it. This blog is the story of one overengineered feature, in exceedingly great detail. The details are, I think, still reasonably easy to understand, as they don't go into advanced math.

To recap the blog I linked above very briefly:

I wrote a game for an early 1980s microcomputer in 2018. It's a single-screen racer in the vein of Super Off-Road (or Micromachines, if everything fit on the screen at once). The vehicles are space ships that bounce around the track.

 Screenshot of the current version of the game.

So, on to adding the thruster flames as shown in the picture above. There are three main hardware limitations to pay attention to first.

  1. The sprites are 8x8 or 16x16 pixels in size and each pixel is either transparent or one colour shared by the whole sprite. So, a blue ship sprite is the same shade of blue all over, and to get a red/yellow flame instead of a blue one, another sprite is needed.
  2. Each horizontal scanline (think a row of pixels) can have at most four sprites. If there are five or more, only the first four sprites on the lowest "sprite planes" of these are rendered.
  3. When the opaque pixels of two sprites overlap, the sprite on the lower plane is the one shown.

To give the green ship thruster flames that are not green, I needed another sprite. With four racers, if they're all accelerating at the same time, that's four more sprites in total. At worst, 50% of the sprites wouldn't be fully drawn.

To "appear" to display more sprites on the same scanline, the obvious solutions are to:

  1. Keep the sprites in a fixed sprite order and be consistent of which sprites will not be drawn.
  2. Switch the sprite planes between every screen refresh to display different sprites every time this happens.

Back on the sprite planes. Each plane can have one sprite (visible or not), and the planes are ordered starting from zero upwards. The third constraint means that if I want to use the same ship sprites with and without thrusters active, the flames will need to be on a lower plane.

The ships are (obviously) more important than the flames. One solution in case 1) would be to assign ships to planes 4-7 and the flames to planes 0-3. The flames would always be rendered, but the ships would be shown only if the sprite threshold wasn't breached, or I defined another set of sprites the ships but everything under the flames being invisible. That's not good.

Another solution to 1) is defining another set of sprites for ship sprites minus the matching flames, but I might eventually have a better use for the video RAM reserved for those sprite patterns (such as a checkered or white flag being waved or fireworks for creating a track record). So maybe not that one either.

The first solution to case 2) I tried was ordering the sprites on every other frame so that every even (including 0) would be the thruster and every odd one the ship, and the players are in order 1, 2, 3, 4. On the other frames, the players would be in order 4, 3, 2, 1.

This is enough for every ship to be drawn on at least every other frame. That goal isn't good enough, though, and we'll get to that in a bit, but first there was a curious bug.

When I tried this on an emulator, I noticed that if the fourth player was near the top of the screen, they were often completely invisible. No matter if there were other players' sprites around.

Here we get to a topic I was at first positively giddy about encountering. The main game loop worked so that it waited until the display was being drawn on the screen (because that's a handy start signal to help with timing), and then it went through the players one at a time: read their input, update their positions and draw them on screen. Note that the players were drawn at very different times on screen: player 1's sprite positions were updated before going to read player 2's input.

And the core point: player 1's sprites were updated early, but player 4's sprites were updated as the last thing before going to wait for the next screen refresh to start. As I was updating the sprites' positions on the screen, the video processor would keep sending out image information to the display from top to bottom.

Imagine this sequence of events, if you will.

  1. The display signal starts from the top corner of the screen. The CPU starts the main player loop: read player 1's controls, update the player 1's position, render the player 1 on display -- but using the sprite plane assigned to the ship of player 4 in the previous frame. Player 4's sprite is reassigned to player 1, and the very next thing is to repurpose the flame sprite plane as well for player 1.
  2. Players 2 and 3 are handled like player 1 was. The CRT scanline keeps on moving ahead.
  3. The main loop finally gets to placing player 4's sprite in its position... but the data sent from the video chip to the screen has already advanced past where player 4's sprite would appear. As a result, the sprite is not visible in this frame.
  4. Rinse and repeat.

This was actually an easy bug to (mostly) fix: just make rendering the sprites their own loop to be executed as quickly as possible. Ideally, I'd do it when the display has finished rendering, but I don't know if there's a suitable hook for that. Instead, I just chose to do it as soon as the redraw starts, hoping to get this done before the scanline reaches the area with the sprites.

Then I realized that this wasn't that different from screen tearing and I realized there's nothing that fascinating going on here.

But now we get to why having every sprite shown on at least every 2nd frame isn't good enough a goal.

First, remember that there are no explosions, smoke trails or anything of the sort that require sprites. We know there are at most eight sprites and what those sprites are. These limitations let us find better solutions than in the general case where the number of sprites varies wildly.

Assume we have players 1, 3 and 4 on the same scanlines, with thruster flames on display as well, and we use the scheme described earlier (reversing player order). Every other frame, players 1 and 3 with the flames are shown. Every other frame, players 3 and 4 with the flames are shown. This means that player 3 is always rendered properly, but both players 1 and 4 are invisible half of the time.

It'd be better if we rendered the sprites equally often, right? Or even better, can we somehow make this work better in a round-robin way without doing extra calculation on where the sprites are and whenever any three players are on the same scanline?

To rephrase this: if we have a sequence 1234 (the order in which the players occupy the sprite planes from lowest to highest), then can we create a loop of such sequences where no matter which three players of the four are on the same scanline ({1, 2, 3}, {1, 2, 4}, or {2, 3, 4}), they will all be rendered in two out of three frames?

After writing a quick script to solve this for me, I have the answer: yes. One example of such a sequence is 1234, 4132, 2431, and then back to 1234.

So by using the frame counter modulo 3 to determine which permutation in the above sequence to use, it is assured that if there are at most any three players on the same scanlines, then the ship sprite will be invisible only one out of three frames, max.

However -- all such solutions would leave one player (the 3rd one) always invisible if all four players were on the same scanlines. Unless I made a mistake with my solver script, which wouldn't be unheard of either.

Thankfully, switching between two permutation schemes depending on if all player sprites are clumped tightly enough on the Y-axis (min- and max-Y-coordinates differ less than 16 pixels) is enough to avoid this. Evaluating that condition can be done in the spare CPU cycles after everything else for the frame is computed. Then every ship and their thrusters are shown at least once every other frame even when they're all on the same scanlines.

But remember that the flames are separate sprites. If they were not displayed, all four ships would be visible even if they all were on the same scanline.

That's why when all players are vertically clumped, the thrusters are shown for two frames before being hidden for another two. This reduces the ship flickering to at most one frame out of four, which is far better.

Wouldn't the flickering thrusters then stand out more?

I would hazard a guess that the answer is "Not as much." The player shouldn't need to focus on the thruster flames over the the actual ship. Second, the flames already flickered between yellow and red already anyway, so having the thrusters blink out for a while shouldn't be that much more distracting.

So in theory, what should these animated frames look like in practice? In total, one complete loop for this takes eight frames. Looking at this as a binary sequence, the third bit tells the colour of the flame (red or yellow). The second bit tells if the flames are displayed. And the first bit tells if the players are rendered in which order, 1234 or 4321. In this animation, the flame colour is always red, so the animation loop is reduced to only four frames. 

 

All in all, one full loop (8 frames) takes 0.16 seconds.

You'd be right to ask why the ships are invisible at most 25% of the time when they are all on the same scanline, but possibly even 33% at other times. Other than my laziness to update the code (I'm not an egoless programmer either), the flames not sputtering in and out all the time will give superficial credence to the ships not being mere rustbuckets.

All this is probably what is called "overengineering", and the time spent fiddling with this might've been better spent on balancing the different vehicles instead or doing better graphics or adding some music replay routine to have actual music in the game.

But I had more fun figuring this out than with any recent game I remember, except maybe Return of the Obra Dinn (Lucas Pope, 2018).

While I doubt I'll manage another equally long blog on only one small part of BSR, there are some smaller details I might write about... as self-aggrandising as these blogs are.

Note to self: never apply for a job involving creating games.

Login to vote this up!

LOOK WHO CAME:


Flegma   
Santador   17
lewness   15
Zalno   14
Roager   5


 
 

  0 COMMENTS

Please login (or) make a quick account (free)
to view and post comments.



 Login with Twitter

 Login with Dtoid

Three day old threads are only visible to verified humans - this helps our small community management team stay on top of spam

Sorry for the extra step!

 

About Flegmaone of us since 11:34 PM on 01.17.2015

Very much unprofessional writer, don't take anything I write without a truckload of salt.

On a hopefully long-term break from saying anything.