769 lines
29 KiB
Text
769 lines
29 KiB
Text
[#_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.
|