← Back to Courses
module
13

Arrays and lookup tables

Why this matters

S11 walked a string: bytes in a row, read one at a time. An array is the same thing holding numbers instead of characters, and everything you learned there applies unchanged.

What is new is the second idea in this section, and it is one of the most useful in the whole course. A lookup table replaces decisions with data. Instead of a chain of comparisons working out what to do, you index into a table and take the answer. It is shorter, it is faster, and - the part that matters for a game - you can change what the program does by editing data rather than logic.

Sprite frames, level layouts, colour maps, the sequence a monster moves in: all tables.

An array is just bytes

numbers: DEFB 10,20,30,40,50

Five bytes, one after another. Nothing else - no length stored anywhere, no type, no bounds. If you want to know how many there are, you have to remember.

address:  0117 0118 0119 011A 011B
value:      10   20   30   40   50
index:       0    1    2    3    4

The index is not stored either. It is just how far along you are.

Reaching an element

Element n lives at base + n. So put the base in HL, add the index, and read:

        LD HL,numbers   ; base
        LD BC,2         ; index
        ADD HL,BC       ; HL = numbers + 2
        LD A,(HL)       ; A = 30

ADD HL,BC adds a 16-bit value to HL, which is what you need since addresses are 16-bit even when indexes are small.

Getting BC right

There is a trap here worth more than a passing mention, because the wrong version runs.

B and C are the two halves of BC, and B is the top half. So this:

        LD B,2
        LD C,0          ; this does NOT make BC = 2

makes BC = 512. ADD HL,BC then jumps 512 bytes past your array and LD A,(HL) reads whatever is living there - which on a freshly booted machine is FF, and later could be anything at all, including a plausible-looking number.

Nothing warns you. There is no such thing as reading past the end of an array on this machine; there is only reading, and you asked for that address.

Two ways to be safe:

        LD BC,2         ; one instruction - the assembler splits it for you

or, if the index is already in A:

        LD C,A
        LD B,0          ; index in the low half, zero in the high half

The single-instruction form is the one to prefer. Getting halves the wrong way round is not a mistake you can make if you never write the halves.

Looping with DJNZ

The counted loop from S6 was DEC C / JR NZ. There is an instruction that does both at once:

        DJNZ label

Decrement B, and Jump if Not Zero. One instruction, two bytes, and it is why B is the conventional loop counter.

Summing the array:

        ORG 256

        LD HL,numbers   ; where the data starts
        LD B,5          ; how many
        LD A,0          ; running total

sum:    ADD A,(HL)      ; add this element
        INC HL          ; step along
        DJNZ sum        ; five times

        RET

numbers: DEFB 10,20,30,40,50

A comes back as 96 - which is 150 in decimal, and 10+20+30+40+50 is 150.

Two things to hold on to. DJNZ uses B and only B, so if you need B for something else you are back to DEC/JR NZ. And it is a short jump like JR, so the loop body has to be small - which loop bodies usually are.

Lookup tables

Now the idea worth the section.

Suppose you need to turn a number 0 to 3 into a letter. You could compare your way there:

        CP 0
        JR Z,is_a
        CP 1
        JR Z,is_b
        CP 2
        JR Z,is_c
        ...

That is eight instructions and growing, and every new case means more code. Or you could put the answers in a table and index it:

        ORG 256

        LD A,2          ; the number we have
        LD C,A
        LD B,0          ; BC = the index
        LD HL,table
        ADD HL,BC
        LD A,(HL)       ; A = 'C'

        RET

table:  DEFB 'A','B','C','D'

Six instructions, and it stays six instructions whether the table has four entries or two hundred. Adding a case means adding a byte to the table and touching no code at all.

This is the technique behind most of what a game does with data. A table of sprite addresses indexed by which frame you are on. A table of screen rows indexed by Y. A table of movement offsets indexed by direction, so that "move the player" is one lookup instead of four branches. Whenever you catch yourself writing a chain of CP and JR Z, ask whether the answers could sit in a table instead.

Tables of rows

A table can have a shape. Three rows of three:

grid:   DEFB 1,2,3
        DEFB 4,5,6
        DEFB 7,8,9

That is still nine bytes in a row - the layout only exists in how you index it. The element at row r, column c is at:

base + (r * 3) + c

And here is the practical problem the formula hides: the Z80 has no multiply instruction. Multiplying the row by 3 means adding three times, or writing a loop, and doing it for every access adds up.

Which is why, when you get to choose, you make the row width a power of two. Doubling is one instruction:

        ADD HL,HL       ; HL = HL * 2
        ADD HL,HL       ; HL = HL * 4

A grid eight bytes wide indexes with three of those and no multiplication at all. A grid three bytes wide does not, and you pay for it on every access.

If that means wasting a few bytes per row - eight bytes of storage for five bytes of data - it is very often the right trade. This is the first place in the course where you spend memory to buy speed, and a game does it constantly.

Change one thing

  • Change LD BC,2 to LD B,2 / LD C,0, exactly as the wrong version has it. Then look at HL in the register line and work out where it pointed. Then work out what A came back with and why it looked believable.
  • Change LD B,5 in the sum loop to LD B,4. What total do you get, and which element got left out?
  • Change it to LD B,6. What did the sixth element turn out to be?
  • Put RST 8 / DEFB 158 inside the sum loop to print each element as it is added. Do you get five characters? What are they, and why are most of them not what you would call readable?
  • Extend the lookup table to eight letters and index it with 7. Then index it with 8 and explain what you get.

Exercises

13.1 - Reach the fifth. Make an array of eight bytes with recognisable values and load the fifth element into A. Remember the fifth element is at index 4.

13.2 - Sum of ten. Sum a ten-byte array into A with DJNZ. Choose values whose total is under 256, then work out what happens if it is not.

13.3 - Digits without arithmetic. Build a table mapping 0-9 to the characters '0' to '9', and write a routine that converts a number in A using it. You already know ADD A,48 does this in one instruction - so say in a comment when the table would be the better choice anyway.

13.4 - A grid. Build a 3x3 table and write a routine that fetches the value at a given row and column using base + row*3 + col. You will have to multiply by three without a multiply instruction.

13.5 - The same grid, cheaper. Rebuild 13.4 with rows four bytes wide, wasting the fourth byte of each. Index it with ADD HL,HL. Compare the two routines by counting instructions, and decide which you would put in a game loop.

13.6 - Directions. Make a table of four movement offsets - up, down, left, right - and a routine that takes a direction 0-3 and returns the offset. This is the core of player movement, and you have just written it without a single comparison.

When it goes wrong

What you see What it means
A plausible but wrong value from an array The index arithmetic. Check BC: LD B,n puts n in the high half. Use LD BC,n.
A value from nowhere near your data Same cause, further off. Read HL in the register line and compare it with the array's address in the listing.
The loop runs the wrong number of times DJNZ counts B. Something else in the loop is using B.
error: relative jump out of range on a DJNZ The loop body is too big for a short jump. Use DEC B / JP NZ.
The total is smaller than it should be A holds one byte. Sums past 255 wrap round, and nothing tells you.

Summary

  • An array is bytes in consecutive memory. Nothing records its length or bounds - that is your job.
  • Element n is at base + n. Put the base in HL, the index in BC, and ADD HL,BC.
  • Use LD BC,n. LD B,n sets the high half and gives you an index 256 times too big, and the program runs anyway.
  • DJNZ decrements B and loops if it is not zero - the counted loop in one instruction.
  • A lookup table replaces a chain of comparisons with one index. It stays the same size however many cases there are, and you change behaviour by editing data.
  • There is no multiply instruction, so a power-of-two row width - indexed with ADD HL,HL - is worth wasting a few bytes for.

Next

S14. Tables of addresses. Indexing worked here because every element was the same size. Strings are not - so instead of storing the things in a table, you store where they are, and addresses are always two bytes.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.