Bewegung mit der gfx-Lib


Inhalt dieses Kapitels

Einleitung

Wo wollen wir hin? Laden Sie einmal folgendes Programm herunter und lassen es laufen (Entpacken in ein beliebiges Verzeichnis und demo36.exe laufen lassen). Sie sollten dazu aber über eine Auflösung von mindestens 1024x768 (XGA) verfügen. Mit mehrmaligem Drücken der ESC-Taste verlassen Sie es. Diese Demo gewinnt vielleicht nicht den nächsten Schönheitswettbewerb, aber man kann folgendes daran entdecken:

Nun, trauen Sie sich es zu, das gleich nachzubauen? Dann mal gleich ran ans Werk!

Ansonsten werden wir jetzt hier Schritt für Schritt die Entwicklung eines solchen Programms durchgehen.

Zur Arbeit mit FBIDE und externen Dateien

Wenn Sie die folgenden Beispiele einfach nach FBIDE in eine leere Datei kopieren und ausführen, werden Sie Fehlermeldungen a la "Datei nicht gefunden" bekommen. Der Grund dafür ist, dass beim "Quick run" eine temporäre Datei in einem speziellen Arbeitsverzeichnis erstellt wird. Dort werden sich aber in der Regel nicht die Dateien befinden, die das Programm einlesen will. Daher ist es zu empfehlen, in den Beispielen die relativen Pfade (z.B. "spritered.bmp") durch absolute Pfadangaben zu ersetzen.

Bitmaps mit der gfx-Lib

Bitmaps laden: IMAGECREATE, IMAGEDESTROY, BLOAD, PUT

Wir hatten uns in Kapitel 2 schon einmal mit Sprites befasst. Damals waren sie noch sehr primitiv ausgefallen, weil man sie mittels BASIC-Befehlen "zeichnen" musste. Keine so geniale Methode, um etwas zu produzieren, was einigermassen gut ausschaut. Sehr viel besser ist die Benutzung eines Malprogramms wie Gimp, Paint Shop oder wenigstens das einfache MSPaint, das bei Windows dabei ist. Aber wie bekommen wir das Malergebnis in QBasic, bzw. jetzt in Freebasic? Bei QBasic ist das schwierig, da es nur ein Grafikspeicherformat beherrscht - sein eigenes. Von dem weiss wiederum kein Malprogramm etwas. Natürlich gibt es einen Weg: Man liest die Datei, die das Malprogramm generiert hat, als Binary File ein, zeichnet jedes Pixel auf den Bildschirm und speichert das Ergebnis im QBASIC-eigenen Format wieder ab. Bleibt die nicht ganz unwesentliche Frage: Welche Daten stehen wo in dieser Malprogramm-Datei?

Bei Freebasic sieht es schon besser aus. Das Standard-Bitmap-Format auf Windows ist "bmp". Dies beherrscht jedes Malprogramm. Und Freebasic kennt es auch.

Zum Anzeigen eines bmp-Files auf dem Bildschirm brauchen wir vier Befehle. IMAGECREATE, BLOAD, PUT und IMAGEDESTROY. Und einen neuen Datentyp: ANY PTR

Schauen wir uns zunächst ein kleines Beispielprogramm an. Damit es läuft, müssen Sie die obige Demo installiert haben. Dann befindet sich die Testdatei "scblue_do.bmp" im Demoverzeichnis.

SUB test1
  DIM img1 AS ANY PTR
  SCREEN 18,24
  img1 = IMAGECREATE(68, 87)
  BLOAD "scblue_do.bmp",img1
  PUT (200,200),img1,PSET
  SLEEP
  IMAGEDESTROY img1
END SUB

Was passiert hier? Zuerst wird eine Variable vom Typ "ANY PTR" deklariert. Das ist ein Zeiger. Dieser zeigt allerdings nicht auf ein File oder auf ein Array, sondern auf eine Stelle im Hauptspeicher. Der Hauptspeicher, in dem alle Programme, Variablen und sonstige Daten der laufenden Programme gerade gehalten werden, ist praktisch eine unstrukturierte Menge von Bytes. Jedes Bytes trägt eine Adresse, genau wie die Bytes in einem File. Ein "Heap Pointer", ein Speicherzeiger ist nichts anderes als ein 32-bit-Integer mit einer solchen Adresse. Wir deklarieren hier also einen solchen Zeiger und nennen ihn img1. Zu diesem Zeitpunkt ist noch nicht klar, auf welche Adresse img1 zeigt.

Das nächste ist die SCREEN-Anweisung, das kennen wir schon. Dann kommt IMAGECREATE. Das fragt beim Betriebssystem freien Platz im Hauptspeicher an. Und zwar soviel, wie ein bmp-Bild mit 68x87 Pixeln braucht. 68x87 Pixel hat unsere Testbitmap. Wir könnten hier aber auch mehr angeben, z.B. 100x100. Nur nicht weniger. Das würde später eventuell zu einem Crash unseres Programms führen.

So. Hat die Sache geklappt, gibt IMAGECREATE() einen Zeiger auf den freien Speicherplatz zurück, den wir für das Bild brauchen. Und die Adresse dieses freien Speicherplatzes kommt in img1.

Wenn Sie richtig hinschauen, haben wir hier natürlich einen Fehler gemacht: Wir haben nicht geprüft, ob die Sache geklappt hat. Das ist allerdings für diese Demozwecke verzeihlich, da im Zeitalter von Hauptspeichern mit mehreren Hundert Megabyte es nicht wahrscheinlich ist, dass keine 2 oder 3 Kilobyte mehr für unser Bild übrig sind. Sollten Sie allerdings variable Bilder zulassen, dann muss diese Prüfung unbedingt erfolgen. Hat das Betriebssystem keinen Speicher, dann gibt IMAGECREATE die Adresse 0 zurück.

So, nun kommt der dritte Befehl, BLOAD. Dieser lädt nun die bmp-Datei in den reservierten Speicher img1. Das dürfte selbsterklärend sein. Die Frage bleibt jetzt noch, wo und wann die Grafik auf dem Bildschirm angezeigt werden soll. Das erledigt der Befehl "PUT": PUT (x,y),img,relation. (x,y) ist die Bildschirmkoordinate der linken oberen Ecke. "img" ist der Zeiger auf den Speicherbereich der Grafik. "relation" kann ziemlich verschiedene Werte annehmen, Details können Sie beim Befehl PUT(Grafik) in der unteren Tabelle abrufen. Der Wert PSET sorgt dafür, dass die Pixel der Grafik die sonst schon vorhandenen Pixel auf dem Bildschirm einfach überschreiben. Möglich sind aber auch OR, das eine bitweise OR-Verknüpfung zwischen Bildschirm und Grafik durchführt. Oder AND. Bis hin zu recht komplizierten Sachen mit dem s.g. Alpha-Kanal und benutezrdefinierten Filtern. Aber uns reicht hier ersteinmal PSET.

Damit sollte das Bild auf dem Bildschirm aufgetaucht sein. Benötigen Sie den Grafikspeicher nicht mehr, ist es nett, diesen beim Betriebssystem wieder zur Verfügung zu stellen. Dies geschieht mit IMAGEDESTROY und Angabe des Zeigers. Wieviel Speicher dort reserviert war, weiss das Betriebssystem selbst. Schlaues Kerlchen, das...

Das Ergebnis ist ganz nett, aber noch verbesserungsfähig. Nehmen wir an, das blaue Dingsda ist so etwas wie ein Raumschiff. Dann wäre es wünschenswert, wenn der weisse Kasten drumherum verschwinden würde. Aber wie dies anstellen? Nun gut, man kann im Malprogramm die Aussenfläche schwarz malen. Aber was ist, wenn der Hintergrund auf dem Bildschirm dann plötzlich rot ist? Der Kasten sollte durchsichtig sein. Das wär's. Freebasic hat mit solchen Durchsichtigkeiten keine grossen Probleme, aber die Grafik selbst darauf vorzubereiten, ist nicht ganz so einfach. Damit beschäftigen wir uns bald.

Bitmaps speichern: GET und BSAVE

Eigentlich wollen wir die Bitmaps ja mit einem Malprogramm erzeugen. Aber für Testzwecke ist die Erzeugung via Programm einfacher. Wie bekommen wir die Bitmap vom Schirm in eine bmp-Datei?

Übung:

Zeichne ein Schachbrettmuster, 300 x 300 Pixel, immer abwechselnd ein Pixel rot und eines schwarz.

Lösung:

SUB testbmp
    'plottet eine Test-bmp-Grafik und speichert sie

    DIM ix,iy
    DIM img1 AS ANY PTR
    SCREEN 18,24

    'Plotte Testgrafik: Karo
    FOR ix=0 TO 299
      FOR iy=0 TO 399
        PSET (ix,iy),RGB(((ix+iy) MOD 2)*255,0,0)
      NEXT iy
    NEXT ix

    'Speichere die ersten 10 x 10 Pixel links oben
    img1 = IMAGECREATE(40, 40)
    GET (0,0)-(39,39),img1
    BSAVE "test1.bmp",img1,30000 'Scheint ziemlich egal zu sein, was man hier
                                    'als Groesse angibt
    IMAGEDESTROY img1
    SLEEP

END SUB

Die ersten Zeilen plotten eine Rastergrafik - das Testsprite der Demo am Anfang. Dann wird wieder ein Speicherbereich angelegt, wie auch schon beim letzten Beispiel. Mit GET (x1,y1)-(x2,y2),img wird der Bildschirmausschnitt zwischen (x1,y1) links oben und (x2,y2) rechts unten in den Speicherbereich img gelegt. Dann wird mit BSAVE dieser Bereich im bmp-Format in eine Datei geschrieben: BSAVE name,img,size. name=Dateiname, img=Zeiger auf die Grafik, size=Grösse in Bytes. size scheint aber irgendwie egal zu sein; freebasic nimmt kurzerhand die Grösse der Grafik, wie sie aus GET resultiert - das ist praktisch.

bmp-Bitmaps konvertieren

So, und nun kommen wir zum entscheidenden Problem: Wir wollen die schwarz gezeichneten Pixel durchsichtig haben. So dass der Huntergrund durch unser Sprite immer dort durchschimmert, wo zur Zeit Schwarz ist. Zum Glück nimmt uns dabei Freebasic die Hauptarbeit ab. Es kennt nämlich den PUT-Modus TRANS. Er funktioniert wie PSET, nur dass eine ganz bestimmte Farbe, die Farbe RGB(&HFF,0,&HFF), als "Maskenfarbe" für das "Durchsichtige" dient. Überall, wo diese Maskenfarbe ist, scheint später der Hintergrund durch. &HFF steht für Hex FF. Unsere ganze Arbeit besteht also darin, die Farbe Schwarz in der Bitmap gegen diese Farbe auszutauschen.

Die einfachste Variante wäre in unserem Fall natürlich, das Karo gleich als Mischung aus RGB(&HFF,0,0) und RGB(&HFF,0,&HFF) zu plotten. Aber das ginge am Ziel vorbei: Wir wollen ja soweit kommen, eine extern gemalte bmp-Grafik als Sprite verwenden zu können. Also müssen wir in der Lage sein, in einer externen bmp-Grafik alle Pixel einer bestimmten Farbe, z.B. Schwarz oder Weiss, in diese Transparentfarbe umzuwandeln. Das kann man im externen Malprogramm natürlich von Hand machen und man kann dabei auch sehr gut verrückt werden. Besser ist es, das per Programm zu machen. Und so eins fertigen wir uns jetzt an.

Die Schwierigkeit ist, das Format von bmp-Files zu kennen. Nun, da gibt es zwei Wege. Der erste ist, eine eminent wichtige Webseite für Programmierer zu kennen und dort das Format nachzuschauen. Der zweite Weg ist, per Experiment und Hex-Editor herauszufinden, wie es aufgebaut ist. Das bmp-Format ist einfach genug, dass das nicht zu schwer ist, vor allem bei True-Color-Bitmaps nicht. Ah ja: Sie müssen natürlich darauf achten, im externen Zeichenprogramm die bmp-Grafik mit 24-Bit Farbtiefe abzuspeichern, alles andere würde nicht funktionieren.

Egal, welchen Weg Sie einschlagen, nach kurzer Zeit werden Sie folgendes Ergebnis haben:

Eine 24-Bit-bmp-Datei besteht aus einem Header und den eigentlichen Grafikdaten. Die Grafikdaten bestehen im Wesentlichen aus den RGB-Farbcodes der einzelnen Bildpunkte, die einfach aneinandergereiht sind. Na ja, fast einfach. Auf einen kleinen Haken kommen wir gleich noch. Der Header ist folgendermassen aufgebaut:

StartLängeBedeutung
&H124Breite in Pixel
&H164Höhe in Pixel

Lediglich dies und dass der gesamte Header 54 Bytes lang ist, müssen Sie wissen.

Der Haken: Die Zeilenlänge

Der Haken im Grafikteil ist die Zeilenlänge. Jede Zeile muss nämlich ein Vielfaches von 4-Byte als Länge haben. Wird diese Länge nicht von der Bildinformation benötigt, muss sie aufgefüllt werden. Ist z.B. die Grafik 10 Pixel breit, sind dies 3 x 10 = 30 Bytes. Das nächste 4-Vielfache ist aber 32 Bytes. Also muss man jeweils ncoh zwei Nullbytes ans Zeilenende schreiben.

Für die folgenden Übungen noch ein Tipp: Fertigen Sie sich zwei Routinen an. Eine, die einen RGB-Wert in einen 3-Byte-String übersetzt. Und eine Routine, die das Umgekehrte macht. Ich habe diese rgb2str() und str2rgb() genannt.

Und noch ein Tipp: Benutzen Sie zur Betrachtung Ihrer Programmergebnisse einen guten Bildbetrachter, wie ACDSee oder Irfanview. Auch beim Totalcommander ist ein bmp-Betrachter im Lister dabei. Nur ein solcher Betrachter (am Besten zoomfähig) zeigt Ihnen schnell Ihre eventuellen Fehler auf.

Übung 2:

Schreiben Sie eine Routine zum Einlesen einer bmp-Grafik auf den Bildschirm - ohne Hilfe von BLOAD.

Übung 3:

Schreiben Sie eine Routine zum Rausschreiben einer bmp-Grafik auf den Bildschirm - ohne Hilfe von BSAVE.

Übung 4:

Schreiben Sie auf Basis des Codes der anderen beiden Übungen eine Routine, die eine bestimmte Farbe in einer bmp-Grafik gegen eine andere austauscht.

Meine Lösung von Übung3:

'-----------------------------------------------------

FUNCTION STR2RGB(s AS STRING) AS INTEGER

    DIM retval AS INTEGER=-1
    DIM i AS INTEGER, sum AS INTEGER, ix AS INTEGER

    IF (LEN(s)=3) THEN
      sum=0
      FOR i=1 TO 3
        ix=ASC(mid$(s,i,1))*(256^(3-i))
        'PRINT i,ix
        'sleep
        sum=sum+ix
      NEXT i
      retval=sum
    END IF

    STR2RGB=retval

END FUNCTION


'-----------------------------------------------------

FUNCTION RGB2STR(ix AS INTEGER) AS STRING

    DIM retval AS STRING="xxx" 'Dummy-Wert
    DIM i AS INTEGER, sum AS INTEGER, iy AS INTEGER, idiv AS INTEGER

    sum=ix
    FOR i=1 TO 3
      idiv=256^(3-i)
      iy=INT(ix/idiv)
      mid$(retval,i,1)=CHR$(iy)
      sum=sum-iy
    NEXT i

    RGB2STR=retval

END FUNCTION

SUB convertcolbmp(fname1 AS STRING, fname2 AS STRING, oldcolor AS INTEGER, newcolor AS INTEGER)

  'Konvertiert bei einer 24-Bit-bmp
  'alle colold Pixel in Pixel mit der Farbe
  'colnew

  CONST bmpheadsize=54
  CONST pos_xsize=&H12
  CONST pos_ysize=&H16


  DIM s AS STRING, icol AS INTEGER, k AS INTEGER
  DIM xsize AS INTEGER, ysize AS INTEGER, ix AS INTEGER, fuell AS INTEGER, rest AS INTEGER
  DIM cmd1 AS STRING

  OPEN fname1 FOR BINARY AS #1
  cmd1="del "+fname2
  SHELL(cmd1)
  OPEN fname2 FOR BINARY AS #2
  'Header lesen und schreiben
  s=INPUT$(bmpheadsize,1)
  'Lies x- und y-Breite aus
  xsize=ASC(mid$(s,pos_xsize+1,1))+ASC(mid$(s,pos_xsize+2,1))*256
  ysize=ASC(mid$(s,pos_ysize+1,1))+ASC(mid$(s,pos_ysize+2,1))*256
  PRINT #2,s;

  'Jede Zeile enthaelt ein Vielfaches von 32 Bit, also 4 Bytes.
  'Was nicht Bildinformation ist, muss aufgefuellt werden
  'Ist z.B. eine Zeile 10 Pixel breit ==> 10 x 3 = 30 Bytes ==> 2 Auffuellbytes
  'auf 32 Bytes.
  rest=(xsize*3) MOD 4
  fuell=(4-rest) MOD 4

  k=0
  WHILE NOT EOF(1)
    'Lies die naechste Zeile ein
    FOR ix=0 TO xsize-1
      'Lies das naechste Pixel (3 Bytes) ein
      s=INPUT$(3,1)
      '? ix,tx(s)
      'sleep
      'Pruefe, ob es der Farbe colold entspricht
      icol=STR2RGB(s)
      IF (icol=oldcolor) THEN
        s=RGB2STR(newcolor)
      END IF
      PRINT #2,s;
      k=k+3
    NEXT ix
    'Ueberspringe Fuellbytes
    s=INPUT$(fuell,1)
    PRINT #2,s;
  WEND

END SUB

Die Funktion, die jetzt in einer bmp-Grafik eine bestimmte Farbe zur Transparentfarbe macht, ist nur noch ein Sonderfall der obigen Routine:

SUB convert2transbmp(fname1 AS STRING, fname2 AS STRING, oldcolor AS INTEGER)

  convertcolbmp(fname1,fname2,oldcolor,&HFF00FF)

END SUB

Bewegte Sprites

Der Rest ist eigentlich nur noch Altbekanntes. Allerdings können wir es dieses Mal viel eleganter erledigen.

Wir nehmen uns vor, dass jedes Sprite für sich programmtechnisch möglichst unabhängig stehen soll. So liegt es nahe, einen Typ tsprite zu definieren. Jede tsprite-Variable enthält dann alle notwendigen Informationen zu einem Sprite. Über welche Informationen muss ein Sprite verfügen?

Mein Vorschlag:

Initialisierung

Eine Variable vom Typ tsprite bezeichnen wir jetzt schon einmal als "Objekt". Dieses Objekt muss initialisiert werden. Das heisst erstens, die Sprite-Bitmap zu laden und img darauf zeigen zu lassen. bg sollte auf die Nulladresse gesetzt werden, x, y kann man auch auf Null setzen, xsize und ysize stehen durch die Bitmap schon fest. Allerdings kann man sie nur ermitteln, wenn man den Bitmap-Header manuell ausliest, wie wir das im Vorabschnitt gemacht haben. bgsaved steht natürlich auf Null, ebenso die Koordinaten zum bg-Ausschnitt. Wenn wir dann ein Sprite starten lassen wollen, dann müssen wir lediglich x und y auf eine geeignete Position setzen.

NAN statt Null

Nebenbemerkung: Null als Defaultwert ist eigentlich nicht so geeignet. Ein Defaultwert sollte so gewählt sein, dass auf der einen Seite das Programm noch weiterläuft, auf der anderen Seite es aber so unsinnig weiterläuft, dass der Programmierer den Fehler sofort bemerkt. Null ist in unserem Zusammenhang bei Koordinaten keineswegs unsinnig und wir laufen Gefahr, den ein oder anderen Fehler zu übersehen. Daher verwende ich als Defaultwert von Koordinaten nicht Null, sondern eine Konstante, die ich NAN genannt habe - Not A Number. Paradoxerweise muss es natürlich durchaus eine Zahl sein; ich habe sie auf -999999 gesetzt. Wann immer ich irgendwo Kontrollausgaben zu den Koordinaten mache und ein NAN-Wert dabei verwickelt ist, werden die Ausgabenwerte extrem negativ werden und mir sofort sagen: "Da ist ein Sprite-Wert noch auf Default und muss noch sinnvoll besetzt werden."

Bewege Sprite

Als Nächstes brauchen wir eine Routine um ein Sprite zu bewegen. Die alte Position ist in (x,y) des Sprites notiert; der Routine muss also nur das Sprite und die neuen Koordinaten übergeben werden. Was muss diese Routine alles erledigen?

  1. Als erstes muss, falls ein solcher gespeichert ist, der Bildschirmhintergrund an der alten Stelle wieder hergestellt werden.
  2. Dann werden die Koordinaten auf die neue Position gestellt und der neue Hintergrund wird abgespeichert.
  3. Erst der dritte Schritt bildet dann das Sprite an der neuen Position ab - der leichteste von allen.

Es ist als erstes ratsam, diese Routine ersteinmal ohne Komplikationen zu realisieren, das heisst ohne das Clipping, den Fall, dass das Sprite schon halb aus dem Bildschirm hinausragt.

Den Start eines Sprites erledigt man einfach dadurch, dass man es zur ersten Bildschirmposition bewegt. Nun können Sie schon einmal die ersten Früchte geniessen und Ihre ersten Sprites in einem Testprogramm über den Bildschirm wandern lassen.

Zyklische Bewegungen: Polarkoordinaten

Schwingungen und Kreisbewegungen arten leicht in unübersichtliche Formelarbeit aus. Das kann man mit s.g. Polarkoordinaten verhindern. Dann wird die Beschreibung selbst kompliziert erscheinende Bewegungen ganz einfach. Nehmen wir z.B. eine Kreisbewegung, bei der die Länge des Radius hin- und herschwingt, eine s.g. Lissajous-Figur. In Polarkoordinaten ist dies ganz einfach auszudrücken, da der Radius eine Koordinate darstellt, wie die x-Koordinate im kartesischen System. Ein Koordinatenangabe in Polarkoordinaten lautet also: (r,phi). r ist der Radius, phi der Drehwinkel.

Nun kann man mit r und phi alles mögliche anstellen. Z.B. r=r{0}*cos(phi/10) bilden, was dazu führt, dass r fünfmal pro Kreisumdrehung hin- und herschwingt.

Um die Rückübersetzung kümmert sich dann eine s.g. Koordinatentransformation. Die schaut einfach so aus:

x = r * sin (phi)

y = r * cos (phi)

,

wenn phi der Winkel ist, den die positive x- und y-Achse einschliessen und sich im Uhrzeigersinn öffnet.

Clipping

Beim Darstellen eines Sprites muss man sich nicht gross drum kümmern, ob es noch vollständig auf den Bildschirm passt - das erledigt die gfx-Engine selbst. Sehr aufpassen muss man jedoch beim Abspeichern des Bildschirmhintergrunds. Hier nimmt es einem die gfx-Lib sehr übel, wenn man einen Bildschirmausschnitt angibt, der ganz oder teilweise nicht sichtbar ist. Daher kann man nicht einfach die Spriteposition plus Höhe und Breite des Sprites hernehmen, um den Bildschirmhintergrund wieder einzuladen, sondern muss explizit den zu sichernden Bereich notieren. Dazu dienen die bg-Koordinaten x1bg,y1bg,x2bg und y2bg. Wenn Sie nun die Sache das erste Mal implementieren, werden Sie vielleicht über den gleichen Strick stolpern wie ich. Bei mir funktionierte das Clipping das erste Mal fast perfekt. Fast. Das Ärgerliche war, dass manchmal ein 1-Pixel breiter Streifen des Sprites ganz unten oder oben am Bildschirmrand zurückblieb. Und es war nicht ganz trivial herauszubekommen, warum.

Meine Lösung

Haben Sie die Aufgabe gelöst? Nun, hier ist meine Realisierung:

#INCLUDE "fbgfx.bi"
OPTION EXPLICIT

CONST NAN=-999999
CONST xscreensize=800
CONST yscreensize=600
CONST FALSE=0
CONST TRUE=-1
CONST pi=22/7

'-----------------------------------------------------

FUNCTION min(i AS INTEGER, j AS INTEGER) AS INTEGER
  IF (i<j) THEN min=i ELSE min=j
END FUNCTION

'-----------------------------------------------------

FUNCTION max(i AS INTEGER, j AS INTEGER) AS INTEGER
  IF (i>j) THEN max=i ELSE max=j
END FUNCTION

'-----------------------------------------------------

TYPE tsprite
  img AS ANY PTR
  bg AS ANY PTR
  x AS INTEGER
  y AS INTEGER
  xsize AS INTEGER
  ysize AS INTEGER
  x1bg AS INTEGER
  y1bg AS INTEGER
  x2bg AS INTEGER
  y2bg AS INTEGER
  NAME0 AS STRING*80
  bgsaved AS INTEGER
END TYPE




'-----------------------------------------------------

SUB initsprite(sprite AS tsprite)
  sprite.img=0
  sprite.bg=0
  'Koordinaten sollten auf NAN stehen, wenn das Sprite gerade nicht
  'gesetzt ist.
  sprite.x=NAN:sprite.y=NAN
  sprite.xsize=0:sprite.ysize=0
  sprite.x1bg=NAN:sprite.x2bg=NAN:sprite.y1bg=NAN:sprite.y2BG=NAN
  sprite.bgsaved=NAN
END SUB


'-----------------------------------------------------
SUB loadsprite(sprite AS tsprite, fname AS STRING)

  CONST bmpheadsize=54
  CONST pos_xsize=&H12
  CONST pos_ysize=&H16

  DIM s AS STRING

  'Pruefe, ob der Dateiname die Endung ".bmp" hat.
  DIM s1 AS STRING=RIGHT$(fname,4)
  IF (s1<>".bmp") THEN
    PRINT("Error in loadsprite(): suffix of fname must be .bmp")
    SLEEP
    END
  END IF

  'Lies vorab den Header des bmp-Files ein, um die
  'x- und y-Groesse festzustellen.
  OPEN fname FOR BINARY AS #1
  'Header lesen
  s=INPUT$(bmpheadsize,1)
  IF (LEN(s)=0) THEN
    PRINT "Error in loadsprite(): File not found: ";fname
    SLEEP
    END
  END IF
  'Lies x- und y-Breite aus
  sprite.xsize=ASC(mid$(s,pos_xsize+1,1))+ASC(mid$(s,pos_xsize+2,1))*256
  sprite.ysize=ASC(mid$(s,pos_ysize+1,1))+ASC(mid$(s,pos_ysize+2,1))*256
  CLOSE #1

  'Lies hier die eigentlichen Daten in einen zuvor reservierten
  'Speicherbereich ein
  sprite.img = IMAGECREATE(sprite.xsize+10, sprite.ysize+10)
  sprite.bg = IMAGECREATE(sprite.xsize+10, sprite.ysize+10)
  BLOAD fname,sprite.img
  sprite.NAME0=fname

END SUB

'-----------------------------------------------------

SUB savegetbg(sprite AS tsprite)

  'Speichere den Background so, dass das Sprite sich auch teilweise
  'ausserhalb des Screens befinden kann.
  'Dazu muss an GET der Ausschnitt weitergegeben werden, der
  'sich noch im Screen befindet.


  DIM x1 AS INTEGER,y1 AS INTEGER,x2 AS INTEGER,y2 AS INTEGER
  DIM visible AS INTEGER

  'Stelle zunaechst den Ausschnitt des Sprites fest, der sich innerhalb
  'des Bildschirms befindet.
  x1=min(max(sprite.x,0),xscreensize-1)
  y1=min(max(sprite.y,0),yscreensize-1)
  x2=min(max(sprite.x+sprite.xsize-1,0),xscreensize-1)
  y2=min(max(sprite.y+sprite.ysize-1,0),yscreensize-1)

  'Befindet sich das Sprite komplett ausserhalb des Screens?
  visible=(sprite.x+sprite.xsize-1>=0 AND sprite.y+sprite.ysize-1>=0)
  visible=visible AND (sprite.x<=xscreensize-1) AND (sprite.y<=yscreensize-1)

  'Speichere nun ab
  IF (NOT visible) THEN
    sprite.bgsaved=0 'Zeigt an, ob ein Hintergrund gespeichert ist
  ELSE
    GET (x1,y1)-(x2,y2),sprite.bg
    sprite.bgsaved=1
  END IF

  sprite.x1bg=x1:sprite.x2BG=x2:sprite.y1bg=y1:sprite.y2bg=y2

END SUB

'-----------------------------------------------------

SUB movesprite(sprite AS tsprite, xnew AS INTEGER, ynew AS INTEGER)

  'Bewegt das Sprite zur Position xnew/ynew
  'xnew und ynew werden ggf. so korrigiert, dass das ganze Sprite
  'auf den Bildschirm passt.

  'Ist das Sprite an einer alten Stelle?
  IF (sprite.x<>NAN) THEN
    'Ist ein Background bzgl. der alten Position gespeichert?
   IF (sprite.bg>0 AND sprite.bgsaved=1) THEN
      'Restauriere Background
      PUT (sprite.x1bg,sprite.y1bg),sprite.bg,PSET
    ELSE
      'Sprite befindet sich komplett ausserhalb des Screens
    END IF
  END IF

  'Speichere Hintergrund ab
  sprite.x=xnew:sprite.y=ynew
  savegetbg(sprite)

  'Setze Sprite an neuer Stelle
  PUT (sprite.x,sprite.y),sprite.img,TRANS


END SUB

'-----------------------------------------------------

SUB closesprite(sprite AS tsprite)

  IMAGEDESTROY sprite.img
  IF (sprite.bg>0) THEN IMAGEDESTROY sprite.bg

END SUB


'-----------------------------------------------------

SUB testscreen

  DIM ix AS INTEGER, iy AS INTEGER, i0 AS INTEGER

  SCREEN 19,24

  'Plotte Hintergrundgrafik
  FOR ix=0 TO 79
    FOR iy=0 TO 59
      i0=((ix+iy) MOD 2)
      LINE (ix*10,iy*10)-((ix+1)*10-1,(iy+1)*10-1),RGB(i0*200,i0*200,i0*200),BF
    NEXT iy
  NEXT ix
END SUB


'-----------------------------------------------------

SUB main1a

  DIM spritered AS tsprite, spritegreen AS tsprite, spriteblue AS tsprite
  DIM ix AS INTEGER, ix1 AS INTEGER, iy AS INTEGER,isig AS INTEGER, i AS INTEGER, _
       rblue AS DOUBLE, rgreen AS DOUBLE, phi AS DOUBLE

  testscreen
  initsprite(spritered)
  loadsprite(spritered,"testred.bmp")
  initsprite(spritegreen)
  loadsprite(spritegreen,"test3.bmp")
  initsprite(spriteblue)
  loadsprite(spriteblue,"testblue.bmp")

  ix1=0
  isig=1
  i=0
  WHILE (1=1)
    iy=300-350*SIN(ix1/800*8*pi)
    movesprite(spritered,ix1,iy)
    'Polarkoordinaten
    phi=i/400*2*pi
    IF (i=400) THEN i=0
    'Blaues Sprite laeuft auf konstantem Umkreis
    rblue=250
    'Gruenes Sprite pendelt 30 mal hin und her ==>> Lissajousfigur
    rgreen=250*SIN(phi*10)
    'Umwandlung blaue Koordinaten in kartesische Koordinaten
    ix=xscreensize/2+rblue*SIN(phi)
    iy=yscreensize/2-rblue*COS(phi)
    movesprite(spriteblue,ix,iy)
    'Umwandlung gruene Koordinaten in kartesische Koordinaten
    ix=xscreensize/2+rgreen*SIN(phi-1)
    iy=yscreensize/2-rgreen*COS(phi-1)
    movesprite(spritegreen,ix,iy)

    IF MULTIKEY(SC_ESCAPE) THEN EXIT WHILE
    ix1+=isig*2
    i+=1
    IF (ix1>=xscreensize-5 OR ix1<=0) THEN
      isig=-isig
    END IF
    SLEEP 20
  WEND
  SLEEP
  closesprite(spritered)
  closesprite(spritegreen)
  closesprite(spriteblue)
END SUB

Der Fallstrick befindet sich in savegetbg(), wenn man die Sichtbarkeitsprüfung (visible=....) weglässt.

Kollidierende Sprites

Wenn Sie nun einige Sprites gleichzeitig auf dem Bildschirm herumflitzen lassen, dann werden diese recht bald komische Farbflecken darauf hinterlassen. Der Grund ist auch klar: Überlappt ein Sprite plötzlich mit einem anderen, überlappen sich auch ihre Restaurationsversuche des Bildschirmhintergrunds. Da passiert es dann recht schnell, dass das Bild des einen Sprites in den bg-Speicher des anderen gerät. Dazu bewegen sich die Sprites dann noch - da entsteht Salat.

Es gibt drei Möglichkeiten, damit umzugehen.

  1. Man verhindert das Überlappen generell. Dann muss man eine s.g. "Kollisionsüberwachung" einbauen.
  2. Man lässt die Überlappung zu und aktualisiert laufend den bg-Speicher aller beteiligten Sprites. Sehr anspruchsvoll, aber möglich.
  3. Man regelt den Bildschirmaufbau grundsätzlich anders und zeichnet ihn in jeder Schleifenrunde komplett neu. Dann entfällt das ganze bg-Speicherproblem, allerdings steigt der Rechenaufwand erheblich.

Die Betrachtung dieser drei Alternativen ist gleichzeitig ein Blick in die Computergeschichte. Alternative I) werden wir hier behandeln und sie ist auch heute noch für viele 2D-Spiele und 2D-Anwendungen die Grundlage. In früheren Computern waren Sprites und "Collision Detection" bereits in den Grafikchips eingebaut, z.B. beim Commodore Amiga. Genauer gesagt ist der Begriff "Sprite" eigentlich nur für solche hardwareimplementierten Grafikobjekte korrekt. Das, was wir hier behandeln, die reine Softwarelösung, heisst korrekt eigentlich "Shape". Aber da der Begriff "Sprite" sehr viel weiter verbreitet ist und sich auch über seine einstmals spezifische Bedeutung hinaus verbreitet hat, bin ich hier auch mal nicht so genau gewesen. Jedenfalls spielte die Collision Detection bei Sprites im eigentlichen Sinn früher eine grosse Rolle und auf ihr basierten fast alle Action- und Animationsspiele bei Atari, C64 und Co.

Alternative II ist eher exotisch.

Alternative III ist die heutige Standardmethode. Das hat einen einfachen Grund: Echte 3D-Darstellung ist anders praktisch nicht möglich. Der 3D-Raum muss immer von hinten nach vorne auf den 2D-Schirm gebracht werden. Was hinten ist und was vorne, das bestimmt oft eine vierte Koordinate, der s.g. z-Buffer.

Zurück zur Alternative I: Die Grundaufgabe besteht darin, festzustellen, ob Sprite 1 mit Sprite 2 einen überlappenden Bereich hat. Allerdings soll dieser Test erfolgen, bevor ein Sprite an einer neuen Stelle gezeichnet wird. Daher müssen wir die Aufgabe etwas umformulieren: Wir müssen feststellen, ob Sprite 1 mit Sprite 2 überlappen würde, wenn wir Sprite 1 an den neuen Koordinaten xnew, ynew zeichnen würden. Wenn das der Fall wäre, tun wir's eben nicht und die Collision detection ist erledigt. Dabei stellt sich die Frage, wie man so eine Überlappung feststellen kann. Nun, eigentlich ganz einfach: Man muss feststellen, ob eine der Ecken von Sprite 1 innerhalb von Sprite2 liegt. Und umgekehrt. Das sind also insgesamt acht if-Abfragen. Man kann das sicher auch noch abstrakter lösen, aber so tut's uns das.

Die nächste Aufgabe: Das Ganze muss ja nicht nur für ein Spritepaar erledigt werden. Wir brauchen eine Routine, die ein Sprite gegen alle anderen Sprites testet, informatischer gesprochen, ein Sprite gegen ein ganzes Array von Sprites. Dabei ist es nützlich, dies als Funktion zu gestalten und diese die Nummer des Sprites aus dem Array zurückgeben zu lassen, das die Kollision verursacht.

Die dritte Aufgabe: Wir müssen uns überlegen, wie wir mit der Kollisionsinformation umgehen. Die erste und einfachste Möglichkeit ist, das Sprite dann einfach nicht zu zeichnen. Erweitern Sie Ihr Programm ersteinmal so!

Wenn Sie das dann getan haben, werden Sie allerdings feststellen, dass das nicht so gut aussieht, da dann die Sprites bei Kollisionen die anderen "überhüpfen". Besser wäre es, sie würden an ihrer alten Position warten wie ein Auto an einer geschlossenen Bahnschranke. Wenn Sie bisher alle Sprites innerhalb einer einzigen Schleife bewegt haben, dann ist dieses Konzept hier allerdings am Ende. Wegen den Wartevorgängen muss jedes Sprite seine eigene, individuell voranschreitende Bewegungsschleife, gewissermassen seine eigene Uhr haben.

Ich habe das so realisiert, dass ich jedes Spriteobjekt durch ein anderes Objekt "umhüllt" habe, das die "Uhr" enthält:

'-----------------------------------------------------

TYPE torigin
  x AS INTEGER
  y AS INTEGER
END TYPE

'-----------------------------------------------------

TYPE tspriteclock
  i AS INTEGER
  radius AS DOUBLE
  org AS torigin
  move_v AS DOUBLE
  move_dir AS INTEGER
  bmpname AS STRING*80
  sprite AS tsprite
END TYPE

Genausogut kann man natürlich die Uhr auch in das Spriteobjekt selbst einbauen. Das wäre allerdings insofern schlecht, als tspriteclock eine Spezialisierung darstellt, mit der man das schöne allgemeine tsprite-Objekt nicht "verschmutzen" sollte. tspriteclock enthält die Speicherstruktur für eine "Uhrenbewegung", wie wir es in der Eingangsdemo gesehen haben. Für andere Bewegungen müsste es anders aussehen. org ist der Mittelpunkt der Kreisbewegung, radius ist klar, move_v ist die Bewegungsgeschwindigkeit und move_dir die Richtung.

So, nun bauen Sie Ihr Programm so aus, dass ganz viele Sprites darauf herumschwirren können!

Der restliche Code sieht bei mir so aus:

'-----------------------------------------------------

'%%2
FUNCTION colldetectsprite(sprite1 AS tsprite, sprite2 AS tsprite, newx AS INTEGER, newy AS INTEGER) AS INTEGER

  'Schaue, ob sprite1 mit sprite2 kollidieren wuerde, wenn es an newx/newy waere.
  'Gibt TRUE zurueck, wenn ja

  DIM x11 AS INTEGER = newx
  DIM y11 AS INTEGER = newy
  DIM x12 AS INTEGER = newx+sprite1.xsize-1
  DIM y12 AS INTEGER = newy+sprite1.ysize-1
  DIM x21 AS INTEGER = sprite2.x
  DIM y21 AS INTEGER = sprite2.y
  DIM x22 AS INTEGER = sprite2.x+sprite2.xsize-1
  DIM y22 AS INTEGER = sprite2.y+sprite2.ysize-1
  DIM s1ins2 AS INTEGER, s2ins1 AS INTEGER

  'Bedingung ist, dass mindestens ein Eck eines Sprites innerhalb des anderen liegen muss.
  s1ins2=(x11>=x21 AND x11<=x22 AND y11>=y21 AND y11<=y22)
  s1ins2=s1ins2 OR (x11>=x21 AND x11<=x22 AND y12>=y21 AND y12<=y22)
  s1ins2=s1ins2 OR (x12>=x21 AND x12<=x22 AND y11>=y21 AND y11<=y22)
  s1ins2=s1ins2 OR (x12>=x21 AND x12<=x22 AND y12>=y21 AND y12<=y22)

  'Jetzt versuchen wir's andersrum
  s2ins1=(x21>=x11 AND x21<=x12 AND y21>=y11 AND y21<=y12)
  s2ins1=s2ins1 OR (x21>=x11 AND x21<=x12 AND y22>=y11 AND y22<=y12)
  s2ins1=s2ins1 OR (x22>=x11 AND x22<=x12 AND y21>=y11 AND y21<=y12)
  s2ins1=s2ins1 OR (x22>=x11 AND x22<=x12 AND y22>=y11 AND y22<=y12)

  colldetectsprite=s1ins2 OR s2ins1

END FUNCTION

'-----------------------------------------------------

FUNCTION colldetectsprite2(sprite1 AS tsprite, sprites() AS tsprite, isprite AS INTEGER, nsprite AS INTEGER, newx AS INTEGER, newy AS INTEGER) AS INTEGER

  'Schaue, ob sprite1 mit einem Sprite aus dem Sprite-Array sprites() kollidieren wuerde, wenn es an newx/newy waere.
  'isprite identifiziert sprite1 in sprites() (kann auch ausserhalb von 0..nsprite-1 liegen)
  'nsprite = Anzahl der relevanten Sprites in sprites()
  'Gibt TRUE zurueck, wenn ja

  DIM i AS INTEGER, retval AS INTEGER=FALSE

  FOR i=0 TO nsprite-1
    IF (i<>isprite) THEN
      IF (colldetectsprite(sprite1,sprites(i),newx,newy)) THEN retval=TRUE
    END IF
  NEXT i

  colldetectsprite2=retval

END FUNCTION


'-----------------------------------------------------

FUNCTION colldetectsprite3(sprite1 AS tsprite, sprites() AS tsprite, isprite AS INTEGER, nsprite AS INTEGER, newx AS INTEGER, newy AS INTEGER) AS INTEGER

  'Wie colldetectsprite2(), aber gibt die Nummer des Sprites zurueck, mit dem kollidiert wird.
  '-1, wenn keine Kollision vorliegt
  '%%4

  DIM i AS INTEGER, retval AS INTEGER=-1

  FOR i=0 TO nsprite-1
    IF (i<>isprite) THEN
      IF (colldetectsprite(sprite1,sprites(i),newx,newy)) THEN retval=i
    END IF
  NEXT i

  colldetectsprite3=retval

END FUNCTION


'-----------------------------------------------------

SUB flipscreen

  'Switche aktive Seite zur sichtbaren Seite
  activepage=1-activepage
  SCREENSET activepage,1-activepage

  'Aktualisiere die bisher sichtbare und "veraltete" Seite
  SCREENCOPY 1-activepage,activepage

  'Alle neuen Operationen werden nun auf activepage ausgefuehrt.
END SUB


'-----------------------------------------------------

TYPE torigin
  x AS INTEGER
  y AS INTEGER
END TYPE

'-----------------------------------------------------

TYPE tspriteclock
  i AS INTEGER
  radius AS DOUBLE
  org AS torigin
  move_v AS DOUBLE
  move_dir AS INTEGER
  bmpname AS STRING*80
  sprite AS tsprite
END TYPE


'-----------------------------------------------------

SUB movespriteclock(sc AS tspriteclock, sprites() AS tsprite, isprite AS INTEGER, nsprite AS integer)

  DIM phi AS DOUBLE
  DIM ix,iy, collision,collision1 AS INTEGER

  'Teste erst Kollision an der alten Stelle
  collision1=colldetectsprite3(sc.sprite,sprites(),isprite,nsprite,sc.sprite.x,sc.sprite.y)

  'Bilde neue kartesische Koordinaten
  phi=sc.i/400*2*pi
  IF (sc.i=400) THEN sc.i=0
  ix=sc.org.x+sc.radius*SIN(sc.move_dir*sc.move_v*phi)
  iy=sc.org.y-sc.radius*COS(sc.move_dir*sc.move_v*phi)


  collision=colldetectsprite3(sc.sprite,sprites(),isprite,nsprite,ix,iy)

  IF ((collision<0 OR collision<isprite) AND (collision1<0 OR collision1<isprite)) THEN
    'Keine Kollision oder aktuelles Sprite hat Vorfahrt
    '==> Bewege Sprite an neue Position
    movesprite(sc.sprite,ix,iy)
    '? #3,isprite;" ";ix;" ";iy;nt color=teal>" ";phi
    'Aktualisiere Information auch in sprites()
    sprites(isprite)=sc.sprite
    sc.i+=1
  END IF

END SUB


'-----------------------------------------------------

SUB main3

  'Version mit colldetectsprite2 und Sprite-Arrays
  '%%3

  DIM ix AS INTEGER, ix1 AS INTEGER, iy AS INTEGER,isig AS INTEGER, i AS INTEGER, _
       rblue AS DOUBLE, rgreen AS DOUBLE, phi AS DOUBLE

  CONST nsprite=6

  DIM spritecl(nsprite) AS tspriteclock
  DIM bmpname(nsprite) AS STRING
  DIM radius(nsprite) AS INTEGER
  DIM org(nsprite) AS torigin
  DIM move_v(nsprite) AS DOUBLE
  DIM move_dir(nsprite) AS INTEGER
  DIM sprites(nsprite) AS tsprite

  'Definiere die Parameter der Spritebewegung und des
  'Spriteaussehens

  bmpname(0)="testgreen.bmp"
  radius(0)=300
  org(0).x=xscreensize/2
  org(0).y=yscreensize/2
  move_v(0)=1
  move_dir(0)=1

  bmpname(1)="testgreen.bmp"
  radius(1)=100
  org(1).x=600
  org(1).y=300
  move_v(1)=8
  move_dir(1)=1

  bmpname(2)="testred.bmp"
  radius(2)=100
  org(2).x=600
  org(2).y=300
  move_v(2)=16
  move_dir(2)=-1

  bmpname(3)="testred.bmp"
  radius(3)=70
  org(3).x=200
  org(3).y=400
  move_v(3)=2
  move_dir(3)=-1

  bmpname(4)="testblue.bmp"
  radius(4)=70
  org(4).x=200
  org(4).y=100
  move_v(4)=1.7
  move_dir(4)=1

  bmpname(5)="testblue.bmp"
  radius(5)=150
  org(5).x=500
  org(5).y=500
  move_v(5)=3
  move_dir(5)=-1

  'Zeichne Hintergrundbild
  testscreen

  'Initialisiere Sprites
  FOR i=0 TO nsprite-1
    initsprite(spritecl(i).sprite)
    spritecl(i).i=0
    spritecl(i).radius=radius(i)
    spritecl(i).org=org(i)
    spritecl(i).move_v=move_v(i)
    spritecl(i).move_dir=move_dir(i)
    loadsprite(spritecl(i).sprite,bmpname(i))
  NEXT i

  'Hauptschleife
  WHILE (1=1)

    'Bewege alle Sprites
    FOR i=0 TO nsprite-1
      movespriteclock(spritecl(i),sprites(),i,nsprite)
    NEXT i

    flipscreen

    IF MULTIKEY(SC_ESCAPE) THEN EXIT WHILE
    IF MULTIKEY(SC_F1) THEN SLEEP
    SLEEP 20

  WEND

  SLEEP
  FOR i=0 TO nsprite-1
    closesprite(sprites(i))
  NEXT i

END SUB

In den Code ist noch ein Double-Buffering eingebaut, was sich allerdings nur auf langsameren Rechnern bemerkbar macht. A propos Schnelligkeit: Die Geschwindigkeit lässt sich mit dem sleep-Befehl ganz am Ende variieren.

Die Bereitstellung der Startwerte für die Sprites "hardcoded" ist natürlich nicht so elegant. Viel eleganter wäre eine benutzerdefinierte Eingabe oder ein Einlesen der Daten aus einem ini-File. Kleine Übung!

Variation mit Maus

Grundsätzliches

Was jetzt noch ganz entscheidend fehlt, ist die Bedienung per Maus. Die ist in der gfx-Lib allerdings vergleichsweise einfach. Es gibt zwei Befehle dafür, GETMOUSE und SETMOUSE, wobei man wahrscheinlich in fast allen Fällen ledglich GETMOUSE benötigt: GETMOUSE x,y,wheel,buttons. GETMOUSE fragt die Mausposition, das Rad und die Buttons ab. Befindet sich die Maus im aktiven Programmfenster, sind die Koordinaten x,y auf der Mausposition, ansonsten sind sie negativ. Die einzelnen Bits von buttons geben an, welcher Knopf gerade gedrückt ist. Zum Auswerten ist die BIT-Funktion ganz nützlich; sie gibt an, ob das Bit Nr. x im Argument gesetzt ist oder nicht. BIT(4,2) = 1, da Bit Nr. 2 im Argument, der Zahl 4, auf 1 ist. Im integer-Wert buttons repräsentiert Bit Nr. 0 den linken, Bit Nr. 1 den rechten und Bit Nr. 2 den mittleren Mausknopf. Wenn wir

CONST LEFTKEY=0
CONST RIGHTKEY=1
CONST TRUE=-1
CONST FALSE=0

definieren, dann können wir mit BIT(buttons,LEFTKEY) den Status der linken Maustaste abfragen. Die Funktion gibt TRUE zurück, wenn sie gedrückt ist.

Wahrscheinlich das kleinste Zeichenprogramm der Welt:

'-----------------------------------------------
'Small test for mouse
'-----------------------------------------------


#INCLUDE "fbgfx.bi"


OPTION EXPLICIT

CONST LEFTKEY=0

DIM AS INTEGER x,y,buttons

SCREEN 19,24,2

WHILE (1=1)
   GETMOUSE x,y,,buttons
   IF (x>=0 AND y>=0 AND BIT(buttons,LEFTKEY)) THEN PSET (x,y)
   IF MULTIKEY(SC_ESCAPE) THEN EXIT WHILE
WEND

Mit SETMOUSE können Sie den Mauszeiger irgendwohin schieben und den Mauszeiger sichtbar oder unsichtbar machen.

Ausbau: Bewegte Sprites mit Fenster

Wir wollen unsere Mausattraktion nun in unser Sprite-Programm einbauen. Testen Sie mal folgendes Programm. (Es benötigt die gleichen bmp-Files wie das Grundprogramm). Zunächst scheint nichts anders. Wenn Sie aber nun auf die Taste F1 drücken, erscheint ein Fenster. Offenbar ein "selbstgetricktes". Sie können das Fenster mit der Maus bewegen, indem Sie es irgendwo "anfassen", das braucht nicht die Titelleiste zu sein. Schliessen können Sie es mit dem Kreuz rechts oben. Mit mehrmals ESC verlassen Sie das Programm wieder.

Übung: Programmieren Sie das nach!

Das ist keine einfache Übung. Wenn Sie das Programm aufmerksam betrachten, sehen Sie, dass die Sprites sich unter dem Fenster wegbewegen. Beim Verschieben wird der Hintergrund des Fensters trotz sich bewegender Sprites korrekt wiederhergestellt.

Ein paar Tipps:

  1. Grundsätzlich ist so ein Fenster ein Sprite. Sie können es also mit Hilfe der schon erstellten Sprite-Bibliothek realisieren.
  2. Es gibt allerdings ein paar Unterschiede. Der erste ist die Initialisierung: Diese wird nicht durch Laden eines bmp's bewerkstelligt, sondern durch das Zeichnen eines "Leerfenster" (also ohne Text), das dann mittels GET unter img abgespeichert wird. Damit das der User nicht mitkriegt, sollte das auf der unsichtbaren Bildschirmseite passieren.
  3. Der nächste Unterschied ist das Bildschirmmanagement: Das Geheimnis der reibungslosen Überlagerung von Bewegungsobjekten und Fenster ist, dass bezüglich des Fensters die Verwaltung auf Typ III umgestellt wurde. Genauer: Bevor die Weiterbewegung der Sprites erfolgt, werden zuerst alle Fenster geschlossen. Nachdem die Sprites sich bewegt haben, werden wieder alle Fenster geöffnet.
  4. Wenn das Fenster manuell ganz geschlossen wird, erspart man sich auch viel Arbeit, indem man einfach den ganzen Bildschirm neu zeichnen lässt. Daher flackert das auch ein bisschen.
  5. Aufgrund der geänderten Verwaltung ist es notwendig, in den meisten Fällen beim Fenster-Sprite auf das Restaurieren des Bildschirmhintergrunds zu verzichten. Man braucht also eine weitere Routine movesprite2(), bei der das Restaurieren des Hintergrunds entfällt.
  6. Einzige Ausnahme: Wenn das Fenster wirklich mit der Maus verschoben wird. Dann braucht man die Restaurierung.

Ansonsten soll hier nur ein Ausschnitt aus der Hauptschleife betrachtet werden:

(...)
    IF MULTIKEY(SC_ESCAPE) THEN EXIT WHILE
    IF MULTIKEY(SC_F1 AND wins0(0).show=0) THEN
      twindow_show(wins0(0),200,200)
      twindow_write(wins0(0),0,0,"Hallo!")
    END IF
    IF MULTIKEY(SC_F2) THEN twindow_hide(wins0(0),spritecl(),sprites(),nsprite)
    GETMOUSE xnew,ynew,,buttons
    IF (twindow_inrange(wins0(0),xold,yold) AND BIT(buttons,LEFTKEY) AND wins0(0).show=1 AND xnew>=0 AND ynew>=0) THEN

      'Postion des Mauszeigers innerhalb des Close-Buttons
      IF (twindow_mouseclose(wins0(0),xnew,ynew)) THEN
        twindow_hide(wins0(0),spritecl(),sprites(),nsprite)
      ELSE
        'Postion des Mauszeigers innerhalb des Fensters
        xrel=xold-wins0(0).SCREEN.x
        yrel=yold-wins0(0).SCREEN.y
        IF (xold<-9999 OR yold<-9999) THEN
          xrel=xnew-wins0(0).SCREEN.x
          yrel=ynew-wins0(0).SCREEN.y
        END IF

        twindow_move(wins0(0),xnew-xrel,ynew-yrel)
      END IF
    END IF
    xold=xnew
    yold=ynew

(...)

Hier findet praktisch die ganze Mausverwaltung statt. Die Funktion twindow_inrange() prüft, ob sich die übergebenen Koordinaten innerhalb des übergebenen Fensters befinden. Das entspricht der Frage, ob sich der Mauszeiger innerhalb des Fensters befindet. BIT() prüft, ob der linke Mausknopf gedrückt ist. xnew>=0 and ynew>=0 prüft, ob sich der Mauszeiger überhaupt innerhalb des Programmfensters befindet. Das ist dann zusätzlich relevant, wenn das Fenster schon gepackt und dann über den Bildschirmrand hinausgeschoben wird.

Innerhalb der if-Bedingung werden dann zwei Funktionalitäten behandelt: Die Aktivierung des Schliessen-Knopfs und das Verschieben. twindow_mousclose() prüft lediglich, ob sich der Mauszeiger im Schliessknopf-Bereich des Fensters befindet. (wins0() ist übrigens ein globales Array mit allen Fenstern.) Ist das der Fall, dann wird twindow_hide() aufgerufen, das das Fenster unsichtbar macht. (Es bleibt allerdings noch erhalten und kann wieder sichtbar gemacht werden!) Wegen der oben erwähnten Restaurierungsmethode, bei der dann der ganze Bildschirm neu gezeichnet wird, muss man dieser Funktion auch sämtliche Sprite-Objekte übergeben.

Innerhalb der Verschiebeaktivität wird zunächst die relative Position zwischen Mauszeiger und Fensterposition (linke obere Ecke) festgestellt. Diese wird von der Mausposition abgezogen, um twindow_move() die neue Fensterposition zu übergeben. twindow_move() besteht nur aus einem Aufruf von movesprite2() und dem Setzen des show-Flags.

Allgemeines und Weiteres

Ganz interessant an dem Fenster-Beispiel dürfte sein: Erstens die Erkenntnis, dass auch Fenster im Grunde nichts anderes sind als Sprites. Und zweitens, dass man mit doch sehr begrenztem Aufwand selbst eine einfache Fensterverwaltung programmieren kann. Und drittens bekommt man einen Einblick, wie so eine Fensterverwaltung funktioniert - und auch, welche Komplikationen sie bereithält, wenn man sie grösser und leistungsfähiger machen will.

Die Themen, die wir jetzt in der gfx-Lib nicht behandelt haben, sind Superbitmaps und Sound. Superbitmaps oder Layers oder Maps sind Bildschirmspeicher, die grösser sind als der Bildschirm. Man kann dann mit dem Bildschirm einen Ausschnitt des Layers darstellen und mit diesem auf dem Layer hin- und herfahren. Die Grafikoperationen geschehen alle auf dem Layer. Fast jedes 2D-Spiel benutzt solche Layer, ganz typisch z.B. bei Jump-and-Run-Spielen.

Die Realisation solcher Layer steht und fällt mit Bildschirmseiten und Grafikspeicher via IMAGECREATE. Da die primitiven Grafikoperationen (LINE, PSET usw.) und die Bitmap-Operationen (GET, PUT) nur auf Bildschirmseiten arbeiten, muss der Weg zur Manipulation der Layer immer über Bildschirmseiten führen. Will man z.B. ein Sprite auf dem Layer bewegen, muss man zuerst einen passenden Ausschnitt des Layers in eine dafür vorgesehen Page laden, dort das Sprite bewegen und dann den Ausschnitt zurückspeichern. Das Laden und Zurückspeichern mittels PUT und GET verlangt allerdings, die Position des Zeigers im Bildspeicher des Layers zu kennen, wenn man einen bestimmten Ausschnitt (x1,y1)-(x2,y2) haben will. Da muss man vorsichtig rechnen, da man sonst viele Programmabstürze produziert...Eine (bessere) Alternative ist es allerdings, in so einem Fall nicht die gfx-Lib zu nehmen, sondern auf die SDL umzuschwenken, die sich ganz dem Thema "Layer" widmet. gfx ist nicht dafür gedacht, Grundlage aufwändiger Grafikprogramme zu sein.

Beim Thema "Sound" sieht's ganz mager aus: Freebasic und die gfx unterstützen ausser "beep" nichts in Richtung Sound. Den Grund kann man hier wörtlich von den Programmieren von Freebasic erfahren:

PLAY (Music):

Unsupported.Type: statement

Too specific and not needed by most. If anybody wants this functionality a music library would be more practical.This was barely practical in the 80's with the pc speaker. Anyone who wants this feature should rethink motives, or be shot.

Die meisten benutzen die SDL-Sound-Funktionen.