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

807 lines
29 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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