Finely Chopped Meat - The Mathematics of Retro Programming
If you have already programmed for retro devices, you probably noticed that the mathematical capabilities of retro processors are not very rich, and our NES or ZX-Spectrum can only perform a couple of mathematical operations: addition and subtraction. At the same time, our computer operates with numbers of only one type: integers. Support for real numbers and floating-point operations appeared in computers much later, so we have to accept that and work around it.
Working within these mathematical constraints can be difficult, but it will also make you a more creative programmer. All you need is the right techniques, like a butcher and his blade. Let’s examine how we can create movement mechanics within retro limitations.
Cut the Steaks - Movement
The limited mathematical capabilities of retro processors are often good enough for simple tasks, like the movement of a character. It is quite simple: we should add 1 to the current value of the X coordinate or subtract one from it:
In C language, it could look like this:
unsigned char x; // one byte for x
while (1) {
x=x+1;
}
I defined the variable x as a byte. Since we are talking about the NES/Famicom, this will be enough for us, because the width of our screen is 256 pixels, which is equal to the maximum value of a byte: 255 (0 is also a value). Next, I increment each frame x by 1. Let's see what the x values look like for the first five frames:
Frame 1: x = 0
Frame 2: x = 1
Frame 3: x = 2
Frame 4: x = 3
Frame 5: x = 4
Our console’s frame refresh rate is 60 frames per second (NTSC), which means that Bill can be moved by 60 pixels per second forward or backward.
But this may not be enough. What if we need to add not one pixel per frame, but half or one-tenth of a pixel?
Here, the speed of movement of the fighter depends on how long you held the right or left button. The longer you hold it, the faster the fighter flies. Sometimes the speed of flight may be much slower than one pixel per frame.
Obviously, for this, we must use real numbers. But, as we already know, our console is able to operate only with integer values. So we have to cheat a little.
Finely Chopping - Acceleration
Let's define two bytes for the x coordinate instead of one. This means that we do not have 256 values, but two times for 256 values.
We will use the high byte as the integer part of the coordinate, and the low byte as its fractional part.
unsigned int x; // two bytes for x
while (1) {
x=x+1;
}
What has changed? Absolutely nothing. The low part of our value is still increasing by 1. But what happens to the high one? All these five frames it is still zero. But something will happen if we repeat the addition 256 times?
Frame 1: x = x + 1; // x high = 0, x low = 1
Frame 2: x = x + 1; // x high = 0, x low = 2
Frame 3: x = x + 1; // x high = 0, x low = 3
Frame 4: x = x + 1; // x high = 0, x low = 4
Frame 5: x = x + 1; // x high = 0, x low = 5
...
Frame 255: x = x + 1; // x high = 0, x low = 255
Frame 256: x = x + 1; // x high = 1, x low = 0
What happened to our value at frame 256? It's easy, the low byte reached its maximum (or overflowed), then dropped to zero, and the high byte increased by 1. If we do this another 256 times, the same thing happens - the low part of the value will be reset to zero, and the high part will increase again. We will use only the high byte of our value as the x coordinate. That is our x coordinate increases by 1 every 256 frames. In other words, we divided the frame into 256 parts.
So, in order to move our character to the “half” pixel in a frame, we need to add not 1, but 128 (256/2 or half of frame):
unsigned int x; // two bytes for x
while (1) {
x=x+128;
}
Frame 0: x = 0; // x high = 0, x low = 0 => 0.0
Frame 1: x = x + 128; // x high = 0, x low = 128 => 0.128
Frame 2: x = x + 128; // x high = 1, x low = 0 => 1.0
Frame 3: x = x + 128; // x high = 1, x low = 128 => 1.128
Frame 4: x = x + 128; // x high = 2, x low = 0 => 2.0
Frame 5: x = x + 128; // x high = 2, x low = 128 => 2.128
Frame 6: x = x + 128; // x high = 3, x low = 0 => 3.0
Frame 7: x = x + 128; // x high = 3, x low = 128 => 3.128
To simplify understanding, I will show how this happens with a calculator.
As you can see, each click on the button (each frame) adds 128 to the current value (80 in hexadecimal). And each time the low byte overflows (every second frame), the high byte increments. We will use this byte as an integer to position our sprite. As for the low byte, we do not need it.
This means that we have reached the goal - our character moves at a speed of half a pixel per frame. No magic, just math.
So, if you want to change the value every 4 frames, just divide 256 by 4 and you will get the desired value for the increment: 64. Add 64 to your two-byte value 64 and use the high byte as the x coordinate. Now our character flies 4 times slower than 60 pixels per second.
Inside the Core - ASM and C
Now, there is one thing, though. If you use assembly language, the solution looks a little different. For example, ZX Spectrum has 16-bit registers, so increasing the two-byte number is very simple:
ld bc, 0
loop: inc bc
jr loop
Every time the low 8-bit register C overflows, the ZX Spectrum increases the high register B automatically:
But NES does not have 16-bit registers, so you have to provide that yourself:
Mathmeatatics
Hopefully, this simple math will make your work much easier. This method can be used wherever you need more accurate calculations, such as coloring intensity interpolation, simulating inertia, and building simple vector graphics (if your retro device can afford it). Happy carving.
Alex is the developer behind The Meating, our ghost-minotaur platformer. He is an avid carnivore and has no beef with beef.