Häufig begangene Fehler von Anfängern

Einleitung

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.

Vermeiden Sie unnötige Codewiederholungen!

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.

Implementation einer Pause

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.

Verwenden Sie Unterprogramme

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.

Arbeiten Sie mit Feldvariablen und DATA!

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.

Diskdateien anstelle fester Daten im BASIC-Code

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!


Wieder zurück zur Übersicht


© 2000 by Andreas Meile