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