545 lines
22 KiB
Text
545 lines
22 KiB
Text
[#_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.
|