One line:
usePostProcess(dither({ levels: COLOR_LEVELS.genesis }));Take it out and put it back. That's the whole chapter.
Post-processing: a filter over the finished image
Everything so far told the engine what to draw. Post-processing is different: the engine draws the complete frame and then runs a shader over it (the whole image) before showing it.
Which is why usePostProcess isn't attached to any sprite. It doesn't belong to the paddle
or the ball: it belongs to the screen.
What dithering does
levels: 8 means eight tones per colour channel instead of the 256 your monitor has.
That's exactly what a Mega Drive could show: 3 bits per channel.
Cutting colours down bluntly produces ugly banding: visible steps where there used to be a smooth gradient. Dithering is the period trick for hiding them: it mixes the two nearest available tones in a regular pattern and lets your eye average them. Grain instead of steps.
The detail that makes it work is inside and you never have to touch it: the noise
amplitude has to equal exactly one quantisation step. Too small and the bands survive
under the grain; too large and pixels dither between colours that aren't neighbours, which
reads as static. Here that number is derived from levels, so the two can't drift apart.
Try the others:
COLOR_LEVELS.genesis // 8 tones: Mega Drive
COLOR_LEVELS.snes // 32: Super Nintendo
COLOR_LEVELS.ps1 // 32: PlayStation, Saturn
COLOR_LEVELS.poster // 4: not any real console, but it looks goodAnd strength, which scales the effect without breaking that relationship:
usePostProcess(dither({ levels: COLOR_LEVELS.poster, strength: 0.5 }));Why this is the engine's job and not your shader's
You could write the shader by hand: the engine lets you. But getting the noise amplitude right is the hard part, and getting it wrong doesn't raise an error: it gives you an image that's almost right and you can't tell why.
That's the idea behind all of NacatamalOn, and now you've seen it from the inside: the limitation (short palette, low geometry, grain) isn't a shortcoming to paper over. It's the style, and it comes fitted.
What's a shader?
A tiny program that runs on the graphics card, once per pixel, in parallel across all of them at once.
Here the shader receives the colour a pixel was going to be and returns another: the same one, rounded to one of the eight available tones, plus a touch of ordered noise. Multiply that by the 153,600 pixels of a 480×320 game and sixty times a second, and you can see why the graphics card does it and not the processor.
You don't need to know how to write them to use the engine. dither and its companions come
ready-made.
That's it
You have a game. It can be played, won and lost, and it looks the way you wanted.
Take stock of what you learned, because it isn't little: the loop and dt, sprites and
coordinates, keyboard input, box collision, creating objects with a loop, destroying them in
a deferred way, bitmap text and post-processing. That's enough to make a real game, not a
tutorial game.
And you installed nothing.