|
No one likes a dead video game. Inanimate invaders and stationary space ships don't capture your imagination. I can't think of a single game with a display that just sits there and stares back at you. Movement and action are synonymous with fun and excitement. Many games have become famous just because of the motions of meandering monsters. PACMAN ™ and DONKEY KONG ™ have patterns everybody wanted to learn . The alien ships in GALAGA ™ have a variety of interesting flight patterns. CENTIPEDE ™ has some truly unique movement routines. Games like VANGUARD ™ and DEFENDER ™ rely quite heavily on making things move on the screen. Trying to learn the maneuvers contributes to the game's intrigue and to the player's fascination.
In the last installment of Frontier, I discussed Player/ Missile Graphics (PMGs) motion, covering horizontal movement and lightly touching on the difficulties of vertical movement. While working on the problems associated with that subject, I discovered a way to control all vertical movement fast, efficiently and easily from BASIC.
Vertical motion on PMGs is not a difficult concept to understand or implement. To move a Player around the screen from BASIC is quite easy. To move it left or right, only a single POKE is required. To move it up or down, use a simple FOR/NEXT loop. The real problem is that it takes a relatively long time to move a Player vertically. If you want to move a PMG shape up or down, move all the bytes (within PMG RAM) that define the shape. To control how far it moves, count the number of scan lines, and shift the PMG's memory location by the appropriate number of bytes.
Each byte in a PMG shape draws one or two scan lines of graphics depending on the resolution mode. When the shape in Player 0 needs to move from Y coordinate 4 to Y coordinate 18, the shape defining bytes must be moved fourteen bytes in PMG RAM. In addition, if you desire smooth motion, you must display all the Y coordinate positions between both locations as well. Then the shape will appear to travel, rather than jump, from one point to another on the screen.
Traditionally, PMG vertical movement is accomplished in two ways: from BASIC in a FOR/NEXT loop, or from Machine Language with a block move or page rotate routine. Until now, programmers have always opted for the Machine Language method since it was incredibly fast compared to BASIC. But BASIC can do the same thing at almost the same incredible speed. The answer is to fool Atari BASIC into using the text in a string variable for PMG RAM. If you put the data for a PMG into a string, you can make modificatons to the PMG shape using the normal string operations, but which resemble Machine Language capabilities. In reality, a PMG shape is nothing more than a series of consecutive bytes in memory with values ranging from 0 to 255. That description also suits a BASIC string variable. The only difference between the two is that bytes in a string variable are represented on the screen as an ATASCII character. When used as PMG data, the same bytes represent a series of eight bits which correspond to eight graphics pixels on a line of a PMG shape. Even though the bytes are identical, they yield different results depending on what section of the hardware/software interprets them.
The problem with this idea is that PMGs must be at the beginning of a memory page boundary, whereas strings are free floating in memory. If this were not so, you could point PMBASE (see Table 1) to a string variable text address (i.e. POKE PMBASE,ADR(PM$)/256). One way to get around this problem is to DIMension so much memory for a string that some portion of it will have to fall on a page boundary. I don't like this solution, because it wastes precious program memory space. The elegant way around this problem is to change the location where a string variable points for its text data. This method also allows you to point a string anywhere in ROM or RAM, such as screen memory, an I/O buffer, or the ROM character set. But what are variables really, and where are they in memory?
Table 1
IMPORTANT PMG SHADOW REGISTER LOCATIONS
Hex | Dec | Title | Register Description |
---|---|---|---|
0058 | 00088 | SAVMSC | LSB of screen memory pointer. |
0059 | 00089 | MSB of screen memory pointer. | |
006A | 00106 | RAMTOP | Top of RAM pointer. |
0082 | 00130 | VNTP | LSB of variable name table pointer. |
0083 | 00131 | MSB of variable name table pointer. | |
0084 | 00132 | VNTD | LSB of variable name table delimiter. |
0085 | 00133 | MSB of variable name table delimiter. | |
0086 | 00134 | VVTP | LSB of variable value table pointer. |
0087 | 00135 | MSB of variable value table pointer. | |
008C | 00140 | STARP | LSB of string/array data pointer. |
008D | 00141 | MSB of string/array data pointer. | |
022F | 00559 | SDMCTL | Direct Memory Access control. |
02C0 | 00704 | PCOLR0 | Player zero color register. |
IMPORTANT PMG HARDWARE REGISTER LOCATIONS
Hex | Dec | Title | Register Description |
---|---|---|---|
D000 | 53248 | HPOSP0 | Player 0 horizontal position. |
D004 | 53252 | POPF | Player zero to Playfield collision. |
D01C | 53276 | VDELAY | Player/Missile Vertical Delay. |
D01D | 53277 | GRACTL | Player/Missile Graphics control. |
D01E | 53278 | HITCLR | PMG collision register reset strobe. |
D407 | 54279 | PMBASE | Memory page of PMG data. |
Whenever you enter a variable into a line of Atari BASIC code, it is "tokenized." A line of BASIC code in memory looks nothing like the display you get when you type LIST. Tokenization is the process by which long groups of symbols are replaced with a single or smaller group of symbols that mean the same thing. For example: When you type the command PRINT from BASIC, it goes into the input buffer. The Atari looks up PRINT in a table of tokens understood by Atari BASIC. Then it replaces the ATASCII values for the characters in the word PRINT (80,82,73,78, and 84) with a single byte having, in this case, a value of 32. All Atari BASIC commands, statements and operators have a replacement token value.
When BASIC encounters a variable during the tokenizing process, it places the variable name in a table. From then on, a single byte represents that variable in the stored BASIC code. The value of the byte is the variable's number in the table minus one, plus 128. For example: the first variable in a program always becomes the token value of 128. The second is 129, and so on, up to 255. This is why all variable names take only one byte of memory in a BASIC program regardless of the name's length. Another table holds the information on what the variable contains. For a string variable, the table has pointers for the current length of the string, its maximum length (the DIM value), and the address where the string's text is located in memory. This value is an offset from the start of the string and array data block in memory. By changing the current length, DIM length, and the offset, you can point a string to any amount of existing memory - all without taking any string space. Finding the location of the variable in the name and value tables can be complicated, so I wrote the program STRPUT, which stands for STRing PUTter.
Listing 1.
30000 VNTP=PEEK(130)+PEEK(131)*256 30010 VNTD=PEEK(132)+PEEK(133)*256 30020 VVTP=PEEK(134)+PEEK(135)*256 30030 STARP=PEEK(140)+PEEK(141)*256 30040 AZ1=-1 30050 AZ1=AZ1+1:FOR AZ=1 TO LEN(VAR$) 30060 AZ2=PEEK(VNTP):IF AZ2>127 AND AZ<LEN(VAR$) THEN 30080 30070 IF AZ2-128*(AZ2>127)=ASC(VAR$(AZ)) THEN VNTP=VNTP+1:NEXT AZ:GOTO 30110 30080 IF PEEK(VNTP)<128 THEN VNTP=VNTP+1:GOTO 30080 30090 VNTP=VNTP+1:IF VNTP<VNTD THEN 30050 30100 GRAPHICS 0:? VAR$;" ISN'T A LEGAL VARIABLE":END 30110 AZ=VVTP+AZ1*8+2:IF PEEK(AZ-2)<>129 THEN 30090 30120 AZ1=LOC-STARP:GOSUB 30140:AZ1=LEN:GOSUB 30140:AZ1=LEN:GOSUB 30140 30130 RETURN 30140 AZ2=INT(AZ1/256):AZ1=AZ1-AZ2*256:POKE AZ,AZ1:POKE AZ+1,AZ2:AZ=AZ+2:RETURN
The subroutine in Listing 1 can be merged into any program when you need to redirect strings. It adds only six extra sectors to a disk file, or about 700 additional bytes of BASIC program code. To use it, first set up a few variables and then execute a GOSUB to line 30000. Assign variables as follows:
The variables LEN and LOC are scalar variables - simply assign the values listed. However, VAR$ adds a string variable entry into the variable table. Since all strings must be preDIMensioned before use, don't forget to dimension VAR$ at the beginning of your program. Also, the variable to be reassigned, whose name is contained in VAR$, must be a string variable. For best results, put the string to be reassigned at the beginning of your program and use the LIST/ENTER technique from SWAT to reorder the table of variable names. This places the variable name at the beginning of the name table and speeds up the search time for the subroutine.
Lines 30000 through 30030 set up some important memory locations used by Atari BASIC in page zero. Table 1 shows memory locations that bear the same name as the variables in these lines. VNTP is the Variable Name Table Pointer. It points to the memory address for the start of the variable names list. VNTD is the Variable Name Table Delimiter pointer. The Atari can have up to 128 variables in a program. If you use less than 128, this location points to the zero byte following the last variable name. If you use exactly 128 variables, it points to the last character of the last variable name in the table. VVTP is the Variable Value Table Pointer. This points to the variable value entries which are all eight bytes long, regardless of variable type. The last one, STARP, is the STring/ ARray Pointer. It points to the first byte of the string and array data block in memory.
Lines 30040 and 30050 set up the variable name search routine in a FOR/ NEXT loop. Line 30060 gets a character from the name table and checks for inverse. To signify the end of an entry, the table stores the variables with the last character of the name in inverse. Scalar variables have the last character in inverse, array variables end with an inverse left parenthesis, and string names are followed by an inverse dollar sign.
Line 30070 continues the name matching process and if a match is made, jumps to line 30110. Line 30080 bumps the pointer to the next variable name entry (next character after an inverse character). If the pointer is now past the end of the table (VNTP is equal to or greater than VNTD) then line 30090 prints an error message stating that the variable is invalid.
Line 30110 checks to see if the program currently is using the string to be reassigned, and prints an error message if not. Every variable entered since the last time the computer was powered up or a NEW command was executed; is considered active. The Atari "hangs on" to old variable names even though the program currently in memory does not use them. The SAVE command stores the entire variable name table, induding unused variables, along with the program data (which is in the tokenized format). LOADing the file back in restores all the variable names to memory. But when you LIST a program to cassette or disk, it saves the expanded ATASCII characters, exactly as you see on the screen. What the ENTER command does is to cause each line of BASIC code to be "typed in," or reentred, as if by hand. This process creates a new variable table consisting only of variables used in the program, and in the order they occur within the program text.
Lines 30120 through 30140 POKE the new length and memory location of the reassigned string. Note that for ease of use, the subroutine sets the maximum DIM length equal to the current string length in LEN. It adjusts the memory location in LOC by subtracting the starting address of string and array data from the actual memory location. This allows it to store the actual location in the variable value table as an offset, as is expected. This also means that pointing a string to a memory area lower than the start of STARP is a bit tricky. You must add 65536 to the actual memory address so the offset will wrap around to the start of memory. For example, if I wanted a string to point to the last half of page zero, I would set LEN equal to 128, and LOC equal to 65664 (128 plus 65536) instead of 128.
Listing 2 is a short demo that moves the letters 'SS' around the screen with a joystick. Type it in and try it, then look at the listing. Starting with the first line, let's take it apart:
Listing 2.
100 DIM VAR$(3),PM$(256),C$(11):POKE 106,PEEK(106)-8:GRAPHICS 0:LOC=PEEK(106)*256+1024:LEN=256:POKE 752, 1 110 PRINT:VAR$="PM$":POKE 704,30:GOSUB 290:PM$=CHR$(0):PM$(256)=CHR$(0):PM$(2)=PM$(1):C$=PM$ 120 FOR X=3 TO 9:READ AZ:C$(X,X)=CHR$(AZ):NEXT X:POKE 559,62:POKE 53277,3:POKE 54279,PEEK(106):PMV=40:PMH=60 130 ST=STICK(0):IF ST=15 THEN 130 140 IF ST=5 THEN PMH=PMH+1:PMV=PMV+2 150 IF ST=6 THEN PMH=PMH+1:PMV=PMV-2 160 IF ST=7 THEN PMH=PMH+1 170 IF ST=9 THEN PMH=PMH-1:PMV=PMV+2 180 IF ST=10 THEN PMH=PMH-1:PMV=PMV-2 190 IF ST=11 THEN PMH=PMH-1 200 IF ST=13 THEN PMV=PMV+2 210 IF ST=14 THEN PMV=PMV-2 220 IF PMV<1 THEN PMV=256 230 IF PMV>256 THEN PMV=1 240 IF PMH<30 THEN PMH=220 250 IF PMH>220 THEN PMH=30 260 POKE 53248,PMH:PM$(PMV)=C$ 270 GOTO 130 280 DATA 68,170,136,68,34,170,68 290 VNTP=PEEK(130)+PEEK(131)*256 300 VNTD=PEEK(132)+PEEK(133)*256 310 VVTP=PEEK(134)+PEEK(135)*256 320 STARP=PEEK(140)+PEEK(141)*256 330 AZ1=-1 340 AZ1=AZ1+1:FOR AZ=1 TO LEN(VAR$) 350 AZ2=PEEK(VNTP):IF AZ2>127 AND AZ<LEN(VAR$) THEN 370 360 IF AZ2-128*(AZ2>127)=ASC(VAR$(AZ)) THEN VNTP=VNTP+1:NEXT AZ:GOTO 400 370 IF PEEK(VNTP)<128 THEN VNTP=VNTP+1:GOTO 370 380 VNTP=VNTP+1:IF VNTP<VNTD THEN 340 390 GRAPHICS 0:? VAR$;" ISN'T A LEGAL VARIABLE":END 400 AZ=VVTP+AZ1*8+2:IF PEEK(AZ-2)<> 129 THEN 380 410 AZ1=LOC-STARP:GOSUB 430:AZ1=LEN:GOSUB 430:AZ1=LEN:GOSUB 430 420 RETURN 430 AZ2=INT(AZ1/256):AZ1=AZ1-AZ2*256:POKE AZ,AZ1:POKE AZ+1,AZ2:AZ=AZ+2:RETURN
Line 100 DIMensions all of the program strings. PM$ is for PMG RAM manipulations. Eventually you will reassign it to point at the memory occupied by Player 0. C$ holds the PMG shape data in the form of ATASCII characters. By assigning C$ to a portion of PM$, you can place a shape instantly anywhere in the Player 0 RAM. Line 100 also reserves eight pages of memory (2K RAM) for PMGs with single-line-resolution. The program also bumps back the register location known as RAMTOP (see Table 1 for all register descriptions), thus reducing the amount of free RAM by lowering the value contained in the pointer. The extra RAM, which BASIC thinks is not available, is now free for PMGs. Line 100 also sets the variable LOC equal to the start of PMG RAM for Player 0, and sets LEN to 256, the maximum number of bytes in a single-line-resolution Player shape.
Line 110 POKEs the PMG shape's color into the PCOLR0 register, assigns PM$ to point to the Player 0 PMG RAM, and uses an interesting method for quickly filling a string with any character from BASIC. Because of a quirk in the way Atari BASIC handles strings, a string assignment can copy the first character into every position in the string up to the current length. When you specify only the first position within a string, such as A$(44), Atari BASIC assumes the second position is the default, which is the end of the string. If you assign the second position in a string equal to the first position, in the form A$(2)=A$(1), the filling effect occurs. The second character will become the same as the first, and then the third will become the same as the second, etc. The computer thinks it's moving an entire string down one character, but in actuality, it's filling consecutive characters with the previous character. In this case, PM$ and C$ will be set to character zeros.
Line 120 reads the PMG shape data into C$ and does the three mandatory POKEs into SDMCTL, GRACTL, and PMBASE that are required to implement PMGs. The values used are for single line resolution PMGs. SDMCTL turns on the PMG DMA control and sets the resolution mode, GRACTL enables the GTIA's PMG processing hardware, and PMBASE tells GTIA where to look for PMG RAM. Line 120 also sets the vertical and horizontal positions for the shape in Player 0 to 40 (PMV = 40) and 60 (PMH = 60).
Lines 130 through 210 convert joystick input into adjustments for the horizontal and vertical posititions of Player 0. Lines 220 through 250 check these new positions to see if they are still within the valid coordinate range. Line 260 POKEs the adjusted horizontal position into the HPOSP0 register and assigns the PMG shape string into PMG RAM via PM$. Line 270 branches back for more input and line 280 contains the Player/Missile shape data. Lines 290 through 430 are the STRPUT subroutine which I discussed previously.
While experimenting with PMGs and the STRPUT subroutine, I somewhat accidentally started to write a demo, which eventually evolved into a car race program that used a PMG car shape and a downward scrolling racetrack. I had gone through the trouble of writing a screen scroll routine in 6502 Machine Language to make the game playable, but I wasn't satisfied with it. The whole purpose of writing the demo was to show what could be done with PMGs from Atari BASIC through the use of STRPUT. Adding a Machine Language routine to the demo seemed to defeat the entire purpose. Then a thought came to me: "Wait a minute... a downward scrolling screen is similar to a downward scrolling PMG." That's when I got the idea of using STRPUT to assign a string to the screen memory RAM. By setting a string equal to the entire area of screen memory, a simple string assignment statement could scroll the entire screen downward.
The final result is Listing 3, which I call RACER. Combining most of the code from Listings 1 and 2, plus a little extra for a racetrack algorithm, I created a complete game written entirely from BASIC. The startling thing about RACER is that it is fast - surprisingly fast when you consider what is being done in BASIC. Since I have already discussed most of the code for RACER the only thing (other than the screen scroll routine) that might not be clear is in lines 150 and 410. In Table 1 you'll find two POKE locations with the names HITCLR and POPF. They deal with the PMG collison registers which I will cover in a later installment. These registers can tell BASIC when the car touches a racetrack wall.
Listing 3.
100 DIM VAR$(3),PM$(256),C$(20),S$(400),T$(400):POKE 106,PEEK(106)-8:GRAPHICS 4:LOC=PEEK(106)*256+1024 110 VAR$="PM$":LEN=256:GOSUB 480:LOC=PEEK(88)+PEEK(89)*256:VAR$="S$":LEN=400:GOSUB 480:POKE 752,1 120 POKE 708,68:POKE 709,14:POKE 710,0:RESTORE 470:FOR AZ=1 TO 18:READ AZ1:C$(AZ)=CHR$(AZ1):NEXT AZ:POKE 559,62 130 PM$=CHR$(0):PM$(256)=CHR$(0):PM$(2)=PM$(1):POKE 53277,3:POKE 54279,PEEK(106):POKE 53248,122:SCORE=0 140 DIS=3:SIDE=1:FAC=0.9:LAP=0:POS=32:? "RACER BY ALAN J. ZETT":? :? :PM$(176)=C$:PMH=122:PMV=176:POKE 704,200 150 COLOR 1:PLOT 32,0:DRAWTO 32,39:PLOT 45,0:DRAWTO 45,39:POKE 53278,0 160 LAP=LAP+1:? CHR$(28);"SCORE: ";INT(SCORE):T$=S$:S$(11)=T$:IF RND(0)>FAC AND D=0 THEN D=INT(RND(0)*DIS)-SIDE 170 COLOR 0:PLOT POS,0:PLOT POS+13,0 180 IF D>0 THEN POS=POS+1 190 IF D<0 THEN POS=POS-1 200 IF POS<10 THEN POS=10:D=0 210 IF POS>64 THEN POS=64:D=0 220 COLOR 1:PLOT POS,0:PLOT POS+13,0 230 SCORE=SCORE+22/PMV:POKE 77,0 240 SOUND 0,52+(PMV/2),2,4 250 IF D<0 THEN D=D+1 260 IF D>0 THEN D=D-1 270 IF LAP=200 THEN DIS=DIS+4:SIDE=SIDE+2:IF DIS>61 THEN DIS=61:SIDE=30 280 IF LAP>400 THEN LAP=0:FAC=FAC-0.07:IF FAC<0.25 THEN FAC=0.25 290 ST=STICK(0):IF ST=15 THEN 410 300 IF ST=5 THEN PMH=PMH+2:PMV=PMV+2 310 IF ST=6 THEN PMH=PMH+2:PMV=PMV-2 320 IF ST=7 THEN PMH=PMH+2 330 IF ST=9 THEN PMH=PMH-2:PMV=PMV+2 340 IF ST=10 THEN PMH=PMH-2:PMV=PMV-2 350 IF ST=11 THEN PMH=PMH-2 360 IF ST=13 THEN PMV=PMV+2 370 IF ST=14 THEN PMV=PMV-2 380 IF PMV<32 THEN PMV=32 390 IF PMV>176 THEN PMV=176 400 POKE 53248,PMH:PM$(PMV)=C$ 410 IF PEEK(53252)=0 THEN 160 420 FOR X=15 TO 200 STEP 5:SOUND 0,X,8,8:NEXT X:FOR X=-1 TO 18:PM$(PMV+X)=CHR$(RND(0)*254):NEXT X 430 FOR X=255 TO 0 STEP -1:POKE 704,X:SOUND 0,X,X,X:NEXT X:SOUND 0,0,0,0:COLOR 0:PLOT 0,0:DRAWTO 79,0 440 POKE 53248,0:FOR X=0 TO 39:T$=S$:S$(11)=T$:NEXT X:? :? "GAME OVER (PRESS TRIGGER)" 450 IF STRIG(0)=1 THEN 450 460 GOTO 130 470 DATA 0,0,153,153,255,255,189,199,36,36,199,189,255,255,153,153,0,0 480 VNTP=PEEK(130)+PEEK(131)*256 490 VNTD=PEEK(132)+PEEK(133)*256 500 VVTP=PEEK(134)+PEEK(135)*256 510 STARP=PEEK(140)+PEEK(141)*256 520 AZ1=-1 530 AZ1=AZ1+1:FOR AZ=1 TO LEN(VAR$) 540 AZ2=PEEK(VNTP):IF AZ2>127 AND AZ<LEN(VAR$) THEN 560 550 IF AZ2-128*(AZ2>127)=ASC(VAR$(AZ))THEN VNTP=VNTP+1:NEXT AZ:GOTO 590 560 IF PEEK(VNTP)<128 THEN VNTP=VNTP+1:GOTO 560 570 VNTP=VNTP+1:IF VNTP<VNTD THEN 530 580 GRAPHICS 0:? VAR$;" ISN'T A LEGAL VARIABLE":END 590 AZ=VVTP+AZ1*8+2:IF PEEK(AZ-2)<>129 THEN 570 600 AZ1=LOC-STARP:GOSUB 620:AZ1=LEN:GOSUB 620:AZ1=LEN:GOSUB 620 610 RETURN 620 AZ2=INT(AZ1/256):AZ1=AZ1-AZ2*256:POKE AZ,AZ1:POKE AZ+1,AZ2:AZ=AZ+2:RETURN
To play the RACER program just type it in and RUN it. You earn points for staying alive on the racetrack. You get higher points for driving faster, but this cuts down on your response time, making accidents more probable. The racetrack also gets more and more twisting, so the right combination of speed and caution will payoff. To move the car, push your joystick left or right. To accelerate push the joystick forward, to decelerate, pull back. See how long you can survive!
Up until now, I have been concerned only with vertical movement for single-line-resolution PMGs. The only real difference between them and double-line-resolution PMGs is that the latter take half as many bytes. That will change some of the calculations for STRPUT and such, but not too seriously. For example in Listing 2, line 100: LOC would change to PEEK(l06)*256 + 512, and the LEN of PM$ would change to 128. The rest would stay the same. In fact, the only really objectionable problem with double line resolution PMGs is that when you move them, they also move two lines at a time. For really smooth motion, a PMG shape should move one scan line at a time vertically, otherwise a certain amount of jumping will be visible.
To get around this problem, the GTIA processor has a built in hardware register known as VDELAY, as listed in Table 1. The purpose of VDELAY is to cause the video processing hardware to wait for one scan line before "drawing" the PMG shape into the video signal. You can activate VDELAY by POKEing values into it. Look at the bit map for VDELAY in Figure 1. It shows which bits in VDELAY affect which Players and Missiles. Instead of moving the shape in memory one byte at a time, you can alternate between moving one byte and POKEing the shape down one scan line with VDELAY. Note that you can set more than one bit at any time. To calculate what value to POKE into VDELAY, add the decimal values of each of the bits together.
Figure 1. VDELAY - VERTICAL DELAY ($D01C) A hardware register which controls the vertical position of PMGs. Bit Map: ┌───┬───┬───┬───┬───┬───┬───┬───┐ Bit: │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │ └───┴───┴───┴───┴───┴───┴───┴───┘ B7 - 1 Delays Player 3. Values 128. B6 - 1 Delays Player 2. Values 64. B5 - 1 Delays Player 1. Values 32. B4 - 1 Delays Player 0. Values 16. B3 - 1 Delays Missile 3. Values 8. B2 - 1 Delays Missile 2. Values 4. B1 - 1 Delays Missile 1. Values 2. B0 - 1 Delays Missile 0. Values 1.
That's it for this month. Next time we get together for a Frontier Fireside we'll continue our chat on PMGs. Until then - how about some letters people?