Ereignisse gleichzeitig abfragen

Einleitung und Motivation

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.

Geeignete Abfrage-Befehle selber

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.

Tastatur

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.

Joystick

Die beiden Funktionen STICK() und STRIG() eignen sich problemlos, weil überall eine Abfrage des momentanen Zustandes erfolgt, was keine Blockierung bewirkt.

Serielle Schnittstelle

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.

Netzwerk

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.

Einfache Zeit-Ereignisse

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.

Programmgerüst für Ihre Projekte

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

Anmerkung zu Multitasking-Betriebssystemen

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.


Wieder zurück zur Übersicht


© 2000 by Andreas Meile