807 lines
29 KiB
Text
807 lines
29 KiB
Text
[#_lesson_9_pictures]
|
||
== Leçon 9 : Des images
|
||
|
||
image::learn/09-pictures.png[La leçon terminée, 480]
|
||
|
||
Tout ce que vous avez dessiné jusqu'ici, vous l'avez dessiné avec des boîtes,
|
||
des lignes et le texte tout simple du moteur. C'était voulu : huit leçons sans
|
||
graphismes, c'étaient huit leçons où rien ne pouvait mal tourner à part votre
|
||
programme. Maintenant, les boîtes deviennent un vaisseau et des rochers.
|
||
|
||
Une image que le moteur dessine sur l'overlay s'appelle un *sprite*. Cette
|
||
leçon les charge, les dessine, les mesure, les met dans le bon ordre, et fait
|
||
défiler une image qui contient plusieurs images d'une animation. À la fin, le
|
||
jeu de la leçon sept ressemble à un jeu.
|
||
|
||
[#_the_art_kit]
|
||
=== Le kit graphique
|
||
|
||
Vous n'avez rien à dessiner. Ce livre est livré avec un petit dossier de
|
||
graphismes, et tout ce qui se trouve dans cette leçon et la suivante s'en sert.
|
||
|
||
Trouvez le dossier `art` fourni avec le livre et copiez le dossier entier dans
|
||
le dossier de cette leçon, à côté de votre script. Vous devriez obtenir ceci :
|
||
|
||
----
|
||
singe/ <- votre dossier de travail
|
||
Singe/
|
||
data/
|
||
rocks/
|
||
rocks.singe
|
||
art/
|
||
spaceship.png
|
||
asteroid.png
|
||
bullet.png
|
||
star.png
|
||
walk.png
|
||
shoot.wav
|
||
boom.wav
|
||
----
|
||
|
||
Vos graphismes vont dans le dossier propre à votre jeu, et le dossier `Singe` du
|
||
moteur reste où il est, dehors, dans le dossier de travail. Ça compte dans un
|
||
instant, parce que c'est ce qui décide de la manière dont vous nommez un fichier
|
||
quand vous le chargez.
|
||
|
||
Le dossier `Singe` est celui que le moteur a extrait pour vous lors de votre
|
||
tout premier lancement, à la leçon un. Ça vaut la peine de savoir ce qu'il
|
||
contient, parce que ces fichiers sont à vous aussi, et que tous les lecteurs de
|
||
ce livre les ont :
|
||
|
||
* `Singe/click.wav` -- un clic bref, dont le menu du moteur se sert pour son
|
||
test du son.
|
||
* `Singe/FreeSansBold.ttf` -- une police TrueType, pour la leçon dix-neuf.
|
||
* `Singe/menuIntro.flac` -- la musique que joue le menu du moteur. Vous vous en
|
||
servirez à la leçon dix.
|
||
* `Singe/menuBackground.mkv` -- une vidéo, pour la leçon quatorze.
|
||
* `Singe/DragonModel.glb` et `Singe/SingeText.glb` -- deux modèles 3D, pour la
|
||
leçon vingt-trois.
|
||
* `Singe/missing.png` -- l'image que dessine le moteur quand un jeu en demande
|
||
une qu'il ne trouve pas.
|
||
|
||
Les images du kit sont petites exprès. Le vaisseau fait 32 pixels de large et
|
||
24 de haut, un rocher fait 24 sur 24, un tir 4 sur 10, et une étoile 8 sur 8.
|
||
Ça a l'air minuscule jusqu'à ce que vous vous rappeliez la taille réelle de
|
||
l'écran, et c'est la prochaine chose à mettre au clair.
|
||
|
||
[#_how_big_the_screen_is]
|
||
=== La taille de l'écran
|
||
|
||
Sans vidéo à lire, Singe donne à votre jeu un canevas de 720 pixels de large et
|
||
480 de haut, et la surface sur laquelle vous dessinez -- l'*overlay* -- en fait
|
||
la moitié dans chaque direction : *360 sur 240*. Singe l'étire pour remplir la
|
||
fenêtre à votre place, si bien qu'un vaisseau de 32 pixels fait environ un
|
||
dixième de la largeur de l'écran, ce qui est à peu près l'allure d'un vaisseau
|
||
dans un jeu d'arcade.
|
||
|
||
N'écrivez jamais 360 et 240 dans votre programme. Demandez :
|
||
|
||
[source,lua]
|
||
----
|
||
local screenWidth = overlayGetWidth()
|
||
local screenHeight = overlayGetHeight()
|
||
----
|
||
|
||
Ces deux-là donnent la taille de l'overlay, et un jeu qui demande est un jeu
|
||
qui marche encore quand quelqu'un le lance à une autre taille.
|
||
|
||
Il y a une chose qui ne compte pas dans ces pixels : `overlayPrint`, de la leçon
|
||
un, compte en cellules de caractère. Chaque cellule fait 6 pixels de large et 13
|
||
de haut, donc un overlay de 360 sur 240 fait 60 colonnes sur 18 lignes de texte.
|
||
Les sprites sont en pixels, `overlayPrint` est en cellules, et confondre les
|
||
deux vaut bien un après-midi de perplexité si vous le laissez faire.
|
||
|
||
[#_one_picture_on_screen]
|
||
=== Une image à l'écran
|
||
|
||
Commencez un nouveau fichier à côté de votre dossier `art` et tapez ceci.
|
||
|
||
[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
|
||
----
|
||
|
||
Lancez-le. Un petit vaisseau bleu se tient près du milieu d'un écran noir, la
|
||
pointe vers le haut.
|
||
|
||
[#_what_just_happened_5]
|
||
=== Ce qui vient de se passer
|
||
|
||
Quatre choses nouvelles, une par une.
|
||
|
||
[source,lua]
|
||
----
|
||
local ship = spriteLoad(DIR .. "art/spaceship.png")
|
||
----
|
||
|
||
`spriteLoad` lit un fichier image sur le disque, le décode, et garde l'image en
|
||
mémoire. Ce qu'il vous rend n'est pas l'image. C'est un *handle* : un simple
|
||
nombre, un ticket d'entrée, la manière qu'a le moteur de dire « celle-là ». Vous
|
||
redonnez ce nombre au moteur chaque fois que vous voulez faire quelque chose
|
||
avec l'image, et vous n'avez jamais besoin de savoir ce qu'est ce nombre, ni de
|
||
vous en soucier.
|
||
|
||
Affichez-le si ça vous chante -- `debugPrint(ship)`, de la leçon huit, vous
|
||
montrera un petit nombre -- mais vous n'avez pas à raisonner sur le nombre
|
||
lui-même, et rien dans votre programme ne doit jamais dépendre de ce qu'il se
|
||
trouve être. La seule règle, c'est de le garder quelque part où vous pourrez le
|
||
retrouver, et c'est exactement à ça que sert une variable.
|
||
|
||
`DIR` est nouveau aussi, et il règle un problème que crée la disposition dont
|
||
vous vous servez depuis la leçon un. Singe cherche les fichiers par rapport au
|
||
dossier où vous vous trouviez quand vous l'avez lancé, *pas* par rapport à votre
|
||
script. Vous le lancez depuis votre dossier de travail, donc un simple
|
||
`art/spaceship.png` envoie le moteur chercher `singe/art/spaceship.png`, dehors,
|
||
à côté du dossier `Singe`. Vos graphismes ne sont pas là. Ils sont un niveau
|
||
plus bas, dans `rocks/art/spaceship.png`, avec votre script.
|
||
|
||
`DIR` comble cet écart. C'est un morceau de texte qui contient le dossier où se
|
||
trouve votre script, avec la barre oblique déjà au bout, donc
|
||
`DIR .. "art/spaceship.png"` -- deux morceaux de texte joints avec `..`, ce que
|
||
vous faites depuis la première partie -- nomme l'image correctement quelle que
|
||
soit la manière dont le jeu a été lancé, et continue de la nommer correctement
|
||
quand vous déplacez le dossier entier ailleurs.
|
||
|
||
`DIR` est l'un des noms que définit `Singe/Framework.singe`, et c'est à ça que
|
||
sert la ligne `dofile` en haut de votre script. Vous avez rencontré cette ligne,
|
||
et ce qu'elle contient, à la leçon sept, et c'est le moment où elle gagne sa
|
||
place : sans elle, pas de `DIR`, et sans `DIR`, le moteur ne trouve pas votre
|
||
vaisseau.
|
||
|
||
Utilisez `DIR` pour chaque fichier que charge votre jeu, d'ici à la fin du
|
||
livre.
|
||
|
||
[source,lua]
|
||
----
|
||
spriteDraw(ship, 160, 100)
|
||
----
|
||
|
||
Ça dessine le sprite qui a ce handle, à cette position. Comme tous les appels de
|
||
dessin, ça ne marche qu'à l'intérieur de `onOverlayUpdate`.
|
||
|
||
La position est le *coin supérieur gauche* de l'image, pas son milieu. Demandez
|
||
`(160, 100)` et le pixel le plus en haut à gauche du vaisseau atterrit là, et le
|
||
reste pend vers le bas et vers la droite. Ça piège tout le monde une fois, en
|
||
général en essayant de centrer quelque chose qui sort trop bas et trop à
|
||
droite, de la moitié de sa propre taille.
|
||
|
||
Si vous préférez donner le milieu, dites-le :
|
||
|
||
[source,lua]
|
||
----
|
||
spriteDraw(ship, 160, 100, true)
|
||
----
|
||
|
||
Un quatrième argument `true` veut dire « prends ce point comme centre ». Les
|
||
deux formes sont dans l'entrée de `spriteDraw` dans la Référence, avec deux
|
||
autres qui étirent l'image dans un rectangle.
|
||
|
||
[source,lua]
|
||
----
|
||
function onShutdown()
|
||
spriteUnload(ship)
|
||
end
|
||
----
|
||
|
||
`onShutdown` est un callback, comme `onOverlayUpdate` et les deux callbacks des
|
||
commandes de la leçon trois. Le moteur l'appelle une fois, quand votre jeu se
|
||
termine. C'est là que vous rendez ce que vous avez pris : `spriteUnload` libère
|
||
l'image et jette le handle.
|
||
|
||
À strictement parler, vous pourriez vous en passer. Le moteur libère tout ce que
|
||
votre script a chargé quand le jeu se termine, donc un `spriteUnload` oublié
|
||
n'est pas une fuite. Écrivez-le quand même. Ça coûte une ligne, ça dit ce que
|
||
possède votre programme, et dans une minute vous verrez le cas où oublier de
|
||
décharger compte vraiment.
|
||
|
||
Un avertissement à propos de ce handle : une fois que vous avez déchargé un
|
||
sprite, le nombre est mort. Dessiner avec ne dessine pas rien, ça termine votre
|
||
jeu sur une erreur.
|
||
|
||
[#_transparency_and_why_it_is_a_png]
|
||
=== La transparence, et pourquoi c'est un PNG
|
||
|
||
Le vaisseau est un triangle avec deux ailerons. Le fichier où il vit est un
|
||
rectangle, 32 sur 24, parce que tout fichier image est un rectangle. Alors,
|
||
qu'est-il arrivé aux coins ?
|
||
|
||
Ils sont *transparents*. Un PNG peut dire, pour chaque pixel, à quel point il
|
||
est opaque, et les images du kit disent « pas là du tout » pour chaque pixel en
|
||
dehors de la forme. Singe le respecte, donc le fond noir se voit à travers les
|
||
coins, et le vaisseau a un contour au lieu d'une boîte autour de lui.
|
||
|
||
C'est la raison pour laquelle le kit est en PNG et pas en JPEG. Un JPEG ne peut
|
||
pas du tout stocker de transparence -- il vous donnerait le vaisseau dans une
|
||
boîte grise -- et il bave aussi sur les bords nets, ce dont de petits
|
||
graphismes ont le moins besoin. Pour les graphismes d'un jeu, PNG. Pour une
|
||
photo qui remplit tout l'écran et n'a aucune partie transparente, le JPEG
|
||
convient et pèse bien moins lourd. L'entrée de `spriteLoad` dans la Référence
|
||
liste tous les formats que Singe sait lire, et il y en a beaucoup.
|
||
|
||
Essayez vous-même : changez le noir en autre chose en dessinant une boîte
|
||
derrière le vaisseau avant de dessiner le vaisseau. Les coins montreront ce
|
||
qu'il y a derrière eux, et c'est tout l'intérêt.
|
||
|
||
[#_do_not_load_while_you_draw]
|
||
=== Ne chargez pas pendant que vous dessinez
|
||
|
||
Voici l'erreur que cette section existe pour empêcher. Elle a l'air
|
||
parfaitement raisonnable :
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
local ship = spriteLoad(DIR .. "art/spaceship.png")
|
||
spriteDraw(ship, 160, 100)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Lancez ça et ça marche. Ça dessine le vaisseau. Rien ne se plaint. Vous
|
||
n'auriez aucune raison de penser que quelque chose cloche.
|
||
|
||
`onOverlayUpdate` tourne environ soixante fois par seconde. Ça fait soixante
|
||
nouvelles copies du vaisseau chargées en mémoire chaque seconde, trois mille six
|
||
cents par minute, et pas une seule jamais déchargée. Le jeu tourne
|
||
magnifiquement pendant deux minutes, ralentit, puis la machine manque de
|
||
mémoire et il meurt. Pire, il meurt pendant la démo et pas pendant que vous
|
||
testiez.
|
||
|
||
La règle : *chargez une fois, dessinez souvent*. Charger est lent et prend de la
|
||
mémoire. Dessiner est rapide et n'en prend pas. Donc `spriteLoad` va en haut de
|
||
votre fichier, en dehors de toute fonction, où il s'exécute une seule fois, au
|
||
démarrage du script.
|
||
|
||
Ça ne concerne pas que les sprites. Chaque `somethingLoad` du moteur fonctionne
|
||
ainsi -- les sons à la leçon dix, les polices à la leçon dix-neuf, les modèles à
|
||
la leçon vingt-trois. Si vous vous surprenez à taper `Load` dans une fonction
|
||
qui s'exécute à chaque image, arrêtez-vous et sortez-le de là.
|
||
|
||
[#_how_big_is_it]
|
||
=== Quelle taille fait-il ?
|
||
|
||
Vous savez que le vaisseau fait 32 sur 24 parce que ce livre vous l'a dit.
|
||
Votre programme, lui, ne le sait pas, et il ne devrait pas avoir à le savoir.
|
||
Demandez :
|
||
|
||
[source,lua]
|
||
----
|
||
local shipWidth = spriteGetWidth(ship)
|
||
local shipHeight = spriteGetHeight(ship)
|
||
----
|
||
|
||
Vous pouvez maintenant garder le vaisseau à l'écran, parce que la position la
|
||
plus à droite où il peut se tenir est `screenWidth - shipWidth`, et le mettre
|
||
au milieu, parce que le milieu est `(screenWidth - shipWidth) / 2`. Aucune de
|
||
ces lignes ne contient de nombre qui devrait changer si quelqu'un redessinait
|
||
le vaisseau un peu plus grand.
|
||
|
||
Ces deux fonctions donnent la taille *telle que le sprite serait dessiné en ce
|
||
moment*. Si un jour vous mettez un sprite à l'échelle ou le faites tourner, avec
|
||
`spriteScale` ou `spriteRotate`, la réponse change en conséquence. C'est en
|
||
général ce que vous voulez, et ça vous surprendra exactement une fois.
|
||
|
||
[#_drawing_order]
|
||
=== L'ordre de dessin
|
||
|
||
Les sprites ne se mélangent pas. Ce qui vient après couvre ce qui vient avant,
|
||
pixel par pixel, exactement comme des découpes de papier collées sur un mur.
|
||
L'ordre de vos appels à `spriteDraw` est donc l'ordre de l'arrière vers
|
||
l'avant :
|
||
|
||
[source,lua]
|
||
----
|
||
drawStars()
|
||
drawRocks()
|
||
drawShots()
|
||
spriteDraw(shipSprite, shipX, shipY)
|
||
drawHud()
|
||
----
|
||
|
||
Les étoiles sont tout au fond, donc elles sont posées en premier. Le vaisseau
|
||
est devant tout ce qui se trouve sur l'aire de jeu. Le score est par-dessus tout
|
||
ça. Faites-le à l'envers et votre vaisseau si soigneusement dessiné passe la
|
||
partie caché derrière le champ d'étoiles, et aucune erreur ne vous le dit --
|
||
juste un vaisseau que vous ne voyez pas.
|
||
|
||
`overlayClear` vient toujours en premier, avant tout ça. Dessiner ne remplace
|
||
jamais, ça ne fait que couvrir, donc l'image commence vide à chaque fois.
|
||
|
||
[#_frames]
|
||
=== Les images d'une animation
|
||
|
||
Ouvrez `art/walk.png` dans n'importe quelle visionneuse d'images. Ce n'est pas
|
||
l'image d'une personne. C'est l'image de quatre personnes, côte à côte : la même
|
||
silhouette à quatre moments d'une marche, chacune large de 24 pixels, dans une
|
||
bande de 96 de large.
|
||
|
||
Une seule image qui contient plusieurs images d'une animation, c'est une
|
||
*planche de sprites* (« sprite sheet »), et c'est ainsi que presque toute
|
||
l'animation 2D est stockée. Un fichier, un chargement, un handle, et le jeu
|
||
choisit quelle tranche dessiner.
|
||
|
||
Créez un second fichier, `walk.singe`, à côté du premier :
|
||
|
||
[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
|
||
----
|
||
|
||
Lancez-le avec `Singe rocks/walk.singe`. Une petite silhouette traverse
|
||
l'écran d'un pas régulier, disparaît par le bord droit, et revient par la
|
||
gauche.
|
||
|
||
`spriteLoadFrames`, c'est `spriteLoad` avec une chose de plus à dire : combien
|
||
d'images contient la bande. *Le nombre vient en premier*, avant le nom du
|
||
fichier, ce qui est le contraire de ce que devinent la plupart des gens. Il
|
||
divise la largeur de l'image par ce nombre et retient les tranches. Quatre
|
||
images dans une bande de 96 pixels, ça fait quatre images de 24 pixels de
|
||
large. Trompez-vous de nombre -- disons 3 -- et vous obtenez trois tranches de
|
||
32 pixels, qui montrent chacune des morceaux de deux silhouettes, ce qui est une
|
||
manière mémorablement ridicule de découvrir que vous avez tapé le mauvais
|
||
nombre.
|
||
|
||
`spriteDrawFrame` dessine une tranche. Il prend le handle, l'endroit où la
|
||
mettre, et quelle image, *dans cet ordre*, et les images sont numérotées *à
|
||
partir de 1* : l'image 1 est la silhouette la plus à gauche et l'image 4 la plus
|
||
à droite.
|
||
|
||
Méfiez-vous de ce 1. Ailleurs dans la famille des sprites -- `spriteSetFrame` et
|
||
`spriteGetFrame` -- les images sont numérotées à partir de 0. C'est un accident
|
||
de l'histoire, les deux sont dans la Référence, et ce livre utilise
|
||
`spriteDrawFrame` et compte à partir de 1.
|
||
|
||
`spriteFrameWidth` donne la largeur d'une image, 24 ici, plutôt que les 96 de la
|
||
bande entière. `spriteFrameHeight` est son partenaire. Quand vous voulez savoir
|
||
quelle largeur fait une tranche d'une planche, ce sont ces deux-là qu'il faut
|
||
interroger, et pas `spriteGetWidth`.
|
||
|
||
Le numéro d'image est un nombre dans une variable comme un autre, donc la
|
||
marche, c'est de l'arithmétique :
|
||
|
||
[source,lua]
|
||
----
|
||
walkTick = walkTick + 1
|
||
if walkTick >= 8 then
|
||
walkTick = 0
|
||
walkFrame = walkFrame + 1
|
||
if walkFrame > 4 then
|
||
walkFrame = 1
|
||
end
|
||
end
|
||
----
|
||
|
||
Comptez les images du jeu qui passent. Toutes les huit, passez à l'image
|
||
suivante de la bande, et après la quatrième, revenez à la première. Soixante
|
||
images par seconde divisées par huit, ça fait entre sept et huit pas par
|
||
seconde, ce qui, pour un cycle de quatre images, donne une marche vive mais
|
||
crédible. Changez le 8 et vous changez la vitesse : plus grand, c'est plus
|
||
lent.
|
||
|
||
Vous vous demandez peut-être pourquoi compter, alors que le moteur a
|
||
`spritePlay` pour jouer une animation à votre place. Il l'a, et pour un GIF
|
||
animé c'est la bonne réponse, parce qu'un GIF porte dans le fichier la durée de
|
||
chacune de ses images. Une simple bande comme `walk.png` ne porte aucune durée,
|
||
alors le moteur la joue aussi vite qu'il en a le droit -- cent images par
|
||
seconde, vingt-cinq cycles de marche complets chaque seconde, un flou. Quand les
|
||
images viennent d'une bande, faites le compte vous-même.
|
||
|
||
[#_the_game_gets_its_artwork]
|
||
=== Le jeu reçoit ses graphismes
|
||
|
||
Maintenant, assemblez le tout. Le jeu de la leçon sept dirigeait une boîte le
|
||
long du bas de l'écran et faisait tomber d'autres boîtes dessus. Chaque boîte
|
||
devient une image, et pendant que vous y êtes, le vaisseau reçoit de quoi
|
||
tirer.
|
||
|
||
Les changements forment un tout, alors voici le script complet. Il est plus long
|
||
que tout ce que vous avez écrit, et il ne contient rien que vous n'ayez déjà
|
||
rencontré.
|
||
|
||
[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()
|
||
----
|
||
|
||
Les flèches gauche et droite dirigent. Espace tire. Quand les rochers vous ont
|
||
pris votre dernière vie, `1` lance une nouvelle partie.
|
||
|
||
Quelques éléments méritent qu'on les montre du doigt.
|
||
|
||
Les quatre appels à `spriteLoad` et les huit mesures sont en haut, en dehors de
|
||
toute fonction, donc ils ont lieu une fois. Tout ce qui suit est écrit en
|
||
fonction de `screenWidth`, de `shipWidth` et de leurs amis, et il n'y a pas un
|
||
seul nombre de pixels brut dans tout le code du jeu.
|
||
|
||
Les fonctions sont dans l'ordre alphabétique, et c'est une habitude qui vaut la
|
||
peine d'être prise dès maintenant. Ça n'a pas la moindre importance pour Lua --
|
||
c'est pour vous, dans six mois, quand vous chercherez `updateShots` dans un
|
||
fichier qui contient quarante fonctions.
|
||
|
||
Les deux dernières choses du fichier ne sont pas du tout des fonctions. Ce sont
|
||
de simples instructions en bas du script : construire le champ d'étoiles, puis
|
||
appeler `startGame` pour mettre en place les rochers et remettre le score à
|
||
zéro. Elles s'exécutent une fois, dans l'ordre, pendant le chargement du
|
||
script, et elles doivent venir après les fonctions dont elles se servent.
|
||
|
||
`overlapping`, ce sont les quatre comparaisons dont la leçon sept disait que
|
||
vous pouviez les écrire vous-même au lieu d'appeler `collideRects`, écrites sous
|
||
forme de fonction avec les mêmes huit arguments. Deux rectangles se ratent si l'un est entièrement à gauche, à droite, au-dessus ou
|
||
au-dessous de l'autre ; si aucune de ces quatre conditions n'est vraie, ils se
|
||
chevauchent. La leçon onze la remplace par quelque chose que fournit le moteur,
|
||
et explique pourquoi le rectangle n'a pas la bonne forme pour un vaisseau.
|
||
|
||
Une dernière remarque pour le jour où vous lirez le code Singe d'autres gens.
|
||
Chaque appel de sprite de ce livre prend le handle *en premier*. Singe 2.10 le
|
||
mettait en dernier, et un jeu peut demander au moteur de continuer à faire
|
||
ainsi, donc le code que vous trouverez en ligne peut se lire
|
||
`spriteDraw(x, y, ship)`. Ne le copiez pas. Le handle en premier, c'est l'ordre
|
||
actuel, et celui que documente la Référence.
|
||
|
||
[#_try_it_9]
|
||
=== Essayez
|
||
|
||
. *Centrez le vaisseau sur sa position.* Ajoutez `true` comme quatrième argument
|
||
au `spriteDraw` du vaisseau. Regardez où le vaisseau saute, et déduisez de la
|
||
distance qu'il a parcourue ce qu'a vraiment fait le quatrième argument.
|
||
. *Plus de rochers.* Changez `ROCK_COUNT` en 20. Puis en 200. Quelque part entre
|
||
les deux, le jeu cesse d'être jouable, et quelque part bien au-delà, le jeu
|
||
commence à ralentir. Trouvez les deux.
|
||
. *Faites grandir le vaisseau.* Ajoutez `spriteScale(shipSprite, 2)` tout en bas
|
||
du fichier, sur la ligne au-dessus de `startGame()`. Lancez et foncez vers le
|
||
bord droit : le vaisseau est deux fois plus grand mais il s'arrête toujours là
|
||
où s'arrêtait le petit, et il dépasse du bord droit et du bas. Trouvez
|
||
pourquoi, puis remontez cette ligne au-dessus des appels à `spriteGetWidth` et
|
||
regardez tout rentrer dans l'ordre. L'entrée de `spriteScale` dans la
|
||
Référence explique ce que la mise à l'échelle fait aux mesures.
|
||
. *Ajoutez le marcheur.* Chargez `walk.png` dans le jeu, et dessinez la
|
||
silhouette qui marche en haut de l'écran, comme décor. Il vous faudra le
|
||
comptage des images du script de marche, et vous devrez décider à quel endroit
|
||
de `onOverlayUpdate` va le dessin -- devant les étoiles, derrière les rochers.
|
||
. *Déchargez quelque chose dont vous vous servez encore.* Ajoutez
|
||
`spriteUnload(starSprite)` en bas du fichier, après `startGame()`, et lancez.
|
||
Lisez ce que vous obtenez.
|
||
|
||
[#_break_it_on_purpose_8]
|
||
=== Cassez-le exprès
|
||
|
||
Les majuscules comptent, et c'est avec les graphismes qu'elles mordent le plus
|
||
fort. Changez le chargement du vaisseau pour qu'il utilise un S majuscule :
|
||
|
||
[source,lua]
|
||
----
|
||
local shipSprite = spriteLoad(DIR .. "art/Spaceship.png")
|
||
----
|
||
|
||
Le jeu refuse de démarrer :
|
||
|
||
----
|
||
10:spriteLoad: Couldn't open rocks/art/Spaceship.png: No such file or directory
|
||
----
|
||
|
||
Lisez-le de la même façon que l'erreur de la leçon un, mais remarquez que la
|
||
forme est un peu différente. `10`, c'est la ligne. `spriteLoad`, c'est la
|
||
fonction du moteur qui a abandonné, à la place d'un nom de fichier -- quand la
|
||
plainte vient de Singe plutôt que de Lua, c'est ce que vous obtenez. Puis la
|
||
raison, qui est ici formulée par votre propre système d'exploitation et variera
|
||
légèrement d'une machine à l'autre.
|
||
|
||
`No such file or directory`, c'est l'ordinateur qui est précis plutôt que
|
||
méchant. Il n'y a vraiment aucun fichier appelé `Spaceship.png` ; il y en a un
|
||
appelé `spaceship.png`, et pour une machine Linux, ce sont deux noms
|
||
différents. Sous Windows, et sur la plupart des Mac, ça aurait marché, ce qui
|
||
est pire, parce que ça veut dire que le bug voyage jusqu'à la machine de quelqu'un d'autre et apparaît
|
||
là-bas pour la première fois.
|
||
|
||
L'autre moitié de cette erreur, celle que vous rencontrerez aussi, c'est le
|
||
dossier. Si vous avez mis le kit graphique au mauvais endroit, ou lancé Singe
|
||
depuis le mauvais dossier, le message est exactement le même, et le remède ne
|
||
l'est pas. Le chemin du message vous aide : c'est le nom complet que Singe a
|
||
essayé, compté à partir du dossier où vous l'avez lancé, et le `rocks/` du
|
||
début est la partie qu'a ajoutée `DIR`. Vérifiez trois choses, dans cet
|
||
ordre : le fichier porte-t-il
|
||
exactement le nom que vous avez tapé, est-il dans le dossier `art`, et ce
|
||
dossier `art` est-il à côté de votre script ?
|
||
|
||
[#_what_you_learned_9]
|
||
=== Ce que vous avez appris
|
||
|
||
* Une image dessinée sur l'overlay est un sprite, chargé avec `spriteLoad`.
|
||
* `spriteLoad` vous rend un handle : un nombre qui veut dire « cette image-là »
|
||
pour le moteur, et que vous gardez dans une variable.
|
||
* `DIR` est le dossier où se trouve votre script, et chaque fichier que charge
|
||
votre jeu doit être nommé avec.
|
||
* `spriteDraw(handle, x, y)` le dessine, à partir de son coin supérieur gauche,
|
||
et seulement à l'intérieur de `onOverlayUpdate`. Un quatrième argument `true`
|
||
désigne plutôt le centre.
|
||
* Chargez une fois, en haut du fichier. Charger à l'intérieur de
|
||
`onOverlayUpdate` dévore la mémoire jusqu'à ce que le jeu meure.
|
||
* `spriteUnload` rend une image, en général depuis `onShutdown`, et le handle
|
||
est mort ensuite.
|
||
* Le PNG porte la transparence, et c'est pourquoi les coins du vaisseau ne sont
|
||
pas des boîtes noires.
|
||
* `spriteGetWidth` et `spriteGetHeight` mesurent un sprite tel qu'il serait
|
||
dessiné maintenant, pour que vous n'ayez jamais à taper sa taille dans votre
|
||
programme.
|
||
* Les sprites se couvrent les uns les autres. Vous dessinez dans l'ordre, de
|
||
l'arrière vers l'avant.
|
||
* `spriteLoadFrames(count, file)` découpe une bande en images, et
|
||
`spriteDrawFrame(handle, x, y, frame)` en dessine une, en comptant à partir
|
||
de 1.
|
||
|
||
[#_next_time_9]
|
||
=== La prochaine fois
|
||
|
||
Le jeu a fière allure et ne fait pas le moindre bruit. À la leçon dix, les tirs
|
||
reçoivent un son, les rochers une explosion, et quelque chose joue sous le
|
||
tout -- et vous rencontrez votre cinquième callback, celui dont le moteur se sert
|
||
pour vous dire qu'un son est terminé.
|