Sprites: things that move without the screen moving
Everything drawn on this machine was in a cell. Something between two cells meant rewriting both; something moving a pixel at a time meant rewriting them sixty times a second, which is affordable for one thing and not for twenty. A sprite is put at a pixel and the device draws it over whatever is behind, so moving it costs two bytes. MADE OF TILES, which is the decision the rest follows from: m by n taken in reading order from one index, so there is no second pixel format, no second kind of memory, and nothing a sprite can show that the map cannot. A 16 by 16 character is four tiles and the background can name the same four. 256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry address is a shift, the same no-multiply argument as the palette's four. Position is signed and sixteen bits, because 640 by 400 does not fit in a byte and a sprite has to be able to sit half off the left rather than appearing whole at the edge. A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested before the attribute is added, so a hole belongs to the art and not to the colour scheme. The same rule the other way round is what "behind" means: drawn only where the background pixel was zero, so a thing walks behind a pillar and in front of the floor in one frame. All of them draw, every frame, so they cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers; this has a loop. The limit is the size of the table, which is a constant rather than a property of what is on screen. And the system takes them down at exit. The sprite table sits in the gap the screen save walks around - to the end of the map, then the palette - and that is right, because nothing the shell draws is a sprite: there is nothing to give back, only something to take away. Otherwise a program that put a ball up and left would leave it over the prompt, in front of everything, with nothing able to type it away. Sprite.asm deliberately leaves its own, because a program that faulted could not have cleared it. Every check here was re-broken and failed: transparency, reading order, draw order, priority, and size. Size needed breaking twice - the first attempt did not compile, and a silent build failure had left the old binary passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
eee95ef0ce
commit
a916103a7f
+105
-1
@@ -17,6 +17,18 @@
|
||||
static uint8_t videoAtlas[VIDEO_MEMORY_BYTES];
|
||||
static uint8_t videoScreen[VIDEO_SCREEN_COUNT][VIDEO_MEMORY_BYTES];
|
||||
|
||||
// ---- Where the background was empty ----
|
||||
//
|
||||
// One byte a pixel, set while the map or the bitmap is drawn and read while the sprites are.
|
||||
// A sprite marked "behind" needs to know whether the thing already at a pixel was a picture
|
||||
// or a gap, and by the time it is drawn the pixel holds a colour rather than the index it
|
||||
// came from - the palette is not one to one, so two different indices can be the same
|
||||
// colour and asking the picture would get it wrong.
|
||||
//
|
||||
// Host memory, and it costs the machine nothing: it is scratch the device uses inside one
|
||||
// frame, exactly like the pixel buffer beside it.
|
||||
static uint8_t backgroundEmpty[VIDEO_MAX_WIDTH * VIDEO_MAX_HEIGHT];
|
||||
|
||||
// Which screen is being shown. The console draws into THIS one rather than into a screen of
|
||||
// its own, so text goes where whoever is looking is looking - which matters most when the
|
||||
// text is a fault message printed over a game that had flipped.
|
||||
@@ -346,6 +358,81 @@ uint8_t videoRead(uint8_t port) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- The sprites, over whatever is already there ----
|
||||
//
|
||||
// BACKWARDS THROUGH THE TABLE, so that where two overlap the lower number comes out on top:
|
||||
// it is drawn last and writes over. Every entry is looked at, because the ones that draw
|
||||
// nothing say so in a byte and skipping them costs one test.
|
||||
static void drawSprites(uint8_t *pixels, int width, int height) {
|
||||
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
for (int n = VIDEO_SPRITE_COUNT - 1; n >= 0; n--) {
|
||||
const uint8_t *entry = videoAtlas + VIDEO_SPRITE_BASE + n * VIDEO_SPRITE_BYTES;
|
||||
const int wide = (entry[VIDEO_SPRITE_SIZE] >> 4) & 0x0F;
|
||||
const int tall = entry[VIDEO_SPRITE_SIZE] & 0x0F;
|
||||
if (wide == 0 || tall == 0) {
|
||||
continue;
|
||||
}
|
||||
// Signed, and low byte first like everything else this machine writes to a device.
|
||||
const int left = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_X]
|
||||
| (entry[VIDEO_SPRITE_X + 1] << 8));
|
||||
const int top = (int16_t)(uint16_t)(entry[VIDEO_SPRITE_Y]
|
||||
| (entry[VIDEO_SPRITE_Y + 1] << 8));
|
||||
const uint8_t bank = (uint8_t)((entry[VIDEO_SPRITE_ATTRIBUTE] & 0x0F) << 4);
|
||||
const uint8_t flags = entry[VIDEO_SPRITE_FLAGS];
|
||||
const int mirrored = (flags & VIDEO_SPRITE_HFLIP) != 0;
|
||||
const int inverted = (flags & VIDEO_SPRITE_VFLIP) != 0;
|
||||
const int behind = (flags & VIDEO_SPRITE_BEHIND) != 0;
|
||||
|
||||
for (int downTile = 0; downTile < tall; downTile++) {
|
||||
for (int acrossTile = 0; acrossTile < wide; acrossTile++) {
|
||||
// ---- Flipping moves the tiles as well as the pixels ----
|
||||
//
|
||||
// A mirrored sprite is not each of its tiles mirrored in place; the tile at
|
||||
// the left end has to come out at the right end too, or a thing made of more
|
||||
// than one tile turns inside out instead of round.
|
||||
const int readAcross = mirrored ? (wide - 1 - acrossTile) : acrossTile;
|
||||
const int readDown = inverted ? (tall - 1 - downTile) : downTile;
|
||||
// In reading order from the first, and wrapping, because a byte plus a byte
|
||||
// is a byte and the tile number is one.
|
||||
const uint8_t tile = (uint8_t)(entry[VIDEO_SPRITE_TILE]
|
||||
+ readDown * wide + readAcross);
|
||||
const uint8_t *art = videoAtlas + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
||||
|
||||
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
||||
const int atY = top + downTile * VIDEO_CELL_PIXELS + y;
|
||||
if (atY < 0 || atY >= height) {
|
||||
continue;
|
||||
}
|
||||
const int fromY = inverted ? (VIDEO_CELL_PIXELS - 1 - y) : y;
|
||||
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||
const int atX = left + acrossTile * VIDEO_CELL_PIXELS + x;
|
||||
if (atX < 0 || atX >= width) {
|
||||
continue;
|
||||
}
|
||||
const int fromX = mirrored ? (VIDEO_CELL_PIXELS - 1 - x) : x;
|
||||
const uint8_t pixel = art[fromY * VIDEO_CELL_PIXELS + fromX];
|
||||
// Nought is not a colour here, it is the absence of one, and it is
|
||||
// tested before the attribute is added so that it stays the same
|
||||
// hole in all sixteen schemes.
|
||||
if (pixel == 0) {
|
||||
continue;
|
||||
}
|
||||
if (behind && !backgroundEmpty[atY * width + atX]) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t index = (uint8_t)(pixel + bank);
|
||||
const uint8_t *colour = palette + index * VIDEO_PALETTE_BYTES;
|
||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||
out[0] = colour[0];
|
||||
out[1] = colour[1];
|
||||
out[2] = colour[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void videoRender(void) {
|
||||
if (mode == VIDEO_MODE_BITMAP) {
|
||||
// ---- A byte a pixel, and nothing in the way ----
|
||||
@@ -361,9 +448,14 @@ void videoRender(void) {
|
||||
*out++ = entry[0];
|
||||
*out++ = entry[1];
|
||||
*out++ = entry[2];
|
||||
backgroundEmpty[at] = (from[at] == 0);
|
||||
}
|
||||
renderedWidth = VIDEO_BITMAP_WIDTH;
|
||||
renderedHeight = VIDEO_BITMAP_HEIGHT;
|
||||
// Over a picture as much as over a map. A bitmap is what a program draws once and
|
||||
// leaves; sprites are what moves on top of it, and there is no reason the mode that
|
||||
// cannot afford to redraw itself should be the one that cannot have them.
|
||||
drawSprites(pixels, VIDEO_BITMAP_WIDTH, VIDEO_BITMAP_HEIGHT);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -428,17 +520,29 @@ void videoRender(void) {
|
||||
// Wrapping, because a byte plus a byte is a byte. A tile using the
|
||||
// high end of the palette with a nibble set comes round the bottom,
|
||||
// which is what an adder does and what the manual says it does.
|
||||
const uint8_t index = (uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
|
||||
const uint8_t was = art[y * VIDEO_CELL_PIXELS + x];
|
||||
const uint8_t index = (uint8_t)(was + bank);
|
||||
const uint8_t *entry = videoAtlas + VIDEO_PALETTE_BASE
|
||||
+ index * VIDEO_PALETTE_BYTES;
|
||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||
out[0] = entry[0];
|
||||
out[1] = entry[1];
|
||||
out[2] = entry[2];
|
||||
// Before the nibble, so that a cell drawn in scheme five is empty in the
|
||||
// same places as the same cell drawn in scheme nought.
|
||||
backgroundEmpty[atY * width + atX] = (was == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ---- And then the things that move ----
|
||||
//
|
||||
// After the map and not woven into it, because a sprite is not tied to a cell: one can
|
||||
// sit across four of them, and a pass that drew each cell and then whatever overlapped it
|
||||
// would have to draw parts of the same sprite four times and get the order right between
|
||||
// them. Over the finished picture there is no order to get wrong.
|
||||
drawSprites(pixels, width, height);
|
||||
|
||||
renderedWidth = width;
|
||||
renderedHeight = rows * VIDEO_CELL_PIXELS;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,81 @@
|
||||
#define VIDEO_BITMAP_WIDTH 320
|
||||
#define VIDEO_BITMAP_HEIGHT 200
|
||||
|
||||
// ---- Sprites ----
|
||||
//
|
||||
// Things that move without the map moving. A map cell is where it is, and a program that
|
||||
// wanted something between two cells had to redraw both of them; a sprite is put at a PIXEL
|
||||
// and the device draws it over whatever is behind.
|
||||
//
|
||||
// MADE OF TILES, which is the decision the rest follows from. A sprite is m by n tiles taken
|
||||
// in reading order from one index, so it needs no pixel format of its own, no second kind of
|
||||
// memory, and no way for its art to be anything the map could not also show. A 16 by 16
|
||||
// character is four tiles and a program that wants it in the background too just names the
|
||||
// same four.
|
||||
//
|
||||
// The table is 256 entries of 8 bytes. Eight so that entry n begins at n times eight, which
|
||||
// is a shift - the same no-multiply argument that makes a palette entry four bytes and a map
|
||||
// row a page. It sits above the tiles with the whole of 0x4000 to 0xBFFF still clear beneath
|
||||
// it, which is two more 16K pages of tiles if they are ever wanted.
|
||||
#define VIDEO_SPRITE_BASE 0xC000
|
||||
#define VIDEO_SPRITE_COUNT 256
|
||||
#define VIDEO_SPRITE_BYTES 8
|
||||
|
||||
// Byte 0 is the top left tile, byte 1 the attribute, which means what a map cell's attribute
|
||||
// means: its low nibble times sixteen is added to every index in the art.
|
||||
#define VIDEO_SPRITE_TILE 0
|
||||
#define VIDEO_SPRITE_ATTRIBUTE 1
|
||||
|
||||
// Bytes 2 to 5, low byte first, and SIGNED - a screen is 640 by 400 in the larger mode, so
|
||||
// neither axis fits in a byte, and a sprite has to be able to sit half off the left or the
|
||||
// top rather than appearing whole at the edge.
|
||||
#define VIDEO_SPRITE_X 2
|
||||
#define VIDEO_SPRITE_Y 4
|
||||
|
||||
// Byte 6: how many tiles across in the high nibble, how many down in the low. Fifteen each
|
||||
// way, so 120 by 120 pixels.
|
||||
//
|
||||
// A SPRITE OF NO WIDTH OR NO HEIGHT DRAWS NOTHING, and that is the off switch. It saves a
|
||||
// flag, it is per sprite rather than a global the whole table shares, and it means the table
|
||||
// is already off when the machine starts, since the atlas wakes up cleared.
|
||||
//
|
||||
// Deliberately the OPPOSITE of what a length of zero means to the memory controller, where
|
||||
// it means the whole 64K. The reason is the same both times: moving no bytes is a useless
|
||||
// thing to ask for, so zero was free to mean something else there - and drawing no sprite is
|
||||
// the commonest state in this table, so zero has to mean nothing here.
|
||||
#define VIDEO_SPRITE_SIZE 6
|
||||
|
||||
// Byte 7.
|
||||
#define VIDEO_SPRITE_FLAGS 7
|
||||
#define VIDEO_SPRITE_HFLIP 0x01
|
||||
#define VIDEO_SPRITE_VFLIP 0x02
|
||||
// Drawn only where the background had nothing, so a thing can walk behind a pillar. See
|
||||
// below for what "nothing" means.
|
||||
#define VIDEO_SPRITE_BEHIND 0x04
|
||||
|
||||
// ---- What a sprite does not cover ----
|
||||
//
|
||||
// A PIXEL OF ZERO IS NOT DRAWN. Without that every sprite is a rectangle, and there is no
|
||||
// other candidate: the font already uses index 0 for paper, so it is the value art in this
|
||||
// machine has always left empty.
|
||||
//
|
||||
// Tested BEFORE the attribute is added, so it is a property of the art and not of the colour
|
||||
// scheme it is being shown in. A sprite drawn in indices 1 to 15 is transparent in the same
|
||||
// places in all sixteen schemes, which is the whole point of the additive nibble.
|
||||
//
|
||||
// The same rule read the other way is what "behind" means: a sprite marked behind draws only
|
||||
// where the BACKGROUND pixel was zero. One rule, applied to whichever layer is in front.
|
||||
//
|
||||
// ---- How many at once ----
|
||||
//
|
||||
// All of them. Every entry in the table is drawn every frame, so sprites cannot flicker.
|
||||
// Real machines dropped them per scanline because they had a fixed number of shift registers
|
||||
// and a fixed time to fill them; this has a loop. The limit is how many entries there are,
|
||||
// which is a constant a program can count on rather than a property of what it happens to be
|
||||
// drawing this frame.
|
||||
//
|
||||
// Where they overlap, THE LOWER NUMBER IS IN FRONT.
|
||||
|
||||
// ---- The palette ----
|
||||
//
|
||||
// Four bytes an entry rather than three, for the same reason a map row is a page: entry n
|
||||
|
||||
Reference in New Issue
Block a user