[#_lesson_31_in_a_cabinet] == Leçon 31 : Dans une borne image::learn/31-cabinet.png[La leçon terminée, 480] Il y a une différence entre un jeu et une machine d'arcade, et ce n'est pas le bois. Une machine s'allume toute seule le matin, joue pour une salle vide, prend une pièce à quelqu'un qui n'a jamais lu un mot à son sujet, et tourne encore à minuit sans que personne ait touché un clavier, parce qu'il n'y a pas de clavier. Tout ce qui se trouve dans cette leçon en découle. Vous n'avez pas besoin d'une borne pour faire quoi que ce soit de tout ça. Un stick et deux boutons sur un bureau, ou une manette, suffisent pour trouver chaque erreur de cette leçon, et presque tout ce que vous apprenez ici rend aussi un jeu meilleur sur un ordinateur de bureau. [#_there_is_no_keyboard] === Il n'y a pas de clavier Commencez par ce qui manque, parce que c'est ce qui casse les jeux. Une borne a un stick, quelques boutons, une porte de monnayeur, un bouton start, et quelque part à l'intérieur un petit bouton que l'exploitant peut atteindre. C'est tout. Pas d'Échap, pas de lettres, pas de souris, aucun moyen de taper. Donc : * Chaque menu de votre jeu doit pouvoir être atteint avec haut, bas et un bouton. Si un écran de votre jeu dit « appuyez sur Y pour continuer », il ne marche pas dans une borne. * Entrer ses initiales, c'est trois boutons et une lettre qui défile, pas un champ de texte. C'est pour ça que les jeux d'arcade ont des noms de trois lettres. * Un message qui nomme une touche par sa lettre a tort. Dites « bouton 1 », ou dessinez le bouton. * Il n'y a pas de sortie. Un joueur ne peut pas quitter, et ne doit pas pouvoir le faire. Parcourez votre jeu une fois, écran par écran, et demandez-vous pour chacun : est-ce que quelqu'un pourrait s'en sortir avec un stick et un seul bouton ? Ce passage sur votre propre jeu vaut plus que tout le reste de cette leçon. [#_controls_cfg] === controls.cfg Depuis la leçon trois, vous lisez les entrées sous la forme `SWITCH_UP`, `SWITCH_BUTTON1`, `SWITCH_COIN1` et le reste, et vous n'avez pas dit une seule fois de quelles touches il s'agit. C'est tout leur intérêt. Quelque part entre le microrupteur de la borne et votre `onInputPressed`, il y a un fichier qui décide, et votre jeu ne prend pas part à cette conversation. Ce fichier, c'est `controls.cfg`. C'est du Lua, comme votre script, et c'est une liste de tables -- une par contact -- qui nomment tout ce qui peut le produire : [source,lua] ---- INPUT_1P_START = { SCANCODE.MAIN_1, GAMEPAD_0.BUTTON_START } INPUT_1P_COIN = { SCANCODE.MAIN_5, GAMEPAD_0.BUTTON_LEFT_BUMPER } INPUT_ACTION_1 = { SCANCODE.LCTRL, GAMEPAD_0.BUTTON_A } INPUT_UP = { SCANCODE.UP, GAMEPAD_0.DPAD_UP, GAMEPAD_0.AXIS_LEFT_Y_U } ---- Chaque nom `INPUT_` du fichier devient la valeur `SWITCH_` correspondante dans votre jeu : `INPUT_ACTION_1` est le `SWITCH_BUTTON1` que vous testez depuis le début, et `INPUT_1P_COIN` est `SWITCH_COIN1`. Chaque élément d'une table produit le même contact, donc un jeu ne se soucie pas de savoir si le joueur a appuyé sur la touche, sur le bouton de la manette, ou poussé le stick. Vous n'avez pas à écrire tout le fichier. Singe part de ses propres valeurs par défaut et lit `controls.cfg` à quatre endroits tour à tour -- le dossier du script du jeu, le dossier depuis lequel Singe a été lancé, le dossier au-dessus du dossier de données du jeu, et le dossier de données du jeu -- et chaque fichier qu'il trouve remplace ce qui précède, clé par clé. Ainsi, une borne a un seul fichier en haut pour toute la machine, et un jeu qui a besoin d'un bouton de plus a son propre fichier de quatre lignes. Il y a un `controls.cfg.example` dans le dossier `Singe`, à copier. `DEAD_ZONE` est là aussi : de combien un stick doit bouger avant de compter comme enfoncé, dans les unités brutes que signale le matériel, où un stick poussé à fond donne `32767`. La valeur par défaut est `15000`. Un stick usé qui dérive a besoin d'un nombre plus grand, et `DEAD_ZONES` règle un axe à la fois pour le seul stick qui est fatigué. La section du manuel sur la personnalisation des commandes couvre tout cela. [#_every_device_is_a_gamepad] === Chaque périphérique est une manette Les boutons d'une borne d'arcade sont câblés sur une carte d'encodeur, que l'ordinateur voit comme une sorte de joystick, et ça ne ressemble en rien à une manette Xbox. Singe écrit lui-même une affectation pour un périphérique de ce genre, puis l'ouvre comme `GAMEPAD_0`, si bien qu'il arrive à `controls.cfg` et à votre jeu exactement comme une manette reconnue. Il y a une seule famille de périphériques et pas de second vocabulaire à apprendre. L'affectation qu'il invente est toute simple : les boutons de la carte, dans l'ordre, deviennent `BUTTON_A`, `BUTTON_B`, `BUTTON_X`, `BUTTON_Y` et ainsi de suite. Si les boutons de votre borne ne sont pas câblés dans cet ordre, ils sortiront aux mauvais endroits, et la correction n'est pas dans votre jeu : `--program` écrit dans la trace l'affectation que Singe a créée, sous une forme que vous pouvez modifier et mettre dans un `gamecontrollerdb.txt` à côté du jeu. Quand un bouton « ne fait rien », l'outil *Input Test* du menu répond à la question en cinq secondes. Il montre chaque périphérique que le moteur voit, et le dernier contact qu'un jeu aurait reçu. Soit le contact arrive et votre jeu l'ignore, soit il n'arrive jamais et le problème vient de l'affectation ou du fil. [#_four_switches_are_not_yours] === Quatre contacts ne sont pas à vous La plupart des contacts vont directement à votre jeu. Quatre, non : le moteur gère lui-même la pause, la sortie, la capture d'écran et le verrouillage de la souris. La pause est vraiment utile, et gratuite. Tant que le moteur la garde pour lui, la touche affectée à `INPUT_PAUSE` gèle tout -- le disque, chaque vidéo, chaque son et votre script -- et affiche son propre indicateur, puis tout repart à l'appui suivant. Tout ce que votre jeu croyait maintenu enfoncé est relâché proprement, puis appuyé de nouveau après, si bien qu'un stick tenu pendant une pause ne reste jamais coincé. La sortie est celle qu'il faut lui retirer, ce que fait une section plus loin. [#_coins_credits_and_start] === Pièces, crédits et départ Une machine d'arcade a une forme plus ancienne que chacun d'entre nous, et les joueurs la connaissent sans qu'on la leur explique : . *Mode attraction.* Personne n'est là. La machine montre ce qu'elle est, fait clignoter `INSERT COIN`, et ne demande rien. . *Crédit.* Une pièce arrive sous la forme de `SWITCH_COIN1`. Les pièces deviennent des crédits au tarif que l'exploitant a réglé -- pas toujours une pour un. . *Partie.* `SWITCH_START1` dépense un crédit et lance la partie. . *Fin de partie.* Le score, brièvement, puis retour au mode attraction tout seul. Personne n'appuie sur rien pour en sortir ; le joueur est parti. Deux détails que les gens ratent. Les pièces sont comptées, pas dépensées : à deux pièces par crédit, une pièce doit rester là à attendre sa partenaire, et un joueur qui en a mis une puis est parti l'a laissée à la personne suivante. Et la machine doit retrouver d'elle-même le chemin du mode attraction, parce que la dernière chose que fait un joueur, c'est d'arrêter de jouer. [#_behind_the_service_switch] === Derrière le bouton service Derrière la porte du monnayeur, il y a un bouton que le joueur ne voit jamais et dont l'exploitant se sert sans arrêt. C'est `SWITCH_SERVICE`, et le moteur vous le passe directement. Un exploitant qui ouvre votre menu de service s'attend à y trouver, à peu près dans cet ordre : * *le prix* -- pièces par crédit, et un réglage de jeu gratuit pour une machine à la maison ou dans un salon ; * *la difficulté*, parce qu'une machine dans un bar et une machine dans une salle d'arcade ne veulent pas les mêmes réponses ; * *un moyen d'effacer le tableau des meilleurs scores*, la chose la plus demandée de toute cette liste ; * *ce qu'est la machine* -- la version du moteur, l'écran, l'endroit où vivent les données -- pour qu'un signalement de panne contienne des faits ; * *une sortie*, sans rien éteindre. Tout ce que change un exploitant doit survivre à une coupure de courant, ce qui, dans une salle d'arcade, veut dire une coupure en pleine partie, sans prévenir, par quelqu'un qui tire sur une prise. `saveSet` écrit à la fin de l'image, ce qui est assez tôt pour un score et pas assez tôt pour un réglage que quelqu'un vient d'ajuster. `saveFlush()` écrit immédiatement, et un menu de service est exactement ce à quoi il sert. Les outils de service du menu fourni, derrière le même bouton, valent la peine d'être connus puisqu'ils sont sur chaque machine où votre jeu tournera : le test des entrées, les tests du son et du pistolet optique, la géométrie de l'affichage, le délai audio, le compte en ligne, et une page qui supprime les données sauvegardées de n'importe quel jeu. Votre menu n'a pas à les refaire. [#_a_machine_that_starts_itself_and_never_quits] === Une machine qui démarre toute seule et ne quitte jamais La borne est mise sous tension à la prise. Personne ne se connecte. Un jeu doit être à l'écran au bout d'une minute environ, et y rester. Faire démarrer Singe à la mise sous tension, c'est le travail de votre système d'exploitation plutôt que celui de Singe -- la machine lance le `Menu.sh` ou le `Menu.bat` que le moteur écrit à côté de vos jeux, ou bien votre jeu directement. Ce qui est votre travail, c'est ce qui se passe pendant le reste de la journée. Retirez la touche de sortie : [source,lua] ---- singeSetQuitKeyEnabled(false) ---- Le moteur ignore alors le contact affecté à `INPUT_QUIT` et vous le livre sous la forme de `SWITCH_QUIT`, comme n'importe quel autre, si bien que vous pouvez demander d'abord, sauvegarder d'abord, ou ne rien faire du tout. Le bouton de fermeture de la fenêtre quitte toujours, quoi que vous régliez, et ça comptera dans une minute. Ensuite, une poignée d'options appartiennent à la machine plutôt qu'à votre script, et vont dans un `settings.cfg` à côté des jeux -- les mêmes noms que les options de ligne de commande, sans les tirets : [source,lua] ---- fullscreen = true screen = 2 idleexit = 900 startsilent = true ---- `idleexit` quitte au bout de ce nombre de secondes sans aucune entrée d'aucune sorte, pour une borne qui doit rendre l'écran à un menu ou à un front-end. `startsilent` démarre en sourdine jusqu'à ce que quelqu'un touche quelque chose, et c'est ce qui rend supportable une salle pleine de machines. Il y en a d'autres ; les sections du manuel sur les réglages et la ligne de commande les listent toutes. [#_artwork_round_the_picture] === Une illustration autour de l'image Une vraie borne a un bezel (un habillage d'écran) imprimé autour de l'écran : une illustration percée d'un trou à travers lequel on voit le jeu. Singe en dessine un pour vous. ---- Singe --bezel cabinet.png MyGame.game ---- Le fichier est cherché dans un dossier `bezels` à l'intérieur du jeu -- un jeu empaqueté peut emporter le sien -- puis dans un dossier `bezels` du dossier de données, si bien qu'un joueur peut mettre sa propre illustration sur un jeu sans toucher au jeu. À côté de l'image, et nommé d'après elle, un petit fichier Lua dit où va l'image du jeu, en pixels de l'illustration elle-même : [source,lua] ---- -- bezels/cabinet.cfg CUTOUT = { x = 200, y = 120, width = 800, height = 500 } ---- Comme ces nombres sont ceux de l'illustration, ils restent justes à n'importe quelle taille de fenêtre : l'illustration et le trou changent d'échelle ensemble. Sans un tel fichier, l'image du jeu garde toute la fenêtre, comme toujours. Par défaut, l'illustration est derrière tout et votre jeu couvre le trou. `--bezelflip` la met devant à la place, si bien que seule sa transparence laisse passer le jeu, ce qu'il vous faut pour des bords adoucis ou découpés -- et alors votre overlay est dessous lui aussi, sauf si vous dites le contraire : [source,lua] ---- if mainBezelLoaded() then setOverlayOnTop(true) end ---- `mainBezelLoaded()` vous dit aussi s'il y a une illustration ou non, et c'est comme ça qu'un jeu décide de dessiner son propre cadre quand il n'y en a pas. [#_a_monitor_on_its_side] === Un moniteur sur le côté Beaucoup de moniteurs d'arcade sont montés à la verticale. Faites tourner toute la présentation : ---- Singe --rotate=90 MyGame.game ---- L'image, l'overlay, toute interface, une scène 3D et les particules tournent tous ensemble, et les positions de la souris et du pistolet optique sont tournées en sens inverse, si bien qu'un clic tombe toujours là où le joueur le voit. Un quart de tour échange aussi la forme de la zone, si bien qu'une fenêtre haute est remplie au lieu de n'en occuper qu'une bande entre deux barres noires. Seuls `0`, `90`, `180` et `270` sont acceptés. Un script peut la changer en cours de route avec `vldpSetRotate`, qui répond `false` pour toute autre valeur plutôt que de mettre fin à votre jeu, et `vldpGetRotate()` la relit -- ce qui vaut la peine d'être mis sur un écran de service, puisque « l'image est sur le côté » et « le moniteur est sur le côté » sont deux pannes différentes. Ce que la rotation ne fait pas, c'est réécrire votre jeu. Un jeu dessiné pour un écran large, tourné de quatre-vingt-dix degrés, est un jeu large couché sur le côté. Concevoir pour un écran haut, c'est concevoir pour un écran haut. [#_the_script_3] === Le script Toute la forme : mode attraction, pièce, crédit, partie, fin de partie, service, et une sortie qui demande confirmation. Le jeu au milieu, c'est un bouton et un minuteur de quinze secondes, parce que le jeu n'est pas le sujet cette fois -- c'est la machine autour qui l'est. [source,lua] ---- dofile("Singe/Framework.singe") local ROUND_MS = 15000 local MENU = { "Coins per credit", "Difficulty", "Free play", "Clear high score", "Exit service" } local HARDNESS = { "Easy", "Normal", "Hard" } local coinsPerCredit = saveGet("coinsPerCredit", 1) local difficulty = saveGet("difficulty", 2) local freePlay = saveGet("freePlay", false) local best = saveGet("best", 0) local state = "attract" local goBackTo = "attract" local coins = 0 local credits = 0 local score = 0 local endsAt = 0 local item = 1 local saying = "" local hasBezel = false local function wrap(value, low, high) if value < low then return high end if value > high then return low end return value end local function canStart() return freePlay or credits > 0 end local function insertCoin() coins = coins + 1 while coins >= coinsPerCredit do coins = coins - coinsPerCredit credits = credits + 1 end end local function startGame() if not freePlay then credits = credits - 1 end score = 0 endsAt = singeGetTicks() + ROUND_MS state = "play" end local function endGame() if score > best then best = score saveSet("best", best) end saveFlush() endsAt = singeGetTicks() + 5000 state = "over" end local function settingOf(which) if which == 1 then return tostring(coinsPerCredit) elseif which == 2 then return HARDNESS[difficulty] elseif which == 3 then if freePlay then return "on" end return "off" elseif which == 4 then return tostring(best) end return "" end local function adjust(step) if item == 1 then coinsPerCredit = wrap(coinsPerCredit + step, 1, 4) saveSet("coinsPerCredit", coinsPerCredit) elseif item == 2 then difficulty = wrap(difficulty + step, 1, #HARDNESS) saveSet("difficulty", difficulty) elseif item == 3 then freePlay = not freePlay saveSet("freePlay", freePlay) end saveFlush() end local function serviceInput(what) if what == SWITCH_UP then item = wrap(item - 1, 1, #MENU) elseif what == SWITCH_DOWN then item = wrap(item + 1, 1, #MENU) elseif what == SWITCH_LEFT then adjust(-1) elseif what == SWITCH_RIGHT then adjust(1) elseif what == SWITCH_BUTTON1 then if item == 4 then best = 0 saveSet("best", 0) saveFlush() saying = "High score cleared." elseif item == 5 then state = goBackTo end elseif what == SWITCH_BUTTON2 or what == SWITCH_SERVICE then state = goBackTo end end function onInputPressed(what) if what == SWITCH_QUIT then if state == "confirm" then singeQuit() else goBackTo = state state = "confirm" end return end if state == "confirm" then if what == SWITCH_BUTTON2 then state = goBackTo end return end if state == "service" then serviceInput(what) return end if what == SWITCH_SERVICE then goBackTo = state saying = "" state = "service" return end if what == SWITCH_COIN1 then insertCoin() return end if state == "play" then if what == SWITCH_BUTTON1 then score = score + 10 * difficulty end return end if what == SWITCH_START1 and canStart() then startGame() end end local function drawAttract() overlayPrint(2, 2, "ROCK DODGER") if canStart() then overlayPrint(2, 4, "PRESS START") elseif (singeGetTicks() // 400) % 2 == 0 then overlayPrint(2, 4, "INSERT COIN") end overlayPrint(2, 6, "High score " .. best) end local function drawService() overlayPrint(2, 0, "SERVICE MENU") for row, name in ipairs(MENU) do if row == item then overlayPrint(1, row + 1, ">") end overlayPrint(3, row + 1, name) overlayPrint(24, row + 1, settingOf(row)) end overlayPrint(2, 8, saying) overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate()) if hasBezel then overlayPrint(2, 11, "Bezel artwork loaded.") else overlayPrint(2, 11, "No bezel artwork.") end overlayPrint(2, 12, singeGetDataPath()) overlayPrint(2, 16, "Up/Down choose. Left/Right change. Button 1 does it.") overlayPrint(2, 17, "Button 2 or the service switch leaves.") end function onOverlayUpdate() overlayClear() if state == "play" and singeGetTicks() >= endsAt then endGame() elseif state == "over" and singeGetTicks() >= endsAt then state = "attract" end if state == "service" then drawService() elseif state == "confirm" then overlayPrint(2, 2, "QUIT THIS GAME?") overlayPrint(2, 4, "Quit again to quit. Button 2 stays.") elseif state == "play" then overlayPrint(2, 2, "SCORE " .. score) overlayPrint(2, 3, "TIME " .. ((endsAt - singeGetTicks()) // 1000)) overlayPrint(2, 5, "Button 1 scores. " .. HARDNESS[difficulty] .. ".") elseif state == "over" then overlayPrint(2, 2, "GAME OVER") overlayPrint(2, 4, "You scored " .. score .. ".") overlayPrint(2, 6, "High score " .. best) else drawAttract() end if state ~= "service" then overlayPrint(2, 17, "CREDITS " .. credits .. " COINS " .. coins .. "/" .. coinsPerCredit) end return OVERLAY_UPDATED end singeSetQuitKeyEnabled(false) hasBezel = mainBezelLoaded() if hasBezel then setOverlayOnTop(true) end ---- Lancez-le et appuyez sur `5` pour une pièce, `1` pour start, et `9` pour le bouton service -- ce sont les valeurs par défaut du `controls.cfg` fourni. Puis appuyez sur Échap, et lisez ce qui s'affiche avant d'appuyer de nouveau. [#_what_just_happened_26] === Ce qui vient de se passer [source,lua] ---- local coinsPerCredit = saveGet("coinsPerCredit", 1) local difficulty = saveGet("difficulty", 2) local freePlay = saveGet("freePlay", false) ---- Les réglages de l'exploitant sont lus une fois, en haut, avec une valeur raisonnable pour une machine qui n'a jamais été réglée. `saveGet` avec un second argument renvoie cette valeur quand rien n'a été sauvegardé, et c'est comme ça qu'on distingue un premier lancement des suivants. [source,lua] ---- local function insertCoin() coins = coins + 1 while coins >= coinsPerCredit do coins = coins - coinsPerCredit credits = credits + 1 end end ---- Des pièces qui entrent, des crédits qui sortent, au tarif que l'exploitant a réglé. Le `while` plutôt qu'un `if`, ce n'est pas de la prudence face à une entrée impossible -- c'est l'exploitant qui baisse le prix de quatre pièces à une alors que trois pièces attendent dedans. [source,lua] ---- if what == SWITCH_QUIT then if state == "confirm" then singeQuit() else goBackTo = state state = "confirm" end return end ---- Le contact de sortie n'arrive à votre jeu que grâce au `singeSetQuitKeyEnabled(false)` tout en bas du script. Un premier appui pose la question ; au second appui, depuis l'écran de la question, c'est vous qui appelez `singeQuit()`. `goBackTo` retient où était le joueur, si bien que répondre non le remet dans sa partie plutôt que sur l'écran d'attraction. Ce bloc est le premier de la fonction et il se termine par `return`, donc rien d'autre ne peut avaler le contact. Tous les autres états du jeu sont traités en dessous. [source,lua] ---- if state == "service" then serviceInput(what) return end ---- La même forme, encore. Tant que le menu de service est ouvert, chaque contact lui appartient -- le stick ne dirige pas le jeu en dessous, une pièce ne tombe pas au milieu d'un réglage. Un `if`, tôt, avec un `return`. Tout écran posé par-dessus un autre en a besoin, et un jeu qui en accumule trois sans ça produit des bogues presque impossibles à décrire. [source,lua] ---- saveSet("coinsPerCredit", coinsPerCredit) ... saveFlush() ---- D'abord enregistrer, puis forcer l'écriture. Sans cette écriture forcée, elle a lieu à la fin de l'image, ce qui est rapide mais pas immédiat. Un exploitant ajuste un réglage et débranche la prise dix secondes plus tard, et c'est le seul endroit d'un jeu où ça arrive vraiment. [source,lua] ---- elseif (singeGetTicks() // 400) % 2 == 0 then overlayPrint(2, 4, "INSERT COIN") end ---- Le clignotement. `singeGetTicks()` donne les millisecondes écoulées depuis le démarrage du moteur, `//` divise et jette le reste, si bien que le nombre change toutes les quatre cents millisecondes, et `% 2` le fait alterner entre zéro et un. Pas de variable, pas de minuteur, rien à remettre à zéro : l'horloge comptait déjà. [source,lua] ---- overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate()) ---- La moitié de la page de service qui est pour l'exploitant. Une version, une rotation, la présence ou non d'une illustration, et l'endroit où vivent les données. Rien de tout cela n'est pour le joueur, et c'est exactement ce que vous regretterez de ne pas avoir quand quelqu'un vous téléphonera à propos d'une machine à cinq cents kilomètres de là. [source,lua] ---- singeSetQuitKeyEnabled(false) hasBezel = mainBezelLoaded() ---- En bas, hors de toute fonction, pour que ça s'exécute une seule fois au chargement du script. Aucune des deux lignes n'est à faire à chaque image, et `mainBezelLoaded` ne peut pas changer pendant que le jeu tourne. [#_try_it_31] === Essayez . *Changez le prix.* Ouvrez le menu de service, réglez les pièces par crédit sur deux, et mettez une pièce. Regardez le crédit ne pas apparaître, et regardez le compteur de pièces sur la ligne du bas. Puis quittez, relancez, et constatez que le prix est resté. . *Jouez gratuitement.* Activez le jeu gratuit et vérifiez que start marche sans pièces et que les crédits ne deviennent pas négatifs. . *Ajoutez une page.* Mettez une seconde page d'informations de service derrière le bouton 3 -- les champs de `singeGetSystemInfo()` sont un bon début, et son entrée dans la Référence les liste tous. . *Tournez l'écran.* Lancez-le avec `--rotate=90` et regardez votre mise en page. Puis rendez la page de service lisible avec cette rotation, ce qui est plus dur qu'il n'y paraît et correspond exactement au travail que demande une borne verticale. . *Retirez le clavier.* Débranchez-le, ou promettez-vous de ne pas y toucher, et parcourez chaque écran avec une manette. Tout ce que vous ne pouvez pas atteindre est un bogue. [#_break_it_on_purpose_30] === Cassez-le exprès Supprimez le bloc `SWITCH_QUIT` de `onInputPressed`, en laissant `singeSetQuitKeyEnabled(false)` là où il est, et lancez. Maintenant, plus rien ne quitte. Échap ne fait rien, `Q` ne fait rien, et votre jeu n'a aucune sortie, ce qui est précisément ce que vous avez demandé. Ce n'est pas un plantage et il n'y a aucun message ; le moteur a fait exactement ce qu'on lui a dit. Fermez la fenêtre avec son bouton de fermeture. Ça marche toujours, quoi qu'en dise un script, et c'est la porte que vous vous êtes laissée. En plein écran, `Alt-Enter` vous remet d'abord dans une fenêtre. Ça vaut la peine de le sentir, celui-là. Désactiver la touche de sortie est la seule chose de ce livre qui puisse vous laisser avec un programme dont vous ne pouvez pas sortir, et l'habitude qui l'évite est simple : écrivez la sortie dans la même séance que celle où vous désactivez la touche de sortie, jamais plus tard. [#_what_you_learned_31] === Ce que vous avez appris * Une borne n'a pas de clavier, donc chaque écran du jeu doit marcher avec un stick et un bouton. * `controls.cfg` affecte les touches, les boutons de manette et les directions du stick aux noms `SWITCH_` que votre jeu utilise déjà, et votre jeu ne sait jamais lequel a été pressé. * Quatre fichiers d'affectations sont lus tour à tour, chacun remplaçant le précédent, si bien qu'un fichier pour toute la machine et un fichier par jeu peuvent coexister. * Chaque périphérique qu'ouvre Singe est une manette, y compris une carte d'encodeur d'arcade, et l'outil Input Test dit ce qui arrive. * Le moteur garde pour lui la pause, la sortie, la capture d'écran et le verrouillage de la souris ; le reste est à vous. * Les pièces sont converties en crédits au tarif de l'exploitant, et une machine revient d'elle-même au mode attraction. * `SWITCH_SERVICE` est le bouton de l'exploitant, et derrière lui se trouvent le prix, la difficulté, un moyen d'effacer les scores, et ce qu'est la machine. * `saveFlush()` écrit un réglage tout de suite, parce qu'une borne perd le courant sans prévenir. * `singeSetQuitKeyEnabled(false)` donne le contact de sortie à votre jeu. Écrivez la sortie au même moment. * Un bezel est une illustration percée d'un trou, et `--rotate` fait tourner toute la présentation pour un moniteur vertical. [#_the_end] === La fin Ça fait trente et une leçons, et vous savez écrire un jeu. Pas un jeu parfait, et pas encore un gros, mais ce qui vous sépare de l'un ou de l'autre, c'est du temps et de la pratique plutôt qu'un autre livre. Vous avez commencé avec cinq lignes qui mettaient un mot sur un écran noir. Vous savez maintenant ce qu'est une variable, à quoi sert une fonction, comment dessiner, comment faire du bruit, comment savoir quand deux choses se sont touchées, comment garder un score quelque part où il survit, comment faire d'un film un monde, comment mettre un modèle dans une pièce avec une lumière dessus, et comment confier le tout à un inconnu. Ce dont vous avez besoin maintenant, c'est de la _Référence de Singe_, pas de ce livre. C'est la référence de chaque fonction, de chaque argument et de chaque valeur renvoyée, et c'est elle que vous aurez vraiment ouverte pendant que vous travaillez. Elle accompagne ce livre, sous les noms `Reference.pdf` et `Reference.html`. Lisez l'entrée avant d'utiliser l'appel ; la plupart contiennent une remarque sur l'erreur que les gens font avec. Lisez aussi les scripts fournis avec Singe. `Singe/Menu.singe`, `Singe/Tools.singe` et `Singe/Framework.singe` ne sont pas des démonstrations écrites pour être lues -- ce sont le menu et les outils de service dont vous vous servez depuis le début, qui font un vrai travail avec les mêmes fonctions que vous. Comprendre comment le programme de quelqu'un d'autre fait quelque chose est une compétence à part entière, et ces scripts sont un endroit bienveillant pour s'y exercer. Et il y a Forge, un outil séparé avec son propre livre, pour décrire un jeu plutôt que l'écrire. C'est une autre route. Vous n'en avez pas besoin, mais elle est là. Le prochain jeu est le vôtre. Faites d'abord le petit, finissez-le, et mettez-le devant quelqu'un. Finir, c'est la partie dont personne ne vous dit que c'est la plus dure, et c'est la seule qui transforme un dossier plein de scripts en un jeu auquel quelqu'un a joué. Allez le construire.