Unit 4 showed a flexible representation with finite precision. Another option is fixed-point: agree on one scale and store a whole number of those smaller steps. That can simplify the arithmetic, but it does not make every fraction exact.
Agree on a scale
Pick a scale, say 256, and agree that each stored unit means one 256th. For this example, use a sixteen-bit unsigned integer, from 0 to 65535. That gives us two bytes to hold both whole and fractional parts.
Then 1.5 is not stored as “1.5” at all. It is stored as 1.5 × 256, which is 384. That is an ordinary whole number.
To read the real value back, divide by the agreed scale:
SHOW 384 / 2561.5
The point is not floating. It is fixed at “divide by 256”, and everyone who touches the value agrees to it.
The fraction is being carried as a whole number. That is the trick, and it is also the name: fixed-point.
Adding at one scale
When two values share a scale, their stored integers can be added directly. The answer keeps that scale, provided it still fits in the chosen width.
Add 1.5 and 0.5. Store both scaled, as 384 and 128, then add the integers:
SHOW 384 + 128512
512 is 2 at our scale, so 1.5 + 0.5 came to 2 using nothing but whole-number addition.
Reading the value is not dropping the fraction
A power-of-two scale makes the split visible. At scale 256, the high byte counts whole units and the low byte counts 256ths. For 384, those bytes are 1 and 128: one whole plus 128/256, or 1.5.
A right shift by eight keeps only the whole part: 1. It does not decode the full value 1.5. The discarded low byte held the fraction.
Multiplying an integer by 256 can be expressed as a left shift by eight if the result fits. That does not mean the full operation of reading a fractional value is the same as an integer right shift.
At scale 256, does shifting stored 384 right by eight give 1.5?
No. It gives the whole part, 1. Reading the full stored value means 384/256 = 1.5. The low byte, 128, accounts for the half that a right shift removes.
Keep the fraction between moves
Try this on paper. A position starts at zero and moves half a pixel on each update. At our scale, the stored step is 128. We keep the full position, but choose a whole-pixel coordinate for drawing it.
This is teaching notation, not a program to paste into a machine. LET changes
a named value; SHOW displays it. The loop visits updates 1 through 4 and adds
the step each time. Doing It Again offers more
loop practice. No installation is needed for this trace.
LET position = 0
LET step = 128
FOR update = 1 TO 4
LET position = position + step
LET pixel = position SHIFTRIGHT 8
SHOW pixel
ENDHere SHIFTRIGHT 8 extracts the whole part of our nonnegative, scale-256 value.
Putting it in pixel leaves position unchanged. Predict all four displayed
coordinates before reading on.
For a deliberately broken version, add this line immediately after SHOW pixel,
before END:
LET position = pixel * 256A hint before the explanation
Which value does the next update read? After the first drawing, compare how much
of the half-pixel move each version keeps in position.
Both versions first show 0. What happens over all four updates, and which value must the next update keep?
Show the explanation
The original keeps the fraction between updates:
| Update | Stored | Full pixels | Drawn |
|---|---|---|---|
| 1 | 128 | 0.5 | 0 |
| 2 | 256 | 1 | 1 |
| 3 | 384 | 1.5 | 1 |
| 4 | 512 | 2 | 2 |
It shows 0, 1, 1, 2. The broken version shows 0, 0, 0, 0: after each
drawing it replaces position with zero, so every move starts from zero again.
Its stored state first differs after drawing update 1; its visible result first
differs at update 2. One correct first drawing did not show that the change was safe.
Remove the added assignment to repair it. Keep the full stored position for the next update; use the extracted whole coordinate only for drawing. A picture at one pixel cannot show every fraction that the program is keeping.
For a variation, change step to 64, a quarter-pixel move. Predict the four
coordinates again, then check your reasoning.
Check the quarter-pixel moves
The stored positions are 64, 128, 192 and 256: full positions 0.25, 0.5, 0.75 and 1. The drawings are 0, 0, 0, 1. Keeping each quarter lets four updates reach the next pixel. Discarding it after each drawing loses that progress.
These examples stay within our stored range, 0 to 65535. A larger position or step needs a range check and a decision about what happens if the sum will not fit; do not silently assume wrapping or stopping. This paper trace counts updates, not elapsed time, and does not measure smoothness or speed.
What it costs you
Fixed-point makes several choices explicit.
You remember the scale. The machine sees 384 and thinks 384. Only you know it means 1.5.
You keep the scales matching. Every value in a sum has to share one scale, or the answer is nonsense.
You choose how to round. At scale 256, 0.1 would need a stored integer of 25.6. If we round to the nearest integer, we store 26, which means 26/256 = 0.1015625. A half fits exactly; a tenth does not at this scale.
The scale need not be a power of two. At scale 10, a tenth is exactly one stored unit. Pick the scale and rounding rule for the job. Operations such as multiplying two scaled values also need care with the resulting scale and intermediate range.
Choosing the scale
The scale decides what you get and what you give up.
A bigger scale buys finer fractions. At ×256 you can hold 256ths, which is far finer than a pixel.
It costs range. Our largest stored integer is 65535, so at scale 256 the largest value is 65535/256 = 255.99609375. More fractional steps mean fewer whole units in the same storage. Choose the width and scale together.
Matching-scale integer addition can be cheaper than software floating-point on a Z80. That is a reason to consider fixed-point, not a guarantee that it is best for every game. Check the precision, range and work needed on the target machine.
For another explicit conversion from fractions to whole numbers, see
Meet BASIC’s secret-number expression.
Its table explains how INT removes the fractional part; you can read that section
without following the preceding game lessons.
When it’s wrong, see why
- A value printed as a big whole number, like 384 rather than 1.5. You forgot to divide by the scale. The machine only ever sees integers.
- Two values fought. Adding a ×256 number to a ×16 number gives nonsense. Everything in a sum must share the same scale.
- The numbers ran out of room. A scale eats range, so a value that fitted as a plain integer may not fit once scaled.
- A fraction vanished. Scaled values are still whole numbers, so values between the steps of your scale need rounding. Check the chosen rule and allowed error.
What you’ve learnt
- Fixed-point stores a fraction as a whole number at an agreed scale.
- Addition at one scale uses integer addition, while the result fits.
- Extracting the whole part with a shift is different from reading the full fraction.
- You carry the bookkeeping: remembering the scale, and keeping every value in a sum on the same one.
- Its precision, range and rounding depend on the chosen width and scale.
- Its speed depends on the target and operations, so measure the work that matters.
Where this leaves you
That is the primer. You can look at a number the way the machine does, whole or fractional, positive or negative, in bits or in hex, and you can mask it, shift it and scale it.
From here the road goes down into a real machine, where hex addresses, bit flags, signed offsets and fixed-point are not background facts. They are the language the code is written in.