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
@@ -0,0 +1,247 @@
|
||||
; A thing that moves without the screen moving.
|
||||
;
|
||||
; Everything drawn on this machine before sprites was in a CELL. Something between two cells
|
||||
; meant rewriting both of them, and 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 already there. Moving
|
||||
; it costs two bytes: the low and high halves of where it now is. That is the whole of the
|
||||
; loop below, and it is why this program can run over the shell's own text without disturbing
|
||||
; a single character of it - nothing underneath is written to at all.
|
||||
;
|
||||
; ---- What it leaves behind ----
|
||||
;
|
||||
; The sprite, still in the table. On purpose, and for the same reason Flip leaves the screen
|
||||
; it flipped to: a program that FAULTED would have left it too, and a system that only tidied
|
||||
; up after programs which remembered would be one that left a ball sitting over the prompt
|
||||
; the first time somebody's game crashed. The table is the system's to clear.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x5000
|
||||
|
||||
start:
|
||||
; The atlas, which is where both the tiles and the sprite table live. Four is what CosmOS
|
||||
; uses for it; see the table in the CosmOS README for who owns which number.
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x30
|
||||
OUTA 0xE2
|
||||
INIA 0x03
|
||||
OUTA 0xE8
|
||||
|
||||
SETD.0 Message
|
||||
SWI osPrintString
|
||||
INIA 0x0A
|
||||
OUTA 0x00
|
||||
|
||||
CALL putBall
|
||||
CALL screenWidth
|
||||
CALL putSprite
|
||||
|
||||
; Key mode, so a key arrives when it is pressed rather than when Return is.
|
||||
INIA 0x01
|
||||
OUTA 0x02
|
||||
|
||||
everyFrame:
|
||||
CALL waitFrame
|
||||
CALL stepBall
|
||||
CALL moveSprite
|
||||
|
||||
; Anything typed ends it. Asked for and never waited on, so a key pressed between frames
|
||||
; is still there when this looks.
|
||||
INA 0x01
|
||||
INIB 0x01 ; READY
|
||||
AND
|
||||
BRQ everyFrame
|
||||
INA 0x00 ; Taken, so the shell is not handed a key meant for this.
|
||||
|
||||
RSTA
|
||||
OUTA 0x02 ; Line mode again. The sprite is left where it is.
|
||||
SWI osExit
|
||||
|
||||
; ---- The art, into a tile above the font ----
|
||||
;
|
||||
; Two hundred, which is well clear of the 135 glyphs the character generator copies back, so
|
||||
; nothing here costs the shell a letter. Blitted rather than poked: it is already sixty four
|
||||
; bytes of Data Segment and the controller moves it in one command.
|
||||
putBall:
|
||||
INIA 0x01
|
||||
OUTA 0xE0 ; SourceBank: Data Memory.
|
||||
; The pointer written down before it is read out of memory a byte at a time, because a Data
|
||||
; Pointer's two halves cannot be got at any other way.
|
||||
SETD.1 BallArtAt
|
||||
SETD.0 BallArt
|
||||
STD.0.1
|
||||
LDA.1
|
||||
OUTA 0xE1
|
||||
INCD.1
|
||||
LDA.1
|
||||
OUTA 0xE2
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0x32
|
||||
OUTA 0xE4 ; Tile 200 begins at 200 times 64, which is 0x3200.
|
||||
RSTA
|
||||
OUTA 0xE5
|
||||
OUTA 0xE6
|
||||
INIA 0x40
|
||||
OUTA 0xE7 ; Sixty four bytes.
|
||||
INIA 0x01
|
||||
OUTA 0xE8 ; Blit.
|
||||
RET
|
||||
|
||||
; ---- How wide the screen is, in pixels ----
|
||||
;
|
||||
; Columns times eight, and this machine cannot multiply. A and B are one sixteen bit shift
|
||||
; register though: with the column count in A and nothing in B, A:B holds columns times 256,
|
||||
; and five shifts right divide that by thirty two - which is columns times eight, high byte
|
||||
; left in A and low byte in B.
|
||||
;
|
||||
; Asked rather than assumed, because the shell runs eighty columns and a game may well have
|
||||
; asked for forty before starting this.
|
||||
screenWidth:
|
||||
INA 0x32
|
||||
RSTB
|
||||
SHR
|
||||
SHR
|
||||
SHR
|
||||
SHR
|
||||
SHR
|
||||
SETD.0 WidthHigh
|
||||
STA.0
|
||||
; B cannot be stored, and there is no move from it. Adding nothing to it puts it in Q,
|
||||
; which can be copied to A, which can.
|
||||
RSTA
|
||||
CCF
|
||||
ADD
|
||||
MVQA
|
||||
SETD.0 WidthLow
|
||||
STA.0
|
||||
RET
|
||||
|
||||
; ---- The entry, written straight through ----
|
||||
;
|
||||
; The controller's Data port steps its address on after every byte, so all eight go out of
|
||||
; one port with the address named once.
|
||||
putSprite:
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
RSTA
|
||||
OUTA 0xE5 ; Sprite nought is at 0xC000.
|
||||
INIA 0xC8
|
||||
OUTA 0xE9 ; Tile 200.
|
||||
INIA 0x01
|
||||
OUTA 0xE9 ; Attribute one, so the ball comes out in scheme one's ink.
|
||||
RSTA
|
||||
OUTA 0xE9
|
||||
OUTA 0xE9 ; X, low then high.
|
||||
INIA 0d96
|
||||
OUTA 0xE9
|
||||
RSTA
|
||||
OUTA 0xE9 ; Y, ninety six pixels down.
|
||||
INIA 0x11
|
||||
OUTA 0xE9 ; One tile across by one down.
|
||||
RSTA
|
||||
OUTA 0xE9 ; Not mirrored, not turned over, not behind.
|
||||
RET
|
||||
|
||||
; A frame, which is the only regular beat this machine has.
|
||||
waitFrame:
|
||||
INA 0x30
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ waitFrame
|
||||
RET
|
||||
|
||||
; ---- One pixel to the right ----
|
||||
;
|
||||
; Sixteen bits in two bytes, so the high one is stepped only when the low one came back round
|
||||
; to nought - which is what a carry is, done by hand.
|
||||
stepBall:
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
BNA stepCheck
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
|
||||
; Round to the left edge at the far side. Both halves have to match, and the high one is
|
||||
; tested first because it is the one that is usually wrong.
|
||||
stepCheck:
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
SETD.1 WidthHigh
|
||||
LDB.1
|
||||
CCF
|
||||
SUB
|
||||
BNQ stepDone
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
SETD.1 WidthLow
|
||||
LDB.1
|
||||
CCF
|
||||
SUB
|
||||
BNQ stepDone
|
||||
RSTA
|
||||
STA.0 ; DP0 is still BallX, from the comparison just above.
|
||||
SETD.0 BallXHigh
|
||||
STA.0
|
||||
stepDone:
|
||||
RET
|
||||
|
||||
; Two bytes out of one port, which is the whole cost of moving a sprite.
|
||||
moveSprite:
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
INIA 0x02
|
||||
OUTA 0xE5 ; X is bytes two and three of the entry.
|
||||
SETD.0 BallX
|
||||
LDA.0
|
||||
OUTA 0xE9
|
||||
SETD.0 BallXHigh
|
||||
LDA.0
|
||||
OUTA 0xE9
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x3000
|
||||
|
||||
Message:
|
||||
"A ball, over the shell's own words. Nothing underneath is written to. Press a key."
|
||||
|
||||
; Index nought is not a colour, it is a hole - so the corners of the tile are what the ball
|
||||
; is not, and whatever is behind shows through them.
|
||||
BallArt:
|
||||
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
|
||||
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
|
||||
0x00 0x01 0x01 0x01 0x01 0x01 0x01 0x00
|
||||
0x00 0x00 0x01 0x01 0x01 0x01 0x00 0x00
|
||||
|
||||
BallArtAt:
|
||||
#Reserve 0d2
|
||||
|
||||
BallX:
|
||||
0x00
|
||||
BallXHigh:
|
||||
0x00
|
||||
WidthLow:
|
||||
0x00
|
||||
WidthHigh:
|
||||
0x00
|
||||
@@ -725,6 +725,7 @@ from every assembly file in it. Several are old programs written for the bare ma
|
||||
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
||||
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
||||
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. |
|
||||
| Sprite | Moves a ball across the shell's own text, writing not one byte of the map to do it. It leaves the sprite in the table on the way out, because clearing them is the system's job - see below. |
|
||||
| Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It writes nothing else at all - not a tile, not a colour - so it does not ask for the screen to be saved, and the line it printed is still there when it comes back. It deliberately does not put the displayed screen back either, because that is the system's to restore: a program that faulted while flipped could not have. |
|
||||
| Edit | A line editor. |
|
||||
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
||||
@@ -1204,6 +1205,14 @@ before quietly answers to nothing.
|
||||
| 5 | The screen's map, for the same. |
|
||||
| 6 and up | Free for a program to use. |
|
||||
|
||||
**The sprite table is cleared at exit, not saved and restored.** It lives in the atlas at
|
||||
0xC000, and the pages the screen save walks run to the end of the map and pick up again at the
|
||||
palette, with the sprites in the gap between. That is deliberate: nothing the shell draws is a
|
||||
sprite, so there is nothing to give back. What is needed is to take away, or a program that
|
||||
put something on the screen and left would have left it sitting over the prompt, in front of
|
||||
everything, with nothing able to type it away. A program that faulted could not have cleared
|
||||
its own, which is why the system does it for every program rather than trusting each one.
|
||||
|
||||
4 and 5 are only registered while a screen is being saved or given back, so a program is free
|
||||
to point them somewhere else in between - but a program that takes the screen will find them
|
||||
pointing at the screen again afterwards, so there is nothing to be gained by it. `Grid` uses
|
||||
|
||||
@@ -5150,6 +5150,32 @@ screenSane:
|
||||
; the floor for the programs that saved nothing.
|
||||
INIA 0x03
|
||||
OUTA 0x39
|
||||
|
||||
; ---- And no sprites left standing over the shell ----
|
||||
;
|
||||
; The table is in the atlas at 0xC000, and the screen save does not cover it: the pages it
|
||||
; walks run to the end of the map and pick up again at the palette, with the sprites in the
|
||||
; gap between. That is deliberate rather than an oversight. Nothing the shell draws is a
|
||||
; sprite, so there is nothing to GIVE BACK - what is needed is to take away, or a program
|
||||
; that put something on the screen and left would have left it sitting over the prompt,
|
||||
; still there, still in front of everything, and nothing able to type it away.
|
||||
;
|
||||
; A size of nought is no sprite, so filling the table with nought is all of it, and it is
|
||||
; one command rather than 256 of them.
|
||||
CALL screenBank
|
||||
INIA 0d4
|
||||
OUTA 0xE3
|
||||
INIA 0xC0
|
||||
OUTA 0xE4
|
||||
RSTA
|
||||
OUTA 0xE5
|
||||
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
|
||||
INIA 0x08
|
||||
OUTA 0xE6
|
||||
RSTA
|
||||
OUTA 0xE7 ; 0x0800, which is 256 entries of eight bytes.
|
||||
INIA 0x02
|
||||
OUTA 0xE8
|
||||
RET
|
||||
|
||||
; Video memory as banks 4 and 5, which is what makes it reachable at all. Bank 3 is the
|
||||
|
||||
Reference in New Issue
Block a user