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