745 lines
29 KiB
Text
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.
|