| Please spread the word if you like this content! |
|
|
|
|
| |
I think it's safe to say that you, I, and everyone else on the planet is now sick of talking about cycle counting. We've already looked a little into coloring the background by setting the COLUBK address before a scanline is drawn. But what about those pesky sprites?
Right off the bat, let's get this out: there are no drawing routines. Zip. Nada. None. Let me qualify this. We now live in the blissful days of GDI, DirectX, OpenGL, even Flash for f--k's (folk's?) sake! We take for granted our DrawLine and FillEllipse methods, optionally hardware-driven matrix transforms, and 232 colors with transparency. The Atari VCS laughs at us and calls us 'p--sies' (as in cats or willows) directly to our faces. And it kind of has a leg to stand on...
We have seven "objects" we can work with: two playfield objects, two player objects, two missle objects, and a ball object. Drawing these objects is always in essence the same two or three tasks:
- per scanline, is (a line of) the object residing on this scanline?
- if so, enable the bit(s) for that object that will get color for that scanline
- optionally, set the color for the object, so that that color will be used for that scanline
Therefore, we still need to know what scanline we're on to know to draw a sprite onto it. With the possibility of a frame with seven objects utilizing a particular scanline, and a scanline only having 76 machine cycles to possibly do those three tasks for all seven objects, do we begin to see the pure art of assembly sculpting required with cycle counting to fit everything in? Oh...you will.
Now some of these objects are “easier” to render than others. The missles and ball objects, for instance, are really just enabling a bit on a particular scanline. Done. Player objects get more complex. Let’s look at a smiley-face player object:
So, I’d be thrown out of the programmer’s union if I didn’t show a grid with bit values. Here’s our smiley face. All player objects are 8 bits (a byte!) wide. The right side shows the byte value for each row of smiley’s face. While you are setting up your current scanline within the 192 (for NTSC) display scanlines, you would need to check and see if smiley is on that scanline, and if so, set the byte for the respective row in smiley’s face you’re on. For example, let’s say smiley is player 0 and his top row starts on the 8th scanline in our display. Then, when we get to the 8th scanline, we would set the player 0 graphics address (GRP0) to 60. On the next scanline, we would set GRP0 to 126, then 219 on the next, and so on until the 16th scanline, where smiley is no longer being drawn, where we would need to finish with setting GRP0 to 0. The TIA is simple and just draws on each scanline for player 0 whatever bits are on in GRP0, for whatever color is stored at COLUP0. This automatically shows one constraint:
is impossible to draw (unless you use player 1 for the eyes, I guess), since you can only set the color for the enabled bits, and not per-bit.
This is fine, although, as the picture notes, we’ll have the extra expense of setting the color for the player for each scanline.
We can also change smiley’s size by setting the NUSIZ0 address:
Single, double, and quad-width rastering. Basically NUSIZ0 tells the TIA chip how many clocks to "use up” per “pixel”. The Stella guide (pg.40) is not extremely clear on this, as it leaves it ambiguous whether or not bits D4 and D5 are just for missles or also somehow affect players. We can test this with the following code:
processor 6502
include "vcs.h"
include "macro.h"
seg org $F000
VAR_TEMP = $80
VAR_PLYR0_Y = $81
VAR_PLYR0_HEIGHT = $82
VAR_NUSIZ0 = $83
VAR_NUSIZ0_CNTR = $84
Initialize
; === set background color
LDA #0
STA COLUBK
STA VAR_NUSIZ0
STA VAR_NUSIZ0_CNTR
; === set player Y position
LDA #107
STA VAR_PLYR0_Y
LDA #8
STA VAR_PLYR0_HEIGHT
; === set player "ball" color
LDA #$1F
STA COLUP0
; === set player ball location
; ----------------------------------------------------------------------------------- MAIN LOOP ( "kernel" )
Kernel
; ---------------------------- perform VSYNC
LDA #2
STA VSYNC
STA WSYNC
STA WSYNC
; ---------------------------- clear our player graphics here
LDA #0
STA GRP0
STA WSYNC
STA VSYNC
; ---------------------------- perform VBLANK
LDA #255
AND VBLANK
ORA #2
STA VBLANK
; flip through bits D0, D1, D2, D4, and D5 for NUSIZ0
INC VAR_NUSIZ0_CNTR
LDA #60
CMP VAR_NUSIZ0_CNTR
BNE __nxt3
LDA #0
STA VAR_NUSIZ0_CNTR
INC VAR_NUSIZ0
LDA #8
CMP VAR_NUSIZ0
BNE __nxt
LDA #16
STA VAR_NUSIZ0
JMP __nxt3
__nxt
LDA #24
CMP VAR_NUSIZ0
BNE __nxt2
LDA #32
STA VAR_NUSIZ0
JMP __nxt3
__nxt2
LDA #40
CMP VAR_NUSIZ0
BNE __nxt3
LDA #0
STA VAR_NUSIZ0
__nxt3
LDX VAR_NUSIZ0
STX NUSIZ0
STA WSYNC
repeat 35
STA WSYNC
repend LDX #0
LDY #0
LDA #253
AND VBLANK
STA WSYNC
STA VBLANK
; ---------------------------- 192 picture lines
PICTURE_LINES
CPX #192
BEQ OVERSCAN
DRAW_PLYR_0
CPX VAR_PLYR0_Y
BNE DRAW_PLYR_0_N
DRAW_PLYR_0_Y
LDY #8
JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_N
NOP
JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_DO
CPY #0
BEQ DRAW_PLYR_0_DO_N
DRAW_PLYR_0_DO_Y
DEY
LDA GFXOBJ_Ball,Y
JMP DRAW_PLYR_0_FINAL
DRAW_PLYR_0_DO_N
LDA #0
DRAW_PLYR_0_FINAL
STA GRP0
STA WSYNC
INX
JMP PICTURE_LINES
OVERSCAN
; ---------------------------- 30 overscan lines
repeat 30
STA WSYNC
repend JMP Kernel
; ----------------------------------------------------------------------------------- MAIN LOOP END
GFXOBJ_Ball
dc.b #60, #126, #195, #189, #255, #219, #126, #60
org $FFFA
.word Initialize ; NMI
.word Initialize ; RESET
.word Initialize ; IRQ
end
Here’s some adjustments if you want Rainbow Smiley:
processor 6502
include "vcs.h"
include "macro.h"
seg org $F000
VAR_TEMP = $80
VAR_PLYR0_Y = $81
VAR_PLYR0_HEIGHT = $82
VAR_NUSIZ0 = $83
VAR_NUSIZ0_CNTR = $84
Initialize
; === set background color
LDA #0
STA COLUBK
STA VAR_NUSIZ0
STA VAR_NUSIZ0_CNTR
; === set player Y position
LDA #107
STA VAR_PLYR0_Y
LDA #8
STA VAR_PLYR0_HEIGHT
; === set player "ball" color
LDA #$1F
STA COLUP0
; === set player ball location
; ----------------------------------------------------------------------------------- MAIN LOOP ( "kernel" )
Kernel
; ---------------------------- perform VSYNC
LDA #2
STA VSYNC
STA WSYNC
STA WSYNC
; ---------------------------- clear our player graphics here
LDA #0
STA GRP0
STA WSYNC
STA VSYNC
; ---------------------------- perform VBLANK
LDA #255
AND VBLANK
ORA #2
STA VBLANK
; flip through bits D0, D1, D2, D4, and D5 for NUSIZ0
INC VAR_NUSIZ0_CNTR
LDA #60
CMP VAR_NUSIZ0_CNTR
BNE __nxt3
LDA #0
STA VAR_NUSIZ0_CNTR
INC VAR_NUSIZ0
LDA #8
CMP VAR_NUSIZ0
BNE __nxt
LDA #16
STA VAR_NUSIZ0
JMP __nxt3
__nxt
LDA #24
CMP VAR_NUSIZ0
BNE __nxt2
LDA #32
STA VAR_NUSIZ0
JMP __nxt3
__nxt2
LDA #40
CMP VAR_NUSIZ0
BNE __nxt3
LDA #0
STA VAR_NUSIZ0
__nxt3
LDX VAR_NUSIZ0
STX NUSIZ0
STA WSYNC
repeat 35
STA WSYNC
repend LDX #0
LDY #0
LDA #253
AND VBLANK
STA WSYNC
STA VBLANK
; ---------------------------- 192 picture lines
PICTURE_LINES
CPX #192
BEQ OVERSCAN
DRAW_PLYR_0
CPX VAR_PLYR0_Y
BNE DRAW_PLYR_0_N
DRAW_PLYR_0_Y
LDY #8
JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_N
NOP
JMP DRAW_PLYR_0_DO
DRAW_PLYR_0_DO
CPY #0
BEQ DRAW_PLYR_0_DO_N
DRAW_PLYR_0_DO_Y
DEY
LDA COLOR_Ball,Y
STA COLUP0
LDA GFXOBJ_Ball,Y
JMP DRAW_PLYR_0_FINAL
DRAW_PLYR_0_DO_N
LDA #0
DRAW_PLYR_0_FINAL
STA GRP0
STA WSYNC
INX
JMP PICTURE_LINES
OVERSCAN
; ---------------------------- 30 overscan lines
repeat 30
STA WSYNC
repend JMP Kernel
; ----------------------------------------------------------------------------------- MAIN LOOP END
GFXOBJ_Ball
dc.b #60, #126, #195, #189, #255, #219, #126, #60
COLOR_Ball
dc.b #159, #130, #119, #92, #74, #68, #253, #31
org $FFFA
.word Initialize ; NMI
.word Initialize ; RESET
.word Initialize ; IRQ
end
Either way, we see that bits D4 and D5 do not affect player objects, just D0, D1, and D2. Also, NUSIZ0 doesn’t just affect player width; it can give us clones, too, albeit just single-width. Activision’s Laser Blast quickly comes to mind (I actually defeated that game! That and Yars Revenge, I think…). See the grey vehicle-things at the bottom? (picture shamelessly purloined from AtariAge)

On another cycle-counting side note, I’d like to note that the code that cycles through D0, D1, D2, D4, and D5 was written on-the-fly in like 5 minutes. This is not to try and sound all boastful n’ shite. Rather, I’m going to bring up how damn helpful Kowalski’s 6502 simulator is. I threw the code into the simulator:
and debugging it with values that would pass through the most costly “path” of instructions, I saw that only 56 cycles were taken up:
This was incredibly useful, because then I knew I didn’t need to trim it down or anything; I just needed to follow it with a WSYNC call to easily stay in sync. If it had been over 76 cycles, I would have had to break it in two to make sure one part operated within one scanline, and the second in the following. Below is the code. If you’re using the Kowalski simulator, try it out in there with different values for VAR_NUSIZ0 and VAR_NUSIZ0_CNTR.
*= $1000
NUSIZ0 = $80
VAR_NUSIZ0 = $81
VAR_NUSIZ0_CNTR = $82
LDA #39
STA VAR_NUSIZ0
LDA #59
STA VAR_NUSIZ0_CNTR
; reset cycle counter here
INC VAR_NUSIZ0_CNTR
LDA #60
CMP VAR_NUSIZ0_CNTR
BNE __skip
LDA #0
STA VAR_NUSIZ0_CNTR
INC VAR_NUSIZ0
LDA #8
CMP VAR_NUSIZ0
BNE __nxt
LDA #16
STA VAR_NUSIZ0
JMP __skip
__nxt
LDA #24
CMP VAR_NUSIZ0
BNE __nxt2
LDA #32
STA VAR_NUSIZ0
JMP __skip
__nxt2
LDA #40
CMP VAR_NUSIZ0
BNE __skip
LDA #0
STA VAR_NUSIZ0
__skip
LDX VAR_NUSIZ0
STX NUSIZ0
BRK ; 19 cycles if 0, 56 cycles max
Next post we’ll try adding some missles and see how collisions work. Until then, take the player code above and try messing around with it. The player can be 1 to 192 scanlines high, but will always be 8 bits wide. What needs to change to accomodate different height players? Also, the vertical positioning code is more than grossly inefficient. How might you make it better?
atari,
2600,
vcs,
stella,
grp0,
nusiz0,
colup0,
colubk,
activision,
laser%20blast,
yars%20revenge,
atariage,
kowalski,
6502,
6507,
simulator,
wsync,
sprite
posted @ Sunday, August 29, 2010 4:56 PM