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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user