A tale about chasing a pixel perfect tail


Although hindsight is a wonderful thing, I learned while making my first game, Unicopia, that naivety also has benefits, namely the ability to make ill-informed decisions while remaining blithely optimistic.

When I started Unicopia it was chiefly as a learning aid, a chance to make a game without getting bogged down in the details. For graphics, I knew I needed something that looked pretty, but I wanted something super simple. Given the popularity of modern retro games it seemed easy: find and commission a pixel artist to make some pixel art, drop them into the game, and that'd be the graphics sorted.  I hadn't really understood the implications, but had the vague idea that limiting palette and pixels is the path to pixel perfection.

Having chanced across the amazing Metal Zebra, we discussed the game's needs, he busily drafted a few iterations of sprites and tiles, and about a month later he delivered a raft of pixel magic. Augmenting Metal Zebra's sprites with some stock artwork from Clockwork Raven, and there it was, and what I thought was the end of my journey sorting the game's graphics was actually the beginning of a circuitous chase after pixel perfection.

I fist realized there were issues with the game's graphics when I received an offhand comment everything looked a bit small. Having settled on a 1280x720 design resolution, I was in love with how much gameplay could  fit on a single screen. 

1280x720 resolution pixel art

Although the game had been designed to scroll vertically there was enough space for some really quite complex single screen behavior. Not only did it help mixing up the game's flow across levels, but the sheer scale of the screen compared to the the player's character, Cornelia, added this daunting feel to the game. 

Nonetheless, I experimented with lowering the resolution to 640x360, and instantly knew I had to stick with the lower resolution. It was as if the graphics had come alive and the lower resolution magnified the craftsmanship in Metal Zebra's sprites while also increasing the player's connection with Cornelia.

I spent a busy few evenings resizing the tilemaps, redesigning a handful of levels, and tweaking some weird physics issues, but it was clear the drop in resolution was a major step forward. To cap it off I spent an evening playing with Godot's stretch settings (Mode: viewport, Aspect: keep, Scale: 1, and Scale Mode: integer) and thought I was well and truly done.

640x360 resolution pixel art

However, even though I'd got the size of the screen and graphics to feel right, it wasn't long before I ran in my next problem, mixels, or mixed-resolution pixels.

Early on there'd been a few sprites I'd resized to better fit with game play, most memorably the moving platforms, the springs, a couple of enemies, and the key. Most I'd scaled using nearest neighbor scaling with a factor of 2, although the key annoying needed scaling by 1.5. But after downsizing the design resolution it quickly became apparent that scaling pixel art by non-integers is a catastrophe, messing with the pixel proportions, and creating ugly artefacts that break the pixel purity.

Fractionally scaled pixel art vs whole number scaling

Fractional vs whole-number scaling

Swapping out the fractionally-scaled sprites for redrawn alternatives, it then dawned on me there was also a problem with only scaling some of my sprites. Although whole number scaling keeps the proportions correct within a single sprite, the scaling looses precision when compared to non-scaled sprites, essentially doubling the size of my sprite's black borders, and creating a notable and ugly visual contrast against the non-scaled sprites. 

Turning my hand to paint.net, I touched up all the sprites I'd scaled, ensuring their pixel definition fitted with their new resolution, and carefully erasing their extra thick borders.

  

2x scaled vs redrawn spring

It was at this point of development I really appreciated why mixels were so hated in the pixel art community. Mixing resolutions was dirty. It  left ugly artefacts,and resulted in unnecessary contrasts. Creating visually appealing sprites using limited number of pixels is time-consuming and hard, but mixels are lazy at best and disrespectful of the craft at worst.

However, there was one mixel in Unicopia that felt different, which was the clock that counts down when a level's time runs out.  It started life as a 16x16 sprite for a unused pickup, but it quickly morphed into the end of level clock when I needed a strong visual warning for the player.

Mixels are sometimes an acceptable evil

Having learned mixels were bad and to be shunned, I did my best to get rid of it. I tried removing it, redrawing it, and even considered replacing it with an egg-timer, but nothing seemed to fit. Eventually I came to the realization it plain and simply just fitted with the game's retro aesthetic, so I decided to let it stay. It felt right and left me with the feeling mixels can be okay, as long as they're used with purpose and intent.

Although I was pretty sure I'd cracked pixel perfection, one minor annoyance I found was rotation. There's not a lot of rotation in the game but Cornelia spins out of scene when she exits a level and gems also make a noise and rotate slightly back and forward when Cornelia approaches them for the first time.


Pixel perfect rotation

While Cornelia's spin wasn't a problem since she spins quickly into the darkness, the gem rotation looked glitchy, especially the red gem as its straight lines were notably broken by the rotation. Nonetheless, having learned that pixel perfection was about making compromises, I decided this was entirely in keeping with the limited pixel, low-palette aesthetic.

Pixel perfect physics

After hitting every hurdle to get the game's graphics pixel perfect, I was pretty sure I was almost done. I'd learned all there was to learn about pixel perfection, and it was simply about scaling your pixel art to the same degree, and also choosing an appropriate resolution for the game.

It was then that I noticed the game suffered from various forms of minor, but noticeable jitter. Having done some research it was pretty clear what the problem was. Although the graphics were pixel perfect, Godot, like almost all modern game engines, is based on floating-point arithmetic, meaning game objects aren't restricted to moving in integer steps, unlike the pixel which are constrained to integers.

So the first step to fixing jitter was to use Godot'sSnap 2D Transforms to Pixel setting, which makes the renderer snap the display of objects to whole pixels, rather than blending them across partial pixel positions. The physics still uses floating point arithmetic, and an object's underlying position is stored as a floating point, but the associated sprites are only displayed in integer positions. 

Although snapping to pixels is good, I quickly became aware of a further problem. Whenever the player stood still on a moving platform they occasionally wobbled on the platform. Essentially the platform sprite was snapping to pixels and the player's sprite was snapping to pixels, but every so often as their positions were rounded to the nearest integer, one would snap forward a pixel and the other would snap back a pixel causing a wobble. 

As as fast moving game there's actually very few times the player stands still on a moving platform, but unfortunately that served to make the wobble all the more noticeable. To fix it I ended up forcibly rounding the moving platform's position every frame, effectively forcing the platforms position to match its sprite's integer position. Since rounding the position changes the physics calculations, I ended-up only rounding the player's position on frames they were standing still.  While technically the player still wobbles on moving platforms when they're moving, the wobble is negligible compared to their movement, making the wobble utterly imperceptible. 

Conclusions

With properly scaled pixel art, snapping transforms, and rounded positions for moving platforms, I assumed I'd learned everything there was about pixel perfection. None of the games' testers were commenting on pixel problems, the physics movement was nice and smooth, and I was pretty sure I'd got rid of any jitter caused by the mismatch between pixel art and floating-point physics.

But there was something that still niggled, and eventually, after a couple of hours play-testing, I realized the janky movement on the rotating gems was breaking the illusion of pixel perfection. As a quick experiment I added an interpolation shader to the rotating gems, effectively  anti-aliasing the sprites, and bingo, their movement now felt right.

Pixel imperfect rotation

I then spent a couple of hours debating if the interpolation shader was cheating? The interpolation felt right, but the sprites were no longer constrained to their limited palette, and it felt like I'd broken a cardinal rule of pixel art: 

Limiting palette and pixels is the path to pixel perfection.

But removing the shader was really unappealing. Even though it wasn't obvious to the naked eye, the janky gems felt wrong, making the game feel like it stuttering, even though it was running smooth as ice.  And it was then that I came to a realization about pixel perfection. Pixel perfection isn't a real thing. There isn't a single rule, formula or platonic ideal for pixel perfection, it's really just about the player's perception of the pixels. 

Limited palette and pixels can look great, can lend a great retro aesthetic to a game, but the game needs to feel right. If the player feels a game is properly pixelated, then it is. Integer graphics and floating point physics can work perfectly together, but that's solely based on the player perceiving things are perfect. So rather than continue to chase some ideal about the number, color and movement of Unicopia's pixels, I finally let it go, understanding its pixels have the perfect combination of colour, craft, and imperceptible jitter.

Leave a comment

Log in with itch.io to leave a comment.