Skip to content
Game 2 Unit 6 of 16 1 hr learning time

Through the Doorway

Unit 5's room travel was a teleport to the centre. Make it a step: leave by one edge and arrive at the opposite edge of the next room, at the same height, mid-stride — so the flick reads as walking through a doorway.

38% of Shadowkeep

Last unit the thief could travel the keep, but the crossing cheated: walk through any door and you appeared in the middle of the next room. It worked, but it didn't feel like a doorway — it felt like a blink. This unit fixes that, and the fix is the beating heart of the flick-screen adventure.

The rule: leave by one edge, arrive at the opposite edge — at the same height. Walk off the east edge at row 11 and you step in at the west edge, still row 11, mid-stride. Your position across the seam is preserved; only the side flips. That one change turns a teleport into a step.

What you'll see by the end

The second chamber, with the red thief standing right at the doorway gap in its west wall, mid-height — exactly where he'd emerge after stepping east through the first room's east door.
Walked east, arrived at the next room's west doorway — at the same height he left, not dumped in the middle. The seam reads as a step through the wall, not a blink.

Walk east out of the pillared hall and you don't land in the middle of the next room — you arrive at its west doorway, at the same height you left, as if you'd walked straight through the wall. Turn around and walk back west, and you emerge at the east doorway of the hall, again at the same height. The two rooms feel joined at the threshold now, not wired together by teleport.

Opposite edge, same height

In Unit 5, travelling always set the thief to the centre. Now each edge sets a different entry point — the opposite edge — and leaves the cross-axis (his height for an east/west door, his column for a north/south door) untouched:

.east:                              ; left by the east edge -> enter at the west
            ; ... follow the east link into current_room ...
            ld      a, 1            ; west side, one cell in
            ld      (thief_col), a  ; (thief_row is left as it was)
            jr      .enter
.west:                              ; left by the west edge -> enter at the east
            ; ... follow the west link ...
            ld      a, 30           ; east side, one cell in
            ld      (thief_col), a
            jr      .enter

North and south mirror this on the other axis: leave by the top, arrive near the bottom at the same column; leave by the bottom, arrive near the top. We only set the axis we crossed; the other carries over, and that's what makes it read as a continuous step.

One cell in, not on the edge

Notice the entry column is 1, not 0 — one cell inside the west wall, not on it. That matters. The west edge is column 0; if we dropped him exactly there, check_exit would see him on an edge the next frame and bounce him straight back out. Landing one cell in puts him safely on the floor just past the doorway, free to walk on. The same reason gives 30 (not 31) for the east, 22 for the bottom, 1 for the top.

Doors have to line up

For this to land him on floor and not face-first into stone, the doorways must agree: room 0's east door is at row 11, so room 1's west door is at row 11 too. Leave east at row 11, arrive west at row 11 — straight onto the doorway floor of the next room. Lining up connected doors is now part of designing the keep; a door that leads to a blank stretch of the next room's wall would strand the thief against it. (Our two rooms agree, so the step is clean both ways.)

Milestone — make the crossing a step

check_exit keeps Unit 5's link-following, but instead of dropping the thief in the centre it sets only the axis he crossed — to the opposite edge, one cell in — and leaves the other axis alone. Leave east at row 11, arrive west at row 11; his height carries across the seam.

Step 1: arrive at the opposite edge, same height — a step, not a teleport
+52-61
11 ; Shadowkeep — Unit 6: Through the Doorway
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 5's end: travel works, but lands the thief in the room's centre.
3+; step-01 makes the crossing a step — arrive at the opposite edge, same height.
44
55 org 32768
66
...
4242 call player_step
4343 jr .loop
4444
45-; ----------------------------------------------------------------------------
46-; room_entry_addr — HL = rooms + current_room * 6 (entry = map ptr + 4 links).
47-; ----------------------------------------------------------------------------
4845 room_entry_addr:
4946 ld a, (current_room)
5047 ld l, a
5148 ld h, 0
5249 ld d, h
53- ld e, l ; DE = room number
54- add hl, hl ; *2
55- add hl, de ; *3
56- add hl, hl ; *6
50+ ld e, l
51+ add hl, hl
52+ add hl, de
53+ add hl, hl
5754 ld de, rooms
5855 add hl, de
5956 ret
6057
61-; ----------------------------------------------------------------------------
62-; draw_room — draw the current room's map. Same walk as Unit 4, but the map
63-; pointer comes from the room table now, not a fixed label.
64-; ----------------------------------------------------------------------------
6558 draw_room:
6659 call room_entry_addr
67- ld a, (hl) ; map pointer, low
60+ ld a, (hl)
6861 inc hl
69- ld h, (hl) ; map pointer, high
62+ ld h, (hl)
7063 ld l, a
7164 ld (map_ptr), hl
7265 ld b, 0
...
128121 pop bc
129122 ret
130123
131-; ----------------------------------------------------------------------------
132-; player_step — move as before, then check whether he stepped onto a doorway.
133-; ----------------------------------------------------------------------------
134124 player_step:
135125 ld a, (thief_col)
136126 ld (tcol), a
...
191181 ret
192182
193183 ; ----------------------------------------------------------------------------
194-; check_exit — if the thief is standing on an edge, follow that edge's link in
195-; the room table and travel there.
184+; check_exit — on an edge, follow that edge's link AND set the entry position
185+; to the opposite edge at the same height: a step through the doorway, not a
186+; jump to the middle. He arrives one cell inside the edge so he doesn't sit on
187+; the doorway and bounce straight back.
196188 ; ----------------------------------------------------------------------------
197189 check_exit:
198190 ld a, (thief_col)
199191 or a
200- jr z, .west ; column 0
192+ jr z, .west
201193 cp 31
202- jr z, .east ; column 31
194+ jr z, .east
203195 ld a, (thief_row)
204196 or a
205- jr z, .north ; row 0
197+ jr z, .north
206198 cp 23
207- jr z, .south ; row 23
208- ret ; not on an edge
209-.north:
199+ jr z, .south
200+ ret
201+.east: ; left by the east edge -> enter at the west
210202 call room_entry_addr
211- inc hl
212- inc hl ; +2 = North link
203+ ld de, 4
204+ add hl, de
213205 ld a, (hl)
214- jr .travel
215-.south:
206+ cp NO_EXIT
207+ ret z
208+ ld (current_room), a
209+ ld a, 1 ; west side, one cell in; row unchanged
210+ ld (thief_col), a
211+ jr .enter
212+.west: ; left by the west edge -> enter at the east
216213 call room_entry_addr
217- inc hl
218- inc hl
219- inc hl ; +3 = South link
214+ ld de, 5
215+ add hl, de
220216 ld a, (hl)
221- jr .travel
222-.east:
217+ cp NO_EXIT
218+ ret z
219+ ld (current_room), a
220+ ld a, 30 ; east side, one cell in; row unchanged
221+ ld (thief_col), a
222+ jr .enter
223+.north: ; left by the north edge -> enter at the south
223224 call room_entry_addr
224- ld de, 4 ; +4 = East link
225- add hl, de
225+ inc hl
226+ inc hl
226227 ld a, (hl)
227- jr .travel
228-.west:
228+ cp NO_EXIT
229+ ret z
230+ ld (current_room), a
231+ ld a, 22 ; bottom, one cell in; column unchanged
232+ ld (thief_row), a
233+ jr .enter
234+.south: ; left by the south edge -> enter at the north
229235 call room_entry_addr
230- ld de, 5 ; +5 = West link
231- add hl, de
236+ inc hl
237+ inc hl
238+ inc hl
232239 ld a, (hl)
233-.travel:
234240 cp NO_EXIT
235- ret z ; a doorway to nowhere — stay
241+ ret z
236242 ld (current_room), a
237- call draw_room
238- ld a, START_COL ; crude: drop him in the centre (Unit 6 fixes this)
239- ld (thief_col), a
240- ld a, START_ROW
243+ ld a, 1 ; top, one cell in; column unchanged
241244 ld (thief_row), a
245+.enter:
246+ call draw_room
242247 call save_under
243248 call draw_thief
244249 ret
...
335340 ret
336341
337342 ; ----------------------------------------------------------------------------
338-; Palette.
343+; Palette and the room graph (unchanged from Unit 5 — doors aligned at row 11).
339344 ; ----------------------------------------------------------------------------
340345 palette:
341346 defb '.'
...
345350 defw wall_tile
346351 defb WALL_ATTR
347352
348-; ----------------------------------------------------------------------------
349-; The room graph. Each entry: map pointer, then North, South, East, West links
350-; ($FF = no door). Two rooms, joined east-west: room 0's east door leads to
351-; room 1, room 1's west door leads back.
352-; ----------------------------------------------------------------------------
353353 rooms:
354354 defw room0_data
355355 defb NO_EXIT, NO_EXIT, 1, NO_EXIT
356356 defw room1_data
357357 defb NO_EXIT, NO_EXIT, NO_EXIT, 0
358358
359-; Room 0 — the pillared hall, with a doorway in the east wall (row 11).
360359 room0_data:
361360 defb "################################"
362361 defb "#..............................#"
...
383382 defb "#..............................#"
384383 defb "################################"
385384
386-; Room 1 — a plainer chamber with a block near the top, doorway in the west
387-; wall (row 11) leading back to room 0.
388385 room1_data:
389386 defb "################################"
390387 defb "#..............................#"
...
411408 defb "#..............................#"
412409 defb "################################"
413410
414-; ----------------------------------------------------------------------------
415-; Tiles and the thief.
416-; ----------------------------------------------------------------------------
417411 floor_tile:
418412 defb %10101010
419413 defb %01010101
...
444438 defb %00111100
445439 defb %00100100
446440
447-; ----------------------------------------------------------------------------
448-; Variables.
449-; ----------------------------------------------------------------------------
450441 current_room:
451442 defb 0
452443 thief_col:
The complete program
; Shadowkeep — Unit 6: Through the Doorway
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 makes the crossing a step — arrive at the opposite edge, same height.

            org     32768

WALL_ATTR   equ     %01001000
FLOOR_ATTR  equ     %00001000
THIEF       equ     %01001010
WALL_BIT    equ     6

START_COL   equ     15
START_ROW   equ     11
NO_EXIT     equ     $FF

KEYS_OP     equ     $DFFE
KEYS_Q      equ     $FBFE
KEYS_A      equ     $FDFE

; ----------------------------------------------------------------------------
; SETUP.
; ----------------------------------------------------------------------------
start:
            ld      a, 0
            out     ($FE), a

            xor     a
            ld      (current_room), a
            ld      a, START_COL
            ld      (thief_col), a
            ld      a, START_ROW
            ld      (thief_row), a

            call    draw_room
            call    save_under
            call    draw_thief

            im      1
            ei
.loop:
            halt
            call    player_step
            jr      .loop

room_entry_addr:
            ld      a, (current_room)
            ld      l, a
            ld      h, 0
            ld      d, h
            ld      e, l
            add     hl, hl
            add     hl, de
            add     hl, hl
            ld      de, rooms
            add     hl, de
            ret

draw_room:
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      (map_ptr), hl
            ld      b, 0
.room_row:
            ld      c, 0
.room_col:
            ld      hl, (map_ptr)
            ld      a, (hl)
            call    lookup_tile
            call    draw_tile
            ld      hl, (map_ptr)
            inc     hl
            ld      (map_ptr), hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .room_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .room_row
            ret

lookup_tile:
            ld      hl, palette
.scan:
            cp      (hl)
            jr      z, .found
            inc     hl
            inc     hl
            inc     hl
            inc     hl
            jr      .scan
.found:
            inc     hl
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            inc     hl
            ld      a, (hl)
            ld      (tile_attr), a
            ret

draw_tile:
            push    bc
            call    attr_addr_cr
            ld      a, (tile_attr)
            ld      (hl), a
            call    scr_addr_cr
            ld      de, (tile_ptr)
            ld      b, 8
.tile_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .tile_row
            pop     bc
            ret

player_step:
            ld      a, (thief_col)
            ld      (tcol), a
            ld      a, (thief_row)
            ld      (trow), a

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a
            jr      z, .left
            bit     0, a
            jr      z, .right
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a
            jr      z, .up
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a
            jr      z, .down
            ret
.left:
            ld      hl, tcol
            dec     (hl)
            jr      .move
.right:
            ld      hl, tcol
            inc     (hl)
            jr      .move
.up:
            ld      hl, trow
            dec     (hl)
            jr      .move
.down:
            ld      hl, trow
            inc     (hl)
.move:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz

            call    restore_under
            ld      a, (tcol)
            ld      (thief_col), a
            ld      a, (trow)
            ld      (thief_row), a
            call    save_under
            call    draw_thief
            call    check_exit
            ret

wall_at:
            call    attr_addr_cr
            bit     WALL_BIT, (hl)
            ret

; ----------------------------------------------------------------------------
; check_exit — on an edge, follow that edge's link AND set the entry position
; to the opposite edge at the same height: a step through the doorway, not a
; jump to the middle. He arrives one cell inside the edge so he doesn't sit on
; the doorway and bounce straight back.
; ----------------------------------------------------------------------------
check_exit:
            ld      a, (thief_col)
            or      a
            jr      z, .west
            cp      31
            jr      z, .east
            ld      a, (thief_row)
            or      a
            jr      z, .north
            cp      23
            jr      z, .south
            ret
.east:                              ; left by the east edge -> enter at the west
            call    room_entry_addr
            ld      de, 4
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1            ; west side, one cell in; row unchanged
            ld      (thief_col), a
            jr      .enter
.west:                              ; left by the west edge -> enter at the east
            call    room_entry_addr
            ld      de, 5
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 30           ; east side, one cell in; row unchanged
            ld      (thief_col), a
            jr      .enter
.north:                             ; left by the north edge -> enter at the south
            call    room_entry_addr
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 22           ; bottom, one cell in; column unchanged
            ld      (thief_row), a
            jr      .enter
.south:                             ; left by the south edge -> enter at the north
            call    room_entry_addr
            inc     hl
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1            ; top, one cell in; column unchanged
            ld      (thief_row), a
.enter:
            call    draw_room
            call    save_under
            call    draw_thief
            ret

; ----------------------------------------------------------------------------
; Save / restore / draw the thief — unchanged.
; ----------------------------------------------------------------------------
pos_bc:
            ld      a, (thief_row)
            ld      b, a
            ld      a, (thief_col)
            ld      c, a
            ret

save_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.save_row:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .save_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (hl)
            ld      (under_thief + 8), a
            ret

restore_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.restore_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .restore_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (under_thief + 8)
            ld      (hl), a
            ret

draw_thief:
            call    pos_bc
            call    attr_addr_cr
            ld      (hl), THIEF
            call    pos_bc
            call    scr_addr_cr
            ld      de, thief
            ld      b, 8
.thief_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .thief_row
            ret

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

; ----------------------------------------------------------------------------
; Palette and the room graph (unchanged from Unit 5 — doors aligned at row 11).
; ----------------------------------------------------------------------------
palette:
            defb    '.'
            defw    floor_tile
            defb    FLOOR_ATTR
            defb    '#'
            defw    wall_tile
            defb    WALL_ATTR

rooms:
            defw    room0_data
            defb    NO_EXIT, NO_EXIT, 1, NO_EXIT
            defw    room1_data
            defb    NO_EXIT, NO_EXIT, NO_EXIT, 0

room0_data:
            defb    "################################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................."
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "################################"

room1_data:
            defb    "################################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............##..............#"
            defb    "#..............##..............#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "...............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "################################"

floor_tile:
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101

wall_tile:
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000

thief:
            defb    %00011000
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00100100

current_room:
            defb    0
thief_col:
            defb    START_COL
thief_row:
            defb    START_ROW
tcol:
            defb    0
trow:
            defb    0
map_ptr:
            defw    0
tile_ptr:
            defw    0
tile_attr:
            defb    0
under_thief:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0

            end     start

Walk east through the hall's right-hand door and you arrive at the left edge of the next room, level with where you left — then walk on. It feels like one place now:

East through the hall's doorway — and the thief steps in at the next room's west doorway, the same height he left, then walks on into the chamber. His position across the seam is preserved; only the side flipped. A teleport became a step.

Try this: misalign a door

Move room 1's west doorway up a few rows (shift the . in its west wall) but leave room 0's east door at row 11. Walk east. You'll arrive at row 11 of room 1 — against its west wall, because the door's elsewhere. A vivid lesson in why connected doors must share a row (or column). Put it back, or move both to match.

Try this: keep his exact row

Right now his row carries across unchanged — try breaking it on purpose: in .east, also force thief_row to the centre. Walk through at the top of the door, and watch him jump to mid-height as he crosses. Ugly — and proof of how much the preserved cross-axis was doing for the feel. Undo it.

Try this: a corridor of rooms

Add a third room east of room 1 (room 1 gains an east door and link; room 2 gets a west door and link back). Now you can walk a straight line of three chambers, the screen flicking each time you cross a threshold, your height carried the whole way. A keep is just enough rooms with enough agreeing doors.

When it's wrong, see why

  • He bounces straight back through the door. The entry cell is on the edge (column 0 or 31, row 0 or 23). Use one cell in — 1, 30, 22, 1 — so he doesn't re-trigger check_exit on arrival.
  • He arrives stuck against a wall. The connected doors don't share a row (for east/west) or column (for north/south). Line them up.
  • He jumps to a different height when crossing. An entry branch is writing the cross-axis too. East/west should set only thief_col; north/south only thief_row.
  • He enters at the wrong side. Check the mapping: leave east → enter west (small column); leave west → enter east (large column). It's the opposite edge, not the same one.
  • The screen tears for a moment on crossing. That's the full-room repaint — the genre flick. It settles at once in play.

Before and after

You started with travel that worked but felt like a blink, and finished with a crossing that reads as a walk — leave one edge, arrive at the opposite one at the same height, one cell in so the exit check doesn't fire again. It is a handful of bytes: which axis to set, which edge to set it to, and the discipline of landing just inside. But it's the difference between a wired-up pair of screens and a keep that feels like a continuous place — and a new level-design rule, that connected doors must agree, came with it.

What you've learnt

  • A doorway is a preserved coordinate. Crossing an edge flips the side you're on but keeps your position along the wall — that continuity is what makes a flick feel like a step.
  • Land one cell inside the edge. On the edge itself, the exit check fires again and bounces you. Just inside, you're free to walk.
  • Connected doors must agree. East-to-west doors share a row; north-to-south doors share a column. Alignment is a level-design rule now.
  • Small rules, big feel. A couple of bytes of entry coordinate turn a teleport into a walk — most of "game feel" is details exactly this size.

What's next

The thief steps cleanly between rooms, but each room is drawn fresh every time he enters — the keep has no memory. In Unit 7, "The Hero Remembers," we make the keep a persistent place: it tracks where the thief is across the whole world, so the keep holds its state as he moves through it — the groundwork for rooms that stay as he left them, and for a keep big enough to get lost in. From a pair of rooms that flick, toward a world that remembers.