[#_lesson_11_hitting_things] == Leçon 11 : Toucher des choses image::learn/11-hitting-things.png[La leçon terminée, 480] Votre jeu de la leçon dix marche. Les tirs font éclater les rochers, les rochers coûtent des vies, et les explosions tombent aux bons endroits. Regardez-le de près, pourtant, et vous verrez qu'il triche, dans les deux sens. Un rocher qui frôle le bout d'une aile coûte une vie. Un tir qui traverse le ciel vide à côté d'un rocher le fait éclater quand même. Dans un jeu, deux choses se touchent quand les nombres le disent. Personne ne regarde les images. Cette leçon met les nombres d'accord avec ce que voit le joueur : elle remplace le test que vous avez écrit à la main par celui du moteur, réduit les boîtes testées pour qu'elles collent aux choses qu'elles contiennent, et vous donne un interrupteur qui vous montre ces boîtes. L'image ci-dessus, c'est le jeu avec cet interrupteur allumé. [#_where_you_are] === Où vous en êtes Le jeu pose la question « ces deux-là se touchent-ils ? » à exactement deux endroits, et les deux se servent de `overlapping`, la fonction que vous avez tapée à la leçon neuf. L'un est dans `updateRocks`, là où un rocher rencontre le vaisseau : [source,lua] ---- if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- L'autre est dans `updateShots`, là où un tir rencontre un rocher : [source,lua] ---- if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- Tout ce que contient cette leçon change ces deux lignes et ajoute quelques petites fonctions pour les nourrir. Rien d'autre ne bouge dans le jeu. Le script terminé se trouve tout entier dans `learn/11-hitting-things.singe` si vous vous perdez en route. [#_read_it_once_more] === Relisez-la une fois de plus La leçon neuf a déjà fait cette partie : `overlapping`, c'est la collision de rectangles écrite à la main. Relisez-la une dernière fois avant de la jeter, parce qu'elle contient toute l'idée : [source,lua] ---- 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 ---- Il est difficile de répondre directement à la question « ces deux-là se touchent-ils ? », et facile d'y répondre à l'envers. Deux rectangles ne se touchent *pas* quand l'un d'eux est entièrement à gauche de l'autre, ou entièrement à droite, ou entièrement au-dessus, ou entièrement en dessous. Il n'y a pas de cinquième façon de se rater. Si aucune des quatre n'est vraie, ils se touchent. Chaque `if` est l'une des quatre façons de se rater. `ax + aw` est le bord droit du premier rectangle, donc `ax + aw <= bx` se lit « le premier finit avant que le second commence », et c'est ce que « entièrement à gauche » veut dire en nombres. La même forme encore trois fois, pour la droite, le dessus et le dessous, avec `y` et les hauteurs à la place de `x` et des largeurs, et la seule chose qui reste, c'est `true`. Ce sont les huit nombres dont tout test de collision a besoin : un coin et une taille pour chacune des deux choses. Gardez cette forme en tête, parce que vous êtes sur le point de la supprimer. [#_the_engine_already_has_it] === Le moteur l'a déjà Vous avez rencontré `collideRects` à la leçon sept, où il attrapait les blocs qui tombaient dans Dodge. La leçon neuf a écrit le même test à la main pour que vous puissiez voir ce qu'il y a dedans. Vous l'avez vu. Supprimez `overlapping`, et changez les deux lignes de *Où vous en êtes* pour qu'elles appellent le moteur à la place : [source,lua] ---- if collideRects(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- [source,lua] ---- if not gone and collideRects(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- Les mêmes huit nombres, dans le même ordre : le coin et la taille du premier rectangle, puis ceux du second. Lancez-le, et le jeu se joue exactement comme avant, et c'est bien le but. La seule différence est tout au bord. Votre `<=` disait que deux rectangles qui se rejoignent le long d'un bord ne se touchent pas. `collideRects` dit qu'ils se touchent, et un rectangle sans largeur ou sans hauteur ne touche rien du tout. C'est dans ces cas délicats que les versions écrites à la main se trompent sans bruit, et c'est une des raisons de se servir de celle du moteur. `collideRects` fait partie d'une famille de tests de collision, et cette famille est une partie de ce que ce livre appelle le *kit de jeu* : une poignée de fonctions simples pour les choses que presque tous les jeux écrivaient autrefois eux-mêmes. Tester si deux choses se touchent en est une. Garder un nombre pour la prochaine fois en est une autre, et c'est la leçon douze. Attendre un moment, faire glisser un nombre en douceur d'une valeur à une autre, et montrer combien de temps chaque image a pris, voilà le reste. La Référence donne à chaque partie sa propre section : Collisions, Sauvegarde, Minuteur, Tween et Statistiques. Les tests de collision sont les plus simples du lot : de simples questions, sans handle à garder et sans rien à préparer ni à libérer. Et aucun d'eux ne déplace quoi que ce soit. `collideRects` vous dit que le vaisseau et le rocher sont au même endroit ; c'est à vous de décider quoi en faire, et tout ce qui est intéressant dans un jeu se passe dans cette décision. [#_the_picture_is_not_the_hit_box] === L'image n'est pas la boîte de collision La leçon dix a promis d'expliquer pourquoi le rectangle autour d'un rocher rond n'a pas la bonne forme. Le rectangle autour du vaisseau n'a pas la bonne forme non plus, et pour la même raison. Dessinez sur papier le rectangle de 32 sur 24 du vaisseau et coloriez le vaisseau. C'est un triangle avec deux ailerons, donc il reste du ciel vide dans les deux coins du haut et entre les ailerons. Le rocher est rond, donc il reste du ciel vide dans les quatre coins de son carré. Un rocher qui frôle le coin supérieur du rectangle du vaisseau n'a pas touché le vaisseau. Il est passé à côté de l'aile, et un joueur qui perd une vie pour ça dira que le jeu a triché -- et il aura raison. Donc la boîte que vous testez n'est pas l'image que vous dessinez. Elle est plus petite, et elle se trouve à l'intérieur de l'image. Ajoutez trois nombres aux constantes en haut de votre fichier : [source,lua] ---- local SHIP_PAD_X = 6 local SHIP_PAD_Y = 5 local ROCK_PAD = 4 ---- La marge, c'est la distance entre chaque bord et l'endroit où commence la vraie chose. Six pixels de moins de chaque côté du vaisseau, et cinq en haut et en bas, laissent une boîte de 20 sur 14 au milieu d'une image de 32 sur 24, ce qui correspond à peu près à la coque. Quatre de moins de chaque côté du rocher laissent 16 sur 16, et c'est presque entièrement du rocher. Les deux boîtes sont un peu plus petites que les choses qu'elles représentent, et c'est voulu. Une boîte un peu trop petite donne au joueur des passages de justesse qui ressemblent à de l'adresse. Une boîte un peu trop grande lui donne des coups qui ressemblent à de la triche. Les joueurs pardonnent le premier cas, jamais le second. Plutôt que d'écrire ce calcul à chaque test, écrivez-le une fois pour chaque sorte de chose : [source,lua] ---- function rockBox(rock) return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2 end function shipBox() return shipX + SHIP_PAD_X, shipY + SHIP_PAD_Y, shipWidth - SHIP_PAD_X * 2, shipHeight - SHIP_PAD_Y * 2 end ---- Les tailles viennent de `rockWidth`, de `shipWidth` et de leurs amis, les mesures que la leçon neuf a prises sur les images, donc il n'y a toujours pas un seul nombre de pixels brut dans le code du jeu. Redessinez le vaisseau plus grand et la boîte grandit avec lui. Chacune de ces fonctions renvoie quatre nombres à la fois, séparés par des virgules. Une fonction peut renvoyer autant de valeurs qu'elle veut, et la façon de les attraper est une liste de noms à gauche du `=` : [source,lua] ---- function shipHitsRock(rock) local sx, sy, sw, sh = shipBox() local rx, ry, rw, rh = rockBox(rock) return collideRects(sx, sy, sw, sh, rx, ry, rw, rh) end ---- Maintenant, le test de `updateRocks` se lit comme la question qu'il pose : [source,lua] ---- if shipHitsRock(rock) then ---- Le calcul de la marge se trouve à exactement deux endroits, et les nombres qu'il utilise sont en haut du fichier : vous pouvez donc régler l'équité du jeu en changeant un nombre plutôt qu'en fouillant dans vos tests. Rangez les nouvelles fonctions à leur place dans l'ordre alphabétique, comme l'a demandé la leçon neuf : `rockBox` après `onSoundCompleted`, puis `shipBox` et `shipHitsRock`. [#_firing_back] === Riposter Voilà pour le côté du vaisseau. L'autre collision du jeu, c'est celle avec laquelle vous ripostez : chaque tir contre chaque rocher, dans `updateShots`. Elle a le même problème que le vaisseau, en plus petit. Un tir est un rectangle de 4 sur 10 testé contre le carré entier de 24 sur 24 du rocher, donc un tir qui ne fait que traverser le coin vide à côté d'un rocher le fait éclater quand même. Vous pourriez donner au tir sa propre boîte avec une marge. Regardez d'abord les nombres, pourtant. Retirez une marge à un tir de quatre pixels de large et il ne reste presque rien, et tester un rectangle entier pour savoir si un filet de lumière a touché quelque chose, c'est plus de machinerie que la question n'en mérite. [#_a_shot_is_almost_a_point] === Un tir est presque un point Testez plutôt la pointe. `collidePointRect` demande si un point tombe à l'intérieur d'un rectangle -- deux nombres pour le point, puis le coin et la taille du rectangle : [source,lua] ---- function shotHitsRock(shot, rock) local rx, ry, rw, rh = rockBox(rock) return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh) end ---- `shot.y` est le haut du tir et `shot.x + shotWidth / 2` est le milieu de sa largeur, donc le point testé est le milieu du nez du tir. C'est là qu'un joueur pense que se trouve un tir. Le côté rocher de la question se sert du même `rockBox` que le vaisseau, donc un rocher a la même taille, quoi qui le touche. Elle va après `shipHitsRock`, et le test de `updateShots` devient : [source,lua] ---- if not gone and shotHitsRock(shot, rock) then ---- [#_deciding_what_happened] === Décider de ce qui s'est passé Aucun des tests de collision ne fait quoi que ce soit. Ils répondent, et c'est votre code qui décide. Voici les deux endroits qui décident, avec leurs nouvelles questions dedans. D'abord `updateRocks` : [source,lua] ---- if shipHitsRock(rock) then local channel = soundPlay(boomClip) newRock(rock) lives = lives - 1 if lives <= 0 then dying = true deathChannel = channel if channel < 0 then endGame() end end end ---- Et la boucle à l'intérieur de `updateShots` : [source,lua] ---- for _, rock in ipairs(rocks) do if not gone and shotHitsRock(shot, rock) then newRock(rock) score = score + HIT_SCORE gone = true soundPlay(boomClip, 0, 45) end end ---- Tout ce qui se trouve sous les questions est exactement ce que la leçon dix y a laissé. Un rocher qui touche le vaisseau fait boum, retourne en haut et coûte une vie. Un rocher abattu fait boum, retourne en haut et rapporte des points. Le tir disparaît aussi, et le `not gone` empêche un seul tir de faire éclater deux rochers. Changez quoi que ce soit là-dedans et vous changez le jeu, sans toucher à un seul test de collision, et c'est la raison pour laquelle les questions et les réponses sont gardées à part. Lancez-le. Passez tout près d'un rocher et laissez-le effleurer un aileron : vous gardez la vie. Tirez juste à côté d'un rocher et le tir passe sans le toucher. Tirez en plein milieu d'un rocher et il éclate, comme toujours. [#_seeing_the_boxes] === Voir les boîtes Les boîtes de collision sont invisibles, ce qui rend invisible aussi toute erreur qui s'y cache. Dessinez-les pendant que vous travaillez. Une constante de plus en haut : [source,lua] ---- local SHOW_BOXES = false ---- Deux fonctions de plus, qui vont tout en haut de l'ordre alphabétique : [source,lua] ---- function drawBox(x, y, width, height) overlayBox(x, y, x + width - 1, y + height - 1) end function drawBoxes() colorForeground(0, 255, 0) drawBox(shipBox()) for _, rock in ipairs(rocks) do drawBox(rockBox(rock)) end end ---- Et dans `onOverlayUpdate`, après le dessin du vaisseau et avant que le score vienne par-dessus : [source,lua] ---- spriteDraw(shipSprite, shipX, shipY) if SHOW_BOXES then drawBoxes() end drawHud() ---- Passez `SHOW_BOXES` à `true` et des contours verts apparaissent par-dessus les graphismes, comme sur l'image en haut de cette leçon. Remettez-le comme avant quand vous avez fini. Un interrupteur comme celui-ci, un mot en haut d'un fichier qui allume une image de ce que croit le programme, vaut mieux que des heures à fixer l'écran. [#_what_just_happened_7] === Ce qui vient de se passer [source,lua] ---- function rockBox(rock) return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2 end ---- Quatre valeurs pour un seul `return`, séparées par des virgules. Lua renvoie volontiers autant de valeurs que vous voulez, et c'est l'appelant qui décide combien il en garde. Quand un appel comme celui-ci est la *dernière* chose entre les parenthèses d'un autre appel, les quatre sont transmises, et c'est pourquoi `drawBox(rockBox(rock))` marche et donne à `drawBox` ses quatre arguments. Partout ailleurs dans une liste d'arguments, seule la première valeur survit. Cette règle mord exactement une fois, et elle mord à la fin de cette leçon. [source,lua] ---- return collideRects(sx, sy, sw, sh, rx, ry, rw, rh) ---- `collideRects` veut huit nombres : un coin et une taille, un coin et une taille. Il renvoie `true` ou `false` et ne change rien. Comme la réponse ressort directement de la fonction avec `return`, `shipHitsRock(rock)` se lit comme une question partout où on s'en sert. [source,lua] ---- return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh) ---- `collidePointRect` en veut six : un point, puis un rectangle. Le bord compte comme l'intérieur, donc un tir pile sur la limite touche. Servez-vous-en chaque fois qu'un côté de la question est assez petit pour que sa taille n'ait pas d'importance. [source,lua] ---- colorForeground(0, 255, 0) overlayBox(x, y, x + width - 1, y + height - 1) ---- Ces deux appels viennent de la leçon sept. `overlayBox` prend deux *coins*, pas un coin et une taille -- le seul endroit de cette leçon où les nombres changent de forme -- et le `- 1` est là parce que les deux coins sont inclus. `colorForeground` est un interrupteur qui reste basculé, donc tout ce que dessine `drawBoxes` sort en vert. Les sprites et `overlayPrint` n'en tiennent pas compte, donc le reste du jeu garde le même aspect. [#_the_rest_of_the_family] === Le reste de la famille `collideRects` et `collidePointRect` suffiront à la plupart des jeux 2D. Les autres sont là pour les formes que les rectangles décrivent mal, et chacun est le même genre de question simple : * `collideCircles` et `collidePointCircle`, pour tout ce qui est rond. Un cercle, c'est un centre et un rayon. * `collideRectCircle`, pour une chose ronde contre une chose carrée. * `collidePointPolygon`, pour une forme qui a ses propres coins -- un dragon, un continent, un bouton à la forme bizarre. Vous lui donnez une simple liste de nombres `x`, `y`, `x`, `y`. * `collideSegments`, pour deux segments qui se croisent. Soyons honnêtes : votre rocher est rond, et les tests de cercle lui vont mieux que n'importe quelle boîte. La marge, c'est la réponse bon marché, et elle suffit à la plupart des jeux, et c'est pourquoi elle vient en premier. La réponse exacte fait partie des choses à essayer plus bas. `collideSegments` compte plus qu'on ne le croirait. Votre tir avance de six pixels par image et les rochers ont seize pixels de boîte de collision, donc un tir ne peut pas en dépasser un sans être testé à l'intérieur. Rendez le tir assez rapide -- quarante pixels par image -- et il saute d'en dessous d'un rocher à au-dessus, sans se trouver une seule fois au même endroit que lui, et il ne touche jamais rien. Tracer la ligne entre l'endroit où était le tir et celui où il est maintenant, et demander à `collideSegments` si cette ligne croise un bord, voilà le remède. Les choses lentes n'en ont pas besoin. Il y a un seul callback de collision dans Singe, `onCollision`, et il n'est pas fait pour ça. Il appartient au moteur physique 3D : des corps qui ont une masse, qui tombent et rebondissent les uns sur les autres en trois dimensions, et qui vous disent où ils se sont touchés et avec quelle force. Vous le rencontrerez à la leçon vingt-quatre. Rien sur l'overlay ne se signale tout seul, et rien n'en a besoin. Vos rochers sont dans une liste, vous savez combien il y en a, et demander ne coûte pas cher. Et quand vous voulez savoir non seulement *si* deux choses se sont rencontrées, mais aussi où celle qui bouge devrait finir -- en glissant le long d'un mur au lieu de s'arrêter net dedans -- les appels collide ne répondent pas à cette question, et c'est voulu. Une bibliothèque appelée `bump` est fournie avec Singe exactement pour ce travail, et la section Bibliothèques incluses de la Référence explique comment y accéder. [#_try_it_11] === Essayez . *Retirez la marge.* Mettez `ROCK_PAD` à `0` et jouez une minute. Puis mettez-le à `8` et rejouez. Avec l'un des deux, on a l'impression que le jeu vous ment. . *Regardez ce que vous testez.* Mettez `SHOW_BOXES` à `true` et observez les contours verts pendant qu'un rocher passe près du vaisseau. Maintenant, mettez `SHIP_PAD_X` à `16`, jouez, et remarquez que plus rien ne peut vous toucher. Trouvez pourquoi à partir du calcul de `shipBox`. . *Rendez le tir trop rapide.* Mettez `SHOT_SPEED` à `40`. Comptez combien de rochers vous touchez sur dix. Trouvez avec les nombres pourquoi ça arrive avant de le remettre comme avant. . *Servez-vous du tir entier.* Réécrivez `shotHitsRock` pour qu'il utilise `collideRects` avec le rectangle complet du tir au lieu de `collidePointRect` avec son nez. Jouez avec les deux et décidez lequel vous préférez ; il n'y a pas de bonne réponse, et c'est bon à savoir. . *Rendez le rocher rond.* Donnez au rocher un cercle au lieu d'une boîte : son centre est à `rock.x + rockWidth / 2` vers la droite et à `rock.y + rockHeight / 2` vers le bas, et un rayon de `10` convient à peu près. Servez-vous de `collideRectCircle` pour le vaisseau et de `collidePointCircle` pour le tir, et dessinez le cercle dans `drawBoxes` avec `overlayCircle`, qui prend un centre et un rayon. [#_break_it_on_purpose_10] === Cassez-le exprès Les deux boîtes font déjà quatre valeurs chacune, alors on est tenté de les passer directement et de se passer des huit noms locaux : [source,lua] ---- function shipHitsRock(rock) return collideRects(shipBox(), rockBox(rock)) end ---- Ça a l'air juste, et c'est faux. Lancez-le et le jeu s'arrête avant d'avoir dessiné quoi que ce soit : ---- 186:collideRects: Expected 8 argument(s), got 5. ---- La ligne est celle que vous venez de taper, donc la vôtre n'est peut-être pas la 186. Cinq, par contre. `rockBox(rock)` est la dernière chose entre les parenthèses, donc ses quatre valeurs sont toutes entrées. `shipBox()` ne l'est pas, donc il a été réduit à une seule. Un plus quatre font cinq, et `collideRects` ne va pas deviner les trois autres. Cette erreur vient du moteur et non de Lua, donc elle a la même forme que celles des leçons neuf et dix : le nombre au début est la ligne, puis vient le nom de la fonction qui a refusé, puis la plainte. Remettez les huit noms locaux. [#_what_you_learned_11] === Ce que vous avez appris * La collision, c'est de l'arithmétique sur des rectangles, et vous pouvez l'écrire vous-même. * Deux rectangles se ratent d'exactement quatre façons ; si aucune n'est vraie, ils se touchent. * `collideRects` prend un coin et une taille pour chacun de deux rectangles et répond `true` ou `false`. Pour lui, des bords qui ne font que se toucher se touchent. * Les tests de collision font partie du kit de jeu : de simples fonctions, sans rien à préparer. Sauvegarde, Minuteur, Tween et Statistiques en sont le reste. * `collidePointRect` demande si un point est à l'intérieur d'un rectangle, ce qui est la bonne question pour tout ce qui est petit. * La boîte que vous testez est plus petite que l'image que vous dessinez, sinon le jeu semble injuste. * Une fonction peut renvoyer plusieurs valeurs, et seul le dernier appel d'une liste d'arguments les transmet toutes. * Les tests de collision ne font que répondre. Ce qui se passe ensuite, c'est votre code, et garder les deux à part vous permet de changer l'un sans l'autre. * Dessiner vos boîtes de collision à l'écran transforme un bogue invisible en bogue visible. * `onCollision` est pour les corps de la physique 3D, pas pour les sprites de l'overlay. [#_next_time_11] === La prochaine fois Le jeu compte votre score et vous prend vos vies, et dès que vous quittez, le meilleur score jamais obtenu disparaît. Il démarre au milieu de l'action à l'instant où il se charge. Et il garde la trace de ce qu'il fait avec deux variables vrai-ou-faux, `over` et `dying`, ce qui est exactement le piège dont la leçon sept vous a prévenu. La leçon douze lui donne un écran titre, une seule variable à la place de ces deux-là, et un meilleur score qui est toujours là le lendemain.