Spiele mit `grafik` — die 2D-Engine
Verfügbar ab Kiste 0.10.0. Vollständige Spec in
docs/grafik.md.
Das bild-Modul (Kapitel 35) malt einzelne Bilder und speichert sie. Das grafik-Modul macht
etwas anderes: es ist eine Echtzeit-Engine für Spiele. Viele bewegte Figuren, 60 Bilder pro
Sekunde, direkt über die Grafikkarte. Wo gui (Kapitel 34) Werkzeuge mit Knöpfen und Menüs baut,
baut grafik Spiele.
Drei Dinge vorweg, damit es keine Überraschung gibt:
grafikläuft nur nativ — mitkiste build, nicht überkiste runoder den Browser. Es braucht ein echtes Fenster und die Grafikkarte.nutze guiundnutze grafikgehen nicht im selben Programm. Beide bringen ein eigenes Fenstersystem mit; der Compiler lehnt die Kombination mit einer deutschen Meldung ab.nutze klangdagegen geht — eingrafik-Spiel darf klingen (Schuss, Treffer, Musik).- Pixel sind Pixel.
öffne_fenster(800, 600, …)bestellt 800×600 echte Bildpunkte. Auf einem hochauflösenden Bildschirm sieht das Fenster kleiner aus, hat aber genau so viele Pixel wie bestellt.
38.1 Die Schleife — das Herz jedes Spiels
Ein Spiel läuft in einer Dauerschleife: Bild für Bild wird alles neu gezeichnet. In grafik treibst
du diese Schleife selbst. Fünf Aufrufe je Bild, immer in derselben Reihenfolge:
nutze grafik
grafik.öffne_fenster(800, 600, "Mein erstes Spiel")
solange grafik.läuft() {
grafik.beginne_bild()
solange grafik.nächster_schritt() {
// hier bewegt sich alles (gleich mehr dazu)
}
grafik.leere(grafik.farbe(20, 25, 40)) // Hintergrundfarbe (r, g, b von 0–255)
grafik.zeichne_szene()
grafik.zeige()
}
grafik.schliesse_fenster()
grafik.läuft()istfalsch, sobald du das Fenster schließt — die äußere Schleife endet dann.grafik.leere(farbe)füllt die Fläche mit einer Farbe.grafik.farbe(r, g, b)baut sie aus drei Zahlen 0–255, genau wie inbild. Soll etwas durchscheinen, nimmgrafik.farbe_mit_alpha(r, g, b, a)— der vierte Wert ist die Deckkraft, ebenfalls 0–255:255ist voll deckend,128halbdurchsichtig,0unsichtbar. Werte außerhalb 0–255 werden auf den Rand geklemmt.grafik.zeige()zeigt das fertige Bild an.
Die innere Schleife wird oft übersehen. solange grafik.nächster_schritt() sorgt dafür, dass
sich dein Spiel auf jedem Rechner gleich schnell bewegt — egal ob er 60 oder 144 Bilder pro
Sekunde schafft. Schreibst du dort aus Versehen wenn statt solange, meldet dir grafik.zeige()
das. Bewegung rechnest du immer mit grafik.schritt() (der festen Dauer eines Schrittes in
Sekunden):
x = x + geschwindigkeit * grafik.schritt() // gleich schnell auf jedem Rechner
38.2 Das erste Sprite
Eine bewegliche Figur heißt Sprite. Ein Sprite trägt ein Bild und hat eine Position. Das Bild
malst du mit dem bild-Modul (oder lädst es von der Platte) und übergibst es an grafik:
nutze grafik
nutze bild
// Ein kleines Bild malen — das darf VOR dem Fenster passieren.
nimm figur = bild.neu(32, 32)
bild.setze_block(figur, 0, 0, 32, 32, 240, 80, 80) // ein rotes Quadrat
grafik.öffne_fenster(800, 600, "Sprite")
// WICHTIG: erst das Fenster, dann das Bild zur Grafikkarte hochladen.
nimm held = grafik.neues_sprite(grafik.aus_bild(figur))
grafik.setze_anker(held, 0.5, 0.5) // Bezugspunkt = Mitte (statt obere linke Ecke)
nimm x = 100f
solange grafik.läuft() {
grafik.beginne_bild()
solange grafik.nächster_schritt() {
x = x + 200 * grafik.schritt() // 200 Pixel pro Sekunde nach rechts
}
grafik.setze_position(held, x, 300)
grafik.leere(grafik.farbe(20, 25, 40))
grafik.zeichne_sprite(held) // Sprite in dieses Bild eintragen
grafik.zeichne_szene()
grafik.zeige()
}
grafik.schliesse_fenster()
Ein paar Dinge, die man einmal wissen muss:
- Fenster zuerst, dann Bilder laden.
grafik.aus_bild(...)lädt das Bild auf die Grafikkarte, und das geht erst, wenn das Fenster (und damit die Grafikkarte) bereitsteht. Das Malen der Leinwand darf davor stehen, nur das Hochladen nicht. (grafik.lade_bild("held.png")lädt stattdessen direkt von der Platte.) - Ein Sprite bleibt. Du legst es einmal an;
setze_positionändert nur seinen Ort. Erstgrafik.zeichne_sprite(held)trägt es ins aktuelle Bild ein. Zeichnest du es in einem Bild nicht, ist es in diesem Bild einfach nicht zu sehen — es „gibt" das Sprite trotzdem. fbeinimm x = 100f. Weilxsich um Bruchteile bewegt (200 * schritt()), muss es eine Kommazahl sein — daher dasf. Ganze Zahlen darfst du danach direkt zuweisen (x = 740), Kiste hebt sie an.
Weitere Sprite-Befehle: grafik.setze_drehung(sprite, grad) (in Grad, im Uhrzeigersinn),
grafik.setze_skalierung(sprite, sx, sy), grafik.setze_farbe(sprite, farbe) (färbt es ein), und
grafik.entferne_sprite(sprite), wenn du es nicht mehr brauchst.
38.3 Steuern mit der Tastatur
Zuerst gibst du einer Aktion einen Namen und bindest sie an Tasten (das machst du einmal, vor
der Schleife). Danach fragst du die Aktion ab. Die Tastennamen heißen grafik.TASTE_...:
grafik.belege("links", [grafik.TASTE_LINKS, grafik.TASTE_A])
grafik.belege("rechts", [grafik.TASTE_RECHTS, grafik.TASTE_D])
// … in der inneren Schleife:
x = x + grafik.achse("links", "rechts") * 300 * grafik.schritt()
grafik.achse("links", "rechts") liefert -1, 0 oder +1 — perfekt für Bewegung in einer
Richtung. Für „ist die Taste gerade gedrückt?" gibt es grafik.ist_an("aktion").
Die Maus geht genauso — mit grafik.belege_maustaste(...) und denselben Abfragen. Das steht in Abschnitt 38.13.
38.4 Ein Schuss — die Flanke
Beim Schießen willst du genau einen Schuss pro Tastendruck, egal wie lange die Taste gehalten
wird. Dafür ist grafik.wurde_gedrückt da — es ist genau einmal wahr, im Moment des Drückens:
grafik.belege("schuss", [grafik.TASTE_LEERTASTE])
// … in der inneren Schleife:
wenn grafik.wurde_gedrückt("schuss") {
// einen Schuss abfeuern
}
Der Unterschied ist wichtig: ist_an fragt den Dauerzustand (zum Steuern), wurde_gedrückt
fragt die Flanke (zum Auslösen). Es gibt auch grafik.wurde_losgelassen.
38.5 Zusammenstöße — wer berührt wen
grafik zeichnet — es kennt keine Physik und keine Zusammenstöße. Ob sich zwei Dinge berühren,
gehört zu deinem Spiel, nicht zur Zeichen-Engine. Dafür gibt es kiste2d, die
Spielengine-Schicht von Kiste:
nutze kiste2d
wenn kiste2d.überlappt(schuss_x, schuss_y, 6, 14, ziel_x, ziel_y, 40, 24) {
// Treffer!
}
Die acht Zahlen sind zweimal dasselbe Muster: Mitte x, Mitte y, Breite, Höhe — erst für das eine
Rechteck, dann für das andere. Das passt zu Sprites mit grafik.setze_anker(sprite, 0.5, 0.5), deren
Position ebenfalls ihre Mitte ist.
Berühren zählt nicht als Treffer: Liegen zwei Rechtecke exakt Kante an Kante, überlappen sie nicht. Sonst würde ein Geschoss „treffen", das gerade eben vorbeistreift.
Zum Ausprobieren — das läuft ohne Fenster:
nutze kiste2d
// Ziel: Mitte (400, 80), Größe 40 x 28
// Schuss: Mitte (395, 90), Größe 6 x 16
wenn kiste2d.überlappt(395f, 90f, 6f, 16f, 400f, 80f, 40f, 28f) {
sag "Treffer!"
} sonst {
sag "daneben"
}
// Weit weg — kein Treffer
wenn kiste2d.überlappt(100f, 500f, 6f, 16f, 400f, 80f, 40f, 28f) {
sag "Treffer!"
} sonst {
sag "daneben"
}
Warum nicht einfach selbst rechnen?
Man könnte den Abstand von Hand vergleichen, etwa mit mathe.absolut. Für einen Schuss macht das
keinen Unterschied — aber jeder Aufruf einer eingebauten Funktion kostet Zeit, und ein Spiel prüft
nicht einmal, sondern ständig: Bei 40 Geschossen gegen 25 Gegner sind das 1000 Prüfungen pro Bild,
sechzigmal pro Sekunde. Gemessen frisst die Variante mit mathe.absolut dabei rund ein Fünftel der
16 Millisekunden, die ein Bild hat — mit wert.als_komma in derselben Schleife sogar fast alles.
kiste2d.überlappt rechnet ohne solche Aufrufe und ist damit rund 300-mal billiger; es bleibt bei
unter einem Prozent.
Merksatz fürs eigene Spiel: In einer Schleife, die pro Bild hundertfach läuft, haben eingebaute
Funktionen nichts verloren. Rechne dort direkt, oder nimm einen fertigen Baustein wie überlappt.
kiste2d ist kein verschlossener Kasten
nutze kiste2d funktioniert überall — die Datei wird mit Kiste mitgeliefert und liegt im Ordner lib
neben kiste.exe. Und sie ist ganz normales Kiste, das du lesen kannst: überlappt ist keine
zwanzig Zeilen lang. Schau hinein, wenn du wissen willst, wie so eine Prüfung funktioniert.
Willst du etwas ändern, kopiere kiste2d.ki neben dein Spiel — eine Datei mit demselben Namen im
eigenen Ordner gewinnt gegen die mitgelieferte. So kannst du experimentieren, ohne die Vorlage zu
verlieren.
Wie
grafikbrauchtkiste2d-Spielcode ein gebautes Programm (kiste build) — im Browser-Playground läuft er nicht.
Genau hier zeigt sich, was eine Engine ausmacht: Sie gibt dir Positionen und Größen in die Hand, die Spielregeln schreibst du darauf.
38.6 Ebenen — was liegt vorne, was hinten
Alles landet standardmäßig in der Ebene "welt". Wer mehr Ordnung will, legt weitere Ebenen an —
die zuerst angelegte liegt hinten:
grafik.neue_ebene("hintergrund") // liegt hinter "welt"
grafik.staffle_nach_y("welt") // in "welt": wer weiter unten ist, ist vorn
nimm boden = grafik.neues_sprite(kachel, "hintergrund") // Sprite in eine benannte Ebene
grafik.staffle_nach_y(ebene) ist die klassische Tiefenstaffelung eines Draufsicht-Spiels: Figuren
weiter unten am Bildschirm werden vor die weiter oben gezeichnet.
Für Ebenen, in denen sich nichts überlappt, gibt es die schnellere Variante:
grafik.bündle_nach_textur("hintergrund") // sortiert nach Bild, nicht nach Tiefe
grafik.bündle_nach_textur(ebene) gruppiert die Sprites nach ihrem Bild, damit die Grafikkarte so
selten wie möglich die Textur wechselt — dasselbe Ziel wie beim Atlas (Abschnitt 38.7), nur von der
anderen Seite. Typisch für Kachelböden, Sternenfelder, Hintergründe.
Das ist eine Zusicherung, kein reiner Geschwindigkeits-Schalter. Mit diesem Aufruf sagst du Kiste: In dieser Ebene überlappt sich nichts. Danach bestimmt nicht mehr die Reihenfolge deiner Aufrufe, was vorne liegt, sondern die Textur. Überlappen sich die Sprites doch, zeichnet Kiste sie in einer anderen Reihenfolge als erwartet — nimm dann
staffle_nach_yoder eine eigene Ebene.
Wie staffle_nach_y gehört auch dieser Aufruf vor die Schleife, nicht hinein.
38.7 Der Atlas — viele Bilder, ein Zeichenbefehl
Jedes Mal, wenn die Grafikkarte die Textur wechselt, kostet das Zeit. Kommen alle Figuren aus einem großen Bild (einem Atlas), gibt es keinen Wechsel — hunderte Sprites kosten dann zusammen einen Zeichenbefehl statt hunderte. Du legst deine Sprite-Bilder in einen Ordner und lädst ihn in einem Zug:
grafik.lade_atlas("bilder/", "sprites") // alle Bilder aus bilder/ → ein Atlas namens "sprites"
nimm held = grafik.neues_sprite(grafik.kachel("sprites", "held")) // Kachel = Dateiname ohne Endung
nimm feind = grafik.neues_sprite(grafik.kachel("sprites", "feind"))
Wie viele Zeichenbefehle ein Bild wirklich kostet, verrät grafik.zeichenbefehle() — das erste
Werkzeug, um ein ruckelndes Spiel zu prüfen. Aus einem Atlas steht dort 1, aus vielen Einzelbildern
eine große Zahl.
38.8 Die Kamera
Ohne Kamera ist der Bildschirm die Welt: Punkt (0,0) ist oben links. Mit der Kamera kannst du der Figur folgen, hinein zoomen oder drehen. Jeder Setter ändert nur sein Feld:
grafik.setze_kamera(spieler_x, spieler_y) // dieser Weltpunkt liegt in der Bildschirmmitte
grafik.setze_zoom(2.0) // doppelt so groß
grafik.setze_kameradrehung(15) // 15 Grad, im Uhrzeigersinn
38.9 Ein kleines vollständiges Spiel
Alles zusammen: ein Schiff, das du steuerst, schießt auf ein wanderndes Ziel. Das ist der Kern des
Beispiels beispiele/grafik_durchstich.ki, das mit Kiste mitgeliefert wird — spiel es und lies den Code.
nutze grafik
nutze bild
nutze mathe
nimm schiff_b = bild.neu(40, 28)
bild.setze_block(schiff_b, 0, 0, 40, 28, 70, 200, 120)
nimm ziel_b = bild.neu(40, 28)
bild.setze_block(ziel_b, 0, 0, 40, 28, 220, 70, 60)
nimm schuss_b = bild.neu(6, 16)
bild.setze_block(schuss_b, 0, 0, 6, 16, 255, 220, 60)
grafik.öffne_fenster(800, 600, "Grafik-Durchstich")
grafik.belege("links", [grafik.TASTE_LINKS, grafik.TASTE_A])
grafik.belege("rechts", [grafik.TASTE_RECHTS, grafik.TASTE_D])
grafik.belege("schuss", [grafik.TASTE_LEERTASTE])
nimm schiff = grafik.neues_sprite(grafik.aus_bild(schiff_b))
grafik.setze_anker(schiff, 0.5, 0.5)
nimm ziel = grafik.neues_sprite(grafik.aus_bild(ziel_b))
grafik.setze_anker(ziel, 0.5, 0.5)
nimm schuss = grafik.neues_sprite(grafik.aus_bild(schuss_b))
grafik.setze_anker(schuss, 0.5, 0.5)
nimm sx = 400f // Schiff-Position
nimm tx = 200f // Ziel-Position
nimm richtung = 1f // Ziel wandert hin und her
nimm schuss_aktiv = falsch
nimm bx = 0f
nimm by = 0f
solange grafik.läuft() {
grafik.beginne_bild()
solange grafik.nächster_schritt() {
nimm dt = grafik.schritt()
sx = sx + grafik.achse("links", "rechts") * 360 * dt
sx = mathe.maximum(20, mathe.minimum(780, sx)) // am Rand klemmen
tx = tx + richtung * 150 * dt
wenn tx > 740 { tx = 740; richtung = -1 }
wenn tx < 60 { tx = 60; richtung = 1 }
wenn grafik.wurde_gedrückt("schuss") und schuss_aktiv == falsch {
schuss_aktiv = wahr
bx = sx
by = 520
}
wenn schuss_aktiv {
by = by - 560 * dt
wenn by < -20 {
schuss_aktiv = falsch
} sonst {
wenn mathe.absolut(bx - tx) < 23 und mathe.absolut(by - 80) < 22 {
schuss_aktiv = falsch
tx = 400 // Ziel zurücksetzen (echtes Spiel: zufällig)
}
}
}
}
grafik.setze_position(schiff, sx, 550)
grafik.setze_position(ziel, tx, 80)
grafik.leere(grafik.farbe(8, 10, 24))
grafik.zeichne_sprite(ziel)
wenn schuss_aktiv {
grafik.setze_position(schuss, bx, by)
grafik.zeichne_sprite(schuss)
}
grafik.zeichne_sprite(schiff)
grafik.zeichne_szene()
grafik.zeige()
}
grafik.schliesse_fenster()
Übersetzen und starten:
kiste build grafik_durchstich.ki -o grafik_durchstich.exe
38.10 Text auf den Schirm schreiben
Jedes Spiel muss etwas sagen: wie viele Punkte du hast, welche Welle kommt, dass es vorbei ist. Dafür gibt es einen Befehl:
grafik.zeichne_text("Hallo", 20, 15, grafik.farbe(255, 255, 255))
Die vier Angaben sind: der Text, die x-Stelle, die y-Stelle und die Farbe. x und y zählen von der
oberen linken Ecke des Fensters — 20, 15 heißt also „20 Pixel nach rechts, 15 nach unten". Ein
fünfter Wert vergrößert die Schrift: grafik.zeichne_text("GROSS", 20, 60, farbe, 3) schreibt
dreimal so groß.
Wichtig: Text zeichnest du NACH grafik.zeichne_szene(). Sonst malen die Sprites über deine
Schrift. Und der Text bleibt immer an derselben Stelle am Fenster stehen, auch wenn die Kamera durch
die Welt fährt — genau das will man bei einem Punktestand.
Eine Zahl in den Text hineinschreiben
Der Punktestand ist eine Zahl, zeichne_text will aber einen Text. Du musst nichts von Hand
umwandeln: Schreib die Zahl in geschweiften Klammern mitten in den Text, dann setzt Kiste sie beim
Zeichnen automatisch als Text ein.
grafik.zeichne_text("Punkte: {punkte}", 16, 14, weiss)
Steht in punkte gerade die Zahl 120, erscheint Punkte: 120. Das ist dieselbe Schreibweise wie bei
sag "Punkte: {punkte}" (§4) — und der übliche Weg, Text und Zahlen zu mischen.
Warum nicht einfach
"Punkte: " + punkte? Weil+in Kiste nur Text mit Text verbindet. Text und Zahl zusammenzuzählen lehnt Kiste ab und sagt dir auch gleich, was stattdessen geht: „+ kann text und ganz nicht verbinden — explizit umwandeln (als_text / …) oder Interpolation nutzen". Die geschweiften Klammern sind diese Interpolation, und sie sind der kürzere Weg.
Text mittig setzen — textbreite
Ein „GAME OVER" gehört in die Mitte. Dafür musst du wissen, wie breit der Text wird:
nimm breite = grafik.textbreite("GAME OVER")
textbreite gibt die Breite in Pixeln zurück (bei normaler Größe). Zum Mittigsetzen ziehst du sie
von der Fensterbreite ab und teilst durch zwei:
nimm x = (640 - breite) / 2
Zeichnest du größer, wird der Text entsprechend breiter: bei Größe 3 ist er dreimal so breit, also
grafik.textbreite(spruch) * 3.
Alles zusammen
Drück die Leertaste für Punkte. Nach zehn Sekunden ist Schluss, und „GAME OVER" steht mittig:
nutze grafik
grafik.öffne_fenster(640, 400, "Punkte zählen")
grafik.belege("punkten", [grafik.TASTE_LEERTASTE])
nimm punkte = 0
nimm übrig = 10f
nimm weiss = grafik.farbe(240, 240, 255)
nimm gelb = grafik.farbe(255, 210, 40)
solange grafik.läuft() {
grafik.beginne_bild()
solange grafik.nächster_schritt() {
nimm dt = grafik.schritt()
wenn übrig > 0f {
übrig = übrig - dt
wenn grafik.wurde_gedrückt("punkten") {
punkte = punkte + 10
}
}
}
grafik.leere(grafik.farbe(10, 12, 30))
grafik.zeichne_szene()
grafik.zeichne_text("Punkte: {punkte}", 16, 14, weiss)
wenn übrig <= 0f {
nimm spruch = "GAME OVER"
nimm breite = grafik.textbreite(spruch) * 3
nimm x = (640 - breite) / 2
grafik.zeichne_text(spruch, x, 180, gelb, 3)
}
grafik.zeige()
}
grafik.schliesse_fenster()
Die eingebaute Schrift kann Deutsch: ä ö ü Ä Ö Ü ß und das €-Zeichen erscheinen richtig, nicht als Kästchen.
38.11 Ton im Spiel
Ein Spiel ohne Ton fühlt sich tot an. grafik und klang funktionieren im selben Programm — du
schreibst einfach beides oben hin:
nutze grafik
nutze klang
Die wichtigste Regel: die Klangdatei liegt neben deiner .ki-Datei
Hier scheitern die meisten beim ersten Mal. Kiste sucht die Klangdatei in dem Ordner, in dem
deine .ki-Datei liegt — nicht dort, wo du gerade kiste aufrufst.
mein-spiel/
spiel.ki ← dein Programm
schuss.wav ← die Klangdatei DANEBEN
Dann lädst du sie mit dem bloßen Dateinamen:
nimm schuss = klang.lade("schuss.wav")
Und wenn die Datei fehlt? Dann stürzt nichts ab. Dein Spiel läuft ganz normal weiter, nur eben stumm, und Kiste schreibt einmal beim Start eine Zeile in die Konsole:
kiste: klang.lade: "schuss.wav" nicht ladbar (open schuss.wav: The system cannot find the file specified.)
Die Zeile nennt den Dateinamen, aber keine Zeilennummer — sie ist eine Warnung, kein Absturz.
Siehst du sie, liegt die Datei am falschen Ort oder heißt anders (Groß- und Kleinschreibung zählt).
Auch die Aufrufe von klang.spiele bleiben dann einfach still; du bekommst die Meldung nicht bei
jedem Schuss noch einmal.
Abspielen
klang.lade machst du einmal vor der Schleife — es liest die Datei von der Festplatte, und das
willst du nicht sechzigmal pro Sekunde. In der Schleife spielst du nur noch ab:
klang.spiele(schuss)
Das hält das Spiel nicht an, und mehrere Klänge dürfen sich überlagern — der Treffer darf klingen,
während der Schuss noch nachhallt. Mehr klang-Befehle (Hintergrundmusik in Schleife, Lautstärke,
Stoppen) stehen in §34.10g.
Ein Beispiel mit Schuss
Leertaste drücken, Schuss hören, Zähler steigt:
nutze grafik
nutze klang
grafik.öffne_fenster(640, 300, "Ton im Spiel")
grafik.belege("schiessen", [grafik.TASTE_LEERTASTE])
nimm schuss = klang.lade("schuss.wav")
nimm schüsse = 0
nimm weiss = grafik.farbe(240, 240, 255)
solange grafik.läuft() {
grafik.beginne_bild()
solange grafik.nächster_schritt() {
nimm dt = grafik.schritt()
wenn grafik.wurde_gedrückt("schiessen") {
klang.spiele(schuss)
schüsse = schüsse + 1
}
}
grafik.leere(grafik.farbe(10, 12, 30))
grafik.zeichne_szene()
grafik.zeichne_text("Leertaste drücken — Schüsse: {schüsse}", 16, 14, weiss)
grafik.zeige()
}
grafik.schliesse_fenster()
Zum Ausprobieren ohne eigene Klangdatei: Im Ordner beispiele/ liegt grafik_durchstich.ki
zusammen mit schuss.wav und treffer.wav — ein kleines Spiel mit Ton, das du direkt bauen und
hören kannst.
Nur
nutze guiundnutze grafikvertragen sich nicht im selben Programm.klangdarfst du zu beiden dazunehmen.
38.12 Viele Geschosse gleichzeitig
In §38.4 fliegt ein Schuss, und der nächste geht erst, wenn er verschwunden ist. Das fühlt sich schnell einschränkend an. Ein richtiges Spiel lässt mehrere Geschosse gleichzeitig fliegen.
Dafür brauchst du kein neues Kiste-Wissen — nur ein Muster: für jede Eigenschaft eine Liste, und ein Merker, welcher Platz gerade benutzt wird.
fest MAX = 6
nimm schuss_x = liste.gefüllt(MAX, 0f) // wo ist Geschoss Nummer i?
nimm schuss_y = liste.gefüllt(MAX, 0f)
nimm schuss_aktiv = liste.gefüllt(MAX, falsch) // fliegt es gerade?
Statt Geschosse zu erzeugen und wieder wegzuwerfen, gibt es feste Plätze. Ein Schuss belegt einen freien Platz; ist er aus dem Bild, wird der Platz wieder frei. Drei Dinge macht dein Spiel damit: einen freien Platz suchen (beim Schießen), alle aktiven bewegen und prüfen (jeden Schritt), alle aktiven zeichnen (jedes Bild).
Ehrlich gesagt: das ist umständlich
Vier Listen für eine Sorte Objekt (x, y, aktiv, dazu die Sprites) — und jede neue Eigenschaft,
etwa Schaden oder Tempo, wäre eine Liste mehr. Die Suche nach einem freien Platz sind acht Zeilen, die
nichts über dein Spiel aussagen, und dieselbe solange-Schleife steht am Ende dreimal da.
Für eine Sorte Objekt ist das gut auszuhalten, und es ist ein Muster, das du in vielen Spielen
wiedererkennen wirst. Sobald eine zweite Sorte dazukommt — Explosionen, Gegner, herabfallende
Teile — kopierst du dasselbe Gerüst noch einmal. Spätestens dann wünscht man sich einen fertigen
Baustein dafür. Er ist geplant und kommt in kiste2d; bis dahin ist dieses Muster der ehrliche
Weg, und er funktioniert.
Ein vollständiges Beispiel
Pfeiltasten steuern, Leertaste schießt, bis zu sechs Geschosse gleichzeitig, das Ziel wandert:
// Viele Schüsse: Pfeiltasten steuern, Leertaste schießt.
// Bis zu sechs Geschosse fliegen gleichzeitig.
nutze grafik
nutze bild
nutze liste
nutze kiste2d
grafik.öffne_fenster(640, 480, "Viele Schüsse")
grafik.belege("links", [grafik.TASTE_LINKS, grafik.TASTE_A])
grafik.belege("rechts", [grafik.TASTE_RECHTS, grafik.TASTE_D])
grafik.belege("schiessen", [grafik.TASTE_LEERTASTE])
// --- Drei einfarbige Bilder malen ---
nimm schiff_b = bild.neu(40, 20)
bild.setze_block(schiff_b, 0, 0, 40, 20, 70, 200, 120)
nimm schuss_b = bild.neu(6, 14)
bild.setze_block(schuss_b, 0, 0, 6, 14, 255, 220, 60)
nimm ziel_b = bild.neu(40, 24)
bild.setze_block(ziel_b, 0, 0, 40, 24, 220, 70, 60)
nimm schiff = grafik.neues_sprite(grafik.aus_bild(schiff_b))
grafik.setze_anker(schiff, 0.5, 0.5)
nimm ziel = grafik.neues_sprite(grafik.aus_bild(ziel_b))
grafik.setze_anker(ziel, 0.5, 0.5)
// --- Die Geschosse: feste Plätze statt wachsender Liste ---
//
// MAX ist die Höchstzahl gleichzeitiger Geschosse. Für JEDE Eigenschaft gibt es
// eine eigene Liste, und Platz Nummer i gehört überall zum selben Geschoss:
// schuss_x[2], schuss_y[2] und schuss_aktiv[2] beschreiben dasselbe.
//
// Warum feste Plätze und nicht eine Liste, die wächst und schrumpft? Weil
// Löschen mitten in einer Liste alle folgenden Einträge verschiebt — dann zeigt
// Platz 2 plötzlich auf ein anderes Geschoss. Mit einem "fliegt gerade"-Merker
// bleibt jeder Platz, wo er ist; ein abgeschossenes Geschoss gibt seinen Platz
// einfach wieder frei.
fest MAX = 6
nimm schuss_kachel = grafik.aus_bild(schuss_b)
nimm schuss_sprite = liste.gefüllt(MAX, 0)
nimm i = 0
solange i < MAX {
schuss_sprite[i] = grafik.neues_sprite(schuss_kachel)
grafik.setze_anker(schuss_sprite[i], 0.5, 0.5)
i = i + 1
}
nimm schuss_x = liste.gefüllt(MAX, 0f)
nimm schuss_y = liste.gefüllt(MAX, 0f)
nimm schuss_aktiv = liste.gefüllt(MAX, falsch)
nimm schiff_x = 320f
nimm ziel_x = 320f
nimm ziel_richtung = 1f
nimm nachlade = 0f
nimm treffer = 0
nimm weiss = grafik.farbe(240, 240, 255)
solange grafik.läuft() {
grafik.beginne_bild()
solange grafik.nächster_schritt() {
nimm dt = grafik.schritt()
schiff_x = schiff_x + grafik.achse("links", "rechts") * 300 * dt
// Das Ziel wandert hin und her.
ziel_x = ziel_x + ziel_richtung * 80 * dt
wenn ziel_x > 600 {
ziel_richtung = -1
}
wenn ziel_x < 40 {
ziel_richtung = 1
}
// Nachladezeit — ohne sie flögen sechzig Schüsse pro Sekunde heraus.
wenn nachlade > 0f {
nachlade = nachlade - dt
}
// (1) SCHIESSEN: einen freien Platz suchen.
//
// Das ist die umständlichste Stelle: Kiste hat keinen Befehl "gib mir
// einen freien Platz", also schaust du selbst alle durch und merkst dir
// den ersten freien. `frei` startet bei -1 (= keiner gefunden).
wenn grafik.wurde_gedrückt("schiessen") und nachlade <= 0f {
nimm frei = -1
nimm j = 0
solange j < MAX {
wenn frei < 0 und nicht schuss_aktiv[j] {
frei = j
}
j = j + 1
}
// Sind alle sechs unterwegs, wird eben nicht geschossen.
wenn frei >= 0 {
schuss_x[frei] = schiff_x
schuss_y[frei] = 430f
schuss_aktiv[frei] = wahr
nachlade = 0.15f
}
}
// (2) BEWEGEN UND PRÜFEN: über alle Plätze laufen, die aktiven anfassen.
nimm k = 0
solange k < MAX {
wenn schuss_aktiv[k] {
schuss_y[k] = schuss_y[k] - 900f * dt
wenn schuss_y[k] < -20f {
// Oben aus dem Bild: Platz wieder freigeben.
schuss_aktiv[k] = falsch
} sonst {
// Die Trefferfläche des Ziels ist mit 70 x 32 bewusst
// GRÖSSER als sein Bild (40 x 24) — das fühlt sich fairer
// an. Die Hitbox muss nicht das Bild sein.
wenn kiste2d.überlappt(schuss_x[k], schuss_y[k], 6f, 14f, ziel_x, 80f, 70f, 32f) {
treffer = treffer + 1
schuss_aktiv[k] = falsch
}
}
}
k = k + 1
}
}
grafik.setze_position(schiff, schiff_x, 450)
grafik.setze_position(ziel, ziel_x, 80)
grafik.leere(grafik.farbe(10, 12, 30))
grafik.zeichne_sprite(ziel)
// (3) ZEICHNEN: dieselbe Schleife ein drittes Mal — nur die aktiven.
// Genau diese dreifache Wiederholung ist der Grund, warum kiste2d
// später einen fertigen Baustein fürs Verwalten vieler Objekte bekommt.
nimm z = 0
solange z < MAX {
wenn schuss_aktiv[z] {
grafik.setze_position(schuss_sprite[z], schuss_x[z], schuss_y[z])
grafik.zeichne_sprite(schuss_sprite[z])
}
z = z + 1
}
grafik.zeichne_sprite(schiff)
grafik.zeichne_szene()
grafik.zeichne_text("Treffer: {treffer}", 16, 14, weiss)
grafik.zeige()
}
grafik.schliesse_fenster()
38.13 Steuern mit der Maus
Die Maus funktioniert genau wie die Tastatur (Abschnitt 38.3): Du gibst einer Aktion einen Namen, bindest sie an eine Maustaste, und fragst sie danach mit denselben drei Befehlen ab. Neu zu lernen ist nur ein Wort:
grafik.belege_maustaste("schuss", [grafik.MAUS_LINKS])
grafik.belege_maustaste("menü", [grafik.MAUS_RECHTS])
Das machst du einmal, vor der Schleife — genau wie grafik.belege(...). Danach fragst du ab:
grafik.ist_an("schuss")— wird die Taste gerade gehalten?grafik.wurde_gedrückt("schuss")— wurde sie in diesem Schritt gedrückt? Genau einmal je Klick.grafik.wurde_losgelassen("schuss")— wurde sie losgelassen?
Der schöne Teil: eine Aktion darf auf beidem liegen.
grafik.belege("schuss", [grafik.TASTE_LEERTASTE])
grafik.belege_maustaste("schuss", [grafik.MAUS_LINKS])
Jetzt schießt sowohl die Leertaste als auch die linke Maustaste — dein Spielcode fragt weiterhin nur grafik.wurde_gedrückt("schuss") und weiß gar nicht, was der Spieler benutzt hat.
Wo zeigt die Maus hin?
nimm zx = grafik.zeiger_x()
nimm zy = grafik.zeiger_y()
Beide liefern Bildschirmpixel, (0, 0) ist oben links — dieselben Koordinaten, in denen du auch zeichnest. Ein Fadenkreuz ist damit zwei Zeilen:
nutze grafik
grafik.öffne_fenster(800, 600, "Fadenkreuz")
grafik.belege_maustaste("schuss", [grafik.MAUS_LINKS])
solange grafik.läuft() {
grafik.beginne_bild()
solange grafik.nächster_schritt() {
wenn grafik.wurde_gedrückt("schuss") {
nimm zx = grafik.zeiger_x()
nimm zy = grafik.zeiger_y()
sag "Treffer bei {zx} / {zy}"
}
}
grafik.leere(grafik.farbe(20, 25, 40))
grafik.zeichne_text("+", grafik.zeiger_x(), grafik.zeiger_y(), grafik.farbe(255, 230, 120))
grafik.zeichne_szene()
grafik.zeige()
}
grafik.schliesse_fenster()
Zwei Dinge, die dich sonst überraschen:
Der Zeiger steht innerhalb eines Schrittes still. Er wird zusammen mit den Tasten einmal je Schritt abgelesen. Deshalb treffen Zielen und Schießen denselben Punkt — auch wenn die Maus währenddessen weiterwandert.
Die Koordinaten können außerhalb des Fensters liegen. Zieh die Maus bei gedrückter Taste aus dem Fenster heraus, und du bekommst Werte wie 139 / 886 in einem 600 Pixel hohen Fenster — auch negative. Wenn dein Spiel darauf baut, dass der Zeiger drin ist, prüf es selbst.
Solange du die Kamera nicht bewegst, sind Bildschirmpixel und Weltkoordinaten dasselbe. Mit grafik.setze_kamera(...) gilt das nicht mehr — dann zeigt zeiger_x() weiter auf den Bildschirm, deine Figuren stehen aber in der Welt.
Das Mausrad
grafik.rad() liefert, wie weit in diesem Schritt gedreht wurde: 1.0 nach vorn, -1.0 zurück, 0.0 wenn nichts passiert ist. Drehst du schnell, kommen auch 2.0 oder mehr — die Rastungen eines Schrittes werden zusammengezählt, es geht nichts verloren.
nimm zoom = als_komma(1.0)
// … in der inneren Schleife:
nimm r = grafik.rad()
wenn r != als_komma(0.0) {
zoom = zoom + r * als_komma(0.1)
grafik.setze_zoom(zoom)
}
Achtung, hier stolpert fast jeder:
grafik.rad(),zeiger_x(),zeiger_y()undachse()liefernkomma(IEEE-754). Das Literal0.1ist aber eindezimal, und die beiden mischen sich nicht —zoom + grafik.rad() * 0.1lehnt Kiste ab. Schreibals_komma(0.1), dann passt es. Die Fehlermeldung nennt dir den Ausweg übrigens selbst.
38.14 Fehler — zwei Regeln
- Alles vor der Schleife wirft sofort mit Zeile und Spalte: eine fehlende Bilddatei, ein unbekannter Atlas, eine doppelt angelegte Ebene.
- Alles in der Schleife klebt und kommt beim nächsten
grafik.zeige()heraus: ein Tippfehler in einem Aktionsnamen oder ein falsches Sprite bricht das Spiel nicht mitten im Bild ab, sondern einmal sauber je Bild.
38.15 Was grafik schon kann — und was noch fehlt
Text und Ton sind da. Ein Spiel mit grafik kann beides:
- Text auf den Schirm schreiben — Punktestand, Wellen, „Game Over" — mit
grafik.zeichne_text(text, x, y, farbe). Du brauchst keine Ziffern-Bilder zu malen.grafik.textbreite(text)sagt dir, wie breit ein Text wird (fürs Mittig- oder Rechtsbündig-Setzen). - Ton abspielen —
nutze klangfunktioniert im selben Programm wienutze grafik. Der Schuss knallt, der Treffer bumst. Wie das geht, steht in §38.11 — die Klangdatei legst du neben deine.ki-Datei.
Nur
nutze guiundnutze grafikzusammen gehen nicht — dafür kommt eine deutliche Fehlermeldung. Alles andere darfst du dazunehmen:klang,bild,mathe,zufall,datei, …
Was wirklich noch fehlt: eigene Schriftarten laden (die eingebaute Schrift kann Deutsch samt Umlauten, aber du kannst noch keine eigene TTF mitgeben), Zwischenbilder (Interpolation) für noch weichere Bewegung, Vollbild und mehrere Fenster.