Speziell bei der Spielprogrammierung kommen Sie fast immer wieder in
ähnliche Situationen, in welchen Sie zum einen die Tastatur und Joystick
auf Steuerkommandos abfragen müssen, gleichzeitig aber noch zeitgesteuert
das Geschehen am Bildschirm laufend aktualisieren. Im folgenden Artikel zeige
ich Ihnen, wie Sie dies einfach und effizient ohne irgendwelche
ON
-Unterbrechungsoperationen ausführen. In den Spielen
Netzwerk-Rasterbike, Wurmi, Zelda
2, Pong sowie Kisten-Schiffspiel sind im
Quellcode entsprechende Ereignisschleifen zu finden.
Voraussetzung sind die Verwendung von Befehlen, welche nicht blockieren. Den C-Programmierern unter UNIX und Linux ist dies unter dem Begriff Non-blocking I/O bekannt.
Die Befehle INPUT
, LINE INPUT
und
INPUT$
eignen sich nicht, denn sie halten die
Programmausführung solange an, bis der erwartete Tastendruck
kommt. Daher kommt nur INKEY$
in Frage.
Die beiden Funktionen STICK()
und STRIG()
eignen
sich problemlos, weil überall eine Abfrage des momentanen
Zustandes erfolgt, was keine Blockierung bewirkt.
Spiele für mehrere Mitspieler erfreuen sich nicht zuletzt dank dem
Internet immer grösserer Beliebtheit, so dass dieses Thema ebenfalls hier
hineingehört. Das Schreiben mit PRINT#
ist soweit kein
Problem, dagegen blockiert INPUT$
immer solange, bis die
angeforderte Anzahl Bytes verfügbar ist. Mit Hilfe der
LOC()
-Funktion können Sie diese Blockierung vermeiden, in dem
Sie damit zuerst nachschauen, ob sich überhaupt etwas im
Kommunikationspuffer befindet und lesen dementsprechend auch nur so viele
Bytes, wie im Moment zur Verfügung stehen.
Auch dieses Thema gehört, im heutigen Internet-Zeitalter sowieso, hier
hinein, wie das Netzwerk-Rasterbike
zeigt. Da QuickBASIC von sich aus keine Netzwerk-Unterstützung bietet,
müssen Sie in der Regel über das jeweilige API (Application Programming
Interface) des verwendeten Protokollstacks über
CALL ABSOLUTE
oder CALL INTERRUPT
arbeiten, und
dabei die dortigen Spezifikationen beachten. Im Falle von DOSISODE müssen
Sie also mit INT 17h
, Funktion AX=0B00h (SI_IOCTL)
die sog. Non-blocking I/O
aktivieren und dabei den Fehlercode 35 (EWOULDBLOCK) als Empfang
von 0 Bytes interpretieren. Im Artikel TCP/IP mit QuickBASIC finden Sie
übrigens das Ganze im Detail beschrieben.
Leere FOR
-NEXT
haben hier absolut nichts zu
suchen, siehe dazu häufige Fehler von
Anfänger. Auch SLEEP
sollten Sie besser meiden, da nur
ganz bestimmte Ereignisse wie Tastatur das System wieder
»aufwecken«. Daher arbeitet man am besten mit TIMER
.
Falls diverse Ereignisse anfallen (beispielsweise in einem Spiel mehrere,
unterschiedlich schnelle Förderbänder), so verwenden Sie am besten
eine sog. Halde (engl. heap) für die
Ereignisliste. Ein Beispiel einer Halde finden Sie in der Billard-Simulation.
Im wesentlichen müssen Sie alle Ereignisse innerhalb einer
WHILE
-Schleife hintereinander abfragen, daneben können Sie
auch kontinuierliche Aufgaben wie eine Animation erledigen. Das
folgende Beispiel ist daher so gestaltet, dass Sie einfach alles nicht
benötigte jeweils weglassen.
' Programmgerüst für eine Ereignisschleife
' ... (Initialisierung weggelassen)
OPEN "COM2:9600,N,8,1" AS 1 LEN=128
Drinbleib% = -1
nEreig! = TIMER + ZeitBisZumErstenEreignis!
WHILE Drinbleib%
' kontinuierliche Animation
xpos! = x0! + geschwindigk! * (TIMER - t0!)
ZeichneAuto xpos! ' beispielsweise Auto im Spiel
' Tastatur
t$ = INKEY$
IF t$ <> "" THEN
SELECT CASE t$
CASE CHR$(0) + "H" ' Pfeil hoch
' Bewege Spieler nach oben
CASE CHR$(0) + "P" ' Pfeil runter
' Bewege Spieler nach unten
CASE CHR$(0) + "K" ' Pfeil links
' Bewege Spieler nach links
CASE CHR$(0) + "M" ' Pfeil rechts
' Bewege Spieler nach rechts
CASE CHR$(27)
Drinbleib% = 0 ' Spiel komplett abbrechen
CASE ...
' ...
END SELECT
END IF
' Joystick
x% = STICK(0)
y% = STICK(1)
Knopf1Unten% = STRIG(1)
Knopf2Unten% = STRIG(5)
' x% und y% sowie Knopf1Unten% und Knopf2Unten% verarbeiten
' serielle Schnittstelle
aZ% = LOC(1)
IF aZ% > 0 THEN
Tel$ = INPUT$(aZ%, 1)
' Tel$ verarbeiten, z.B. Lenkbewegung des Gegner-Rennautos
END IF
' Netzwerk (DOSISODE)
dosIntEin.ax = &H800 ' SI_RECVFROM
dosIntEin.cx = 100 ' max. 100 Zeichen lesen
dosIntEin.dx = 1 ' Socket-ID
CALL INTERRUPT(&H17, dosIntEin, dosIntAus)
IF dosIntAus.ax = -1 THEN
SELECT CASE dosIntAus.cx
CASE 35
' EWOULDBLOCK ignorieren (warten!)
CASE 6
' EBADF => TCP/IP-Socket durch Partner geschlossen => Ende
Drinbleib% = 0
CASE ELSE
' I/O-Fehler im Netzwerk
Drinbleib% = 0
' Fehler behandeln
END SELECT
ELSE
Tel$ = ""
FOR i% = 0 TO dosIntAus.ax - 1
Tel$ = Tel$ + CHR$(PEEK(PufOffs% + 16 + i%))
NEXT i%
' Tel$ verarbeiten
END IF
' Zeitkomponente bearbeiten
IF TIMER > nEreig! THEN
' Zeitabhängiges Ereignis verarbeiten, z.B. Stoppuhr
' um 1 Sekunde reduzieren
nEreig! = nEreig! + ZeitBisZumNaechstenEreignis!
END IF
WEND
' Schluss wie gewohnt (alles wieder zurücksetzen)
END
In der hier beschriebenen Technik sollten Sie nur ausschliesslich in Single-Tasking-Betriebssystemen
wie MS-DOS arbeiten, da eine solche Schleife in einer Multitasking-Umgebung
mit sog. aktivem Warten an der
CPU-Rechenzeit zerrt. UNIX- und Linux-Programmierer kennen in diesem
Zusammenhang den select()
-Systemaufruf, um den Prozess solange
suspendieren zu können, bis eine bestimmte Zeit verstrichen ist oder
irgend ein Dateideskriptor (typischerweise Tastatur, serielle Schnittstelle
oder TCP/IP-Socket) der angegebenen Liste etwas erhalten hat. Siehe auch Migration alter DOS-Programme nach Visual Basic
für Windows zu dieser Thematik.