- 2/14/2017 – Added some section headers, bolded benchmark values.
Since I am right in the middle of a multi-part article on interfacing assembly with BASIC, now is a great time to discuss something completely different.
INPUT, INKEY, INSTR, and POKE
The reason I do this now is because it is going to tie it in with the next part of the assembly article. Since I have been discussing using assembly to speed things up, it is a good time to address a few more things that can be done to speed up BASIC before resorting to 6809 code. Since BASIC will be the weakest link, we should try to make it as strong weak link.
In 1980, Color BASIC offered a simple way to input a string or number:
10 INPUT "WHAT IS YOUR NAME";A$ 20 PRINT "HELLO, ";A$ 30 INPUT "HOW OLD ARE YOU";A 40 PRINT A;"IS PRETTY OLD."
The original INPUT command was a very simple way to get data in to a program, but it was quite limited. It didn’t allow for string input containing commas, for instance, unless you typed it in quotes:
INPUT also prints the question mark prompt.
When Extended Color BASIC was introduced, it brought many new features including the LINE INPUT command. This command did not force the question mark prompt, and would accept commas without quoting it:
If you were trying to write a text based program (or a text adventure game), INPUT or LINE INPUT would be fine.
For times when you just wanted to get one character, without requiring the characters to be echoed as the user types them, and without requiring the user to press ENTER, there was INKEY$.
INKEY$ returns whatever key is being pressed, or nothing (“”) if no key is ready.
10 PRINT "PRESS ANY KEY TO CONTINUE..." 20 IF INKEY$="" THEN 20 30 PRINT "THANK YOU."
It can also be used with a variable:
10 PRINT "ARE YOU READY? "; 20 A$=INKEY$:IF A$="" THEN 20 30 IF A$="Y" THEN PRINT "GOOD!" ELSE PRINT "BAD."
This is the method we might use for a keyboard-controlled BASIC video game. For instance, if we want to read the arrow keys (up, down, left and right), each one of those keys generates an ASCII character when pressed:
UP - CHR$(94) - ^ character DOWN - CHR$(10) - line feed LEFT - CHR$(8) - backspace RIGHT - CHR$(9) - tab
Knowing this, we can detect arrow keys using INKEY$:
10 CLS:P=256+16 20 PRINT@P,"*"; 30 A$=INKEY$:IF A$="" THEN 30 40 IF A$=CHR$(94) AND P>31 THEN P=P-32 50 IF A$=CHR$(10) AND P<479 THEN P=P+32 60 IF A$=CHR$(8) AND P>0 THEN P=P-1 70 IF A$=CHR$(9) AND P<510 THEN P=P+1 80 GOTO 20
The above program uses PRINT@ to print an asterisk (“*”) in the middle of the screen. Then, it waits until a key is pressed (line 30). Once a key is pressed, it looks at which key it was (up, down, left or right) and then will move the position of the asterisk (assuming it’s not going off the end of the screen).
Side Note: The CoCo’s text screen is 32×16 (512 characters). PRINT@ can print at 0-511, but if you print to 511 (the bottom right location), the screen will scroll up. I have adjusted this code to disallow moving the asterisk to that location.
You now have a really crappy text drawing program. To make it less crappy, you could check for other keys to change the character that is being drawn, or make it use simple color graphics:
10 CLS0:X=32:Y=16:C=0 20 SET(X,Y,C) 30 A$=INKEY$:IF A$="" THEN 30 40 IF A$=CHR$(94) AND Y>0 THEN Y=Y-1 50 IF A$=CHR$(10) AND Y<31 THEN Y=Y+1 60 IF A$=CHR$(8) AND X>0 THEN X=X-1 70 IF A$=CHR$(9) AND X<63 THEN X=X+1 80 IF A$="C" THEN C=C+1:IF C>8 THEN C=0 90 GOTO 20
That program uses the primitive SET command to draw in beautiful 64×32 resolution with eight colors. The arrow keys move the pixel, and pressing C toggles through the colors. Spiffy!
Instead of using “C” to just cycle through the colors, you could check the character returned and see if it was between “0” and “8” and use that value to set the color (0=RESET pixel, 1-8=SET pixel to color).
10 CLS0:X=32:Y=16:C=1 20 IF C=0 THEN RESET(X,Y) ELSE SET(X,Y,C) 30 A$=INKEY$:IF A$="" THEN 30 40 IF A$=CHR$(94) AND Y>0 THEN Y=Y-1 50 IF A$=CHR$(10) AND Y<31 THEN Y=Y+1 60 IF A$=CHR$(8) AND X>0 THEN X=X-1 70 IF A$=CHR$(9) AND X<63 THEN X=X+1 80 IF A$=>"0" AND A$<="8" THEN C=ASC(A$)-48 90 GOTO 20
Now that we have refreshed our 1980 BASIC programming, let’s look at lines 40-70 which are used to determine which key has been pressed.
If we are going to be reading the keyboard over and over for an action game, doing so with a bunch of IF/THEN statements is not very efficient. Lets do some tests to find out how not very efficient it is.
For our example, we would be using INKEY$ to read a keypress, then GOSUBing to four different subroutines to handle up, down, left and right actions. To see how fast this is, we will once again use the TIMER command and do our test 1000 times. We’ll skip doing the actual INKEY$ for now, and hard code a keypress. Since we will be checking for keys in the order of up, down, left then right, we will simulate pressing last key check, right, to get the worst possible condition.
Here is version 1 that does a brute-force check using IF/THEN/ELSE.
0 REM KEYBD1.BAS 10 TM=TIMER:FORA=1TO1000 15 A$=CHR$(9) 20 REM A$=INKEY$:IFA$=""THEN20 30 IFA$=CHR$(94)THENGOSUB100ELSEIFA$=CHR$(10)THENGOSUB200ELSEIFA$=CHR$(8)THENGOSUB300ELSEIFA$=CHR$(9)THENGOSUB400 50 NEXT:PRINT TIMER-TM 60 END 100 RETURN 200 RETURN 300 RETURN 400 RETURN
When I run this in the XRoar emulator, I get back 1821. That is now our benchmark to beat.
Rather than doing a bunch of IF/THENs, if we are using Extended Color BASIC, there is the INSTR command. It will take a string and a pattern, and return the position of that pattern in the string. For example:
PRINT INSTR("CAT DOG RAT", "DOG")
If you run this line, it will print 5. The string “DOG” appears in “CAT DOG RAT” starting at position 5. You can use INSTR to parse single characters, too:
PRINT INSTR("ABCDEFGHIJ", "F")
This will print 6, because “F” is found in the search string starting at position 6.
If the search string is not found, it returns 0. Using this, you can parse a string containing all the possible keypress options, and turn them in to a number. You could then use that number in an ON GOTO/GOSUB statement, like this:
0 REM KEYBD2.BAS 10 A$=INKEY$:IF A$="" THEN 10 20 A=INSTR("ABCD",A$) 30 IF A=0 THEN 10 40 ON A GOSUB 100,200,300,400 50 GOTO 10 100 PRINT "A WAS PRESSED":RETURN 200 PRINT "B WAS PRESSED":RETURN 300 PRINT "C WAS PRESSED":RETURN 400 PRINT "D WAS PRESSED":RETURN
A long line of four IF/THEN/ELSE statements is now replaced by INSTR and ON GOTO/GOSUB.
Let’s rewrite our test program slightly, this time using INSTR:
10 TM=TIMER:FORA=1TO1000 15 A$=CHR$(9) 20 REM A$=INKEY$:IFA$=""THEN20 30 LN=INSTR(CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9),A$) 35 IF LN=0 THEN 20 40 ONLN GOSUB100,200,300,400 50 NEXT:PRINT TIMER-TM 60 END 100 RETURN 200 RETURN 300 RETURN 400 RETURN
Running this gives me 1724. We are now slightly faster.
We can do better.
One of the reasons this version is so slow is line 30. Every time that line is processed, BASIC has to dynamically build a string containing the four target characters — CHR$(94), CHR$(10), CHR$(8) and CHR$(9). String manipulation in BASIC is slow, and we really don’t need to do it every time. Instead, let’s try a version 3 where we create a string containing those characters at the start, and just use the string later:
0 REM KEYBD3.BAS 5 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9) 10 TM=TIMER:FORA=1TO1000 15 A$=CHR$(9) 20 REM A$=INKEY$:IFA$=""THEN20 30 LN=INSTR(KB$,A$) 35 IF LN=0 THEN 20 40 ONLN GOSUB100,200,300,400 50 NEXT:PRINT TIMER-TM 60 END 100 RETURN 200 RETURN 300 RETURN 400 RETURN
Running this gives me 902! It appears to be twice as fast as the original IF/THEN version!
Now we have a much faster way to handle the arrow keys. Let’s go back to the original program and update it:
0 REM INKEY3.BAS 5 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9) 10 CLS:P=256+16 20 PRINT@P,"*"; 30 A$=INKEY$:IF A$="" THEN 30 40 LN=INSTR(KB$,A$) 50 ONLN GOSUB100,200,300,400 60 GOTO 20 100 IF P>31 THEN P=P-32 110 RETURN 200 IF P<479 THEN P=P+32:RETURN 210 RETURN 300 IF P>0 THEN P=P-1 310 RETURN 400 IF P<510 THEN P=P+1 410 RETURN
Side Note: Using GOSUB/RETURN may be slower than using GOTO, but that will be the subject of another installment.
Now that we have a faster keyboard input routine, let’s do one more thing to try to speed it up.
We are currently using PRINT@ to print a character on the screen in positions 0-510 (remember, we can’t print to the bottom right position because that will make the screen scroll). Instead of using PRINT, we can also use POKE to put a byte directly in to screen memory:
Location is an address in the up-to-64K memory space (0-65535) and value is an 8-bit value (0-255).
Let’s see if it’s faster.
First, PRINT@ wants positions 0-511, and POKE wants an actual memory address. The 32 column screen is located from 1024-1535 in memory, so PRINT@0 is like POKE 1024. PRINT@511 is like POKE 1535. Let’s make some changes:
0 REM INKEY4.BAS 5 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9) 10 CLS:P=1024+256+16 20 POKE P,106 30 A$=INKEY$:IF A$="" THEN 30 40 LN=INSTR(KB$,A$) 50 ONLN GOSUB100,200,300,400 60 GOTO 20 100 IF P>1024+31 THEN P=P-32 110 RETURN 200 IF P<1024+479 THEN P=P+32:RETURN 210 RETURN 300 IF P>1024+0 THEN P=P-1 310 RETURN 400 IF P<1024+511 THEN P=P+1 410 RETURN
WARNING: While PRINT@ is safe (a bad value just generates an error), POKE is dangerous! If you POKE the wrong value, you could crash the computer. Instead of POKEing a character on the screen, you could accidentally POKE to memory that could crash the system.
This program will behave identically to the original, BUT since we are using POKE, we can now go all the way to the bottom right of the screen :) That is just one of the reasons we might use POKE over PRINT@.
But is it faster, or slower? Let’s find out…
10 TM=TIMER:FORA=1TO1000 20 PRINT@0,"*"; 30 NEXT:PRINT TIMER-TM
10 TM=TIMER:FORA=1TO1000 20 POKE1024,106 30 NEXT:PRINT TIMER-TM
The PRINT@ version shows 259, and the POKE version shows 655. POKE appears to be significantly slower. Some reasons could be:
- POKE has to translate four digits (1024) instead of just one (0) so that’s longer to parse.
- POKE has to also translate the value (106) where PRINT can probably just jump to the string that is in the quotes.
Let’s try to test this… By giving PRINT@ a three digit number, 510, it slows down from 259 to 424. Parsing that number is definitely part of the problem. Let’s eliminate the number parsing completely by using variables:
5 P=0 10 TM=TIMER:FORA=1TO1000 20 PRINT@P,"*"; 30 NEXT:PRINT TIMER-TM
This gives us 229, so it’s a bit faster than the original. Now let’s try the POKE version:
5 P=1024: 10 TM=TIMER:FORA=1TO1000 20 POKEP,106 30 NEXT:PRINT TIMER-TM
This gives us 400, so it’s faster than the original 655, but still nearly twice as slow as using PRINT@. But wait, there’s still that 106 value. Let’s replace that with a variable, too.
5 P=1024:V=106 10 TM=TIMER:FORA=1TO1000 20 POKEP,V 30 NEXT:PRINT TIMER-TM
This slows it down from 229 to 234! We are now almost as fast as PRINT@! But now the POKE version has to look up two variables, while the PRINT@ version only looks up one, so that might give PRINT@ an advantage. Let’s test this by making the PRINT@ version also use a variable for the character:
5 P=0:V$="*" 10 TM=TIMER:FORA=1TO1000 20 PRINT@P,V$; 30 NEXT:PRINT TIMER-TM
That slows it down to 231. This seems to indicate the speed difference is really not between PRINT@ and POKE, but between how much number conversion of variable lookup each needs to do. You can use PRINT@ without having to look up the string to print (“*”), but POKE always has to either convert a numeric value (106) or do a variable lookup (L).
So why bother with POKE if the only advantage, so far, is that you can POKE to the bottom right character on the screen?
PEEK lets us see what byte is at a specified memory location. If I were writing a game and wanted to tell if the player’s character ran in to an enemy, I’d have to compare the player position (X/Y address, or PRINT@ location) with the locations of all the other objects. The more objects you have, the more compares you have to do and the slower your program becomes.
For example, here’s a simple game where the player (“*”) has to avoid four different enemies (“X”):
0 REM GAME.BAS 5 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9) 10 CLS:P=256+16 15 E1=32:E2=63:E3=448:E4=479 20 PRINT@P,"*";:PRINT@E1,"X";:PRINT@E2,"X";:PRINT@E3,"X";:PRINT@E4,"X"; 30 A$=INKEY$:IF A$="" THEN 30 40 LN=INSTR(KB$,A$):IF LN=0 THEN 30 45 PRINT@P," "; 50 ONLN GOSUB100,200,300,400 60 IF P=E1 OR P=E2 OR P=E3 OR P=E4 THEN 90 80 GOTO 20 90 PRINT@267,"GAME OVER!":END 100 IF P>31 THEN P=P-32 110 RETURN 200 IF P<479 THEN P=P+32:RETURN 210 RETURN 300 IF P>0 THEN P=P-1 310 RETURN 400 IF P<510 THEN P=P+1 410 RETURN
In this example, the four enemies (“X”) remain static in the corners, but if you move your player (“*”) in to one, the game will end.
It’s not much of a game, but with a few more lines you could make the enemies move around randomly or chase the player.
Take a look at line 60. Every move we have to compare the position of the player with four different enemies. This is a rather brute-force check. We could also use an array for the enemies. Not only would this simplify our code, but it would make the number of enemies dynamic.
Here is a version that lets you have as many enemies as you want. Just set the value of EN in line number 1 to the number of enemies-1.
Side Note: Arrays are base-0, so if you DIM A(10) you get 11 elements — A(0) through A(10). Thus, if you want ten elements in an array, you would do DIM A(9), and cycle through them using base-0 like FOR I=0 TO 9:PRINT A(I):NEXT I.
0 REM GAME2.BAS 1 EN=10-1 'ENEMIES 2 DIM E(EN) 5 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9) 10 CLS:P=256+16 15 FOR A=0 TO EN:E(A)=RND(510):NEXT 20 PRINT@P,"*"; 25 FOR A=0 TO EN:PRINT@E(A),"X";:NEXT 30 A$=INKEY$:IF A$="" THEN 30 40 LN=INSTR(KB$,A$):IF LN=0 THEN 30 45 PRINT@P," "; 50 ONLN GOSUB100,200,300,400 60 FOR A=0 TO EN:IF P=E(A) THEN 90 ELSE NEXT 80 GOTO 20 90 PRINT@267,"GAME OVER!":END 100 IF P>31 THEN P=P-32 110 RETURN 200 IF P<479 THEN P=P+32:RETURN 210 RETURN 300 IF P>0 THEN P=P-1 310 RETURN 400 IF P<510 THEN P=P+1 410 RETURN
Now the program is more flexible, but it has gotten slower. After every move, the code must now compare locations of every enemy. This limits BASIC from being able to do a fast game with a ton of objects.
Which brings me back to PEEK… Instead of comparing the player against every enemy, all we really need to know is if the location of the player is where an enemy is. If we are using POKE to put the player on the screen, we know the location the player is, and can just PEEK that location to see if anything is there.
Let’s change the program to use POKE and PEEK:
0 REM GAME2.BAS 1 EN=10-1 'ENEMIES 2 DIM E(EN) 5 KB$=CHR$(94)+CHR$(10)+CHR$(8)+CHR$(9) 10 CLS:P=1024+256+16:V=106:VS=96:VE=88 15 FOR A=0 TO EN:E(A)=1024+RND(511):NEXT 20 POKEP,V 25 FOR A=0 TO EN:POKEE(A),VE:NEXT 30 A$=INKEY$:IF A$="" THEN 30 40 LN=INSTR(KB$,A$):IF LN=0 THEN 30 45 POKEP,VS 50 ONLN GOSUB100,200,300,400 60 IF PEEK(P)=VE THEN 90 80 GOTO 20 90 PRINT@267,"GAME OVER!":END 100 IF P>1024+31 THEN P=P-32 110 RETURN 200 IF P<1024+479 THEN P=P+32:RETURN 210 RETURN 300 IF P>1024+0 THEN P=P-1 310 RETURN 400 IF P<1024+510 THEN P=P+1 410 RETURN
Now, instead of looping through an array containing the locations of all the enemies, we simply PEEK to our new player location and if the byte there is our enemy value (VE, character 88), game over.
It should be a bit faster now.
We should also change the “1024+XX” things t0 the actual values to avoid doing math each time, but I was being lazy.
Now we know a way to improve the speed of reading key presses, and ways to use POKE/PEEK to avoid having to do manual comparisons of object locations. Maybe this will come in handy someday when you need to write a game where an asterisk is being chased by a bunch of Xs.
Until next time…