Seit Bestehen meiner Heimseite bekomme ich öfters Problemanfragen betreffend QuickBASIC, wo ich dann einen Blick in den BASIC-Code werfe. Dabei stosse ich in etwa immer wieder auf dieselben Fehler. Auf dieser Seite sind so die häufigsten Fehler betreffend Programmdesign von Programmierneulingen zusammen mit konkreten Verbesserungsvorschlägen zusammengetragen.
Als Beispiel möchte man
A An And Andr Andre Andrea Andreas
ausgeben. Schlechte Lösung:
' So sollte man es nicht tun! PRINT "A" PRINT "An" PRINT "And" PRINT "Andr" PRINT "Andre" PRINT "Andrea" PRINT "Andreas"
Problem: Falls man den auszugebenden Namen ändern möchte, muss man an insgesamt 7 Stellen ändern! Gute Lösung:
' Viel bessere Variante FOR i%=1 TO 7 PRINT LEFT$("Andreas", i%) NEXT i%
Die Kunst ist das Erkennen von regelmässigen Mustern, so dass man eben den Bildungsalgorithmus (in diesem Fall immer derselbe Text, jeweils einen Buchstaben mehr) eben durch die Maschine ausführen lässt. Noch bessere Lösung:
' Optimale Lösung DECLARE SUB GibAlsDreieckAus(t$) GibAlsDreieckAus "Andreas" SUB GibAlsDreieckAus(t$) FOR i%=1 TO LEN(t$) PRINT LEFT$(t$, i%) NEXT i% END SUB
Diese Lösung erfüllt sogar das Kriterium der
Wiederverwendbarkeit optimal, denn Sie könnten diese
SUB
-Routine in eine Bibliothek stellen und beliebig in anderen
Projekten einsetzen.
Viel krasser das folgende Beispiel:
100 ' Abenteuerspiel 110 ' So sollte man es nicht tun! 120 SCREEN 1 130 : 200 ' Küche 210 CLS 220 LINE(5,140)-(312, 145),2,B 230 LOCATE 20,1 240 PRINT "Wir befinden uns in der Küche." 250 PRINT "Mögliche Richtungen: S, O" 260 INPUT "Was soll ich tun"; K$ 270 IF K$="S" OR K$="s" THEN GOTO 400 280 IF K$="O" OR K$="o" THEN GOTO 600 290 PRINT "Kommando nicht verstanden!" 300 GOTO 250 310 : 400 ' Wohnzimmer 410 CLS 420 LINE(5,140)-(312, 145),2,B 430 LOCATE 20,1 440 PRINT "Wir befinden uns im Wohnzimmer." 450 PRINT "Mögliche Richtungen: N, O" 460 INPUT "Was soll ich tun"; K$ 470 IF K$="N" OR K$="s" THEN GOTO 200 480 IF K$="O" OR K$="o" THEN GOTO 800 490 PRINT "Kommando nicht verstanden!" 500 GOTO 450 510 : 600 ' Schlafzimmer 610 CLS 620 LINE(5,140)-(312, 145),2,B 630 LOCATE 20,1 640 PRINT "Wir befinden uns im Schlafzimmer." 650 PRINT "Mögliche Richtungen: W" 660 INPUT "Was soll ich tun"; K$ 670 IF K$="W" OR K$="w" THEN GOTO 200 680 PRINT "Kommando nicht verstanden!" 690 GOTO 650 700 : 800 ' draussen auf dem Balkon 810 CLS 820 LINE(5,140)-(312, 145),2,B 830 LOCATE 20,1 840 PRINT "Wir befinden uns draussen auf dem Balkon." 850 PRINT "Mögliche Richtungen: W" 860 INPUT "Was soll ich tun"; K$ 870 IF K$="W" OR K$="w" THEN GOTO 400 880 PRINT "Kommando nicht verstanden!" 890 GOTO 850
Dieser Programmierer hatte offensichtlich Freude an Copy & Paste! :-) Nun stellen Sie sich folgende Änderungswünsche einmal vor:
Ich denke, Sie sehen das Problem selber: Alle Änderungen müsste
man n-fach ausführen! In diesem Beispiel bewirkt eine
Parametrisierung für einen allgemeinen Raum in Form
eines GOSUB
-Unterprogramms bereits eine markante Verbesserung:
100 ' Abenteuerspiel 110 ' viel bessere Lösung! 120 SCREEN 1 130 : 200 ' Küche 210 N$="in der Küche":GK$="S,O" 220 GOSUB 1000 230 ON GK% GOTO 400, 600 400 ' Wohnzimmer 410 N$="im Wohnzimmer":GK$="N,O" 420 GOSUB 1000 430 ON GK% GOTO 200, 800 600 ' Schlafzimmer 610 N$="im Schlafzimmer":GK$="W" 620 GOSUB 1000 630 ON GK% GOTO 200 800 ' draussen auf dem Balkon 810 N$="draussen auf dem Balkon":GK$="W" 820 GOSUB 1000 830 ON GK% GOTO 400 1000 ' allgemeiner Raum 1010 ' Parameter: N$ = Raumname 1020 ' MK$ = mögliche Kommandos. Format: 1030 ' "k1,k2,k3,..,kn" 1020 ' GK% = gewähltes Kommando 1030 : 1040 CLS 1050 LINE(5,140)-(312, 145),2,B 1060 LOCATE 20,1 1070 PRINT "Wir befinden uns "; N$;"." 1080 PRINT "Mögliche Kommandos: "; MK$ 1090 INPUT "Was soll ich tun"; K$ 1100 ' Umwandlung in Grossbuchstaben 1100 FOR I%=1 TO LEN(K$) 1110 H$=MID$(K$,I%,1):IF H$>="a" AND H$<="z" THEN MID$(K$,I%)=CHR$(ASC(H$)-32) 1120 NEXT I% 1130 ' Kommando auswerten 1140 I%=1:GK%=1 1150 J%=INSTR(I%, MK$, ",") ' Kommandoweise durchgehen 1160 IF J%=0 THEN 1900 1170 IF K$=MID$(MK$, I%, J%-I%) THEN RETURN ' Kommando gefunden 1180 GK%=GK%+1:I%=J%+1:GOTO 1150 1190 IF K$<>MID$(MK$, I%) THEN PRINT "Kommando nicht verstanden!":GOTO 1080
Hier wäre eine Änderung des Bildschirmlayouts keinen Alptraum mehr :-). Funktionen wie Spielstandspeicherung wären aber auch hier noch nicht optimal möglich. Ein wirklich perfektes Softwaredesign erhalten Sie erst unter Zuhilfenahme einer sog. Zustandsmaschine, wofür ich Sie auf den speziellen Abenteuerspielartikel verweisen möchte.
In alten BASIC-Lehrbüchern findet man leider :-( immer die bekannte, leere
FOR
-NEXT
-Schleife:
10 ' Pause: besser nicht so! 20 FOR I!=1! TO 10000!:NEXT I!
Nun, was passiert wohl, wenn Intel oder AMD ihren brandneuen 1 GHz-Prozessor herausgeben? Und was, wenn ein ewig gestrig Gebliebener Ihr Programm auf seinem ganz alten Original-IBM XT mit 4,77 MHz getakteten Intel 8088-Prozessor laufen lässt? Mit Hilfe der in jedem PC vorhandenen Echtzeituhr macht man es besser:
10 ' Pause: So ist es gut! :-) 20 T! = TIMER + 5! ' 5 Sekunden lang warten 30 WHILE TIMER < T! : WEND
Anmerkung zu anderen Betriebssystemen: Diese Variante dürfen Sie nur
in reinen Single-Task-Betriebssystemen wie MS-DOS verwenden! Bereits
in Visual Basic sollten Sie mit WaitEvent()
und
sleep()
Ihren Prozess so lange suspendieren, damit dieser
keine CPU-Rechenzeit wegnimmt. In QBASIC.EXE
existiert zu diesem Zweck der SLEEP
-Befehl, mit welchem
immerhin eine ganze Anzahl Sekunden sowie auf Ereignisse gewartet
werden kann. Siehe auch Migration von
BASIC-Programmen auf neuere Entwicklungsumgebungsversionen. Für das
während einer Pause gleichzeitige Abfragen von Tastatur oder sonstige
Kommunikation siehe Programmierung von
Ereignisschleifen.
Das A und O für ein gutes Programm-Design ist eine saubere Aufteilung
in möglichst voneinander unabhängigen Programmeinheiten. Ein
Beispiel dazu ist die zu Beginn gezeigte SUB
-Prozedur
GibAlsDreieckAus(t$)
, ebenso könnte man die vorhin
beschriebene Pause als
SUB Pause(Zeit!) t! = TIMER + Zeit! WHILE TIMER < t! WEND END SUB
formulieren, womit eine optimale Wiederverwendbarkeit vorhanden wäre
und insbesondere später eine Umstellung auf SLEEP
problemlos
möglich wird. Unterprogramme dürfen auch komplexere Aufgaben
erfüllen wie beispielsweise ein Bildschirmmenü beinhalten:
DECLARE SUB Bildschirmmenue(mp$(), akt%, esc%) DIM mpkt$(1 TO 4) FOR i%=1 TO 4 READ mpkt$(i%) NEXT i% DATA "Lagerbestand abfragen", "Position erfassen", "Auswertung", "Ende" aktMp%=1 ' Vorgabewert esc%=0 WHILE aktMp% <> 4 OR esc% Bildschirmmenue mpkt$(), aktMp%, esc% SELECT CASE aktMp% CASE 1 ' Lagerbestand abfragen CASE 2 ' Position erfassen CASE 3 ' Auswertung END SELECT WEND SUB Bildschirmmenue(mp$(), akt%, esc%) ' Hier kann dann das Menü mit allem Firlefanz wie Pfeiltastenauswahl, ' mit <Alt> + <hervorgehobenem Buchstaben> usw. implementiert ' werden! ' Menü zeichnen DO ' Tastatur abfragen ' Menübalken bewegen LOOP UNTIL Taste$ = CHR$(13) OR Taste$ = CHR$(27) OR Taste$ = <Alt>+<hervorgehobener Buchstabe> ' Bildschirm löschen esc% = Taste$ = CHR$(27) END SUB
Für all diejenigen von Ihnen, welche auf die objektorientierte Denkweise mit C++ und Java umsteigen möchten, ist eine gute Beherrschung der Unterprogrammtechnik sogar zwingende Voraussetzung, weshalb ich Sie auf den Artikel über SUB und FUNCTION verweisen möchte.
Beispiel: Sie möchten ein Spiel im Stil von Zelda 2 schreiben, wo Perlen eingesammelt werden müssen. Typischer Ansatz eines Anfängers:
' Juwelen-Spiel ' So besser nicht! Perle1x% = 10 : Perle1y% = 23 Geholt1% = 0 Perle2x% = 17 : Perle2y% = 13 Geholt2% = 0 Perle3x% = 12 : Perle3y% = 7 Geholt3% = 0 Perle4x% = 33 : Perle4y% = 23 Geholt4% = 0 SpielerX% = 12 : SpielerY% = 19 Punkte% = 0 SCREEN 0: WIDTH 40, 25 CLS COLOR 12 LOCATE SpielerY%, SpielerX% : PRINT CHR$(2); COLOR 11 LOCATE Perle1y%, Perle1x% : PRINT CHR$(4); LOCATE Perle2y%, Perle2x% : PRINT CHR$(4); LOCATE Perle3y%, Perle3x% : PRINT CHR$(4); LOCATE Perle4y%, Perle4x% : PRINT CHR$(4); WHILE Punkte% < 4 LOCATE 25, 1 COLOR 14 PRINT USING "Punkte:###"; punkte%; DO t$ = INKEY$ LOOP WHILE t$ = "" SELECT CASE t$ LOCATE SpielerY%, SpielerX% PRINT " "; CASE CHR$(0) + "H" IF SpielerY% > 1 THEN SpielerY% = SpielerY% - 1 END IF CASE CHR$(0) + "K" IF SpielerX% > 1 THEN SpielerX% = SpielerX% - 1 END IF CASE CHR$(0) + "M" IF SpielerX% < 40 THEN SpielerX% = SpielerX% + 1 END IF CASE CHR$(0) + "P" IF SpielerY% < 24 THEN SpielerY% = SpielerY% + 1 END IF END SELECT IF SpielerX% = Perle1x% AND SpielerY% = Perle1y% AND Geholt1% = 0 THEN Punkte% = Punkte% + 1 Geholt1% = 1 SOUND 311, 1! END IF IF SpielerX% = Perle2x% AND SpielerY% = Perle2y% AND Geholt2% = 0 THEN Punkte% = Punkte% + 1 Geholt2% = 1 SOUND 311, 1! END IF IF SpielerX% = Perle3x% AND SpielerY% = Perle3y% AND Geholt3% = 0 THEN Punkte% = Punkte% + 1 Geholt3% = 1 SOUND 311, 1! END IF IF SpielerX% = Perle4x% AND SpielerY% = Perle4y% AND Geholt4% = 0 THEN Punkte% = Punkte% + 1 Geholt4% = 1 SOUND 311, 1! END IF WEND COLOR 9 LOCATE 13, 10 PRINT "Level geschafft!"
Und nun denken Sie sich einen Level mit 300 aufzusammelnden Perlen auf diese Art und Weise programmiert! Noch viel krasser wird das Ganze, wenn Feinde vorkommen, welche es auf den Spieler abgesehen haben und die möglichst um die Perlen herum laufen sollen...
Mit Feldvariablen verschwinden eigentlich all diese Probleme:
' Juwelen-Spiel ' So ist es besser! CONST nPerlen% = 4 DIM PerleX%(1 TO nPerlen%), PerleY%(1 TO nPerlen%), Geholt%(1 TO nPerlen%) FOR i%=1 TO nPerlen% READ PerleX%(i%), PerleY%(i%) Geholt%(i%) = 0 NEXT i% DATA 10, 23, 17, 13, 12, 7, 33, 23 Punkte% = 0 SpielerX% = 12 : SpielerY% = 19 SCREEN 0: WIDTH 40, 25 CLS COLOR 12 LOCATE SpielerY%, SpielerX% PRINT CHR$(2); COLOR 11 FOR i%=1 TO nPerlen% LOCATE PerleY%(i%), PerleX%(i%) PRINT CHR$(4); NEXT i% LOCATE 25, 1 COLOR 14 PRINT USING "Punkte:###"; punkte%; WHILE Punkte% < nPerlen% DO t$ = INKEY$ LOOP WHILE t$ = "" SELECT CASE t$ LOCATE SpielerY%, SpielerX% PRINT " "; CASE CHR$(0) + "H" IF SpielerY% > 1 THEN SpielerY% = SpielerY% - 1 END IF CASE CHR$(0) + "K" IF SpielerX% > 1 THEN SpielerX% = SpielerX% - 1 END IF CASE CHR$(0) + "M" IF SpielerX% < 40 THEN SpielerX% = SpielerX% + 1 END IF CASE CHR$(0) + "P" IF SpielerY% < 24 THEN SpielerY% = SpielerY% + 1 END IF END SELECT LOCATE SpielerY%, SpielerX% COLOR 12 PRINT CHR$(2); FOR i%=1 TO nPerlen% IF SpielerX% = PerleX%(i%) AND SpielerY% = PerleY%(i%) AND Geholt%(i%) = 0 THEN Geholt%(i%) = 1 punkte% = punkte% + 1 SOUND 311, 1! LOCATE 25, 8 COLOR 14 PRINT USING "###"; punkte%; END IF NEXT i% WEND COLOR 9 LOCATE 13, 10 PRINT "Level geschafft!"
Ein ähnliches Beispiel sind grafische Figuren: Meiden Sie Befehlswiederholungen im Sinne von
SUB ZeichneSchweizerkreuz(mx%, my%, f%) LINE (mx% + 3, my% + 3)-(my% + 3, my% + 10), f% LINE -(my% - 3, my% + 10), f% LINE -(my% - 3, my% + 3), f% LINE -(my% - 10, my% + 3), f% LINE -(my% - 10, my% - 3), f% LINE -(my% - 3, my% - 3), f% LINE -(my% - 3, my% - 10), f% LINE -(my% + 3, my% - 10), f% LINE -(my% + 3, my% - 3), f% LINE -(my% + 10, my% - 3), f% LINE -(my% + 10, my% + 3), f% LINE -(my% + 3, my% + 3), f% END SUB
Stattdessen verwenden Sie auch wieder besser DATA
-Zeilen:
SUB ZeichneSchweizerkreuz(mx%, my%, f%) RESTORE Daten READ xAnf%, yAnf%, x%, y% ' Startlinie LINE (xAnf%, yAnf%)-(x%, y%), f% ' Kurvenzug DO READ x%, y% IF x% <> 9999 THEN LINE -(x%, y%), f% END IF LOOP UNTIL x% = 9999 ' Abschlusssegment LINE -(xAnf%, yAnf%), f% Daten: DATA 3, 3, 3, 10, -3, 10, -3, 3, -10, 3, -10, -3, -3, -3, -3, -10, 3, -10 DATA 3, -3, 10, -3, 10, 3, 9999, 9999 END SUB
Noch besser geeignet für das obige Beispiel wäre der
DRAW
-Befehl.
Eine sehr gute Alternative zu DATA
-Zeilen ist die Verwendung
von Daten-Dateien wie dies beispielsweise bei Zelda 2 und Wurmi mit den Level-Dateien der Fall ist,
speziell bei grösserem Umfang, z.B. grosse Titel-Bitmapdatei oder MIDI-Musikdatei
sowie Flexibilität (in den vorher erwähnten Spielen kann man
andere Levels erstellen, ohne dabei den BASIC-Quellcode ändern zu
müssen) wird die Verwendung von Diskdateien sogar dringend empfohlen.
Professionelle Software-Häuser arbeiten eigentlich vorwiegend auf
diese Weise, daher finden Sie bei einer Software-Installation ausser reinen
.DLL- und .EXE typischerweise noch zahlreiche
andere Dateien wie .BMP, .WAV und .DAT
im Programmverzeichnis vor!