Sprechen Sie GEM? Anwendungsprogrammierung mit GFA-Basic, Teil 1

Wer in vergangene Zeiten zurückblickt, wird sich an die unzähligen kleinen PD-Programme erinnern, deren Entwicklung meist durch einen spontanen Einfall und ein augenblickliches «Das kann nicht schwer sein!» ausgelöst wurde. Und in der Tat: Es war nicht schwer, denn GFA-Basic hatte sich als für jedermann leicht erlernbare Sprache erwiesen. Aus Sicht der Hobbyprogrammierer ist an diesem Punkt die Zeit leider nicht stehengeblieben. Die Anforderungen, meist zusammengefasst unter dem abstrakten Begriff „sauberes Programmieren", stiegen steil an. Selbst kleine Anwendungen müssen sich an eine Vielzahl von Protokollen halten - sie müssen die noch immer wachsende Sprache des GEM sprechen. Zum Glück beherrscht ein Werkzeug für GFA-Programmierer diese Sprache perfekt: Wenn Sie GFA sprechen, dann ist faceVALUE [1] Ihr Dolmetscher zum Betriebssystem. Wie Sie Anwendungsprogrammierung wieder zu einem Vergnügen machen, werden Sie in diesem Grundlagenkurs erlernen.

Learning by doing

Da GFA-Basic-Programmierer Menschen der Praxis sind, werden wir jeden Kursteil mit einem Praxisbeispiel versüßen. Für den ersten Teil nehmen wir uns dazu die Realisierung eines Tetris-Spieles vor.

Ein Grund, weshalb das Erstellen von Anwendungen heute schwerer ist als früher, wurde einleitend bereits genannt: die Anforderungen sind gestiegen, und deren Umsetzung erfordert einiges an Spezialwissen. Der zweite Grund liegt aber darin, dass die meisten GEM-Systeme bereits seit langem die Rechenzeit preemptiv auf die Anwendungen verteilen. Dass dies in der Tat große Bedeutung für den Anwendungsprogrammierer hat, und wie man diese Situation meistern kann, wird nun erläutert.

Kooperativ versus preemptiv

In seinen Anfängen war GEM ein kooperatives Multitaskingbetriebssystem. Multitaskingsysteme erlauben das parallele Abwickeln mehrerer Aufgaben (Task = Aufgabe), auch wenn nur eine geringe Zahl von Abwicklern (üblicherweise genau ein Prozessor) zur Verfügung steht. Um dies zu ermöglichen, sorgen die Multitaskingbetriebssysteme dafür, dass hin und wieder aktuelle Aufgabenbearbeitungen unterbrochen werden, und andere anstehende Aufgaben zu den aktuellen ernannt werden. An dieser Stelle unterscheidet man zwischen freiwilligen und unfreiwilligen Unterbrechungen.

Freiwillige sind solche, die der Anwendungsprogrammierer explizit im Programm vorgesehen hat. Sie werden also ausgelöst, wenn bei der Programmausführung ein spezieller Befehl erreicht wird, der da lautet: „Stopp! An dieser Stelle können nun auch die anderen Programme an die Reihe kommen." In GFA-Basic lautet dieser Befehl ON MENU. Es bleiben zwei Arten von Unterbrechungen, die man als unfreiwillig bezeichnet. Der erste Fall sollte im Normalbetrieb nicht Vorkommen, denn er liegt vor, wenn die Unterbrechung aufgrund eines Laufzeitfehlers zustande kam. Systeme, bei denen nur diese beiden genannten Unterbrechungsarten Vorkommen, nennt man kooperativ, da mehrere Anwendungen nur dann nebenläufig aktiv sein können, wenn sie sich oft genug gegenseitig über den „Stopp"-Befehl die Kontrolle übergeben. Beim zweiten Fall der unfreiwilligen Unterbrechungen nimmt sich das Betriebssystem die Freiheit, zu von ihm selbst festgelegten Zeitpunkten die aktuelle Bearbeitung auf andere Aufgaben umzuschalten. Ist dies möglich, spricht man vom preemptiven Multitasking. Die Eigenschaft des GEM, ein kooperatives Betriebssystem zu sein, machte man sich früher gerne zu nutze. Durch Vermeidung des Stopp-Befehls wurde erreicht, dass keine andere Anwendung die Kontrolle erhielt und so vielleicht den Zustand der Bildschirmausgabe ungewollt verändern konnte. Auf diese Weise wurde das Multitasking untergraben, was längst nicht mehr zeitgemäß und mit Realisierung des preemptiven Multitaskings auch nicht mehr möglich ist. Um eine Anwendung erfolgreich ins Multitasking zu integrieren, sollte man den Unterschied zwischen Steuer-und Operationsvariablen kennen.

Steuer- und Operationszustand

Unter [2] findet man eine genaue Definition dieser Begriffe. An dieser Stelle genügt eine unscharfe Auslegung. Üblicherweise legt der Programmierer Variablen an, um darin Werte zu speichern und mit diesen dann zu rechnen - um also mit ihnen zu „operieren". All diese Variablen hat er stets vor Augen, meist dokumentiert er ihre Funktion genau. Man nennt sie Operationsvariablen. Die Existenz einer besonderen Variablen übersieht der Programmierer aber meist: Gemeint ist der Programmzähler (PC). Diese Variable beschreibt, an welcher Stelle der Ablaufbeschreibung sich die Abwicklung gerade befindet. Es ist der Zeigefinger des Abwicklers, den er wie der Leser eines Buches an seinem Programm anlegt, um zu wissen, wo er gerade ist. So muss der Abwickler des Tetrisspieles wissen, ob er noch an der Stelle ist, wo er darauf wartet, dass der Spieler das Spiel startet, oder ob er bereits an der Stelle ist, wo das Spiel läuft. Solche Variablen, in denen man sich Orte aus der Ablaufbeschreibung merkt, nennt man Steuervariablen. Wie wir nun sehen werden, geraten bei der Programmierung von multitaskingfähigen Anwendungen die Steuervariablen plötzlich in das Rampenlicht. Der Grund ist die hier meist angewandte Hauptschleifen-Architektur, die auch in faceVALUE-Programmen eingesetzt wird. Ein so strukturiertes Programm besteht aus drei Ablaufteilen: der Initialisierung am Programmanfang, der Hauptschleife (die sog. Idle-Loop) und den Aufräumarbeiten am Programmende.

Abbildung 1 - die Hauptschleife: Bei der Initialisierung werden wie erwartet alle Variablen angelegt, es wird aber zudem entschieden, welches Ereignis eintreten muss, ehe wieder eine für das Programm relevante Operation getätigt werden muss. Durch die Hauptschleife wird nun dafür gesorgt, dass sich folgende Dinge stets wiederholen: Es wird zunächst gewartet, bis das gewünschte Ereignis eingetreten ist. Unterdessen wird die Kontrolle an andere Anwendungen abgegeben (zur Unterstützung des kooperativen Multitaskings). Nach Eintritt des Ereignisses wird die dadurch ausgelöste Operation getätigt und es wird errechnet, auf welches Ereignis als nächstes zu warten ist. Schließlich wird festgestellt, ob das Programmende erwünscht ist -falls nein, beginnt die nächste Wiederholung der Hauptschleife.

GFA-Listing 1

PROCEDURE tetris_init 
    LOCAL stein$
    '
    ' +++ SYM 
    LET tetris_w_&=10 
    LET tetris_h_&=20 
    LET tetris_speed_&=300 
    ' +++ SYM
    '
tetris_steine_:
    DATA .....
    DATA ..X..
    DATA ..X..
    DATA ..X..
    DATA ..X..
    '
    DATA .....
    DATA .....
    DATA .XXX.
    DATA ..X..
    DATA .....
    '
    DATA .....
    DATA ..X..
    DATA ..X..
    DATA ..XX.
    DATA .....
    '
    DATA ENDE
    '
    DIM tetris_&(tetris_w_&-1,tetris_h_&-1)
    DIM tetris_faellt_&(4,4)
    '
    ' Steine zählen ...
    '
    LET tetris_steine_&=-1 
    RESTORE tetris_steine_
    REPEAT 
        INC tetris_steine_&
        READ stein$ 
    UNTIL stein$="ENDE"
    DIV tetris_steine_&,5
    @tetris_new
RETURN
PROCEDURE tetris_new 
    ARRAYFILL tetris_&(),0 
    ARRAYFILL tetris_faellt_&(),0 
    CLR tetris_punkte_%
    '
    @tetriswin_redraw(0,0,tetris_w_&,tetris_h_&)
    @tetris_info
RETURN
PROCEDURE tetris_start 
    @tetris_neuer_stein 
    @tetris_info 
RETURN
PROCEDURE tetris_langsam 
    LET tetris_laeuft_!=TRUE 
    @aes_timer_every(tetris_speed_&)
RETURN
PROCEDURE tetris_schnell 
    LET tetris_laeuft_!=TRUE 
    @aes_timer_every(1)
RETURN
PROCEDURE tetris_stop 
    @aes_timer_stop 
    LET tetris_laeuft_!=FALSE 
    @tetris_info 
RETURN
PROCEDURE tetris_neuer_stein 
    LOCAL stein&,farbe&,zeile$,x&,y&
    '
    ' Form und Farbe festlegen ...
    '
    LET stein&=RANDOM(tetris_steine_&)
    LET farbe&=RANDOM(7)+1
    '
    RESTORE tetris_steine_
    WHILE stein&>0 
        READ zeile$,zeile$,zeile$,zeile$,zeile$
        DEC stein&
    WEND
    '
    FOR y&=0 TO 4 
        READ zeile$ 
        FOR x&=0 TO 4 
            IF MID$(zeile$,x&+1,1)="."
                LET tetris_faellt_&(x&,y&)=0 
            ELSE
                LET tetris_faellt_&(x&,y&)=farbe&
            ENDIF 
        NEXT x&
    NEXT y&
    '
    LET tetris Jaellt_x_&=(tetris_w_&-5)\2 
    LET tetris_faellt_y_&=-5
    '
    @tetris_langsam
RETURN
FUNCTION tetris_faellt(x&,y&)
    LOCAL fx&,fy&
    LET fx&=x&-tetris_faellt_x_&
    LET fy&=y&-tetris_faellt_y_&
    IF fx&>=0 AND fx&<=4 AND fy&>=0 AND fy&<=4 
        RETURN tetris_faellt_&(fx&,fy&)
    ELSE 
        RETURN 0 
    ENDIF 
ENDFUNC
PROCEDURE tetris_links 
    DEC tetris_faellt_x_&
    IF @tetris_kollision 
        INC tetris_faellt_x_&
    ELSE
        @tetriswin_redraw(tetris_faellt_x_&,tetris_faellt_y_&,6,5)
    ENDIF
RETURN
PROCEDURE tetris_rechts 
    ' Analog zu tetris_links 
RETURN
PROCEDURE tetris_drehe 
    ' Hier den fallenden Stein drehen 
RETURN
PROCEDURE tetris_falle 
    INC tetris_faellt_y_&
    IF @tetris_kollision 
        DEC tetris_faellt_y_&
        @tetris_stein_hinsetzen 
        IF tetris_laeuft_! 
            @tetris_volle_zeilen_loeschen 
            @tetris_neuer_stein 
        ENDIF 
    ELSE
        @tetriswin_redraw(tetris_faellt_x_&,tetris_faellt_y_&-1,5,6)
    ENDIF
RETURN
FUNCTION tetris_kollision 
    LOCAL x&,y&
    LOCAL feld_x&,feld_y&
    LOCAL kollision!
    CLR kollision!
    FOR y&=0 TO 4 
        FOR x&=0 TO 4 
            IF tetris_faellt_&(x&,y&)
                LET feld_x&=tetris_faellt_x_&+x&
                LET feld_y&=tetris_faellt_y_&+y&
                IF feld_x&<0 OR feld_x&>=tetris_w_& OR feld_y&=>tetris_h_&
                    LET kollision!=TRUE 
                ELSE 
                    IF feld_y&>=0 
                        IF tetris_&(feld_x&,feld„y&)
                            LET kollision!=TRUE 
                        ENDIF 
                    ENDIF 
                ENDIF 
            ENDIF
            EXIT IF kollision!
        NEXT x&
        EXIT IF kollision!
    NEXT y&
    RETURN kollision!
ENDFUNC
PROCEDURE tetris_stein_hinsetzen 
    LOCAL x&,y&
    LOCAL feld_x&,feld_y& 
    LOCAL game_over!
    FOR y&=0 TO 4 
        FOR x&=0 TO 4 
            IF tetris_faellt_&(x&,y&)
                LET feld_x&=tetris_faellt_x_&+x&
                LET feld_y&=tetris_faellt_y_&+y&
                IF feld_y&<0 
                    LET game_over!=TRUE 
                ELSE 
                    LET tetris_&(feld_x&,feld_y&)=tetris_faellt_&(x&,y&) 
                ENDIF 
            ENDIF 
        NEXT x&
    NEXT y&
    IF game_over!
        @tetris_stop
    ENDIF
RETURN
PROCEDURE tetris_volle_zeilen_loeschen 
    ' Hier die vollen Zeilen löschen und die Punkte erhöhen 
RETURN 
PROCEDURE tetris_draw(col&,line&,cols&,lines&,off_x&,off_y&,w&,h&)
    LOCAL ko_x&,ko_y&
    LOCAL x&,y&
    LOCAL stein&
    '
    ~GRAF_MOUSE(256,0)  ! Mauszeiger abschalten
    '
    COLOR 0 
    BOUNDARY 1
    '
    LET ko_y&=off_y&+line&*h&
    FOR y&=line& TO line&+lines&-1 
        LET ko_x&=off_x&+col&*w&
        FOR x&=col& TO col&+cols&-1 
            LET stein&=tetris_&(x&,y&)
            IF stein&=0 
                LET stein&=@tetris_faellt(x&,y&)
            ENDIF 
            IF stein&
                BOX ko_x&,ko_y&,ko_x&+w&-1,ko_y&+h&-1 
                IF WORK_OUT(13)=2 ! mono 
                    DEFFILL 1,2,stein&
                ELSE
                    DEFFILL stein&,2,8 
                ENDIF
                PBOX ko_x&+1,ko_y&+1,ko_x&+w&-2,ko_y&+h&-2 
            ELSE 
                DEFFILL 0,2,8
                PBOX ko_x&,ko_y&,ko_x&+w&-1,ko_y&+h&-1 
            ENDIF
            ADD ko_x&,w&
        NEXT x&
        ADD ko_y&,h&
    NEXT y&
    '
    ~GRAF_MOUSE(257,0) ! Mauszeiger anschalten 
RETURN
PROCEDURE tetris_info 
    IF tetris_laeuft_! 
        @tetriswin_info(" Punkte:"+STR$(tetris_punkte_%))
    ELSE
        @tetriswin_info(" Punkte:"+STR$(tetris_punkte_%)+" <RET>")
    ENDIF
RETURN

Explizite Steuervariablen

Die Hauptschleifenarchitektur verlangt also, dass nie ein langer Ablauf am Stück bearbeitet wird, und damit stellt sie an den Programmierer die Forderung, den Steuerzustand in expliziten Variablen abzulegen. Dass das Tetrisspiel über die zwei Zustände „warten auf Spielbeginn" und „Spiel läuft" verfügt, äußert sich nun nicht mehr dadurch, dass das Programm selbst aus zwei Teilen besteht. Es äußert sich dadurch, dass der Programmierer eine Variable eingeführt hat, in der abgelegt ist, ob das Spiel im Gange ist oder nicht. Anhand dieser Variablen muss er nach Eintritt eines Ereignisses in der Phase „kurz operieren" die Situation erkennen und durch Fallunterscheidung das jeweils Richtige veranlassen.

Der Zwang der Einführung solcher Steuervariablen ist es, der erfahrungsgemäß Einsteigern in die GEM-Programmierung die meisten Probleme bereitet. Viele sprechen etwas naiv davon, dass man „umdenken" muss. Dem ist aber nicht wirklich so - das Ablaufdenken ist nach wie vor das gleiche. Der Unterschied besteht nur darin, dass man als Programmierer den Ablauf nicht gleich im Quelltexteditor, sondern zunächst auf dem Papier beschreibt. Dann vergibt man allen Stellen in diesem Ablauf eine Nummer - üblicherweise handelt es sich um eine ganz winzige Anzahl von Nummern. Eine einzige Variable, die diese Nummer aufnimmt, reicht dann aus, um festzuhalten, an welchem Ort in der Ablaufbeschreibung (in welchem Steuerzustand) sich das Programm aktuell befindet. Anhand dieser Variablen finden dann die Fallunterscheidungen statt, also z.B. ob die Return-Taste nun das Starten des Tetrisspiels (im Zustand „warten auf Spielbeginn") oder das Fallen des Steines (im Zustand „Spiel läuft") bewirkt.

Das Tetrisspiel hat also gerade mal zwei Steuerzustände, mit Einführung eines Pausezustandes oder einer Bonusrunde wären es drei oder vier - für alle Probleme gilt aber, dass mehr als sieben Zustände schon eine Seltenheit sind. Der Grund liegt übrigens darin, dass man all diese Zustände und deren Abfolge dem Benutzer auch explizit erklären muss. Eine Anwendung mit hunderten von Steuerzuständen könnte kein Mensch begreifen und bedienen.

Abbildung 3: Die Einstellungen im Hauptdialog

Nebenläufigkeit

Wie man gesehen hat, leidet unter der Hauptschleifenarchitektur die Anschaulichkeit des Quelltextes. Die Zusammenhänge des Ablaufs erkennt man manchmal nur anhand der Skizze auf dem Papier. Der Gewinn liegt jedoch darin, dass durch diese Architektur auch Systeme mit Nebenläufigkeiten realisiert werden können. Bereits für das kleine Tetrisspiel muss man von dieser Möglichkeit Gebrauch machen: Während das fallende Steinchen zum einen durch Tasteneingaben bewegt werden kann, veranlasst nebenläufig dazu auch eine Automatik eine Bewegung des Sternchens, nämlich das stetige Herabfallen.

GFA-Listing 2

PROCEDURE tetriswin_init 
    ' ++SYM
    LET tetriswin_stein_w_&=15 
    LET tetriswin_stein_h_&=15 
    ' ++SYM
    LET tetriswin_index_&=-1 
RETURN
PROCEDURE tetriswin_open 
    LOCAL ww&,wh&,x&,y&,w&,h&
    LOCAL handle&
    '
    LET ww&=tetris_w_&*tetriswin_stein_w_&
    LET wh&=tetris_h_&*tetriswin_stein_h_&
    LET x&=100 
    LET y&=100
    @win_calc_wh(fixwin%,-1,-1,ww&,wh&,w&,h&)
    '
    LET handle&=@win_open(" fV-Tetris ","<RETURN> = Start",fixwin%,-1,ww&,wh&,0,0,-1 ,x&,y&,w&,h&,-1) 
    IF handle&=0 
        ' Fenster konnte nicht geöffnet werden 
        LET exit_program!=TRUE 
    ELSE
        LET tetriswin_index_&=@win_get_index(handle&) 
    ENDIF 
RETURN 
PROCEDURE tetriswin_content(off_x%,off_y%,cx&,cy&,cw&,ch&) 
    LOCAL x&,y&,w&,h&
    LOCAL feld_x1&,feld_y1&,feld_x2&,feld_y2&
    '
    @win_get_workarea(tetriswin_index_&,x&,y&,w&,h&)
    '
    LET feld_x1&=MAX(0,MIN(tetris_w_&,(cx&-x&)\tetriswin_stein_w_&))
    LET feld_y1&=MAX(0,MIN(tetris_h_&,(cy&-y&)\tetriswin_stein_h_&))
    LET feld_x2&=MAX(-1,MIN(tetris_w_&-1,(cx&+cw&-x&)\tetriswin_stein_w_&))
    LET feld_y2&=MAX(-1,MIN(tetris_h_&-1,(cy&+ch&-y&)\tetriswin_stein_h_&))
    '
    IF feldjxl &<=feld_x2& AND feld_y1&<=feld_y2& 
        @tetris_draw(feld_x1 &,feld_y1&,feld_x2&-feld_x1 &+1,feld_y2&-feld_y1 &+1 ,0,0,tetriswin_stein_w_&,tetriswin_stein_h&)
    ENDIF
RETURN
PROCEDURE tetriswin_keyb(ks&,key&)
    LOCAL scan&
    LET scan&=BYTE(SHR(key&,8))
    IF tetris_laeuft_! 
        SELECT scan& 
        CASE 1 ! Escape
            @tetris_stop 
        CASE 75 ! links
            @tetris_links 
        CASE 77 ! rechts
            @tetris_rechts 
        CASE 80 ! runter
            @tetris_schnell 
        CASE 57,72 ! space,hoch 
            @tetris_drehe 
        ENDSELECT 
    ELSE 
        SELECT scan&
        CASE 28,114 ! Return, Enter 
            @tetris_new 
            @tetris_start 
        ENDSELECT 
    ENDIF 
RETURN 
PROCEDURE tetriswin_redraw(col&,line&,cols&,lines&)
    LOCAL x&,y&,w&,h&
    IF tetriswin_index_&=>0
        @win_get_workarea(tetriswin_index_&,x&,y&,w&,h&)
        @win_send_redraw(tetriswin_index_&,x&+col&*tetriswin_stein_w_&,y&+line&*tetriswin_stein_h_&,cols&*tetriswin_stein_w_&,lines&*tetriswin_stein_h_&)
    ENDIF
RETURN
PROCEDURE tetriswin_info(info$)
    IF tetriswin_index_&=>0 
        @win_set_infoline(tetriswin_index_&,info$)
    ENDIF
RETURN
Abbildung 4: Einstellungen für den Userfenstertyp „fixwin“

Durch die Hauptschleifenarchitektur ist die Lösung einfach: An der Stelle „auf Ereignis warten“ wird dazu auf das Tastaturereignis und zugleich auf ein Weckereignis gewartet, welches ausgelöst wird, wenn die Zeit, die der Stein während eines Schrittes in der Luft steht, abläuft. Tritt eines der beiden Ereignisse ein, dann wird im Schritt „kurz operieren“ die dazu passende Operation ausgeführt. Daraus ergibt sich das für GEM- und damit auch für faceVALUE-Programme typische Aussehen: Der Block „kurz operieren" besteht aus einem mitunter riesigen Fallunterscheider, durch den abhängig von Steuerzustand und eingetretenem Ereignis die aktuell passende Operation ausgewählt wird. Anschließend, während der Abarbeitung derer, werden dann der nächste Steuerzustand und die zu erwartenden Ereignisse für den nächsten Schritt ermittelt. Wir werden später sehen, wie faceVALUE diesen Fallunterscheider im generierten Listing anlegt.

Phase 1: die RSC-Datei

Nach diesen Vorüberlegungen beginnen wir nun mit der Entwicklung des Tetris-Spiels. Das Geschehen wird sich in einem einzigen Fenster abspielen, welches, wie im Titelbild dargestellt, über einen Schließknopf, den Fenstertitel (der ja gleichzeitig dem Verschieben des Fensters dient) und eine Infozeile verfügen soll. Die freie Fensterfläche bildet schließlich das Spielfeld. Eine Menüzeile oder ein Dialogfenster benötigen wir nicht. Nach diesen Vorgaben richtet sich nun die erste Phase der Programmentwicklung eines faceVALUE-Programms: Das Erstellen der RSC-Datei.

Für ein Programm ohne Dialogfenster genügt es, die faceVALUE beiliegende RSC-Datei MINIMUM.RSC in den RSC-Editor einzuladen und dort den System-Dialog und das Hauptmenü wie in der Dokumentation beschrieben anzupassen. Wir haben den System-Dialog unserer Datei FVTETRIS.RSC wie folgt gestaltet:

Anwender von faceVALUE 3.1 sollten den Menübaum löschen - ab dieser Version kann faceVALUE auch Programme ohne Menüzeile erstellen. Damit sind die Arbeiten an der RSC-Datei bereits abgeschlossen.

Phase 2: die LST-Datei

Das Ergebnis des zweiten Schrittes wird eine LST-Datei sein, die vom GFA-Basic-Interpreter gelesen werden kann. Genau diese Aufgabe erledigt das faceVALUE-Hauptprogramm. Dort wird die RSC-Datei FVTETRIS.RSC eingeladen, woraufhin der Hauptdialog (Abbildung 3) geöffnet wird. Die Einstellungen, die hier getätigt werden, beeinflussen ergänzend zu dem Inhalt der RSC-Datei das erzeugte Listing. Je mehr Optionen hier angewählt werden, desto leistungsfähiger wird die in die LST-Datei eingebundene Bibliothek. Mit jeder Optionsanwahl vergrößern sich natürlich auch das Listing und der Speicherverbrauch des laufenden Programmes.

Für das Tetris-Spiel genügt das Anwählen zweier Optionen: Aus den Bibliotheken „Library & user" wird die Komponente „AES TimerEvent" benötigt. Hierbei handelt es sich um einen sauberen Ersatz der EVERY/AFTER-Befehle. Dies ist die Weckfunktion, mit der das automatische Herunterfallen des Steines erreicht werden soll. Die zweite Option bindet alle für das Fenster nötigen Quelltextteile ein. Dazu muss ein passender Fenstertyp definiert werden. Wir haben den Fenstertyp „fixwin" getauft, da es sich um ein Fenster fixer Größe handelt. Die anzuwählenden Fensterelemente für diesen Typ (vgl. Abbildung 4) entsprechen den Überlegungen aus Phase 1: Title, Closer, Mover und Info Line.

## Kursübersicht

Teil 1. Grundlagen, Fenster fester Grösse, Tastatureingaben, Echtzeitprogrammierung

Teil 2. Strukturierte Informationsspeicher mit Hilfe der OT/OB-Lib, größenveränderbare Fenster mit scrollbarem Inhalt

Teil 3. Mauseingaben, Selektion, nachscrollende Fenster

Teil 4. Menüs, Dialogfenster, Werkzeugleisten

Teil 5. Modulares Programmieren: Die Wrinkles

Im nächsten Kursteil wird auf die anderen Einstellungen dieses Dialoges näher eingehen. Über die Funktion „Save LST" kann nun die LST-Datei erzeugt werden.

Phase 3: die Programmierarbeit

Die erzeugte LST-Datei wird in den GFA-Editor über den Menüpunkt „Merge" eingeladen. Es handelt sich bei dem Listing bereits um ein ausführbares GEM-Programm mit der angesprochenen Hauptschleifenarchitektur. Wenn Sie in Phase 2 allerdings auf die Menüzeile verzichtet haben, dann sollten sie mit der Ausführung noch warten: ohne Menüpunkt „Programm verlassen" wird die Hauptschleife zur Endlosschleife.

Unsere Arbeit wird sich dreiteilen in die Erstellung des Teils, der die Informationsverarbeitung vornimmt (der Tetris-Kern: Listing 1), in den Teil, der sich um die Benutzerschnittstelle, also um das Tetris-Fenster kümmert (Listing 2), und schließlich in die Anbindung dieser beiden Komponenten an die faceVALUE-Engine (Listing 3). Wie die Routinen des Tetris-Kerns funktionieren, kann unter Zuhilfenahme der Dokumentationen in den Tabellen 1 und 2 nachvollzogen werden. Es ist zu erkennen, wie die Steuervariable tetris_laeuft_! jeweils gesetzt und gelöscht und gleichzeitig die Stein-Fallautomatik an- und abgeschaltet wird. Im folgenden wird darauf eingegangen, wie das Tetris-Fenster realisiert und mit der Engine verbunden wird. Dabei wird das Fenster an den Tetris-Kern angeknüpft.

Fenster öffne dich!

Das Öffnen eines Fensters geschieht über die Bibliotheksfunktion win_open(). Nach dem Öffnen kann das Fenster über den Fensterindex eindeutig identifiziert werden. Etwas ungeschickt ist der Umstand, dass win_open() das sog. /Fensterhandle/ zurückliefert. Diese Kennung wird nur intern von der Engine benötigt, wenn das Fenster gegenüber dem GEM identifiziert werden muss. Wie im Listing 2 in der Prozedur tetriswin_open dargestellt, sollte man sich aus dem Handle sofort den Index errechnen lassen.

Die wichtigen, an win_open zu übergebenden Parameter sagen aus, von welchem Typ das Fenster ist (hier kommt der Bezeichner „fixwin" wieder ins Spiel), wo und in welcher Größe das Fenster geöffnet werden soll (Außenmaße), sowie welche Größe die Innenfläche haben soll (die Dokumentgröße). Da beim Tetris-Fenster keine Scrollbalken vorgesehen sind, werden vor dem Öffnen die Außenmaße über win_calc_wh() [3] so errechnet, dass das Fenster die Innenfläche passgenau aufnehmen kann. Die Größe der Innenfläche ergibt sich ihrerseits aus den Spielfeld- und den Spielsteingrößen. Vom Moment des Öffnens an überwacht die faceVALUE-Engine alle für das Fenster relevanten Ereignisse und reagiert - soweit möglich - automatisch darauf. Wenn es jedoch darum geht, dass der Fensterinhalt neu gezeichnet werden muss, dass ein Tastendruck ausgewertet werden soll, oder wenn der Benutzer das Fenster schließen möchte, dann leitet die Engine das Ereignis in die entsprechende Userroutine um. Die Userroutinen befinden sich in der Mitte des generierten Listings und bilden damit nicht nur optisch die Schnittstelle zwischen Generiertem und Programmiertem.

Listing 3 zeigt, wie dort auf die gemeldeten Ereignisse reagiert wird. Zunächst wird passend zum vom Ereignis betroffenen Fenster, und dann (im Listing 2 bei tetriswin_keyb zu sehen) nach dem Steuerzustand unterschieden. Auch das Weckereignis für die Fallautomatik wird von der Engine über eine Userroutine (user_aes_timer) gemeldet, wo als Reaktion das Fallen des Steines veranlasst wird.

Eingabe: die Tastaturauswertung

Die Auswertung von Tastendrücken ist allgemein sehr leicht durchzuführen. Die Engine übermittelt an user_keyb() den ASCII- und den SCAN-Code der gedrückten Taste, sowie den Zustand der Tastaturumschalttasten. Entsprechend werden diese Ereignisse durch tetriswin_keyb in Steinbewegungen bzw. Steuerbefehle (Spiel starten oder stoppen) umgesetzt.

## Tabelle 1: Speichervariablen des Tetrisspiels

Steuervariablen

tetris_laeuft_! FALSE = warten auf Spielbeginn, TRUE = Spiel läuft

Operationsvariablen

Tetris-Kern

tetris_&(x,y) Spielfeldmatrix; je Feldelement gilt: 0 = leeres Feld, 1 -7 = Farbe des Feldes
tetris_faellt_&(x,y) Matrix, die den fallenden Stein beschreibt: 0 = leeres Feld, 1-7 = Farbe des Feldes

tetris_faellt_x_&
tetris_faellt_y_& aktuelle Position des fallenden Steines tetris_punkte_% Punktezahl, die der Spieler aktuell erreicht hat

Tetris-Fenster

tetriswin_index_& Fensterindex des Tetrisfensters (-1 = geschlossen)

Ausgabe: der Fensterinhalt

Die Ausgabe in GEM-Fenster ist ebenfalls einfach zu realisieren, denn sie erfolgt über die normalen GFA-Grafikbefehle, wie etwa CIRCLE, BOX, LINE oder TEXT. Allerdings haben GEM-Fenster einen flüchtigen Inhalt. Das Betriebssystem merkt sich nicht, was einmal in ein Fenster hineingezeichnet wurde. Jede Anwendung muss jederzeit in der Lage sein, den Fensterinhalt durch erneutes Zeichnen wieder herstellen zu können. Dadurch wird viel Grafikspeicher gespart, dieser Umstand bringt den Programmierer aber in die Verlegenheit, den Fensterinhalt komplett in Operationsvariablen ablegen zu müssen. In den Teilen zwei und drei dieses Kurses werden wir noch ausführlicher darauf eingehen.

Wichtig zu wissen ist für den Anfang, dass ständig ein Redraw-Ereignis eintreten kann, bei dem ein Ausschnitt des Fensterinhaltes neu gezeichnet werden muss. Die Engine meldet ein solches Ereignis durch Aufruf der Prozedur user_win_content. Im Beispiel wird dort durch Aufruf die Routine tetriswin_content darauf reagiert. Das, was diese Routine dann etwas komplizierter macht, ist eine dort vorgenommene Optimierung: Die Prozedur ermittelt aus dem zu zeichnenden Rechteck die betroffene Fläche des Spielfeldes (feld_xy/12&) und lässt vom Tetris-Kern tatsächlich nur diese Fläche zeichnen. Für heute sollten Sie über diese Berechnung hinwegsehen und sich nur damit befassen, wie der Fensterinhalt in tetris_draw tatsächlich auf den Bildschirm gebracht wird.

Die faceVALUE-Engine hat bereits eine Koordinatenverschiebung vorgenommen, sodass der Koordinatenursprung nicht in der linken, oberen Bildschirmecke, sondern in der linken, oberen Fensterecke liegt. Zudem wurde das sog. Clipping aktiviert, wodurch die Ausgaben automatisch auf das Fenster beschränkt werden. Es lässt sich daher mit den normalen GFA-Grafikbefehlen so zeichnen, als habe man den normalen Bildschirm - allerdings in Größe des Fensters - vor sich.

Am leichtesten erlernt man dieses Verhalten „by doing". Wer den Quelltext aus der Routine tetriswin_content einmal entfernt und durch einfache Grafikbefehle (wie CIRCLE 50,50,50) ersetzt, wird bald begreifen, wie einfach die Ausgabe in GEM-Fenster tatsächlich ist. Ein Tipp: Man sollte dort stets zuerst mit einer PBOX das komplette „Blatt" weiß malen und sich dann künstlerisch darauf austoben - wie einst auf dem Bildschirm.

Frisch gestrichen!

Zwischen dem Tetris-Kern und dem Tetris-Fenster gilt es abschließend noch eine wichtige Verbindung anzusprechen: Im Fenster wird nämlich das dargestellt, was in den Operations- und Steuervariablen des Kerns festgehalten wird. Diese Informationen liegt also zweimal vor: einmal in den Variablen und einmal im Fenster. Man spricht in solchen Fällen von Redundanz. Da beide Informationsspeicher aber stets synchron sein müssen, muss der Tetris-Kern nach Änderungen an seinen Variablen immer veranlassen, dass diese Änderungen auch in das Fenster übertragen werden. Deshalb werden nach solchen Änderungen vom Kern aus (Listing 1) jeweils tetriswin_redraw bzw. tetriswin_info angesprungen (Listing 2), wo die Aktualisierungen vorgenommen werden.

GFA-Listing 3

PROCEDURE user_rsc_var_init 
    @tetris_init 
    @tetriswin_init 
RETURN
PROCEDURE user_on_open 
    @tetriswin_open 
RETURN 
PROCEDURE user_window_content(index&,userhandle&,off_x%,off_y%,cx&,cy&,cw&,ch&)
    IF index&=tetriswin_index_&
        @tetriswin_content(off_x%,off_y%,cx&,cy&,cw&,ch&)
    ENDIF
RETURN
PROCEDURE user_keyb(handle&,userhandie&,index&,ks&,key&)
    IF index&=tetnswin_index_& 
        @tetriswin_keyb(ks&,key&)
    ENDIF
RETURN
FUNCTION user_win_close_ok(index&,userhandle&) 
    $F%
    IF index&c=tetriswin_index_&
        @tetris_stop
        LET exit_program!=TRUE 
    ENDIF
    RETURN TRUE 
ENDFUNC
PROCEDURE user_aes_timer 
    @tetris_falle 
RETURN

Ausblick

Nachdem der erste Teil Grundlegendes behandelt hat, wird im kommenden Monat die Thematik der Fensterausgabe weiter vertieft. Gleichzeitig werden wir die OT/OB-Lib zu Hilfe nehmen, um strukturierte Informationsspeicher anzulegen. Das Ergebnis soll ein kleines „DTP-Fenster" sein: Ein Fenster, in dem verschiedene Grafikobjekte angezeigt werden.

[1] RUN! Software - friendly applications Vorgartenstraße 9, D-66424 Homburg, info@run-software.de, http://www.run-software.de

[2] Operations- vs. Steuerzustand, Siegfried Wendt 1998, http://www-agwendt.eit.uni-kl.de/ WWW-AG-Wendt/Mosaiksteine/OP-ST-Zustand_Auswahl.html

[3] Verfügbar ab faceVALUE 3.1 oder per Download unter http://www.run-software.de

## Tabelle 2: Unterprogramme des Tetris-Kerns

tetris_init Operationsvariablen anlegen

tetris_new Operationsvariablen für ein neues Spiel einrichten

tetris_start Spiel starten

fallender Stein

tetris_neuer_stein neuen Stein ausdenken und Fallautomatik starten

tetris_faellt(x,y) ermitteln, ob sich der fallende Stein derzeit über das Feld (x,y) des Spielfeldes ausdehnt

Fallautomatik

tetris_langsam Fallautomatik mit Geschwindigkeit "langsam" starten

tetris_schnell Fallautomatik mit Geschwindigkeit "schnell" starten

tetris_stop Fallautomatik stoppen

Steinbewegung

tetris_links,
tetris_rechts,
tetris_drehe den Stein, wenn möglich, entsprechend bewegen

tetris_falle den Stein nach unten bewegen; falls unmöglich Stein absetzen

tetris_kollision ermitteln, ob der Stein an aktueller Position Platz hat

tetris_stein_hinsetzen Stein in das Spielfeld übernehmen, volle Zeilen löschen, Punkte erhöhen, ermitteln, ob das Spielfeld überläuft und ggf. einen neuen Stein ins Spiel bringen oder das Spiel beenden

tetris_draw ein Rechteck des Spielfeldes an Bildschirmposition (off_x, off_y) zeichnen; die Steingröße wird in (w, h) übergeben


Holger Herzog
Aus: ST-Computer 08 / 2000, Seite 34

Links

Copyright-Bestimmungen: siehe Über diese Seite