English draft · one game, one ROM per chapter

Atari 2600 Mastery

From an empty folder to a cartridge: how Dusk Manor was made for the Melody, chapter by chapter, with its book edition to build along. Every number in it was measured; the pictures are named where they belong and still to be placed.

Part 0 · Before the first line

Part 1 · Foundations

Part 2 · The line

Part 3 · Engine

Chapter 16

Isometric rooms

ch16-isometric-rooms.bin

ROM: ch16-isometric-rooms/ch16-isometric-rooms.bin; make room1 and make room2 build the same ROM starting in the other two rooms; make ticks measures the frame

Goal

A room is data: a width, a depth, a colour, a type, a doorway or not on each of its four walls. From that the ARM draws the paved floor of chapter 15, or a staircase, with doorways standing on the far walls and mats of paving running out through the near ones. The adventurer and the things live in units and are projected to the screen where they are drawn; the stick moves along the room's axes. And the frame's ARM budget becomes a number, measured, and the work is split over the two blanks to fit it. Prerequisites: chapter 15. New: the overscan call does real work; make ticks.

A room in six bytes

typedef struct {
    unsigned char w, h;             /* extents in units (half of DUSK MANOR's, to fit a byte: x2) */
    unsigned char colour;           /* the room's hue */
    unsigned char type;
    unsigned char door[4];          /* a doorway per wall, centred, or none */
} room_t;

static const room_t ROOMS[3] = {
    { 100, 100, 0x20, RT_FLOOR, { DOOR_OPEN, DOOR_OPEN, DOOR_OPEN, DOOR_OPEN } },
    { 100,  50, 0x40, RT_FLOOR, { DOOR_OPEN, DOOR_NONE, DOOR_OPEN, DOOR_OPEN } },
    {  50, 100, 0x00, RT_STAIR, { DOOR_OPEN, DOOR_NONE, DOOR_OPEN, DOOR_NONE } }
};

The walls are numbered from the far wall on the top right, WALL_N (y = 0), round through WALL_E (x = W, bottom right), WALL_S (y = H, bottom left) to WALL_W (x = 0, top left). The two far walls face the player; a doorway in them is a frame. The two near walls are the screen's bottom edges; a doorway in them is paving that runs out of the picture. Chapter 17 turns DOOR_OPEN into "which room is through it".

Entering a room derives everything the projection needs:

static void enterRoom(int r)
{
    room = r;
    W = ROOMS[r].w * 2;
    H = ROOMS[r].h * 2;
    dOff   = (W - H) >> 1;
    rowTop = ISO_CROW - (((W + H) * 82) >> 9);
    rowBot = ROW_OF(W + H);
    nTreads = (H * 41) >> 10;
    ...
}

rowTop places the diamond's far corner so that the diamond is centred on row 104 whatever its depth: 40 for the square room, 56 for the wide one and the staircase. dOff recentres a room that is wider than it is deep, since d = x − y is not symmetric then. Nothing else in the projection changes; PX_OF and ROW_OF are chapter 15's.

The doorways

The far walls are not drawn — a wall is the edge of the floor, and the black beyond it is wall enough. A doorway in a far wall is a frame: a lintel, two jambs, a sill, white on the frame lines, which are the odd rows chapter 15 left empty for this. The lintel follows the wall's slope, with shoulders at both ends:

            for (t = a; t <= b; t++) {
                int d = (wall == WALL_N) ? t : -t;
                int shoulder = (t < a + 5 || t > b - 5) ? 4 : 0;
                int top = ROW_OF(t) - lift - DOOR_H + shoulder - 1;
                p = PX_OF(d);
                while (!IS_FRAME(top)) top--;
                fb_set(&fb, p, top);
                if (shoulder) fb_set(&fb, p, top - 4);
            }

Every point of the lintel is a point on the wall's line, DOOR_H rows up, then moved to the nearest frame line above: a frame drawn on the even rows would take the floor's colour on those rows and disappear into it. The jambs are the same idea vertically, FRAME_FOOT rows of them solid on the floor lines too, so that they read as posts; the sill is the wall's line itself between the jambs. A doorway in a near wall is a mat, MAT_OUT units of paved() past the edge, and the eye reads it as an opening because the floor does not stop.

ch16-isometric-rooms at frame 40 — the square room: frames on the two far walls, mats through the two near ones, the paving, the three figures.

The staircase

A staircase is a room whose floor climbs. nTreads treads of TREAD units each run from the near wall to the far one, each RISER rows above the one before; a point's lift is its tread's:

static int liftAt(int x, int y)
{
    return (ROOMS[room].type == RT_STAIR) ? treadOf(x, y) * RISER : 0;
}

treadOf is (H − y) / TREAD as a multiply and a shift, 41 >> 10 for 1/25. The flight is drawn tread by tread, each on the floor's even rows with its rows lifted; each riser is the face under a tread's near edge, RISER rows of the frame lines, white. The far wall's doorway stands on the last tread, lifted with it, which is what wallLift is for.

ch16-isometric-rooms-2 at frame 40 — the staircase, eight treads climbing to the top right, the doorway on the top one.

Units, and the sprites

The adventurer stands at (pxu, pyu) in units; so do the bat and the spider. Where a sprite goes is one function:

static int spriteLine(int x, int y, int hover)
{
    return ROW_OF(x + y) - SPR_H - liftAt(x, y) - hover;
}
static int spriteClk(int x, int y)
{
    return CLK_OF(x - y) - SPR_CLK / 2;
}

Its bottom row is the row of its position, lifted on a stair, its column centred on it; the bat hovers eight rows above that. Walking up the staircase, the adventurer rises 89 rows from the near end to the far — 62 of them the floor's slope and 28 the treads.

The stick follows the room, not the screen: left and right change x, up and down change y, which on the screen are the two diagonals. That is how the full game plays, and it takes a minute to learn and then is never thought about again; the exercise offers the alternative.

The budget, measured

Chapter 15 said the floor is the biggest thing the ARM does. This chapter finds out what that costs, and the tool for it is new: the meters can show an overrun but cannot measure one, because the timer runs out and wraps, and the frame rolls. make ticks builds the ROM with both blanks 120 ticks long — the frame is about 400 lines and Stella shows it in grey, which is the colour-loss emulation of the developer settings telling the truth about a frame that is not 262 — and asks the debugger for the four timer readings the kernel keeps in zero page:

frame 40
dump $80 $8f 1

A dump with the 1 writes to a file, and the file is exact where the picture was not. With the ARM's time charged, ticks of 64 cycles, and the numbers with the 6507's own overhead taken out:

RoomThe floor (overscan)The rest (blank)Blanks: 31 and 46
0, square 200 × 2002627fits
1, wide 200 × 1001425fits
2, the staircase2323fits

The rest is the doorways, the colours, the sprites, the packer and the jump stream. It fitted because of two changes. The first is where the work is done: the vertical blank's call has 46 ticks and the overscan's 31, and a frame that draws in the blank only, as every chapter so far has, throws the overscan away. Here OverScan() ends with the floor:

    frame++;

    drawRoom();                         /* the next frame's picture, in this blank's time */
}

and VerticalBlank() starts from a framebuffer that already has the floor in it. The order of production is overscan, blank, picture; the simulator's frames() runs the two calls in that order for the same reason. Chapter 15's ROM, which draws everything in the blank, measures 57 ticks there against 46: it overruns on the cartridge, and its text says so now.

The second change is the scan. Chapter 15's exercise asked for it: a row of the diamond knows its own span in d, so the scan visits only the pixels between the two edges,

        int dLo = (-s > s - 2 * H) ? -s : s - 2 * H;     /* the row's span in d: */
        int dHi = (s < 2 * W - s) ? s : 2 * W - s;         /* x and y both in the room */
        int p0 = PX_OF(dLo), p1 = PX_OF(dHi) + 1;

and the staircase does the same per tread, which brought it from 45 ticks to 23. Chapter 31 goes on from here; what this chapter needed was to fit, with some room for the things chapters 17 to 20 add.

A finding about Stella

The measurements above were nearly not made. Stella keeps every option it is given on the command line as its new default, in a settings database it shares between runs; -dev.settings 1 -dev.thumb.inccycles 1, given once for chapter 15's experiment, was in force for every snapshot after it, including the plain ones, and this chapter's first pictures rolled for a reason its code could not explain. The snapshot script gives Stella a fresh base directory on every run now (-basedir), so nothing said once is still in force on the next run, and nothing said to the script reaches the Stella on the desk. F-17.

Verify

make run: the square room, the doorways, the mats. The button goes to the wide room and then to the staircase; on the staircase the adventurer climbs. 262. make ticks ROOM=0, 1, 2: the table above, give or take a tick. make sim:

    expect(W == 200 && H == 200 && rowTop == 40 && rowBot == 168, "room 0 is 200 x 200, rows 40..168");
    expect(fb.colour[41] == FRAME_COL && fb.colour[42] == 0x26, "frame lines white, floor lines the room's hue");
    expect(pxu == 4 && pyu == 4, "left and up for two hundred frames: the far corner");
    expect(room == 1 && W == 200 && H == 100 && dOff == 50, "the button: room 1, 200 x 100, recentred");
    expect(liftAt(50, 4) == 7 * RISER && liftAt(50, 196) == 0, "the far end is lifted seven risers, the near end none");
    expect(spriteLine(50, 4, 0) < spriteLine(50, 196, 0) - 80, "the adventurer climbs with the treads: 89 rows from the near end to the far");

Exercise

  1. Add a fourth room, 60 by 60, with one doorway. Which of the constants had to change? (None. That is what the table is for.)
  2. Make the stick follow the screen instead: right moves the adventurer along the screen's horizontal, which is x and y together. Play both for a minute. Dusk Manor kept the room's axes; Atic Atac had no choice to make.
  3. Move the doorways into the overscan too and measure. Then move the packer. Then take everything out of the blank's call and measure what an empty call costs: that is the floor under every number in the table.

Part 4 · Sound

Part 5 · From technique to game

Part 6 · Performance and real hardware

Part 7 · From ROM to product

Appendices