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.
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.)
The color registers of the TIA are:
- COLUP0 … player 0, missile 0
- COLUP1 … player 1, missile 1
- COLUPF … playfield, ball
- COLUBK … background
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:
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:
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.
April 2018, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2018/04. —