Aus Linux-Magazin 04/2003

Das Canvas-Widget von Tk für Spiele nutzen

Das schnelle Entwickeln von GUI-Anwendungen ist eine der Stärken von Tcl/Tk. Ein einfaches Spiel zeigt verschiedene Aspekte des Canvas-Widgets, von Animation über Bilder bis Zahlendarstellung.

Zu ihrem vierten Geburtstag wollte ich meiner Tochter etwas Besonderes schenken: ein selbst geschriebenes Spiel. Es sollte irgendwas mit Zahlen zu tun haben und natürlich Spaß machen – Edutainment also, für spielerisches Lernen. Eine Idee war schnell gefunden: Die Spielerin soll Zahlen erkennen, die auf dem Bildschirm auftauchen, und jeweils die passende Taste drücken.

Soweit zu “Edu”, fehlt noch das “tainment”, die nette Verpackung. Beim Vorlesen der Geschichte vom kleinen Wassermann kam die zündende Idee: ein Fisch, der Luftblasen mit den Zahlen aufsteigen lässt. Da ich Karpfen nicht besonders lustig finde, gibt es stattdessen einen Bitterling, zu dem auch gleich eine Teichmuschel als Lebensgefährten gehört. Diese Muschel produziert – gewissermaßen als Belohnung für jede gefundene Zahl – eine Perle.

Nach der Idee machte ich mich an die Umsetzung – natürlich mit Tcl. Das Ergebnis ist in Abbildung 1 zu sehen. Da das Tcl-Programm recht überschaubar ist, steht der komplette Code in einer einzelnen Datei, inklusive aller Bilder. Aus Platzgründen enthält Listing 1 keine Daten für die Bilder, die vollständigen Quellen stehen wie immer auf dem FTP-Server des Linux-Magazins bereit[2].

Bunte Unterwasserszene

Das Spielfeld wird mit dem Canvas-Widget dargestellt, hier gilt es, den Hintergrund zu zeichnen, ein paar Wasserpflanzen und Sand als Dekoration, den Fisch, die Muschel und die Blasen natürlich. Mit meinen Zeichenkünsten ist es leider nicht sonderlich weit her, also habe ich den Bitterling und die Teichmuschel per Google gesucht. Den Hintergrund bildet ein großes Bitmap, das auch die Muschel und die Wasserpflanzen zeigt (Gimp sei Dank).

Von Haus aus kann Tk nur wenige Bitmap-Typen lesen und darstellen. Anders als bei anderen Bildformaten kann Tk aber Bilder im GIF-Format nicht nur aus einer Datei lesen, sondern auch direkt aus einem String. Dieser String muss die Bilddaten Base-64-kodiert enthalten (auch Mime-encoded genannt). Das Umwandeln geht mit Hilfe der Tcllib[1] in wenigen Zeilen:

package require base64
set fd [open bitterling.gif]
fconfigure $fd -translation binary
set bin [read $fd]
close $fd
set bild [base64::encode $bin]

Nun enthält »$bild« das Bild in der Base-64-Darstellung. Diese Variante stellt die Binärdaten ausschließlich durch Ascii-Zeichen dar. Das hat den Vorteil, dass man den String-Inhalt direkt in den Quellcode einfügen kann, ohne den Editor oder den Interpreter zu verwirren. Aus dem String lässt sich dann wieder ein Bild zum Anzeigen erzeugen:

package require Tk
image create photo bitterling -data $bild
label .test -image bitterling
pack .test

Die Tcllib ist in den meisten Linux-Distributionen bereits enthalten, die neuste Version kann man von[1] holen. Das Installieren übernimmt ein Skript, das im Paket zu finden ist.

Die Leinwand

Das Fischbild und den Hintergrund malt das Spiel auf ein Canvas (eine Leinwand; siehe Zeile 37). Pro Foto genügt ein Kommando dieses Widgets:

$canvas create image 0 500 
  -image hintergrund -anchor sw

Wenn alle Bilder positioniert sind, schwebt der Bitterling an einem Tannenwedel und knabbert daran. Aber kein echter Fisch verharrt permanent auf der gleichen Stelle, also muss sich auch der Bitterling bewegen. Diese Aufgabe übernimmt die Prozedur »fischBewegen« ab Zeile 55. Zuerst berechnet sie einen zufälligen Wert für die Verschiebung in x- und y-Richtung, dann bewegt sie den Fisch entsprechend. Der Animationstrick steht in der letzte Zeile: Hier sorgt die Prozedur mit »after« dafür, dass sie von der Event-Queue nach einer bis zehn Millisekunden wieder aufgerufen wird. Zwischendurch kann die Queue andere Events abarbeiten.

Blubbernde Luftblasen

Als Nächstes sind die Luftblasen dran. Eine neue Blase entsteht mit Hilfe der Prozedur »blaseNeu« (Zeile 68), sie kombiniert einen ausgefüllten Kreis mit einer Zahl. Um die Bestandteile eindeutig zu identifizieren, können Canvas-Objekte ein Tag (als Markierung) tragen. Als Basis dient eine per »clock clicks« erhaltene Zeitmarke. Da Tags nicht mit einer Zahl anfangen dürfen, setzt Zeile 78 ein »t« davor. Neben dem Tag, das die ganze Blase bezeichnet, braucht das Programm später eine eigene Kennung für Kreis und Zahl: Zeile 78 vergibt daher zusätzlich ein Tag mit dem Präfix »blase«, Zeile 87 entsprechend »text«.

Als Startpunkt der Blase dienen die Koordinaten »$x« und »$y«, also der Mund des Fisches. Es sind globale Variablen, da »fischBewegen« den Mund (inklusive Fisch) permanent bewegt. Die Blase soll beim Drücken der richtigen Taste platzen, deshalb vereinbart Zeile 90 per »bind« die »blaseWeg«-Prozedur. Das Bind-Kommando hat die Form »bind Widget Event Befehl«. Das Widget ist hier einfach das ganze Fenster ».«, der Event das Drücken der richtigen Zahlentaste, der Befehl lautet »blaseWeg« mit der gewünschten ID.

Bei den Zahlen gibt es eine Besonderheit: Sie kommen zweimal auf der Tastatur vor. Die Events aus dem Nummernfeld lauten jedoch – egal ob Numlock gedrückt ist – nicht auf die Zahl, sondern tragen einen eindeutigen Namen. Die Zuordnung erfolgt per Array (Zeile 12 bis 23), ein zusätzlicher Bind-Aufruf in Zeile 91 kümmert sich um das Binding.

Animation

Wenn die Prozedur »blaseNeu« fertig ist, hat der Fisch noch eine ziemlich kleine Blase vor seinem Maul. Als Nächstes soll sie größer werden und dann aufsteigen. Hierfür ist »blaseAktualisieren« (Zeile 97) verantwortlich. Diese Prozedur überprüft zuerst, ob die Blase schon gelöscht wurde oder aus dem sichtbaren Bereich verschwunden ist, und ruft gegebenenfalls »blaseLöschen« und »blaseNeu« auf. Andernfalls hängt das Verhalten vom Alter der Blase ab.

Da die ID der Blase eine Zeitmarke ist, lässt sich ihr Alter ziemlich einfach ermitteln. In den ersten 1200 Millisekunden soll die Blase größer werden. Dazu löscht Zeile 116 den alten Kreis, danach malt die Prozedur einen größeren. Bei der Zahl verändert sie nur per »$canvas itemconfigure« die Schriftgröße. Da der neue Kreis über der Zahl steht und sie verdeckt, bringt »$canvas raise« die Ziffer wieder nach vorn. Hier dienen die oben besprochenen Tags zur Identifizierung von Kreis und Zahl.

Ist die Blase älter als 1200 Millisekunden, fängt sie an zu steigen. Die Steiggeschwindigkeit (Zeile 131) hängt vom Alter ab, damit die Blase oben schneller aufsteigt. Die zufällige Verschiebung in horizontaler Richtung (Zeile 130) ist notwendig, da das Gebilde sonst einfach nicht wie eine Blase aussieht. Das Kommando »$canvas move« verschiebt Kreis und Zahl gleichzeitig, da es das gemeinsame Tag »t$id« verwendet. Wie die »fischBewegen«-Prozedur lässt sich auch »blaseAktualisieren« regelmäßig von der Event-Queue aufrufen (Zeile 136), die verwendeten 50 Millisekunden reichen für die fließende Bilddarstellung (20 Frames pro Sekunde) vollkommen aus.

Trifft die Spielerin die richtige Taste, führt deren Binding die Prozedur »blaseWeg« (Zeile 149) auf, die ihrerseits die Aufgabe zuerst an »blaseLöschen« (Zeile 139) weitergibt. Diese entfernt die Objekte mit »$canvas delete« aus dem Canvas; die beiden »bind«-Kommandos löschen das bisherige Binding.

Ein wenig Motivation muss sein, deshalb gibt es für jede getroffene Zahl eine neue Perle (»perleNeu«, Zeile 155). Die Perlen rollen aus der Muschel und reihen sich rechts unten am Spielfeldrand ein (»perleBewegen«, Zeile 171). Enthält die Reihe zehn Perlen, werden sie gelöscht, der Zähler ganz rechts unten erhöht sich entsprechend.

Der Quelltext zum Erzeugen der Perlen bietet nichts Neues, das Aufreihen der Perlen dagegen schon. Statt die Endposition jeder Perle zu berechnen, wird sie so lange verschoben, bis sie an die anderen Perlen anstößt (Zeile 183). Diese bereits vorhandenen Perlen sind mit dem Tag »perlen« gekennzeichnet. Ist die neue Perle bei ihnen angekommen, erhält auch sie mit »$canvas addtag« das »perlen«-Tag und die nächste Perle kann kommen. Das funktioniert zuverlässig, auch wenn mehrere Perlen gleichzeitig rollen.

Abbildung 1: Zahlenspiele mit Tcl: Die Spielerin soll die Zahl in der Luftblase erkennen und die passende Taste drücken, dann erhält sie eine Perle (rechts unten).

Abbildung 1: Zahlenspiele mit Tcl: Die Spielerin soll die Zahl in der Luftblase erkennen und die passende Taste drücken, dann erhält sie eine Perle (rechts unten).

Abbildung 2: Das Zeichenprogramm Impress speichert Grafiken als Tcl-Code. Das Bitterling-Bitmapbild (oben) lässt sich damit als Vektorgrafik nachbilden.

Abbildung 2: Das Zeichenprogramm Impress speichert Grafiken als Tcl-Code. Das Bitterling-Bitmapbild (oben) lässt sich damit als Vektorgrafik nachbilden.

Abbildung 3: Das Tcl-Plugin führt Tcl-Skripte - ähnlich einem Java-Applet - direkt im Webbrowser aus. Hier läuft das Spiel im Galeon-Browser.

Abbildung 3: Das Tcl-Plugin führt Tcl-Skripte – ähnlich einem Java-Applet – direkt im Webbrowser aus. Hier läuft das Spiel im Galeon-Browser.

Nur 215 Zeilen

Mit diesen 215 Zeilen Quelltext ist das komplette Spiel entwickelt. Für Verbesserungen bleibt aber genug Raum, zum Beispiel bei den Bildern: Entsprechendes Talent vorausgesetzt ist hier viel mehr möglich. Es würde sich anbieten, statt der starren Bilder mehr animierte Vektorgrafiken einzusetzen.

Der Import von Bitmaps ist für Tk kein Problem, aber auch für Vektorgrafiken gibt es verschiedene Wege. Liegt die Zeichnung in Xfig, Tgif, Sodipodi oder ähnlichen Tools vor, verwandelt Pstoedit[3] deren Postscript-Ausgabe in Tcl-Code. Alternativ ist Impress[4] ein gutes Zeichenprogramm, das komplett in Tcl geschrieben ist und die Bilder als Tcl/Tk-Code speichert. Auch Xfig kennt Tk als Exportformat.

Bin ich schon drin?

Ein weiterer Punkt für Verbesserungen wäre Sound, vom freundlich Blubb beim Erzeugen einer Blase bis zum Bling bei ihrem Platzen. Ebenso denkbar wäre, dass das Spiel die Zahlen vorliest oder eine nette Hintergrundmusik abspielt. Von der Tcl-Seite ist das dank Snack[5] kein Problem, es fehlen nur die entsprechenden Audiodateien. Vielleicht findet sich ja ein audiophiler Leser, der diese ergänzt? (fjl)

Das Neueste

Für einen Jahresrückblick ist es zwar etwas spät, unter[6] findet sich aber eine sehr interessante Übersicht über das vergangene Skriptsprachen-Jahr. Sie fasst die wesentlichen Entwicklungen für Lua, Perl, Python, Ruby und natürlich Tcl zusammen.

Das Canvas-Widget dient nicht nur als Basis für einfache Spiele. Tkvnc[7] zeigt, dass es dem Widget sogar gelingt, komplexe Desktops remote anzuzeigen. Damit das Ganze auch mit gesicherten VNC-Servern funktioniert, hat Jochen Loewer die DES-Verschlüsselung in reinem Tcl implementiert[8].

Activestate stellte eine Betaversion ihres Tcl Dev Kit[9] vor. Der Nachfolger des Tcl-Pro-Pakets enthält neben einigen Werkzeugen zum Debuggen und Wrappen von Anwendungen eine aktualisierte Version des Tcl-Plugins. Damit lassen sich Programme auch im Webbrowser verwenden (siehe Abbildung 3). Aus Sicherheitsgründen überwacht und verhindert das Plugin dabei einige potenziell gefährliche Operationen. Tcl-Programmierer können so bei ihrer Sprache bleiben und müssen nicht auf Java-Applets setzen.

Auch im Netzbereich angesiedelt ist Sockspy[10]. Dieser Proxy lässt sich zwischen beliebige Netzanwendungen einklinken und protokolliert den Netzverkehr. Das Anzeigen der übertragenen Daten übernimmt Sockspy selbst. Das Debuggen von Client-Server-Anwendungen wird so wesentlich erleichtert.

Listing 1: Das Spiel

001 #!/bin/sh
002 # 
003 exec wish $0 $@
004 
005 package require Tk
006 package require opt
007 
008 # Zeitauflösung
009 set res 50
010 
011 # Äquivalenztabelle für Zahlen und Zahlenfeld
012 array set zahl2kp {
013    0  KP_Insert
014    1  KP_End
015    2  KP_Down
016    3  KP_Next
017    4  KP_Left
018    5  KP_Begin
019    6  KP_Right
020    7  KP_Home
021    8  KP_Up
022    9  KP_Prior
023 }
024 
025 # Anzahl Perlen
026 set perlen 0
027 
028 # Bilder:
029 image create photo fisch  -data "..."
030 image create photo hintergrund -data "..."
031 image create photo perle -data "..."
032 
033 proc gui {} {
034    global canvas x y
035 
036    # Canvas erzeugen und mit Bildern füllen
037    set canvas [canvas .canvas -width 500 -height 500 -bg grey20]
038    grid $canvas -row 0 -column 0 -sticky nesw
039    $canvas create image 0 500 -image hintergrund  -anchor sw
040    $canvas create image 140 390 -image fisch  -anchor sw -tag fisch
041 
042    # Blasenstartpunkt festlegen
043    set x 167
044    set y 365
045 
046    # Fisch-Animation starten
047    fischBewegen
048 
049    wm sizefrom . program
050    wm geometry . 500x500
051    bind . <q> "exit 0"
052    bind . <Control-q> "exit 0"
053 }
054 
055 proc fischBewegen {} {
056    global canvas x y res
057 
058    set dx [expr {round(rand() * 1 * ((rand()<0.5)*2 -1))}]
059    set dy [expr {round(rand() * 1 * ((rand()<0.5)*2 -1))}]
060    $canvas move fisch $dx $dy
061 
062    incr x $dx
063    incr y $dy
064 
065    after [expr {round(rand()*2*$res)}] fischBewegen
066 }
067 
068 proc blaseNeu {} {
069    global canvas x y zahl2kp res
070 
071    # Tag generieren
072    set id [clock clicks -milliseconds]
073 
074    # Blase
075    $canvas create oval 
076       [expr {$x -3}] [expr {$y -3}] 
077       [expr {$x +3}] [expr {$y +3}] 
078       -tags [list "t$id" "blase$id"] 
079       -fill skyblue
080 
081    # Text
082    set zahl [expr {round( floor( rand() * 9.0 ) )}]
083    $canvas create text $x $y 
084       -font "Helvetica 5 bold" 
085       -text $zahl 
086       -anchor center 
087       -tags [list "t$id" "text$id" ]
088 
089    # Auf richtigen Tastendruck hören
090    bind . <KeyPress-$zahl> "blaseWeg $id"
091    bind . <KeyPress-$zahl2kp($zahl)> "blaseWeg $id"
092 
093    # Start
094    after $res blaseAktualisieren $id
095 }
096 
097 proc blaseAktualisieren {id} {
098    global canvas x y res
099 
100    # Blase noch da?
101    if {[string eq [$canvas type "blase$id"] ""]} {
102       return;
103    }
104 
105    # Blase schon aus dem Canvas raus?
106    if {[lindex [$canvas bbox "blase$id"] 1] < 0} {
107       blaseLöschen $id
108       blaseNeu
109       return;
110    }
111 
112    # wie alt?
113    set alter [expr {[clock clicks -millisec] - $id}]
114    if { $alter < 1200 } {
115       # Blase vergrößern
116       $canvas delete "blase$id"
117       set radius [expr {$alter / $res * 0.75}]
118       $canvas create oval 
119          [expr {$x -$radius}] [expr {$y -$radius}] 
120          [expr {$x +$radius}] [expr {$y +$radius}] 
121          -tags [list "t$id" "blase$id"] 
122          -fill skyblue -outline ""
123 
124       # Schrift größer und nach vorn setzen
125       set punkte [expr {round($alter / $res * 0.5)}]
126       $canvas itemconfigure "text$id" -font "Helvetica $punkte bold"
127       $canvas raise text$id blase$id
128    } else {
129        # Blase aufsteigen lassen
130        set dx [expr {cos( rand() * 20 )*1.2}]
131        set vy [expr {$::tempo * ($alter - 700) / double(3e5)}]
132        set dy [expr {-$vy * $res}]
133 
134        $canvas move "t$id" $dx $dy
135    }
136    after $res blaseAktualisieren $id
137 }
138 
139 proc blaseLöschen {id} {
140    # Welche Zahl steht in der Blase?
141    set zahl [$::canvas itemcget "text$id" -text]
142    # Blase im Canvas löschen
143    $::canvas delete "t$id"
144    # Bindings löschen
145    bind . <KeyPress-$zahl> ""
146    bind . <KeyPress-$::zahl2kp($zahl)> ""
147 }
148 
149 proc blaseWeg {id} {
150    blaseLöschen $id
151    perleNeu
152    blaseNeu
153 }
154 
155 proc perleNeu {} {
156    global canvas perlen
157 
158    # ID generieren
159    set id [clock clicks -milliseconds]
160 
161    set einer [expr {int(fmod($perlen, 10))}]
162    if { $einer == 0 && $perlen != 0 } {
163       perleZähler
164    }
165    $canvas create image 140 480 -image perle 
166       -tags p$id
167    incr perlen
168    perleBewegen $id
169 }
170 
171 proc perleBewegen {id} {
172    global canvas res
173 
174    # Position der anderen Perlen
175    set xSoll [lindex [$canvas bbox perlen] 0]
176    if {[string match $xSoll ""]} {
177       set xSoll 420
178    }
179 
180    # Eigene Position
181    set xIst [lindex [$canvas bbox p$id] 0]
182 
183    if { $xIst >= $xSoll - 30 } {
184       $canvas addtag perlen withtag p$id
185    } else {
186       $canvas move p$id 5 0
187       after $res "perleBewegen $id"
188    }
189 }
190 
191 proc perleZähler {} {
192    global canvas perlen
193 
194    $canvas delete perlen
195    $canvas delete zähler
196 
197    $canvas create text 480 480 -text $perlen 
198       -font {Palatino 25 bold } 
199       -fill #b0c5db 
200       -tags zähler
201 }
202 
203 tcl::OptProc main {
204    {-tempo -int 10 "Tempo, normal ist 10"}
205 } {
206 
207    if { $tempo < 1 || $tempo > 100} {
208       error "tempo muss zwischen 1 und 100 liegen"
209    }
210    set ::tempo $tempo
211    gui
212    blaseNeu
213 }
214 
215 eval main $argv

Infos

[1] Tcllib: [http://www.tcl.tk/software/tcllib/]

[2] Quellen: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2003/04/Feder-Lesen/]

[3] Pstoedit: [http://www.pstoedit.net]

[4] Impress-Grafikprogramm: [http://www.ntlug.org/~ccox/impress/]

[5] Snack: [http://www.speech.kth.se/snack/]

[6] Jahresübersicht der Skriptsprachen: [http://www.vendian.org/language_year/]

[7] Tkvnc, ein VNC-Client in Tcl: [http://www.ifost.org.au/Software/tkvnc/]

[8] DES in Tcl: [http://mini.net/tcl/8196]

[9] Tcl Dev Kit: [http://www.activestate.com/Products/Tcl_Dev_Kit/]

[10] Sockspy: [http://sockspy.sourceforge.net/sockspy.html]

Der Autor

Carsten Zerbst arbeitet bei Atlantec an einem PDM-System für den Schiffbau. Daneben beschäftigt es sich mit dem Einsatz von Tcl/Tk. Auf seiner Homepage [http://www.groy-groy.de/czerbst/tcltk.html] sind einige Beispielprogramme zu finden.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben