10 MODE2 20 VDU23,1,0;0;0;0; 30 FOR A%=0 TO 1 40 FOR B%=0 TO 7 50 FOR C%=0 TO 3 60 READ D% 70 ?(&3000+A%*640+B%+C%*8)=D% 80 NEXT,, 90 GOTO90 20000 DATA&0C,&00,&00,&0C 20010 DATA&04,&08,&04,&08 20020 DATA&00,&0C,&0C,&00 20030 DATA&00,&09,&06,&00 20040 DATA&01,&03,&03,&02 20050 DATA&03,&03,&03,&03 20060 DATA&17,&2B,&17,&2B 20070 DATA&17,&21,&12,&2B 20080 DATA&17,&21,&12,&2B 20090 DATA&03,&03,&03,&03 20100 DATA&03,&03,&03,&03 20110 DATA&01,&03,&03,&02 20120 DATA&00,&21,&12,&00 20130 DATA&00,&20,&10,&00 20140 DATA&30,&20,&10,&30 20150 DATA&30,&20,&10,&30
Notice in line 70 that the first eight rows of the sprite are placed from address &3000 onwards and the second eight rows are placed 640 bytes further on because they are on the next text line. Notice also that the position ALONG a row, C%, is multiplied by eight in line 70 because the columns take up eight bytes each. All this is to get around the problem caused by the complex way in which the screen is laid out. If we want to be able to move the sprite UP and DOWN a pixel at a time we are going tohave to find a way of knowing when to add 640 to the address to get us tothe next text line.
For our final sprite routine, we are going to assume that the data for the shape of the sprite is already stored in a section of memory (perhaps in an array) in the format we used in the Basic example. We can then copythis sprite table into the screen memory at whatever screen position wewant the sprite.
Before we can write a complete sprite routine, we should write an experimental version in BASIC. The easiest way to do this is as a procedure.
1000 DEF PROCsprite(L%,X%,Y%,W%,H%)
Here L% is the location of the first byte of the sprite shape table, X% and Y% are, respectively, the X and Y coordinates of the top left-handcorner of where we want the sprite to appear on the screen, and W% and H% are the width and height of the sprite. To make the program simpler, wewill take the X coordinate and the width W% as being in bytes (i.e. they give the number of pixels divided by two). Thus the X coordinate can take values from 0 to 79. The Y coordinate and height H% will be in pixels.
First we need to find the screen memory address of the top left-handcorner of where we want the sprite to appear. We need to split the Ycoordinate up into the text row number (from 0 to 31) and the number ofpixels down within that row (from 0 to 7). Because each row contains eightpixels vertically, bits 0 to 2 of the Y coordinate will be the number ofpixels down within the row and the other five bits will be eight times therow number. So, if we take Y% DIV 8, this will give us the text row number. As one row takes up 640 bytes of memory, we must multiply this by640 then add &3000 (which is the address of the start of the screen). Then we add eight times the X coordinate and finally the least significant three bits of the Y coordinate (Y% MOD 8). This will give us the addressof the first byte of the screen memory that we will need to change to place the sprite on the screen. We will, however, need to keep the Y% MOD8 part of the address separate as it will tell us how far down within the text row we are. So we end up with something like this:
1010 A%=&3000+(Y%DIV8)*640+X%*8 1020 Y%=Y%MOD8
Notice that in line 1020 we have altered Y% so that, instead of containingthe complete Y coordinate, it now only holds the Y coordinate within thetext row.
We now have all the information we need to fix the address of the topleft-hand corner of the sprite's intended position on the screen and so place the first row of pixels of the sprite on the screen. By adding eight to the current address (in A%+Y%) each time, we will move two pixels (=one byte in Mode 2) to the right. We can continue to copy the table intoevery byte of the screen memory, moving one byte through the table eachtime, until we have copied the number of bytes specified by the width W%.We don't need the X coordinate again so we can use X% to count bytes.
1030 FOR X%=0 TO W%-1 1040 ?(A%+Y%+X%*8)=?L% 1050 L%=L%+1 1060 NEXT
We have now placed the first row of pixels of the sprite on the screen.Now we need to go to the next row of pixels. We can do this by adding 1 toY%. However, if Y% then equals eight, we need to move to the next text rowby adding 640 to A%. At the same time we can check to see if the addresshas passed through &8000. If so, the sprite has dropped off the bottom ofthe screen and we want the remainder of it to appear at the top of the screen to produce a 'wrap around' effect. To do this we need only subtract&5000 from A% to move back to the corresponding position at the top of thescreen.
1070 Y%=Y%+1 1080 IF Y%=8 THEN Y%=0:A%=A%+640:IF A%>&7FFF THENA%=A%-&5000
Next we need to check whether we have finished drawing the sprite. Theeasiest way to do this is to subtract one from the height (H%) after eachrow until H% is 0.
1090 H%=H%-1 1100 IF H%>0 THEN 1030 1110 ENDPROC
We now have a complete BASIC sprite routine.
However, this routine would be very awkward for realistic animation asit provides no means of removing the sprite again. To do this, we need to Exclusive-Or the sprite with the screen to put it on and then do the sameagain to remove it. This is the same technique that can be used from BASICwith the VDU23 user-defined characters. For this we need to alter ourprogram. Line 1040 should now read:
1040 ?(A%+Y%+X%*8)=?(A%+Y%+X%*8) EOR ?L%
Here again, then, is the complete BASIC sprite routine.
1000 DEF PROCsprite(L%,X%,Y%,W%,H%) 1010 A%=&3000+(Y%DIV8)*640+X%*8 1020 Y%=Y%MOD8 1030 FOR X%=0 TO W%-1 1040 ?(A%+Y%+X%*8)=?(A%+Y%+X%*8) EOR ?L% 1050 L%=L%+1 1060 NEXT 1070 Y%=Y%+1 1080 IF Y%=8 THEN Y%=0:A%=A%+640:IF A%>&7FFF THENA%=A%-&5000 1090 H%=H%-1 1100 IF H%>0 THEN 1030 1110 ENDPROC
You may have noticed that the BASIC program is a little awkwardly written. Even so, it has been specifically written to be easy to code into machine code.
At this stage in our exploration it is hardly necessary to give the corresponding line numbrs of the sections discussed. You may find ituseful to refer to the above listing for the next section
1010 A%=&3000+(Y%DIV8)*640+X%*8
Let's deal with this in reverse order. The first task, then, is to calculate X%*8. The answer to this could be larger than 255 (since X% can go up to 79) so we will need two bytes to hold the answer. We are going to store the final answer to A% at &70 and &71 so let's start by putting X% in &70 and zero in &71. Thus we can treat X% as a two-byte number and multiply it by two, three times, so as to finally multiply it by eight. X% is initially contained in the X register, so the code for the multiplication by eight looks like this:
STX &70 LDX #0 STX &71 ASL &70 ROL &71 ASL &70 ROL &71 ASL &70 ROL &71
Notice that as we don't need the X coordinate again the X register is free for other use.
We have now calculated the X%*8 part of A%, so our next task is the multiplication by 640. Conveniently, the 1.2 Operating System ROM contains a 'times 640' table. This is stored starting at address &C375. It is stored as 32 entries each two bytes long (high byte then low byte). So the contents of &C375 and &C376 are 0 (for 640 x 0), the contents of &C377 and &C378 are 640 (for 640 x 1) and so on. (If you don't have a 1.2 Operating System then it is relatively easy to write a BASIC program that calculates such a table and stores it in an array.) So, to add the result of (Y% DIV 8)*640 to A% (stored at &70 and &71) we do this:
TYA AND #&F8 LSRA LSR TAX LDA &C376,X CLC ADC &70 STA &70 LDA &C375,X ADC &71 CLC ADC #&30 STA &71
The first section masks off the bottom three bits of Y% then divides by four. This leaves the accumulator containing the equivalent of (Y%DIV8)*2. This is because each byte in the times 640 table takes up TWO bytes. Thus the result of (Y%DIV8)*640 can be looked up using the X register as a pointer and added to the rest of A% in &70 and &71. Notice that, in the last three commands, we have added &30 to the high byte. This is the equivalent of adding &3000 to the whole number. So now we have the equivalent of A% in &70 and &71. Notice that this is onlyu the address in the screen memory of the first character position at which the sprite is to be placed, not the actual byte within that character position.
The next line in our BASIC example was:
1020 Y%=Y%MOD8
This represents how far down the character position the sprite is to be placed. Converting this to machine code is very easy. We want only the least significant three bits of the Y register, so we do:
TYA AND #7 STA &74
We have copied this answer into &74 as we are going to need to use it again. The next section of the BASIC program was a loop from 0 to W%-1 for the width of the sprite. Notice that in line 1040 we have to add to A% (now in &70 and &71) the current contets of &74 (Y%) and X%*8. The easiest way to do this in machine code is to first load the Y register with the contents of &74, then add eight to it each time around th loop. If we do this, then the values the loop takes do not matter so long as it is executed the correct number of times.
1040 ?(A%+Y%+X%*8)=?(A%+Y%+X%*8) EOR ?L%
So, we can load the X register with the width (W%) and decrement it each time round the loop until it is zero. Because we have A% in &70 and &71 and the rest of the expression in the Y register, we can use post-indexed indirect addressing to access the screen, i.e. LDA (&70),Y will be the equivalent of ?(A%+Y%+X%*8).
Next we need to load a byte from the sprite shape table into the accumulator. Because at this stage we are using both the X and Y registers, we cannot easily use any form of indexed addressing. What we do instead is use absolute addressing. In this addressing mode the address is stored as two bytes after the command byte. The first byte is the low byte, the second is the high byte. By changing these two bytes, we can use this addressing mode for looking into tables. This method is not, strictly speaking, 'legal' as it would not work if the routine were in ROM, but it does save on memory and speed.
The next section of our program will look like:
.row LDY &74 LDX &72
This sets the Y register to the position of the first pixel of the sprite row and puts the width W% in the X register. Because we jump back to here at the start of each new row of the sprite, we need the label ROW.
.byte LDA &FFFF
Above is the command that looks into the sprite shape table: the two bytes after it will be modified by the program as it runs, so the number &FFFF is unimportant. After each byte of the sprite is placed on the screen we will need to jump back to this command; so again a label, BYTE, is needed. However, before we can carry on we need to backtrack to the very beginning of the routine because we need to place the address of the first byte of the sprite shape table at BYTE+1 and BYTE+2 (low, high) in place of the 'dummy' address we have specified in the assembly code. Thus the program will start by copying the first byte of the table into the screen memory.
We have specified that when calling this routine, the address of the first byte of the table should be stored at &75 and &76, so the first two lines of the program need to be:
.sprite LDA &75 STA byte+1 LDA &76 STA byte+2
Going back to where we left off, we have just loaded a byte from the sprite shape table. So now we will need to EOR this byte with the relevent byte of the screen memory and place the result back on the screen:
EOR (&70),Y STA (&70),Y
Then we must add eight to the Y register to move one byte (two pixels) to the right:
TYA CLC ADC #8 TAY
The next line of the BASIC program was
1050 L%=L%+1
So we need to add one to the sprite shape table pointer. This is held, remember, in the two bytes after the LDA command at BYTE so we can do this:
INC byte+1 BNE nocarry INC byte+2 .nocarry DEX BNE byte
Notice that we then carry on doing one row of the sprite, moving from left to right, until X has reached zero and we have completed a row. The next two lines of the BASIC program were:
1070 Y%=Y%+1 1080 IF Y%=8 THEN Y%=0:A%=A%+640:IF A%>&7FFF THEN A%=A%-&5000
See if you can spot how this relates to the following machine code:
INC &74 LDA &74 CMP #8 BNE notline LDA #0 STA &74 LDA &70 CLC ADC #&80 STA &70 LDA &71 ADC #2 STA &71 CMP #&80 BCC notline SEC SBC #&50 STA &71 .notline ...
Lastly, we need to subtract one from H% and branch back to the label ROW if H% is larger than zero, or return from the subroutine, with the sprite completed, if not.
.notline DEC &73 BNE row RTS
We now have a complete assembly language sprite drawing routine.
.sprite LDA &75 DEFPROCsprite (L%,X%,Y%,W%,H%) STA byte+1 LDA &76 STA byte+2 STX &70 A% = LDX #0 STX &71 ASL &70 X% * 8 ROL &71 ASL &70 ROL &71 ASL &70 ROL &71 TYA + (Y%DIV8) AND #&F8 LSR A LSR A TAX LDA &C376,X * 640 CLC ADC &70 STA &70 LDA &C375,X ADC &71 CLC ADC #&30 + &3000 STA &71 TYA AND #7 Y%=Y%MOD8 STA &74 .row LDY &74 LDX &72 FOR X% = 0 TO W%-1 .byte LDA &FFFF ?(A%+Y%+X%*8)= ?(A%+Y%+X%*8) EOR ?L% EOR (&70),Y STA (&70),Y TYA CLC ADC #8 TAY INC byte+1 L%=L%+1 BNE nocarry INC byte+2 .nocarry DEX BNE byte INC &74 Y%=Y%+1 LDA &74 IF Y%<>8 CMP #8 BNE notline THEN 'notline' LDA #0 Y%=0 STA &74 LDA &70 A%=A%+640 CLC ADC #&80 STA &70 LDA &71 ADC #2 STA &71 CMP #&80 IF A%<&8000 BCC notline THEN 'notline' SEC SBC #&50 STA &71 .notline DEC &73 H%=H%-1 BNE row IF H%>0 THEN 'row' RTS ENDPROC
This routine will only Exclusive-Or a sprite with the screen memory; it will not move it for us.
.sprite STX &70 LDX #0 STX &71
Next we need to use the sprite number (which we have said must be in the accumulator when the routine is called) as a pointer for the information table. As each entry takes up four bytes we need to multiply the accumulator by four then transfer it to the X register ready to point into the information tabke.
ASL A ASL A TAX
Now we can transfer the four bytes of information about the sprite (sprite shape table address, width and height) from the information table into the relevant locations ready for the rest of the program.
LDA sprites,X STA byte+1 LDA sprites+1,X STA byte+2 LDA sprites+2,X STA &72 LDA sprites+3,X STA &73
Notice that, by taking the address of the sprite shape table directly from the sprite information table and placing it in BYTE we don't need to use locations &75 and &76 any more.
The rest of the routine is the same as before (Listing 1 on page 228), starting with:
ASL &70 ROL &71 ASL &70 ... ...
We now only have to give the number of the sprite we want to place on the screen, but we still have to move the sprites by putting them on the screen and then taking them off again. This problem can be solved by keeping track of which of the sprites are being shown on the screen at any one time. The best way to do this is to have a number assigned to each moving sprite (or 'film') on the screen. This number should not be confused with the number we assigned to each sprite shape table. If, for example, we had a game with a user-controlled man and eight monsters, the man could be film 0 and the monsters could be films 1 to 8. Any of these nine films (0 to 8) could actually appear as any of the sprites we have shape tables for. And the man would probably alternate between two different sprite shapes so that he would appear to walk.
What we will do is keep a further set of tables that contain the number of the sprite shape that was last used for each film and where on the screen it was placed. To do this we will have three tables.
The table OLDS will contain the number of the last sprite used and OLDX and OLDY will contain the last X and Y coordinates for each of the films. Each film will use one byte of each of these three tables. Thus the details for film 0 will be at the first byte of each table, the details for film 1 will be at the second byte of each table, etcetera. So at the beginning of our program we will need:
DIM olds 255, oldx 255, oldy 255
If fewer films are needed then the size of the arrays can be reduced accordingly. So that we know that none of the films are in use at the start of the program, let's set all the entries in OLDS to 255. So a 255 in OLDS tells us that the relevant films is not yet active. This means that we can't have a sprite shape numbered 255.
FORA%=0TO255 olds?A%=255 NEXT
We can now write a new routine, which will use the subroutine SPRITE, that will move a film on the screen. We can enter this routine with just the number of the film we want to move, the number of the new sprite shape we want to use for the film, and the X and Y coordinates to which we want to move the film. For convenience let's keep the sprite number in the accumulator, the X and Y coordinates in the X and Y registers, and store the film number in &75. The first job the new routine must do is to save the contents of the three registers as they will not be needed immediately. We could push these on the stack but this is slow. It is quicker to store them in zero page:
.move STA &76 STX &77 STY &78
Next we need to load the X register with the film number so that we can look into the film tables. We then need to check if this is a new film (whether the relevant entry in OLDS contains 255). If so, we don't need to remove an old image and can go straight to the new image routine.
LDX &75 LDA olds,X CMP #255 BEQ newfilm
If we find that we need to remove an old image we can load the data about the old image from the film tables. Note that because of the shortage of registers we have to temporarily save the contents of the accumulator (which contains the byte from OLDS) in yet another zero page location.
STA &79 LDA oldy,X TAY LDA oldx,Y TAX LDA &79
We can now call SPRITE to remove the old image. Notice that we then need to reload the X register with the current film number, as this has been lost.
JSR sprite LDX &75
We now have to place the new sprite on the screen and at the same time place the data on the new sprite back in the film tables ready for the next animation. Note that the new sprite shape number and the new X and Y coordinates have to be reloaded from zero page where we stored them temporarily.
.newfilm LDA &78 STA oldy,X TAY LDA &77 STA oldx,X LDA &76 STA olds,X LDX &77
Note again the problems caused by the lack of registers.
Next we need to call SPRITE again. In fact, it is better to place the whole film move routine directly before the sprite routine so that the next command will be the start of the sprite routine. This saves a JSR command and an RTS command (See full listing below).
We now have a complete sprite routine. For clarity here is a complete assembler listing of it. To use it from BASIC use the command PROCassemble at the beginning of the program to initialise it and then call MOVE with A%, X%, Y% and &75 set correctly, as explained earlier. (Listing two below.)
10000 DEFPROCassemble 10010 DIMsprites 255,olds 255,oldx 255,oldy 255 10020 FORA%=0TO255 10030 olds?A%=255 10040 NEXT 10050 DIMZ%200 10060 FORpass%=0TO2STEP2 10070 P%=Z% 10080 [OPTpass% 10090 .move STA &76 \ Store info 10100 STX &77 \ on new sprite 10110 STY &78 \ temporarily 10120 LDX &75 \ in zero page 10130 LDA olds,X 10140 CMP #255 10150 BEQ newfilm 10160 STA &79 10170 LDA oldy,X \ Remove old 10180 TAY \ sprite if nec. 10190 LDA oldx,X 10200 TAX 10210 LDA &79 10220 JSR sprite 10230 LDX &75 10240 .newfilm LDA &78 \ Replace 10250 STA oldy,X \ with 10260 TAY \ 10270 LDA &77 \ new sprite 10280 STA oldx,X \ and store 10290 LDA &76 \ new sprite in 10300 STA olds,X \ arrays 10310 LDX &77 10320 .sprite STX &70 \ Main sprite 10330 \ routine. 10340 LDX #0 10350 STX &71 10360 ASL A 10370 ASL A 10380 TAX \ Get info 10390 LDA sprites,X \ on sprite shape 10400 STA byte+1 \ from info 10410 LDA sprites+1,X \ table 10420 STA byte+2 10430 LDA sprites+2,X 10440 STA &72 10450 LDA sprites+3,X 10460 STA &73 \ Calculate screen 10470 ASL &70 \ addr of top of 10480 ROL &71 \ LH corner 10490 ASL &70 \ of sprite 10500 ROL &71 10510 ASL &70 10520 ROL &71 10530 TYA 10540 AND #&F8 10550 LSR A 10560 LSR A 10570 TAX 10580 LDA &C376,X 10590 CLC 10600 ADC &70 10610 STA &70 10620 LDA &C375,X 10630 ADC &71 10640 CLC 10650 ADC #&30 10660 STA &71 10670 TYA 10680 AND #7 10690 STA &74 10700 .row LDY &74 \ Plot row 10710 LDX &72 \ of pixels 10720 .byte LDA &FFFF \ Plot 1 pair of 10730 EOR (&70),Y \ pixels (1 byte) 10740 STA (&70),Y 10750 TYA \ move right to 10760 CLC \ next pair of 10770 ADC #8 \ pixels 10780 TAY 10790 INC byte+1 \ set shape table 10800 BNE nocarry \ pointer to 10810 INC byte+2 \ next byte 10820 .nocarry DEX \ next pair of 10830 BNE byte \ pixels until 10840 \ row complete. 10850 INC &74 \ move down to 10860 LDA &74 \ next row of 10870 CMP #8 \ pixels 10880 BNE notline \ If nec. move 10890 LDA #0 \ down to 10900 STA &74 \ next text row 10910 LDA &70 10920 CLC 10930 ADC #&80 10940 STA &70 10950 LDA &71 10960 ADC #2 10970 STA &71 10980 CMP #&80 10990 BCC notline \ If nec. 'wrap' 11000 SEC \ back to top 11010 SBC #&50 \ of screen 11020 STA &71 11030 .notline DEC &73 \ Move down a row 11040 BNE row \ until sprite 11050 RTS \ complete 11060 ] 11070 NEXT 11080 ENDPROC
This routine has several limitations. Firstly, it cannot be used from a second processor because it uses direct screen access rather than the official Acorn commands. For a routine to work with the second processor, only the operating system commands must be used for input and output, but this slows down a machine code program considerably. For most purposes it is better to leave this routine in the main processor and call it from the second processor.
Secondly, notice that as the Y register is used to hold 8 times the X coordinate within the sprite, the largest width of sprite the routine will handle is 32 bytes. Vertically there is no limit. Also note that by using a differently coded sprite shape table, this routine may be used in any 20K mode. However, the number of horizontal positions the sprite may be positioned at will still only be 80.
Now that we have our sprite routine we must see how it can be used. Let's write a routine to animate a small man around the screen under the control of the Z, X, /, and : keys. We will use the man we used for the BASIC example. First we must have the sprite shape table as data.
20000 DATA&0C,&00,&00,&0C 20010 DATA&04,&08,&04,&08 20020 DATA&00,&0C,&0C,&00 20030 DATA&00,&09,&06,&00 20040 DATA&01,&03,&03,&02 20050 DATA&03,&03,&03,&03 20060 DATA&17,&2B,&17,&2B 20070 DATA&17,&21,&12,&2B 20080 DATA&17,&21,&12,&2B 20090 DATA&03,&03,&03,&03 20100 DATA&03,&03,&03,&03 20110 DATA&01,&03,&03,&02 20120 DATA&00,&21,&12,&00 20130 DATA&00,&20,&10,&00 20140 DATA&30,&20,&10,&30 20150 DATA&30,&20,&10,&30
Our main program must first go into Mode 2 and turn off the cursor then assemble the machine code. Next it must load the sprite shape table into a reserved block of memory (in this case an array) and place the information about the size of the sprite and location of the shape table in the memory, in SPRITES. We are only going to use one film (film zero) so we can set the film number stored at &75 permanently to zero.
10 MODE 2 20 VDU23,1,0;0;0;0; 30 PROCassemble 40 DIM man% 63 50 FOR A% = 0 TO 63 60 READ man%?A% 70 NEXT 80 !sprites = man% 90 sprites?2 = 4 100 sprites?3 = 16 110 ?&75 = 0
Notice that the command READ man%?A% is legal. This is because expressions using the operators ?, ! and $ are treated as variable names. Next we set the starting position of the man into X% and Y% and set the sprite shape number in A% to 0.
120 X% = 0 130 Y% = 0 140 A% = 0
Next we must place the man on the screen.
150 CALL move
Next we need to alter X% and Y% according to which keys are being pressed.
160 IF INKEY-98 AND X%>0 X%=X%-1 170 IF INKEY-67 AND X%<76 X%=X%+1 180 IF INKEY-105 AND Y%<240 Y%=Y%+4 190 IF INKEY-73 AND Y%>0 Y%=Y%-4
Notice the checks to prevent our man from wandering off the edge of the screen. Next we need to go back to line 150 to remove his old image and plot his new one.
200 GOTO150
If we now add in the main sprite routine assembly code (Listing 2) the program should work.
195 *FX19
If you try this, however, you will find that now there is a region at the top of the screen where the sprite disappears completely. This is because the synchronisation produced by *FX19 occurs roughly in the middle of the blanking period. The time between this and the beam's starting to draw the top of the screen is too small to move the sprite in. If the sprite is at the top of the screen the beam arrives at the sprite position before it has been replaced, so the sprite vanishes.
Unfortunately there is no simple solution to this problem. In many cases it is easiest to ignore the flicker completely. In most slow-moving games, such as PACMAN or DONKEY KONG, where the sprites move only every other frame the flicker will not be much of a problem. Some games, however, could be improved considerably by the removal of flicker.
To do this we need some means of synchronising the sprite movement with the beginning of the blanking period rather than the middle as produced by *FX19. We need an interrupt to occur at the very beginning of the blanking period.
To do this we can use *FX19 to start an interrupt timer in one of the VIAs which will count for just long enough for it to create an interrupt when we need it. Before we can get into the details of this, however, we need to solve another problem. If we are using several films they must all move at the same time - during the blanking period, not when the commands for each of them to move is sent. For this reason we will need a buffer, for each film, which contains the X and Y coordinates and the new sprite shape number for the next move of that film.
The easiest way to do this is to have three tables NEWX, NEWY and NEWS. As with the three tables for the old positions each film uses one byte of each table. The main program can then set these tables to hold the movements required. When the interrupt occurs, the movements can be processed in one go. We may not want to move all the films every 50th of a second, so any one we don't want to move will have 255 in its corresponding byte of NEWS. Once each film has been moved its entry in NEWS will be set to 255 ready for the NEXT move. We could use the move routine we already have for this and add to it, but it is easier to rewrite it to handle all the sprites. Firstly we will need the actual sprite routine from LISTING 2, starting at the label SPRITE. Next we need to know how many films are in use at any moment. This can be stored in &77. (Remember that the sprite routine only uses locations &70 to &74.) Now we can start.
If the contents of &77 are, say, 7, then we will refer to bytes 0 to 6 in each of the tables, so we need to load the contents of &77 into the X register and decrement them. While we are at it we need to check whether X was 0 and, if so, end the routine. So the start of the routine will look like this:
.rts RTS .films LDX &77 .next DEX CPX #255 BEQ rts
Notice that it is more efficient to place the RTS command before the start of the routine as this ensures that even if the rest of the routine is long the branch range won't be exceeded. The label NEXT can be jumped to, to process the next film.
Next we need to check whether this film needs moving. Remember we said that static films would have 255 in NEWS (the table holding the new sprite shape number for each film).
LDA news,X CMP #255 BEQ next
We now check whether there is an old sprite to remove from the screen and, if so, remove it.
LDA olds,X CMP #255 BEQ newfilm STA &76 LDA oldy,X TAY LDA oldx,X STX &75 TAX LDA &76 JSR sprite LDX &75
Now we have to place the new sprite on the screen and store the data on it in the relevant bytes of OLDS, OLDX and OLDY.
.newfilm LDA news,X STA olds,X STA &76 LDA newy,X STA oldy,X TAY LDA newx,X STA oldx,X STX &75 TAX LDA &76 JSR sprite
Finally we have to store 255 in the relevant bytes of NEWS (so that the program does not repeat this move in the next frame), and go back for the next film.
LDX &75 LDA #255 STA news,X JMP next
Notice the similarities between this version of MOVE and the previous one.
Next we need a routine that will place a command into the buffer. For this purpose we will use &78 to hold the current film number, and the accumulator and the X and Y registers to hold the sprite number and X and Y coordinates respectively, as before. Thus, to instruct the sprite routine to move a film, you must place the film number in &78, the number of the sprite shape you want to use in the accumulator, and the new X and Y coordinates in the X and Y registers. Then you must call the routine MOVE.
.move STX &76 LDX &78 STA news,X TYA STA newy,X LDA &76 STA newx,X RTS
Now we have come to the difficult bit. We need to intercept the interrupt which *FX19 uses and use it to start an interrupt timer in the system VIA. We will need an initialisation routine - first, to copy the IRQ1 vector into a spare vector, and second, to alter it to point to our own interrupt routine. While doing this we need to disable the interrupts so that no interrupt can occur when the vector is changed.
.init SEI LDA &204 STA &230 LDA &205 STA &231 LDA #irq MOD256 STA &204 LDA #irq DIV256 STA &205
We also need to disable the TIMER 1 interrupts and the analogue-to-digital converter interrupts in the system VIA. These tend to cause trouble by interrupting at awkward moments. Doing this means the analogue-to-digital converter interrupts and the centisecond clock will not work. However, if you want a clock, say for a game, it is easy to add into the IRQ routine a section which counts video sync interrupts (which occur 50 times a second) and increments a location every fiftieth interrupt so that it counts in seconds. It also means that though INKEY will work GET will not.
LDA #&50 STA &FE4E CLI RTS
(See chapter 4 for more information on the registers in the VIA.)
Now that we have initialised the interrupts we need a routine to handle them. This routine must appear totally 'transparent' to the operating system - it mustn't interfere with the operating system at all. For this reason it must not change any of the registers (A, X or Y). When the operating system processes an interrupt it stores the accumulator in location &FC and leaves the other two registers as they were when the interrupt occurred. If then jumps to the vector IRQ1.
We also have the problem that if another interrupt occurs while we are processing the current one, the routine will be entered again. This must not disturb the first entry. For these reasons both the contents of location &FC and the X and Y registers must be pushed onto the stack. This saves them so that they can be recovered before returning control to the operating system.
.irq LDA &FC PHA TXA PHA TYA PHA
Next we must check that the interrupt that has occurred is the one we want. We can look at the VIA for the answer to this.
LDA #2 BIT &FE4D BEQ notsync
We now have to deal with the occurrence of a video sync interrupt. We must set TIMER 2 in the system VIA to count for just long enough for the scanning beam in the monitor to reach the vertical blanking period, and then produce an interrupt. So that we can make the fine adjustments later, we use the variable T% to set the time. T% can then be tuned by trial and error to get the best flicker-free picture.
LDA &FE4B AND #&DF STA &FE4B LDA &FE4E ORA #&20 STA &FE4E LDA #T%MOD256 STA &FE48 LDA #T%DIV256 STA &FE49
Lastly we need to exit from the routine, remembering to replace all the registers as they were. It is VITAL that, when dealing with interrupts, the registers A, X and Y (and any variables or locations in memory that the main program may be using) hold the same values as they did when the interrupt occurred, before the interrupt-servicing routine passes control back the operating system.
.exit PLA TAY PLA TAX PLA STA &FC JMP (&230)
Now we must check to see if a TIMER 2 interrupt has occurred.
.notsync LDA #&20 BIT &FE4D BEQ exit
Then, when TIMER 2 interrupt occurs we must clear the interrupt status of the VIA ready for the next interrupt.
STA &FE4D
We are now ready to move the films. Moving the films will take most of the blanking period so the synchronisation interrupt will happen right in the middle of the films. For this reason we must ensure that the interrupts are enabled while the films are moved. However, we must not have changed the state of the interrupts when we exit the routine. The easiest way to ensure this is to push the processor status register on the stack before changing the interrupt status and then pull it back off again just before we exit the routine.
PHP CLI JSR films PLP JMP exit
We now have the complete routine. For simplicity of use we can dimension all the arrays inside the procedure that assembles the machine code. These arrays need not be longer than the maximum number of sprite shape tables and films that we are going to use, so we might as well specify these two numbers as procedure parameters. At the same time we can set the number of films in use (stored at &77) to the maximum number of films. If, at some point, we want to use fewer films than this number, then we can alter the contents of &77 after using the procedure. While we are bout it we may as well clear OLDS and NEWS, ready for use, by filling them with 255. We also need to set T% to a suitable value. This value you can experiment with to get the best results.
Here, then, is the complete sprite routine with films and flicker-prevention.
10000DEFPROCassemble(NF%,NS%) 10010?&77=NF% 10020NF%=NF%-1 10030NS%=NS%*4-1 10040T%=18000 10050DIMsprites NS%,olds NF%,oldx NF%,oldy NF%,news NF%,newx NF%,newy NF% 10060FORA%=0TONF% 10070olds?A%=255 10080news?A%=255 10090NEXT 10100DIMZ%400 10110FORpass%=0TO2STEP2 10120P%=Z% 10130[OPTpass% 10140.sprite STX &70 \ draw a sprite 10150 LDX #0 \ given sprite 10160 STX &71 \ NOT & XY 10170 ASL A \ coordinates 10180 ASL A 10190 TAX 10200 LDA sprites,X 10210 STA byte+1 10220 LDA sprites+1,X 10230 STA byte+2 10240 LDA sprites+2,X 10250 STA &72 10260 LDA sprites+3,X 10270 STA &73 10280 ASL &70 10290 ROL &71 10300 ASL &70 10310 ROL &71 10320 ASL &70 10330 ROL &71 10340 TYA 10350 AND #&F8 10360 LSR A 10370 LSR A 10380 TAX 10390 LDA &C376,X 10400 CLC 10410 ADC &70 10420 STA &70 10430 LDA &C375,X 10440 ADC &71 10450 CLC 10460 ADC #&30 10470 STA &71 10480 TYA 10490 AND #7 10500 STA &74 10510.row LDY &74 10520 LDX &72 10530.byte LDA &FFFF 10540 EOR (&70),Y 10550 STA (&70),Y 10560 TYA 10570 CLC 10580 ADC #8 10590 TAY 10600 INC byte+1 10610 BNE nocarry 10620 INC byte+2 10630.nocarry DEX 10640 BNE byte 10650 INC &74 10660 LDA &74 10670 CMP #8 10680 BNE notline 10690 LDA #0 10700 STA &74 10710 LDA &70 10720 CLC 10730 ADC #&80 10740 STA &70 10750 LDA &71 10760 ADC #2 10770 STA &71 10780 CMP #&80 10790 BCC notline 10800 SEC 10810 SBC #&50 10820 STA &71 10830.notline DEC &73 10840 BNE row 10850.rts RTS 10860 10870.films LDX &77 \ Move all films 10880.next DEX \ currently 10890 CPX #255 \ active 10900 BEQ rts 10910 LDA news,X 10920 CMP #255 10930 BEQ next 10940 LDA olds,X 10950 CMP #255 10960 BEQ newfilm 10970 STA &76 10980 LDA oldy,X 10990 TAY 11000 LDA oldx,X 11010 STX &75 11020 TAX 11030 LDA &76 11040 JSR sprite 11050 LDX &75 11060.newfilm LDA news,X 11070 STA olds,X 11080 STA &76 11090 LDA newy,X 11100 STA oldy,X 11110 TAY 11120 LDA newx,X 11130 STA oldx,X 11140 STX &75 11150 TAX 11160 LDA &76 11170 JSR sprite 11180 LDX &75 11190 LDA #255 11200 STA news,X 11210 JMP next 11220 11230.move STX &76 \ Now 11240 LDX &78 \ move a film in 11250 STA news,X \ next blanking 11260 TYA \ period given 11270 STA newy,X \ film no in &78 11280 LDA &76 \ shape in A 11290 STA newx,X \ coords in X & Y 11300 RTS 11310 11320.init SEI \ Init. all 11330 LDA &204 \ interrupts 11340 STA &230 \ needed for 11350 LDA &205 \ sprite routines 11360 STA &231 11370 LDA #irq MOD256 11380 STA &204 11390 LDA #irq DIV256 11400 STA &205 11410 LDA #&50 11420 STA &FE4E 11430 CLI 11440 RTS 11450 11460.irq LDA &FC \ Handle any 11470 PHA \ interrupts 11480 TXA \ that occur 11490 PHA 11500 TYA 11510 PHA 11520 LDA #2 11530 BIT &FE4D 11540 BEQ notsync 11550 LDA &FE4B 11560 AND #&DF 11570 STA &FE4B 11580 LDA &FE4E 11590 ORA #&20 11600 STA &FE4E 11610 LDA #T%MOD256 11620 STA &FE48 11630 LDA #T%DIV256 11640 STA &FE49 11650.exit PLA 11660 TAY 11670 PLA 11680 TAX 11690 PLA 11700 STA &FC 11710 JMP (&230) 11720.notsync LDA #&20 11730 BIT &FE4D 11740 BEQ exit 11750 STA &FE4D 11760 PHP 11770 CLI 11780 JSR films 11790 PLP 11800 JMP exit 11810] 11820NEXT 11830ENDPROC
First we go into Mode 2 and remove the cursor.
10 MODE 2 20 VDU23,1,0;0;0;0;
Then we need to assemble the machine code. We are only going to use one film and one sprite so these are the parameters we specify.
30 PROCassemble(1,1)
Now we need to set up the sprite table.
40 DIM man% 63 50 FOR A% = 0 TO 63 60 READ man%?A% 70 NEXT
Now we have to set up the parameters of this sprite shape (start of sprite shape table, width and height) in the right table.
80 !sprites = man% 90 sprites?2 = 4 100 sprites?3 = 16
We can now initialise the interrupts.
110 CALL init
Next we need to put the man on the screen. We can set both the film and the sprite number permanently to 0. And we can set up the initial X and Y coordinates. Then we can call the sprite routine to place the man on the screen.
120 ?&78 = 0 130 X% = 0 140 Y% = 0 150 A% = 0 160 CALL move
Next we want to check to see if any keys are being pressed, and alter the X and Y coordinates accordingly.
170 IF INKEY-98 AND X%>0 X%=X%-1 180 IF INKEY-67 AND X%<76 X%=X%+1 190 IF INKEY-105 AND Y%<240 Y%=Y%+4 200 IF INKEY-73 AND Y%>0 Y%=Y%-4
Now we can go back and place the film in its new position.
This sprite routine will only move the film every fiftieth of a second. If we try to move it more often than this, some positions will just be ignored and so the sprite will sometimes jump more than one position at a go. This means, of course, that we can move the film as fast or as slowly as we like and the sprite will still appear flicker free. For our purposes we want the sprite to move one jump every fiftieth of a second. To do this we can use *FX19 to synchronise the BASIC program with the screen.
210 *FX19 220 GOTO 160
If you try this program you will find that unlike the last program it is totally flicker-free and smooth.
Try deleting line 210. You will find that although the man moves a lot faster he appears to move jerkily. There is no flicker but the movement is not smooth. In fact the smoother speeds at which to move the man turn to be multiples of 50 moves a second. This means that the man moves exactly the same distance between each frame. So if you want to make a film move faster it is better to increase the distance it moves each time rather than to try and make more moves per second.
This program seems to be perfect for our needs. However, if you use this routine to move more than two or three films you will find that the flicker prevention begins to fail. There is no practical solution to this problem that also allows fifty moves a second for all the films over a full screen. The only way to beat it is to use a smaller amount of the screen. You will find that when flicker occurs it usually occurs only at the top or bottom of the screen. If you ensure that your sprites never move into these areas then your problems are solved. Obviously, this is not always practicable; but if, for instance, you have a 'space invaders' type base at the bottom of the screen it would make sense to make this film zero. This is because lower-number films are processed later (film 0 is processed last) and these will be the ones that are still being processed when the scan starts at the top of the screen so causing possible flicker. By ensuring that film 0 never goes anywhere near the top of the screen you ensure that it will not flicker. If you do have trouble, try altering the value of T% set at the beginning of the procedure as the best value for this will depend on your program. Beyond this the only advice I can give is to experiment. Do not take these routines as perfect - feel free to customise them to your needs.
10 MODE 7 20 PAGE=&3000 30 CHAIN"TENNIS1"
Save this 'loader' first as TENNIS.
Now we must write the assembly code. Because we are using the sprite routine for a specific purpose we can simplify it a bit. This is important to remember - WHEN USING A GENERAL ROUTINE FOR A SPECIFIC PURPOSE ALWAYS TRY AND SIMPLIFY IT SO THAT IT ONLY DOES WHAT IS NEEDED OF IT.
Because we know how many sprites and films we need we can set up the seven tables that the routine uses part of the assembly code. The table SPRITES will need to be eight bytes long to accommodate the two sprites. Each of the other six table will need to be three bytes long to accommodate the three films. We can also initialise the routine with no films active by setting up all the bytes of OLDS and NEWS to &FF. All this data can be added to the end of the assembly code.
.sprites EQUD 0 EQUD 0 .olds EQUW &FFFF EQUB &FF .oldx EQUW 0 EQUB 0 .oldy EQUW 0 EQUB 0 .news EQUW &FFFF EQUB &FF .newx EQUW 0 EQUB 0 EQUW 0 EQUB 0
This simplifies the first few lines of the assembler section of the program to:
10 ?&77=3 20 T%=18000 30 FORpass%=0TO2STEP2 40 P%=&2000 50 [OPTpass% 60 .sprite STX &70 .. ..
The assembly code part of the routine is as before (see LISTING 3).
One problem is that the third section - the BASIC part of the game - will need to refer to some of the labels in the assembly code, but CHAIN clears all normal variables. The answer to this problem is to copy the four labels we are going to need into integer variables A%, B%, C% and D% which are only cleared on CTRL BREAK. Then the first four lines of the BASIC section can copy them back into the normal variables for easy use.
Let's now look at the beginning of the BASIC section of the program and ignore the rest of the assembly code for a moment. As we have said, the first four lines must be:
10 sprites=A% 20 init=B% 30 move=C% 40 tennis=D%
Note that TENNIS is the extra assembly code routine we still have to write. Before we write this we should look at the main, BASIC, part of the program.
The next thing we need to do is set the PRINT format for printing the score (see page 325 of the USER GUIDE).
50 @%=3
Now we can go into Mode 2 and set HIMEM so as to safeguard the machine code that the second section of the program has placed from &2D00 to &2FFF. We can turn off the cursor and disable the copy cursor. (This otherwise tends to produce annoying results if the cursor keys are pressed while the game is being played.)
60 MODE2 70 HIMEM=&2D00 80 VDU23,1,0;0;0;0; 90 *FX4,1
The next job is to print the scores at the top of the screen in appropriate colours.
100 COLOUR1:COLOUR130 110 PRINT"SCORE:0 SCORE:0 "
Now we must draw the tennis court.
120 GCOL0,2:MOVE0,0:MOVE1279,0 130 PLOT85,0,12:PLOT85,1279,12 140 MOVE0,988:MOVE1279,988 150 PLOT85,0,976:PLOT85,1279,976 160 GCOL0,3:FORA%=24TO972STEP32 170 MOVE632,A%:DRAW632,A%+12 180 MOVE640,A%:DRAW640,A%+12 190 NEXT
We have two players, so we need two variables to store their scores. The best way to do this is with an array.
200 DIMscore(1)
Next we must set up the designs for the two sprites. For this game we only need very simple sprites. The bat can be made of a red rectangle two pixels (one byte) by 24 pixels (24 bytes). The ball will be a cyan square 2 pixels (1 byte) by four pixels (4 bytes). This mean that between them the two sprites need 28 bytes of storage. We can reserve these with the DIM command and set up the sprite with some simple BASIC.
210 DIMsp%28 220 FORA%=0TO23:sp%?A%=3:NEXT 230 sp%!24=&3C3C3C3C
Next we need to put the data on the sizes, and addresses, of these two sprite shapes in SPRITES.
240 !sprites=sp%+&18010000 250 sprites!4=sp%+24+&4010000
This makes the bat sprite 0 and the ball sprite 1. We can now initialise the sprite routine (the films have already been turned off in the assembler section).
260 CALLinit
We are now into the main section of the game. Let's specify some of the zero page locations we will use.
Locations &70 to &78 are used by the sprite routine. Let's say that &7E and &7F give the current X and Y coordinates of the ball. We can then say that &79 contains the current direction of the ball horizontally - if it is 1, then the ball is travelling to the right, if it is 255 the ball is travelling to the left. Location &7A can contain the vertical speed of the ball. Because we can position the ball more accurately vertically than horizontally we will make the ball travel left and right at a constant speed while travelling up and down at a variable speed. This will give a range of speeds and directions the ball can take. Location &7A will contain the number of pixels moved down for each position moved along. Positive numbers will be moving down, negative numbers will be moving up.
We now need to initialise these ready for a serve. We shall say that the end that the ball appears at is random and that if it appears on the left it will subsequently travel right, and vice versa. So we now have to decide which end the ball starts. We can set the horizontal direction to either 1 or -1 first and use this to set the X coordinate of the ball either to 0 or 79. The Y coordinate will always be 12 so that the ball starts at the top.
270 ?&79=RND(2)*2-3 280 ?&7E=(?&79=255)*-79 290 ?&7F=12
We can now set the vertical speed of the ball randomly between 0 and 3. Having done this we must place the ball on the screen for the players to see. The ball is film 0.
300 ?&7A=RND(4)-1 310 ?&78=0 320 A%=1 330 X%=?&7E 340 Y%=?&7F 350 CALLmove
The bats will be at either side of the screen and will only move up and down. We can say that the Y coordinate for the left-hand bat is in &7C and for the right-hand bat is in &7D. We will start both bats halfway up the screen and, again, we need to place them on the screen. The left-hand bat is film 1 and the right hand bat is film 2.
360 ?&7C=124 370 ?&7D=124 380 ?&78=1 390 A%=0 400 X%=0 410 Y%=?&7C 420 CALLmove 430 ?&78=2 440 X%=79 450 CALLmove
Now we have to start the game. We are going to do all the moment of the sprites from machine code. We still have to write the routine (TENNIS) which handles all the movement of ball and bats until one of the player misses the ball. When this happens the routine will exit with the number of the player who won that volley (0 for left and 1 for right) in &7B. So, we can call this routine, then update the score, then have a slight pause, and then serve the next ball. This gives us:
460 CALLtennis 470 score(?&7B)=score(?&7B)+1 480 PRINTTAB(6,0);score(0);TAB(17,0); score(1) 490 FORA%=1TO10000:NEXT 500 GOTO270
Now we must go back to the second section of the game and write TENNIS. The first thing this must to is wait for the vertical sync so as to make the routine move the films every fiftieth of a second. Then we must move the three sprites to their original positions.
.tennis LDA #19 JSR &FFF4 LDA #0 STA &78 LDA #1 LDX &7E LDY &7F JSR move
This moves the ball.
INC &78 LDA #0 LDX #0 LDY &7C JSR move INC &78 LDA #0 LDX #79 LDY &7D JSR move
This moves the two bats.
Next, we have to alter &7E and &7F to point to the next position of the ball. To move it horizontally all we have to do is add the horizontal direction and the sign will take care of which way the ball moves. We can do the same sort of thing for the vertical direction.
LDA &7E CLC ADC &79 STA &7E LDA &7F CLC ADC &7A STA &7F
Next we must check to see if the ball has hit the top wall of the court. If so, then the Y coordinate will be less than 12 so that we want the ball to bounce. First, it mustn't go through the wall, so we must reposition it to be just touching the wall. The horizontal direction must remain the same but the ball must begin to travel in the opposite direction vertically with the same speed as it hit the wall vertically. This means that we want to multiply the speed by -1. The easiest way to do this is to subtract it from 0.
CMP #12 BCS skip1 LDA #12 STA &7F LDA #0 SEC SBC &7A STA &7A
Next we must check whether the ball has hit the bottom wall of the court. We can deal with this in a similar way.
.skip1 LDA &7F CMP #249 BCC skip2 LDA #248 STA &7F LDA #0 SEC SBC &7A STA &7A
Next we need to check whether the ball has reached the left-hand side of the screen. If so, we need to check whether it has hit the bat. If the contents of &7F fall outside the range of ?&7C-4 to ?&7C+23 then the ball has missed the bat and the right-hand player has won. Otherwise, we must make the ball bounce off the bat.
.skip2 LDA &7E CLC ADC #4 CMP &7C BCC rwin LDA &7C CLC ADC #23 CMP &7F BCS skip3 .rwin LDA #1 STA &7B RTS .skip3
To make the ball bounce, we first have to make it travel to the right. We also have to set the vertical speed of the ball to a random value to simulate the range of shots a real player could play. We must, in a moment, write the routine RAND to do this.
.skip3 LDA #1 STA &79 JSR rand .skip4 ...
For RAND we need to call the random number routine from the BASIC ROM. For BASIC I this is at &AFB6 and for BASIC II this is at &AF87. This routine creates a 32-bit random number in zero page locations &0D to &11. We need a random number between -3 and 4. (The slight bias to the direction is not enough to matter and makes the coding easier.) To get this we need to take a three-bit random number between 0 and 7 and subtract 3. To get a three-bit random number we need only take any three bits of the 33-bit random number that BASIC generates.
.rand JSR &AFB7 LDA &D AND #7 SEC SBC #3 STA &7A RTS
We can now check for the ball reaching the right-hand side of the screen in the same way.
LDA &7E CMP #79 BNE skip6 LDA &7F CLC ADC #4 CMP &7D BCC lwin LDA &7D CLC ADC #23 CMP &7F BCS skip5 .lwin LDA #0 STA &7B RTS .skip5 LDA #255 STA &79 JSR rand
Next we must move the bats. Before we do this we can simplify matters by writing an INKEY routine. We will use the OSBYTE equivalent of INKEY with a negative number to test individual keys. For this we need to set A to &81. Y to &FF and X to the key number. We then call &FFF4 and compare X with &FF. We can do most of this in a subroutine.
.key LDA #&81 LDY #&FF JSR &FFF4 CPX #&FF RTS
If we jump to this with the key number in X, then, on exit, the zero flag will be set if the is pressed and be clear if it is not.
If the left-hand bat is not already at the bottom of the screen then we can test the Z key. If it is pressed, we can move the bat down four pixels.
.skip6 LDA &7C CMP #228 BEQ skip7 LDX #&9E JSR key BNE skip7 LDA &7C CLC ADC #4 STA &7C
We can do the same sort of thing for moving up under control of the A key, provided that the bat isn't already at the top.
.skip7 LDA &7C CMP #12 BEQ skip8 LDX #&BE JSR key BNE skip8 LDA &7C SEC SBC #4 STA &7C
We can then do the same for the right-hand bat under control of the ] and SHIFT keys.
.skip8 LDA &7D CMP #228 BEQ skip9 LDX #&FF JSR key BNE skip9 LDA &7D CLC ADC #4 STA &7D .skip9 LDA &7D CMP #12 BEQ skip10 LDX #&A7 JSR key BNE skip10 LDA &7D SEC SBC #4 STA &7D
Finally, we need to go back to the beginning and repeat the whole process over and again until someone misses the ball.
.skip10 JMP tennis
This, of course, is a very simple game; but it illustrates the techniques used for much more complicated games.
Here is a listing of all three sections of the program. Type in Listing 4 and save it as TENNIS; then type in Listing 5 and save it as TENNIS1; and then type in Listing 6 and save it as TENNIS2.
By this stage you should have become reasonably familiar with assembly code. You might like to try and follow these three listings, as I have purposely omitted the comments, and work out how they work for yourself. If you get lost, refer back to the program descriptions in this chapter.
10 MODE 7 20 PAGE=&3000 30 CHAIN"TENNIS1"
10 ?&77=3 20 T%=18000 30 FORpass%=0TO2STEP2 40 P%=&2000 50 [OPTpass% 60 .sprite STX &70 70 LDX #0 80 STX &71 90 ASL A 100 ASL A 110 TAX 120 LDA sprites,X 130 STA byte+1 140 LDA sprites+1,X 150 STA byte+2 160 LDA sprites+2,X 170 STA &72 180 LDA sprites+3,X 190 STA &73 200 ASL &70 210 ROL &71 220 ASL &70 230 ROL &71 240 ASL &70 250 ROL &71 260 TYA 270 AND #&F8 280 LSR A 290 LSR A 300 TAX 310 LDA &C376,X 320 CLC 330 ADC &70 340 STA &70 350 LDA &C375,X 360 ADC &71 370 CLC 380 ADC #&30 390 STA &71 400 TYA 410 AND #7 420 STA &74 430 .row LDY &74 440 LDX &72 450 .byte LDA &FFFF 460 EOR (&70),Y 470 STA (&70),Y 480 TYA 490 CLC 500 ADC #8 510 TAY 520 INC byte+1 530 BNE nocarry 540 INC byte+2 550 .nocarry DEX 560 BNE byte 570 INC &74 580 LDA &74 590 CMP #8 600 BNE notline 610 LDA #0 620 STA &74 630 LDA &70 640 CLC 650 ADC #&80 660 STA &70 670 LDA &71 680 ADC #2 690 STA &71 700 CMP #&80 710 BCC notline 720 SEC 730 SBC #&50 740 STA &71 750 .notline DEC &73 760 BNE row 770 .rts RTS 780 .films LDX &77 790 .next DEX 800 CPX #255 810 BEQ rts 820 LDA news,X 830 CMP #255 840 BEQ next 850 LDA olds,X 860 CMP #255 870 BEQ newfilm 880 STA &76 890 LDA oldy,X 900 TAY 910 LDA oldx,X 920 STX &75 930 TAX 940 LDA &76 950 JSR sprite 960 LDX &75 970 .newfilm LDA news,X 980 STA olds,X 990 STA &76 1000 LDA newy,X 1010 STA oldy,X 1020 TAY 1030 LDA newx,X 1040 STA oldx,X 1050 STX &75 1060 TAX 1070 LDA &76 1080 JSR sprite 1090 LDX &75 1100 LDA #255 1110 STA news,X 1120 JMP next 1130 .move STX &76 1140 LDX &78 1150 STA news,X 1160 TYA 1170 STA newy,X 1180 LDA &76 1190 STA newx,X 1200 RTS 1210 .init SEI 1220 LDA &204 1230 STA &230 1240 LDA &205 1250 STA &231 1260 LDA #irq MOD256 1270 STA &204 1280 LDA #irq DIV256 1290 STA &205 1300 LDA #&50 1310 STA &FE4E 1320 CLI 1330 RTS 1340 .irq LDA &FC 1350 PHA 1360 TXA 1370 PHA 1380 TYA 1390 PHA 1400 LDA #2 1410 BIT &FE4D 1420 BEQ notsync 1430 LDA &FE4B 1440 AND #&DF 1450 STA &FE4B 1460 LDA &FE4E 1470 ORA #&20 1480 STA &FE4E 1490 LDA #T%MOD256 1500 STA &FE48 1510 LDA #T%MOD256 1520 STA &FE49 1530 .exit PLA 1540 TAY 1550 PLA 1560 TAX 1570 PLA 1580 STA &FC 1590 JMP (&230) 1600 .notsync LDA #&20 1610 BIT &FE4D 1620 BEQ exit 1630 STA &FE4D 1640 PHP 1650 CLI 1660 JSR films 1670 PLP 1680 JMP exit 1690 .sprites EQUD 0 1700 EQUD 0 1710 .olds EQUW &FFFF 1720 EQUB &FF 1730 .oldx EQUW 0 1740 EQUB 0 1750 .oldy EQUW 0 1760 EQUB 0 1770 .news EQUW &FFFF 1780 EQUB &FF 1790 .newx EQUW 0 1800 EQUB 0 1810 .newy EQUW 0 1820 EQUB 0 1830 .tennis LDA #19 1840 JSR &FFF4 1850 LDA #0 1860 STA &78 1870 LDA #1 1880 LDX &7E 1890 LDY &7F 1900 JSR move 1910 INC &78 1920 LDA #0 1930 LDX #0 1940 LDY &7C 1950 JSR move 1960 INC &78 1970 LDA #0 1980 LDX #79 1990 LDY &7D 2000 JSR move 2010 LDA &7E 2020 CLC 2030 ADC &79 2040 STA &7E 2050 LDA &7F 2060 CLC 2070 ADC &7A 2080 STA &7F 2090 CMP #12 2100 BCS skip1 2110 LDA #12 2120 STA &7F 2130 LDA #0 2140 SEC 2150 SBC &7A 2160 STA &7A 2170 .skip1 LDA &7F 2180 CMP #249 2190 BCC skip2 2200 LDA #248 2210 STA &7F 2220 LDA #0 2230 SEC 2240 SBC &7A 2250 STA &7A 2260 .skip2 LDA &7E 2270 BNE skip4 2280 LDA &7F 2290 CLC 2300 ADC #4 2310 CMP &7C 2320 BCC rwin 2330 LDA &7C 2340 CLC 2350 ADC #23 2360 CMP &7F 2370 BCS skip3 2380 .rwin LDA #1 2390 STA &7B 2400 RTS 2410 .skip3 LDA #1 2420 STA &79 2430 JSR rand 2440 .skip4 LDA &7E 2450 CMP #79 2460 BNE skip6 2470 LDA &7F 2480 CLC 2490 ADC #4 2500 CMP &7D 2510 BCC lwin 2520 LDA &7D 2530 CLC 2540 ADC #23 2550 CMP &7F 2560 BCS skip5 2570 .lwin LDA #0 2580 STA &7B 2590 RTS 2600 .skip5 LDA #255 2610 STA &79 2620 JSR rand 2630 .skip6 LDA &7C 2640 CMP #228 2650 BEQ skip7 2660 LDX #&9E 2670 JSR key 2680 BNE skip7 2690 LDA &7C 2700 CLC 2710 ADC #4 2720 STA &7C 2730 .skip7 LDA &7C 2740 CMP #12 2750 BEQ skip8 2760 LDX #&BE 2770 JSR key 2780 BNE skip8 2790 LDA &7C 2800 SEC 2810 SBC #4 2820 STA &7C 2830 .skip8 LDA &7D 2840 CMP #228 2850 BEQ skip9 2860 LDX #&FF 2870 JSR key 2880 BNE skip9 2890 LDA &7D 2900 CLC 2910 ADC #4 2920 STA &7D 2930 .skip9 LDA &7D 2940 CMP #12 2950 BEQ skip10 2960 LDX #&A7 2970 JSR key 2980 BNE skip10 2990 LDA &7D 3000 SEC 3010 SBC #4 3020 STA &7D 3030 .skip10 JMP tennis 3040 .rand JSR &AFB7 3050 LDA &D 3060 AND #7 3070 SEC 3080 SBC #3 3090 STA &7A 3100 RTS 3110 .key LDA #&81 3120 LDY #&FF 3130 JSR &FFF4 3140 CPX #&FF 3150 RTS 3160 ] 3170 NEXT 3180 A%=sprites 3190 B%=init 3200 C%=move 3210 D%=tennis 3220 PAGE=&1900 3230 CHAIN"TENNIS2"
10 sprites=A% 20 init=B% 30 move=C% 40 tennis=D% 50 @%=3 60 MODE2 70 HIMEM=&2D00 80 VDU23,1,0;0;0;0; 90 *FX4,1 100 COLOUR1:COLOUR130 110 PRINT"SCORE:0 SCORE:0 " 120 GCOL0,2:MOVE0,0:MOVE1279,0 130 PLOT85,0,12:PLOT85,1279,12 140 MOVE0,988:MOVE1279,988 150 PLOT85,0,976:PLOT85,1279,976 160 GCOL0,3:FORA%=24TO972STEP32 170 MOVE632,A%:DRAW632,A%+12 180 MOVE640,A%:DRAW640,A%+12 190 NEXT 200 DIMscore(1) 210 DIMsp%28 220 FORA%=0TO23:sp%?A%=3:NEXT 230 sp%!24=&3C3C3C3C 240 !sprites=sp%+&18010000 250 sprites!4=sp%+24+&4010000 260 CALLinit 270 ?&79=RND(2)*2-3 280 ?&7E=(?&79=255)*-79 290 ?&7F=12 300 ?&7A=RND(4)-1 310 ?&78=0 320 A%=1 330 X%=?&7E 340 Y%=?&7F 350 CALLmove 360 ?&7C=124 370 ?&7D=124 380 ?&78=1 390 A%=0 400 X%=0 410 Y%=?&7C 420 CALLmove 430 ?&78=2 440 X%=79 450 CALLmove 460 CALLtennis 470 score(?&7B)=score(?&7B)+1 480 PRINTTAB(6,0);score(0);TAB(17,0);score(1) 490 FORA%=1TO10000:NEXT 500 GOTO270