[#_lesson_9_pictures] == Lektion 9: Bilder image::learn/09-pictures.png[Die fertige Lektion, 480] Alles, was du bisher gezeichnet hast, hast du mit Kästen, Linien und dem schlichten Text der Engine gezeichnet. Das war Absicht: Acht Lektionen ohne Grafiken hießen acht Lektionen, in denen nichts schiefgehen konnte außer deinem Programm. Jetzt werden aus den Kästen ein Raumschiff und ein paar Felsen. Ein Bild, das die Engine aufs Overlay zeichnet, heißt *Sprite*. Diese Lektion lädt sie, zeichnet sie, misst sie aus, bringt sie in die richtige Reihenfolge und geht durch ein Bild, das mehrere Frames einer Animation enthält. Am Ende sieht das Spiel aus Lektion sieben wie ein Spiel aus. [#_the_art_kit] === Das Grafik-Kit Du musst nichts zeichnen. Zu diesem Buch gehört ein kleiner Ordner mit Grafiken, und alles in dieser und der nächsten Lektion benutzt ihn. Such den Ordner `art`, der mit dem Buch gekommen ist, und kopier den ganzen Ordner in den Ordner dieser Lektion, neben dein Skript. Am Ende sollte das so aussehen: ---- singe/ <- dein Arbeitsordner Singe/ data/ rocks/ rocks.singe art/ spaceship.png asteroid.png bullet.png star.png walk.png shoot.wav boom.wav ---- Deine Grafiken kommen in den eigenen Ordner deines Spiels, und der Ordner `Singe` der Engine bleibt, wo er ist, draußen im Arbeitsordner. Das ist gleich wichtig, denn es entscheidet, wie du eine Datei benennst, wenn du sie lädst. Der Ordner `Singe` ist der, den die Engine bei deinem allerersten Start in Lektion eins für dich entpackt hat. Es lohnt sich zu wissen, was darin ist, denn diese Dateien darfst du ebenfalls benutzen, und jeder Leser dieses Buchs hat sie: * `Singe/click.wav` -- ein kurzer Klick, den das Menü der Engine für seinen Soundtest benutzt. * `Singe/FreeSansBold.ttf` -- eine TrueType-Schriftart, für Lektion neunzehn. * `Singe/menuIntro.flac` -- die Musik, die das Menü der Engine spielt. Du benutzt sie in Lektion zehn. * `Singe/menuBackground.mkv` -- ein Video, für Lektion vierzehn. * `Singe/DragonModel.glb` und `Singe/SingeText.glb` -- zwei 3D-Modelle, für Lektion dreiundzwanzig. * `Singe/missing.png` -- das Bild, das die Engine zeichnet, wenn ein Spiel nach einem fragt, das sie nicht finden kann. Die Bilder im Kit sind absichtlich klein. Das Raumschiff ist 32 Pixel breit und 24 hoch, ein Felsen ist 24 mal 24, ein Schuss 4 mal 10 und ein Stern 8 mal 8. Das klingt winzig, bis du dich daran erinnerst, wie groß der Bildschirm wirklich ist, und das klären wir als Nächstes. [#_how_big_the_screen_is] === Wie groß der Bildschirm ist Ohne Video zum Abspielen gibt Singe deinem Spiel eine Zeichenfläche von 720 Pixeln Breite und 480 Höhe, und die Fläche, auf die du zeichnest -- das *Overlay* -- ist in jeder Richtung halb so groß: *360 mal 240*. Singe streckt sie für dich, bis sie das Fenster füllt, also ist ein Raumschiff von 32 Pixeln etwa ein Zehntel so breit wie der Bildschirm, und ungefähr so sieht ein Raumschiff in einem Arcade-Spiel aus. Tipp nie 360 und 240 in dein Programm. Frag: [source,lua] ---- local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() ---- Diese beiden melden die Größe des Overlays, und ein Spiel, das fragt, ist ein Spiel, das immer noch funktioniert, wenn jemand es in einer anderen Größe startet. Eine Sache zählt nicht in diesen Pixeln: `overlayPrint` aus Lektion eins zählt in Zeichenzellen. Jede Zelle ist 6 Pixel breit und 13 hoch, also passen auf ein Overlay von 360 mal 240 60 Spalten mal 18 Zeilen Text. Sprites sind in Pixeln, `overlayPrint` ist in Zellen, und wer die beiden verwechselt, verbringt damit einen verwirrten Nachmittag, wenn er es zulässt. [#_one_picture_on_screen] === Ein Bild auf dem Bildschirm Leg neben deinem Ordner `art` eine neue Datei an und tipp das ein. [source,lua] ---- dofile("Singe/Framework.singe") local ship = spriteLoad(DIR .. "art/spaceship.png") function onOverlayUpdate() overlayClear() spriteDraw(ship, 160, 100) return OVERLAY_UPDATED end function onShutdown() spriteUnload(ship) end ---- Starte es. Ein kleines blaues Raumschiff sitzt nahe der Mitte eines schwarzen Bildschirms und zeigt nach oben. [#_what_just_happened_5] === Was gerade passiert ist Vier neue Dinge, eins nach dem anderen. [source,lua] ---- local ship = spriteLoad(DIR .. "art/spaceship.png") ---- `spriteLoad` liest eine Bilddatei von der Festplatte, dekodiert sie und hält das Bild im Speicher. Was es zurückgibt, ist nicht das Bild. Es ist ein *Handle*: eine schlichte Zahl, eine Eintrittskarte, die Art der Engine zu sagen: „das da“. Du gibst diese Zahl jedes Mal an die Engine zurück, wenn du etwas mit dem Bild tun willst, und du musst nie wissen oder dich darum kümmern, welche Zahl es ist. Gib sie aus, wenn du willst -- `debugPrint(ship)` aus Lektion acht zeigt dir eine kleine Zahl -- aber über die Zahl selbst musst du dir keine Gedanken machen, und nichts in deinem Programm sollte je davon abhängen, welche es ist. Die einzige Regel ist, dass du sie irgendwo aufbewahrst, wo du wieder an sie herankommst, und genau dafür ist eine Variable da. `DIR` ist auch neu, und es löst ein Problem, das der Aufbau erzeugt, den du seit Lektion eins benutzt. Singe sucht Dateien relativ zu dem Ordner, in dem du standest, als du es gestartet hast, *nicht* relativ zu deinem Skript. Du startest es aus deinem Arbeitsordner, also schickt ein schlichtes `art/spaceship.png` die Engine auf die Suche nach `singe/art/spaceship.png`, draußen neben dem Ordner `Singe`. Dort sind deine Grafiken nicht. Sie liegen eine Ebene tiefer, in `rocks/art/spaceship.png`, bei deinem Skript. `DIR` schließt diese Lücke. Es ist ein Stück Text mit dem Ordner, in dem dein Skript liegt, mit dem Schrägstrich schon am Ende, also nennt `DIR .. "art/spaceship.png"` -- zwei Stücke Text mit `..` verbunden, wie du es seit Teil eins machst -- das Bild richtig, egal wie das Spiel gestartet wurde, und nennt es auch dann noch richtig, wenn du den ganzen Ordner woandershin verschiebst. `DIR` ist einer der Namen, die `Singe/Framework.singe` festlegt, und dafür ist die Zeile mit `dofile` oben in deinem Skript da. Diese Zeile und was darin steckt hast du in Lektion sieben kennengelernt, und jetzt verdient sie sich ihren Platz: Ohne sie gibt es kein `DIR`, und ohne `DIR` findet die Engine dein Raumschiff nicht. Benutz `DIR` für jede Datei, die dein Spiel lädt, von hier bis zum Ende des Buchs. [source,lua] ---- spriteDraw(ship, 160, 100) ---- Zeichne das Sprite mit diesem Handle an dieser Position. Wie jeder Zeichenaufruf funktioniert es nur in `onOverlayUpdate`. Die Position ist die *linke obere Ecke* des Bildes, nicht seine Mitte. Verlang `(160, 100)`, und das oberste, linkeste Pixel des Raumschiffs landet dort, und der Rest hängt nach rechts unten. Darüber stolpert jeder einmal, meistens, wenn er etwas zentrieren will und es um seine halbe Größe zu weit rechts unten herauskommt. Wenn du lieber die Mitte angeben willst, sag es: [source,lua] ---- spriteDraw(ship, 160, 100, true) ---- Ein viertes Argument `true` bedeutet „nimm diesen Punkt als Mitte“. Beide Formen stehen im Eintrag zu `spriteDraw` in der Referenz, zusammen mit zwei weiteren, die das Bild in ein Rechteck strecken. [source,lua] ---- function onShutdown() spriteUnload(ship) end ---- `onShutdown` ist ein Callback wie `onOverlayUpdate` und die beiden Eingabe-Callbacks aus Lektion drei. Die Engine ruft ihn einmal auf, wenn dein Spiel endet. Hier gibst du zurück, was du genommen hast: `spriteUnload` gibt das Bild frei und wirft das Handle weg. Genau genommen könntest du ihn weglassen. Die Engine gibt alles frei, was dein Skript geladen hat, wenn das Spiel endet, also ist ein vergessenes `spriteUnload` kein Speicherleck. Schreib es trotzdem. Es kostet eine Zeile, es sagt, was deinem Programm gehört, und gleich siehst du den Fall, in dem das vergessene Freigeben wirklich zählt. Eine Warnung zu diesem Handle: Sobald du ein Sprite freigegeben hast, ist die Zahl tot. Damit zu zeichnen zeichnet nicht einfach nichts, es beendet dein Spiel mit einem Fehler. [#_transparency_and_why_it_is_a_png] === Transparenz und warum es ein PNG ist Das Raumschiff ist ein Dreieck mit zwei Flossen. Die Datei, in der es wohnt, ist ein Rechteck, 32 mal 24, denn jede Bilddatei ist ein Rechteck. Was ist also mit den Ecken passiert? Sie sind *transparent*. Ein PNG kann für jedes einzelne Pixel sagen, wie deckend es ist, und die Bilder im Kit sagen für jedes Pixel außerhalb der Form „überhaupt nicht da“. Singe hält sich daran, also scheint der schwarze Hintergrund durch die Ecken, und das Raumschiff hat einen Umriss statt eines Kastens drumherum. Das ist der Grund, warum das Kit aus PNG besteht und nicht aus JPEG. Ein JPEG kann überhaupt keine Transparenz speichern -- es würde dir das Raumschiff in einem grauen Kasten geben -- und es verschmiert außerdem harte Kanten, und das ist das Letzte, was kleine Grafiken brauchen. Für Spielgrafiken PNG. Für ein Foto, das den ganzen Bildschirm füllt und keine transparenten Stellen hat, ist JPEG in Ordnung und viel kleiner. Der Eintrag zu `spriteLoad` in der Referenz listet jedes Format auf, das Singe lesen kann, und das sind viele. Probier es selbst: Mach aus dem Schwarz etwas anderes, indem du einen Kasten hinter das Raumschiff zeichnest, bevor du das Raumschiff zeichnest. Die Ecken zeigen, was auch immer dahinter ist, und genau darum geht es. [#_do_not_load_while_you_draw] === Lade nicht, während du zeichnest Hier ist der Fehler, den dieser Abschnitt verhindern soll. Er sieht völlig vernünftig aus: [source,lua] ---- function onOverlayUpdate() overlayClear() local ship = spriteLoad(DIR .. "art/spaceship.png") spriteDraw(ship, 160, 100) return OVERLAY_UPDATED end ---- Starte das, und es funktioniert. Es zeichnet das Raumschiff. Nichts beschwert sich. Du hättest keinen Grund zu glauben, dass etwas falsch ist. `onOverlayUpdate` läuft etwa sechzigmal pro Sekunde. Das sind sechzig frische Kopien des Raumschiffs, die jede Sekunde in den Speicher geladen werden, dreitausendsechshundert pro Minute, und keine einzige davon wird je freigegeben. Das Spiel läuft zwei Minuten lang wunderbar, wird langsamer, und dann geht dem Rechner der Speicher aus, und es stirbt. Schlimmer noch: Es stirbt bei der Vorführung und nicht, während du getestet hast. Die Regel: *einmal laden, oft zeichnen*. Laden ist langsam und braucht Speicher. Zeichnen ist schnell und braucht keinen. Also kommt `spriteLoad` oben in deine Datei, außerhalb jeder Funktion, wo es einmal läuft, wenn das Skript startet. Das gilt nicht nur für Sprites. Jedes `somethingLoad` in der Engine funktioniert so -- Sounds in Lektion zehn, Schriftarten in Lektion neunzehn, Modelle in Lektion dreiundzwanzig. Wenn du merkst, dass du `Load` in eine Funktion tippst, die in jedem Frame läuft, halt an und schieb es hinaus. [#_how_big_is_it] === Wie groß ist es? Du weißt, dass das Raumschiff 32 mal 24 groß ist, weil dieses Buch es dir gesagt hat. Dein Programm weiß es nicht, und es sollte es auch nicht wissen müssen. Frag: [source,lua] ---- local shipWidth = spriteGetWidth(ship) local shipHeight = spriteGetHeight(ship) ---- Jetzt kannst du das Raumschiff auf dem Bildschirm halten, weil die Position ganz rechts, an der es sitzen darf, `screenWidth - shipWidth` ist, und es in die Mitte setzen, weil die Mitte `(screenWidth - shipWidth) / 2` ist. In keiner der beiden Zeilen steht eine Zahl, die sich ändern müsste, wenn jemand das Raumschiff ein bisschen größer neu zeichnet. Beide melden die Größe *so, wie das Sprite gerade gezeichnet würde*. Wenn du ein Sprite je mit `spriteScale` oder `spriteRotate` skalierst oder drehst, ändert sich die Antwort entsprechend. Meistens ist das genau, was du willst, und es wird dich genau einmal überraschen. [#_drawing_order] === Zeichenreihenfolge Sprites verschmelzen nicht. Späteres verdeckt Früheres, Pixel für Pixel, genau wie Papierausschnitte, die du an eine Wand klebst. Die Reihenfolge deiner `spriteDraw`-Aufrufe ist also die Reihenfolge von hinten nach vorne: [source,lua] ---- drawStars() drawRocks() drawShots() spriteDraw(shipSprite, shipX, shipY) drawHud() ---- Die Sterne sind am weitesten hinten, also kommen sie zuerst. Das Raumschiff ist vor allem anderen auf dem Spielfeld. Die Punktzahl liegt über allem. Dreh das um, und dein sorgfältig gezeichnetes Raumschiff versteckt sich das ganze Spiel über hinter dem Sternenfeld, und keine Fehlermeldung sagt dir das -- nur ein Raumschiff, das du nicht siehst. `overlayClear` kommt immer noch zuerst, vor allem anderen. Zeichnen ersetzt nie, es verdeckt nur, also fängt der Frame jedes Mal leer an. [#_frames] === Frames Öffne `art/walk.png` in irgendeinem Bildbetrachter. Es ist kein Bild von einer Person. Es ist ein Bild von vier Personen, nebeneinander: dieselbe Figur an vier Punkten eines Schritts, jede 24 Pixel breit, in einem Streifen von 96 Pixeln Breite. Ein Bild, das mehrere Frames einer Animation enthält, ist ein *Sprite-Sheet*, und so wird fast jede 2D-Animation gespeichert. Eine Datei, ein Laden, ein Handle, und das Spiel sucht sich aus, welchen Ausschnitt es zeichnet. Leg neben der ersten eine zweite Datei an, `walk.singe`: [source,lua] ---- dofile("Singe/Framework.singe") local walk = spriteLoadFrames(4, DIR .. "art/walk.png") local walkFrame = 1 local walkTick = 0 local walkX = 0 function onOverlayUpdate() overlayClear() walkTick = walkTick + 1 if walkTick >= 8 then walkTick = 0 walkFrame = walkFrame + 1 if walkFrame > 4 then walkFrame = 1 end end walkX = walkX + 1 if walkX > overlayGetWidth() then walkX = -spriteFrameWidth(walk) end spriteDrawFrame(walk, walkX, 100, walkFrame) return OVERLAY_UPDATED end function onShutdown() spriteUnload(walk) end ---- Starte es mit `Singe rocks/walk.singe`. Eine kleine Figur läuft gleichmäßig über den Bildschirm, verschwindet am rechten Rand und kommt von links wieder herein. `spriteLoadFrames` ist `spriteLoad` mit einer zusätzlichen Angabe: wie viele Frames im Streifen sind. *Die Anzahl kommt zuerst*, vor dem Dateinamen, und das ist das Gegenteil von dem, was die meisten raten. Es teilt die Breite des Bildes durch die Anzahl und merkt sich die Ausschnitte. Vier Frames in einem Streifen von 96 Pixeln heißt vier Frames von 24 Pixeln Breite. Gib die falsche Anzahl an -- sagen wir 3 -- und du bekommst drei Ausschnitte von 32 Pixeln, die jeweils Teile von zwei Figuren zeigen, und so findest du auf denkwürdig alberne Weise heraus, dass du die falsche Zahl getippt hast. `spriteDrawFrame` zeichnet einen Ausschnitt. Es bekommt das Handle, wohin er soll, und welchen Frame, *in dieser Reihenfolge*, und die Frames sind *ab 1* nummeriert: Frame 1 ist die Figur ganz links und Frame 4 die ganz rechts. Pass bei dieser 1 auf. Anderswo in der Sprite-Familie -- `spriteSetFrame` und `spriteGetFrame` -- sind die Frames ab 0 nummeriert. Das ist ein Zufall der Geschichte, beide stehen in der Referenz, und dieses Buch benutzt `spriteDrawFrame` und zählt ab 1. `spriteFrameWidth` meldet die Breite eines Frames, hier 24, und nicht die 96 des ganzen Streifens. `spriteFrameHeight` ist sein Partner. Wenn du wissen willst, wie breit ein Ausschnitt eines Sheets ist, sind das die beiden, die du fragst, und nicht `spriteGetWidth`. Die Frame-Nummer ist eine Zahl in einer Variablen wie jede andere, also ist das Laufen Rechnerei: [source,lua] ---- walkTick = walkTick + 1 if walkTick >= 8 then walkTick = 0 walkFrame = walkFrame + 1 if walkFrame > 4 then walkFrame = 1 end end ---- Zähl die Frames, die vorbeiziehen. Bei jedem achten geh zum nächsten Bild weiter, und nach dem vierten Bild geh zurück zum ersten. Sechzig Frames pro Sekunde geteilt durch acht sind zwischen sieben und acht Schritte pro Sekunde, und für einen Zyklus aus vier Frames ist das ein flotter, aber glaubhafter Gang. Ändere die 8, und du änderst die Geschwindigkeit: größer ist langsamer. Du fragst dich vielleicht, warum du überhaupt zählst, wenn die Engine `spritePlay` hat, um eine Animation für dich abzuspielen. Das hat sie, und für ein animiertes GIF ist das die richtige Antwort, denn ein GIF trägt die Zeitsteuerung für jeden seiner Frames in der Datei. Ein schlichter Streifen wie `walk.png` hat keine Zeitsteuerung, also spielt die Engine ihn so schnell ab, wie sie darf -- hundert Frames pro Sekunde, fünfundzwanzig vollständige Schrittzyklen pro Sekunde, ein verwischter Fleck. Wenn die Frames aus einem Streifen kommen, zähl selbst. [#_the_game_gets_its_artwork] === Das Spiel bekommt seine Grafiken Jetzt setz es zusammen. Im Spiel aus Lektion sieben hast du einen Kasten am unteren Rand des Bildschirms gesteuert, und andere Kästen sind darauf gefallen. Jeder Kasten wird zu einem Bild, und wenn du schon dabei bist, bekommt das Raumschiff etwas zum Schießen. Die Änderungen hängen alle zusammen, also kommt hier das ganze Skript. Es ist länger als alles, was du bisher geschrieben hast, und es steckt nichts darin, was du nicht schon kennst. [source,lua] ---- dofile("Singe/Framework.singe") local SHIP_SPEED = 3 local SHOT_SPEED = 6 local ROCK_COUNT = 6 local STAR_COUNT = 40 local START_LIVES = 3 local HIT_SCORE = 10 local shipSprite = spriteLoad(DIR .. "art/spaceship.png") local rockSprite = spriteLoad(DIR .. "art/asteroid.png") local shotSprite = spriteLoad(DIR .. "art/bullet.png") local starSprite = spriteLoad(DIR .. "art/star.png") local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local shipWidth = spriteGetWidth(shipSprite) local shipHeight = spriteGetHeight(shipSprite) local rockWidth = spriteGetWidth(rockSprite) local rockHeight = spriteGetHeight(rockSprite) local shotWidth = spriteGetWidth(shotSprite) local shotHeight = spriteGetHeight(shotSprite) local shipX = (screenWidth - shipWidth) / 2 local shipY = screenHeight - shipHeight - 4 local goLeft = false local goRight = false local rocks = {} local shots = {} local stars = {} local score = 0 local lives = START_LIVES local over = false function drawHud() overlayPrint(1, 1, "SCORE " .. score) overlayPrint(50, 1, "LIVES " .. lives) if over then overlayPrint(25, 8, "GAME OVER") overlayPrint(17, 10, "PRESS 1 TO PLAY AGAIN") end end function drawRocks() for _, rock in ipairs(rocks) do spriteDraw(rockSprite, rock.x, rock.y) end end function drawShots() for _, shot in ipairs(shots) do spriteDraw(shotSprite, shot.x, shot.y) end end function drawStars() for _, star in ipairs(stars) do spriteDraw(starSprite, star.x, star.y) end end function newRock(rock) rock.x = math.random(0, screenWidth - rockWidth) rock.y = -rockHeight - math.random(0, 160) rock.speed = math.random(8, 20) / 10 end function onInputPressed(what) if over then if what == SWITCH_START1 then startGame() end return end if what == SWITCH_LEFT then goLeft = true elseif what == SWITCH_RIGHT then goRight = true elseif what == SWITCH_BUTTON1 then shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY } end end function onInputReleased(what) if what == SWITCH_LEFT then goLeft = false elseif what == SWITCH_RIGHT then goRight = false end end function onOverlayUpdate() overlayClear() updateStars() if not over then updateShip() updateShots() updateRocks() end drawStars() drawRocks() drawShots() spriteDraw(shipSprite, shipX, shipY) drawHud() return OVERLAY_UPDATED end function onShutdown() spriteUnload(shipSprite) spriteUnload(rockSprite) spriteUnload(shotSprite) spriteUnload(starSprite) end function overlapping(ax, ay, aw, ah, bx, by, bw, bh) if ax + aw <= bx then return false end if bx + bw <= ax then return false end if ay + ah <= by then return false end if by + bh <= ay then return false end return true end function startGame() shipX = (screenWidth - shipWidth) / 2 score = 0 lives = START_LIVES over = false shots = {} for i = 1, ROCK_COUNT do rocks[i] = {} newRock(rocks[i]) end end function updateRocks() for _, rock in ipairs(rocks) do rock.y = rock.y + rock.speed if rock.y > screenHeight then newRock(rock) end if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then newRock(rock) lives = lives - 1 if lives <= 0 then over = true end end end end function updateShip() if goLeft then shipX = shipX - SHIP_SPEED end if goRight then shipX = shipX + SHIP_SPEED end if shipX < 0 then shipX = 0 end if shipX > screenWidth - shipWidth then shipX = screenWidth - shipWidth end end function updateShots() for i = #shots, 1, -1 do local shot = shots[i] local gone = false shot.y = shot.y - SHOT_SPEED if shot.y + shotHeight < 0 then gone = true end for _, rock in ipairs(rocks) do if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then newRock(rock) score = score + HIT_SCORE gone = true end end if gone then table.remove(shots, i) end end end function updateStars() for _, star in ipairs(stars) do star.y = star.y + star.speed if star.y > screenHeight then star.y = 0 star.x = math.random(0, screenWidth - 1) end end end for i = 1, STAR_COUNT do stars[i] = { x = math.random(0, screenWidth - 1), y = math.random(0, screenHeight - 1), speed = math.random(1, 3) / 4 } end startGame() ---- Die Pfeiltasten nach links und rechts steuern. Die Leertaste feuert. Wenn die Felsen dir das letzte Leben genommen haben, startet `1` ein neues Spiel. Auf ein paar Dinge darin lohnt es sich zu zeigen. Die vier Aufrufe von `spriteLoad` und die acht Messungen stehen oben, außerhalb jeder Funktion, also passieren sie einmal. Alles danach ist mit `screenWidth`, `shipWidth` und ihren Freunden geschrieben, und im ganzen Spielcode steht keine einzige nackte Pixelzahl. Die Funktionen stehen in alphabetischer Reihenfolge, und diese Gewohnheit lohnt es sich jetzt anzunehmen. Lua ist das völlig egal -- sie ist für dich, in sechs Monaten, wenn du in einer Datei mit vierzig Funktionen nach `updateShots` suchst. Die letzten beiden Dinge in der Datei sind überhaupt keine Funktionen. Es sind schlichte Anweisungen am Ende des Skripts: das Sternenfeld bauen und dann `startGame` aufrufen, um die Felsen aufzustellen und die Punktzahl auf null zu setzen. Sie laufen einmal, der Reihe nach, wenn das Skript geladen wird, und sie müssen nach den Funktionen kommen, die sie benutzen. `overlapping` sind die vier Vergleiche, von denen Lektion sieben gesagt hat, dass du sie selbst schreiben könntest, statt `collideRects` aufzurufen, ausgeschrieben als Funktion mit denselben acht Argumenten. Zwei Rechtecke verfehlen sich, wenn eines ganz links, rechts, über oder unter dem anderen liegt; ist keins dieser vier wahr, überlappen sie sich. Lektion elf ersetzt das durch etwas, das die Engine bereitstellt, und erklärt, warum das Rechteck die falsche Form für ein Raumschiff ist. Ein letzter Hinweis für den Fall, dass du Singe-Code von anderen Leuten liest. Jeder Sprite-Aufruf in diesem Buch nimmt das Handle *zuerst*. Singe 2.10 hat es ans Ende gesetzt, und ein Spiel kann die Engine bitten, das weiter so zu machen, also steht in Code, den du im Netz findest, vielleicht `spriteDraw(x, y, ship)`. Kopier das nicht. Das Handle zuerst ist die aktuelle Reihenfolge und die, die die Referenz beschreibt. [#_try_it_9] === Probier es aus . *Zentrier das Raumschiff auf seiner Position.* Gib dem `spriteDraw` des Raumschiffs `true` als viertes Argument. Sieh dir an, wohin das Raumschiff springt, und finde aus der Strecke, um die es sich bewegt hat, heraus, was das vierte Argument wirklich getan hat. . *Mehr Felsen.* Ändere `ROCK_COUNT` in 20. Dann in 200. Irgendwo zwischen diesen beiden hört das Spiel auf, spielbar zu sein, und irgendwo weit darüber hinaus fängt das Spiel an, langsamer zu werden. Finde beide Stellen. . *Lass das Raumschiff wachsen.* Füg `spriteScale(shipSprite, 2)` ganz unten in der Datei ein, in der Zeile über `startGame()`. Starte es und steuere gegen den rechten Rand: Das Raumschiff ist doppelt so groß, hält aber immer noch dort an, wo das kleine angehalten hat, und es hängt über den rechten Rand und über den unteren hinaus. Finde heraus, warum, dann schieb diese eine Zeile nach oben über die Aufrufe von `spriteGetWidth` und sieh zu, wie es richtig wird. Der Eintrag zu `spriteScale` in der Referenz erklärt, was das Skalieren mit den Maßen macht. . *Bau den Läufer ein.* Lade `walk.png` im Spiel und lass die laufende Figur als Kulisse über den oberen Rand des Bildschirms laufen. Du brauchst das Zählen der Frames aus dem Lauf-Skript, und du musst entscheiden, wohin in `onOverlayUpdate` das Zeichnen kommt -- vor die Sterne, hinter die Felsen. . *Gib etwas frei, das du noch benutzt.* Füg `spriteUnload(starSprite)` ganz unten in der Datei ein, nach `startGame()`, und starte es. Lies, was du bekommst. [#_break_it_on_purpose_8] === Mach es absichtlich kaputt Großbuchstaben zählen, und bei Grafiken beißt das am heftigsten. Ändere das Laden des Raumschiffs so, dass es ein großes S benutzt: [source,lua] ---- local shipSprite = spriteLoad(DIR .. "art/Spaceship.png") ---- Das Spiel weigert sich zu starten: ---- 10:spriteLoad: Couldn't open rocks/art/Spaceship.png: No such file or directory ---- Lies es genauso wie den Fehler in Lektion eins, aber beachte, dass die Form ein bisschen anders ist. `10` ist die Zeile. `spriteLoad` ist die Engine-Funktion, die aufgegeben hat, und kein Dateiname -- wenn die Beschwerde von Singe kommt statt von Lua, bekommst du das. Dann der Grund, und der ist hier in den eigenen Worten deines Betriebssystems formuliert und wird von Rechner zu Rechner leicht anders aussehen. `No such file or directory` heißt, dass der Computer genau ist, nicht unfreundlich. Es gibt wirklich keine Datei namens `Spaceship.png`; es gibt eine namens `spaceship.png`, und für einen Rechner mit Linux sind das zwei verschiedene Namen. Unter Windows und auf den meisten Macs hätte es funktioniert, und das ist schlimmer, denn es heißt, dass der Fehler auf den Rechner von jemand anderem wandert und dort zum ersten Mal auftaucht. Die andere Hälfte dieses Fehlers, der du begegnen wirst, ist der Ordner. Wenn du das Grafik-Kit an die falsche Stelle gelegt oder Singe aus dem falschen Verzeichnis gestartet hast, ist die Meldung genau dieselbe und die Lösung nicht. Der Pfad in der Meldung hilft: Er ist der ganze Name, den Singe versucht hat, gezählt ab dem Ordner, in dem du es gestartet hast, und das `rocks/` vorne ist der Teil, den `DIR` hinzugefügt hat. Prüf drei Dinge, in dieser Reihenfolge: Heißt die Datei genau so, wie du getippt hast, liegt sie im Ordner `art`, und liegt dieser Ordner `art` neben deinem Skript? [#_what_you_learned_9] === Was du gelernt hast * Ein Bild, das aufs Overlay gezeichnet wird, ist ein Sprite und wird mit `spriteLoad` geladen. * `spriteLoad` gibt ein Handle zurück: eine Zahl, die für die Engine „dieses Bild“ bedeutet und die du in einer Variablen aufbewahrst. * `DIR` ist der Ordner, in dem dein Skript liegt, und jede Datei, die dein Spiel lädt, sollte damit benannt werden. * `spriteDraw(handle, x, y)` zeichnet es, ab seiner linken oberen Ecke, und nur in `onOverlayUpdate`. Ein viertes Argument `true` meint stattdessen die Mitte. * Lade einmal, oben in der Datei. Laden in `onOverlayUpdate` frisst Speicher, bis das Spiel stirbt. * `spriteUnload` gibt ein Bild zurück, meistens aus `onShutdown`, und danach ist das Handle tot. * PNG kann Transparenz, und darum sind die Ecken des Raumschiffs keine schwarzen Kästen. * `spriteGetWidth` und `spriteGetHeight` messen ein Sprite so, wie es jetzt gezeichnet würde, also musst du seine Größe nie in dein Programm tippen. * Sprites verdecken einander. Die Reihenfolge, in der du zeichnest, geht von hinten nach vorne. * `spriteLoadFrames(count, file)` zerteilt einen Streifen in Frames, und `spriteDrawFrame(handle, x, y, frame)` zeichnet einen davon, gezählt ab 1. [#_next_time_9] === Beim nächsten Mal Das Spiel sieht richtig aus und klingt nach überhaupt nichts. In Lektion zehn bekommen die Schüsse ein Geräusch, die Felsen eine Explosion, und unter dem Ganzen spielt etwas -- und du lernst deinen fünften Callback kennen, den, mit dem die Engine dir sagt, dass ein Sound zu Ende ist.