A Hooded Figure
Shadowkeep begins where Gloaming ended — the same engine, a new face. Design a hooded thief as eight bytes and stand him, alone, in the cold blue dark of the keep.
You have already finished a game. Gloaming — a complete thing, played from a title and back again, verified on real iron. Shadowkeep is the next one, and it is bigger: a lit, multi-room keep you explore by night. But it does not start from nothing, and it does not start from a blank file.
It starts from Gloaming's engine. The cell sprite, the save-and-restore, the collision, the game loop, the state machine — you wrote all of it, and Shadowkeep carries it over wholesale. So the first thing a new game needs isn't new code. It's a new face.
What you'll see by the end
One small figure, dead centre, in the cold blue dark of the keep. He is a hooded thief — a cloaked silhouette, no arms, a pointed hood — and he is the reason this is Shadowkeep and not Gloaming. He doesn't move yet. He doesn't do anything yet. He just stands there, and that's enough: the moment you see him, you know whose game this is.
Starting from Gloaming
Most of this unit's code you have met before. Lift your eyes from the page and you could almost write it from memory:
- A black border —
ld a, 0/out ($FE), a. The keep wants the dark. - The stone wash — one
LDIRthat pours a single attribute byte across all 768 cells, exactly as Gloaming washed its square in cobble. Here the byte isSTONE: PAPER 1 (blue), INK 0. Cold, flat, and dim — a keep at night. - The address helpers —
scr_addr_crandattr_addr_cr, carried over byte-for-byte. Give them a row inBand a column inC, and they hand back the screen or attribute address of that cell. You built these in Gloaming; here they are again, doing the same job. - The eight-row draw loop —
ld a, (de)/ld (hl), a/inc de/inc h, eight times. Theinc his the Spectrum's famous screen quirk: the next pixel row of a character cell lives 256 bytes further on, so steppingHup by one walks straight down the cell.
None of that is new. What's new is the eight bytes that loop reads.
Designing a silhouette
A Spectrum sprite, at its simplest, is eight bytes — eight rows of eight pixels, a bit per pixel. Sixty-four pixels to say "this is the hero." It isn't much — and Ultimate's famous characters were bigger than this, several cells across and masked, drawn with far more effort than a single cell takes. But they won their recognisability the same way one cell must, and the trick is always the same: get the silhouette right and the detail doesn't matter. At eight-by-eight you have room for nothing but silhouette — which makes it the purest place to learn the rule.
Here is the thief, written so you can read the figure in the ones and zeros:
defb %00011000 ; ...XX... the hood's peak
defb %00111100 ; ..XXXX.. the hood
defb %01111110 ; .XXXXXX. hood meets shoulders
defb %01111110 ; .XXXXXX. the cloak
defb %01111110 ; .XXXXXX. the cloak
defb %01111110 ; .XXXXXX. the cloak
defb %00111100 ; ..XXXX.. the cloak narrows
defb %00100100 ; ..X..X.. two feet at the hem
Read the Xs down the page and the shape is there: a narrow point at the top for the hood, widening through the shoulders, a full cloaked body, then narrowing to two feet at the hem. A 1 bit is a lit pixel; a 0 is bare stone showing through. No face, no hands — a silhouette doesn't need them. Gloaming's hero was a little lamplighter with his arms out; the thief is all cloak and hood. Same eight bytes of effort, an entirely different character.
That difference — same engine, different silhouette — is the whole of this unit.
His colour, and the keep's
A sprite's shape is in the bitmap; its colour is in the one attribute byte of the cell it stands in. We wash the whole keep in STONE (blue), then overwrite the thief's own cell with THIEF:
STONE equ %00001000 ; PAPER 1 (blue), INK 0 — cold blue stone
THIEF equ %01001010 ; BRIGHT, PAPER 1 (blue), INK 2 (red)
THIEF keeps the blue PAPER, so he stands on the same stone as everything around him — but its INK is 2 (red) and its BRIGHT bit is set, so his lit pixels burn a vivid red. A red hooded figure on cold blue stone: he reads at a glance, even one character-cell tall.
Drawing him
draw_thief is Gloaming's draw_lamp with one word changed — it points at thief instead of the lamplighter:
draw_thief:
ld b, HERO_ROW
ld c, HERO_COL
call attr_addr_cr ; colour his cell
ld (hl), THIEF
call scr_addr_cr ; B,C still hold row and column
ld de, thief ; his eight bytes
ld b, 8
.dt:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dt
ret
Colour the cell, then lay the eight rows. That's the same draw a moving sprite will use a thousand times a game — but here it runs once, at the centre, and stops.
Milestone — stand him in the keep
We wash the whole keep dark and empty in cold blue stone, then stand the thief in its centre: colour his cell, then lay his eight rows into the bitmap with the carried-over draw loop.
| 1 | 1 | ; Shadowkeep — Unit 1: A Hooded Figure | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-01 washes the keep in cold blue stone — dark, and empty. | |
| 3 | + | ; step-02 stands the hooded thief in the centre of the keep. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | STONE equ %00001000 ; PAPER 1 (blue), INK 0 — cold blue stone | |
| 8 | + | THIEF equ %01001010 ; BRIGHT, PAPER 1 (blue), INK 2 (red) — the thief | |
| 9 | + | HERO_COL equ 15 ; the middle of the keep, as in Gloaming | |
| 10 | + | HERO_ROW equ 11 | |
| 8 | 11 | | |
| 9 | 12 | ; ---------------------------------------------------------------------------- | |
| 10 | - | ; SETUP — the keep is dark, and empty. | |
| 13 | + | ; SETUP — the keep is dark, and a figure stands in it. | |
| 11 | 14 | ; ---------------------------------------------------------------------------- | |
| 12 | 15 | start: | |
| 13 | 16 | ld a, 0 | |
| ... | |||
| 18 | 21 | ld (hl), STONE | |
| 19 | 22 | ld bc, 767 | |
| 20 | 23 | ldir | |
| 24 | + | | |
| 25 | + | call draw_thief | |
| 21 | 26 | | |
| 22 | 27 | im 1 | |
| 23 | 28 | ei | |
| 24 | 29 | .loop: | |
| 25 | 30 | halt | |
| 26 | 31 | jr .loop | |
| 32 | + | | |
| 33 | + | ; ---------------------------------------------------------------------------- | |
| 34 | + | ; draw_thief — colour his cell, then lay his eight rows into the bitmap. | |
| 35 | + | ; Exactly Gloaming's draw_lamp, pointed at a different eight bytes. | |
| 36 | + | ; ---------------------------------------------------------------------------- | |
| 37 | + | draw_thief: | |
| 38 | + | ld b, HERO_ROW | |
| 39 | + | ld c, HERO_COL | |
| 40 | + | call attr_addr_cr | |
| 41 | + | ld (hl), THIEF | |
| 42 | + | call scr_addr_cr ; B,C still hold the row and column | |
| 43 | + | ld de, thief | |
| 44 | + | ld b, 8 | |
| 45 | + | .dt: | |
| 46 | + | ld a, (de) | |
| 47 | + | ld (hl), a | |
| 48 | + | inc de | |
| 49 | + | inc h ; next pixel row is one cell-third down: +256 | |
| 50 | + | djnz .dt | |
| 51 | + | ret | |
| 52 | + | | |
| 53 | + | ; ---------------------------------------------------------------------------- | |
| 54 | + | ; scr_addr_cr / attr_addr_cr — row in B, column in C, address out in HL. | |
| 55 | + | ; Carried verbatim from Gloaming. | |
| 56 | + | ; ---------------------------------------------------------------------------- | |
| 57 | + | scr_addr_cr: | |
| 58 | + | ld a, b | |
| 59 | + | and %00011000 | |
| 60 | + | or %01000000 | |
| 61 | + | ld h, a | |
| 62 | + | ld a, b | |
| 63 | + | and %00000111 | |
| 64 | + | rrca | |
| 65 | + | rrca | |
| 66 | + | rrca | |
| 67 | + | or c | |
| 68 | + | ld l, a | |
| 69 | + | ret | |
| 70 | + | | |
| 71 | + | attr_addr_cr: | |
| 72 | + | ld a, b | |
| 73 | + | ld l, a | |
| 74 | + | ld h, 0 | |
| 75 | + | add hl, hl | |
| 76 | + | add hl, hl | |
| 77 | + | add hl, hl | |
| 78 | + | add hl, hl | |
| 79 | + | add hl, hl | |
| 80 | + | ld de, $5800 | |
| 81 | + | add hl, de | |
| 82 | + | ld a, c | |
| 83 | + | ld e, a | |
| 84 | + | ld d, 0 | |
| 85 | + | add hl, de | |
| 86 | + | ret | |
| 87 | + | | |
| 88 | + | ; ---------------------------------------------------------------------------- | |
| 89 | + | ; The hooded thief — eight bytes, drawn here so you can read the figure in the | |
| 90 | + | ; ones and zeros: a pointed hood, a cloaked body, two feet at the hem. | |
| 91 | + | ; ---------------------------------------------------------------------------- | |
| 92 | + | thief: | |
| 93 | + | defb %00011000 ; ...XX... the hood's peak | |
| 94 | + | defb %00111100 ; ..XXXX.. the hood | |
| 95 | + | defb %01111110 ; .XXXXXX. hood meets shoulders | |
| 96 | + | defb %01111110 ; .XXXXXX. the cloak | |
| 97 | + | defb %01111110 ; .XXXXXX. the cloak | |
| 98 | + | defb %01111110 ; .XXXXXX. the cloak | |
| 99 | + | defb %00111100 ; ..XXXX.. the cloak narrows | |
| 100 | + | defb %00100100 ; ..X..X.. two feet at the hem | |
| 27 | 101 | | |
| 28 | 102 | end start | |
| 29 | 103 | |
The complete program
; Shadowkeep — Unit 1: A Hooded Figure
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 stands the hooded thief in the centre of the keep.
org 32768
STONE equ %00001000 ; PAPER 1 (blue), INK 0 — cold blue stone
THIEF equ %01001010 ; BRIGHT, PAPER 1 (blue), INK 2 (red) — the thief
HERO_COL equ 15 ; the middle of the keep, as in Gloaming
HERO_ROW equ 11
; ----------------------------------------------------------------------------
; SETUP — the keep is dark, and a figure stands in it.
; ----------------------------------------------------------------------------
start:
ld a, 0
out ($FE), a ; black border — the keep wants the dark
ld hl, $5800 ; wash all 768 attribute cells in stone
ld de, $5801
ld (hl), STONE
ld bc, 767
ldir
call draw_thief
im 1
ei
.loop:
halt
jr .loop
; ----------------------------------------------------------------------------
; draw_thief — colour his cell, then lay his eight rows into the bitmap.
; Exactly Gloaming's draw_lamp, pointed at a different eight bytes.
; ----------------------------------------------------------------------------
draw_thief:
ld b, HERO_ROW
ld c, HERO_COL
call attr_addr_cr
ld (hl), THIEF
call scr_addr_cr ; B,C still hold the row and column
ld de, thief
ld b, 8
.dt:
ld a, (de)
ld (hl), a
inc de
inc h ; next pixel row is one cell-third down: +256
djnz .dt
ret
; ----------------------------------------------------------------------------
; scr_addr_cr / attr_addr_cr — row in B, column in C, address out in HL.
; Carried verbatim from Gloaming.
; ----------------------------------------------------------------------------
scr_addr_cr:
ld a, b
and %00011000
or %01000000
ld h, a
ld a, b
and %00000111
rrca
rrca
rrca
or c
ld l, a
ret
attr_addr_cr:
ld a, b
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld de, $5800
add hl, de
ld a, c
ld e, a
ld d, 0
add hl, de
ret
; ----------------------------------------------------------------------------
; The hooded thief — eight bytes, drawn here so you can read the figure in the
; ones and zeros: a pointed hood, a cloaked body, two feet at the hem.
; ----------------------------------------------------------------------------
thief:
defb %00011000 ; ...XX... the hood's peak
defb %00111100 ; ..XXXX.. the hood
defb %01111110 ; .XXXXXX. hood meets shoulders
defb %01111110 ; .XXXXXX. the cloak
defb %01111110 ; .XXXXXX. the cloak
defb %01111110 ; .XXXXXX. the cloak
defb %00111100 ; ..XXXX.. the cloak narrows
defb %00100100 ; ..X..X.. two feet at the hem
end start
A black border, a screen of cold blue stone, and a single red hooded figure standing in the middle of it. The keep has its first inhabitant.
Try this: redraw the silhouette
The eight bytes are a drawing grid. Sketch your own hero on paper — eight rows, eight squares each — fill the squares that should be lit, then read each row off as a binary number into a defb. Make the hood taller, give him a hint of a face by clearing a pixel, widen the cloak. Reassemble and he changes. This is the most direct relationship in the whole machine: the bits are the picture.
Try this: a thief of a different colour
Change THIEF's INK from 2 (red) to another colour — %01001110 for yellow (INK 6), %01001111 for white (INK 7). Which reads best against the blue stone? Try clearing the BRIGHT bit (%00001010) and watch him dim into the gloom. You're tuning a character's presence with three bits.
Try this: a warmer keep
Change STONE from %00001000 (PAPER 1, blue) to %00010000 (PAPER 2, red) or %00000000 (black). The whole keep's mood shifts with one byte — a cold blue dungeon, a hellish red one, or a pitch-black void where only the figure shows. We'll keep the cold blue; but feel how much the background colour alone decides the place.
When it's wrong, see why
- No figure at all.
draw_thiefisn't being called, ordedoesn't point atthief. The draw loop reads its eight bytes from(de), sold de, thiefmust come first. - The figure is there but the wrong shape. Check the
defbbytes — a mistyped bit flips a pixel. Read theXs in the comments against what's on screen. - The figure is a single solid block, or scrambled. The
inc hin the loop is what walks down the cell. Withinc linstead you'd write eight bytes along one pixel row, not down eight rows — a thin streak, not a figure. - The screen is grey-white, not blue. The
LDIRstone wash didn't run, orDEwasn't set toHL + 1. Without the wash the keep is whatever the ROM left behind. - Wrong place on screen.
HERO_ROWandHERO_COLset the cell.scr_addr_crandattr_addr_crexpect the row inB, the column inC— swap them and he lands somewhere unexpected.
Before and after
You started with a finished game behind you and a blank ambition ahead, and ended with Shadowkeep's hero standing in its first dark room — and almost none of it was new code. The black border, the LDIR stone wash, scr_addr_cr and attr_addr_cr, the eight-row inc h draw loop all carried over from Gloaming intact; the one new thing was eight bytes of silhouette and a colour. That is how a sequel starts — not from a blank file, but from a finished engine wearing a new face.
What you've learnt
- A game's identity starts with its silhouette. Eight bytes, sixty-four pixels — get the shape right and the character reads, even tiny.
- A sprite is shape plus colour. The bitmap (eight
defbrows) is the shape; the one attribute byte of its cell is the colour. - The bits are the picture. Lit pixels are
1s, bare cell is0s — you can draw straight in binary. - Shadowkeep stands on Gloaming. The border, the
LDIRwash,scr_addr_cr/attr_addr_cr, theinc hdraw loop — all carried over. A new game reuses a finished engine; it doesn't rebuild it.
What's next
The thief stands in an empty blue void. A keep needs walls. In Unit 2, "The First Hall," we draw the keep's first room — a designed hall of stone walls and floor, laid from a data table the way Gloaming drew its square, but composed to look like somewhere. By the end the thief will be standing in a place, not a void — the first room of a keep that will grow, over this game, into many.