See also: part 1, part 2, part 3, part 4, part 5, part 6, part 7 and more (coming “soon”).
NOTE: This one is gonna jump around a bit, referring to examples in the previous installment, so hang tight…
Some comments from the previous installment, where I shared some code that wasn’t speeding up like I thought it should when I replaced hard-coded values with variables. As the post when live, I read through it and noticed my mistake. I added a comment to see if anyone else could spot it:
0 REM arrowbench9.bas
5 V=&HF7:U=&H155:D=&H156:L=&H157:R=&H158
10 TIMER=0:FOR A=1 TO 1000
20 POKEU,V:POKED,V:POKEL,V:POKER,V
30 IF PEEK(U)=V THEN IF Y>.THEN Y=Y-1:GOTO90
40 IF PEEK(D)=V THEN IF Y<&HDF THEN Y=Y+1:GOTO90
50 IF PEEK(L)=V THEN IF X>.THEN X=X-1:GOTO90
60 IF PEEK(R)=V THEN IF X<&HFE THEN X=X+1:GOTO90
90 NEXT:PRINT TIMER
craig immediately chimed in with the issue:
In arrowbench9.bas it pokes $F7 instead of $FF ?
In test, shouldn’t line 10 should use same as arrowbench9.bas line 20 ?
Can you use ‘next’ instead of ‘goto90’ ?
– craig
Right off the bat, craig noticed my mistake. In like 20, it’s supposed to POKE those four keyboard column values to 255 (&HFF). But, when I substituted the variables, I did not make a variable for 255 — instead, I incorrectly used the V variable which was the &HF7 value the PEEK would change to when that key was being held down. Oops! Thus, I was never resetting it so it was, apparently, always acting as if the key was being held down, processing all the variable X/Y stuff every time.
A fix to the original arrowbench2.bas might look like this:
0 REM arrowbench10.bas
5 Z=&HFF:V=&HF7:U=&H155:D=&H156:L=&H157:R=&H158
10 TIMER=0:FOR A=1 TO 1000
20 POKEU,Z:POKED,Z:POKEL,Z:POKER,Z
30 IF PEEK(U)=V THEN IF Y>.THEN Y=Y-1
40 IF PEEK(D)=V THEN IF Y<&HDF THEN Y=Y+1
50 IF PEEK(L)=V THEN IF X>.THEN X=X-1
60 IF PEEK(R)=V THEN IF X<&HFE THEN X=X+1
90 NEXT:PRINT TIMER
Now the keyboard table is properly reset back to 255 before each scan. While the original arrowbench2.bas reported 2890, this version (using the properly variable to reset those locations) reports 2351 and is indeed faster. My bad.
As to the other comments, that was yet another typo, where I created variables then forgot to use them:
5 V=&HF7:U=&H155:D=&H156:L=&H157:R=&H158
10 POKE&H155,&HFF:POKE&H156,&HFF:POKE&H157,&HFF:POKE&H158,&HFF
20 PRINT HEX$(PEEK(U))" "HEX$(PEEK(D))" "HEX$(PEEK(L))" "HEX$(PEEK(R)),HEX$(PEEK(&H155))" "HEX$(PEEK(&H156))" "HEX$(PEEK(&H157))" "HEX$(PEEK(&H158))
30 GOTO 10
…should have used U/D/L/R and a new Z in line 10, just like the example before.
As to using “NEXT” instead of “GOTO 90”, this refers to trying to bypass additional checks if one key is satisfied:
0 REM arrowbench11.bas
5 Z=&HFF:V=&HF7:U=&H155:D=&H156:L=&H157:R=&H158
10 TIMER=0:FOR A=1 TO 1000
20 POKEU,Z:POKED,Z:POKEL,Z:POKER,Z
30 IF PEEK(U)=V THEN IF Y>.THEN Y=Y-1:GOTO90
40 IF PEEK(D)=V THEN IF Y<&HDF THEN Y=Y+1:GOTO90
50 IF PEEK(L)=V THEN IF X>.THEN X=X-1:GOTO90
60 IF PEEK(R)=V THEN IF X<&HFE THEN X=X+1:GOTO90
90 NEXT:PRINT TIMER
In this benchmark example, I did GOTO 90 to go to the “end of what we are timing” line. But if this was being used in a program, a NEXT would have been faster than scanning forward to find line 90 and then doing the NEXT. BUT, if I tried that here, when NEXT was done (1 to 1000), it would not return and would go to the next line — and if that was 40, 50 or 60, it would do the check then try a NEXT and error with a “?NF ERROR” (next without for).
But, the point is well made — NEXT with a check after it could even be faster than a GOTO (scanning lines) to a next. That would be a fun benchmark.
Faster, even.
craig also pointed out an interesting optimization… Rather than clear all four keyboard values, whether they need it or not, why not just clear the one(s) that changed?
Faster.. what about moving line 20 out of the loop and only poking the matching peeks?
IF PEEK(U)=V THEN POKEU,F:IF Y>.THEN Y=Y-1:GOTO90
F=&HFF
For diagonals, change to GOTO50 on line 30.
– craig
Line 60 needs no GOTO90
This is worthy of an updated benchmark. Let’s take arrowbench10.bas above (2351) and modify it like this:
0 REM arrowbench12.bas
5 Z=&HFF:V=&HF7:U=&H155:D=&H156:L=&H157:R=&H158
10 TIMER=0:FOR A=1 TO 1000
30 IF PEEK(U)=V THENPOKEU,Z:IF Y>.THEN Y=Y-1
40 IF PEEK(D)=V THENPOKED,Z:IF Y<&HDF THEN Y=Y+1
50 IF PEEK(L)=V THENPOKEL,Z:IF X>.THEN X=X-1
60 IF PEEK(R)=V THENPOKER,Z:IF X<&HFE THEN X=X+1
90 NEXT:PRINT TIMER
Now it only clears the value if it was changed. This produces a value of … 1603! We have a winner! Great improvement, craig!
Also, when I added the “GOTO 90” at the end of the example, to bypass the other checks (eliminating the possibility of diagonals), craig is suggesting the code could simply skip checking DOWN if we had an UP (GOTO 50 moves to the Left/Right check next) and, likewise, like 50 could GOTO 90 to skip over the Right check. Thus:
- If UP is pressed…
- Check for LEFT. If LEFT is pressed…
- Exit checks. We have an UP and a LEFT
- else Check for RIGHT. If RIGHT is pressed…
- Exit checks. We have an UP and RIGHT.
- else Exit checks. We have UP.
- Check for LEFT. If LEFT is pressed…
- else check DOWN. If DOWN is pressed…
- Check for LEFT. If LEFT is pressed…
- Exit checks. We have DOWN and LEFT
- else Check for RIGHT. If RIGHT is pressed…
- Exit checks. We have DOWN and RIGHT.
- else Exit checks. We have DOWN.
- Check for LEFT. If LEFT is pressed…
When I write it out that way, you can see that this type of logic (adding GOTO to skip steps) means that the program would be fastest checking for UP and LEFT. And slowest for checking for JUST down. This means a game would move at different speeds based on which direction is detected. While this is a great optimization, it may not be desirable since a game may wish consistent speed (i.e. always worst case) than having a speed that varies.
Big thanks to craig for spotting my typo, and providing these two additional optimizations. I really like the “only POKE if it changed” one. It would be fun to benchmark and see if “worst case” all four arrows were being held down, is this slower than just clearing them all at once.
IF ELSE SLOWER
One additional note about using IF… Once BASIC starts processing a line, it has to parse the entire line whether that code is executed or not. For example:
IF A=42 THEN DO THIS:DO THAT:DO THE OTHER:DO NOTHING:DO OVER
Even if A is not 42, BASIC still has to at least scan through all the tokens and such on the rest of the line to skip it. Also, since ELSE could be used on Extended Color BASIC, there could be an ELSE clause that still needs to be processed:
10 IF A=42 THEN DO THIS:DO THAT:DO THE OTHER:DO NOTHING:DO OVER ELSE ...
Because of this, IF/ELSE can actually be slower than doing something like:
10 IF A=42 THEN 30
20 DO OVER:GOTO 40
30 DO THIS:DO THAT:DO THE OTHER:DO NOTHING:GOTO 40
40 ...continue...
This looks quite awful, but that’s how I had to program on my Commodore VIC-20 because it had no ELSE. And, it turns out, this can be quite a bit faster! Now line 10 checks for A to be 42, and if it is, it skips a line (which is fast) to get to 30 and hande it. If it is NOT 42, it quickly skips two lines instead to having to parse through a whole line of BASIC tokens.
I benchmarked something like this in an earlier article series, and was very impressed at the speedups that can be achieved just by making any IF line use a GOTO… though if it’s likely the line is true (A=42) more often than it’s not, it might not make sense. One size does not fit all.
And with that, I’ll end today’s installment without providing anything new, other than some handy speedups craig showed us.
We now have some quick ideas on using arrow keys to change X and Y coordinates. X and Y coordinates are used in games like Atari Adventure and Pac-Man to know which direction to send an enemy at the player. In the case of Pac-Man (see my earlier article series on that one), the ghosts target spots around (or on top of ) Pac-Man and decide which direction to turn based on which choice would be closer (using the wonderful Pythagorean theorem we learned about in school).
BUT, if our program was not using that, and just wanted a screen location to POKE a player character to, we could probably simplify this keyboard code a bit and use less variables.
To be continued…
Hi. In arrowbench12.bas, it still needs line 20 before the loop to start with:
8 POKEU,Z:POKED,Z:POKEL,Z:POKER,Z
“craig is suggesting the code could simply skip checking DOWN if we had an UP”: This is what I had in mind:
10 TIMER=0:FOR A=1 TO 1000
20 POKEU,Z:POKED,Z:POKEL,Z:POKER,Z
30 IF PEEK(U)=V THEN IF Y>.THEN Y=Y-1:GOTO50
40 IF PEEK(D)=V THEN IF Y<&HDF THEN Y=Y+1
50 IF PEEK(L)=V THEN IF X>.THEN X=X-1:GOTO90
60 IF PEEK(R)=V THEN IF X<&HFE THEN X=X+1
90 NEXT:PRINT TIMER
Based on what you said about extra code on the line, what if, you turn the test around and test for not a key and skip the ‘pressed’ code for keys not pressed, e.g.:
30 IF PEEK(U)=Z GOTO40
35 IF Y>.THEN Y=Y-1:GOTO90
40 IF PEEK(U)=Z GOTO50
45 IF Y<&HDF THEN Y=Y+1:GOTO90
50 IF PEEK(L)=Z GOTO40
55 IF X>.THEN X=X-1:GOTO90
60 IF PEEK(R)=Z GOTO90
65 IF Y<&HDF THEN Y=Y+1
sorry, that should read:
30 IF PEEK(U)=Z GOTO40
35 IF Y>.THEN Y=Y-1:GOTO90
40 IF PEEK(D)=Z GOTO50
45 IF Y<&HDF THEN Y=Y+1:GOTO90
50 IF PEEK(L)=Z GOTO60
55 IF X>.THEN X=X-1:GOTO90
60 IF PEEK(R)=Z GOTO90
65 IF X<&HFE THEN X=X+1