singe/docs/fr/lessons/31-cabinet.adoc

769 lines
29 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

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

[#_lesson_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.