2600 Cookbook:
Subpixel Positioning
Problem:
You'd like to move players or other objects in smaller increments than pixels.
Solution:
Use "subpixel positioning", using 2 bytes to store the object's
position. One byte will represent the fractional position, the other
will be the integer positioning, used for the display logic.
Discussion:
It can be hard to get subtle animation when only moving
things in pixel increments (in particular, if a object's
speed is only N pixels per turn it may be hard to get nice
smooth motion.)
By using 2 bytes, you can can keep track of the object's
position with both bytes, then just use one for deciding when
or where to draw the object.
Traditionally 16-bit values are stored low byte first.
This might seem to put the bits "out of order" if you think
of it as a single bit string,
but it makes sense if you think about how memory addresses
are stored. It's a little annoying because whenever you're
referring to the integer bit alone, it looks like
MemoryLabel+1, but still, it's the Way Things Are Done and
you probably shouldn't go against it unless you have a
very good reason.
(Obviously, you don't have to use 1 byte for the
fraction and 1 byte for the integer, you could put the
"binary decimal point" wherever you want, but this method
generally works well with Atari coding.)
So, whenever you adjust the object's
position, you need to do 16-bit math.
If you defined 2 bytes for the object speed
and the object position, you might get code
like this: (I'm just including this for people
unfamiliar with 16-bit math.)
clc
lda YPosFromBot
adc YSpeed
sta YPosFromBot
lda YPosFromBot+1
adc YSpeed+1
sta YPosFromBot+1
(Remember, clear the carry before addition,
set it before subtraction, and then the flags
will take care of themselves and bob's your uncle.)
(Also, the 2's complement notation can be a little
difficult to work with, especially in doing comparisions.)
As a sidenote, you can still define your constants
(for example, player speed) as decimals, and then let
DASM break 'em up into the two bytes. For instance,
in today's example, we defined "C_GHOST_SPEED = 300",
then later referred to #<C_GHOST_SPEED as the
low byte and adc #>C_GHOST_SPEED as the high byte.
This value divided by 256 is the integer value, and
the result when mod 256 is the fractional part.
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