Retrochallenge 2018/04 (Now in COLOR)
Refraction for the Atari 2600

Episode 2: Sketching the Game / First Playfield

As soon as I found out, I wouldn't pursue the alien abduction theme, I was thinking about doing something completely different: Not another of the usual video games, but going back to the roots, when video games were about friends and family gathering in front of a TV set in cheerful exaltation. So it would be about two players facing directly in a noble, abstract competition, something about skill but also a bit of the unexpected for excitement. Interestingly enough, the game I came up with is entirely in the comfort zone of the Atari VCS (2600)! We won't need much of the various tricks, we discussed in the previous episode, maybe a variation of the 48-pixel sprite trick for some nicer scores. And, hopefully, we may go for a single-line kernel in best resolution, flicker-free.

Note/Update: In hindsight, the notion of the game being totally in the comfort zone and us not having to refer to any tricks may have been overly optimistic.

Sketching the Game

The game may be described as a mixture of Pong and Tank with a twist (literally): An enclosed rectangualr playfield with visible borders, two player sprites (spaceships, since it's an Atari game), each fixed to a vertical axis near the left and right border, respectively. The playfield is divided by two vertical, glittering barriers into three compartments, two home fields at either side and a neutral zone in the middle. Like in most two players games, players may fire missiles at each other, but this is also, where any similarities end: As a missile crosses the barrier of the opponent's home field, the missile is deflected at a refraction angle proportional to the distance from the vertical center. By moving the joystick left or right (it's not a paddle game) players may either invert the refraction angle or increase it. Once a missile has entered the oppsite home field, it is never to leve it again, bouncing from the border walls and the inner side of the barrier. There's only one missile per player (firing a new missile will terminate any previous one) and missiles will eventually expire after a period of time.

As a bonus, a Pong-like ball, commonly known as a bouncer, will invariably and indefinitely bounce across the entire playfield, reflected by the outer walls. Any contact of a ship with an opponents missile or the bouncer will result in the destruction of the ship. Scores will be displayed at the top of the playfield, limited to 2 decimal digits. Possible variations of the game may allow for increased speeds of missile and bouncer, or tighter home fields.

Sketch of an Atari 2600 game (Refraction)

A sketch of our game.

For whatever reasons, I do see it in purple, the ships probably in a lighter color, maybe in shades of light blue. Since we may have some of the memory of a 4K cartridge to spare, we may even add a fine start screen, where we may show off some of the tricks. However, we'll start with the main course, particularly the playfield.

A First Playfield

Let's dive right into it: We'll start with a nice definition block, providing essential dimesional data for both NTSC and PAL. While we'll concentrate on NTSC during development, it may be nice to have the data required, to compile a PAL version by the flip of a switch (i.e., uncommenting a line.) DASM provides the usual syntactic constructs for checkking for the existance of a variable, as in "ifnconst ... else ... endif". As may be observed, we're going to handle VBLANK as well as the overscan by timers.

; tv standard specifics
; uncomment for PAL
;PAL = 1

     ifnconst  PAL
;----------------------------- NTSC

; 262 lines: 3+37 VBlank, 192 kernel, 30 overscan
; timers (@ 64 cycles)
;  VBlank    43 * 64 = 2752 cycles = 36.21 lines
;  Overscan  35 * 64 = 2240 cycles = 29.47 lines

ScanLines      = 192
T64VBlank      =  43
T64Overscan    =  35

;-----------------------------
     else
;----------------------------- PAL

; 312 lines: 3+45 VBlank, 228 kernel, 36 overscan
; timers (@ 64 cycles)
;  VBlank    53 * 64 = 3392 cycles = 44.63 lines
;  Overscan  42 * 64 = 2688 cycles = 35.36 lines

ScanLines      = 228
T64VBlank      =  53
T64Overscan    =  42

;-----------------------------                      
    endif

Next, we're going to have a nice initialization routine. Mind the I/O configuration at the end — the VCS supports both input and output via it's ports!

    org $F000

Start
    sei             ; disable interrupts
    cld             ; clear BCD mode
    
    ldx #$FF
    txs             ; reset stack pointer

    lda #$00
    ldx #$28        ; clear TIA registers ($04-$2C)
TIAClear
    sta $04,X
    dex
    bpl TIAClear    ; loop exits with X=$FF
    
;   ldx #$FF
RAMClear
    sta $00,X       ; clear RAM ($FF-$80)
    dex
    bmi RAMClear    ; loop exits with X=$7F

    sta SWBCNT      ; set console I/O to INPUT
    sta SWACNT      ; set controller I/O to INPUT

So, we're nearly ready to go, But before we do so, we'll have to talk about colors and the playfield. Colors (128 for NTSC) are defined as 16 base colors, to be set in the high nible (D7-D4) of any color register, and 8 shades of luminance, to be defined in the highest 3 bits (D4-D1) of the low nible (in increasing luminosity). In PAL, there are fewer colors and they do not align that nice and logically, but we'll be doing fine. SECAM however is another story (just 8 colors!) and is commonly ignored. (We'll do the same here.)

Atari 2600 TIA colors

TIA color palettes. (Source: randomterrain.com: Interactive TIA Color Charts and Tools.)

As we may see, there is actually a single row of neutral/grey and 13 rows, over which the hue is rotated by 360° over the spectrum, and 2 rows of redundant colors, where the first color rows are repeated. Therefore, for NTSC, there are in actuality 8 shades of grey, 104 unique colors and 16 redundant ones. The PAL palette is made of interleaved jumps across the color wheel and provides 8 greys and 12 × 8 = 96 unique colors.

Update: Or so I thought, based on the above palettes.
However, according to other sources there are, in deed, 128 unique NTSC colors:

Atari 2600 TIA colors (wikipedia)

TIA color palettes, alternate representation. (Source: en.wikipedia.org.)
Thanks to Jeremiah Knol, who confirmed these palettes after some extensive color testing, for pointing this out!

The color registers of the TIA are:

Playfield graphics are a bit different, to say the least. First, these are “wide pixels,” stretching over 4 normal pixels (color clocks). Second, while there are 40 wide pixels in total (stretching over 160 normal pixels), only 20 are to be defined, the second half is either repeated or mirrored. (Set D0 in CTLPF to 1 for a mirrored or symmetrical playfield, 0 for a repeating playfield.) Third, they are arranged in the registers in an unusual way:

Atari 2600 playfield graphics and TIA registers

Atari VCS playfield graphics and TIA playfield registers.

For our game, we'll use a border-height of 6 lines (to be defined as a constant for easy changes), a border-width of a single, wide playfield pixel. The “barriers” will be another playfield pixel, D0 of PF1. For the score, we reserve a horizontal band of 8 lines height, and we're going to fill it with horizontal stripes of background color for the time being.

To make things a bit more interesting, we're going to render the “barriers” in alternating lines and are going to animate them. For this, we're reserving two bytes of memory, pfMask (a playfield mask to termine, if we're going to render a stripe or not on a particular line) and frCntr (a wrap-around frame counter). At the end of each frame, we'll advance the frame counter and every fourth frame, we'll invert the playfield mask (either 0 or 1). Depending on this mask, we'll set PF1 either to 0 or to 1 on alternating lines.

Finally, to make things even a bit more interesting, we're going to change the playfield color for the barriers. This involves changing the color after PF0 has began to be rendered and changing it back to the frame color before it used on the right side again. Colors definitions will go in the top configuration section, since they differ for NTSC and PAL.

Here is, what we eventually get, when running the program in Stella:

A simple playfield for the Atari 2600

Our playfield, click to toggle animation (on/off).

And here is our little program…

(DASM's macro "sleep n" comes handy for handling the playfield color, since it allows us to include an arbitrary number of neutral cycles, where n is an integer greater than 1 (NOP is two cycles). For the time being, we'll do with an educated guess for the number of cycles to sleep. The assembler instructions "repeat n … repend" include the code enclosed n times. Here used for repeated WSYNCs, where we just repeat the previous scan-line to draw the top and bottom borders.)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Program:        A Simple Playfield
; System:         Atari 2600
; Source Format:  DASM
; Author:         N. Landsteiner, 2018
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    processor 6502
    include vcs.h
    include macro.h


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Constants
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; tv standard specifics
; uncomment for PAL
;PAL = 1

    ifnconst  PAL
;----------------------------- NTSC

; 262 lines: 3+37 VBlank, 192 kernel, 30 overscan
; timers (@ 64 cycles)
;  VBlank    43 * 64 = 2752 cycles = 36.21 lines
;  Overscan  35 * 64 = 2240 cycles = 29.47 lines

ScanLines      = 192
T64VBlank      =  43
T64Overscan    =  35

FrameClr       = $64
BarrierClr     = $68
ScoreClr       = $EC

;-----------------------------
    else
;----------------------------- PAL

; 312 lines: 3+45 VBlank, 228 kernel, 36 overscan
; timers (@ 64 cycles)
;  VBlank    53 * 64 = 3392 cycles = 44.63 lines
;  Overscan  42 * 64 = 2688 cycles = 35.36 lines

ScanLines      = 228
T64VBlank      =  53
T64Overscan    =  42

FrameClr       = $C4
BarrierClr     = $C6
ScoreClr       = $2C

;-----------------------------                      
    endif

; general definitions

ScoresHeight   =   8
BorderHeight   =   6

; vars

frCntr    = $80
pfMask    = $81


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Initialization
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    org $F000

Start
    sei             ; disable interrupts
    cld             ; clear BCD mode
    
    ldx #$FF
    txs             ; reset stack pointer

    lda #$00
    ldx #$28        ; clear TIA registers ($04-$2C)
TIAClear
    sta $04,X
    dex
    bpl TIAClear    ; loop exits with X=$FF
    
;   ldx #$FF
RAMClear
    sta $00,X       ; clear RAM ($FF-$80)
    dex
    bmi RAMClear    ; loop exits with X=$7F

    sta SWBCNT      ; set console I/O to INPUT
    sta SWACNT      ; set controller I/O to INPUT


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Game Init
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    lda #1
    sta CTRLPF      ; set up symmetric playfield graphics
    lda #0
    sta pfMask
    sta frCntr


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Start a new Frame / VBLANK
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Frame
    lda #$02
    sta WSYNC        ; wait for horizontal sync
    sta VBLANK       ; turn on VBLANK
    sta VSYNC        ; turn on VSYNC
    sta WSYNC        ; leave VSYNC on for 3 lines
    sta WSYNC
    sta WSYNC
    lda #$00
    sta VSYNC        ; turn VSYNC off

    lda #T64VBlank   ; set timer for VBlank
    sta TIM64T

VBlankWait
    lda     INTIM
    bne VBlankWait   ; wait for timer (INTIM going lo)
    sta WSYNC        ; finish current line
    sta VBLANK       ; turn off VBLANK


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Visible Kernel
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Scores
    ; just a dummy, render alternating lines of bg color
    ldy #[ScoresHeight-1]
    lda #0
    sta COLUBK      ; bg to black as we enter the visible area

ScoresLoop
    sta WSYNC       ; start of scan line
    tya             ; copy line-count in Y to AC
    and #1          ; and get bit D0
    beq s1          ; use black (AC=0) for even lines
    lda #ScoreClr   ; and the scores' color for odd ones
s1  sta COLUBK 
    dey             ; count down on lines to render
    bpl ScoresLoop


TopBorder           ; set bg color to FrameClr for #BorderHeight lines
    sta WSYNC
    lda #FrameClr
    sta COLUBK
    repeat BorderHeight
    sta WSYNC
    repend

Playfield
    lda #$0         ; arrange first scan line
    sta COLUBK      ; bg color to black
    lda #FrameClr
    sta COLUPF      ; playfield color
    lda #16         ; set up playfield border
    sta PF0
    lda pfMask
    sta PF1

                    ; oops, PF0 is already rendering, no time to lose...
    lda #BarrierClr ; switch to barrier color and back again
    sta COLUPF
    sleep 36
    lda #FrameClr
    sta COLUPF

                    ; now set up remaining lines in Y to count down on
    ldy #[ScanLines-1 - ScoresHeight - 2 * BorderHeight]

PfLoop
    sta WSYNC

    tya             ; flickering barrier animation
    and #1
    eor pfMask
    sta PF1

    sleep 10        ; switch playfield color
    lda #BarrierClr
    sta COLUPF
    sleep 36
    lda #FrameClr
    sta COLUPF

    dey
    bne PfLoop


BottomBorder
    sta WSYNC
    lda #FrameClr
    sta COLUBK
    lda #0           ; playfield off
    sta PF0
    sta PF1
    sta PF2
    repeat BorderHeight
    sta WSYNC
    repend

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Overscan
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

OverscanStart
    lda #$02
    sta VBLANK
    sta WSYNC
    lda #T64Overscan  ; set timer for overscan
    sta TIM64T

    inc frCntr        ; increment frame counter
    lda frCntr
    and #3
    bne OverscanWait
    lda pfMask        ; flip playfield mask
    eor #1
    sta pfMask

OverscanWait
    lda INTIM
    bne OverscanWait  ; wait for timer
    jmp Frame


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Interrupt and reset vectors
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    org $FFFA
    .word Start       ; NMI
    .word Start       ; Reset
    .word Start       ; IRQ

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    end
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Note: While there are no interrupts on the VCS / 2600, interrupt vectors are required for the Atari 7800. So it's a good idea to include them anyway.

 

 

Next:  Episode 3: Sprites

Previous:   Episode 1: Preliminary Considerations!

Back to the index.

— This series is part of Retrochallenge 2018/04. —