2600 Cookbook:
Graphical Pointers and Indirect Indexing
Problem:
You want to have different frames of animation for a player.
Solution:
Set up a 2 byte pointer to the correct graphic during the VBLANK,
then use the Indirect Indexing mode to get to the correct graphic scanline in the kernal.
Discussion:
This is one of the most basic Atari recipes.
2600 101 walks through an example
that displays a smileyface graphic. In that case, it was very simple to just set
X to which line of the thing we were drawing, and then use the ROM label directly.
(The memory was "Absolute Indexed, X", in other words.)
lda BigHeadGraphic,X
sta GRP0
The trouble is we can't easily point to a different frame of animation if we
use the ROM label directly, we'd have to put conditional code in our kernal,
and there's hardly ever going to be time for stuff like that. So the solution
is to some memory as a pointer to the correct graphic location that will be
set up during the VBLANK and the dereferenced on the fly.
You need to set aside 2 bytes for memory
for each thing you're animating:
P0_Ptr ds 2 ;ptr to current graphic
As always, this location is stored low byte first: P0_Ptr+1 will be set to
the page where the graphics are stored, P0_Ptr is then an offset in that page.
Most times it makes sense to put all your player data in the same page of
memory. That way, you only need to set the high byte of your graphical pointers
once at the begining of your program:
lda #>GhostGraphic ;high byte of graphic location
sta P0_Ptr+1 ;store in high byte of graphic pointer
In this case, "#>" is a useful notation that means "grab the high byte"
of this two-byte memory location.
Later in the kernal we can then load the low byte with the graphic to be used this frame:
lda #<GhostBooGraphic ;low byte of ptr is boo graphic
sta P0_Ptr ;(high byte already set)
Finally, in the Kernal itself, we dereference the pointer at the appropriate time
and store the value it is pointing to in the graphical register GRP0. (Or GRP1 for
the other player, obviously.) This kind of memory addressing is called Indirect Indexing
and you can only do it with Y
lda (P0_Ptr),Y
sta GRP0
Those last code snippets assume Y is set to the current line of he graphic that is
being drawn. Now, in "real code", there probably won't be time to set up Y like that...
in fact, many kernals use Y as a "scanline countdown" variable.
"milquetoast the ghost", the example used here, does that, but the explanation
for what skipdraw is doing gets a little complicated, so we'll save the explanation
for skipdraw. This lesson is mostly to get you familiar with the memory manipulation
in general.
References:
Example:
;milquetoast the ghost
;by Kirk Israel
;a cute ghost. press button to say boo!
processor 6502
include vcs.h
include macro.h
;label some variables....
SEG.U VARS
ORG $80
;the ghosts height will be in 2 bytes;
;low, high (fractional byte, than integer byte)
;drawing onscreen, we only care about the integer byte
;but we will keep track of both
P0_YPosFromBot ds 2
P0_XPos ds 1 ;horizontal position
P0_Y ds 1 ;needed for skipdraw
P0_Ptr ds 2 ;ptr to current graphic
SEG CODE
org $F000
;a few constants...
C_GHOST_SPEED = 300 ;subpixel, divide by 256 for pixel speed
C_P0_HEIGHT = 8 ;height of ghost sprite
C_KERNAL_HEIGHT = 192 ;height of kernal (full screen, single line kernal)
Start
CLEAN_START
;black background, white ghost...
lda #$00
sta COLUBK
lda #$0F
sta COLUP0
lda #12
sta P0_YPosFromBot+1 ;Initial Y Position in integer part of 2 byte speed
lda #90
sta P0_XPos
lda #>GhostGraphic ;high byte of graphic location
sta P0_Ptr+1 ;store in high byte of graphic pointer
MainLoop
VERTICAL_SYNC
lda #43
sta TIM64T
;joystick pressed left?
lda #%01000000
bit SWCHA
bne DoneMoveLeft
dec P0_XPos ;move ghost left
lda #%00001000 ;a 1 in D3 of REFP0 says make it mirror
sta REFP0
DoneMoveLeft
;joystick pressed right?
lda #%10000000
bit SWCHA
bne DoneMoveRight
inc P0_XPos ;move ghost right
lda #%00000000
sta REFP0 ;unmirrored P0
DoneMoveRight
; for up and down, we INC or DEC
; the Y Position
;joystick down?
lda #%00010000
bit SWCHA
bne DoneMoveDown
;16 bit math, add both bytes
;of the ghost speed constant to
;the 2 bytes of the position
clc
lda P0_YPosFromBot
adc #<C_GHOST_SPEED
sta P0_YPosFromBot
lda P0_YPosFromBot+1
adc #>C_GHOST_SPEED
sta P0_YPosFromBot+1
DoneMoveDown
lda #%00100000 ;Up?
bit SWCHA
bne DoneMoveUp
;16 bit math, subtract both bytes
;of the ghost speed constant to
;the 2 bytes of the position
sec
lda P0_YPosFromBot
sbc #<C_GHOST_SPEED
sta P0_YPosFromBot
lda P0_YPosFromBot+1
sbc #>C_GHOST_SPEED
sta P0_YPosFromBot+1
DoneMoveUp
;check firebutton
ldx INPT4
bmi MouthIsClosed ;(button not pressed)
MouthIsOpen
lda #<GhostBooGraphic ;low byte of ptr is boo graphic
sta P0_Ptr ;(high byte already set)
jmp DoneWithMouth
MouthIsClosed
lda #<GhostGraphic ;low byte of ptr is normal graphic
sta P0_Ptr ;(high byte already set)
DoneWithMouth
;for Battlezone style exact horizontal repositioning
;subroutine as rediscovered by R. Mundschau and explained by Andrew Davie,
;set A = desired horizontal position, then X to object
;to be positioned (0->4 = P0->BALL)
lda P0_XPos
ldx #0
jsr bzoneRepos
;for skipDraw, P0_Y needs to be set (usually during VBLANK)
;to Vertical Position (0 = top) + height of sprite - 1.
;we're storing distance from bottom, not top, so we have
;to start with the kernal height and YPosFromBot...
lda #C_KERNAL_HEIGHT + #C_P0_HEIGHT - #1
sec
sbc P0_YPosFromBot+1 ;subtract integery byte of distance from bottom
sta P0_Y
;we also need to adjust the graphic pointer for skipDraw
;it equals what it WOULD be at 'normally' - it's position
;from bottom plus sprite height - 1.
;(note this requires that the 'normal' starting point for
;the graphics be at least align 256 + kernalheight ,
;or else this subtraction could result in a 'negative'
; (page boundary crossing) value
lda P0_Ptr
sec
sbc P0_YPosFromBot+1 ;integer part of distance from bottom
clc
adc #C_P0_HEIGHT-#1
sta P0_Ptr ;2 byte
WaitForVblankEnd
lda INTIM
bne WaitForVblankEnd
ldy #C_KERNAL_HEIGHT - 1; (off by one error)
sta WSYNC
sta HMOVE ;move objecs that were finely positioned
sta VBLANK
;main scanline loop...
ScanLoop
;skipDraw
; draw player sprite 0:
lda #C_P0_HEIGHT-1 ; 2
dcp P0_Y ; 5 (DEC and CMP)
bcs .doDraw0 ; 2/3
lda #0 ; 2
.byte $2c ;-1 (BIT ABS to skip next 2 bytes)
.doDraw0:
lda (P0_Ptr),y ; 5
sta GRP0 ; 3 = 18 cycles (constant, if drawing or not!)
sta WSYNC
dey
bne ScanLoop
lda #2
sta WSYNC
sta VBLANK
ldx #30
OverScanWait
sta WSYNC
dex
bne OverScanWait
jmp MainLoop
;Battlezone style exact horizontal repositioning
;subroutine as rediscovered by R. Mundschau and explained by Andrew Davie,
;set A = desired horizontal position, then X to object
;to be positioned (0->4 = P0->BALL)
bzoneRepos
sta WSYNC ; 00 Sync to start of scanline.
sec ; 02 Set the carry flag so no borrow will be applied during the division.
.divideby15
sbc #15 ; 04 Waste the necessary amount of time dividing X-pos by 15!
bcs .divideby15 ; 06/07 11/16/21/26/31/36/41/46/51/56/61/66
tay
lda fineAdjustTable,y ; 13 -> Consume 5 cycles by guaranteeing we cross a page boundary
sta HMP0,x
sta RESP0,x ; 21/ 26/31/36/41/46/51/56/61/66/71 - Set the rough position.
rts
;-----------------------------
; This table converts the "remainder" of the division by 15 (-1 to -15) to the correct
; fine adjustment value. This table is on a page boundary to guarantee the processor
; will cross a page boundary and waste a cycle in order to be at the precise position
; for a RESP0,x write
ORG $FE00
fineAdjustBegin
DC.B %01110000 ; Left 7
DC.B %01100000 ; Left 6
DC.B %01010000 ; Left 5
DC.B %01000000 ; Left 4
DC.B %00110000 ; Left 3
DC.B %00100000 ; Left 2
DC.B %00010000 ; Left 1
DC.B %00000000 ; No movement.
DC.B %11110000 ; Right 1
DC.B %11100000 ; Right 2
DC.B %11010000 ; Right 3
DC.B %11000000 ; Right 4
DC.B %10110000 ; Right 5
DC.B %10100000 ; Right 6
DC.B %10010000 ; Right 7
fineAdjustTable EQU fineAdjustBegin - %11110001 ; NOTE: %11110001 = -15
org $FEC0
GhostGraphic
.byte #%11110000
.byte #%01111100
.byte #%00111111
.byte #%11111101
.byte #%10110110
.byte #%00111110
.byte #%00101010
.byte #%00011100
GhostBooGraphic
.byte #%11110000
.byte #%01111100
.byte #%00111110
.byte #%01100101
.byte #%10100011
.byte #%10111110
.byte #%00101010
.byte #%00011100
org $FFFC
.word Start
.word Start
Back to 2600 Cookbook