singe/docs/de/lessons/09-pictures.adoc

745 lines
29 KiB
Text

[#_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.