singe/docs/fr/Reference.adoc

20358 lines
830 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.

= Référence de Singe
Scott Duensing <scott@kangaroopunch.com>
:revnumber: 3.00
:revdate: 2026
:doctype: book
:toc: left
:toclevels: 3
:sectnums:
:sectnumlevels: 3
:title-logo-image: image:images/singeLogo.png[Singe, pdfwidth=2.6in, align=center]
:source-highlighter: rouge
:icons: font
:experimental:
:lang: fr
:imagesdir: ..
include::locale.adoc[]
[#_game_development]
== Développement de jeux
[#_lua]
=== Lua
Singe utilise le langage de programmation Lua (http://www.lua.org) pour écrire la
logique des jeux. Lua est rapide, léger, orienté objet, facile à utiliser, et
réellement employé dans l'industrie du jeu vidéo. Ce livre ne l'enseigne pas ;
_Apprendre à programmer avec Singe_, dans le même téléchargement que celui-ci,
s'en charge, de zéro jusqu'à un jeu terminé, en trente et une leçons. Des
tutoriels vidéo sur Lua se trouvent sur la chaîne YouTube de Kangaroo Punch, et
la référence du langage lui-même est sur http://www.lua.org.
[#_basic_rules]
=== Règles de base
* Singe est multiplateforme. Des choses dont vous n'avez normalement pas à vous
soucier sous Windows deviennent importantes. Les noms de fichiers sont
sensibles à la casse -- `MyScript.singe` et `myscript.singe` ne sont pas le
même fichier. Utilisez aussi les séparateurs de chemin UNIX -- `/` plutôt que
`\`. Cela rend votre code plus propre (pas de barres obliques inverses
doublées) et fonctionne partout où Singe est pris en charge.
* Choisissez pour le dossier de votre jeu un nom qui le distingue. N'y mettez
pas d'espaces.
* Utilisez le framework de Singe fourni. Ne copiez pas le framework dans les
scripts de votre jeu. En incluant le framework fourni, vous favorisez la
compatibilité future à mesure que Singe est mis à jour. Commencez (ou
terminez) le script de votre jeu par :
+
[source,lua]
----
dofile("Singe/Framework.singe")
----
* Gardez votre jeu autonome. Si vous vous appuyez sur un framework tiers ou
partagez du code entre vos jeux, copiez-le dans le dossier de votre jeu. Ne
faites jamais référence à un dossier situé à côté de votre jeu ; le seul
fichier hors de votre jeu qu'un script peut charger est
`Singe/Framework.singe`. Un jeu en un seul fichier (voir
<<singlefile,Jeux en un seul fichier>>) empaquette exactement un dossier, et
un jeu qui va chercher en dehors ne peut pas être empaqueté.
* Restez en dehors du dossier `Singe/`. Il est géré par Singe, et tout ce qui y
est ajouté ou modifié risque d'être supprimé à l'avenir.
* Incluez un `games.dat`. C'est extrêmement important pour les nouveaux
utilisateurs. Même s'il existe d'autres front-ends et des systèmes de menus
basés sur Singe, il est vital d'inclure les données destinées au système de
menus intégré. Presque tous les problèmes d'assistance viennent de fichiers
`games.dat` manquants ou mal formés.
* Ne distribuez pas de fichiers non essentiels. Ne livrez jamais les exécutables
de Singe ni les scripts fournis par Singe avec votre jeu. Ne livrez pas les
fichiers d'index générés à partir des vidéos.
[#reloading]
=== Recharger pendant que vous travaillez
Lancez le jeu avec `--reload` (`-R`) et Singe surveille chaque fichier de script
non empaqueté que le jeu a chargé. Enregistrez une modification dans l'un d'eux
et le jeu repart de son script un instant plus tard : tout ce que le script a
créé disparaît (sprites, sons, polices, vidéos, GUI, la scène 3D, la
physique, les émetteurs, l'overlay revient à sa taille par défaut, le volume des
effets et la souris reviennent à leur état de démarrage), le moteur reste (la
fenêtre, le GPU, les manettes), le disque est de nouveau arrêté sur sa première
image comme au démarrage, et le script s'exécute à neuf, si bien qu'un modèle de
52 Mo revient à l'écran dans le temps qu'il faut pour le charger plutôt que dans
celui qu'il faut pour démarrer Singe. Une touche ou un bouton maintenu pendant le
rechargement est ignoré jusqu'à ce qu'il soit relâché, comme lors d'un démarrage
à froid. `F5` recharge à la demande, et un script peut le demander avec
`singeReload()`. Les jeux empaquetés n'ont rien à surveiller ; l'option est
faite pour un jeu dans un dossier. Une erreur de script au rechargement est
affichée et le jeu reste vide jusqu'à ce que le rechargement suivant la corrige ;
le fichier reste surveillé, donc enregistrer la correction suffit. Comme le
rechargement exécute le même nettoyage que lorsqu'on quitte, toute fuite d'un
script se voit ici en premier.
[[zerobrane]]
=== Déboguer dans ZeroBrane Studio
https://studio.zerobrane.com[ZeroBrane Studio] est un IDE Lua gratuit, et Singe
apporte tout ce qu'il faut pour y lancer et y déboguer un jeu : points d'arrêt,
pas à pas, fenêtres Pile d'exécution et Expressions espionnes, un jeu qui reste
immobile tant qu'il est arrêté, un arrêt dans l'IDE quand une erreur va mettre
fin au jeu, et l'autocomplétion avec des infobulles pour toute l'API, en
anglais, en français, en italien, en allemand ou en espagnol. La partie
débogueur est MobDebug, intégré à Singe et démarré par `--debug`, si bien qu'un
jeu n'a besoin d'aucune ligne à lui ; tout autre outil qui parle le protocole de
MobDebug peut se connecter de la même façon.
[[zerobranesetup]]
==== Mise en place
Singe décompresse un paquet pour ZeroBrane Studio dans son dossier de support,
sous `Singe/ZeroBrane/singe.lua`, et le tient à jour d'une version à l'autre.
Indiquez à ZeroBrane Studio où il se trouve par une seule ligne dans son
`user.lua` (Édition > Préférences > Paramètres : Utilisateur), puis
redémarrez-le :
[source,lua]
----
package "/home/me/Singe/Singe/ZeroBrane/singe.lua"
----
Singe apparaît alors dans Projet > Interpréteur Lua, avec Projet > Déboguer avec
Singe et un bouton Singe dans la barre d'outils pour lancer un jeu sous le
débogueur en une seule étape. Les fichiers `.singe` s'ouvrent en Lua, les
documents `.rml` en XML et les feuilles de style `.rcss` en CSS. Faites du
dossier du jeu le projet (Projet > Répertoire de projet > Choisir...). Le
paquet cherche Singe dans ce dossier et dans les trois au-dessus, en prenant la
version la plus récente pour votre machine, puis dans le `PATH` ; s'il n'en
trouve aucun, il vous le demande la première fois que vous lancez un jeu, et
s'en souvient. Projet > Choisir l'exécutable de Singe... en désigne un autre, et
`path.singe = "/path/to/Singe"` dans `user.lua` l'emporte sur les deux.
[[zerobranerun]]
==== Lancer et déboguer
Projet > Exécuter (`F6`) lance le jeu, et Projet > Lancer le débogage (`F5`) le
lance sous le débogueur. Le script est le point d'entrée du projet s'il y en a
un (faites un clic droit dessus dans l'arborescence du projet et choisissez
Définir le fichier en tant que point d'entrée), et sinon le fichier en cours
d'édition ; quand celui-ci est un des modules du jeu plutôt qu'un fichier
`.singe`, c'est l'unique fichier `.singe` du projet qui est lancé. Singe démarre
dans le dossier qui contient son exécutable, comme le menu le lancerait, si
bien que `Singe/Framework.singe` et tous les noms relatifs à ce dossier se
résolvent comme d'habitude, et l'entrée `games.dat` du jeu fournit sa vidéo et
ses réglages. Projet > Paramètres de ligne de commande... ajoute vos propres
options, dont `-v` et une vidéo pour un jeu sans entrée.
Cliquez dans la marge pour poser un point d'arrêt. Le jeu s'y arrête avec ses
variables locales dans la fenêtre Pile d'exécution, la fenêtre Expressions
espionnes évalue des expressions, et Pas à pas détaillé (`F10`), Pas à pas
sommaire (`Shift+F10`) et Pas à pas sortant (`Ctrl+F10`) se comportent comme
dans n'importe quel programme Lua, à l'intérieur de `singeMain` aussi. Projet >
Interrompre arrête un jeu là où il en est. Tant que le jeu est arrêté, le
disque, chaque vidéo et chaque son se mettent en pause exactement comme pour la
touche pause, et reprennent ensuite au même endroit.
Quand une erreur va mettre fin au jeu -- une erreur Lua, ou un appel du moteur
qui reçoit quelque chose dont il ne peut rien faire --, l'IDE s'arrête d'abord,
dans le `Debugger.singe` du dossier Singe, et la fenêtre Sortie dit ce qui ne
va pas. La fenêtre Pile d'exécution garde sous ce fichier les propres cadres du
script, avec leurs variables locales telles qu'elles étaient. Continuez, et le
jeu se termine comme il l'aurait fait, avec l'erreur et une trace de pile dans
la fenêtre Sortie ; double-cliquez sur une ligne de la trace pour ouvrir le
fichier à cet endroit.
Le jeu tourne avec `--reload`, si bien qu'enregistrer un script relance le jeu
et le débogueur avec lui, points d'arrêt compris. Projet > Relancer Singe quand
un script change désactive ce comportement.
[[zerobranecompletion]]
==== Autocomplétion et Référence
L'autocomplétion propose chaque fonction, callback et constante de l'API, et
une infobulle en donne les arguments et le premier paragraphe de l'entrée.
Projet > Analyser (`Shift+F7`) connaît l'API lui aussi, si bien qu'il signale un appel mal orthographié plutôt que chaque appel. Le
réglage de langue de ZeroBrane Studio choisit la langue des infobulles :
`language = "fr"` dans `user.lua` donne le français, et il en va de même pour
l'italien (`it`), l'allemand (`de`) et l'espagnol (`es`). Faites un clic droit
sur un nom et choisissez Chercher dans la Référence de Singe pour ouvrir son
entrée dans un navigateur ; Aide > Référence de Singe ouvre le début. Les deux
ont besoin du téléchargement de la documentation décompressé à côté de Singe,
sous `Singe-Docs-v3.00` ou `Singe-Docs-v3.00-fr` et ainsi de suite, celui dans
la langue de ZeroBrane Studio étant pris en premier ;
`singe = { reference = "/path/to/Reference.html" }` dans `user.lua` en désigne
un autre.
[[zerobranecabinet]]
==== Déboguer une borne
Le débogueur fonctionne à travers un réseau. Sur la machine qui a ZeroBrane
Studio, ouvrez le dossier du jeu comme projet et choisissez Projet > Lancer le
serveur de débogage. Sur la borne, lancez le jeu avec `--debug=HOST`, où `HOST`
est le nom ou l'adresse de cette machine ; le port est `8172` sauf si
`--debug=HOST:PORT` en donne un autre, et la première machine doit accepter les
connexions sur ce port. Une borne qui lance ses jeux depuis le menu peut porter
`debug = "HOST:PORT"` dans son fichier de réglages à la place, et chaque jeu
qu'elle lance se connecte, le menu compris. L'IDE arrête chacun sur sa première
ligne, où Continuer (`F5`) le met en route ; `debugger.runonstart = true` dans
`user.lua` le laisse aller tout droit jusqu'au premier point d'arrêt. La borne
annonce ses propres chemins, que ZeroBrane Studio fait correspondre aux fichiers
du projet la première fois qu'il s'arrête, si bien que le même dossier de jeu
sur les deux machines suffit. Si rien n'écoute, un jeu se joue comme d'habitude
une fois que la tentative de connexion abandonne, ce qui ne prend pas plus de
deux secondes.
[[zerobranelimits]]
==== Limites
* Déboguez un jeu non empaqueté, depuis son dossier. Un `.game` empaqueté n'a aucun fichier sur le disque que l'IDE puisse ouvrir.
* Une erreur dans `singeMain` s'arrête dans `Framework.singe`, qui exécute `singeMain` comme une coroutine ; le message donne la ligne de votre script.
* Tant que l'IDE tient le jeu arrêté, la fenêtre ne se redessine pas, et la fermer attend que vous continuiez. Arrêter le processus (`Shift+F5`) termine le jeu aussitôt.
* Singe arrête un script qui ne rend jamais la main quand on ferme sa fenêtre, mais pas sous le débogueur, qui possède le script tant qu'il tourne ; Interrompre trouve où il est bloqué.
[[deterministic]]
=== Mode de test déterministe
`--deterministic` est un outil de test, pas une façon de jouer. Il fait ignorer
complètement le temps réel au moteur : au lieu de demander à la machine l'heure
qu'il est, le moteur tourne sur une horloge virtuelle qui avance d'un pas fixe
-- 15 millisecondes par défaut -- exactement une fois par image. Tout ce qui est
cadencé par le temps suit cette horloge : les pas de la physique, l'animation des
sprites et des modèles, les particules, la navigation, les animations et les
transitions de la GUI, `os.clock()`, `singeGetTicks()` et un `os.time()`
sans argument (un jeu qui initialise son générateur à partir de l'horloge au
début d'une partie l'initialise à partir de l'horloge virtuelle, et tire donc les
mêmes nombres à chaque exécution ; `os.time` auquel on donne une table de date
la convertit comme Lua l'a toujours fait). Le disque la suit aussi : pendant la
lecture, chaque image de la vidéo reste affichée pendant autant d'images du
moteur que vaut son temps de présentation sur cette horloge (deux ou trois au
pas par défaut pour un disque à trente images par seconde), comme en jeu réel,
et un disque en pause ou qui a fait un saut reste là où on l'a mis. Le
générateur de Lua et celui du moteur sont initialisés à partir d'un nombre fixe.
Rien n'attend rien, donc le jeu tourne aussi vite que la machine le permet.
Tout l'intérêt est qu'une capture d'écran prise à un numéro d'image fixe est la
même image à chaque exécution. Sans cela, l'image 40 est un moment différent à
chaque fois -- la physique a fait un nombre de pas différent et le disque est sur
une autre image de la vidéo -- si bien que deux exécutions du même jeu inchangé
peuvent différer d'un million de pixels, et qu'un vrai changement peut se cacher
dans ce bruit.
Ce que cela coûte, c'est tout ce qui dépend du temps réel. Le son est toujours
joué sur l'horloge du périphérique, qu'aucune horloge virtuelle n'atteint, donc
le son dérive par rapport à l'image et ne veut rien dire -- sauf si l'exécution
est enregistrée avec `--record` : le son est alors mixé en mémoire un pas à la
fois en même temps que l'image, si bien qu'un enregistrement fait de cette façon
est exact image par image et échantillon par échantillon (voir
<<singerecordstart,singeRecordStart>>). La fréquence d'images ne veut rien dire
non plus, pas plus que tout ce qu'un jeu mesure en secondes. Ne rapportez jamais
un chiffre de performance, un problème de son ou un problème de synchronisation
à partir d'une exécution dans ce mode, et ne livrez jamais un lanceur qui
l'utilise.
Le nombre que porte l'option, si elle en porte un, est à la fois la taille du pas
et la graine dont partent les générateurs, de `1` à `1000` :
----
Singe --deterministic le pas par défaut de 15 ms, graine 15
Singe --deterministic=20 un pas de 20 ms, graine 20
----
Deux exécutions avec le même nombre sont identiques ; deux exécutions avec des
nombres différents sont chacune reproductibles mais différentes l'une de
l'autre, et c'est ainsi qu'un test demande une exécution différente mais tout
aussi reproductible. L'option peut se régler dans un fichier de réglages sous le
nom `deterministic`, comme toute autre option.
[#_reference_screenshots]
==== Captures d'écran de référence
Les scènes de `testScripts` prennent chacune une capture d'écran ou deux à un
numéro d'image fixe et quittent d'elles-mêmes, si bien que dans ce mode elles
produisent un jeu d'images de référence auquel un changement ultérieur peut être
comparé pixel par pixel. Pour les enregistrer, lancez chaque scène avec l'option
et gardez ce qui arrive dans le dossier de données :
[source,sh]
----
for n in 2 3 4 5 6 7 8 9 10 11 13 14 15 16 18 19 21 22 23 24 25 26 \
36 37 38 39 40 41 42 43 44 45 48 49 50 51; do
Singe -w -d data --deterministic -v Singe/menuBackground.mkv \
testScripts/scene$n.singe
done
----
Les scènes laissées hors de cette boucle veulent leurs propres options, celles
que leur donne le tableau de `testScripts/README.md` : `scene12` se lance sans
aucun disque, `scene17` avec le disque, et `scene20` avec `-C 720x480`, toutes
trois avec `SDL_GPU_DRIVER=nothing` ; `scene27` à `scene35` veulent `-x 1280 -y 720
-C 1280x720` et pas de disque ; `scene46` veut `-g '10 5'` ; et `scene47` veut
`--bezeldir=testScripts/bezels --bezel=cabinet.png`. Copiez les fichiers
`singe*.png` hors de `data/testScripts` après chaque exécution, puisque
l'exécution suivante numérote de nouveau ses captures à partir de zéro.
Pour comparer une version ultérieure, il suffit alors de lancer les mêmes
commandes et de demander la différence, qui doit être nulle :
[source,sh]
----
compare -metric AE reference/scene36-singe000.png new/scene36-singe000.png null:
----
[#_where_files_live]
=== Où vivent les fichiers
Les chemins dans un script sont relatifs au dossier depuis lequel Singe a été
lancé, pas au script. Utilisez la variable globale `DIR`, que `Framework.singe`
règle sur le dossier du script lui-même, pour qu'un jeu fonctionne où qu'il soit
installé :
[source,lua]
----
crosshair = spriteLoad(DIR .. "sprite_Crosshair.png")
----
Tout ce que Singe écrit va dans le dossier de données (`singeGetDataPath()`),
`data/` à côté des jeux sauf si `--datadir` en décide autrement, si bien qu'un
jeu peut vivre sur un support en lecture seule. Singe 2.x écrivait les
sauvegardes et les réglages dans le dossier du jeu lui-même ; un jeu qui le fait
encore échoue sur une installation en lecture seule, alors gardez chaque
`io.output` sous `singeGetDataPath()`.
[#_packaging_your_game]
=== Empaqueter votre jeu
Un jeu terminé se livre sous la forme d'un seul fichier `.game`, décrit
ci-après. Rien d'autre n'est nécessaire : le joueur copie le fichier à côté de
l'exécutable de Singe et le menu l'affiche. Singe n'installe plus d'archives.
[#singlefile]
=== Jeux en un seul fichier
Un jeu peut tenir en un seul fichier. `singe --pack` écrit un dossier de jeu dans
une base de données SQLite, et Singe le lance depuis celle-ci : scripts,
sprites, sons, polices, framefiles et vidéo se chargent tous directement depuis
la base de données, et installer le jeu revient à copier le fichier à côté de
l'exécutable. Le menu affiche chaque fichier `.game` qu'il trouve à côté des
dossiers de jeux.
----
Singe --pack MyGame MyGame.game Empaquette le dossier MyGame dans MyGame.game
Singe MyGame.game Lance la première entrée du games.dat qu'il contient
Singe --entry=2 MyGame.game Lance sa deuxième entrée
Singe --unpack MyGame.game MyGame Réécrit les fichiers sur le disque
Singe --patch MyGame.game fixes Remplace des fichiers depuis un dossier (ou une base de correctif)
----
Rien dans un jeu n'a besoin de changer pour qu'il soit empaqueté. Chaque nom
qu'utilise un script, que ce soit par `dofile`, `require`, `io.open` et ses
semblables, `spriteLoad`, `soundLoad`, `fontLoad`, `videoLoad`, un framefile ou
un `controls.cfg`, passe par une seule et même recherche. Pour un jeu non
empaqueté, cette recherche est le simple système de fichiers, relativement au
dossier depuis lequel Singe a été lancé, exactement comme avant. Pour un jeu
empaqueté, un nom est essayé à trois endroits, dans cet ordre, et le premier
trouvé l'emporte :
. Un dossier non empaqueté portant le nom de la base de données sans son
extension (`MyGame/` à côté de `MyGame.game`). C'est ainsi que vous continuez à
modifier un jeu empaqueté, et qu'un joueur en fait un mod : les fichiers
déposés là l'emportent sur les copies empaquetées.
. Le dossier de données du jeu, sous `files/`. Tout ce qu'un jeu écrit avec
`io.output` ou `io.open(name, "w")` arrive ici, si bien qu'un `game.cfg`
enregistré l'emporte sur l'original empaqueté et que la base de données n'est
jamais écrite.
. La base de données elle-même.
Dans une base de données, les noms sont comparés sans tenir compte de la casse,
avec l'un ou l'autre séparateur de chemin, et relativement à la racine du jeu.
Un premier composant égal au nom du dossier du jeu lui-même est ignoré, si bien
que `MyGame/Cfg/game.cfg`, `Cfg/game.cfg` et `DIR .. "Cfg/game.cfg"` trouvent
tous le même fichier. Le nom de la base de données elle-même (`MyGame.game`,
avec ou sans séparateur final) est la racine du jeu, comme l'est `.` depuis
l'intérieur. Les noms commençant par `Singe/` ou par le dossier de données
restent sur le système de fichiers ; `Singe/Framework.singe` et
`singeGetDataPath()` fonctionnent comme toujours, et un tel nom peut utiliser
`..` tant qu'il reste sous ce préfixe (le `controls.cfg` à la racine du dossier
de données est trouvé ainsi). Tout autre nom relatif comportant un composant
`..` ne mène nulle part dans un jeu empaqueté : `io.open` renvoie `nil` avec un
message disant que le nom sort du jeu, `lfs.attributes` renvoie `nil`, et
`spriteLoad` et ses semblables signalent le fichier comme manquant. La racine du
jeu est un dossier, pas un fichier : `io.open` sur elle renvoie `nil`, et
`lfs.mkdir` et `lfs.rmdir` la refusent. `lfs.dir` sur un dossier empaqueté liste
la réunion des trois endroits, triée sans tenir compte de la casse ; quand un
fichier non empaqueté et un fichier empaqueté ne diffèrent que par la casse,
c'est l'orthographe du fichier non empaqueté qui est listée. `lfs.attributes`
sur un fichier empaqueté répond comme le vrai `lfs` : la table entière avec un
seul argument, un champ par son nom (un nom inconnu lève une erreur), ou une
table à vous qu'il remplit ; un fichier manquant renvoie `nil`, un message et le
numéro d'erreur. `require("name")` trouve `name.lua`, `name/init.lua` ou
`name.singe` sous le dossier du script, puis sous la racine du jeu.
`io.open`, `io.lines` et `io.input` sur un nom qui n'existe que dans la base de
données renvoient un fichier propre à Singe plutôt qu'un fichier de Lua. Le
handle de fichier de Lua est un `FILE *` du C, et une ligne de base de données
n'est pas un fichier, si bien que la lire voulait dire autrefois l'écrire à côté
du jeu et ouvrir cette copie ; le handle de Singe lit la ligne là où elle se
trouve. Il répond à tout ce à quoi répond celui de Lua -- `read` avec tous les
formats (`"a"`, `"l"`, `"L"`, `"n"` et un nombre d'octets), `lines`, `seek`,
`close`, `flush` et `setvbuf` -- `io.type` le considère comme un fichier,
`io.read` et `io.close` l'acceptent, et il se ferme de lui-même quand il est
collecté ou quand une variable `<close>` sort de sa portée. La seule différence
est qu'y écrire est une erreur, parce que ce qu'il lit est dans la base de
données et que la base de données n'est jamais écrite. Un jeu qui écrit un
fichier en obtient un vrai dans son dossier de données exactement comme avant,
et il en va de même pour chaque nom qui n'est pas empaqueté.
`testScripts/packedIo` est le test qui confronte tout cela au comportement de
Lua lui-même ; son en-tête dit comment le lancer.
L'empaqueteur refuse un dossier qui n'a pas de `games.dat`, qui contient
`controls.dat`, `Framework.singe`, un fichier dont l'extension est `exe`, `sh`,
`bat` ou `cmd`, ou un fichier sans extension dont le nom commence par `singe`,
qui a une entrée de premier niveau portant le nom du dossier lui-même (cela
rendrait ambiguë la règle du dossier propre au jeu), ou dont les scripts et les
fichiers de données sortent du jeu avec `..` ; il signale chacun de ces
fichiers, avec la ligne. Deux fichiers dont les noms ne diffèrent que par la
casse ne peuvent pas être empaquetés tous les deux. Les fichiers `.index`
périmés sont ignorés. `--unpack` refuse une base de données dont les noms
stockés atterriraient hors du dossier cible (un chemin absolu, une lettre de
lecteur ou un composant `..`). Un jeu qui fait référence au dossier d'un autre
jeu (un framework partagé situé à côté de lui) doit d'abord copier ce dossier à
l'intérieur. Avant que la base de données soit validée, son `games.dat` est
vérifié : chaque entrée a besoin d'un `SCRIPT`, et chaque `SCRIPT`, `VIDEO`,
`CABINET`, `MARQUEE` et `ATTRACT` qu'elle nomme doit être un fichier de la base
de données (ou sous `Singe/`) ; un mauvais chemin est signalé avec le titre de
l'entrée et l'empaquetage est refusé. `--patch` effectue la même vérification
sur le jeu corrigé, si bien qu'un correctif ne peut pas non plus laisser derrière
lui un chemin qui ne mène nulle part.
`--patch` prend soit un dossier organisé comme le jeu, dont les fichiers
remplacent ceux qui sont empaquetés, soit un fichier `.patch` créé par `--pack`
à partir d'un tel dossier (`Singe --pack fixes MyGame-1.1.patch` ; un correctif
n'a pas besoin de `games.dat`). Les fichiers que le correctif ne nomme pas sont
laissés tels quels. Dans les deux cas, le changement est une seule
transaction : le jeu reste intact s'il est interrompu. Un correctif peut aussi
porter une table `removed(path)` qui nomme des fichiers à supprimer. Un `.patch`
peut être désempaqueté comme un jeu, mais il ne peut pas être lancé et le menu
l'ignore.
Un correctif qui remplace un gros fichier par un petit laisse dans la base de
données l'espace qu'occupait l'ancien contenu, où SQLite le garde pour la
prochaine chose écrite plutôt que de le rendre. C'est sans importance pour un
correctif qui change un script, et c'est du gaspillage pour un développeur qui
réempaquette une vidéo une dizaine de fois : le `.game` peut finir par faire
plusieurs fois la taille de son contenu. Aussi `--patch` regarde l'espace libre
une fois la transaction validée et, quand il y en a au moins un mégaoctet,
compacte la base de données et indique combien il a récupéré :
----
>>> Recovered 8007680 bytes of space the old contents were using
----
Le résultat a, à l'octet près, la taille d'un empaquetage neuf des mêmes
fichiers. Le compactage réécrit le fichier entier, il est donc sauté quand il y
a peu à gagner. Rien d'autre dans Singe n'en a besoin ; un jeu n'est jamais écrit
que par l'empaqueteur.
Le dossier de données d'un jeu empaqueté porte le nom de la base de données,
puis celui du dossier du script à l'intérieur : `MyGame/` pour un `MyGame.game`
dont le script se trouve à la racine, sous le dossier de données
(`data/MyGame/` par défaut), là même où l'a le `MyGame/MyGame.singe` non
empaqueté, si bien que les sauvegardes et les réglages sont partagés entre une
installation non empaquetée et une installation empaquetée ; un script empaqueté
dans un sous-dossier (`Game/Main.singe`) obtient `MyGame/Game/`. L'index de la
vidéo va là aussi.
NOTE: Les versions 3.00 antérieures donnaient à un jeu empaqueté dont le script
se trouvait dans un sous-dossier le dossier de données `Game/` sous la base, que
partageait chaque archive organisée de cette façon. Les sauvegardes d'un tel jeu
ont désormais leur place sous `MyGame/Game/` ; déplacez-les à la main. Les
versions 3.00 antérieures gardaient aussi le dossier propre du jeu, celui sous
lequel son `games.dat` nomme le script, si bien qu'un `MyGame.game` empaqueté
depuis `MyGame/` écrivait dans `MyGame/MyGame/` ; remontez d'un niveau ce qui
s'y trouve.
Quand Singe lance un jeu depuis une base de données, le menu passe
`CONTAINER` dans l'entrée de `games.dat` qu'il lance ; ce champ est réglé par le
menu, jamais à la main.
Pour vos propres données, `require("sqlite3")` donne aux scripts la liaison
https://lua.sqlite.org[lsqlite3] vers le même SQLite que celui qu'utilise le
moteur. Gardez les bases de données que le jeu écrit dans
`singeGetDataPath()` :
[source,lua]
----
local sqlite3 = require("sqlite3")
local db = sqlite3.open(singeGetDataPath() .. "scores.db")
db:exec("CREATE TABLE IF NOT EXISTS scores (name TEXT, score INTEGER)")
for row in db:nrows("SELECT name, score FROM scores ORDER BY score DESC LIMIT 10") do
print(row.name, row.score)
end
db:close()
----
`sqlite3.open` trouve un nom comme le fait `io.open`, si bien qu'une base de
données peut être livrée dans le jeu -- tables de niveaux, dialogues, listes
d'objets -- et s'ouvrir quand même quand le jeu est empaqueté. Ouverte en lecture
seule, une base de données empaquetée est lue là où elle se trouve, sans rien
copier :
[source,lua]
----
local levels = sqlite3.open(DIR .. "levels.db", sqlite3.OPEN_READONLY)
----
Ouverte en écriture, ce que fait par défaut `sqlite3.open`, une base de données
empaquetée est copiée une fois dans le dossier de données du jeu, sous `files/`
comme tout ce qu'écrit un jeu empaqueté, et c'est la copie qui est ouverte. À
partir de là, le nom désigne la copie, en lecture seule ou non, et l'original
empaqueté n'est jamais modifié. La base de données d'un jeu non empaqueté s'ouvre
sur place exactement comme avant. `testScripts/packedSql` est le test ; son
en-tête dit comment le lancer.
Le format de fichier, pour qui écrit des outils : trois tables.
`meta(key, value)` contient `version` (1), `gamedir` (le nom du dossier
empaqueté), `chunk` (la taille des blocs en octets, 4194304) et `packer`.
`assets(path, name, size, data)` a une ligne par fichier, `path` étant la clé en
minuscules avec des barres obliques et `name` l'orthographe de l'auteur ; les
fichiers qui ne dépassent pas la taille d'un bloc sont dans `data`, les plus gros
ont `data` à NULL et vivent dans `chunks(path, chunk, data)` sous forme de
morceaux numérotés consécutifs. Singe ouvre la base de données en lecture seule
et immuable, si bien qu'il tourne depuis un support en lecture seule et n'écrit
jamais de journal à côté.
[#_event_driven_or_not]
=== Piloté par les événements... ou pas ?
Traditionnellement, Singe utilisait un modèle de programmation piloté par les
événements -- Singe s'occupe de tout et n'appelle votre code que lorsqu'il a
besoin de vous dire quelque chose ou de mettre à jour l'écran. C'est efficace et
c'est ainsi que fonctionnent la plupart des moteurs de jeu basés sur Lua, mais
cela peut être verbeux. Depuis Singe 2.10, il existe un nouveau modèle _à fil
d'exécution_ (« threaded ») qui vous permet d'écrire du code procédural.
[#_event_driven]
==== Piloté par les événements
Avec le modèle de programmation piloté par les événements, Singe contrôle la
boucle principale du programme et décide de l'ordre d'exécution. Singe gère
automatiquement tous les détails du décodage et de la présentation de la vidéo
et du son. Il gère les manettes, les souris et le clavier. Quand Singe a besoin
de quelque chose de propre au jeu, il appelle une partie de votre script. Le
script Singe le plus simple qui montre tous les callbacks (fonctions de rappel)
existants ressemble à ceci :
[source,lua]
----
-- Squelette de jeu Singe.
-- Charge le framework de Singe.
dofile("Singe/Framework.singe")
-- Déclarez ici les variables globales dont vous avez besoin.
function onControllerMoved(axis, value, which)
--[[
Indique quel axe de manette a bougé, ainsi que sa valeur actuelle.
(Plage : -32768 à 32767.) Sert pour les périphériques analogiques. Les
entrées numériques sont gérées par onInput et onKey.
--]]
end
function onInputPressed(what, device)
--[[
En MODE_NORMAL du clavier, les événements d'entrée sont signalés ici quand
la touche ou le bouton est enfoncé. Pour la liste complète des touches,
boutons et manettes, voir Singe/Framework.singe. En MODE_FULL, cet événement
est appelé avec le keysym de la touche enfoncée. Quand le contact vient d'une
souris ou d'un pistolet optique, device est l'index de ce périphérique (0 pour
le premier), tel que le passe Hypseus, si bien qu'un jeu à deux pistolets peut
distinguer ses joueurs ; pour une touche ou une manette, il vaut nil.
--]]
end
function onInputReleased(what, device)
--[[
En MODE_NORMAL du clavier, les événements d'entrée sont signalés ici
quand la touche ou le bouton est relâché. Pour la liste complète des
touches, boutons et manettes, voir Singe/Framework.singe. En MODE_FULL,
cet événement est appelé avec le keysym de la touche relâchée. device
est comme pour onInputPressed.
--]]
end
function onKeyPressed(key, scancode)
--[[
En MODE_FULL du clavier, les événements d'entrée sont signalés ici quand
la touche est enfoncée. Le keysym et le scancode sont tous deux renvoyés.
Pour la liste des scancodes disponibles, voir Singe/Framework.singe.
--]]
end
function onKeyReleased(key, scancode)
--[[
En MODE_FULL du clavier, les événements d'entrée sont signalés ici quand
la touche est relâchée. Le keysym et le scancode sont tous deux renvoyés.
Pour la liste des scancodes disponibles, voir Singe/Framework.singe.
--]]
end
function onMouseMoved(x, y, xr, yr, which)
--[[
Appelée quand la souris bouge. En mode SINGLE_MOUSE, les valeurs
absolues de X et Y ainsi que le changement relatif de position sont
renvoyés. En mode MANY_MOUSE, seul le changement relatif est disponible,
ainsi que la souris qui a bougé.
--]]
end
function onOverlayUpdate()
--[[
C'est le seul endroit où vous pouvez dessiner sans risque. Si vous
voulez afficher un curseur de visée, il faut enregistrer la position de
la souris reçue par onMouseMoved dans des variables globales, puis vous
en servir ici pour dessiner le curseur.
--]]
-- Indique à Singe si l'affichage a changé ou non.
return(OVERLAY_UPDATED) -- Ou OVERLAY_NOT_UPDATED si rien n'a été dessiné.
end
function onShutdown()
-- Appelée quand l'utilisateur quitte votre jeu. Libérez ici les ressources chargées.
end
function onSoundCompleted(channel)
-- Le son sur « channel » vient de finir de jouer.
end
-- Remarque : il n'y a pas d'événement « onStartup ».
-- Tout code de démarrage nécessaire peut être placé ici.
----
[#threaded]
==== À fil d'exécution
Depuis Singe 2.10, vous pouvez utiliser le modèle de programmation procédural
classique. Pour utiliser ce modèle, déclarez une fonction `singeMain()` et
incluez le framework de Singe à la *fin* de votre programme plutôt qu'au début.
Lua n'est pas vraiment multithread, si bien que ce modèle compte sur le
développeur du jeu pour faire du multitâche coopératif en appelant
`singeYield()` partout où le code consomme un temps notable. Comme avec le
modèle piloté par les événements, Singe gère toujours toutes les entrées, le
décodage de la vidéo et le décodage du son. Un exemple de programme à fil
d'exécution ressemble à ceci :
[source,lua]
----
function singeMain()
local x = 10
local y = 10
while true do
colorBackground(0, 0, 0, 255)
overlayClear()
colorForeground(255, 255, 255, 255)
overlayPrint(x, y, "+")
if keyboardGetLastUp() == SCANCODE.LEFT.value then x = x - 1 end
if keyboardGetLastUp() == SCANCODE.RIGHT.value then x = x + 1 end
if keyboardGetLastUp() == SCANCODE.UP.value then y = y - 1 end
if keyboardGetLastUp() == SCANCODE.DOWN.value then y = y + 1 end
singeYield()
end
end
dofile("Singe/Framework.singe")
----
[#_hybrid]
==== Hybride
Les deux modèles se combinent librement. Quand `singeMain` existe,
`Framework.singe` installe son propre `onOverlayUpdate`, mais tous les autres
callbacks sont toujours appelés : définissez `onInputPressed`,
`onSoundCompleted` ou `onShutdown` à côté de `singeMain` et ils sont appelés
entre les suspensions de la coroutine. Un découpage typique garde le
déroulement du jeu procédural tout en réagissant aux événements d'entrée et de
son :
[source,lua]
----
local fired = false
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
fired = true
end
end
function onSoundCompleted(channel)
if channel == gunshotChannel then
soundPlay(reloadSound)
end
end
function singeMain()
while true do
if fired then
fired = false
gunshotChannel = soundPlay(gunshotSound)
end
singeYield()
end
end
dofile("Singe/Framework.singe")
----
Ne définissez pas `onOverlayUpdate` vous-même dans ce modèle ; dessinez plutôt
depuis `singeMain`. Une erreur levée dans `singeMain` termine le jeu avec une
trace de pile, et revenir de `singeMain` quitte proprement.
[#pausing]
=== La pause
Chaque jeu dispose d'office d'une pause qui fonctionne. Tant que la touche de
pause est activée (c'est le cas par défaut), appuyer sur la touche affectée à
`INPUT_PAUSE` fait que le moteur met en pause le disque, chaque vidéo chargée et
chaque son, dessine un indicateur PAUSED par-dessus la dernière image et cesse
d'exécuter le script : pas de `onOverlayUpdate`, pas de reprise de `singeMain`,
pas de callbacks d'entrée, de souris, de manette ni de son. Les minuteries
construites sur des bibliothèques Lua s'arrêtent avec lui, parce que plus rien
ne les fait tourner. Appuyer de nouveau sur la touche reprend tout ce qui était
en cours de lecture et le script continue là où il s'était arrêté. Seuls les
contacts propres au moteur (pause, quitter, capture d'écran, capture de la
souris) fonctionnent encore pendant le gel.
Les entrées restent fidèles à la réalité pendant le gel. Tout ce que le script
croit maintenu enfoncé est relâché avec `onInputReleased` (ou `onKeyReleased` en
`MODE_FULL`) quand la pause commence, et tout ce qui est encore physiquement
maintenu quand la pause se termine est de nouveau enfoncé. Un joystick maintenu
pendant une pause ne reste jamais bloqué.
Un jeu qui veut son propre comportement de pause (un menu de pause, un gel de
l'histoire que le joueur ne doit pas pouvoir sauter) appelle
`singeSetPauseKeyEnabled(false)`. La touche est alors transmise au script sous la
forme de `SWITCH_PAUSE` et le jeu pilote lui-même `singeSetPauseFlag`, qui met
les médias en pause sans geler le script.
En `MODE_FULL`, le clavier appartient entièrement au jeu, donc les touches
affectées aux contacts du moteur n'y font rien (un `p` tapé dans une saisie de
meilleur score doit rester un `p`). Les boutons de manette et de souris affectés
à ces contacts continuent de fonctionner dans les deux modes, puisqu'on ne peut
pas les taper.
[#gamesdat]
=== games.dat
Le fichier `games.dat` permet à Singe de trouver automatiquement les nouveaux
jeux quand l'utilisateur final les installe. Ce fichier est extrêmement
important et doit être inclus avec chaque jeu Singe. Placez `games.dat` dans le
dossier le plus haut de votre jeu. Voici un exemple qui contient plusieurs jeux :
[source,lua]
----
GAMES = {
{
TITLE = "Asteroid Patrol",
SCRIPT = "Collection/AsteroidPatrol.singe",
VIDEO = "Collection/frame_AsteroidPatrol.txt",
STRETCH = false,
NO_MOUSE = false,
RESOLUTION_X = 720,
RESOLUTION_Y = 480,
SINDEN_GUN = "",
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10",
CABINET = "Collection/cabinet_AsteroidPatrol.png",
MARQUEE = "Collection/marquee_Collection.png",
ATTRACT = "Collection/video_AsteroidPatrol.mkv",
ATTRACT_START = 3000,
ATTRACT_END = 3500,
YEAR = 2026,
PLATFORM = "Laserdisc",
DEVELOPER = "Your Studio",
PUBLISHER = "Your Studio",
GENRE = "Shooter",
DESCRIPTION = "Keep the asteroids away from the station for as long as you can.",
CREATOR = "Scott Duensing",
SOURCE = "http://kangaroopunch.com"
},
-- ... d'autres entrées ...
}
----
Les clés `SCRIPT`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`, `NO_MOUSE`,
`RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `AUDIO_DELAY`,
`AUDIO_SUFFIX`, `GAME_ID`, `LEGACY_SPRITE_ARGS`, `REQUIRES` et `TIER` sont lues
par le moteur quand le menu (ou votre propre script, par `scriptExecute` /
`scriptPush`) lance l'entrée ; elles l'emportent sur la ligne de commande. La
ligne `VIDEO` est le disque : une entrée qui en nomme un est un jeu laserdisc,
et une entrée qui l'omet ou la laisse vide se lance sans disque et peut régler
`CANVAS_X` / `CANVAS_Y` (720x480 par défaut) ; voir
<<withoutadisc,Jeux sans disque>>. `AUDIO_SUFFIX = "-es"` joue
`<base>-es.ogg`, à côté de la vidéo du disque, au lieu du son qu'elle contient,
pour une édition dont les autres langues sont livrées dans des fichiers séparés,
ce que fait aussi `--altaudio`. `LEGACY_SPRITE_ARGS = true` lance un jeu écrit
pour Singe 2.10 avec l'ancien ordre des arguments des sprites (voir
<<migrating,Migrer depuis Singe 2.10>>). Les autres clés sont lues par le menu
pour l'affichage.
`REQUIRES` indique ce dont le jeu a besoin de la part d'une machine, sous forme
de liste de noms : `"SCENE"` (la scène 3D), `"GUI"` (les documents GUI),
`"SHADOWS"`, `"POST"` (le bloom et les autres effets qui veulent des cibles de
rendu en virgule flottante), `"SKIN"` (les modèles avec skinning) et `"MORPH"`
(les morph targets). Sans `REQUIRES`, le jeu est en 2D. Le menu signale un
jeu que cette machine ne peut pas faire tourner et dit pourquoi au lieu de le
lancer, et vous écrivez rarement la liste vous-même : le moteur remarque ce
qu'un jeu utilise pendant qu'il tourne, signale dans la trace quand le jeu
utilise quelque chose que `REQUIRES` ne liste pas, et `--pack` écrit la liste
dans une entrée qui n'en a pas. `TIER` (0 à 3) indique la puissance de machine
que demande le jeu ; le menu mesure le niveau de cette machine à partir de son
propre décor de fond et signale un jeu qui en demande plus comme pouvant tourner
lentement. Voir <<singecapabilities,singeCapabilities>>.
[source,lua]
----
REQUIRES = { "SCENE", "SHADOWS" },
TIER = 1,
----
Un script lancé directement, par son nom ou par son dossier, reçoit le même
traitement : Singe cherche un `games.dat` dans le dossier du script et jusqu'à
trois dossiers au-dessus, et si le `SCRIPT` d'une entrée nomme le script lancé
(les chemins des entrées partent du dossier situé au-dessus du `games.dat`, si
bien que `Collection/games.dat` nomme `Collection/AsteroidPatrol.singe`), les
clés de cette entrée s'appliquent. La seule différence avec un lancement depuis
le menu est la priorité : une option tapée sur la ligne de commande
(`--framefile`, `--disc`, `--stretch`, `--nomouse`, `--xresolution`,
`--yresolution`, `--sindengun`, `--audio`, `--audiodelay`, `--altaudio`,
`--canvas`) garde sa valeur, puisque la personne qui l'a tapée le voulait. Une
valeur venue d'un fichier de réglages, non : c'est une valeur par défaut, et
l'entrée décrit mieux le jeu (voir
xref:Manual.adoc#settings[Le fichier de réglages]). Un `games.dat` qui ne peut
pas s'exécuter est signalé et ignoré lors d'un lancement direct.
[#withoutadisc]
=== Jeux sans disque
Singe ne suppose plus de laserdisc. Un jeu n'est un jeu laserdisc que lorsqu'il
en nomme un : un `VIDEO` dans son entrée de `games.dat`, ou `--disc` (ou un
`--framefile`) sur la ligne de commande. Tout le reste tourne sans disque, et le
monde est un *canevas* au lieu d'une image de la vidéo : `CANVAS_X` /
`CANVAS_Y` dans `games.dat` ou `--canvas=WxH` sur la ligne de commande, 720x480
par défaut, si bien qu'un jeu sans disque ressemble à un jeu laserdisc en
définition standard, sauf s'il demande autre chose.
Sans disque, le moteur dessine sur du noir, l'overlay fait par défaut la moitié
du canevas comme d'habitude, et les fonctions `disc*` et `vldp*` deviennent
inoffensives : `discGetState` renvoie `DISC_EJECTED`, `discGetFrame` renvoie
`0`, `discGetWidth` / `discGetHeight` renvoient la taille du canevas,
`vldpGetPixel` renvoie du noir, et les appels de transport écrivent dans la trace
et ne font rien. Tout le reste -- l'overlay, les sprites, les polices, les sons,
les vidéos supplémentaires par `videoLoad`, les entrées et les deux modèles de
programmation -- est identique. La variable globale `SINGE_DISC` indique à un
script en tant que quel type de jeu il tourne.
[#gui]
=== GUI
Les menus, les pages d'options, les HUD et la saisie des meilleurs scores se
construisaient autrefois avec `fontPrint`, `overlayBox` et un curseur fait
maison. Singe 3 ajoute une GUI : un document écrit en RML (un balisage proche du
HTML) et mis en forme par RCSS (une feuille de style proche du CSS), mis en page
et dessiné par RmlUi, avec retour à la ligne du texte, flexbox, panneaux en neuf
tranches tirés de vos propres illustrations, transitions, animations, contrôles
de formulaire et un modèle de focus qu'une manette peut piloter. Une *GUI* est
un contexte RmlUi de taille fixe rendu dans sa propre texture, redessiné chaque
fois que son image change (voir <<guirefresh,guiRefresh>>), et cette texture va
partout où une texture peut aller : composée par-dessus l'overlay avec
`guiDraw`, ou affichée sur un matériau de la scène 3D avec `materialSetGui`, si
bien que la même page d'options fonctionne à plat sur l'écran et sur le moniteur
d'une borne d'arcade modélisée. Une GUI a besoin du périphérique GPU qu'utilise
la scène 3D, elle n'est donc pas disponible sur une machine qui n'en a pas (voir
<<sceneperformance,Performances et configuration requise>>) ; `guiNew` met fin
au script au lieu de répondre false, alors interrogez d'abord
<<singehasgpu,singeHasGpu>> si votre jeu peut s'en passer. Le menu fourni fait
exactement cela, et se dessine à la place dans l'overlay quand la réponse est
non (voir xref:Manual.adoc#menurenderers[Les deux moteurs de rendu]). Chaque
appel cité ici a une entrée dans la section <<guiref,Gui>> de la référence.
La référence des langages RML et RCSS est la documentation de RmlUi elle-même,
sur https://mikke89.github.io/RmlUiDoc/, et elle mérite d'être lue avant le
premier document ; ce chapitre couvre ce que Singe ajoute par-dessus.
[#_documents_and_style_sheets]
==== Documents et feuilles de style
`guiNew(width, height)` crée une GUI et renvoie un handle entier ; il peut en
exister jusqu'à seize à la fois. `guiLoad(gui, name)` y charge un document RML,
l'affiche et renvoie un handle de document (jusqu'à trente-deux par GUI) ;
`guiHide`, `guiShow` et `guiClose` font ce que disent leurs noms, et `guiDelete`
supprime la GUI, ses documents et sa texture. Un document a généralement la
taille de sa GUI, et une GUI a généralement la taille de l'overlay pour une page
plein écran, ou celle du panneau pour un élément de HUD :
[source,lua]
----
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local doc = guiLoad(gui, DIR .. "options.rml")
----
Le nom du document passe par le système de fichiers virtuel comme celui d'un
sprite, il peut donc être un fichier non empaqueté sur le disque ou se trouver
dans un jeu empaqueté, et il est résolu relativement au dossier depuis lequel
Singe a été lancé : ajoutez donc `DIR` devant pour les fichiers livrés avec
votre jeu, exactement comme pour `spriteLoad`. Les noms *à l'intérieur* d'un
document (le `href` d'une feuille de style, une image dans un décorateur, le
`src` d'une planche de sprites) sont résolus relativement au fichier qui les
nomme, comme le fait RmlUi, puis par la même recherche, si bien qu'un document,
sa feuille de style et ses images se trouvent côte à côte dans le dossier du jeu
et qu'un jeu empaqueté les trouve tous. Deux sortes de noms sont pris tels
quels : les fichiers propres au moteur sous `Singe/`, et les chemins absolus.
La même règle lue dans l'autre sens permet à un document qui se trouve sous
`Singe/` d'atteindre les fichiers d'un jeu : un nom relatif est résolu depuis le
dossier du document lui-même, donc depuis `Singe/Menu.rml` le nom
`../Collection/cabinet.png` est `Collection/cabinet.png` dans le dossier du jeu,
et un nom sous `../MyGame.game/` est un fichier dans ce jeu empaqueté. Le menu
fourni fait exactement cela pour les illustrations de borne et de fronton du jeu
sélectionné, en réglant depuis Lua un décorateur d'image sur le panneau
d'illustration pour que l'image y soit ajustée quelle que soit sa taille :
[source,lua]
----
-- Menu.singe : ajuste l'image d'un jeu dans l'un des panneaux d'illustration.
function menuArt(id, file)
local path = tostring(file or "")
if path:sub(1, 6):lower() ~= "singe/" then
path = "../" .. path
end
menuElement(id).style.decorator = 'image("' .. path .. '" contain)'
end
----
Un document RML est un `<head>` avec un titre, des liens vers des feuilles de
style ou un `<style>` en ligne, et un `<body>` d'éléments. En voici un minimal,
qui utilise le thème fourni :
[source,html]
----
<rml>
<head>
<title>Options</title>
<link type="text/rcss" href="Singe/gui.rcss"/>
<style>
body { width: 100%; height: 100%; }
.panel { margin: 30dp; }
</style>
</head>
<body id="options">
<div class="panel">
<h1>Options</h1>
<p class="muted" id="status">Choose a difficulty and press Start.</p>
<button id="go">Start</button>
</div>
</body>
</rml>
----
RmlUi ne dessine rien dont on ne lui a pas donné la taille, et chaque élément est
`display: inline` tant qu'un style ne dit pas autre chose, donc une feuille de
style n'est pas facultative. `Singe/gui.rcss` est le thème que livre le moteur :
il donne au corps, aux titres et aux paragraphes une taille et une couleur, fait
de `div` un bloc, et met en forme chaque contrôle de formulaire. Liez-le en
premier et redéfinissez ce que vous voulez dans une feuille à vous ou dans un
`<style>` en ligne ; la règle la plus tardive l'emporte, comme en CSS. Les
longueurs du thème sont en `dp`, le pixel indépendant de la densité de RmlUi,
qui vaut un pixel à la taille propre de la GUI.
Le texte a besoin d'une police. Le moteur charge `Singe/FreeSansBold.ttf` pour
chaque GUI comme police de *secours* (la police qui fournit tout glyphe
qu'aucune autre police n'a) ; son nom de famille est `FreeSans` et elle est en
gras, ce que demande la règle `body` du thème. `guiLoadFont(name)` ajoute l'une
des polices propres au jeu pour chaque GUI, et `guiLoadFont(name, true)` en fait
plutôt la police de secours. Un document désigne une police par le nom de
famille inscrit dans le fichier, pas par le nom du fichier, donc chargez les
polices avant les documents qui les utilisent :
[source,lua]
----
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
----
[source,css]
----
h1 { font-family: Orbitron; font-weight: bold; }
----
Les effets RCSS de RmlUi sont dessinés en entier. Les enfants d'un élément
arrondi ou transformé sont découpés selon sa forme (une liste qui défile avec
`border-radius` rejoint son cadre dans la courbe), `filter: opacity()` estompe
un groupe d'un seul bloc (la propriété `opacity` estompe chaque morceau
séparément, comme le définit RmlUi), `mask-image` découpe un élément selon
l'alpha d'une image, `box-shadow` projette des ombres dures ou douces, et
`filter` et `backdrop-filter` acceptent `blur`, `drop-shadow`, `brightness`,
`contrast`, `invert`, `grayscale`, `sepia`, `hue-rotate` et `saturate`. Les
décorateurs `linear-gradient`, `radial-gradient` et `conic-gradient` (répétés ou
non) sont dessinés comme tels ; les panneaux, les boutons et les barres de
progression du thème fourni s'en servent. Trois limites : le décorateur
personnalisé `shader` n'est pas pris en charge (il est refusé avec un
avertissement), un dégradé porte au plus seize arrêts de couleur (les suivants
sont abandonnés), et les effets s'imbriquent sur huit calques au plus (un
élément plus profond est dessiné sans son propre calque). Chaque effet est rendu
par un calque de la taille de la GUI, et un flou en plusieurs passes, donc sur un
Pi limitez-les à quelques éléments et surveillez <<guigetstats,guiGetStats>>,
qui compte les calques, les passes et les octets qu'une image a coûtés.
[#_theming]
==== Thèmes
Un thème est un dossier de RCSS et d'images, et un jeu livre son propre style
comme il livre des sprites. La forme habituelle est une feuille liée après
`Singe/gui.rcss` qui redéfinit le style des classes du thème et ajoute les
siennes, avec les illustrations à côté. Le thème fourni définit :
* `.panel` -- une boîte bordée, arrondie et translucide avec une marge
intérieure, le cadre d'une page ou d'une boîte de dialogue.
* `.list` -- une boîte qui défile, dont les éléments `div` enfants sont des
lignes : ils prennent le focus, sont mis en évidence au survol et au focus, et
une ligne qui a la classe `selected` reste mise en évidence. Une barre de
défilement verticale apparaît quand les lignes débordent.
* `.muted` -- un texte plus terne, pour les indications et les lignes d'état.
* Les contrôles de formulaire : `button` et `input.submit`, `input.text`,
`input.password` et `textarea`, `input.checkbox` et `input.radio`,
`input.range` avec sa piste et son curseur, `select` avec sa flèche et sa
liste déroulante, et `progress` avec son remplissage ; chacun avec des états
survol, focus et actif, et chacun pouvant recevoir le focus grâce à
`tab-index: auto`, pour qu'une manette puisse l'atteindre.
Un panneau tiré de vos propres illustrations est un décorateur en neuf tranches
appliqué à une planche de sprites, et les animations sont des images clés RCSS,
si bien qu'une barre de chargement qui se remplit toute seule n'a besoin
d'aucun script :
[source,css]
----
@spritesheet crate {
src: crate.png;
crate-outer: 0px 0px 40px 40px;
crate-inner: 10px 10px 20px 20px;
}
.crate {
display: block;
decorator: ninepatch(crate-outer, crate-inner, 1.0);
padding: 14dp;
}
@keyframes fill {
from { width: 0%; }
to { width: 100%; }
}
.bar fill {
animation: 3s cubic-in-out infinite alternate fill;
}
----
[#_scripting_a_document]
==== Piloter un document par script
Il y a deux API, et il vaut mieux savoir clairement laquelle est laquelle. Les
appels `gui*` à plat, dans le style propre à Singe, créent une GUI, chargent et
dessinent des documents, acheminent les entrées et atteignent les éléments
*par id* : `guiGetValue(gui, doc, id)` lit la valeur d'un contrôle de formulaire, ou
le RML intérieur de tout autre élément, sous forme de chaîne ;
`guiSetValue(gui, doc, id, value)` en écrit une ; et `guiSetHandler(gui, doc,
id, event, function)` appelle une fonction Lua quand l'élément déclenche un
événement (`"click"`, `"change"`, `"submit"` ou tout autre nom d'événement
RmlUi). La fonction reçoit `(gui, document, id, event, value)`, `value` étant la
valeur de l'élément sous forme de chaîne au moment de l'événement : le texte
d'un champ texte, la valeur d'une option de `select`, un intervalle dans le
format propre à RmlUi (`"48.000000"`), le RML intérieur d'un bouton. Tout ce
dont une page a besoin au quotidien est couvert par ces trois appels :
[source,lua]
----
guiSetHandler(gui, doc, "go", "click", function(g, d, id, event, value)
local name = guiGetValue(g, d, "name")
local hard = guiGetValue(g, d, "difficulty") == "hard"
startGame(name, hard)
end)
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
soundSetVolume(math.floor(tonumber(value)))
guiSetValue(g, d, "status", "Volume " .. math.floor(tonumber(value)))
end)
----
Tout le reste, dans un document, relève de l'API Lua de RmlUi, qui tourne sur
l'état Lua propre au jeu : un bloc `<script>` dans un document, ainsi que
`onclick` et les autres attributs `on*`, s'exécutent avec les variables globales
du jeu, si bien qu'ils peuvent appeler vos fonctions comme les appels `gui*`.
Côté script, les mêmes objets sont accessibles par la variable globale `rmlui` :
les contextes sont nommés `gui0`, `gui1`, ... d'après leurs handles, et les
documents sont indexés par l'identifiant `id` de leur `<body>`, si bien que le
bouton ci-dessus est
`rmlui.contexts["gui0"].documents["options"]:GetElementById("go")`, et que
`:DispatchEvent("click", {})` sur lui l'enfonce depuis le script (utile pour les
tests qui tournent sans souris). Les éléments, leurs attributs, classes, styles
et enfants, les liaisons `data-model` qui reflètent une table Lua dans un
document, et le reste du modèle objet sont documentés avec le plugin Lua de
RmlUi sur https://mikke89.github.io/RmlUiDoc/ ; `guiSetHandler` est le raccourci
par id, pas un remplaçant. Un document fermé par cette API (`document:Close()`)
est oublié par le moteur, et son handle lève ensuite une erreur.
[#_input]
==== Entrées
Une nouvelle GUI ne fait qu'afficher, ce qui convient à un HUD ou à une
enseigne ; `guiSetInput(gui, true)` lui fait prendre les entrées, et
`guiSetInput(gui, false)` les lui retire de nouveau. Pour une GUI qui prend les
entrées :
* *La souris et un pistolet optique* (qui est une souris) arrivent en
coordonnées de l'overlay, transformées par la bordure Sinden et l'échelle
d'overscan exactement comme pour `onMouseMoved`, en mode `MOUSE_SINGLE`. Le
moteur trouve la GUI qui se trouve sous le pointeur -- la dernière dessinée à
plat à cet endroit pendant cette image est au-dessus, et à défaut une surface
de la scène dont le matériau en affiche une (voir plus bas) -- et convertit la
position en pixels propres à cette GUI. Un appui sur un élément appartient à
la GUI ; les boutons gauche, droit et du milieu cliquent, et la molette fait
défiler.
* *Les touches* vont à chaque GUI qui prend les entrées, et une touche qu'un
élément ayant le focus a utilisée (un caractère dans un champ texte, une
flèche qui a déplacé le focus, Entrée sur un bouton) est consommée. Tant qu'un
champ texte ou mot de passe ou un `textarea` a le focus, le moteur active la
saisie de texte de SDL, si bien que le texte tapé arrive sous forme de texte,
en respectant la disposition du clavier et tout clavier à l'écran ou IME.
* *Les contacts de manette et de souris* (les affectations `SWITCH_*` du
framework depuis un bouton de manette ou de souris) sont traduits en touches
qu'un document comprend : les quatre directions deviennent les touches
fléchées, `INPUT_ACTION_1` devient Entrée et `INPUT_ACTION_2` Échap. Un contact affecté à
une touche du clavier atteint déjà la GUI sous la forme de cette touche.
La règle qui rend un HUD sûr est qu'*un événement qu'un élément a utilisé
n'atteint jamais le jeu* : une touche consommée n'arrive pas comme
`onInputPressed` ou `onKeyPressed`, un clic sur un bouton n'arrive pas comme un
contact de souris, une direction traduite qui a déplacé le focus n'arrive pas
comme `SWITCH_UP`. Tout ce qu'un document n'a pas utilisé passe aux callbacks
habituels, si bien qu'un HUD qui prend les entrées laisse quand même passer le
bouton de tir, et qu'une page où rien n'a le focus laisse passer toutes les
touches.
Le focus et la navigation sont l'affaire du document lui-même. Le thème fourni
donne à chaque contrôle et à chaque ligne de `.list` un `tab-index: auto`, si
bien que Tab passe de l'un à l'autre, qu'Entrée active le contrôle qui a le
focus, et que les touches fléchées se déplacent entre les contrôles grâce aux
propriétés `nav` de RmlUi (`nav-up`, `nav-down`, `nav-left`, `nav-right`, ou
`nav: auto` pour les quatre). Une page destinée à une borne avec un stick et
deux boutons les veut sur tout ce que le joueur doit pouvoir atteindre :
[source,css]
----
button, .list div { nav: auto; }
----
[#guiscene]
==== Une GUI dans la scène
`materialSetGui(material, gui)` affiche la texture d'une GUI comme texture de
couleur de base d'un matériau, mise à jour à chaque image, à côté de
`materialSetVideo` et de `materialSetView` : un moniteur, un tableau de bord de
cockpit, une enseigne. Associez-le à `materialSetUnlit` quand l'image ne doit
pas être ombrée, et laissez le matériau éclairé quand elle doit l'être. Rien
d'autre ne change ; le matériau est éclairé, mélangé et reflété comme n'importe
quel autre, et la même GUI peut être dessinée à plat par-dessus l'overlay
pendant la même image.
`sceneProbeGui(x, y)` indique quelle GUI se trouve sur la surface sous un point
de l'overlay, et à quel endroit : un rayon partant de la caméra et passant par
le point est lancé contre chaque maillage visible dont le matériau affiche une
GUI, et l'appel renvoie le handle et le pixel dans les coordonnées propres à
cette GUI, ou `nil` quand le point n'est sur aucune surface de ce genre. Le
moteur lui pose la même question quand la souris ou un pistolet optique vise la
scène et que rien de dessiné à plat ne se trouve sous le pointeur, si bien qu'un
clic sur le moniteur d'une borne en 3D appuie sur le bouton qui se trouve
dessous sans que le script intervienne ; l'appel est là pour un jeu qui veut le
savoir par lui-même, pour un viseur laser, un effet de survol ou un test.
[source,lua]
----
local screen = materialNew()
materialSetGui(screen, gui)
materialSetUnlit(screen, true)
nodeSetMesh(monitor, meshPlane(3.0, 2.0), screen)
nodeSetRotation(monitor, 90, 0, 0)
----
[#_reloading_performance_and_limits]
==== Rechargement, performances et limites
Une GUI appartient au script qui l'a créée. Quitter, `scriptPush`,
`scriptExecute` et un rechargement (voir
<<reloading,Recharger pendant que vous travaillez>>) détruisent chaque GUI et
chaque document avec le reste de l'état du script, et le script les recrée quand
il s'exécute ; rien du modèle objet `rmlui` ne survit, alors ne gardez pas ces
objets dans des variables globales qu'un rechargement réutiliserait.
Chaque GUI se met à jour à chaque image, qu'elle soit dessinée quelque part ou
non, si bien qu'un document continue de faire tourner ses animations et ses
scripts hors de l'écran, et qu'une GUI dont on n'a plus besoin doit être
supprimée plutôt que simplement cachée. Elle n'est redessinée dans sa texture
que lorsque son image change, si bien qu'une page ou un HUD immobile ne coûte
presque rien au GPU (voir <<guirefresh,guiRefresh>>). Le coût d'une image qui
change, c'est la taille de la texture et la complexité du document : sur un
ordinateur de bureau, une page plein écran ne coûte pas cher ; sur un Raspberry
Pi, gardez une GUI à la taille de l'overlay ou plus petite, et préférez une GUI
avec plusieurs documents à plusieurs GUI. Chaque police coûte un atlas par
taille à laquelle elle est utilisée, si bien qu'une page qui utilise une police
à trois tailles paie trois atlas ; une douzaine de tailles en coûte une
douzaine. Seize GUI peuvent exister à la fois, chacune avec trente-deux
documents, et seize appels à `guiDraw` sont pris en compte par image.
Ce qu'un document peut afficher, et ce qu'il ne peut pas :
* *Les images viennent de fichiers.* Les images, les planches de sprites et les
décorateurs d'image chargent du PNG et les autres formats que lit Singe,
depuis le disque ou depuis un jeu empaqueté. Un document ne peut pas afficher
un sprite de Singe, une vidéo, la scène 3D ni rien de ce qui est dessiné
pendant que le jeu tourne, et un script ne peut pas lui fournir de pixels.
L'inverse fonctionne : la texture d'une GUI va sur un matériau 3D avec
`materialSetGui`.
* *Décorateurs :* `image`, `text`, `tiled-horizontal`, `tiled-vertical`,
`tiled-box`, `ninepatch`, le `gradient` à deux couleurs (et ses noms
`horizontal-gradient` et `vertical-gradient`), et `linear-gradient`,
`radial-gradient` et `conic-gradient` avec leurs formes `repeating-`, avec au
plus seize arrêts de couleur. Le décorateur personnalisé `shader` est refusé.
* *Filtres :* `opacity`, `blur`, `drop-shadow`, `brightness`, `contrast`,
`invert`, `grayscale`, `sepia`, `hue-rotate` et `saturate`. Tout autre nom est
ignoré, avec un avertissement dans la trace.
* *Calques :* les filtres, une `opacity` inférieure à 1, les images de masque et
les ombres de boîte passent chacun par un calque de la taille de la GUI, et
huit au plus s'empilent à la fois ; un de plus est dessiné sans son calque, et
donc sans son effet.
* *Les images* de plus de 64 Mo sont refusées.
* *RML et RCSS* sont ceux de RmlUi, proches du HTML et du CSS sans être
identiques : il y a flexbox mais pas de grille CSS, et un document est piloté
par script en Lua, par `guiSetHandler` et l'objet `rmlui`, plutôt qu'en
JavaScript. La documentation de RmlUi fait autorité sur ce que contiennent ces
langages.
[#_a_worked_example]
==== Un exemple complet
Une page d'options par-dessus le disque : un panneau du thème fourni avec un
champ de nom, un curseur de volume, une liste de difficulté et deux boutons,
dessiné par-dessus l'overlay et piloté par la souris ou le stick. Le document,
`options.rml`, à côté du script :
[source,html]
----
<rml>
<head>
<title>Options</title>
<link type="text/rcss" href="Singe/gui.rcss"/>
<style>
body { width: 100%; height: 100%; }
.panel { margin: 30dp; }
.field { display: block; margin: 6dp 0; }
label { display: inline-block; width: 140dp; }
input.text { width: 300dp; }
button, input { nav: auto; }
#status { min-height: 30dp; }
</style>
</head>
<body id="options">
<div class="panel">
<h1>Options</h1>
<div class="field"><label>Name</label><input type="text" id="name" value="Dirk"/></div>
<div class="field"><label>Volume</label><input type="range" id="volume" min="0" max="63" step="1" value="48"/></div>
<div class="field"><label>Difficulty</label>
<select id="difficulty">
<option value="easy">Easy</option>
<option value="normal" selected>Normal</option>
<option value="hard">Hard</option>
</select>
</div>
<div class="field"><label>Attract</label><input type="checkbox" id="attract" checked/> plays the intro</div>
<div class="field"><button id="go">Start</button><button id="back">Back</button></div>
<p id="status" class="muted">Press Start when ready.</p>
</div>
</body>
</rml>
----
Le script le charge dans une GUI de la taille de l'overlay, branche les trois
éléments qui l'intéressent, et la dessine à chaque image jusqu'à ce que le joueur
appuie sur Start ; le disque joue en dessous pendant tout ce temps :
[source,lua]
----
dofile("Singe/Framework.singe")
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local doc = guiLoad(gui, DIR .. "options.rml")
local settings = { name = "Dirk", volume = 48, hard = false }
local inMenu = true
local function report(text)
guiSetValue(gui, doc, "status", text)
end
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
settings.volume = math.floor(tonumber(value))
soundSetVolume(settings.volume)
report("Volume " .. settings.volume)
end)
guiSetHandler(gui, doc, "difficulty", "change", function(g, d, id, event, value)
settings.hard = (value == "hard")
report("Difficulty " .. value)
end)
guiSetHandler(gui, doc, "go", "click", function(g, d)
settings.name = guiGetValue(g, d, "name")
guiHide(g, d)
guiSetInput(g, false)
inMenu = false
discPlay()
end)
guiSetHandler(gui, doc, "back", "click", function()
singeQuit()
end)
function onInputPressed(what)
-- Seul ce que la page n'a pas utilisé arrive ici : rien tant qu'elle a le
-- focus, tout une fois qu'elle est cachée.
if not inMenu and what == SWITCH_BUTTON1 then
fire()
end
end
function onOverlayUpdate()
overlayClear()
if inMenu then
guiDraw(gui)
else
fontPrint(10, 10, settings.name .. " " .. (settings.hard and "HARD" or "NORMAL"))
end
return OVERLAY_UPDATED
end
----
La même page sur l'écran d'une borne modélisée, ce sont les cinq lignes de
<<guiscene,Une GUI dans la scène>> à la place de `guiDraw`, et un clic sur le
moniteur appuie sur le bouton qui se trouve dessous.
[#subtitles]
=== Sous-titres
Un jeu peut livrer un fichier de sous-titres SubRip et demander à Singe de l'afficher par-dessus le disque pendant la lecture. Trois appels font tout le travail : <<srtload,`srtLoad`>> lit le fichier, <<srtenable,`srtEnable`>> l'active, et <<srtposition,`srtPosition`>> indique où se placent les lignes. <<srtclear,`srtClear`>> retire ce qui est affiché. La fonctionnalité vient d'Hypseus Singe, et les quatre appels sont les siens.
[source,lua]
----
if srtLoad("subs/english.srt") then
srtPosition(80)
srtEnable(true)
end
----
[#_the_file_format]
==== Le format de fichier
SubRip (`.srt`) est un fichier texte brut de sous-titres numérotés :
----
1
00:00:04,120 --> 00:00:06,880
Stay away from the caves.
2
00:00:07,000 --> 00:00:09,500
I mean it -- the caves
are where they nest.
----
Chaque sous-titre est un numéro seul sur sa ligne, puis une ligne contenant deux horodatages séparés par `-->`, puis une ou plusieurs lignes de texte, puis une ligne vide. Un horodatage s'écrit `HH:MM:SS,mmm` : les heures, les minutes et les secondes séparées par des deux-points, et les millisecondes après une virgule. Le numéro du sous-titre est lu puis jeté, si bien que les numéros n'ont pas besoin d'être dans l'ordre, ni même d'être justes.
L'analyseur est volontairement tolérant aux endroits où un fichier modifié à la main tourne mal. Les retours chariot et les espaces au début et à la fin sont retirés de chaque ligne, si bien qu'un fichier écrit sous Windows se charge tel quel. Un sous-titre dont la ligne de temps n'a pas de `-->`, ou dont les horodatages ne s'analysent pas, est sauté et le chargement du fichier continue. Un sous-titre sans texte est sauté. Les lignes d'un sous-titre sont jointes par des retours à la ligne et affichées telles qu'elles sont écrites, et `<`, `>` et `&` arrivent à l'écran tels quels au lieu d'être pris pour du balisage.
Ce qui n'est *pas* pris en charge : les coordonnées de positionnement que certains outils ajoutent à la ligne de temps sont ignorées (`srtPosition` décide de la hauteur), et les balises `<i>` et `<b>` qu'on trouve parfois dans SubRip sont affichées comme du texte au lieu d'être appliquées. Les sous-titres peuvent se chevaucher dans le temps ; le dernier qui a commencé l'emporte.
`srtLoad` répond `false` et ne charge rien quand le fichier ne peut pas être lu, quand rien dedans ne s'analyse, ou quand il n'y a pas de disque -- si bien qu'un fichier cassé est un `false` sur lequel un jeu peut agir, et non un plantage. Il affiche dans chaque cas un avertissement qui nomme le fichier.
[#_the_timing]
==== La synchronisation
Les horodatages sont convertis en *numéros d'image du disque* au chargement du fichier, d'après la fréquence d'images du disque chargé à ce moment-là, et le moteur compare chaque image dessinée à ces numéros. Il en découle deux choses.
Premièrement, **un sous-titre survit à un saut.** Envoyer le disque n'importe où -- `discSearch`, `discSkipForward`, un pas, un framefile qui passe dans un autre segment -- affiche le sous-titre qui couvre l'endroit où il a atterri, à la prochaine image dessinée, en avant comme en arrière. Rien n'a besoin d'être rechargé et aucune horloge n'a besoin d'être remise à zéro.
Deuxièmement, **le fichier doit être synchronisé sur le disque, pas sur l'horloge.** Un `.srt` découpé pour une vidéo à une autre fréquence d'images tombe au mauvais endroit, et un fichier chargé avant l'ouverture du disque ne se charge pas du tout. Chargez les sous-titres après le disque, ce qui pour un jeu ordinaire veut dire depuis le corps du script.
Un sous-titre est affiché tant que l'image du disque se trouve entre ses deux images, incluses. L'affichage n'est redessiné que lorsque le sous-titre change, si bien que les sous-titres ne coûtent rien tant qu'une ligne reste affichée.
[#_styling]
==== Mise en forme
Les lignes sont un document RmlUi, pas une ligne de texte copiée par blit, si bien qu'elles se mettent en forme comme une GUI. Deux fichiers sont livrés dans le dossier `Singe` et extraits à côté des autres au premier lancement :
* `Singe/subtitle.rml` -- le document. Il a un seul élément, `#slot`, dans lequel le moteur écrit le sous-titre en cours ou le bandeau.
* `Singe/subtitle.rcss` -- l'apparence. `.cue` est le bloc qui porte la hauteur demandée par `srtPosition`, sous forme d'un `top` en ligne ; `.cue .text` est la barre de texte qu'il contient, qui par défaut est blanche sur un panneau sombre arrondi qui épouse le texte au lieu de couvrir toute l'image.
Tous deux sont réécrits à chaque démarrage, comme tout ce que contient le dossier `Singe`, si bien que les modifier ne change rien bien longtemps. Pour changer le style, un jeu fournit ses propres copies : un `subtitle.rml` à côté de son script, dont le lien renvoie à une feuille à lui (un `subtitle.rcss` à côté, par exemple), et Singe dessine ce document à la place du sien ; la trace du programme le signale. Le moteur ne règle jamais que le contenu de `#slot` et le `top` de `.cue`, si bien que tout le reste -- la police, la taille, les couleurs, le panneau, une bordure, une ombre -- relève de la feuille du jeu et survit à une mise à jour du moteur. Le document est dessiné par-dessus l'image, au-dessus de l'overlay et sous les GUI en espace écran, suit le rectangle de la vidéo partout où le placent `--scalefactor`, `--shiftx`, une découpe de bezel ou `--rotate`, et ne prend aucune entrée de souris, si bien qu'un clic le traverse directement.
<<overlaybanner,`overlayBanner`>> utilise le même document, si bien qu'un bandeau et un sous-titre se ressemblent, et qu'un jeu qui change le style de l'un change celui des deux.
Sur une machine sans GPU, il n'y a aucune GUI ; `srtLoad` analyse quand même le fichier et répond honnêtement, et rien n'est dessiné. Un avertissement le signale une fois.
[#scenes3d]
=== Scènes 3D
Singe 3 dessine une scène 3D entre la vidéo du laserdisc et l'overlay 2D. Tout
ce que vous savez déjà reste valable : les sprites, le texte et l'overlay se
placent par-dessus la scène, et la vidéo apparaît partout où le fond de la scène
est transparent. Un jeu qui n'appelle jamais de fonction 3D n'est pas concerné,
et un jeu sans disque (voir <<withoutadisc,Jeux sans disque>>) peut être
carrément un jeu 3D.
Les sections qui suivent construisent une scène dans l'ordre où vous en écrirez
une : les nœuds, puis les maillages et les matériaux, les lumières, la caméra,
l'aspect de l'image dans son ensemble, les modèles, et enfin ce qui se place
par-dessus tout cela. Chaque appel cité ici a une entrée dans la
<<reference,Référence de l'API Lua>>.
[#_what_a_scene_is_made_of]
==== De quoi est faite une scène
Tout ce qui se trouve dans une scène est un *nœud* d'un seul et même arbre, et le
nœud `0` en est la racine. Un nœud a une position, une rotation et une échelle
relatives à son parent, et il peut porter une seule chose : un maillage avec un
matériau, une lumière, un sprite ou une ligne de texte. Un modèle chargé devient
un sous-arbre de nœuds sous une seule racine, si bien que les mêmes appels
déplacent aussi bien un personnage modélisé qu'un cube créé par script.
La scène est en repère direct, +Y vers le haut et -Z vers l'avant, comme glTF et
l'export de Blender. Les unités sont celles qu'utilisent vos modèles, et les
primitives prennent les mêmes unités ; la physique (voir <<physics,Physique>>)
suppose des mètres. Les rotations sont en degrés, données sous forme de trois
angles d'Euler appliqués comme lacet, tangage et roulis, ou sous forme de
quaternion quand la valeur vient d'un modèle. L'avant propre d'un nœud est son
-Z : la caméra regarde dans cette direction, les lumières directionnelles et les
projecteurs éclairent dans cette direction, et `nodeLookAt` le tourne vers un
point du monde.
Il y a deux façons de remplir une scène, qui se combinent librement :
* *La construire par script* avec les maillages primitifs (`meshBox`,
`meshSphere`, `meshPlane`, `meshCylinder`, `meshCone`, `meshTorus`,
`meshHeightmap`) ou vos propres données de sommets (`meshNew`), plus des
matériaux, des lumières et une caméra.
* *Charger des modèles* au format binaire glTF 2.0 (`.glb`) avec `modelLoad`,
puis les placer avec `modelInstance` autant de fois que vous voulez. Blender,
Maya, 3ds Max et tous les grands moteurs exportent en `.glb`.
Une scène minimale, c'est un maillage, une lumière et une caméra :
[source,lua]
----
sceneEnable(true)
sceneSetBackground(0, 0, 0, 0) -- Transparent : le disque apparaît derrière la scène.
local red = materialNew()
materialSetColor(red, 220, 40, 40)
local cube = nodeNew()
nodeSetMesh(cube, meshBox(1, 1, 1), red)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 2, 4, 3)
nodeLookAt(sun, 0, 0, 0)
local camera = nodeNew()
nodeSetPosition(camera, 0, 1.5, 5)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
function onOverlayUpdate()
nodeRotate(cube, 0, 1, 0) -- Un degré par image autour de son propre Y.
return OVERLAY_UPDATED
end
----
`sceneEnable(true)` est l'appel que fait tout jeu 3D, et sur une machine qui ne
sait pas dessiner en 3D, c'est l'appel qui échoue (voir
<<sceneperformance,Performances et configuration requise>>). Sans caméra, la
scène est vue depuis `(0, 0, 5)` en regardant l'origine, et sans appel de fond,
le fond de la scène est noir opaque.
[#nodes]
==== Les nœuds
La transformation d'un nœud se règle d'un coup (`nodeSetPosition`,
`nodeSetRotation`, `nodeSetQuaternion`, `nodeSetScale`) ou par petites touches
(`nodeMove`, `nodeRotate`), toujours dans l'espace de son parent.
`nodeSetParent` raccroche un nœud ailleurs en gardant sa transformation locale ;
`nodeGetWorldPosition` lit où il a fini une fois que chaque parent a eu son mot
à dire. `nodeSetVisible` cache un nœud et tout ce qui se trouve en dessous, et
`nodeDelete` le libère avec ses enfants, ainsi que tout corps, émetteur ou son
qui leur est attaché.
Les noms viennent des modèles : `nodeFind(name, root)` trouve une articulation
ou une pièce par le nom qu'elle avait dans le fichier, sous l'instance donnée
comme `root`. Un nœud créé par script peut aussi recevoir un nom
(`nodeSetName`), et c'est là que cherche `nodeFind` sans racine.
[source,lua]
----
-- Une tourelle sur un char : le canon est un enfant de la tourelle, la tourelle de la caisse,
-- si bien que tourner la tourelle entraîne le canon et que déplacer la caisse entraîne les deux.
local hull = nodeNew()
local turret = nodeNew()
local barrel = nodeNew()
nodeSetMesh(hull, meshBox(2, 0.8, 3.2), olive)
nodeSetMesh(turret, meshCylinder(0.7, 0.5, 16), olive)
nodeSetMesh(barrel, meshCylinder(0.1, 1.8, 8), steel)
nodeSetParent(turret, hull)
nodeSetParent(barrel, turret)
nodeSetPosition(turret, 0, 0.65, 0)
nodeSetPosition(barrel, 0, 0, -1.2)
nodeSetRotation(barrel, 90, 0, 0) -- Couche le cylindre le long de -Z.
function onOverlayUpdate()
local stick = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_X) / 32767 -- De -1 à 1.
nodeRotate(turret, 0, stick * 2, 0)
return OVERLAY_NOT_UPDATED
end
----
Deux appels font le pont entre la scène et l'overlay. `sceneProject(x, y, z)`
transforme un point du monde en coordonnées de l'overlay, si bien qu'une
étiquette de nom ou une barre de vie dessinée avec `fontPrint` peut suivre un
objet 3D. `sceneUnproject(sx, sy, distance)` fait l'inverse : il donne le point
du monde situé à cette distance sur le rayon qui passe par un pixel de
l'overlay, ce qui, avec `physicsRaycast`, permet la sélection à la souris.
[source,lua]
----
function onMouseMoved(x, y, dx, dy, which)
-- La souris pointe le long d'un rayon partant de la caméra ; ce qu'il touche est la cible.
local cx, cy, cz = nodeGetWorldPosition(camera)
local fx, fy, fz = sceneUnproject(x, y, 50)
local hit = physicsRaycast(cx, cy, cz, fx - cx, fy - cy, fz - cz, 50)
highlighted = hit
end
----
[#_meshes_and_materials]
==== Maillages et matériaux
Un maillage est une géométrie ; un matériau est son apparence ; un nœud réunit
l'un et l'autre (`nodeSetMesh(node, mesh, material)`). Partagez-les librement :
un seul `meshBox` et un seul matériau peuvent se trouver sur mille nœuds, et
c'est ce partage qui rend le moteur de rendu rapide (voir
<<sceneperformance,Performances et configuration requise>>). `meshNew` construit
un maillage à partir de tables de positions, de normales, de coordonnées de
texture et d'indices quand les primitives ne suffisent pas.
Un matériau commence par une couleur de base avec alpha (`materialSetColor`) et
l'aspect métal-rugosité de glTF : `materialSetMetallic` et
`materialSetRoughness`, de `0` à `1`. Une rugosité proche de `0` donne un reflet
net comme dans un miroir, et `1` une surface mate ; un métal teinte ses reflets
et ses réflexions de sa propre couleur et n'a pas de couleur diffuse à lui.
Au-delà :
* `materialSetEmissive` fait émettre à une surface une lumière de sa propre
couleur (un écran, une lampe, du métal chauffé) ; elle n'éclaire pas les autres
objets, mais elle produit du bloom (voir <<scenelook,L'aspect de l'image>>).
* `materialSetUnlit` affiche la couleur telle quelle, en ignorant toutes les
lumières : éléments de HUD dans la scène, lueur propre au verre d'une ampoule.
* `materialSetDoubleSided` dessine l'arrière des formes ouvertes, comme un plan
vu d'en dessous ou un drapeau.
* `materialSetBlend` fait compter l'alpha, pour le verre, les fantômes et l'eau.
Les maillages mélangés sont dessinés après tout ce qui est opaque, triés par
distance, et ne projettent pas d'ombre.
* `materialSetCutoff` ne garde que les texels dont l'alpha atteint le seuil et
jette le reste, et c'est ainsi que se dessinent les feuilles, les clôtures, les
grilles et le grillage : un seul quad, presque entièrement retiré.
Contrairement au mélange, il n'a besoin d'aucun tri et projette quand même une
ombre, et l'ombre est découpée elle aussi, si bien qu'un arbre projette des
ombres de feuilles plutôt que l'ombre de son quad. Ce qui passe le seuil est
opaque : un texel à moitié transparent est soit gardé entier, soit jeté, jamais
mélangé, si bien que rien de ce qui se trouve derrière la scène (l'image du
disque, surtout) ne teinte une feuille.
Un seuil de 0 (la valeur par défaut) désactive le masquage. Un modèle chargé
depuis glTF apporte le sien : `MASK` règle le seuil d'après le fichier,
`BLEND` active le mélange, et `OPAQUE` ne fait ni l'un ni l'autre.
La texture d'un matériau peut être l'image de n'importe quel sprite chargé
(`materialSetTexture`), le laserdisc lui-même ou une vidéo chargée
(`materialSetVideo`), l'image d'une seconde caméra (`materialSetView`, voir
<<scenecamera,La caméra et les vues>>), ou un document GUI (`materialSetGui`,
voir <<gui,GUI>>). Une borne d'arcade modélisée peut lire le disque sur son
propre écran, ou y afficher sa page d'options. Les coordonnées de texture
viennent du maillage ; les primitives appliquent une image une fois sur chaque
face, et `materialSetTiling` la répète, ce que demandent les sols et les murs.
Les textures ont des mipmaps et un filtrage anisotrope, si bien qu'un sol reste
net jusqu'au loin ; `materialSetFilter(material, FILTER_NEAREST)` garde au
contraire le pixel art bien carré.
[source,lua]
----
local stone = materialNew()
materialSetTexture(stone, spriteLoad(DIR .. "flagstones.png"))
materialSetTiling(stone, 12, 12) -- Douze répétitions sur le plan.
materialSetRoughness(stone, 0.9)
local floor = nodeNew()
nodeSetMesh(floor, meshPlane(24, 24), stone)
local glass = materialNew()
materialSetColor(glass, 180, 220, 255, 90) -- Alpha de 90 sur 255.
materialSetBlend(glass, true)
materialSetRoughness(glass, 0.05)
----
Quatre autres cartes donnent à une surface un détail que sa géométrie n'a pas,
et un modèle chargé les apporte avec lui quand son fichier les contient :
* `materialSetNormalMap` infléchit l'éclairage avec une carte de normales en
espace tangent, si bien que les briques ressortent d'un mur plat. Une intensité
supérieure à `1` exagère l'effet.
* `materialSetOcclusionMap` assombrit les recoins que la lumière ambiante et le
ciel ne peuvent pas atteindre.
* `materialSetMetallicRoughnessMap` fait varier le caractère métallique et la
rugosité sur la surface, rangés à la manière de glTF (la rugosité dans le
vert, le métallique dans le bleu) et multipliés par les facteurs propres du
matériau, alors réglez ceux-ci à `1` quand la carte doit décider.
* `materialSetEmissiveMap` indique quelles parties brillent, teintées et mises à
l'échelle par `materialSetEmissive`.
[source,lua]
----
local brick = materialNew()
materialSetTexture(brick, spriteLoad(DIR .. "brick.png"))
materialSetNormalMap(brick, spriteLoad(DIR .. "brickNormal.png"), 1.5)
materialSetMetallicRoughnessMap(brick, spriteLoad(DIR .. "brickMetalRough.png"))
materialSetMetallic(brick, 1)
materialSetRoughness(brick, 1)
materialSetEmissiveMap(brick, spriteLoad(DIR .. "brickEmissive.png"))
materialSetEmissive(brick, 180, 100, 40) -- Le mortier brille en orange.
----
.Textures compressées
Un PNG est décodé à quatre octets par texel sur le GPU, plus encore un tiers
pour ses mipmaps ; un niveau de la taille de Sponza contient ainsi 390 Mo de
textures, plus qu'un Raspberry Pi 4 n'en partage avec son affichage. Les
textures livrées en fichiers KTX2 (Basis Universal, créés par
`util/compressTextures.py` à partir de PNG ou d'un `.gltf` entier) restent
compressées sur le GPU, au quart de la taille ou moins, dans le format de blocs
dont dispose la machine : BC7 sur les ordinateurs de bureau, ASTC sur les GPU
Apple et le Pi 4, ETC2 sur toute puce OpenGL ES 3 (le Mali de chaque console
portable, le Pi sous GLES), BC3 sur les cartes plus anciennes, et du RGBA simple
là où rien d'autre n'existe, le tout à partir d'un seul fichier. Donnez un nom
`.ktx2` à la place d'un sprite à `materialSetTexture` et aux appels de cartes,
et un modèle dont le fichier utilise `KHR_texture_basisu` les charge de
lui-même. `sceneGetStats` indique la mémoire de textures utilisée, pour que vous
puissiez voir la différence.
[source,lua]
----
materialSetTexture(ground, DIR .. "flagstones.ktx2")
local draws, drawn, calls, textureKB = sceneGetStats()
debugPrint("textures: " .. textureKB .. " KB")
----
[#_lights_and_shadows]
==== Lumières et ombres
Jusqu'à huit lumières éclairent à la fois, et chacune est un nœud, si bien que
les lumières se rattachent à un parent et s'animent comme n'importe quoi
d'autre :
* `LIGHT_DIRECTIONAL` est un soleil : seule sa direction compte, et tout est
éclairé de la même façon. Une intensité de `1` éclaire en blanc une surface
blanche tournée vers lui.
* `LIGHT_POINT` est une ampoule, qui faiblit avec la distance, et
`lightSetRange` l'éteint complètement au-delà d'une distance. Son intensité est
sa luminosité à l'ampoule elle-même, la moitié à une unité de distance, si bien
qu'une ampoule qui doit éclairer une pièce a besoin d'un nombre de l'ordre de quelques dizaines.
* `LIGHT_SPOT` est un cône avec un angle intérieur entièrement éclairé et un
angle extérieur où la lumière s'est éteinte (`lightSetCone`).
`sceneSetAmbient` ajoute un peu de lumière venant de partout pour que les ombres
ne soient pas noires ; un ciel le fait mieux (voir
<<scenelook,L'aspect de l'image>>). Les couleurs données de `0` à `255` sont en
sRGB, comme tout le reste dans Singe, et sont converties en lumière linéaire à
l'entrée, si bien qu'une lumière de `128, 128, 128` est quatre fois moins
lumineuse que le blanc, et non deux fois.
Toute lumière peut projeter des ombres aux bords doux à partir de tout ce qui est
opaque (`lightSetShadow`). Les lumières directionnelles et les projecteurs
projettent par une carte d'ombres ; les lumières ponctuelles par une carte
cubique qui couvre toutes les directions, si bien qu'une ampoule peut se trouver
à l'intérieur de ce qu'elle éclaire (une pièce, un abat-jour, la borne) : les
maillages fermés projettent aussi depuis leurs faces arrière, et la plage de
profondeur de l'ombre est ajustée aux objets qui font de l'ombre autour de la
lumière. `nodeSetShadow(node, false)` dispense un maillage de projeter une
ombre : le verre de l'ampoule elle-même, une enseigne lumineuse, une étiquette de
nom. `sceneSetShadowSize` règle la résolution de la carte (`256` à `4096`,
`1024` par défaut) et échange de la netteté contre de la vitesse.
L'ombre d'une lumière directionnelle est découpée en cascades le long de la vue
(`sceneSetShadowCascades`, trois par défaut), chacune étant une carte ajustée à
une tranche de ce que voit la caméra, si bien que les ombres restent nettes de
près dans un grand niveau et portent quand même jusqu'à
`sceneSetShadowDistance`. Une seule cascade ajuste une carte unique à toute la
scène, ce qui convient aux petites scènes et aux caméras orthographiques. Une
vue (`viewNew`) dessine avec les mêmes cartes, en prenant pour chaque point la
cascade la plus fine qui le couvre, si bien qu'un moniteur montre les ombres que
montre la fenêtre.
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 30, 40, 20)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 2.5)
lightSetColor(sun, 255, 240, 220)
lightSetShadow(sun, true)
sceneSetShadowSize(2048)
sceneSetShadowDistance(80) -- Trois cascades jusqu'à 80 unités.
local lamp = lightNew(LIGHT_POINT)
nodeSetPosition(lamp, 3, 3.1, -2)
lightSetColor(lamp, 255, 230, 190)
lightSetIntensity(lamp, 12)
lightSetRange(lamp, 14)
lightSetShadow(lamp, true) -- Une carte cubique : six passes par image.
nodeSetShadow(bulbGlass, false) -- Le verre qui l'entoure ne projette rien.
----
[#scenecamera]
==== La caméra et les vues
N'importe quel nœud peut être la caméra (`cameraSet`) ; elle regarde le long de
son propre -Z, si bien que `nodeLookAt` la vise, et l'accrocher sous un nœud en
mouvement donne une caméra de poursuite.
`cameraSetPerspective(fovDegrees, near, far)` est l'objectif par défaut ;
`cameraSetOrthographic(height, near, far)` dessine sans perspective, pour les
schémas, les scènes façon HUD et les jeux vus de profil. Gardez `near` aussi
grand que la scène le permet, parce que la précision du tampon de profondeur se
répartit entre `near` et `far`.
[source,lua]
----
local camera = nodeNew()
nodeSetParent(camera, car)
nodeSetPosition(camera, 0, 2.5, 7) -- Derrière et au-dessus de la voiture, dans son espace.
nodeLookAt(camera, 0, 1, 0)
cameraSet(camera)
cameraSetPerspective(55, 0.1, 300)
----
Une *vue* est une seconde caméra rendue dans une texture à chaque image et
affichée sur un matériau : un moniteur de surveillance, un rétroviseur, un
portail, une image dans l'image. `viewNew(width, height)` en crée une à cette
résolution, `viewSetCamera` lui donne un nœud caméra, et `materialSetView` met
son image sur n'importe quel maillage. Les vues partagent les ombres de l'image
et sautent le bloom, et chacune rend de nouveau la scène, alors gardez-les peu
nombreuses et petites. Chaque vue tourne les billboards (voir
<<scenesprites,Sprites et texte dans la scène>>) vers sa propre caméra et dessine
les matériaux transparents de l'arrière vers l'avant depuis celle-ci, si bien
qu'un moniteur voit les étiquettes de nom de face et le verre dans le bon ordre.
[source,lua]
----
local rearCamera = nodeNew()
nodeSetParent(rearCamera, car)
nodeSetPosition(rearCamera, 0, 1.2, -2)
nodeSetRotation(rearCamera, 0, 180, 0) -- Regarde vers l'arrière.
local rear = viewNew(384, 256)
viewSetCamera(rear, rearCamera)
local mirror = materialNew()
materialSetView(mirror, rear)
materialSetUnlit(mirror, true)
local panel = nodeNew()
nodeSetParent(panel, car)
nodeSetMesh(panel, meshPlane(0.6, 0.2), mirror)
nodeSetPosition(panel, 0, 1.6, 0.5)
nodeSetRotation(panel, 90, 0, 0) -- Le plan regarde vers le haut ; on le redresse face au conducteur.
----
[#scenelook]
==== L'aspect de l'image
La scène est éclairée en lumière linéaire. Les couleurs que vous donnez de `0` à
`255` sont en sRGB et sont décodées à l'entrée, les textures sont décodées par le
GPU au moment où elles sont échantillonnées, les lumières s'additionnent sans
écrêtage, et à la fin de l'image une courbe de tons ramène le résultat vers
l'écran. Ce que cela veut dire en pratique :
* `sceneSetExposure(stops)` éclaircit ou assombrit toute l'image comme
l'exposition d'un appareil photo : `+1` double la lumière, `-1` la divise par
deux, si bien qu'une grotte et une plage peuvent partager les mêmes lumières et
les mêmes matériaux.
* `sceneSetTonemap` choisit la courbe : `TONEMAP_NEUTRAL` (par défaut, qui garde
les couleurs là où les visionneuses glTF les gardent et ne compresse que la
lumière la plus vive), `TONEMAP_ACES` (un rendu cinéma avec plus de contraste),
ou `TONEMAP_NONE` (écrêtage au blanc).
* `sceneSetBloom(threshold, strength)` fait briller tout ce qui est plus lumineux
que le seuil : le soleil à travers une fenêtre, une ampoule, une enseigne
émissive, une flamme. La lueur est prise dans la lumière de la scène avant la
courbe de tons, si bien que seul ce qui est vraiment lumineux produit du
bloom ; un seuil de `1` correspond au blanc.
.Le ciel et sa lumière
`sceneSetSky(file)` enveloppe la scène dans un panorama équirectangulaire. Un
`.hdr` Radiance garde toute sa plage ; un PNG ou un JPEG fonctionne aussi. Le
ciel est dessiné derrière tout le reste et, sauf avec
`sceneSetEnvironment(false)`, il éclaire aussi la scène : sa couleur globale,
venue de toutes les directions, remplace l'ambiance uniforme, et son reflet
apparaît sur les métaux et les surfaces brillantes, net sur un miroir et de plus
en plus flou à mesure que la rugosité augmente. Sans ciel, les métaux n'ont rien
à refléter, et seules l'ambiance et les lumières les modèlent.
`sceneSetSkyIntensity` règle ensemble le ciel et sa lumière, si bien qu'un seul
panorama sert pour midi et pour le crépuscule.
.Brouillard
`sceneSetFog(r, g, b, near, far)` estompe tout, particules comprises, vers une
couleur entre deux distances. Le ciel n'est jamais pris dans le brouillard, alors
accordez la couleur du brouillard à l'horizon du ciel et le bord lointain de la
scène s'y fondra.
[source,lua]
----
sceneSetSky(DIR .. "harbor.hdr")
sceneSetSkyIntensity(0.8)
sceneSetFog(190, 200, 215, 40, 160) -- La couleur du ciel à l'horizon.
sceneSetExposure(-0.5) -- Un peu plus sombre que ne le rendrait le soleil.
sceneSetBloom(1.0, 0.35) -- Seul ce qui est plus lumineux que le blanc brille.
----
[#_models_and_animation]
==== Modèles et animation
`modelLoad` prend un `.glb` d'après le même nom relatif au jeu que prend
`spriteLoad`, et un `.game` empaqueté l'emporte avec lui. Le fichier doit être
autonome : maillages, skins, animations et textures, tout dans ce seul fichier,
ce qui est l'export par défaut de Blender. Un modèle qui fait référence à un
`.bin` ou à une image situés à côté de lui est refusé avec un message qui nomme
le fichier, pour la même raison qu'un jeu empaqueté doit être autonome. Les
matériaux utilisent le modèle métal-rugosité avec
`KHR_materials_emissive_strength` et `KHR_texture_basisu` ; une texture adressée par un second jeu de coordonnées
de texture lit le premier jeu, et `KHR_texture_transform` est ignoré, chacun avec
une ligne dans la trace du programme. Un skin peut avoir jusqu'à `128`
articulations. Le chargement est la partie coûteuse ; `modelInstance` place
ensuite une copie de l'arbre de nœuds du modèle, autant de fois que vous voulez,
et chaque instance partage les maillages et les matériaux du fichier.
`modelDelete` libère le modèle ; les instances gardent leurs nœuds, nus.
[source,lua]
----
local foxModel = modelLoad(DIR .. "Fox.glb")
local pack = {}
for i = 1, 6 do
local fox = modelInstance(foxModel)
nodeSetScale(fox, 0.02) -- Le fichier est en centimètres.
nodeSetPosition(fox, i * 1.5 - 5, 0, 0)
pack[i] = fox
end
----
Les animations d'un modèle sont listées par `modelGetAnimations` et pilotées
instance par instance avec `animationPlay(node, nameOrIndex, loop, speed, fade)`
sur le nœud racine de l'instance, plus des appels de pause, de reprise, d'arrêt
et de saut. L'animation de nœuds (des objets qui bougent), le skinning (des
personnages qui se déforment) et les morph targets (blend shapes : visages,
synchronisation labiale, muscles qui gonflent) fonctionnent tous. Les animations
avancent en temps réel et s'arrêtent pendant que le jeu est en pause. Un poids de
morphing peut aussi être réglé par script avec `nodeSetMorph`, par nom ou par
numéro, sur le nœud qui porte le maillage.
Un `fade` en secondes fait un fondu enchaîné depuis ce qui jouait, si bien
qu'une marche devient une course sans à-coup. Quand rien ne jouait (après
`animationStop`, ou quand un ragdoll vient d'être désactivé), le fondu part de la
pose dans laquelle se trouvent les nœuds, si bien qu'un personnage se relève en
douceur de là où il est tombé. Jusqu'à quatre clips de plus peuvent jouer en même
temps sur des couches au-dessus de la base (`animationPlayLayer`), chacun mélangé
selon `animationSetLayerWeight` et limité à une partie du squelette par
`animationSetLayerMask` : un regard alentour sur le cou pendant que les jambes
marchent, un salut d'un bras pendant une course.
[source,lua]
----
animationPlay(fox, "Walk", true)
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5) -- La tête tourne, mélangée en une demi-seconde
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox)) -- ... sur le cou et au-dessus seulement.
function onCollision(a, b, x, y, z, speed)
if (a == fox or b == fox) and speed > 3 then
animationPlay(fox, "Run", true, 1.2, 0.4) -- Plus vite, en fondu enchaîné de 0.4 s.
end
end
----
[#_sound_in_the_scene]
==== Le son dans la scène
Un effet sonore peut être placé dans la scène pour qu'on l'entende depuis
l'endroit où il se trouve. `soundSetPosition(channel, x, y, z)` place un canal en
cours de lecture en un point du monde, `soundSetNode(channel, node)` lui fait
plutôt suivre un nœud, et `soundSetRange(channel, near, far)` indique où il est à
plein volume et où il s'est éteint. Les sons sont entendus depuis la caméra sauf
si `soundSetListener` désigne un autre nœud (la tête du joueur, pour un jeu à la
troisième personne), et ils se répartissent à gauche et à droite quand
l'auditeur tourne. Un canal qui n'est pas placé dans la scène joue comme il l'a
toujours fait, avec `soundSetPan` pour la stéréo.
[source,lua]
----
local crackle = soundPlay(fireSound, -1) -- Boucle sans fin.
soundSetNode(crackle, brazier)
soundSetRange(crackle, 2, 18) -- Plein volume dans un rayon de 2 unités, plus rien à 18.
soundSetListener(hero)
----
[#scenesprites]
==== Sprites et texte dans la scène
N'importe quel nœud peut afficher une image au lieu d'un maillage. `nodeSetSprite(node, sprite,
height)` place un sprite chargé (chacune des images d'un sprite animé, au choix de
`nodeSetSpriteFrame`) sur un quad haut d'autant d'unités du monde, éclairé comme
le reste et projetant des ombres, sauf si `nodeSetShadow` en décide autrement.
`nodeSetText(node, text, height)` fait de même avec une ligne de texte dans la
police et la couleur sélectionnées, sans éclairage. Pour le reste du moteur, les
deux sont des maillages : ils se rattachent à un parent, changent d'échelle et
acceptent le mélange d'un matériau.
`nodeSetBillboard` tourne un nœud face à la caméra à chaque image :
`BILLBOARD_ALL` complètement (étiquettes de nom, lueurs, imposteurs),
`BILLBOARD_Y` autour de son propre axe vertical seulement (arbres, barres de vie,
tout ce qui doit rester debout), et `BILLBOARD_NONE` pour arrêter. Cela
fonctionne aussi sur les maillages.
[source,lua]
----
local tag = nodeNew()
nodeSetParent(tag, fox)
nodeSetPosition(tag, 0, 110, 0) -- Dans les centimètres du Fox, au-dessus de sa tête.
nodeSetScale(tag, 50) -- Annule l'échelle de 0.02 de l'instance.
nodeSetText(tag, "FOX", 0.6)
nodeSetBillboard(tag, BILLBOARD_ALL)
nodeSetShadow(tag, false)
local icon = nodeNew()
nodeSetParent(icon, fox)
nodeSetPosition(icon, 0, 150, 0)
nodeSetScale(icon, 50)
nodeSetSprite(icon, spriteLoad(DIR .. "crate.png"), 0.5)
nodeSetBillboard(icon, BILLBOARD_Y)
----
[#scenefront]
==== Devant l'overlay
La scène est normalement dessinée sous l'overlay, si bien que tout ce qui est
dessiné sur l'overlay la recouvre. `nodeSetLayer(node, LAYER_FRONT)` place
plutôt un nœud, et tout ce qui lui est rattaché, dans la couche de premier plan :
la même caméra le dessine après l'overlay, éclairé par les mêmes
lumières et fini par la même exposition, la même courbe de tons et le même bloom,
si bien qu'un logo qui tourne, un titre en 3D ou un modèle sur un tableau des
meilleurs scores peut se dresser par-dessus des illustrations 2D. La couche de
premier plan est une image à part, effacée en transparent, si bien que l'overlay
apparaît partout où elle ne dessine rien, et rien dans la scène derrière
l'overlay ne peut la cacher. Les GUI et les particules dessinées par-dessus
l'overlay passent toujours au-dessus d'elle.
Les nœuds de premier plan projettent toujours des ombres sur la scène. Le ciel,
les particules 3D et les lignes de débogage restent dans la scène, et les vues
(`viewNew`) ne dessinent que les nœuds de la scène. La couche coûte une seconde
passe de rendu, et seulement pour les images où elle contient quelque chose.
[source,lua]
----
-- Une pièce d'or qui tourne par-dessus l'écran-titre 2D.
local coin = nodeNew()
nodeSetMesh(coin, meshCylinder(1, 0.2, 48), gold)
nodeSetRotation(coin, 90, 0, 0)
nodeSetLayer(coin, LAYER_FRONT)
function onOverlayUpdate()
overlayClear()
spriteDraw(titleArt, 0, 0)
nodeRotate(coin, 0, 2, 0)
return OVERLAY_UPDATED
end
----
[#sceneterrain]
==== Le terrain
`meshHeightmap(image, sizeX, sizeY, sizeZ)` transforme une image en niveaux de
gris en terrain vallonné : un sommet par pixel, le noir en bas et le blanc à
`sizeY` de haut, avec des normales tirées des pentes et des coordonnées de
texture sur l'ensemble. Donnez à son matériau une texture et `materialSetTiling`
pour la répéter toutes les quelques unités. Les images peuvent faire jusqu'à
1025 pixels dans chaque sens ; une carte de 257 pixels sur 120 unités donne un
sommet toutes les demi-unités, ce qui suffit largement pour des collines.
Un corps `BODY_STATIC` avec `SHAPE_MESH` sur ce nœud devient un champ de
hauteur, bien moins coûteux que ne le seraient les triangles, si bien que les
voitures y roulent et que les joueurs y marchent tel quel.
`terrainGetHeight(node, x, z)` indique la hauteur du sol en un point, pour y
placer des choses sans lancer de rayon.
[source,lua]
----
local ground = materialNew()
materialSetTexture(ground, DIR .. "grass.ktx2")
materialSetTiling(ground, 24, 24)
local hills = nodeNew()
nodeSetMesh(hills, meshHeightmap(DIR .. "hills.png", 120, 14, 120), ground)
bodyNew(hills, BODY_STATIC, SHAPE_MESH) -- Un champ de hauteur.
for i = 1, 20 do
local x, z = math.random(-50, 50), math.random(-50, 50)
local tree = modelInstance(treeModel)
nodeSetPosition(tree, x, terrainGetHeight(hills, x, z), z)
end
----
[#_navigation]
==== La navigation
Un maillage de navigation indique aux personnages où ils peuvent marcher. `navNew(radius,
height, maxSlope, maxStep)` en crée un pour une taille d'agent donnée, `navAddNode`
lui fournit la géométrie du niveau (la racine d'une instance de modèle fournit
tout le modèle), et `navBuild` précalcule la surface praticable : les sols, les
rampes et les escaliers que l'agent peut gravir, moins une marge autour de chaque
mur et de chaque colonne. Précalculer Sponza prend un dixième de seconde sur un
ordinateur de bureau ; `navSave` et `navLoad` gardent un maillage précalculé dans
le dossier de données du jeu pour les machines où cela prend plus longtemps, et
un maillage livré avec le jeu se charge de la même façon. `navDraw` affiche le
maillage par-dessus la scène pendant que vous mettez au point les paramètres.
Les requêtes répondent ensuite aux questions que pose un jeu : `navPath` renvoie
les angles du plus court chemin entre deux points, `navNearest` ramène un point
sur le maillage, `navRaycast` indique si une marche en ligne droite est bloquée,
et `navRandomPoint` choisit un endroit vers lequel errer.
Les agents (`navAgentNew`) parcourent le maillage pour vous. `navAgentMoveTo` en
envoie un quelque part, et il contourne les autres agents en chemin, en déplaçant
son nœud (ou en pilotant un contrôleur de joueur posé dessus, avec
`navAgentSetPlayer`) et en appelant `onNavArrived` quand il arrive.
`navAgentGetVelocity` vous dit s'il faut jouer la marche ou la course.
[source,lua]
----
local nav = navNew(0.4, 1.6, 45, 0.3) -- Un agent de 0.8 de large et 1.6 de haut, qui gravit 0.3.
navAddNode(nav, level)
navBuild(nav)
local guards = {}
for i = 1, 6 do
local guard = modelInstance(guardModel)
local x, y, z = navRandomPoint(nav)
nodeSetPosition(guard, x, y, z)
animationPlay(guard, "Walk", true)
local agent = navAgentNew(nav, guard, 0.35, 1.6, 1.4)
navAgentMoveTo(agent, navRandomPoint(nav))
guards[agent] = guard
end
function onNavArrived(agent)
navAgentMoveTo(agent, navRandomPoint(nav)) -- En route vers l'endroit suivant.
end
----
[#sceneperformance]
==== Performances et configuration requise
La scène a besoin d'un GPU qui parle Vulkan (Linux), Direct3D 12 (Windows 10 et
versions ultérieures), Metal (macOS) ou OpenGL ES 3.1. Le backend ES est ce qui
fait entrer dans le jeu les consoles portables Linux bon marché et le Raspberry
Pi, dont les puces Mali et VideoCore ont un pilote GLES mûr et soit pas de
Vulkan du tout, soit un Vulkan immature ; il est choisi automatiquement quand
aucun des trois autres n'est disponible, et il dessine la même image à partir
des mêmes shaders. Le Raspberry Pi 4 reste le Pi minimum pour la 3D : le Pi 3
n'a qu'OpenGL ES 2.0, qui ne peut exprimer ni le skinning ni les morph targets
de la scène. Sur une machine qui n'a aucun d'eux, les jeux 2D tournent
exactement comme avant (physique 2D comprise) et le premier appel 3D termine le
jeu avec une erreur qui nomme le problème.
Les maillages hors du champ de la caméra sont sautés, et les copies d'un même
maillage avec le même matériau sont dessinées en un seul appel instancié, si bien
qu'une forêt faite d'un seul et même arbre coûte à peu près ce que coûte un
arbre. `sceneGetStats` indique les dessins collectés, combien étaient dans le
champ, combien d'appels de dessin cela a pris, et la mémoire de textures
utilisée. Pour l'aider :
* Partagez les maillages et les matériaux entre les nœuds (un seul `meshBox`,
beaucoup de nœuds ; un seul modèle, beaucoup d'appels à `modelInstance`) plutôt
que d'en créer un par objet.
* Les maillages avec skinning ou morphing et les maillages mélangés sont dessinés
un par un ; limitez-les à ce dont la scène a besoin.
* L'anticrénelage (multiéchantillonnage 4x) est actif partout où le GPU le
propose ; `sceneSetAntialias(false)` fait regagner de la vitesse sur un Pi,
tout comme un `sceneSetShadowSize` plus petit et moins de lumières ponctuelles
projetant des ombres, chacune coûtant six passes.
* Surveillez le nombre de triangles et la taille des textures sur les petites
cartes, livrez des textures KTX2, et chargez un modèle une fois plutôt que
deux.
[source,lua]
----
function onOverlayUpdate()
local draws, drawn, calls, textureKB = sceneGetStats()
overlayClear()
local text = string.format("%d/%d meshes, %d calls, %d KB", drawn, draws, calls, textureKB)
fontPrint(10, 10, text)
return OVERLAY_UPDATED
end
----
[#physics]
=== Physique
Les corps, les articulations, les déclencheurs et les lancers de rayons vivent
dans la scène 3D et reposent sur Jolt Physics. Un corps s'attache à un nœud
comme un maillage, et dès lors le nœud et le corps ne font qu'un : la forme du
corps est dimensionnée en unités du monde et mise à l'échelle par le nœud, et
quel que soit celui des deux qui bouge, l'autre suit. Les unités sont les
mètres, les kilogrammes et les secondes, et la gravité vaut par défaut
`0, -9.81, 0`. La physique n'a pas besoin de GPU ; voir <<physics2d,Jeux 2D>>.
[#_bodies]
==== Corps
`bodyNew(node, type, shape, a, b, c)` donne à un nœud un corps de l'un de trois
types :
* Un corps *statique* (`BODY_STATIC`) reste là où était le nœud quand le corps
a été créé : sols, murs, la borne. Déplacer son nœud ensuite ne déplace pas
le corps.
* Un corps *cinématique* (`BODY_KINEMATIC`) suit son nœud partout où le script
ou une animation le déplace, et écarte les corps dynamiques de son chemin
sans être poussé lui-même : une raquette, une plateforme mobile, la main d'un
personnage.
* Un corps *dynamique* (`BODY_DYNAMIC`) est déplacé par la simulation et
entraîne son nœud : il tombe, rebondit, glisse et se fait pousser. Ne
déplacez pas son nœud vous-même ; donnez-lui plutôt des vitesses, des forces
ou des impulsions (`bodySetVelocity`, `bodyApplyForce`, `bodyApplyImpulse`).
La forme est une boîte, une sphère, une capsule ou un cylindre dimensionné par
`a`, `b` et `c` (`SHAPE_BOX` prend la largeur, la hauteur et la profondeur ;
`SHAPE_SPHERE` un rayon ; `SHAPE_CAPSULE` et `SHAPE_CYLINDER` un rayon et une
hauteur le long de Y), une enveloppe convexe du maillage du nœud (`SHAPE_HULL`,
tout ce qui se trouve sous le nœud, si bien que la racine d'une instance de
modèle convient), ou les triangles exacts du maillage (`SHAPE_MESH`, corps
statiques et cinématiques seulement ; sur un maillage de carte de hauteur,
c'est un champ de hauteurs, voir <<sceneterrain,Le terrain>>).
[source,lua]
----
floor = nodeNew()
nodeSetMesh(floor, meshBox(14, 0.2, 14), stone)
nodeSetPosition(floor, 0, -1.9, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 14, 0.2, 14)
for i = 1, 6 do
local crate = nodeNew()
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
nodeSetPosition(crate, -1, -1.5 + (i - 1) * 0.62, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
bodySetMass(crate, 5)
end
paddle = nodeNew()
nodeSetMesh(paddle, meshBox(0.3, 1.2, 1.6), red)
nodeSetPosition(paddle, -4, -1.2, 0)
bodyNew(paddle, BODY_KINEMATIC, SHAPE_BOX, 0.3, 1.2, 1.6)
function onOverlayUpdate()
nodeMove(paddle, 0.06, 0, 0) -- La raquette cinématique balaie la pile.
return OVERLAY_UPDATED
end
----
.Masse et matériaux
La masse d'un corps dynamique vient du volume de sa forme à la densité de
l'eau, si bien qu'une boîte de la taille d'une porte pèse 140 kilogrammes et
qu'une légère poussée ne la fera pas bouger. Réglez `bodySetMass` sur tout ce
qui doit paraître léger, et dimensionnez les impulsions selon la masse : une
impulsion de `mass * speed` donne cette vitesse. `bodySetFriction` et
`bodySetBounce` font la glace et le caoutchouc ; `bodySetEnabled` retire un
corps du monde et l'y remet sans le perdre.
.Synchronisation
Le monde avance d'un pas fixe soixante fois par seconde selon l'horloge, après
que les animations ont posé leurs nœuds et avant le rendu de la scène, si bien
qu'un corps cinématique mû par une animation pousse avec la bonne vitesse.
Mettre le jeu en pause met la physique en pause ; `physicsSetEnabled(false)` la
met en pause à elle seule. `bodyIsResting` indique quand un corps s'est
stabilisé, c'est-à-dire quand un objet lancé s'est arrêté et qu'un score peut
être compté. Un corps, un joueur, un ragdoll, un corps mou ou un véhicule dont
le nœud a été supprimé est libéré au début de l'image suivante, activé ou non
et en pause ou non, si bien que son emplacement redevient libre ; un handle de
nœud que `nodeNew` distribue plus tard à nouveau ne porte rien de la physique
de l'ancien nœud, et `playerExists` et ses semblables répondent `false` pour
lui jusqu'à ce qu'il ait la sienne.
[#_events_and_queries]
==== Événements et requêtes
Définissez `onCollision(nodeA, nodeB, x, y, z, speed)` et Singe l'appelle une
fois pour chaque nouveau contact, avec le point et la vitesse auxquels les deux
corps se sont rencontrés, si bien qu'un petit choc et un fracas peuvent sonner
différemment. Un corps devenu déclencheur par `bodySetTrigger` ne pousse rien
mais signale ce qui y entre et en sort par `onTrigger(trigger, other, entered)` :
une ligne d'arrivée, une porte, une zone dangereuse. `physicsRaycast` trouve le
premier corps le long d'un rayon ; avec `sceneUnproject` pour le rayon, c'est la
sélection à la souris (voir <<nodes,Les nœuds>>).
[source,lua]
----
finish = nodeNew()
nodeSetPosition(finish, 0, 1, -40)
bodyNew(finish, BODY_STATIC, SHAPE_BOX, 12, 2, 0.5)
bodySetTrigger(finish, true)
function onTrigger(trigger, other, entered)
if trigger == finish and other == car and entered then
lapDone()
end
end
function onCollision(a, b, x, y, z, speed)
if speed > 6 then
local channel = soundPlay(crashSound)
soundSetPosition(channel, x, y, z)
end
end
----
[#_joints]
==== Articulations
`jointHinge`, `jointBall` et `jointSlider` relient deux corps, ou un corps et le
monde quand le second nœud vaut `-1` : portes, pendules, tiroirs, les roues
d'une voiture vue de profil. L'ancre et l'axe sont donnés dans l'espace du
monde, là où se trouvent les corps quand l'articulation est créée. Les
charnières et les glissières acceptent des limites (`jointSetLimits`) : un angle
de charnière positif est une rotation du premier nœud autour de l'axe selon la
règle de la main droite, et une distance de glissière positive va dans le sens
de l'axe.
[source,lua]
----
door = nodeNew()
nodeSetMesh(door, meshBox(0.9, 2.0, 0.05), oak)
nodeSetPosition(door, 0.45, 1.0, 0)
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 0.9, 2.0, 0.05)
bodySetMass(door, 20)
local hinge = jointHinge(door, -1, 0, 1.0, 0, 0, 1, 0) -- Sur le chambranle, autour de Y.
jointSetLimits(hinge, 0, 110) -- S'ouvre dans un sens, 110 degrés.
----
Les corps sur des instances de modèle fonctionnent comme les autres : `bodyNew(instanceRoot,
BODY_DYNAMIC, SHAPE_HULL)` entoure tout le modèle d'une enveloppe convexe. Un
personnage skinné est de préférence un joueur (voir plus bas) ou une capsule
cinématique qui pousse les choses plutôt que d'être poussée.
[#physics2d]
==== Jeux 2D
La physique n'est pas liée à la scène 3D. Les nœuds existent sur toutes les
machines, avec ou sans GPU, si bien qu'un jeu 2D peut donner des corps à des
nœuds, appeler `physicsSet2D(true)` pour qu'ils ne se déplacent qu'en X et Y et
ne tournent qu'autour de Z, travailler en coordonnées de l'overlay avec une
gravité dirigée vers le bas de l'écran, et dessiner des sprites là où sont les
nœuds. C'est la forme qui tourne sur un Raspberry Pi 3, qui n'a pas de 3D.
[source,lua]
----
physicsSet2D(true)
physicsSetGravity(0, 900, 0) -- Pixels par seconde au carré ; le Y de l'overlay descend.
floor = nodeNew()
nodeSetPosition(floor, 360, 470, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 720, 20, 50)
crate = nodeNew()
nodeSetPosition(crate, 200, -40, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 40, 40, 50)
bodySetMass(crate, 1)
function onOverlayUpdate()
local x, y = nodeGetPosition(crate)
local _, _, angle = nodeGetRotation(crate)
spriteRotate(crateArt, angle)
spriteDraw(crateArt, x, y, true)
return OVERLAY_UPDATED
end
----
Donnez à chaque corps 2D une certaine profondeur (la troisième dimension) pour
que les formes se chevauchent en Z ; `50` convient. Les cercles sont des
`SHAPE_SPHERE`. Les joueurs et les émetteurs 2D (voir
<<particles,Particules>>) fonctionnent aussi dans ce monde, et un jeu de
plateforme est un joueur avec `playerMove(node, vx)`.
[#_players]
==== Joueurs
Un corps va là où les forces l'envoient ; un *joueur* va là où le script le
dit. `playerNew` met un contrôleur de personnage sur un nœud : une capsule par
défaut (`playerNew(node, radius, height)`), ou n'importe quelle forme convexe
de `bodyNew` (`playerNew(node, SHAPE_BOX, w, h, d)` pour un robot, `playerNew(node,
SHAPE_SPHERE, r, 0)` pour quelque chose qui roule mais monte quand même les
marches, `SHAPE_HULL` pour la silhouette propre du modèle ; trois arguments
signifient toujours une capsule, si bien qu'une sphère donne toutes ses
dimensions). La forme repose sur l'origine du nœud, qui correspond aux pieds. À
chaque image, le script donne à `playerMove` la vitesse horizontale qu'il veut,
et le contrôleur marche sur les sols, monte les marches jusqu'à
`playerSetStep`, glisse le long des murs, refuse les pentes plus raides que
`playerSetSlope`, tombe sous l'effet de la gravité, se laisse porter par les
plateformes mobiles et écarte de son chemin les corps dynamiques légers
(`playerSetPush`). `playerJump` ne fonctionne qu'avec le sol sous les pieds et
dit s'il a sauté. La rotation du nœud appartient au script : tournez-le face à
la direction où il marche, et accrochez le modèle dessous comme enfant.
[source,lua]
----
hero = nodeNew()
nodeSetPosition(hero, 0, 0, 0)
playerNew(hero, 0.3, 1.0) -- Une capsule de rayon 0.3, haute de 1, pieds au nœud.
fox = modelInstance(modelLoad(DIR .. "Fox.glb"))
nodeSetParent(fox, hero)
function onInputPressed(switch)
if switch == SWITCH_BUTTON1 then
playerJump(hero, 4.5) -- Il faut le sol sous les pieds.
end
end
function onOverlayUpdate()
local vx = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767 * 3
local vz = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_Y) / 32767 * 3
playerMove(hero, vx, vz)
if vx ~= 0 or vz ~= 0 then
nodeSetRotation(hero, 0, math.deg(math.atan(-vx, -vz)), 0)
end
return OVERLAY_NOT_UPDATED
end
----
Les joueurs signalent `onCollision` quand ils commencent à toucher un corps et
`onTrigger` quand ils entrent dans un capteur ou en sortent, avec leur nœud à
la place qu'occuperait le nœud d'un corps, si bien qu'une porte, un objet à
ramasser et une fosse de lave fonctionnent de la même façon pour un joueur que
pour une balle. `playerIsOnGround` et `playerGetGround` disent sur quoi le
joueur se tient, pour les bruits de pas et pour descendre exprès d'une
plateforme. Dans un monde 2D, un joueur est un personnage de jeu de plateforme :
`playerMove(node, vx)` court le long de X, la gravité est dirigée vers le bas
de l'écran, et tout le reste est identique. Les tolérances du contrôleur
s'adaptent à la forme, si bien qu'une capsule de 30 pixels dans un jeu de
sprites se comporte comme une capsule de 0,3 mètre dans un jeu 3D.
[#_vehicles]
==== Véhicules
Un *véhicule* est un corps dynamique (le châssis, créé avec `bodyNew` comme
tout corps, tourné vers -Z avec Y vers le haut) que Jolt conduit par des roues :
`vehicleNew(node, VEHICLE_CAR)` pour tout ce qui a des essieux,
`VEHICLE_MOTORCYCLE` pour deux roues en ligne qui s'inclinent dans les virages
et tiennent en équilibre, ou `VEHICLE_TANK` pour des roues sur deux chenilles
qui tournent sur place. Les roues sont des nœuds : `vehicleAddWheel(node, wheelNode, radius, width,
suspension)` prend la position de la roue d'après l'endroit où se trouve son
nœud par rapport au châssis, et dès lors le moteur de jeu pose ce nœud à chaque
image (son axe X est l'essieu, si bien qu'une roue en `meshCylinder` demande un
enfant tourné de 90 degrés). Les roues trouvées dans un modèle avec
`nodeFind("wheelFL", car)` fonctionnent de même. Par défaut, les roues avant
braquent et toutes sont motrices ; `vehicleSetWheel` en décide autrement.
À chaque image, le script tient le rôle du conducteur : `vehicleDrive(node, forward,
right, brake, handBrake)`. Le moteur, la boîte automatique et les différentiels
sont ceux de Jolt, avec les valeurs par défaut d'une voiture de 1500 kg ;
`vehicleSetEngine`, `vehicleSetGears`, `vehicleSetSuspension`,
`vehicleSetSteering`, `vehicleSetBrakes` et `vehicleSetAntiRoll` les règlent,
et toute modification reconstruit le véhicule sur place au pas suivant.
`vehicleGetSpeed`, `vehicleGetRpm` et `vehicleGetGear` alimentent un tableau de
bord. Un véhicule reste un corps : il saute des tremplins, heurte les murs,
bouscule les caisses et signale `onCollision`.
[source,lua]
----
car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 1, 0)
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
bodySetMass(car, 1500)
vehicleNew(car, VEHICLE_CAR)
local corners = {
{ -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, -- Avant gauche et droite ...
{ -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 }, -- ... arrière gauche et droite.
}
for i, p in ipairs(corners) do
local wheel = nodeNew()
nodeSetParent(wheel, car)
nodeSetPosition(wheel, p[1], p[2], p[3])
vehicleAddWheel(car, wheel, 0.35, 0.25, 0.4)
end
function onOverlayUpdate()
local throttle = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
local steer = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767
vehicleDrive(car, throttle, steer, brake, 0)
return OVERLAY_NOT_UPDATED
end
----
Les chars prennent `forward` comme accélérateur et `right` comme virage (sans
accélérateur, ils pivotent sur place), et ignorent les réglages de direction,
de freins et d'antiroulis, puisque leurs chenilles freinent et tournent
d'elles-mêmes. Le ressort d'inclinaison de la moto est proportionné au châssis,
si bien qu'une moto légère tient en équilibre aussi bien qu'une lourde. Les
véhicules à roues demandent un monde 3D ; une voiture 2D vue de profil est un
corps avec deux roues sur charnières, ce que les appels d'articulation savent
déjà construire.
[#_water]
==== Eau
`bodySetWater(node, density, linearDrag, angularDrag)` remplit d'eau un corps
statique : le dessus de sa boîte est la surface, et chaque corps dynamique à
l'intérieur flotte, coule ou roule selon la flottabilité de Jolt par rapport à
cette surface (la partie immergée de la forme compte, si bien qu'une caisse
inclinée se redresse), ralenti par les traînées. `bodySetCurrent` donne un
courant à l'eau. `density` est un multiplicateur, `1` pour l'eau, et le
`bodySetBuoyancy` propre à chaque corps flottant (par défaut `1.2`, qui flotte
doucement ; en dessous de `1`, il coule) le module. Le volume est un
déclencheur, si bien que `onTrigger` se déclenche à l'entrée et à la sortie,
pour les éclaboussures. L'eau n'est pas dessinée : donnez au nœud du volume un
matériau en mélange, ou aucun.
Un joueur dans l'eau nage : `playerIsSwimming` le dit, `playerMove(node, vx,
vy, vz)` dirige alors sur trois axes, il coule lentement sans commande
verticale (`playerSetSwim` règle cette vitesse et la traînée de l'eau), et le
courant l'emporte. Un bateau est une coque flottante à laquelle
`vehicleNew(node, VEHICLE_BOAT)` donne une hélice et un gouvernail :
`vehicleSetThrust` dit avec quelle force elle pousse et à quel endroit de la
coque, `vehicleSetRudder` avec quelle force il tourne, et `vehicleDrive`
fonctionne comme pour une voiture, le tout seulement tant que l'hélice est sous
une surface.
[source,lua]
----
pool = nodeNew()
nodeSetMesh(pool, meshBox(8, 3, 12), blue) -- Bleu en mélange, dessiné comme l'eau.
nodeSetPosition(pool, 0, -1.5, 0) -- Son dessus, à Y = 0, est la surface.
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
bodySetWater(pool, 1.0, 0.6, 0.2)
bodySetCurrent(pool, 0.5, 0, 0) -- Dérive vers +X.
bodySetBuoyancy(crate, 1.6) -- Flotte haut.
bodySetBuoyancy(anchor, 0.3) -- Coule.
----
[#_ragdolls]
==== Ragdolls
`ragdollNew(node)` cherche le skin sous une instance de modèle et transforme le
squelette en recette de ragdoll : chaque articulation qui a une articulation
enfant devient une capsule le long de cet os, reliée à l'os le plus proche
au-dessus d'elle par une articulation swing-twist aux limites raisonnables, et
les articulations terminales (doigts, oreilles, le bout d'une queue) suivent
leurs parents. Rien ne bouge avant `ragdollActivate` : alors l'animation
s'arrête, des corps sont créés là où sont les os, et à partir de ce moment la
physique pilote les nœuds des articulations, si bien que le maillage skinné
s'affaisse, heurte le sol et s'immobilise avec le reste du monde.
`ragdollApplyImpulse` pousse un os désigné par le nom de son articulation.
`ragdollSetStrength` motorise chaque articulation vers la pose dans laquelle le
ragdoll a été activé, ce qui change une chute molle en titubement ou permet à
un personnage à terre de ramener ses membres ; cela ne remet pas un corps
debout, puisque rien ne prend appui sur le sol. `ragdollDeactivate` rend le
squelette à l'animation là où il gît, et un `animationPlay` avec un fondu le
relève alors en douceur depuis cette position. Réglez un os qui a l'air faux
avec `ragdollSetJoint` avant l'activation.
[source,lua]
----
fox = modelInstance(foxModel)
ragdollNew(fox) -- Recette tirée du skin ; toujours animé.
function onCollision(a, b, x, y, z, speed)
if (a == hero or b == hero) and speed > 3 then
playerSetEnabled(hero, false)
ragdollActivate(fox) -- La physique pilote désormais le squelette.
ragdollApplyImpulse(fox, "b_Spine02_03", 40, 30, 0)
end
end
----
[#_soft_bodies]
==== Corps mous
Le tissu, la corde et la gelée sont des maillages dont les sommets sont
simulés. `softNew(node, SOFT_CLOTH)` transforme le maillage du nœud en tissu,
ses sommets (soudés par position) en particules reliées par des contraintes
d'étirement, de cisaillement et de flexion, là où le nœud l'a placé dans le
monde ; un `meshPlane(width, depth, columns, rows)` subdivisé est le point de
départ habituel. `softNew(node,
SOFT_BODY)` fait de même pour un maillage fermé et `softSetPressure` le gonfle :
un ballon, une gelée. `softNew(node, SOFT_ROPE, x, y, z, segments,
radius)` tend une chaîne de particules de la position du nœud jusqu'à ce point
et donne au nœud un maillage tubulaire autour d'elles (réglez son apparence avec
`nodeSetMaterial`).
Les corps mous entrent en collision avec tout ce qui est rigide, tombent sous
l'effet de la gravité et sont dessinés par le maillage dont ils sont issus, que
le moteur réécrit à chaque image. `softPin` maintient la particule la plus
proche là où elle est ou à un nœud (le bord du drapeau au mât, le haut de la
corde à la grue) ; `softSetStiffness`, `softSetMass` et `softSetDamping`
règlent le matériau. Un nœud dont le maillage est partagé avec d'autres nœuds
les modifie aussi, alors donnez à un corps mou un maillage à lui.
[source,lua]
----
flag = nodeNew()
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), red)
nodeSetPosition(flag, -4.2, 2.4, 0)
nodeSetRotation(flag, 90, 0, 0) -- Suspendu à la verticale.
softNew(flag, SOFT_CLOTH)
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
softPin(flag, -5, y, 0) -- Le long du mât.
end
----
Le coût croît avec le nombre de particules : une toile de 20 sur 20 compte 441
particules et passe bien sur un Raspberry Pi 4 ; limitez le tissu à quelques
centaines de particules et une corde à une douzaine de segments. Les particules
entrent en collision avec les corps rigides, pas avec les faces, si bien qu'un
maillage assez fin (des particules plus proches que la plus petite chose qui
atterrira dessus) est ce qui empêche une caisse de passer au travers. Il n'y a
pas d'autocollision : le tissu peut se traverser lui-même quand il est froissé.
Les maillages skinnés ne peuvent pas être mous.
[#_seeing_the_physics]
==== Voir la physique
Quand une caisse passe à travers le sol ou qu'une articulation plie dans le
mauvais sens, `physicsSetDebug` dessine ce que la simulation contient
réellement, sous forme de lignes par-dessus la scène, à chaque image :
* `DEBUG_SHAPES` : les corps en fil de fer, colorés selon leur état (endormi,
en mouvement, statique), avec les joueurs en jaune.
* `DEBUG_CONSTRAINTS` : les articulations, les articulations de ragdoll et les
contraintes des véhicules, avec leurs limites.
* `DEBUG_CONTACTS` : une croix rouge là où des corps se sont rencontrés pendant
ce pas.
* `DEBUG_VELOCITIES` : une ligne le long de la vitesse de chaque corps en
mouvement.
* `DEBUG_STATIC` : les formes des corps statiques aussi, qui restent masquées
sinon parce qu'un maillage de niveau fait beaucoup de lignes.
Additionnez-les, ou utilisez `DEBUG_ALL` pour les quatre premiers. `DEBUG_NONE`
le désactive, et il ne coûte rien tant qu'il est désactivé. Un script dessine
ses propres lignes de la même façon avec `lineDraw` (un lancer de rayon, un
chemin, une boîte englobante), une image à la fois comme l'overlay, et
`navDraw` montre un maillage de navigation.
[source,lua]
----
if debugging then
physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS)
end
function onOverlayUpdate()
local hit, hx, hy, hz = physicsRaycast(gunX, gunY, gunZ, aimX, aimY, aimZ, 60)
if hit then
lineDraw(gunX, gunY, gunZ, hx, hy, hz, 255, 255, 0)
end
return OVERLAY_NOT_UPDATED
end
----
[#_where_it_runs]
==== Où elle tourne
La physique tourne sur le CPU et n'a pas besoin de GPU, mais la bibliothèque
est compilée pour SSE4.1 sur x86 (2008 et après) et NEON sur le Pi ; une
machine en deçà obtient une erreur dès le premier `bodyNew`. La simulation
utilise tous les cœurs sauf un, si bien qu'un Pi à quatre cœurs avance ses pas
sur trois fils d'exécution ; la trace du programme dit combien.
[#particles]
=== Particules
Étincelles, fumée, feu, pluie, poussière, magie : tout ce qui est fait de
nombreuses petites choses éphémères est l'affaire d'un *émetteur*. Un émetteur
contient une recette (combien par seconde, combien de temps elles vivent, à
quelle vitesse et dans quelle direction elles partent, ce qui les attire,
comment elles grandissent, s'estompent et tournent) et une réserve de
particules qu'il anime d'après cette recette. Le script règle la recette une
fois, puis démarre l'émetteur pour un flux régulier (`emitterStart`), le fait
jaillir d'un coup pour une bouffée ponctuelle (`emitterBurst`), ou les deux. Le
moteur déplace chaque particule vivante à chaque image, et cesse de les
déplacer pendant que le jeu est en pause.
Une seule API d'émetteur sert l'overlay et la scène 3D ; seuls les unités et le
dessin diffèrent. Tout ce qui est donné sous forme de minimum et de maximum
(`emitterSetLife`, `emitterSetSpeed`, `emitterSetSize`) est tiré au hasard
entre les deux pour chaque particule.
[#_2d_emitters]
==== Émetteurs 2D
`emitterNew()` sans argument crée un émetteur 2D. Il vit en coordonnées de
l'overlay, Y vers le bas, si bien que la gravité d'une fontaine est un Y
positif. Déplacez-le avec `emitterSetPosition`, et dessinez-le depuis
`onOverlayUpdate` avec `emitterDraw`. L'overlay lui-même est une image que le
CPU peint, si bien que les particules n'y sont pas peintes : le GPU les
dessine, soit au-dessus de tout l'overlay (`PARTICLE_OVER`, par défaut), soit
en dessous de lui et au-dessus de la vidéo et de la scène 3D
(`PARTICLE_UNDER`, pour de la fumée derrière le score). Les émetteurs 2D
tournent sur toutes les machines où tourne Singe, Raspberry Pi 3 compris, et
vont bien avec la physique 2D : lisez la position du nœud d'un corps et
donnez-la à `emitterSetPosition`.
[source,lua]
----
sparks = emitterNew()
emitterSetBlend(sparks, PARTICLE_ADD)
emitterSetLife(sparks, 0.3, 0.7)
emitterSetSpeed(sparks, 80, 200)
emitterSetDirection(sparks, 0, -1) -- Vers le haut de l'écran.
emitterSetSpread(sparks, 70)
emitterSetGravity(sparks, 0, 500)
emitterSetSize(sparks, 5, 1)
emitterSetColor(sparks, 255, 255, 200, 255, 255, 120, 0, 0)
function onOverlayUpdate()
overlayClear()
emitterSetPosition(sparks, grinderX, grinderY)
emitterDraw(sparks)
return OVERLAY_UPDATED
end
----
[#_3d_emitters]
==== Émetteurs 3D
`emitterNew(node)` crée un émetteur 3D attaché à un nœud. Les particules
naissent à la position du nœud dans le monde (réparties sur
`emitterSetRadius`), en unités du monde, et se dessinent dans la scène comme
des billboards tournés vers la caméra, après tous les maillages : testées en
profondeur contre la géométrie, sans jamais écrire la profondeur, non éclairées
sauf demande, sans projeter ni recevoir d'ombres. Par défaut, une particule
reste là où elle est née une fois qu'elle quitte l'émetteur (la fumée continue
de dériver là où elle a été produite pendant que la torche s'éloigne) ;
`emitterSetLocal` fait plutôt suivre le nœud à tout le nuage. Supprimer le nœud
supprime ses émetteurs.
[source,lua]
----
flame = emitterNew(torchTip)
emitterSetBlend(flame, PARTICLE_ADD)
emitterSetRate(flame, 140)
emitterSetLife(flame, 0.4, 0.9)
emitterSetSpeed(flame, 0.6, 1.4)
emitterSetDirection(flame, 0, 1, 0)
emitterSetSpread(flame, 15)
emitterSetSize(flame, 0.35, 0.05, 0.3)
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
emitterStart(flame)
----
[#_the_picture]
==== L'image
Une particule est un quad texturé. Sans texture, c'est un disque aux bords doux
fourni par le moteur ; `emitterSetTexture` lui donne plutôt un sprite chargé
(une bouffée, une traînée d'étincelle, un flocon de neige), et pour un sprite
animé, `emitterSetFrames` choisit une image au hasard par particule, si bien
qu'un seul atlas de bouffées donne un nuage varié. Le mélange est normal
(`PARTICLE_ALPHA`) ou additif (`PARTICLE_ADD`, pour tout ce qui brille).
`emitterSetColor` donne la couleur et l'alpha à la naissance et à la mort, et
la particule passe de l'un à l'autre au fil de sa vie ; `emitterSetSize` fait
de même pour sa taille, avec une troisième valeur pour la vitesse à laquelle
elle tourne.
[#_trails_collisions_lit_and_soft_particles]
==== Traînées, collisions, particules éclairées et particules douces
`emitterSetTrail(emitter, length, width)` garde les dernières positions de
chaque particule et dessine un ruban qui les traverse, en s'estompant vers la
queue : étincelles, balles traçantes, comètes, en 2D comme en 3D.
`emitterSetCollide` fait rebondir les particules : sur un sol horizontal à une
hauteur que vous donnez (`COLLIDE_FLOOR`, gratuit), ou sur le monde physique
par lancers de rayons (`COLLIDE_SCENE`, 3D seulement, plafonné par image, si
bien qu'un gros émetteur répartit le coût sur quelques images).
Deux autres sont réservés à la 3D. `emitterSetLit` éclaire chaque particule
selon les lumières et le ciel de la scène comme une petite sphère tournée vers
la caméra, si bien que la fumée prend la couleur d'un brasero et que la
poussière scintille au soleil (les ombres ne sont pas lues).
`emitterSetSoftness` fait disparaître progressivement une particule sur une
certaine distance là où elle rencontre la géométrie, si bien que la fumée ne
tranche pas dans un sol, ni une fontaine dans son bassin ; cela coûte une passe
de profondeur supplémentaire de la scène par image tant qu'un émetteur
l'utilise.
[source,lua]
----
smoke = emitterNew(brazier)
emitterSetTexture(smoke, spriteLoad(DIR .. "puff.png"))
emitterSetRate(smoke, 30)
emitterSetLife(smoke, 2, 4)
emitterSetSpeed(smoke, 0.3, 0.6)
emitterSetDirection(smoke, 0, 1, 0)
emitterSetSize(smoke, 0.3, 1.2, 0.2)
emitterSetColor(smoke, 90, 90, 90, 160, 60, 60, 60, 0)
emitterSetLit(smoke, true) -- Prend la couleur du feu par en dessous.
emitterSetSoftness(smoke, 0.4) -- Pas d'arête nette là où elle touche la vasque.
emitterStart(smoke)
embers = emitterNew(brazier)
emitterSetBlend(embers, PARTICLE_ADD)
emitterSetTrail(embers, 6, 0.02) -- Une courte traînée derrière chaque braise.
emitterSetCollide(embers, COLLIDE_FLOOR, 0.3, 0.8, 0) -- Rebondit sur le sol à Y = 0.
emitterStart(embers)
----
[#_cost]
==== Coût
Les particules coûtent peu à simuler et coûtent ce qu'elles couvrent à l'écran.
Quelques milliers de petites ne sont rien ; une poignée de grandes particules
additives sur tout l'écran, voilà ce qui ralentit un Raspberry Pi 4. La réserve
est plafonnée par `emitterSetMax` (1000 par défaut) ; quand elle est pleine, il
n'en naît plus aucune tant que certaines ne sont pas mortes.
[#authoring]
=== Décrire un jeu au lieu de l'écrire
Un jeu peut être écrit sous forme de *description* -- une table de couches,
d'entités et de règles -- et compilé en un jeu Singe ordinaire par Forge,
l'outil de création. Forge est un projet distinct, avec son propre
téléchargement et son propre livre.
[[migrating]]
=== Migrer depuis Singe 2.10
Singe 3.00 a déplacé le handle du sprite vers le premier argument de
`spriteDraw`, `spriteLoop`, `spriteQuality`, `spriteRotate`,
`spriteRotateAndScale`, `spriteScale` et `spriteSetFrame`, si bien que chaque
appel de sprite correspond désormais à la famille `video*`. Pour mettre à jour
un jeu, déplacez le dernier argument de chacun de ces appels en tête :
[source,lua]
----
spriteDraw(x, y, cursor) -- 2.10
spriteDraw(cursor, x, y) -- 3.00
spriteRotate(angle, cursor) -- 2.10
spriteRotate(cursor, angle) -- 3.00
----
Un jeu que vous ne pouvez pas modifier peut choisir l'ancien ordre à la place.
Soit vous définissez la variable globale avant de charger le framework :
[source,lua]
----
SINGE_LEGACY_SPRITE_ARGS = true
dofile("Singe/Framework.singe")
----
soit vous ajoutez `LEGACY_SPRITE_ARGS = true` à son entrée de `games.dat`. Rien
d'autre n'a changé de forme, mais quelques comportements, si :
* Les handles et les compteurs sont renvoyés sous forme d'entiers Lua ;
`videoIsPlaying` renvoie un booléen ; `mouseSetMode` ne renvoie rien.
* Les erreurs dans les callbacks terminent désormais le jeu avec une trace de
la pile d'appels au lieu d'être affichées et ignorées, comme les erreurs
d'arguments l'ont toujours fait.
* Les touches maintenues ne se répètent plus en `MODE_NORMAL`.
* Rien n'est écrit à côté du jeu. Sans `--datadir`, le dossier de données est
`data/<game>/` sous le dossier du jeu, là où 2.x utilisait le dossier même du
jeu ; les sauvegardes, les réglages et les meilleurs scores ont leur place
sous `singeGetDataPath()`.
* `math.randomseed` accepte une graine fractionnaire, comme sous le Lua de
2.10 ; elle est arrondie à l'entier inférieur avant que le générateur de
Lua 5.4 ne la voie. `os.clock` est le temps d'horloge écoulé depuis le
démarrage du moteur, pas le temps processeur.
* Le disque est placé sur l'image 1 par le moteur avant que votre script ne
s'exécute ; `Framework.singe` ne fait plus de saut quand il est chargé.
* `onMouseMoved` en mode `MANY_MOUSE` transmet le déplacement réel dans ses
troisième et quatrième arguments, et `onControllerMoved` transmet l'indice de
la manette (`0` à `3`) au lieu de l'identifiant d'instance de SDL.
* `mouseEnable` / `mouseDisable` et `singeEnablePauseKey` /
`singeDisablePauseKey` fonctionnent toujours, comme alias de
`mouseSetEnabled` et `singeSetPauseKeyEnabled`.
* Un laserdisc n'est plus supposé d'office. Une entrée de `games.dat` avec un
`VIDEO` est un jeu laserdisc, comme avant ; une entrée sans tourne plutôt sur
un canevas. En ligne de commande, passez `--disc` (ou `--framefile`), car une
vidéo trouvée à côté du script n'implique plus un disque. Voir
<<withoutadisc,Jeux sans disque>>.
* La touche pause met désormais en pause tout le jeu, pas seulement les
médias : le script est gelé jusqu'à ce que la touche soit de nouveau pressée,
et `SWITCH_PAUSE` n'est transmis au script que lorsque la touche a été
désactivée. Elle agit à l'appui de la touche plutôt qu'au relâchement. Voir
<<pausing,La pause>>.
* `fontPrint` conserve les chaînes qu'il rend, et `overlayClear` ne remplit à
nouveau que ce qui a été dessiné depuis le dernier effacement, si bien qu'un
HUD réimprimé à chaque image coûte peu. Rendre du texte avec `fontToSprite`
pour les performances est déconseillé ; cela fonctionne toujours, et c'est
toujours le moyen d'obtenir du texte que l'on peut mesurer, mettre à
l'échelle ou faire tourner. Voir <<fonttosprite,fontToSprite>>.
[#_engine_constants]
=== Constantes du moteur
Le moteur définit ces variables globales avant l'exécution de tout script, si
bien qu'elles sont disponibles pour `controls.cfg` comme pour `Framework.singe` :
[cols="2,3",options="header"]
|===
| Constante | Signification
| `SWITCH_UP` ... `SWITCH_RECORD` | Valeurs passées à `onInputPressed` / `onInputReleased` en `MODE_NORMAL`. `SWITCH_MOUSE_DISCONNECT` est émis par le moteur lui-même quand une souris ou un pistolet optique est débranché, une fois comme appui et une fois comme relâchement ; aucune touche ni aucun bouton ne le produit. `SWITCH_RECORD` est le contact d'enregistrement, sur lequel le moteur agit lui-même, comme pour le contact de capture d'écran.
| `FONT_QUALITY_SOLID`, `FONT_QUALITY_SHADED`, `FONT_QUALITY_BLENDED` | Arguments de `fontQuality`.
| `MODE_NORMAL`, `MODE_FULL` | Arguments de `keyboardSetMode`.
| `MOUSE_SINGLE`, `MOUSE_MANY` (aussi `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments de `mouseSetMode`.
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Valeurs de retour de `onOverlayUpdate`.
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments de `spriteQuality` / `videoQuality`.
| `SPRITE_ALPHA`, `SPRITE_ADD`, `SPRITE_MULTIPLY` | Mélange pour `spriteSetBlend`.
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Valeurs renvoyées par `discGetState`.
| `LIGHT_DIRECTIONAL`, `LIGHT_POINT`, `LIGHT_SPOT` | Arguments de `lightNew`.
| `BODY_STATIC`, `BODY_DYNAMIC`, `BODY_KINEMATIC` | Types de corps pour `bodyNew`.
| `SHAPE_BOX`, `SHAPE_SPHERE`, `SHAPE_CAPSULE`, `SHAPE_CYLINDER`, `SHAPE_HULL`, `SHAPE_MESH` | Formes de collision pour `bodyNew`.
| `JOINT_HINGE`, `JOINT_BALL`, `JOINT_SLIDER` | Types d'articulation (les appels `joint*` les nomment directement).
| `DEBUG_NONE`, `DEBUG_SHAPES`, `DEBUG_CONSTRAINTS`, `DEBUG_CONTACTS`, `DEBUG_VELOCITIES`, `DEBUG_STATIC`, `DEBUG_ALL` | Ce que dessine `physicsSetDebug` ; additionnez-les.
| `PARTICLE_ALPHA`, `PARTICLE_ADD` | Mélange pour `emitterSetBlend`.
| `PARTICLE_OVER`, `PARTICLE_UNDER` | Où se dessinent les particules 2D, pour `emitterSetLayer`.
| `COLLIDE_NONE`, `COLLIDE_FLOOR`, `COLLIDE_SCENE` | Ce sur quoi rebondissent les particules, pour `emitterSetCollide`.
| `BILLBOARD_NONE`, `BILLBOARD_ALL`, `BILLBOARD_Y` | Comment un nœud fait face à la caméra, pour `nodeSetBillboard`.
| `LAYER_SCENE`, `LAYER_FRONT` | Si un nœud se dessine sous l'overlay ou devant lui, pour `nodeSetLayer`.
| `VEHICLE_CAR`, `VEHICLE_MOTORCYCLE`, `VEHICLE_TANK`, `VEHICLE_BOAT` | Sortes de véhicules pour `vehicleNew`.
| `SOFT_CLOTH`, `SOFT_BODY`, `SOFT_ROPE` | Sortes de corps mous pour `softNew`.
| `FILTER_LINEAR`, `FILTER_NEAREST` | Échantillonnage des textures pour `materialSetFilter`.
| `TONEMAP_NEUTRAL`, `TONEMAP_ACES`, `TONEMAP_NONE` | Courbes de tonalité pour `sceneSetTonemap`.
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, ce que renvoie `soundPlay` quand aucun canal n'est libre.
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | La version du moteur, sous forme d'entiers, de chaîne (`"v3.00"`) et du nombre que renvoie `singeVersion()`.
| `SINGE_DEAD_ZONE` | La `DEAD_ZONE` de `controls.cfg`, en unités brutes d'axe.
| `SINGE_TRIGGER_THRESHOLD` | Le seuil des gâchettes analogiques dans ces mêmes unités brutes ; `0` quand les gâchettes suivent `SINGE_DEAD_ZONE`, ce qui est le cas par défaut.
| `SINGE_LEGACY_SPRITE_ARGS` | Vrai quand le jeu a demandé l'ordre des arguments de sprite de 2.10.
| `SINGE_DISC` | Vrai quand le jeu a un laserdisc ; faux quand le canevas est le monde.
| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE`, `SINGE_CONTROLLER_AXES`, `SINGE_CONTROLLER_BUTTONS` | Disposition des codes d'entrée des manettes et des souris, et nombre d'axes et de boutons d'une manette ; `Framework.singe` construit à partir d'eux les tables `GAMEPAD_N` et `MOUSE_N`, et un écran de service s'en sert pour parcourir une manette.
| `SINGE_SCALE_MIN`, `SINGE_SCALE_MAX`, `SINGE_SHIFT_MIN`, `SINGE_SHIFT_MAX`, `SINGE_ROTATE_STEP` | Ce qu'acceptent `vldpSetScale`, `vldpSetShift` et `vldpSetRotate`. Bornez à ces valeurs plutôt qu'à vos propres copies : un décalage hors limites met fin au script.
|===
`Framework.singe` ajoute les tables `SCANCODE` et `MODIFIER` (les valeurs de
touches et de modificateurs de SDL), `GAMEPAD_0` à `GAMEPAD_3`, `MOUSE_0` à
`MOUSE_3`, les index `GAMEPAD_AXIS_*` pour `controllerGetAxis`, et `DIR`.
[#_included_libraries]
=== Bibliothèques incluses
En plus des bibliothèques standard de Lua et de l'API Singe, les bibliothèques
suivantes sont aussi utilisables dans les programmes Singe sans avoir à
installer de logiciel supplémentaire :
[cols="1,3",options="header"]
|===
| Bibliothèque | Rôle
| Copas | Réseau asynchrone
| LuaFileSystem | Prise en charge étendue du système de fichiers
| json.lua | Encodage et décodage JSON (JavaScript Object Notation)
| binaryheap | Structure de données de tas binaire
| timerwheel | Planification efficace de minuteries
| LuaSec | Communication TLS / SSL
| LuaSocket | TCP et UDP
| LuaRS232 | Accès au port série RS232
| lsqlite3 | Bases de données SQLite, `require("sqlite3")`
| md5 | `require("md5")` pour `sum`, `hexsum`, `exor`, `crypt` et `decrypt` ; la bibliothèque de PUC-Rio qu'Hypseus embarque, dont elle nomme `hexsum` la fonction `sumhexa`
| LuaBitOp | `require("bit")` pour les opérations bit à bit de Lua 5.1, que 5.4 a sous forme d'opérateurs
| lrandom | `require("random")` pour un générateur Mersenne Twister, distinct de `math.random`
| Lua CJSON | `require("cjson")` pour l'encodage et le décodage JSON, à côté du module `json` ci-dessus
| LPeg | `require("lpeg")` pour les grammaires d'expressions d'analyse
| middleclass | `require("middleclass")` pour les classes, si vous préférez construire vos entités avec elles plutôt qu'avec des tables
| lume | `require("lume")` pour les petites choses dont tout jeu a besoin : `lerp`, `clamp`, `round`, `shuffle`, `split`, `serialize`, `weightedchoice`
| inspect | `require("inspect")` pour afficher une table sous une forme qu'un humain peut lire, pour quand `debugPrint` tient lieu de débogueur
| bump | `require("bump")` pour déplacer des choses qui ne doivent pas se traverser -- il répond à la question de savoir où finit un objet qui se déplace, ce que <<collide,les appels collide>> ne font délibérément pas
| MobDebug | `require("mobdebug")`, le débogueur auquel parle <<zerobrane,ZeroBrane Studio>> ; `--debug` le démarre, si bien qu'un jeu n'a besoin d'aucune ligne à lui
| RmlUi | Le modèle objet propre à l'interface, la variable globale `rmlui` (voir <<gui,GUI>>)
|===
Leur utilisation dépasse le cadre de ce document. Les bibliothèques md5,
LuaBitOp, lrandom, Lua CJSON et LPeg sont là parce qu'Hypseus les expose, si bien qu'un jeu écrit pour Hypseus trouve les
mêmes modules sous les mêmes noms ; rien dans Singe n'en a besoin.
[#audiosync]
=== Synchronisation audio
Singe présente la vidéo d'après l'horloge audio : une image est affichée quand
le son qui va avec elle est entendu. Le moteur mesure son propre chemin audio au
démarrage. SDL ne peut pas indiquer combien d'audio est en file d'attente entre
le mélangeur et le haut-parleur, alors Singe vide le périphérique une fois,
avant que quoi que ce soit d'audible ne joue, et compte la rafale de tampons
que SDL utilise pour le remplir de nouveau ; cette rafale est la profondeur de
la file, et l'horloge la soustrait. Lancez avec `--program` pour voir la mesure
dans la trace (`Audio device queue: ...`).
Ce que le moteur ne peut pas voir, c'est tout ce qui se trouve en aval du
serveur audio du système d'exploitation : un DAC, un ampli-tuner HDMI, une
liaison Bluetooth. Ceux-ci ajoutent un délai fixe qui varie selon la machine.
Si les actions à l'écran précèdent visiblement leur son, passez les
millisecondes manquantes avec `--audiodelay=MS`, mettez `AUDIO_DELAY
= MS` dans `games.dat`, ou laissez le jeu le régler depuis un menu de service avec
<<singesetaudiodelay,singeSetAudioDelay>>. Les valeurs positives signifient que
l'audio est en retard ; la vidéo est retenue d'autant. Une valeur négative
traite le cas rare d'un audio qui arrive en avance.
[#menucalibration]
==== Calibrer depuis le menu
Le menu fourni a un écran de calibrage pour ce délai en aval. Appuyez sur la
touche affectée à `INPUT_SERVICE` (la touche `9` par défaut) dans la liste des
jeux et choisissez Audio Delay dans les outils de service (voir xref:Manual.adoc#servicetools[Les outils
de service]). L'écran est une page du document du menu : il recouvre le menu de
noir, affiche le délai essayé et la file du périphérique que le moteur a
mesurée, joue un clic une fois par seconde et devient blanc pendant une image à
chaque flash. Réglez avec gauche et droite (10 ms) et haut et bas (1 ms)
jusqu'à ce que le flash et le clic se produisent ensemble, puis appuyez sur le
bouton 1 pour enregistrer. Start remet à zéro et le bouton 2 (ou de nouveau la
touche de service) annule. La valeur est stockée dans `machine.cfg` à la racine
des données et appliquée par le moteur à chaque jeu de cette machine, qu'il
soit lancé depuis le menu ou depuis la ligne de commande ; elle s'ajoute à tout
`AUDIO_DELAY` propre à un jeu.
Recalibrez après avoir changé de haut-parleurs, de casque ou d'écran.
L'écran fonctionne parce que le clic et l'audio du disque partagent un seul
périphérique de mélangeur et une seule file : le flash est programmé, après le
clic, à la file mesurée plus la valeur candidate, si bien que lorsque les deux
coïncident, la valeur candidate est exactement le délai que le moteur ne peut
pas voir. Le retard de l'affichage est compris gratuitement. Un jeu peut lire
les valeurs avec <<singegetaudiocalibration,singeGetAudioCalibration>> et
<<singegetaudiolatency,singeGetAudioLatency>>, ou proposer son propre écran avec
<<singesetaudiocalibration,singeSetAudioCalibration>>.
Deux propriétés du fichier vidéo lui-même comptent aussi. L'audio AAC porte des
échantillons d'amorçage de l'encodeur que le conteneur doit dire aux décodeurs
d'ignorer ; les fichiers multiplexés sans cette information (un MP4 sans liste
d'édition, un fichier Matroska sans délai de codec) jouent leur audio environ
20 millisecondes en retard, ce que `AUDIO_DELAY` peut absorber. Et quand la
première image vidéo est horodatée plus tard que le premier échantillon audio,
Singe respecte la synchronisation du conteneur, si bien que l'audio de tête
n'est pas perdu.
[#formats]
=== Formats vidéo, audio et de conteneur
Singe décode la vidéo directement avec les bibliothèques de FFmpeg, en utilisant le décodeur
matériel de la plateforme quand elle en offre un pour le codec (VA-API ou VDPAU sous Linux, D3D11VA sous
Windows, VideoToolbox sous macOS, et sur une carte ARM les décodeurs V4L2 -- celui
de mémoire à mémoire, qui sur un Pi 4 couvre le H.264, celui de la Request API
sans état qu'un Pi 5 utilise pour le HEVC, et celui propre à Rockchip sur un
RK3588) et son propre décodeur logiciel sinon ; la trace du programme indique
lequel, et `--softwarevideo` force le logiciel. Tout conteneur
et tout codec que le
FFmpeg fourni sait démultiplexer et décoder sera lu : MP4, MKV, flux de programme MPEG,
AVI, AV1 et les flux élémentaires `.m2v` classiques de Daphne avec un fichier audio `.ogg`
correspondant à côté. Chaque piste audio du fichier est disponible pour
`discSetAudioTrack` / `videoSetAudioTrack` ; toutes les pistes doivent partager
un même format d'échantillon, un même nombre de canaux et une même fréquence.
Un laserdisc contenait des trames entrelacées, et un rip qui les a conservées
présente des effets de peigne sur tout affichage progressif. `--deinterlace`
décide de ce qui se passe : `auto`, la valeur par défaut, applique bwdif aux
images que le fichier marque comme entrelacées et laisse tout le reste tel
quel, si bien qu'un disque progressif ne paie rien ; `on` est pour un fichier
dont les indicateurs sont faux ; `off` ne touche jamais à l'image. Une image
sort pour chaque image qui entre -- un disque est adressé par numéro d'image,
si bien qu'un désentrelaceur qui changerait chaque image en deux trames
décalerait chaque image de l'index.
La première fois qu'une vidéo est ouverte, Singe l'indexe et stocke l'index à
côté des autres données du jeu (`<name>-<hash>.index`, le hachage étant fait à
partir du nom complet de la vidéo pour que deux vidéos de même nom de base gardent des index séparés). L'indexation est un seul passage sur le fichier
sans décodage, quelques secondes même pour un disque de la durée d'un long
métrage, et se refait si la vidéo change. Quand une vidéo est chargée, Singe
indique l'espacement de ses images clés dans la trace du programme
(`--program`), avec un avertissement si les images clés sont espacées de plus
de deux secondes, car un saut doit décoder en avançant depuis l'image clé
précédente. Le décodage se fait sur un fil d'exécution séparé, si bien qu'un
saut lent montre l'image précédente un peu plus longtemps au lieu de bloquer le
jeu.
Les segments d'un framefile sont ouverts à mesure que le disque les atteint,
pas tous d'un coup : celui qui joue, celui d'après, et de la place pour qu'un
saut passe de l'un à l'autre. Un long framefile peut avoir des centaines de
segments -- un jeu publié en a 213 -- et garder un décodeur ouvert pour chacun
d'eux coûte des centaines de fichiers ouverts sans rien y gagner. Un framefile
qui nomme un fichier absent le signale toujours au démarrage.
Pour les séquences de laserdisc, les contraintes sont la précision à l'image et
la vitesse de saut, pas la compression. Le H.264 en MP4 ou MKV avec un
intervalle d'images clés court (une ou deux secondes) saute vite et se lit sur
toutes les plateformes prises en charge ; de longs intervalles d'images clés
rendent `discSearch` visiblement lent car le décodeur doit avancer depuis
l'image clé précédente. La définition standard (720x480 ou 720x576) est le
meilleur compromis. Les sources haute définition fonctionnent mais coûtent
proportionnellement plus de CPU sur du matériel de la classe du Raspberry Pi,
et l'overlay prend par défaut la moitié de la résolution de la vidéo, si bien
qu'une vidéo surdimensionnée n'apporte pas grand-chose.
Les effets sonores et la musique sont lus d'abord par SDL_mixer, puis par
FFmpeg pour tout ce qu'il refuse, si bien qu'à eux deux ils couvrent :
* WAV, AIFF, AU et VOC pour l'audio non compressé. Les fichiers WAV courts non
compressés donnent la latence la plus faible, et c'est ce que devrait être un
coup de feu.
* Ogg Vorbis, Opus, MP3, FLAC et WavPack.
* AAC (`.m4a`, `.aac`), ALAC, AC-3, WMA, APE, TTA, Speex et AMR -- tout le reste
de ce que décode le FFmpeg fourni.
* Les modules de tracker via libxmp : MOD, S3M, XM et IT, et une cinquantaine
d'autres dont 669, AMF, MED, OKT, PTM, STM et ULT.
* Les chiptunes via game-music-emu : NSF et NSFE, SPC, VGM, GBS, AY, GYM, HES,
KSS et SAP. La forme VGZ compressée par gzip n'est pas lue ; décompressez-la
d'abord en VGM. Un chiptune qui ne dit pas sa durée joue jusqu'à ce qu'il se taise, ce qui peut ne jamais arriver, si bien qu'il est décodé au fil de la lecture plutôt que d'avance.
* Le MIDI, synthétisé à partir d'une SoundFont. Voir <<midi,MIDI>>.
La ligne `Audio:` de l'en-tête de la trace (`--program` ou `--trace`) liste les
décodeurs que la version compilée contient réellement.
Les images fonctionnent de la même façon : SDL_image d'abord, FFmpeg pour le
reste. Cela couvre PNG, JPEG, WebP, GIF, BMP, TGA, PCX, PNM, LBM, QOI, XCF, XPM
et SVG du côté de SDL_image, et TIFF, OpenEXR, JPEG 2000, DDS, PSD, DPX, AVIF
et HEIC du côté de FFmpeg. Un GIF ou un WEBP animé se charge comme une
animation de sprite ; voir <<spriteload,`spriteLoad`>>, qui prend aussi une
taille pour rastériser un SVG. Le `.hdr` de Radiance et OpenEXR fonctionnent
tous deux comme ciel (<<scenesetsky,`sceneSetSky`>>) et gardent toute leur
plage.
Les polices sont lues par FreeType : TrueType, OpenType, WOFF, WOFF2, Type 1,
BDF et PCF, avec les glyphes en couleur (bitmap comme vectoriels) et le
façonnage HarfBuzz pour l'arabe, l'hébreu et les écritures indiennes.
[#reference]
== Référence de l'API Lua
Ce chapitre documente l'API Lua exposée par le moteur Singe -- chaque fonction
que le script de votre jeu peut appeler, et chaque callback que Singe appellera
chez vous. Il s'adresse aux développeurs qui écrivent de nouveaux jeux ou qui
portent depuis d'autres moteurs des jeux qui reposent beaucoup sur la vidéo.
[#conventions]
=== Conventions
Chaque entrée de l'API ci-dessous suit la même structure :
* *Signature(s)* dans un bloc de code, montrant chaque forme acceptée.
* *Description* de ce que fait la fonction et de ses éventuels effets de bord.
* *Paramètres* listés en ligne, ou sous forme de tableau quand il y en a plus de deux.
* Ligne *Renvoie* décrivant la valeur rendue à Lua (omise quand la fonction ne renvoie rien).
* *Remarques*, s'il y a des pièges qui méritent d'être signalés.
* *Depuis* -- la version de Singe dans laquelle la fonction est apparue.
* *Voir aussi* -- les fonctions associées que vous utiliserez probablement au même endroit.
* *Exemple* -- un court extrait réaliste, à la forme d'un vrai code de jeu.
Une entrée qui commence par *Extension Hypseus* est là pour qu'un jeu écrit pour
Hypseus Singe tourne ici sans modification. Il en va de même pour une forme
d'argument marquée comme étant celle d'Hypseus sur une fonction qui prend
sinon celle de Singe.
IMPORTANT: *N'écrivez pas de nouveaux jeux pour l'API Hypseus.* Ces entrées
existent pour la compatibilité, pas comme une recommandation. Plusieurs d'entre
elles répondent une valeur fixe parce que la chose qu'elles décrivent n'existe
pas dans Singe, certaines sont acceptées et ignorées, et d'autres portent des
échelles et des ordres d'arguments hérités d'un autre
moteur -- `videoGetVolume()` compte jusqu'à `64` là où le reste de Singe compte jusqu'à
`100`, et les noms `spriteAnim` prennent le sprite en dernier là où chaque appel
de sprite de Singe le prend en premier. Chaque entrée nomme l'équivalent Singe à
utiliser à la place. Un nouveau code qui s'en tient à l'API Singe est plus
court, cohérent avec lui-même, et ne sera pas surpris quand une réponse de
compatibilité reste constante.
Quelques règles s'appliquent à toute l'API :
* Les *couleurs* sont sur 8 bits par canal : le rouge, le vert, le bleu et l'opacité sont chacun des entiers de `0` à `255`.
* Les *coordonnées de l'overlay* sont dans la résolution actuelle de l'overlay (fixée par `overlaySetResolution`), pas en pixels de l'écran. La résolution par défaut de l'overlay est la moitié de la résolution de la vidéo dans chaque dimension ; appelez `overlaySetResolution` pour la changer.
* Les *handles* renvoyés par les fonctions `*Load` (sprites, sons, polices, vidéos) sont des entiers opaques. Repassez-les tels quels aux autres fonctions de cet espace de noms. Ne faites pas de calculs avec.
* *Les arguments invalides mettent fin au script.* Si une fonction est appelée avec un mauvais nombre d'arguments, de mauvais types, ou un handle qui a déjà été déchargé, Singe met fin au script en cours avec une erreur -- elle ne renvoie ni `nil` ni un code d'échec. Validez les saisies de l'utilisateur dans votre script avant de les confier à l'API.
* *Le dessin doit se faire depuis `onOverlayUpdate`.* Les appels à toute fonction `overlay*`, `spriteDraw`, `videoDraw` ou `fontPrint` depuis d'autres callbacks ne se verront pas à l'écran et peuvent corrompre l'overlay. C'est la règle qui piège tous les nouveaux développeurs Singe.
[#animation]
=== Animation
Ces appels pilotent les animations que porte un modèle, instance par instance.
Chacun prend le *nœud racine* renvoyé par `modelInstance` ; les autres nœuds
lèvent une erreur, sauf avec `animationIsPlaying`, qui répond `false`. Les
animations sont numérotées à partir de `1` dans l'ordre où `modelGetAnimations`
les liste, ou nommées comme le fichier les nomme. Un clip joue sur la couche de
base, et jusqu'à quatre autres peuvent jouer sur les couches `1` à `4` au-dessus
d'elle ; les appels de temps, de pause, de reprise et de saut ne visent que la
couche de base. Le temps est en secondes, avance avec l'horloge d'au plus un
dixième de seconde par image, et s'arrête pendant que le jeu est en pause.
L'animation des nœuds, le skinning et
les poids des morph targets sont tous pilotés. Voir la partie Animation de
<<scenes3d,Scènes 3D>>.
[#animationgettime]
==== animationGetTime
[source,text]
----
seconds = animationGetTime(node)
----
Le temps courant dans le clip de la couche de base, en lecture ou en pause, en secondes propres au clip (une `speed` de `2` le fait avancer deux fois plus vite). Un clip en boucle revient à `0` à sa fin ; un clip sans boucle s'arrête à sa durée. Si rien n'a encore joué, la réponse est `0`. Les clips des couches supérieures ne sont pas rapportés. Un nœud qui n'est pas une racine d'instance met fin au script.
*Renvoie :* Les secondes, sous forme de nombre.
*Depuis :* 3.00.
*Voir aussi :* <<animationsettime,animationSetTime>>, <<animationisplaying,animationIsPlaying>>
.Exemple
[source,lua]
----
-- Jouer le bruit de pas une fois à chaque passage du cycle de marche par ses deux points de contact.
function onOverlayUpdate()
local t = animationGetTime(hero)
if (lastStepTime < 0.25 and t >= 0.25) or (lastStepTime < 0.75 and t >= 0.75) then
soundPlay(footstep)
end
lastStepTime = t
return OVERLAY_NOT_UPDATED
end
----
[#animationisplaying]
==== animationIsPlaying
[source,text]
----
playing = animationIsPlaying(node)
----
Indique si le clip de la couche de base avance sur cette instance. Un clip sans boucle cesse de jouer quand il atteint sa fin, et `animationPause` comme `animationStop` rendent cette valeur `false` ; un clip en boucle reste `true` jusqu'à ce qu'il soit arrêté. Contrairement aux autres appels de cette section, un nœud qui n'est pas une racine d'instance ne met pas fin au script mais répond `false`.
*Renvoie :* Booléen.
*Depuis :* 3.00.
*Voir aussi :* <<animationplay,animationPlay>>, <<animationpause,animationPause>>
.Exemple
[source,lua]
----
-- Retour au repos une fois que l'attaque unique est terminée.
function onOverlayUpdate()
if attacking and not animationIsPlaying(hero) then
attacking = false
animationPlay(hero, "Idle", true, 1, 0.2)
end
return OVERLAY_NOT_UPDATED
end
----
[#animationpause]
==== animationPause
[source,text]
----
animationPause(node)
----
Fige le clip de la couche de base à son temps courant ; les nœuds gardent cette pose jusqu'à ce que `animationResume` le reprenne ou qu'un autre clip soit joué. Les clips des couches supérieures continuent de tourner. Mettre en pause une instance sur laquelle rien ne joue est sans danger. Un nœud qui n'est pas une racine d'instance met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<animationresume,animationResume>>, <<animationsettime,animationSetTime>>
.Exemple
[source,lua]
----
-- Une cinématique fige la foule pendant que la caméra bouge.
function beginCutscene()
for _, npc in ipairs(crowd) do
animationPause(npc)
end
cameraSet(cutsceneCamera)
end
----
[#animationplay]
==== animationPlay
[source,text]
----
animationPlay(node, nameOrIndex)
animationPlay(node, nameOrIndex, loop)
animationPlay(node, nameOrIndex, loop, speed)
animationPlay(node, nameOrIndex, loop, speed, fade)
----
Démarre l'un des clips du modèle depuis son début sur la couche de base de cette instance, en remplaçant ce qui y jouait ; la pose se met à jour aussitôt. Avec un `fade`, l'ancien clip continue de tourner pendant qu'il s'estompe sur ce nombre de secondes, et quand rien ne jouait, le fondu part de la pose où se trouvent les nœuds à présent (un ragdoll qui vient d'être désactivé, une pose fixée par le script), les nœuds que le nouveau clip n'anime pas revenant en douceur à la pose de repos du modèle. Un nœud qui n'est pas une racine d'instance, un nom que le modèle n'a pas, ou un numéro hors de `1` au nombre de clips met fin au script.
*Paramètres :*
* `node` -- le nœud racine de l'instance, venant de `modelInstance`.
* `nameOrIndex` -- un nom de clip tel que le fichier le stocke, ou son numéro à partir de `1` dans l'ordre de `modelGetAnimations`.
* `loop` -- booléen, `true` par défaut. À `false`, le clip s'arrête à sa dernière image clé et `animationIsPlaying` devient `false`.
* `speed` -- vitesse de lecture, `1` par défaut. `2` est deux fois plus rapide ; `0.5` deux fois plus lent.
* `fade` -- durée en secondes du fondu enchaîné depuis ce qui jouait, `0` par défaut (un passage sec).
*Depuis :* 3.00.
*Voir aussi :* <<modelgetanimations,modelGetAnimations>>, <<animationplaylayer,animationPlayLayer>>, <<animationstop,animationStop>>, <<animationsettime,animationSetTime>>
.Exemple
[source,lua]
----
-- Un garde qui reste au repos jusqu'à ce que le joueur soit repéré, puis court.
guardModel = modelLoad(DIR .. "Models/Guard.glb")
guard = modelInstance(guardModel)
animationPlay(guard, "Idle")
function onSpotted()
animationPlay(guard, "Run", true, 1.3, 0.4)
end
----
[#animationplaylayer]
==== animationPlayLayer
[source,text]
----
animationPlayLayer(node, layer, nameOrIndex)
animationPlayLayer(node, layer, nameOrIndex, loop)
animationPlayLayer(node, layer, nameOrIndex, loop, speed)
animationPlayLayer(node, layer, nameOrIndex, loop, speed, fade)
----
Joue un clip sur l'une des quatre couches au-dessus de la base, mélangé par-dessus ce qui se trouve en dessous selon le poids de la couche (`animationSetLayerWeight`, `1` par défaut) et limité à une partie du squelette quand un masque est défini (`animationSetLayerMask`). Les couches supérieures se mélangent par-dessus les inférieures ; chaque couche fait tourner son propre clip, son temps et sa boucle indépendamment de la base. `nameOrIndex`, `loop`, `speed` et `fade` ont le même sens que dans `animationPlay`, à une différence près : un `fade` sur une couche qui était vide fait monter le poids de la couche depuis `0` au lieu de faire un fondu enchaîné depuis une pose. Une couche hors de `1` à `4`, un nœud qui n'est pas une racine d'instance, ou un clip que le modèle n'a pas met fin au script.
*Paramètres :*
* `node` -- le nœud racine de l'instance.
* `layer` -- `1` à `4`.
* `nameOrIndex` -- un nom ou un numéro de clip, comme dans `animationPlay`.
* `loop` -- `true` par défaut.
* `speed` -- `1` par défaut.
* `fade` -- en secondes, `0` par défaut.
*Depuis :* 3.00.
*Voir aussi :* <<animationplay,animationPlay>>, <<animationsetlayermask,animationSetLayerMask>>, <<animationsetlayerweight,animationSetLayerWeight>>, <<animationstop,animationStop>>
.Exemple
[source,lua]
----
-- Marcher avec tout le corps, regarder alentour avec la tête seulement.
animationPlay(fox, "Walk", true)
animationPlayLayer(fox, 1, "Survey", true, 1, 0.5)
animationSetLayerMask(fox, 1, nodeFind("b_Neck_04", fox))
-- Plus tard, saluer d'un bras par-dessus ce que font les jambes.
function onGreet()
animationPlayLayer(fox, 2, "Wave", false, 1, 0.2)
end
----
[#animationresume]
==== animationResume
[source,text]
----
animationResume(node)
----
Reprend le clip de la couche de base là où `animationPause` l'a figé, ou là où `animationSetTime` l'a déplacé entre-temps. Reprendre un clip qui joue déjà ne change rien. Une instance sans clip sur sa couche de base (jamais joué, ou arrêté) met fin au script, de même qu'un nœud qui n'est pas une racine d'instance.
*Depuis :* 3.00.
*Voir aussi :* <<animationpause,animationPause>>, <<animationsettime,animationSetTime>>
.Exemple
[source,lua]
----
function endCutscene()
for _, npc in ipairs(crowd) do
animationResume(npc)
end
cameraSet(gameCamera)
end
----
[#animationsetlayermask]
==== animationSetLayerMask
[source,text]
----
animationSetLayerMask(node, layer, maskNode)
animationSetLayerMask(node, layer)
----
Limite une couche à `maskNode` et à tout ce qui se trouve sous lui, si bien que le clip de cette couche ne déplace que cette partie du modèle et que le reste continue de suivre les couches en dessous. `maskNode` doit être l'un des nœuds propres de l'instance, trouvé avec `nodeFind(name, node)` ; la racine de l'instance elle-même ne compte pas. Omettre `maskNode`, ou passer `nil`, lève la limite. Le masque reste sur la couche lors des appels `animationPlayLayer` suivants. Une couche hors de `1` à `4` met fin au script, tout comme un nœud qui n'est pas une racine d'instance ou un `maskNode` qui ne fait pas partie de cette instance.
*Paramètres :*
* `node` -- le nœud racine de l'instance.
* `layer` -- `1` à `4`.
* `maskNode` -- une articulation ou un nœud de l'instance ; omis ou `nil` pour tout le modèle.
*Depuis :* 3.00.
*Voir aussi :* <<animationplaylayer,animationPlayLayer>>, <<nodefind,nodeFind>>, <<animationsetlayerweight,animationSetLayerWeight>>
.Exemple
[source,lua]
----
-- Pointer le haut du corps vers la cible pendant que les jambes continuent de marcher.
spine = nodeFind("b_Spine02_03", hero)
animationSetLayerMask(hero, 1, spine)
animationPlayLayer(hero, 1, "AimForward", true)
function onHolster()
animationStop(hero, 1)
animationSetLayerMask(hero, 1)
end
----
[#animationsetlayerweight]
==== animationSetLayerWeight
[source,text]
----
animationSetLayerWeight(node, layer, weight)
----
Dans quelle mesure le clip d'une couche se voit par-dessus ce qui est en dessous, de `0` (pas du tout) à `1` (entièrement, la valeur par défaut) ; les valeurs hors de cet intervalle sont bornées. Le poids appartient à la couche, pas au clip, si bien qu'il survit à `animationPlayLayer` et à `animationStop` sur cette couche. Changez-le un peu à chaque image pour faire apparaître ou disparaître une couche à la main, ou laissez un `fade` sur `animationPlayLayer` le faire une fois. Une couche hors de `1` à `4` ou un nœud qui n'est pas une racine d'instance met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<animationplaylayer,animationPlayLayer>>, <<animationsetlayermask,animationSetLayerMask>>
.Exemple
[source,lua]
----
-- La boiterie s'aggrave à mesure que la santé baisse.
function onHealthChanged(health)
local limp = 1 - health / maxHealth
animationSetLayerWeight(hero, 1, limp)
end
----
[#animationsettime]
==== animationSetTime
[source,text]
----
animationSetTime(node, seconds)
----
Fait sauter le clip de la couche de base à un temps exprimé en ses propres secondes ; les valeurs négatives deviennent `0`. La pose se met à jour aussitôt, que le clip joue ou non, si bien qu'une instance en pause peut être amenée à une image exacte, et qu'une instance en lecture continue depuis le nouveau temps. Tout fondu enchaîné en cours sur la couche de base est interrompu. Une instance sans clip sur sa couche de base met fin au script, de même qu'un nœud qui n'est pas une racine d'instance.
*Depuis :* 3.00.
*Voir aussi :* <<animationgettime,animationGetTime>>, <<animationpause,animationPause>>, <<animationresume,animationResume>>
.Exemple
[source,lua]
----
-- Une porte dont le clip d'ouverture suit un levier que le joueur fait glisser.
animationPlay(door, "Open", false)
animationPause(door)
function onLeverMoved(fraction)
animationSetTime(door, fraction * doorClipLength)
end
----
[#animationstop]
==== animationStop
[source,text]
----
animationStop(node)
animationStop(node, layer)
----
Arrête et oublie tous les clips de l'instance, ou seulement le clip d'une couche au-dessus de la base (`1` à `4`) ; la couche de base seule ne peut pas être arrêtée sans les couches au-dessus d'elle. Les nœuds gardent leur dernière pose, dont un `animationPlay` suivant avec un `fade` les fait sortir en douceur. Après avoir tout arrêté, `animationResume` et `animationSetTime` mettent fin au script jusqu'à ce que quelque chose soit de nouveau joué. Les poids et les masques des couches sont conservés. Une couche hors de `1` à `4` ou un nœud qui n'est pas une racine d'instance met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<animationplay,animationPlay>>, <<animationplaylayer,animationPlayLayer>>, <<animationpause,animationPause>>
.Exemple
[source,lua]
----
-- Confier le corps au ragdoll physique : figer la pose, puis le laisser tomber.
function onKilled()
animationStop(hero)
ragdollActivate(hero)
end
-- Le salut sur la couche 2 est fini ; la marche en dessous continue.
function onWaveFinished()
animationStop(hero, 2)
end
----
[#bezelref]
=== Bezel
Les illustrations de borne et l'afficheur de scores d'arcade qui va avec. `mainBezelLoaded`
renseigne sur les illustrations que `--bezel` a chargées ; la famille `scoreBezel` pilote le
panneau des scores, un document RmlUi que le moteur fournit et qu'un jeu peut remplacer par sa
propre copie, un `scoreBezel.rml` à côté de son script. Chaque appel de cette section est une
extension d'Hypseus Singe, conservée pour que ses jeux tournent sans modification, et chacun d'eux est décrit du point de vue du joueur dans
xref:Manual.adoc#bezels[Bezels].
Le panneau démarre éteint, comme dans Hypseus, et seul un jeu l'allume. Les valeurs qui lui
sont données sont mémorisées qu'il soit affiché ou non, si bien qu'un jeu peut les régler avant
d'activer le panneau. Chaque afficheur a le nombre de chiffres qu'a l'afficheur de scores
d'Hypseus -- six pour un score, deux pour les crédits, un pour un nombre de vies -- et une valeur y
est affichée alignée à droite avec des zéros en tête ; un score de plus de six chiffres montre
ses six derniers. Les valeurs négatives d'Hypseus sont comprises aussi : `-2` allume un tiret
dans chaque chiffre, `-4` un `H` et `-5` un `A`, et `-1`, `-3` et tout autre nombre négatif
n'allument rien.
[#mainbezelloaded]
==== mainBezelLoaded
[source,text]
----
loaded, id = mainBezelLoaded()
----
Extension Hypseus. Indique si `--bezel` a chargé des illustrations de borne, et l'identifiant
du fichier chargé. L'identifiant est le hachage FNV-1a sur 64 bits des octets du fichier, écrit
sous forme de chaîne décimale, ce que rapporte Hypseus, si bien qu'un jeu qui reconnaît un bezel
particulier à son numéro reconnaît ici le même fichier. Sans illustration chargée, les réponses
sont `false` et `"0"`.
*Renvoie :* booléen, et l'identifiant sous forme de chaîne.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<setoverlayontop,setOverlayOnTop>>, xref:Manual.adoc#bezels[Bezels]
.Exemple
[source,lua]
----
-- Ne dessiner le cadre en sprite que si le joueur n'a pas d'illustrations à lui.
local loaded, id = mainBezelLoaded()
if not loaded then
spriteDraw(frameSprite, 0, 0)
end
----
[#scorebezelclear]
==== scoreBezelClear
[source,text]
----
scoreBezelClear()
----
Extension Hypseus. Vide chaque afficheur du panneau des scores : les crédits, les deux scores et
les deux nombres de vies n'affichent plus rien du tout, ne laissant que les chiffres atténués
derrière eux. Les valeurs ne sont pas oubliées, mais le prochain `scoreBezelCredits`,
`scoreBezelScore` ou `scoreBezelLives` rallume le panneau avec ce qu'il contient alors. Peut être
appelé sans risque quand le panneau est éteint.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<scorebezelenable,scoreBezelEnable>>
[#scorebezelcredits]
==== scoreBezelCredits
[source,text]
----
scoreBezelCredits(credits)
----
Extension Hypseus. Règle l'afficheur des crédits, qui a deux chiffres. Rallume aussi un
panneau que `scoreBezelClear` a vidé.
*Paramètres :*
* `credits` -- entier ; les deux derniers chiffres sont affichés.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<scorebezelscore,scoreBezelScore>>
[#scorebezelenable]
==== scoreBezelEnable
[source,text]
----
scoreBezelEnable(enabled)
scoreBezelEnable(enabled, type)
----
Extension Hypseus. Affiche ou masque le panneau des scores. Le premier `true` le construit
à partir du `scoreBezel.rml` propre au jeu, à côté de son script, quand il y en a un, ou
sinon à partir de `Singe/scoreBezel.rml` ; le masquer et l'afficher de nouveau ensuite ne
coûte rien et conserve ses valeurs. Il démarre masqué, ce qui est aussi le cas d'Hypseus à sa livraison, si
bien qu'un jeu qui n'appelle jamais ceci se comporte exactement comme avant -- et devrait
interroger <<scorebezelgetstate,`scoreBezelGetState`>> avant de supposer qu'il en a un.
Le `type` d'Hypseus choisit parmi ses cinq implémentations d'afficheur de scores. Singe n'en a
qu'une, un document RmlUi, si bien que l'argument est vérifié et ignoré. Une machine sans GPU n'a
aucune GUI ; le panneau ne peut pas y être construit, `scoreBezelGetState` continue de répondre
`false`, et un avertissement est affiché une fois. Un premier argument non booléen met fin au
script.
*Paramètres :*
* `enabled` -- booléen.
* `type` -- entier, accepté et ignoré.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<scorebezelgetstate,scoreBezelGetState>>, xref:Manual.adoc#bezels[Bezels]
.Exemple
[source,lua]
----
-- Le panneau du moteur quand il y en a un, des sprites sinon.
scoreBezelEnable(true, 0)
if scoreBezelGetState() then
scoreBezelTwinScoreOn(twoPlayer)
scoreBezelCredits(credits)
else
drawSpriteScoreboard()
end
----
[#scorebezelgetstate]
==== scoreBezelGetState
[source,text]
----
state = scoreBezelGetState()
----
Extension Hypseus. Indique si le panneau des scores est affiché. Chaque jeu Hypseus pose cette
question avant d'utiliser le panneau et dessine son propre afficheur de scores avec des sprites
quand la réponse est `false`, ce qui est le bon repli et non un échec.
*Renvoie :* booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<scorebezelenable,scoreBezelEnable>>
[#scorebezellives]
==== scoreBezelLives
[source,text]
----
scoreBezelLives(player, lives)
----
Extension Hypseus. Règle l'afficheur des vies d'un joueur, qui a un chiffre. `player` vaut
`1` ou `2` ; tout autre nombre est ignoré, et `2` l'est aussi tant que
<<scorebezeltwinscoreon,`scoreBezelTwinScoreOn(true)`>> n'a pas mis les afficheurs du second
joueur sur le panneau. Rallume aussi un panneau que `scoreBezelClear` a vidé.
*Paramètres :*
* `player` -- `1` ou `2`.
* `lives` -- entier ; le dernier chiffre est affiché.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<scorebezelscore,scoreBezelScore>>
[#scorebezelscore]
==== scoreBezelScore
[source,text]
----
scoreBezelScore(player, score)
----
Extension Hypseus. Règle l'afficheur du score d'un joueur, qui a six chiffres. `player`
vaut `1` ou `2`, avec la même règle pour `2` que `scoreBezelLives`. Rallume aussi un panneau
que `scoreBezelClear` a vidé.
*Paramètres :*
* `player` -- `1` ou `2`.
* `score` -- entier ; les six derniers chiffres sont affichés.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<scorebezellives,scoreBezelLives>>
.Exemple
[source,lua]
----
function addScore(points)
score = score + points
if scoreBezelGetState() then
scoreBezelScore(1, score)
end
end
----
[#scorebezeltwinscoreon]
==== scoreBezelTwinScoreOn
[source,text]
----
scoreBezelTwinScoreOn(enabled)
----
Extension Hypseus. Met sur le panneau les afficheurs de score et de vies du second joueur, ou
les retire et laisse la place aux deux autres. Les désactiver vide aussi le panneau, exactement
comme dans Hypseus, alors faites suivre d'une valeur si les afficheurs du premier joueur doivent
rester allumés. Tant qu'ils sont désactivés,
`scoreBezelScore(2, ...)` et `scoreBezelLives(2, ...)` ne font rien. Un argument non booléen
met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<scorebezelscore,scoreBezelScore>>
[#body]
=== Corps
Un corps donne à un nœud de la scène une place dans le monde physique. Il y a un corps par nœud, désigné par le handle du nœud : un corps statique reste là où était le nœud quand le corps a été créé, un corps cinématique suit son nœud à chaque pas et pousse sans être poussé, et un corps dynamique est déplacé par la simulation et entraîne son nœud. Les tailles, les positions et les vitesses sont en unités du monde (des mètres dans une scène 3D, des pixels de l'overlay dans un monde 2D), les masses en kilogrammes et le temps en secondes. Chaque appel de cette section met fin au script quand le nœud n'a pas de corps ; un appel qui n'a de sens que pour une sorte de corps (une force sur un corps statique, par exemple) est ignoré sur les autres, sauf `bodySetMass`, qui lève une erreur. Voir <<physics,Physique>> dans Développement de jeux pour la façon dont les corps, les nœuds et l'image s'articulent.
[#bodynew]
==== bodyNew
[source,text]
----
bodyNew(node, type, shape)
bodyNew(node, type, shape, a)
bodyNew(node, type, shape, a, b)
bodyNew(node, type, shape, a, b, c)
----
Donne au nœud un corps du type indiqué, placé là où le nœud se trouve dans le monde à cet instant (ses parents compris), avec une forme de collision dimensionnée par `a`, `b` et `c` en unités du monde et mise à l'échelle par l'échelle du nœud dans le monde. Un second appel sur le même nœud remplace le premier, et un nœud qui avait un joueur le perd. La masse d'un corps dynamique vient du volume de sa forme à la densité de l'eau, alors réglez `bodySetMass` sur tout ce qui doit paraître léger. Changer l'échelle du nœud plus tard ne redimensionne pas le corps. Met fin au script pour un type ou une forme inconnus, quand la physique n'est pas disponible sur cette machine, quand le monde contient déjà `4096` corps, ou quand la forme ne peut pas être construite (un `SHAPE_MESH` dynamique, ou une enveloppe sans maillage sous le nœud).
*Paramètres :*
* `type` -- `BODY_STATIC`, `BODY_DYNAMIC` ou `BODY_KINEMATIC`.
* `shape` -- `SHAPE_BOX` (`a`, `b`, `c` sont la largeur, la hauteur et la profondeur), `SHAPE_SPHERE` (`a` est le rayon), `SHAPE_CAPSULE` ou `SHAPE_CYLINDER` (`a` est le rayon, `b` la hauteur totale le long de Y, extrémités comprises), `SHAPE_HULL` (une enveloppe convexe du maillage du nœud et de tout ce qui est sous lui, sans tailles), ou `SHAPE_MESH` (les triangles exacts, corps statiques et cinématiques seulement ; un maillage de carte de hauteur devient un champ de hauteurs).
* `a`, `b`, `c` -- les tailles dont la forme a besoin ; celles qui manquent valent `0` et toute taille est portée à au moins `0.001`.
*Remarques :* Une sphère est mise à l'échelle par le plus grand axe d'échelle du nœud, le rayon d'une capsule ou d'un cylindre par le plus grand de X et de Z. Une capsule plus courte que deux fois son rayon est une sphère avec une fine tranche au milieu.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetmass,bodySetMass>>, <<bodydelete,bodyDelete>>, <<physicssetgravity,physicsSetGravity>>, <<playernew,playerNew>>
.Exemple
[source,lua]
----
-- Une caisse légère et un modèle de canard en enveloppe, lâchés sur le sol.
crate = nodeNew()
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
nodeSetPosition(crate, -1, 1, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
bodySetMass(crate, 5)
duck = modelInstance(modelLoad(DIR .. "Models/Duck.glb"))
nodeSetPosition(duck, 3, 2, 0)
bodyNew(duck, BODY_DYNAMIC, SHAPE_HULL)
----
[#bodyapplyforce]
==== bodyApplyForce
[source,text]
----
bodyApplyForce(node, fx, fy, fz)
bodyApplyForce(node, fx, fy, fz, px, py, pz)
----
Ajoute une force, en newtons, à un corps dynamique pour les pas de cette image seulement ; appelez-la à chaque image pour continuer à pousser. Sans point, la force agit par le centre de masse et ne fait que déplacer le corps ; avec un point du monde `(px, py, pz)`, elle fait aussi tourner le corps autour de son centre. Une force accélère de la force divisée par la masse, alors dimensionnez-la selon `bodySetMass`. Les corps statiques et cinématiques l'ignorent.
*Paramètres :*
* `fx`, `fy`, `fz` -- la force en newtons le long de chaque axe du monde.
* `px`, `py`, `pz` -- point du monde facultatif où la force s'applique ; omettez-le pour le centre de masse.
*Depuis :* 3.00.
*Voir aussi :* <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetmass,bodySetMass>>
.Exemple
[source,lua]
----
-- Un ventilateur pousse le ballon à travers la pièce tant que l'interrupteur est enclenché.
function onOverlayUpdate()
if fanOn then
local bx, by, bz = nodeGetPosition(balloon)
if bx < fanReach then
bodyApplyForce(balloon, 2.5, 0.4, 0)
end
end
return OVERLAY_NOT_UPDATED
end
----
[#bodyapplyimpulse]
==== bodyApplyImpulse
[source,text]
----
bodyApplyImpulse(node, ix, iy, iz)
bodyApplyImpulse(node, ix, iy, iz, px, py, pz)
----
Change d'un coup la quantité de mouvement d'un corps dynamique : une impulsion égale à la masse multipliée par une vitesse ajoute cette vitesse. Sans point, elle agit par le centre de masse ; avec un point du monde `(px, py, pz)`, elle fait aussi tourner le corps, si bien que frapper une caisse près de son sommet la fait basculer. C'est l'appel pour un coup, un coup de pied, une explosion ou un lancement, là où `bodyApplyForce` sert à une poussée qui dure. Les corps statiques et cinématiques l'ignorent.
*Paramètres :*
* `ix`, `iy`, `iz` -- l'impulsion en kilogrammes mètres par seconde le long de chaque axe du monde.
* `px`, `py`, `pz` -- point du monde facultatif où l'impulsion s'applique ; omettez-le pour le centre de masse.
*Depuis :* 3.00.
*Voir aussi :* <<bodyapplyforce,bodyApplyForce>>, <<bodysetvelocity,bodySetVelocity>>, <<physicsraycast,physicsRaycast>>
.Exemple
[source,lua]
----
-- Projeter droit vers le haut ce qui se trouve sous la souris, au point touché.
local mx, my = mouseGetPosition(0)
local ox, oy, oz = sceneUnproject(mx, my, 0)
local fx, fy, fz = sceneUnproject(mx, my, 10)
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
if hit and hit ~= floor then
bodyApplyImpulse(hit, 0, 3, 0, hx, hy, hz)
end
----
[#bodydelete]
==== bodyDelete
[source,text]
----
bodyDelete(node)
----
Retire le corps du monde ; le nœud et son maillage restent où ils sont. Chaque articulation attachée au corps, et tout véhicule construit dessus, disparaissent avec lui. Supprimer le nœud supprime aussi son corps, si bien que ceci ne sert qu'à garder un nœud sans son corps. Met fin au script quand le nœud n'a pas de corps.
*Depuis :* 3.00.
*Voir aussi :* <<bodynew,bodyNew>>, <<bodysetenabled,bodySetEnabled>>, <<jointdelete,jointDelete>>
.Exemple
[source,lua]
----
-- Une pièce ramassée cesse d'être solide et s'élève comme un simple nœud.
function onTrigger(trigger, other, entered)
if trigger == coin and other == hero and entered then
bodyDelete(coin)
collected = collected + 1
coinRising = true
end
end
----
[#bodygetangularvelocity]
==== bodyGetAngularVelocity
[source,text]
----
x, y, z = bodyGetAngularVelocity(node)
----
La vitesse à laquelle le corps tourne autour de chaque axe du monde, en radians par seconde, au dernier pas. Un corps statique renvoie zéro. Utile pour la hauteur du son d'une roue ou un compteur de tours.
*Renvoie :* Trois nombres, en radians par seconde autour de X, Y et Z.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetangularvelocity,bodySetAngularVelocity>>, <<bodygetvelocity,bodyGetVelocity>>
.Exemple
[source,lua]
----
-- La roue de la roulette s'est arrêtée quand elle tourne à peine.
function onOverlayUpdate()
local _, spin = bodyGetAngularVelocity(wheel)
if spinning and math.abs(spin) < 0.05 then
spinning = false
announceWinner(pocketUnder(ball))
end
return OVERLAY_NOT_UPDATED
end
----
[#bodygetvelocity]
==== bodyGetVelocity
[source,text]
----
x, y, z = bodyGetVelocity(node)
----
La vitesse à laquelle le corps se déplace le long de chaque axe du monde, en unités du monde par seconde, au dernier pas. Un corps statique renvoie zéro ; un corps cinématique renvoie la vitesse qu'impliquait le mouvement de son nœud.
*Renvoie :* Trois nombres, en unités par seconde le long de X, Y et Z.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetvelocity,bodySetVelocity>>, <<bodygetangularvelocity,bodyGetAngularVelocity>>
.Exemple
[source,lua]
----
-- Jouer un bruit de souffle quand la balle va vraiment vite.
function onOverlayUpdate()
local vx, vy, vz = bodyGetVelocity(ball)
local speed = math.sqrt(vx * vx + vy * vy + vz * vz)
if speed > 8 and not whooshing then
soundPlay(whoosh)
whooshing = true
elseif speed < 2 then
whooshing = false
end
return OVERLAY_NOT_UPDATED
end
----
[#bodyisresting]
==== bodyIsResting
[source,text]
----
resting = bodyIsResting(node)
----
Indique si la simulation a mis le corps en sommeil parce qu'il a cessé de bouger. Les corps statiques et les corps retirés du monde avec `bodySetEnabled` comptent comme au repos. Un corps au repos se réveille de lui-même quand quelque chose le touche ou quand une vitesse, une force ou une impulsion lui est appliquée.
*Renvoie :* Booléen.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetenabled,bodySetEnabled>>, <<bodysetvelocity,bodySetVelocity>>
.Exemple
[source,lua]
----
-- La manche se termine une fois que toutes les quilles se sont stabilisées.
function onOverlayUpdate()
local settled = true
for _, pin in ipairs(pins) do
settled = settled and bodyIsResting(pin)
end
if settled then
scoreFrame()
end
return OVERLAY_NOT_UPDATED
end
----
[#bodysetangularvelocity]
==== bodySetAngularVelocity
[source,text]
----
bodySetAngularVelocity(node, x, y, z)
----
Règle la rotation autour de chaque axe du monde, en radians par seconde, et réveille le corps. Les corps statiques l'ignorent. La rotation d'un corps cinématique est écrasée à partir de son nœud au pas suivant, si bien que l'appel ne dure que pour les corps dynamiques.
*Depuis :* 3.00.
*Voir aussi :* <<bodygetangularvelocity,bodyGetAngularVelocity>>, <<bodysetvelocity,bodySetVelocity>>
.Exemple
[source,lua]
----
-- Lancer la pièce : vers le haut, en tournant sur elle-même.
function tossCoin()
nodeSetPosition(coin, 0, 1.2, 0)
bodySetVelocity(coin, 0, 4, 0)
bodySetAngularVelocity(coin, 25, 0, 0)
tossed = true
end
----
[#bodysetbounce]
==== bodySetBounce
[source,text]
----
bodySetBounce(node, bounce)
----
Règle la restitution du corps, de `0` (s'arrête net) à `1` (rebondit avec tout ce qu'il avait en arrivant) ; les valeurs hors de cet intervalle sont bornées. La valeur par défaut est `0.1`. Les valeurs des deux corps en contact se combinent, si bien qu'une balle vive sur un sol mort rebondit quand même.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetfriction,bodySetFriction>>, <<bodynew,bodyNew>>
.Exemple
[source,lua]
----
-- Une balle en caoutchouc lâchée sur la scène.
ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.3, 32), blue)
nodeSetPosition(ball, 0, 3, 0)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.3)
bodySetMass(ball, 0.5)
bodySetBounce(ball, 0.7)
----
[#bodysetbuoyancy]
==== bodySetBuoyancy
[source,text]
----
bodySetBuoyancy(node, factor)
----
La façon dont le corps flotte quand il est dans un volume d'eau : `1` est neutre, plus flotte plus haut, moins coule, et `0` est une pierre. La valeur par défaut est `1.2`, qui flotte doucement. Le facteur multiplie la densité propre de l'eau donnée par `bodySetWater`, et les valeurs négatives sont ramenées à `0`. Seuls les corps dynamiques sont déplacés par l'eau, mais la valeur peut être réglée sur n'importe quel corps.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetwater,bodySetWater>>, <<bodysetcurrent,bodySetCurrent>>
.Exemple
[source,lua]
----
-- Une caisse qui flotte haut, une ancre qui coule, une balle qui danse sur l'eau.
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
bodySetMass(crate, 20)
bodySetBuoyancy(crate, 1.6)
bodyNew(anchor, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
bodySetMass(anchor, 200)
bodySetBuoyancy(anchor, 0.3)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.4)
bodySetBuoyancy(ball, 2.0)
----
[#bodysetcurrent]
==== bodySetCurrent
[source,text]
----
bodySetCurrent(node, x, y, z)
----
Règle le courant à l'intérieur d'un volume d'eau, en unités du monde par seconde le long de chaque axe du monde ; les corps dynamiques qui y flottent dérivent avec le courant et un joueur qui nage est emporté par lui. Par défaut, il n'y a pas de courant. Réglez-le sur le nœud sur lequel `bodySetWater` a été appelé ; sur tout autre corps, il est stocké et ne fait rien.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
.Exemple
[source,lua]
----
-- Une rivière qui emporte vers l'aval tout ce qu'on y lâche.
river = nodeNew()
nodeSetMesh(river, meshBox(6, 3, 12), water)
nodeSetPosition(river, 10, -1.5, 0)
bodyNew(river, BODY_STATIC, SHAPE_BOX, 6, 3, 12)
bodySetWater(river, 1.0, 0.8, 0.2)
bodySetCurrent(river, 0, 0, 2.5)
----
[#bodysetenabled]
==== bodySetEnabled
[source,text]
----
bodySetEnabled(node, enabled)
----
Retire le corps du monde avec `false` : il cesse d'entrer en collision, cesse de bouger, n'entraîne plus son nœud ni ne le suit, et compte comme au repos. `true` le remet là où il est, éveillé. Le corps garde sa forme, sa masse et ses réglages, si bien que c'est moins coûteux que de le supprimer et de le recréer pour quelque chose qui va et vient.
*Depuis :* 3.00.
*Voir aussi :* <<bodyisresting,bodyIsResting>>, <<bodydelete,bodyDelete>>
.Exemple
[source,lua]
----
-- Le pont-levis n'est solide que lorsqu'il est baissé.
function lowerBridge()
animationPlay(bridge, "Lower", false)
bodySetEnabled(bridgeDeck, true)
end
function raiseBridge()
animationPlay(bridge, "Raise", false)
bodySetEnabled(bridgeDeck, false)
end
----
[#bodysetfriction]
==== bodySetFriction
[source,text]
----
bodySetFriction(node, friction)
----
Règle le frottement de la surface : `0` est de la glace, `0.5` (la valeur par défaut) est ordinaire, les valeurs plus élevées adhèrent davantage. Les valeurs négatives sont ramenées à `0`. Les valeurs des deux corps en contact se combinent, si bien qu'un sol glissant demande une valeur basse sur le sol, pas sur tout ce qui marche dessus.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetbounce,bodySetBounce>>, <<bodynew,bodyNew>>
.Exemple
[source,lua]
----
-- Un étang gelé au milieu du niveau.
pond = nodeNew()
nodeSetMesh(pond, meshBox(6, 0.1, 6), ice)
nodeSetPosition(pond, 8, -1.95, 0)
bodyNew(pond, BODY_STATIC, SHAPE_BOX, 6, 0.1, 6)
bodySetFriction(pond, 0.02)
----
[#bodysetmass]
==== bodySetMass
[source,text]
----
bodySetMass(node, kilograms)
----
Remet à l'échelle la masse d'un corps dynamique et, avec elle, son inertie. Faites-le pour tout ce qui doit paraître léger ou lourd : par défaut, c'est le volume de la forme à la densité de l'eau, si bien qu'une boîte de la taille d'une porte pèse environ 140 kilogrammes et qu'une légère poussée ne la fera pas bouger. Les forces et les impulsions s'échelonnent avec la masse, alors réglez la masse avant de les ajuster. Met fin au script quand le corps n'est pas dynamique ou que la masse n'est pas positive.
*Depuis :* 3.00.
*Voir aussi :* <<bodynew,bodyNew>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<playersetpush,playerSetPush>>
.Exemple
[source,lua]
----
-- Une caisse que le héros peut pousser et une enclume que personne ne peut bouger.
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
bodySetMass(crate, 5)
bodyNew(anvil, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.3, 0.3)
bodySetMass(anvil, 200)
----
[#bodysettrigger]
==== bodySetTrigger
[source,text]
----
bodySetTrigger(node, trigger)
----
Fait du corps un déclencheur avec `true` : il ne pousse rien et n'arrête rien, mais `onTrigger` signale chaque corps ou joueur qui y entre et en sort. `false` le rend de nouveau solide : ce qui était à l'intérieur est oublié sans sortie signalée, tandis qu'un déclencheur dans lequel se trouve le corps lui-même continue de le signaler. Un déclencheur est généralement une boîte ou une sphère statique placée sur une porte, une ligne d'arrivée ou une zone dangereuse. Les rayons de `physicsRaycast` touchent les déclencheurs comme n'importe quel autre corps.
*Remarques :* Un joueur est signalé dans un déclencheur quand leurs boîtes englobantes se chevauchent, si bien qu'un déclencheur rond est carré pour les joueurs.
*Depuis :* 3.00.
*Voir aussi :* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>, <<bodysetwater,bodySetWater>>
.Exemple
[source,lua]
----
-- Une ligne d'arrivée en travers de la piste.
line = nodeNew()
nodeSetPosition(line, 0, 0, -20)
bodyNew(line, BODY_STATIC, SHAPE_BOX, 6, 3, 0.5)
bodySetTrigger(line, true)
function onTrigger(trigger, other, entered)
if trigger == line and other == car and entered then
lapDone()
end
end
----
[#bodysetvelocity]
==== bodySetVelocity
[source,text]
----
bodySetVelocity(node, x, y, z)
----
Règle la vitesse du corps le long de chaque axe du monde, en unités du monde par seconde, et le réveille. Ceci remplace la vitesse qu'avait le corps, là où `bodyApplyImpulse` s'y ajoute. Les corps statiques l'ignorent, et la vitesse d'un corps cinématique est écrasée à partir du mouvement de son nœud au pas suivant.
*Depuis :* 3.00.
*Voir aussi :* <<bodygetvelocity,bodyGetVelocity>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<bodysetangularvelocity,bodySetAngularVelocity>>
.Exemple
[source,lua]
----
-- Servir la balle vers le côté qui a perdu le dernier point.
function serve()
nodeSetPosition(ball, 0, 0.5, 0)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.15)
bodySetMass(ball, 0.1)
local dir = (lastWinner == 1) and -1 or 1
bodySetVelocity(ball, 4 * dir, 2, (math.random() - 0.5) * 2)
end
----
[#bodysetwater]
==== bodySetWater
[source,text]
----
bodySetWater(node, density)
bodySetWater(node, density, linearDrag)
bodySetWater(node, density, linearDrag, angularDrag)
----
Remplit d'eau un corps statique et en fait un déclencheur : le dessus de sa forme est la surface, et chaque corps dynamique à l'intérieur flotte, coule ou roule par flottabilité par rapport à cette surface (la partie immergée de la forme compte, si bien qu'une caisse inclinée se redresse), ralenti par les traînées. Un joueur dont le milieu est sous la surface nage. Comme le volume est un déclencheur, `onTrigger` se déclenche à l'entrée et à la sortie. L'eau n'est pas dessinée : donnez au nœud un matériau en mélange, ou aucun. Met fin au script quand le corps n'est pas statique.
*Paramètres :*
* `density` -- un multiplicateur, `1` pour l'eau ; le `bodySetBuoyancy` propre à chaque corps le module. Les valeurs négatives sont ramenées à `0`.
* `linearDrag` -- à quelle vitesse l'eau ralentit le mouvement qui la traverse ; `0.5` par défaut.
* `angularDrag` -- à quelle vitesse elle ralentit la rotation ; `0.1` par défaut.
*Remarques :* Les joueurs et les bateaux voient au plus huit volumes d'eau ; au-delà, les volumes font encore flotter les corps mais on ne peut pas y nager.
*Depuis :* 3.00.
*Voir aussi :* <<bodysetbuoyancy,bodySetBuoyancy>>, <<bodysetcurrent,bodySetCurrent>>, <<bodysettrigger,bodySetTrigger>>, <<playerisswimming,playerIsSwimming>>
.Exemple
[source,lua]
----
-- Une piscine creusée dans le pont ; son dessus, à Y = 0, est la surface.
pool = nodeNew()
nodeSetMesh(pool, meshBox(8, 3, 12), water)
nodeSetPosition(pool, 0, -1.5, 0)
bodyNew(pool, BODY_STATIC, SHAPE_BOX, 8, 3, 12)
bodySetWater(pool, 1.0, 0.6, 0.2)
function onTrigger(trigger, other, entered)
if trigger == pool and entered then
soundPlay(splash)
end
end
----
[#camera]
=== Caméra
La caméra est un nœud comme un autre : placez-la avec `nodeSetPosition`, orientez-la avec `nodeLookAt`, rattachez-la à un objet en mouvement pour une vue de poursuite, et désignez-la avec `cameraSet`. Elle regarde le long de son propre -Z. Sans nœud caméra, la scène est vue depuis `(0, 0, 5)` en regardant l'origine. La projection est réglée à part et s'applique au nœud, quel qu'il soit, qui est la caméra : perspective avec un champ de vision vertical de 60 degrés de `0.1` à `1000` unités par défaut, ou orthographique avec `cameraSetOrthographic`. Les vues rendues (`viewNew`) partagent la projection mais regardent depuis leurs propres nœuds. Voir <<scenes3d,Scènes 3D>>.
[#cameraset]
==== cameraSet
[source,text]
----
cameraSet(node)
cameraSet(-1)
----
Fait d'un nœud la caméra : la scène est dessinée depuis sa position dans le monde en regardant le long de son propre -Z, si bien que le mouvement et la rotation de ses parents comptent. N'importe quel nœud convient, y compris un nœud qui porte aussi un maillage ou une lumière, et changer de caméra ne demande qu'un appel. `-1` rétablit la vue par défaut depuis `(0, 0, 5)` en regardant l'origine. Supprimer le nœud caméra ramène aussi à la vue par défaut. Met fin au script pour un handle de nœud qui n'est pas valide.
*Depuis :* 3.00.
*Voir aussi :* <<nodelookat,nodeLookAt>>, <<camerasetperspective,cameraSetPerspective>>, <<viewsetcamera,viewSetCamera>>, <<nodenew,nodeNew>>
.Exemple
[source,lua]
----
-- Une caméra de poursuite derrière la voiture du joueur, et une caméra fixe pour la rediffusion.
chaseCamera = nodeNew(car)
nodeSetPosition(chaseCamera, 0, 2, 6)
nodeLookAt(chaseCamera, nodeGetWorldPosition(car))
cameraSet(chaseCamera)
function onReplayStart()
replayCamera = nodeNew()
nodeSetPosition(replayCamera, 30, 12, 30)
nodeLookAt(replayCamera, 0, 0, 0)
cameraSet(replayCamera)
end
----
[#camerasetorthographic]
==== cameraSetOrthographic
[source,text]
----
cameraSetOrthographic(height, near, far)
----
Passe à une projection parallèle montrant `height` unités du monde de haut en bas, la largeur suivant le format d'image de la couche, de `near` à `far` devant la caméra. La distance ne rapetisse pas les choses, ce qui convient aux schémas, aux vues isométriques et de profil, et aux scènes façon HUD. Le réglage s'applique au nœud caméra actuel et à tout nœud caméra ultérieur jusqu'à l'appel de `cameraSetPerspective`. L'ombre d'une lumière directionnelle utilise une seule carte sous une caméra orthographique, quoi que dise `sceneSetShadowCascades`.
*Paramètres :*
* `height` -- la hauteur visible, en unités du monde.
* `near` -- la distance la plus proche dessinée, en unités du monde devant la caméra.
* `far` -- la distance la plus lointaine dessinée.
*Depuis :* 3.00.
*Voir aussi :* <<camerasetperspective,cameraSetPerspective>>, <<cameraset,cameraSet>>, <<scenesetshadowcascades,sceneSetShadowCascades>>
.Exemple
[source,lua]
----
-- Une vue isométrique du plateau : 12 unités de haut, vue de haut et de côté.
boardCamera = nodeNew()
nodeSetPosition(boardCamera, 20, 20, 20)
nodeLookAt(boardCamera, 0, 0, 0)
cameraSet(boardCamera)
cameraSetOrthographic(12, 1, 100)
----
[#camerasetperspective]
==== cameraSetPerspective
[source,text]
----
cameraSetPerspective(fov, near, far)
----
Passe à la projection en perspective habituelle, celle par défaut. `fov` est le champ de vision vertical en degrés (`60` par défaut) ; `near` et `far` (`0.1` et `1000` par défaut) bornent ce qui est dessiné, en unités du monde devant la caméra. La précision de la profondeur est répartie entre les deux, alors gardez `near` aussi grand que la scène le permet et `far` pas plus grand que nécessaire : `0.05` à `100` pour une pièce, `0.1` à `400` pour un paysage. Le réglage s'applique au nœud caméra actuel et à tout nœud caméra ultérieur, ainsi qu'aux vues rendues.
*Paramètres :*
* `fov` -- champ de vision vertical en degrés ; un large `70` donne une impression de vitesse, un étroit `40` celle d'un téléobjectif.
* `near` -- la distance la plus proche dessinée.
* `far` -- la distance la plus lointaine dessinée.
*Depuis :* 3.00.
*Voir aussi :* <<camerasetorthographic,cameraSetOrthographic>>, <<cameraset,cameraSet>>, <<sceneproject,sceneProject>>
.Exemple
[source,lua]
----
-- Un objectif plus serré pour la vue du cockpit, celui par défaut pour l'extérieur.
function setView(inside)
if inside then
cameraSet(cockpitCamera)
cameraSetPerspective(45, 0.05, 300)
else
cameraSet(chaseCamera)
cameraSetPerspective(60, 0.1, 400)
end
end
----
[#collide]
=== Collisions
Savoir si deux choses plates se touchent. Jolt y répond en trois dimensions pour
les corps (voir <<body,Corps>>) ; ces fonctions y répondent pour l'overlay, là où
vivent les hitbox d'un jeu au pistolet optique et absolument tout d'un jeu 2D.
Ce sont de simples fonctions : pas de handle, pas d'état, rien à créer ni à
libérer, et rien à mettre à jour à chaque image. Les coordonnées sont celles dans
lesquelles vous dessinez déjà -- des pixels de l'overlay, en général. Un rectangle
est un coin et une taille, comme le prend chaque appel de dessin du moteur. Se
toucher exactement sur un bord compte comme se toucher, et un rectangle sans
largeur ou sans hauteur ne touche rien du tout.
Aucune ne déplace quoi que ce soit. Quand vous voulez savoir non seulement *si*
deux choses se sont rencontrées, mais *où finit ce qui bouge* -- glisser le long
d'un mur plutôt que s'y arrêter net -- faites `require("bump")` et laissez-le s'en
charger ; il est fourni exactement pour cette raison.
[#collidecircles]
==== collideCircles
[source,text]
----
touching = collideCircles(x1, y1, radius1, x2, y2, radius2)
----
Indique si deux cercles se chevauchent. Moins coûteux qu'il n'y paraît : rien ne calcule de racine carrée pour répondre à une question par oui ou non.
*Paramètres :*
* `x1`, `y1`, `radius1` -- nombres ; le premier cercle.
* `x2`, `y2`, `radius2` -- nombres ; le second.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
*Voir aussi :* <<colliderectcircle,collideRectCircle>>
[#collidepointcircle]
==== collidePointCircle
[source,text]
----
inside = collidePointCircle(pointX, pointY, x, y, radius)
----
Indique si un point tombe dans un cercle. Le bord compte comme l'intérieur.
*Paramètres :*
* `pointX`, `pointY` -- nombres ; le point.
* `x`, `y`, `radius` -- nombres ; le cercle.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
[#collidepointpolygon]
==== collidePointPolygon
[source,text]
----
inside = collidePointPolygon(pointX, pointY, points)
----
Indique si un point tombe dans un polygone de forme quelconque, concaves compris. La table est une liste plate de `x`, `y`, `x`, `y` -- la forme qu'a déjà une hitbox dans la plupart des jeux -- et le sens de parcours n'a pas d'importance. Avec moins de trois sommets, le polygone ne contient rien. 256 sommets au plus.
*Paramètres :*
* `pointX`, `pointY` -- nombres ; le point.
* `points` -- table ; un nombre pair de nombres, x et y à tour de rôle.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
.Exemple
[source,lua]
----
-- Le tir a-t-il touché le dragon, dont le contour n'est pas un rectangle ?
local dragon = { 120,40, 200,60, 210,140, 150,180, 100,120 }
function onInputPressed(key)
if key == SWITCH_BUTTON1 then
local x, y = mouseGetPosition(0)
if collidePointPolygon(x, y, dragon) then
hit()
end
end
end
----
[#collidepointrect]
==== collidePointRect
[source,text]
----
inside = collidePointRect(pointX, pointY, x, y, width, height)
----
Indique si un point tombe dans un rectangle. Le bord compte comme l'intérieur.
*Paramètres :*
* `pointX`, `pointY` -- nombres ; le point.
* `x`, `y`, `width`, `height` -- nombres ; le rectangle.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
.Exemple
[source,lua]
----
-- Une hitbox de la taille du sprite auquel elle appartient.
local x, y = mouseGetPosition(0)
if collidePointRect(x, y, enemyX, enemyY, spriteGetWidth(enemy), spriteGetHeight(enemy)) then
kill(enemy)
end
----
[#colliderectcircle]
==== collideRectCircle
[source,text]
----
touching = collideRectCircle(x, y, width, height, circleX, circleY, radius)
----
Indique si un rectangle et un cercle se chevauchent, en cherchant le point du rectangle le plus proche du centre du cercle.
*Paramètres :*
* `x`, `y`, `width`, `height` -- nombres ; le rectangle.
* `circleX`, `circleY`, `radius` -- nombres ; le cercle.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
[#colliderects]
==== collideRects
[source,text]
----
touching = collideRects(x1, y1, width1, height1, x2, y2, width2, height2)
----
Indique si deux rectangles se chevauchent. Celle dont vous vous servirez le plus.
*Paramètres :*
* `x1`, `y1`, `width1`, `height1` -- nombres ; le premier rectangle.
* `x2`, `y2`, `width2`, `height2` -- nombres ; le second.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
*Voir aussi :* <<collidepointrect,collidePointRect>>
[#collidesegments]
==== collideSegments
[source,text]
----
crossing = collideSegments(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
----
Indique si deux segments se croisent : un tir le long d'une trajectoire contre un mur, ou le pas que veut faire un objet en mouvement contre le bord qu'il ne doit pas franchir. Des segments couchés l'un sur l'autre répondent `false`, car « où se croisent-ils » n'a alors pas de réponse unique.
*Paramètres :*
* `ax1`, `ay1`, `ax2`, `ay2` -- nombres ; les extrémités du premier segment.
* `bx1`, `by1`, `bx2`, `by2` -- nombres ; celles du second.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
[#color]
=== Couleur
Les couleurs de premier plan et de fond sont des globales du moteur plutôt que des arguments passés à chaque appel : réglez-les avec `colorForeground` et `colorBackground`, et chaque appel de dessin qui suit les lit jusqu'à ce qu'elles changent. La couleur de premier plan sert aux primitives de forme `overlay*` et au texte TrueType de `fontPrint` et `fontToSprite` ; la couleur de fond sert à `overlayClear` et au texte `FONT_QUALITY_SHADED`. `overlayPrint` n'utilise ni l'une ni l'autre. Les canaux sont des entiers de `0` à `255`, ramenés dans cette plage, et les deux couleurs acceptent un alpha facultatif. Voir les sections <<overlay,Overlay>> et <<font,Police>> pour les appels qui les utilisent.
[#colorbackground]
==== colorBackground
[source,text]
----
colorBackground(r, g, b)
colorBackground(r, g, b, a)
----
Règle la couleur de fond : celle dont `overlayClear` remplit l'overlay, et celle que le texte `FONT_QUALITY_SHADED` de `fontPrint` et `fontToSprite` peint derrière ses glyphes. Au démarrage, c'est un noir transparent, et l'alpha vaut `0` par défaut quand il est omis, si bien que `colorBackground(0, 0, 0)` suivi de `overlayClear()` rend de nouveau l'overlay invisible. Un alpha partiel sur du noir assombrit la vidéo ; `255` la masque. Les valeurs de canal hors plage sont ramenées dans la plage plutôt que rejetées.
*Paramètres :*
* `r`, `g`, `b` -- rouge, vert et bleu, de `0` à `255`.
* `a` -- opacité, de `0` (transparent) à `255` (opaque) ; `0` par défaut.
*Depuis :* 1.x
*Voir aussi :* <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>, <<fontquality,fontQuality>>
.Exemple
[source,lua]
----
-- Noir plein derrière l'écran de calibrage, transparent pendant la partie.
function onOverlayUpdate()
if calibrating then
colorBackground(0, 0, 0, 255)
else
colorBackground(0, 0, 0, 0)
end
overlayClear()
drawHud()
return OVERLAY_UPDATED
end
----
[#colorforeground]
==== colorForeground
[source,text]
----
colorForeground(r, g, b)
colorForeground(r, g, b, a)
----
Règle la couleur de premier plan qu'utilisent `overlayBox`, `overlayCircle`, `overlayEllipse`, `overlayLine` et `overlayPlot`, et la couleur du texte de `fontPrint` et `fontToSprite`. Au démarrage, c'est un blanc opaque, et l'alpha vaut `255` par défaut quand il est omis, à l'inverse de `colorBackground`. Les sprites et `overlayPrint` sont dessinés avec leurs propres pixels et ne sont pas teintés. Les valeurs de canal hors plage sont ramenées dans la plage plutôt que rejetées.
*Paramètres :*
* `r`, `g`, `b` -- rouge, vert et bleu, de `0` à `255`.
* `a` -- opacité, de `0` (transparent) à `255` (opaque) ; `255` par défaut.
*Depuis :* 1.x
*Voir aussi :* <<colorbackground,colorBackground>>, <<overlaybox,overlayBox>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
.Exemple
[source,lua]
----
-- Le score en blanc, l'alerte de dernière vie en rouge, tous deux avec la même police.
function onOverlayUpdate()
overlayClear()
fontSelect(hudFont)
colorForeground(255, 255, 255)
fontPrint(8, 8, "Score: " .. score)
if lives == 1 then
colorForeground(255, 32, 32)
fontPrint(8, 32, "LAST LIFE")
end
return OVERLAY_UPDATED
end
----
[#commandlineref]
=== Ligne de commande
Trois appels renvoient des nombres que l'utilisateur a choisis au lancement de
Singe. Ils existent pour qu'un jeu puisse être réglé depuis le lanceur plutôt
qu'en modifiant le jeu. Tous trois sont des extensions d'Hypseus Singe, et Singe
ne fait rien lui-même des valeurs : c'est le jeu qui décide de leur sens. Les
options sont décrites dans
xref:Manual.adoc#commandlineoptions[Options de ligne de commande], et elles peuvent aussi être données dans un
fichier de réglages (voir xref:Manual.adoc#settings[Le fichier de réglages]).
Aucun des trois ne change en cours d'exécution, alors lisez chacun une fois et gardez sa valeur.
[#getfvalue]
==== getFValue
[source,text]
----
value = getFValue()
----
Extension Hypseus. Renvoie le nombre donné avec `--fvalue`, ou `0` quand
l'option n'a pas été utilisée. C'est un canal d'un seul nombre, de celui qui a
lancé Singe vers le jeu, destiné à une valeur qu'un front-end veut faire varier
d'un lancement à l'autre -- une difficulté, un nombre de crédits de départ, un
indice de menu. Le moteur ne fait que la transmettre.
La valeur va de `0` à `100000` et garde trois décimales, comme Hypseus, si bien
qu'un jeu qui compare le nombre voit le même que là-bas.
*Renvoie :* nombre.
*Depuis :* 3.00 (Hypseus)
*Voir aussi :* <<ratiogetx,ratioGetX>>, <<ratiogety,ratioGetY>>
.Exemple
[source,lua]
----
-- Le lanceur passe --fvalue 2 pour la borne réglée en difficile.
local difficulty = getFValue()
if difficulty == 0 then
difficulty = 1
end
----
[#ratiogetx]
==== ratioGetX
[source,text]
----
factor = ratioGetX()
----
Extension Hypseus. Renvoie le nombre donné avec `--xratio`, ou `0` quand
l'option n'a pas été utilisée. Les jeux au pistolet optique écrits pour Hypseus le
lisent et étirent avec lui leurs propres coordonnées de pistolet, pour une borne
dont l'écran n'a pas la forme dans laquelle la vidéo a été masterisée -- `1.33`
fait correspondre une dalle 16:9 à une vidéo 4:3. Singe ne l'applique à rien ;
c'est le jeu qui fait le calcul, exactement comme dans Hypseus, et c'est pourquoi
chaque jeu qui le lit traite `0` et tout ce qui est inférieur à `1` comme `1`.
La valeur va de `0` à `100` et garde deux décimales, comme Hypseus.
*Renvoie :* nombre.
*Depuis :* 3.00 (Hypseus)
*Voir aussi :* <<ratiogety,ratioGetY>>, <<getfvalue,getFValue>>, <<mousegetposition,mouseGetPosition>>
.Exemple
[source,lua]
----
-- La forme des coordonnées de pistolet que veut cette borne.
local ratioX = ratioGetX()
if ratioX <= 1 then
ratioX = 1
end
local offsetX = (ratioX - 1) * (overlayGetWidth() / 2)
----
[#ratiogety]
==== ratioGetY
[source,text]
----
factor = ratioGetY()
----
Extension Hypseus. La moitié verticale de la paire : le nombre donné avec
`--yratio`, ou `0` quand l'option n'a pas été utilisée. Voir <<ratiogetx,ratioGetX>>.
*Renvoie :* nombre.
*Depuis :* 3.00 (Hypseus)
*Voir aussi :* <<ratiogetx,ratioGetX>>, <<getfvalue,getFValue>>
.Exemple
[source,lua]
----
local ratioY = ratioGetY()
if ratioY <= 1 then
ratioY = 1
end
----
[#controller]
=== Manette
Singe prend en charge jusqu'à quatre manettes, d'indices `0` à `3`, ouvertes automatiquement au démarrage et chaque fois qu'une manette est branchée ou débranchée ; il n'y a pas de `controllerLoad`. Les axes analogiques sont signalés par `onControllerMoved` (voir <<enginecallbacks,Callbacks du moteur>>) et interrogés avec `controllerGetAxis`, dans la plage brute de SDL, de `-32768` à `32767`. Les boutons numériques, et chaque direction de chaque axe, sont des contacts comme les touches : ils parviennent à `onInputPressed` et `onInputReleased` quand `controls.cfg` affecte un code des tables `GAMEPAD_0` à `GAMEPAD_3` (par exemple `INPUT_ACTION_1 = { GAMEPAD_0.BUTTON_A }`), et `controllerGetButton` interroge l'état réel d'un bouton. `SINGE_DEAD_ZONE`, tiré de l'entrée `DEAD_ZONE` de `controls.cfg`, est le seuil qu'utilise le moteur lui-même avant qu'une direction d'axe compte comme un contact pressé ; traitez les mouvements d'axe en dessous comme du bruit. Chaque axe peut recevoir son propre seuil avec `DEAD_ZONES`, et les gâchettes analogiques un seuil à elles avec `TRIGGER_THRESHOLD` (relu sous le nom `SINGE_TRIGGER_THRESHOLD`) ; voir xref:Manual.adoc#deadzones[Zones mortes et gâchettes]. Un périphérique que SDL ne reconnaît pas comme manette est tout de même ouvert comme tel, grâce à une affectation que Singe écrit pour lui, si bien qu'un encodeur d'arcade ou un joystick de vol apparaît dans ces emplacements comme n'importe quelle manette -- voir xref:Manual.adoc#devices[Tout périphérique est une manette].
[#controllerdorumble]
==== controllerDoRumble
[source,text]
----
controllerDoRumble(strength, duration)
controllerDoRumble(controller, strength, duration)
----
Extension Hypseus. Fait vibrer une manette avec ses deux moteurs, à l'une de quatre intensités pendant l'une de quatre durées. La forme à deux arguments s'adresse à la manette `0`. Un emplacement sans manette ne fait rien (Hypseus s'interrompt dans ce cas, si bien que ses jeux se protègent avec `controllerIsValid`) ; une intensité ou une durée hors de `1` à `4`, ou un indice hors de `0` à `3`, met fin au script. Une manette sans vibration ignore l'appel.
*Paramètres :*
* `controller` -- indice de la manette, de `0` à `3`.
* `strength` -- de `1` à `4`, d'un quart de la puissance à la pleine puissance.
* `duration` -- de `1` à `4`, soit 150, 300, 600 ou 1200 ms.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<controllerisvalid,controllerIsValid>>, <<controllergetbutton,controllerGetButton>>
.Exemple
[source,lua]
----
-- Une brève secousse à chaque coup, une longue à pleine puissance quand le joueur meurt.
function onHit()
if controllerIsValid(0) then
controllerDoRumble(0, 2, 1)
end
end
function onPlayerDied()
if controllerIsValid(0) then
controllerDoRumble(0, 4, 4)
end
end
----
[#controllergetaxis]
==== controllerGetAxis
[source,text]
----
value = controllerGetAxis(controller, axis)
----
Renvoie la position d'un axe analogique telle que le moteur l'a vue en dernier. La valeur est mise à jour à chaque événement d'axe, y compris les mouvements dans la zone morte, et vaut `0` pour une manette non connectée ou un axe qui n'a jamais bougé. Les sticks vont de `-32768` à `32767` ; les gâchettes reposent à `0` et atteignent `32767` enfoncées à fond. Un indice hors de `0` à `3`, ou un axe hors de `0` à `5`, met fin au script.
*Paramètres :*
* `controller` -- indice de la manette, de `0` à `3`.
* `axis` -- `GAMEPAD_AXIS_LEFT_X` (`0`), `GAMEPAD_AXIS_LEFT_Y` (`1`), `GAMEPAD_AXIS_RIGHT_X` (`2`), `GAMEPAD_AXIS_RIGHT_Y` (`3`), `GAMEPAD_AXIS_LEFT_TRIGGER` (`4`) ou `GAMEPAD_AXIS_RIGHT_TRIGGER` (`5`).
*Renvoie :* entier, de `-32768` à `32767`.
*Depuis :* 2.00
*Voir aussi :* <<controllergetbutton,controllerGetButton>>, <<oncontrollermoved,onControllerMoved>>
.Exemple
[source,lua]
----
-- Diriger avec le stick gauche, accélérer avec la gâchette droite.
function onOverlayUpdate()
local steer = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X)
local throttle = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER)
if math.abs(steer) > SINGE_DEAD_ZONE then
heading = heading + (steer / 32768) * turnRate
end
speed = speed + (throttle / 32767) * acceleration
drawHud()
return OVERLAY_UPDATED
end
----
[#controllergetbutton]
==== controllerGetButton
[source,text]
----
pressed = controllerGetButton(controller, button)
----
Indique si un bouton est maintenu à cet instant, lu directement sur la manette plutôt que dans la file d'événements. Servez-vous-en pour les charges en maintenant le bouton et les cas semblables où `onInputPressed` et `onInputReleased` sont malcommodes. Un emplacement sans manette renvoie `false`. Le code du bouton doit venir de la table `GAMEPAD_N` du même indice de manette ; un code de la table d'une autre manette, ou un code d'axe, tombe hors de la plage des boutons et met fin au script, tout comme un indice hors de `0` à `3`.
*Paramètres :*
* `controller` -- indice de la manette, de `0` à `3`.
* `button` -- une valeur `BUTTON_*` ou `DPAD_*` de la table correspondante, comme `GAMEPAD_0.BUTTON_A.value`.
*Renvoie :* booléen.
*Depuis :* 2.10
*Voir aussi :* <<controllergetaxis,controllerGetAxis>>, <<keyboardisdown,keyboardIsDown>>, <<oninputpressedoninputreleased,onInputPressed>>
.Exemple
[source,lua]
----
-- Charger tant que A est maintenu, tirer au relâchement.
function onOverlayUpdate()
if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then
charge = math.min(charge + 1, chargeMax)
elseif charge > 0 then
fireShot(charge)
charge = 0
end
drawChargeBar(charge)
return OVERLAY_UPDATED
end
----
[#controllergetname]
==== controllerGetName
[source,text]
----
name = controllerGetName(controller)
----
Le nom du périphérique présent dans un emplacement de manette, tel que SDL le donne, ou `nil` quand l'emplacement est vide. Un périphérique que SDL ne reconnaissait pas donne son propre nom par l'affectation que Singe a écrite pour lui, si bien qu'un écran de service peut lister un encodeur d'arcade par son nom aussi facilement qu'une manette de marque. Un indice hors de `0` à `3` met fin au script.
*Paramètres :*
* `controller` -- indice de la manette, de `0` à `3`.
*Renvoie :* chaîne, ou `nil`.
*Depuis :* 3.00
*Voir aussi :* <<controllerisvalid,controllerIsValid>>, <<mousegetname,mouseGetName>>
.Exemple
[source,lua]
----
-- Un écran de service qui liste ce qui est branché.
for slot = 0, 3 do
if controllerIsValid(slot) then
fontPrint(20, 20 + slot * 18, string.format("Pad %d: %s", slot + 1, controllerGetName(slot)))
else
fontPrint(20, 20 + slot * 18, string.format("Pad %d: not connected", slot + 1))
end
end
----
[#controllergetpadding]
==== controllerGetPadding
[source,text]
----
padding = controllerGetPadding()
----
Extension Hypseus, toujours `0` dans Singe. Hypseus ajoute ce décalage au code d'un contact venu d'une manette, pour qu'un jeu sache quel périphérique a pressé quoi tout en retrouvant le contact en le soustrayant. Singe livre les entrées des manettes comme des contacts affectés ordinaires qui ne portent aucune marque de ce genre, et répond donc `0` : un jeu qui teste la plage décalée prend son autre branche et lit le contact tel quel.
*Renvoie :* un entier, toujours `0`.
*Depuis :* 3.00
*Voir aussi :* <<controllersetpadding,controllerSetPadding>>
[#controllerhowmany]
==== controllerHowMany
[source,text]
----
count = controllerHowMany()
----
Extension Hypseus. Le nombre de manettes ouvertes, sur les quatre emplacements dont dispose Singe. Comme tout joystick que Singe peut ouvrir devient une manette (voir xref:Manual.adoc#devices[Tout périphérique est une manette]), un encodeur d'arcade ou un spinner est compté ici aussi.
Le compte change à mesure que des périphériques sont branchés et débranchés : lisez-le donc quand vous en avez besoin plutôt qu'une seule fois au démarrage. Il ne dit rien des emplacements occupés : un périphérique peut laisser l'emplacement 0 vide alors que l'emplacement 1 est utilisé, et <<controllerisvalid,`controllerIsValid`>> reste donc le moyen d'interroger un emplacement précis.
*Paramètres :* aucun.
*Renvoie :* nombre, de `0` à `4`.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<controllerisvalid,controllerIsValid>>, <<controllergetname,controllerGetName>>, <<mousehowmany,mouseHowMany>>
.Exemple
[source,lua]
----
-- Ne proposer le mode deux joueurs que si le joueur deux a quelque chose à tenir en main.
local twoPlayer = controllerHowMany() >= 2
----
[#controllerisvalid]
==== controllerIsValid
[source,text]
----
valid = controllerIsValid(controller)
----
Extension Hypseus. Indique si une manette est ouverte dans l'emplacement à cet instant ; les manettes vont et viennent au gré des branchements à chaud, alors posez la question avant chaque `controllerDoRumble`. Un indice hors de `0` à `3` met fin au script.
*Paramètres :*
* `controller` -- indice de la manette, de `0` à `3`.
*Renvoie :* booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<controllerdorumble,controllerDoRumble>>, <<controllergetaxis,controllerGetAxis>>
.Exemple
[source,lua]
----
-- N'afficher les indications pour la manette que s'il y a une manette.
function drawAttract()
if controllerIsValid(0) then
fontPrint(HINT_X, HINT_Y, "Press A to start")
else
fontPrint(HINT_X, HINT_Y, "Press 1 to start")
end
end
----
[#controllersetpadding]
==== controllerSetPadding
[source,text]
----
controllerSetPadding(boolean)
----
Extension Hypseus, acceptée et ignorée. Voir <<controllergetpadding,controllerGetPadding>> pour savoir pourquoi le décalage qu'elle demande n'existe pas dans Singe. L'activer sans marquer les événements serait pire que de la laisser désactivée, car le jeu soustrairait alors un décalage de codes de contact qui n'en ont jamais porté.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<controllergetpadding,controllerGetPadding>>
[#debug]
=== Débogage
Deux appels pour voir ce que fait un script : `debugPrint` écrit du texte, et `lineDraw` trace une ligne par-dessus la scène 3D pendant une image (`physicsSetDebug` et `navDraw` dessinent par ces mêmes lignes). La sortie affichée va à la console depuis laquelle Singe a été lancé : elle est donc visible pendant le développement et inoffensive dans une borne, où `--noconsole` la fait taire. Quand Singe tourne avec `--trace`, chaque appel de l'API, celui-ci compris, est aussi écrit avec ses arguments dans `trace.txt`, dans le dossier de données, ce qui est le moyen d'en garder un enregistrement permanent. Les options de ligne de commande sont listées dans le chapitre sur le lancement de Singe. Quand l'affichage ne suffit pas, <<zerobrane,ZeroBrane Studio>> arrête le jeu sur un point d'arrêt, montre ce que contiennent ses variables et le fait avancer ligne par ligne.
[[traceheader]]
==== L'en-tête de la trace
Que ce soit `--program` ou `--trace` qui l'ait ouvert, `trace.txt` commence par un
bloc qui décrit la machine et l'exécution, entre deux filets pour qu'on puisse le
coller tel quel dans un rapport de bogue :
----
----------------------------------------------------------------
Singe: v3.00
Command: ./Singe -w -p -d data --screen 2 MyGame
OS: Linux 6.8.0-124-generic (x86_64)
CPU: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz, 8 cores, 20177 MB
Renderer: gpu, GPU driver vulkan
Decoder: hardware vaapi or vdpau where the codec allows, software otherwise
SDL: built 3.4.16, linked 3.4.16
Settings: haptic=0 screen=2 volume_vldp=80
Game: MyGame/MyGame.singe
----------------------------------------------------------------
----
`Renderer` est le moteur de rendu effectivement créé et le backend GPU qu'il a
choisi, ou une note indiquant qu'il n'y a pas de périphérique GPU et que la 3D
n'est pas disponible. `Decoder` est ce qui sera essayé pour la vidéo sur cette
machine ; le décodeur qu'une vidéo donnée a réellement obtenu est tracé à
l'ouverture de cette vidéo, quelques lignes plus bas, car un codec peut n'offrir
aucune voie matérielle. `Settings` liste ce qu'a fourni le fichier de réglages,
`none` quand il n'y en avait pas. Chaque jeu d'une file d'attente a son propre
fichier et son propre en-tête.
[#debugprint]
==== debugPrint
[source,text]
----
debugPrint(message)
----
Affiche une ligne sur la console (sortie standard). Rien n'est affiché quand Singe a été lancé avec `--noconsole` ; avec `--trace`, l'appel et son message atterrissent aussi dans `trace.txt`. L'argument doit être une chaîne ou un nombre ; tout le reste met fin au script : enveloppez donc les valeurs dans `tostring` ou utilisez `string.format`. Il coûte assez peu pour rester dans le code livré, mais il n'est pas limité en débit, si bien qu'un affichage dans `onOverlayUpdate` s'exécute à chaque image.
*Paramètres :*
* `message` -- le texte à afficher ; un nombre est converti.
*Depuis :* 1.x
*Voir aussi :* <<singegetticks,singeGetTicks>>
.Exemple
[source,lua]
----
-- Noter pourquoi le tir a manqué, avec l'image où c'est arrivé.
function resolveShot(x, y)
local hit = targetAt(x, y)
if not hit then
debugPrint(string.format("miss at %d,%d on frame %d", x, y, discGetFrame()))
soundPlay(missClip)
return
end
score = score + hit.points
end
----
[#linedraw]
==== lineDraw
[source,text]
----
lineDraw(x0, y0, z0, x1, y1, z1)
lineDraw(x0, y0, z0, x1, y1, z1, r, g, b)
----
Trace une ligne droite entre deux points de l'espace du monde, par-dessus la scène, pour l'image en cours seulement, en blanc sauf si une couleur est donnée. Appelez-la à chaque image pour garder une ligne à l'écran, comme avec l'overlay. La ligne subit le test de profondeur contre la scène mais n'écrit pas la profondeur : elle se cache donc derrière les murs comme tout le reste et ne masque jamais rien. Rien n'est dessiné tant que la scène est désactivée. Elle sert à voir ce que fait un script (un lancer de rayon, un chemin, une boîte englobante) ; `physicsSetDebug` et `navDraw` dessinent par ces mêmes lignes.
*Paramètres :*
* `x0, y0, z0` -- le point de départ, en unités du monde.
* `x1, y1, z1` -- le point d'arrivée, en unités du monde.
* `r, g, b` -- la couleur de la ligne, de `0` à `255` par canal, ramenée dans la plage ; blanc si elle est omise.
*Depuis :* 3.00.
*Voir aussi :* <<physicssetdebug,physicsSetDebug>>, <<navdraw,navDraw>>, <<physicsraycast,physicsRaycast>>
.Exemple
[source,lua]
----
-- Montrer où vise la tourelle et si le tir porterait.
function onOverlayUpdate()
local tx, ty, tz = nodeGetWorldPosition(turret)
local hit, hx, hy, hz = physicsRaycast(tx, ty, tz, aimX, aimY, aimZ, 40)
if hit then
lineDraw(tx, ty, tz, hx, hy, hz, 255, 255, 0)
else
lineDraw(tx, ty, tz, tx + aimX * 40, ty + aimY * 40, tz + aimZ * 40, 120, 120, 120)
end
return OVERLAY_UPDATED
end
----
[#disc]
=== Disque
Le *disque* est la vidéo laserdisc principale, celle que Singe a été conçu à l'origine pour émuler. Un jeu a au plus un disque, ouvert au démarrage à partir de la `VIDEO` nommée dans `games.dat` ou de `--disc` / `--framefile` sur la ligne de commande ; un jeu qui n'en nomme aucune tourne sans disque (voir <<withoutadisc,Jeux sans disque>>). Toutes les fonctions `disc*` agissent sur ce disque unique et implicite et ne prennent pas de handle. Les numéros d'image sont des positions comptées à partir de zéro dans le fichier vidéo (ou dans le framefile combiné) et servent de référence temporelle à chaque décision d'embranchement d'un jeu laserdisc. Sans disque, les fonctions d'interrogation renvoient `0`, `discGetState` signale `DISC_EJECTED`, et les fonctions de transport écrivent une trace et ne font rien. Les clips supplémentaires superposés au disque sont l'affaire de la famille <<video,Vidéo>>.
[#discaudio]
==== discAudio
[source,text]
----
discAudio(channel, enabled)
----
Active ou coupe l'un des canaux de la bande-son du disque. Le canal est réglé sur le volume laserdisc configuré (`--volume_vldp`, `100` par défaut) quand `enabled` vaut `true`, et sur `0` quand il vaut `false` ; l'autre canal reste tel quel. Beaucoup de titres d'origine portaient une narration ou une piste de données sur un seul canal, et c'est cet appel qui la fait taire. Un canal autre que `1` ou `2` met fin au script. Sans disque, l'appel ne fait rien.
*Paramètres :*
* `channel` -- `1` pour le canal gauche, `2` pour le droit.
* `enabled` -- booléen. `true` remet le canal au volume laserdisc configuré, `false` le rend muet.
*Depuis :* 1.x
*Voir aussi :* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<videosetvolume,videoSetVolume>>
.Exemple
[source,lua]
----
-- Le canal droit de ce disque porte la piste de données de la borne d'arcade.
function startGame()
discAudio(1, true)
discAudio(2, false)
discSkipToFrame(SCENE_FIRST_FRAME)
end
----
[#discaudiosuffix]
==== discAudioSuffix
[source,text]
----
opened = discAudioSuffix(suffix)
----
Extension Hypseus. Fait passer le disque à un fichier audio séparé placé à côté de sa vidéo : `<base><suffix>.ogg`, où la base est le nom du fichier vidéo sans son extension ; ainsi `movie.m2v` avec `"-es"` joue `movie-es.ogg`. Le fichier est ouvert à l'image en cours et la lecture se réaligne, comme avec `discSetAudioTrack`, en gardant le volume, l'état de lecture et l'indice de piste quand le nouveau fichier a autant de pistes. Avec un framefile, tous les segments changent. Un suffixe vide rétablit l'audio par défaut : le `.ogg` à côté d'un `.m2v`, ou l'audio contenu dans tout autre conteneur. Renvoie `true` quand le fichier s'est ouvert ; quand il manque, l'audio précédent continue de jouer, rien n'est mémorisé et `false` est renvoyé (Hypseus laisse alors le silence). Le suffixe est oublié quand le script est rechargé. Sans disque, l'appel renvoie `false`. Servez-vous-en pour un menu de langues dont les pistes sont livrées en fichiers séparés ; les pistes contenues dans un même fichier sont l'affaire de `discSetAudioTrack`.
*Paramètres :*
* `suffix` -- chaîne ajoutée au nom de base de la vidéo, comme `"-es"` ; `""` pour l'audio par défaut.
*Renvoie :* booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<discsetaudiotrack,discSetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discaudio,discAudio>>
.Exemple
[source,lua]
----
-- Les fichiers de langue sont livrés sous les noms movie.ogg, movie-es.ogg et movie-fr.ogg.
languages = { { "English", "" }, { "Spanish", "-es" }, { "French", "-fr" } }
function applyLanguage(index)
if not discAudioSuffix(languages[index][2]) then
discAudioSuffix("")
index = 1
end
settings.language = index
end
----
[#discchangespeed]
==== discChangeSpeed
[source,text]
----
discChangeSpeed(...)
----
Ne fait rien. Les vrais lecteurs de laserdisc pouvaient tourner à une autre vitesse que 1x ; le lecteur logiciel de Singe ne le peut pas, donc l'appel accepte n'importe quels arguments, écrit une ligne de trace et rend la main. Il est conservé pour que les scripts antérieurs à 2.00 se chargent sans modification.
*Remarques :*
WARNING: *Non implémenté.* Conservé uniquement pour la rétrocompatibilité. Il n'y a pas de remplacement ; la vitesse de lecture est fixée à la fréquence d'images du fichier vidéo.
*Depuis :* 1.x
*Voir aussi :* <<discplay,discPlay>>, <<discskiptoframe,discSkipToFrame>>
.Exemple
[source,lua]
----
-- D'anciens scripts encadraient parfois un ralenti par des changements de vitesse.
-- Ces appels sont inoffensifs aujourd'hui, mais le ralenti passe à vitesse normale.
discChangeSpeed(0.5)
discSkipToFrame(REPLAY_FIRST_FRAME)
discChangeSpeed(1)
----
[#discgetaudiotrack]
==== discGetAudioTrack
[source,text]
----
track = discGetAudioTrack()
----
Renvoie l'indice, compté à partir de zéro, de la piste audio que joue actuellement le disque. Au démarrage, le moteur choisit la piste désignée par `--audio` ou par l'entrée `AUDIO_TRACK` de `games.dat` (`0` par défaut) quand le disque a autant de pistes. Renvoie `0` sans disque.
*Renvoie :* indice de piste entier.
*Depuis :* 2.10
*Voir aussi :* <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>, <<discgetlanguage,discGetLanguage>>
.Exemple
[source,lua]
----
-- Passer au doublage suivant chaque fois que le joueur appuie sur la touche de langue.
function onInputPressed(what)
if what == SWITCH_BUTTON2 then
local next = (discGetAudioTrack() + 1) % discGetAudioTracks()
discSetAudioTrack(next)
languageLabel = discGetLanguageDescription(discGetLanguage(next))
end
end
----
[#discgetaudiotracks]
==== discGetAudioTracks
[source,text]
----
count = discGetAudioTracks()
----
Renvoie le nombre de flux audio que contient le fichier vidéo du disque. Plusieurs flux sont en général des doublages dans d'autres langues. Une vidéo muette renvoie `0`, tout comme un jeu sans disque : protégez donc les boucles et les calculs modulo contre le zéro.
*Renvoie :* nombre entier, `0` ou plus.
*Depuis :* 2.10
*Voir aussi :* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetlanguage,discGetLanguage>>, <<discsetaudiotrack,discSetAudioTrack>>
.Exemple
[source,lua]
----
-- Construire un menu de choix de langue à partir des pistes audio du disque.
languages = {}
for track = 0, discGetAudioTracks() - 1 do
local code = discGetLanguage(track)
local name = discGetLanguageDescription(code)
table.insert(languages, { track = track, label = name })
end
----
[#discgetsubtitlelanguage]
==== discGetSubtitleLanguage
[source,text]
----
language = discGetSubtitleLanguage(track)
----
La langue indiquée sur une piste de sous-titres contenue dans le conteneur même du disque, sous la forme du code ISO 639 que porte le fichier (`eng`, `fra`, `spa`). Une piste que le fichier n'étiquette pas renvoie une chaîne vide, ce qui n'est pas une erreur -- bien des fichiers n'étiquettent rien.
*Paramètres :*
* `track` -- nombre ; de `0` à `discGetSubtitleTracks() - 1`.
*Renvoie :* une chaîne.
*Depuis :* 3.00.
*Voir aussi :* <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtloadtrack,srtLoadTrack>>
[#discgetsubtitletracks]
==== discGetSubtitleTracks
[source,text]
----
count = discGetSubtitleTracks()
----
Le nombre de pistes de sous-titres que contient le conteneur même du disque. `0` sans disque, et `0` pour un disque dont les sous-titres sont livrés dans un `.srt` séparé -- c'est la disposition habituelle, et ce à quoi sert <<srtload,`srtLoad`>>.
Les pistes qui contiennent des images plutôt que des mots (celles des DVD et des Blu-ray, VobSub et PGS) sont comptées ici pour que la numérotation corresponde à ce que montre n'importe quel autre lecteur, mais <<srtloadtrack,`srtLoadTrack`>> ne peut pas les lire : il n'y a rien à lire, seulement des bitmaps des mots.
*Paramètres :*
* Aucun.
*Renvoie :* un nombre.
*Depuis :* 3.00.
*Voir aussi :* <<srtloadtrack,srtLoadTrack>>, <<discgetsubtitlelanguage,discGetSubtitleLanguage>>
.Exemple
[source,lua]
----
-- Proposer toutes les langues que porte le disque lui-même.
for track = 0, discGetSubtitleTracks() - 1 do
languages[#languages + 1] = { track = track, code = discGetSubtitleLanguage(track) }
end
----
[#discgetframe]
==== discGetFrame
[source,text]
----
frame = discGetFrame()
----
Renvoie l'image que le disque montre en ce moment. Avec un framefile, le numéro est la position dans la séquence combinée, si bien que les limites des segments sont invisibles pour le script. Renvoie `0` tant que le disque est arrêté, et sans disque. Interrogez-la depuis `onOverlayUpdate` pour réagir quand la vidéo atteint un point de décision.
*Renvoie :* numéro d'image entier.
*Depuis :* 1.x
*Voir aussi :* <<discsearch,discSearch>>, <<discskiptoframe,discSkipToFrame>>, <<discgetstate,discGetState>>
.Exemple
[source,lua]
----
-- Boucler la séquence d'attraction et afficher une invite pendant la fenêtre où il faut se baisser.
function onOverlayUpdate()
local frame = discGetFrame()
if frame >= ATTRACT_LAST_FRAME then
discSkipToFrame(ATTRACT_FIRST_FRAME)
elseif frame >= DUCK_FIRST_FRAME and frame <= DUCK_LAST_FRAME then
drawPrompt("DUCK")
end
return OVERLAY_UPDATED
end
----
[#discgetframecount]
==== discGetFrameCount
[source,text]
----
frames = discGetFrameCount()
----
Le nombre d'images du disque, soit un de plus que la plus haute image que
<<discsearch,discSearch>> trouvera. Un framefile répond pour tout le disque
plutôt que pour le segment en cours de lecture : le compte ne change donc pas à
mesure que les segments vont et viennent. Sans disque, il répond `0`.
*Renvoie :* nombre d'images entier, `0` sans disque.
*Depuis :* 3.00.
*Voir aussi :* <<discgetframe,discGetFrame>>, <<discsearch,discSearch>>
.Exemple
[source,lua]
----
-- Sauter au milieu du disque, où qu'il soit.
discSearch(math.floor(discGetFrameCount() / 2))
----
[#discgetheight]
==== discGetHeight
[source,text]
----
height = discGetHeight()
----
Renvoie la hauteur de la vidéo du disque, en pixels. Sans disque, elle renvoie la hauteur du canevas (`CANVAS_Y` dans `games.dat` ou `--canvas`, `480` par défaut) : c'est donc toujours la taille du fond sur lequel l'overlay est étiré. Associez-la à `discGetWidth` pour donner à l'overlay la même résolution que la vidéo.
*Renvoie :* hauteur entière en pixels.
*Depuis :* 2.00
*Voir aussi :* <<discgetwidth,discGetWidth>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetheight,vldpGetHeight>>
.Exemple
[source,lua]
----
-- Dessiner à la résolution native du disque pour que les tests de pixels et le dessin partagent une même grille.
overlaySetResolution(discGetWidth(), discGetHeight())
crosshairLimitX = discGetWidth() - 1
crosshairLimitY = discGetHeight() - 1
----
[#discgetlanguage]
==== discGetLanguage
[source,text]
----
code = discGetLanguage(track)
----
Renvoie l'étiquette de langue stockée dans le fichier vidéo pour l'une des pistes audio du disque. L'étiquette est un code ISO 639 de trois lettres comme `"eng"` ou `"jpn"`. Les pistes sans étiquette de trois lettres renvoient `"unk"`, comme toutes les pistes quand il n'y a pas de disque. Avec un disque, une piste hors de `0` à `discGetAudioTracks() - 1` met fin au script.
*Renvoie :* code de langue sous forme de chaîne.
*Depuis :* 2.10
*Voir aussi :* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discsetaudiotrack,discSetAudioTrack>>
.Exemple
[source,lua]
----
-- Préférer le doublage japonais quand le disque en porte un.
for track = 0, discGetAudioTracks() - 1 do
if discGetLanguage(track) == "jpn" then
discSetAudioTrack(track)
break
end
end
----
[#discgetstate]
==== discGetState
[source,text]
----
state = discGetState()
----
Renvoie l'état de transport du disque sous la forme de l'une des constantes `DISC_*`. Un jeu sans disque signale `DISC_EJECTED` ; après `discStop`, le disque signale `DISC_STOPPED` jusqu'au prochain `discPlay`, `discSearch` ou `discSkipToFrame` ; sinon l'état est `DISC_PLAYING` tant que la vidéo avance, et `DISC_PAUSED` quand elle n'avance pas. Il n'y a pas d'état de recherche distinct ; un disque en cours de saut signale `DISC_PAUSED` ou `DISC_PLAYING` selon ce qui a été demandé.
*Renvoie :* l'une des constantes suivantes, définies par le moteur et accessibles à tous les scripts.
[cols="1,1,1",options="header"]
|===
| Constante | Valeur | Signification
| `DISC_STOPPED` | `2` | Arrêté (`discStop` a été appelé)
| `DISC_PLAYING` | `3` | En lecture
| `DISC_PAUSED` | `4` | En pause
| `DISC_EJECTED` | `5` | Le jeu n'a pas de disque (voir <<withoutadisc,Jeux sans disque>>)
|===
*Depuis :* 1.x (RDG)
*Voir aussi :* <<discplay,discPlay>>, <<discpause,discPause>>, <<discstop,discStop>>
.Exemple
[source,lua]
----
-- La touche pause ne doit pas relancer un arrêt sur image demandé par le script de l'histoire.
function onInputPressed(what)
if what == SWITCH_PAUSE then
if discGetState() == DISC_PAUSED and not storyPaused then
discPlay()
elseif discGetState() == DISC_PLAYING then
discPause()
end
end
end
----
[#discgetwidth]
==== discGetWidth
[source,text]
----
width = discGetWidth()
----
Renvoie la largeur de la vidéo du disque, en pixels. Sans disque, elle renvoie la largeur du canevas (`CANVAS_X` dans `games.dat` ou `--canvas`, `720` par défaut). Avec `discGetHeight`, c'est la taille sur laquelle l'overlay est mis à l'échelle, et c'est pourquoi le menu fourni passe les deux à `overlaySetResolution`.
*Renvoie :* largeur entière en pixels.
*Depuis :* 2.00
*Voir aussi :* <<discgetheight,discGetHeight>>, <<overlaysetresolution,overlaySetResolution>>, <<vldpgetwidth,vldpGetWidth>>
.Exemple
[source,lua]
----
-- Centrer un sprite de titre, quelle que soit la largeur du disque.
overlaySetResolution(discGetWidth(), discGetHeight())
titleSprite = spriteLoad("graphics/title.png")
titleX = (discGetWidth() - spriteGetWidth(titleSprite)) / 2
----
[#discpause]
==== discPause
[source,text]
----
discPause()
----
Met la lecture en pause sur l'image en cours, qui reste à l'écran. L'audio du disque se met en pause avec elle. L'appel est ignoré tant que le disque est arrêté et ne fait rien sans disque. Reprenez avec `discPlay`.
*Depuis :* 1.x
*Voir aussi :* <<discplay,discPlay>>, <<discstop,discStop>>, <<singesetpauseflag,singeSetPauseFlag>>
.Exemple
[source,lua]
----
-- Garder la dernière image de la scène pendant que le joueur choisit une porte.
function onOverlayUpdate()
if discGetFrame() >= SCENE_LAST_FRAME and not choosing then
discPause()
choosing = true
end
return OVERLAY_UPDATED
end
----
[#discpauseatframe]
==== discPauseAtFrame
[source,text]
----
discPauseAtFrame(frame)
----
Saute à `frame`, l'affiche et se met en pause. C'est la même fonction que `discSearch` sous un second nom ; les deux sont interchangeables en tout point, y compris pour les règles de bornage décrites là-bas. Préférez `discSearch` dans le nouveau code et ne gardez `discPauseAtFrame` que pour porter d'anciens scripts.
*Remarques :*
WARNING: *Alias hérité.* Utilisez <<discsearch,discSearch>> dans le nouveau code.
*Depuis :* 1.18
*Voir aussi :* <<discsearch,discSearch>>, <<discplay,discPlay>>
.Exemple
[source,lua]
----
-- Code 1.x porté : rester sur la première image de la scène jusqu'à ce que le joueur soit prêt.
function onInputPressed(what)
if what == SWITCH_START1 then
discPauseAtFrame(SCENE_FIRST_FRAME)
waitingForPlayer = true
end
end
----
[#discplay]
==== discPlay
[source,text]
----
discPlay()
----
Lance ou reprend la lecture à partir de l'image en cours et efface l'état d'arrêt posé par `discStop`. L'audio reprend avec la vidéo, et l'horloge de lecture repart de l'image en cours pour qu'une longue pause ne provoque pas de saut. Sans disque, ne fait rien d'autre qu'effacer l'indicateur d'arrêt.
*Depuis :* 1.x
*Voir aussi :* <<discpause,discPause>>, <<discstop,discStop>>, <<discskiptoframe,discSkipToFrame>>
.Exemple
[source,lua]
----
-- Démarrage classique du mode attraction : caler la première image, puis lancer.
function attractBegin()
discSearch(ATTRACT_FIRST_FRAME)
overlayClear()
discPlay()
attractRunning = true
end
----
[#discsearch]
==== discSearch
[source,text]
----
discSearch(frame)
----
Fait sauter le disque à `frame`, affiche cette image et s'y met en pause, en effaçant l'état d'arrêt si le disque était arrêté. Le disque garde l'image jusqu'à `discPlay`, `discSkipToFrame` ou un autre appel de transport. Une image négative est ramenée à la première image et une image au-delà de la fin à la dernière ; avec un framefile, le segment qui contient l'image est sélectionné et la position est bornée à l'intérieur. Ne fait rien sans disque.
*Paramètres :*
* `frame` -- numéro de l'image visée, compté à partir de zéro.
*Depuis :* 1.x
*Voir aussi :* <<discskiptoframe,discSkipToFrame>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
.Exemple
[source,lua]
----
-- Le joueur a choisi le couloir de gauche : caler la scène et attendre le départ.
function goLeft()
discSearch(CORRIDOR_LEFT_FIRST_FRAME)
soundPlay(SOUND_DOOR)
waitingForPlayer = true
end
----
[#discsearchblanking]
==== discSearchBlanking
[source,text]
----
discSearchBlanking(...)
----
Ne fait rien. Sur le matériel laserdisc, cet appel choisissait si l'écran s'éteignait pendant que le lecteur cherchait une image ; le lecteur logiciel de Singe saute sans éteindre l'écran, donc l'appel accepte n'importe quels arguments, écrit une ligne de trace et rend la main.
*Remarques :*
WARNING: *Non implémenté.* Conservé uniquement pour la rétrocompatibilité. Il n'y a pas de remplacement.
*Depuis :* 1.x
*Voir aussi :* <<discsearch,discSearch>>, <<discskipblanking,discSkipBlanking>>
.Exemple
[source,lua]
----
-- Les scripts antérieurs à 2.00 réglaient souvent les modes d'extinction une fois au démarrage. Sans danger.
discSearchBlanking(false)
discSkipBlanking(false)
discSearch(TITLE_FRAME)
----
[#discsetaudiotrack]
==== discSetAudioTrack
[source,text]
----
discSetAudioTrack(track)
----
Fait passer le disque à une autre piste audio. L'audio en attente de l'ancienne piste est abandonné et la lecture se réaligne, si bien que le changement s'entend presque aussitôt. Avec un disque, une piste hors de `0` à `discGetAudioTracks() - 1` met fin au script ; sans disque, l'appel ne fait rien. Servez-vous-en pour réaliser un menu de langues ou respecter une préférence enregistrée d'une session à l'autre.
*Paramètres :*
* `track` -- indice de piste audio, compté à partir de zéro.
*Depuis :* 2.10
*Voir aussi :* <<discgetaudiotrack,discGetAudioTrack>>, <<discgetaudiotracks,discGetAudioTracks>>, <<discgetlanguage,discGetLanguage>>
.Exemple
[source,lua]
----
-- Appliquer la langue que le joueur a choisie dans l'écran des options.
function applyLanguage(choice)
if choice >= 0 and choice < discGetAudioTracks() then
discSetAudioTrack(choice)
settings.language = choice
saveSettings()
end
end
----
[#discsetfps]
==== discSetFPS
[source,text]
----
discSetFPS(...)
----
Ne fait rien. La fréquence d'images vient du fichier vidéo, donc l'appel accepte n'importe quels arguments, écrit une ligne de trace et rend la main. Rien dans le `Framework.singe` fourni ne l'appelle ; seuls les scripts antérieurs à 2.00 le font.
*Remarques :*
WARNING: *Non implémenté.* Conservé uniquement pour la rétrocompatibilité. La fréquence d'images est lue dans le fichier vidéo et ne peut pas être réglée depuis un script.
*Depuis :* 1.x
*Voir aussi :* <<discgetframe,discGetFrame>>, <<discskiptoframe,discSkipToFrame>>
.Exemple
[source,lua]
----
-- Un script 1.x porté. L'appel est ignoré ; le nombre d'images par seconde est une propriété du fichier.
discSetFPS(29.97)
discSearch(TITLE_FRAME)
discPlay()
----
[#discskipbackward]
==== discSkipBackward
[source,text]
----
discSkipBackward(frames)
----
Saute de `frames` images en arrière à partir de l'image en cours sans changer l'état de lecture ou de pause : un disque en lecture continue de jouer depuis la nouvelle position, un disque en pause y reste en pause. L'appel est ignoré tant que le disque est arrêté, et sans disque. L'écart n'est pas vérifié ; une valeur négative fait sauter en avant, et un écart plus grand que l'image en cours s'arrête à l'image `0`. Avec un framefile, l'écart s'applique au numéro d'image que renvoie `discGetFrame`, si bien qu'un saut peut passer dans un segment antérieur.
*Paramètres :*
* `frames` -- nombre d'images à retrancher de l'image en cours.
*Depuis :* 1.x
*Voir aussi :* <<discskipforward,discSkipForward>>, <<discstepbackward,discStepBackward>>, <<discgetframe,discGetFrame>>
.Exemple
[source,lua]
----
-- Rejouer la dernière seconde (30 images) quand le joueur rate une invite.
function missedPrompt()
lives = lives - 1
discSkipBackward(30)
promptShown = false
end
----
[#discskipblanking]
==== discSkipBlanking
[source,text]
----
discSkipBlanking(...)
----
Ne fait rien. Il choisissait autrefois si l'écran s'éteignait pendant un saut sur le matériel laserdisc ; le lecteur logiciel de Singe n'éteint jamais l'écran, donc l'appel accepte n'importe quels arguments, écrit une ligne de trace et rend la main.
*Remarques :*
WARNING: *Non implémenté.* Conservé uniquement pour la rétrocompatibilité. Il n'y a pas de remplacement.
*Depuis :* 1.x
*Voir aussi :* <<discsearchblanking,discSearchBlanking>>, <<discskiptoframe,discSkipToFrame>>
.Exemple
[source,lua]
----
-- Les scripts antérieurs à 2.00 réglaient souvent les modes d'extinction une fois au démarrage. Sans danger.
discSearchBlanking(false)
discSkipBlanking(false)
discSearch(TITLE_FRAME)
----
[#discskipforward]
==== discSkipForward
[source,text]
----
discSkipForward(frames)
----
Saute de `frames` images en avant à partir de l'image en cours sans changer l'état de lecture ou de pause. Pendant de `discSkipBackward` : ignoré à l'arrêt et sans disque, l'écart n'est pas vérifié, et une cible au-delà de la dernière image s'arrête à la dernière image. Avec un framefile, l'écart s'applique au numéro d'image que renvoie `discGetFrame`, si bien qu'un saut peut passer dans un segment ultérieur.
*Paramètres :*
* `frames` -- nombre d'images à ajouter à l'image en cours.
*Depuis :* 1.x
*Voir aussi :* <<discskipbackward,discSkipBackward>>, <<discstepforward,discStepForward>>, <<discgetframe,discGetFrame>>
.Exemple
[source,lua]
----
-- Laisser un joueur qui revient sauter l'introduction de deux secondes de la scène.
function onInputPressed(what)
if what == SWITCH_BUTTON1 and inSceneIntro then
discSkipForward(60)
inSceneIntro = false
end
end
----
[#discskiptoframe]
==== discSkipToFrame
[source,text]
----
discSkipToFrame(frame)
----
Saute à `frame` et lance la lecture à partir de là, quel que soit l'état du disque, en effaçant l'état d'arrêt si nécessaire. À comparer avec `discSearch`, qui saute et se met en pause. Une image avant le début ou au-delà de la fin est ramenée à la première ou à la dernière image ; avec un framefile, le segment qui contient l'image est sélectionné et la position est bornée à l'intérieur. Sans disque, ne fait rien d'autre qu'effacer l'indicateur d'arrêt.
*Paramètres :*
* `frame` -- numéro de l'image visée, compté à partir de zéro.
*Depuis :* 1.x
*Voir aussi :* <<discsearch,discSearch>>, <<discplay,discPlay>>, <<discgetframe,discGetFrame>>
.Exemple
[source,lua]
----
-- Sauter l'introduction quand on rejoue.
function startGame()
if hasPlayedBefore then
discSkipToFrame(GAMEPLAY_FIRST_FRAME)
else
discSkipToFrame(INTRO_FIRST_FRAME)
end
end
----
[#discstepbackward]
==== discStepBackward
[source,text]
----
discStepBackward()
----
Recule le disque d'exactement une image et s'y met en pause, quel que soit l'état de lecture précédent. Reculer depuis l'image `0` reste sur l'image `0`. Ignoré tant que le disque est arrêté, comme `discSkipBackward`. Destiné au débogage à l'image près et aux écrans de service. Ne fait rien sans disque.
*Depuis :* 1.x
*Voir aussi :* <<discstepforward,discStepForward>>, <<discskipbackward,discSkipBackward>>, <<discgetframe,discGetFrame>>
.Exemple
[source,lua]
----
-- Écran de service : gauche et droite déplacent le disque d'une image à la fois.
function onInputPressed(what)
if what == SWITCH_LEFT then
discStepBackward()
elseif what == SWITCH_RIGHT then
discStepForward()
end
end
----
[#discstepforward]
==== discStepForward
[source,text]
----
discStepForward()
----
Avance le disque d'exactement une image et s'y met en pause, quel que soit l'état de lecture précédent. Avancer depuis la dernière image reste sur la dernière image. Comme `discStepBackward`, il est ignoré tant que le disque est arrêté. Ne fait rien sans disque.
*Depuis :* 1.x
*Voir aussi :* <<discstepbackward,discStepBackward>>, <<discskipforward,discSkipForward>>, <<discgetframe,discGetFrame>>
.Exemple
[source,lua]
----
-- Chercheur d'images : parcourir une scène image par image et afficher le numéro d'image.
function onOverlayUpdate()
overlayClear()
overlayPrint(8, 8, "Frame " .. discGetFrame())
if stepping then
discStepForward()
end
return OVERLAY_UPDATED
end
----
[#discstop]
==== discStop
[source,text]
----
discStop()
----
Arrête le disque. La lecture se met en pause, la vidéo est remplacée par l'écran bleu classique, `discGetState` signale `DISC_STOPPED` et `discGetFrame` renvoie `0` jusqu'à ce que le disque soit relancé. À l'arrêt, `discPause`, `discSkipForward`, `discSkipBackward`, `discStepForward` et `discStepBackward` sont ignorés. N'importe lequel de `discPlay`, `discSearch` ou `discSkipToFrame` efface l'état d'arrêt. Un second `discStop` est ignoré.
*Depuis :* 1.x
*Voir aussi :* <<discplay,discPlay>>, <<discpause,discPause>>, <<discgetstate,discGetState>>
.Exemple
[source,lua]
----
-- Fin de partie : passer à l'écran bleu derrière le tableau des scores.
function gameOver()
discStop()
soundPlay(SOUND_GAME_OVER)
showScores = true
end
----
[#emitter]
=== Émetteur
Les émetteurs font naître, déplacent et dessinent des particules ; voir
<<particles,Particules>>. Les handles sont des entiers. Un émetteur 2D vit dans
les coordonnées de l'overlay, Y vers le bas, et se dessine quand `emitterDraw` le
demande depuis `onOverlayUpdate` ; un émetteur 3D est posé sur un nœud, vit en
unités du monde et se dessine lui-même dans la scène. Les appels de recette
peuvent se faire dans n'importe quel ordre, avant ou après le démarrage de
l'émetteur, et prennent effet sur les prochaines particules qui naissent
(quelques-uns, signalés plus bas, agissent aussi sur les particules vivantes).
Tout ce qui est donné sous forme de minimum et de maximum est tiré au hasard entre
les deux pour chaque particule. Les particules avancent avec l'horloge, d'un
dixième de seconde par image au plus, et s'arrêtent tant que le jeu est en pause.
[#emitternew]
==== emitterNew
[source,text]
----
emitter = emitterNew()
emitter = emitterNew(node)
----
Crée un émetteur. Sans argument, c'est un émetteur 2D placé à la position `(0, 0)` de l'overlay, que l'on déplace avec `emitterSetPosition` ; avec un nœud, c'est un émetteur 3D dont les particules naissent à la position du nœud dans le monde, et il a besoin de la scène 3D : sur une machine qui n'en a pas, l'appel met fin au script (les émetteurs 2D fonctionnent partout). Un nouvel émetteur n'émet pas en continu ; appelez `emitterStart` ou `emitterBurst`. Supprimer le nœud supprime ses émetteurs. Valeurs par défaut : 50 particules par seconde, vivant une à deux secondes, ouverture de 30 degrés, blanc s'estompant jusqu'au transparent, mélange normal, ni gravité, ni freinage, ni rotation, ni rayon, ni texture, ni traînée, ni collision, une réserve de 1000 ; un émetteur 2D les lance vers le haut de l'écran à 50 à 100 unités de l'overlay par seconde en les réduisant de 16 à 4, un émetteur 3D vers le haut de l'axe Y à 1 à 2 unités du monde par seconde en les réduisant de 0,2 à 0,05.
*Renvoie :* le handle de l'émetteur.
*Depuis :* 3.00.
*Voir aussi :* <<emitterdelete,emitterDelete>>, <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emitterdraw,emitterDraw>>
.Exemple
[source,lua]
----
-- Les gaz d'échappement du nœud moteur du vaisseau ; la recette est réglée avant le début du flux.
exhaust = emitterNew(nodeFind("Engine", ship))
emitterSetBlend(exhaust, PARTICLE_ADD)
emitterSetRate(exhaust, 120)
emitterSetLife(exhaust, 0.3, 0.6)
emitterSetDirection(exhaust, 0, 0, 1)
emitterSetSpeed(exhaust, 3, 5)
emitterSetSize(exhaust, 0.3, 0.05)
emitterSetColor(exhaust, 120, 180, 255, 255, 30, 60, 255, 0)
emitterStart(exhaust)
----
[#emitterdelete]
==== emitterDelete
[source,text]
----
emitterDelete(emitter)
----
Supprime d'un coup l'émetteur, ses particules vivantes et sa copie de la texture ; rien ne s'estompe. Utilisez plutôt `emitterStop` pour laisser les dernières particules finir. Un émetteur 3D est aussi supprimé de lui-même quand son nœud est supprimé. Un handle qui n'est pas celui d'un émetteur vivant met fin au script : effacez donc la variable ensuite.
*Depuis :* 3.00.
*Voir aussi :* <<emitternew,emitterNew>>, <<emitterstop,emitterStop>>, <<emitterisactive,emitterIsActive>>
.Exemple
[source,lua]
----
-- L'explosion est finie : libérer son émetteur une fois la dernière étincelle éteinte.
function onOverlayUpdate()
if explosion ~= nil and not emitterIsActive(explosion) then
emitterDelete(explosion)
explosion = nil
end
return OVERLAY_NOT_UPDATED
end
----
[#emittersettexture]
==== emitterSetTexture
[source,text]
----
emitterSetTexture(emitter, sprite)
emitterSetTexture(emitter)
----
L'image que porte chaque particule : un sprite chargé, ou aucune (un seul argument, ou `nil`) pour le disque flou intégré. L'émetteur garde sa propre copie des pixels du sprite, toutes les images d'un sprite animé comprises, si bien que le sprite peut être déchargé ensuite. Régler la texture ramène la plage d'images à l'image `0` et place chaque particule vivante sur l'image `0` : appelez donc `emitterSetFrames` après, pas avant. Le mélange et la teinte s'appliquent à l'image comme au disque.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetframes,emitterSetFrames>>, <<spriteload,spriteLoad>>, <<emittersetblend,emitterSetBlend>>
.Exemple
[source,lua]
----
-- Neige : une planche de six formes de flocons, une tirée au hasard par particule.
flakes = spriteLoadFrames(6, DIR .. "Sprites/flakes.png")
snow = emitterNew()
emitterSetTexture(snow, flakes)
emitterSetFrames(snow, 0, 5)
emitterSetDirection(snow, 0, 1)
emitterSetSpeed(snow, 20, 40)
emitterSetLife(snow, 6, 9)
emitterSetSize(snow, 6, 6, 0.4)
emitterStart(snow)
----
[#emittersetframes]
==== emitterSetFrames
[source,text]
----
emitterSetFrames(emitter, first, last)
----
Avec un sprite animé comme texture, chaque nouvelle particule prend une image au hasard de `first` à `last` et la garde toute sa vie ; les images se comptent à partir de `0`. Les valeurs sont bornées aux images que possède la texture, et `last` est relevé à `first` quand il est plus petit. Par défaut : l'image `0` seule, qui est aussi ce à quoi `emitterSetTexture` revient. Avec le disque intégré ou un sprite d'une seule image, la plage n'a aucun effet.
*Depuis :* 3.00.
*Voir aussi :* <<emittersettexture,emitterSetTexture>>, <<spriteload,spriteLoad>>
.Exemple
[source,lua]
----
-- La planche de bouffées a quatre bouffées floues, puis quatre éclats de débris plus nets.
emitterSetTexture(smoke, puffSheet)
emitterSetFrames(smoke, 0, 3)
emitterSetTexture(debris, puffSheet)
emitterSetFrames(debris, 4, 7)
----
[#emittersetblend]
==== emitterSetBlend
[source,text]
----
emitterSetBlend(emitter, mode)
----
La façon dont les particules se combinent avec ce qui est derrière elles. `PARTICLE_ALPHA` (par défaut) mélange normalement, pour la fumée, la poussière, la pluie et les débris ; `PARTICLE_ADD` ajoute de la lumière, pour le feu, les étincelles, les lueurs et la magie, et n'assombrit jamais. Prend effet aussitôt sur les particules vivantes. Toute autre valeur que ces deux constantes met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetcolor,emitterSetColor>>, <<emittersettexture,emitterSetTexture>>
.Exemple
[source,lua]
----
-- Les étincelles brillent ; la fumée au-dessus d'elles, non.
emitterSetBlend(sparks, PARTICLE_ADD)
emitterSetColor(sparks, 255, 220, 120, 255, 255, 80, 20, 0)
emitterSetBlend(smoke, PARTICLE_ALPHA)
emitterSetColor(smoke, 160, 160, 170, 120, 120, 120, 130, 0)
----
[#emittersetlayer]
==== emitterSetLayer
[source,text]
----
emitterSetLayer(emitter, layer)
----
2D seulement : où `emitterDraw` place les particules. `PARTICLE_OVER` (par défaut) dessine au-dessus de tout ce qui est sur l'overlay ; `PARTICLE_UNDER` dessine sous l'overlay et au-dessus de la vidéo et de la scène 3D, pour de la fumée derrière le score. Sur un émetteur 3D, le réglage est accepté et ignoré. Toute autre valeur que ces deux constantes met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<emitterdraw,emitterDraw>>, <<emittersetposition,emitterSetPosition>>
.Exemple
[source,lua]
----
-- La pluie tombe derrière le HUD, les étincelles d'impact devant.
rain = emitterNew()
emitterSetLayer(rain, PARTICLE_UNDER)
hits = emitterNew()
emitterSetLayer(hits, PARTICLE_OVER)
function onOverlayUpdate()
overlayClear()
emitterDraw(rain)
drawHud()
emitterDraw(hits)
return OVERLAY_UPDATED
end
----
[#emittersetrate]
==== emitterSetRate
[source,text]
----
emitterSetRate(emitter, perSecond)
----
Le nombre de particules par seconde que crée un émetteur démarré ; `50` par défaut, les valeurs négatives deviennent `0`. Les fractions s'accumulent, si bien que `0.5` en crée une toutes les deux secondes. Changez-le pendant l'émission pour moduler une flamme ou un propulseur. Une réserve pleine (`emitterSetMax`) n'en crée plus tant que certaines ne sont pas mortes, quel que soit le débit.
*Depuis :* 3.00.
*Voir aussi :* <<emitterstart,emitterStart>>, <<emitterburst,emitterBurst>>, <<emittersetmax,emitterSetMax>>
.Exemple
[source,lua]
----
-- Le panache du propulseur suit la manette des gaz.
function onControllerMoved(axis, value, which)
if axis == GAMEPAD_AXIS_RIGHT_TRIGGER then
throttle = value / 32767
emitterSetRate(plume, 30 + 300 * throttle)
end
end
----
[#emittersetlife]
==== emitterSetLife
[source,text]
----
emitterSetLife(emitter, minSeconds, maxSeconds)
----
La durée de vie de chaque nouvelle particule, tirée au hasard entre les deux ; de `1` à `2` par défaut. Sa taille et sa couleur vont de leur début à leur fin pendant ce temps, si bien que la durée de vie donne le rythme de chaque fondu. Le minimum est maintenu à `0.001` ou plus et le maximum est relevé au minimum quand il est plus petit. Les particules vivantes gardent la durée de vie avec laquelle elles sont nées.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetsize,emitterSetSize>>, <<emittersetcolor,emitterSetColor>>, <<emittersetrate,emitterSetRate>>
.Exemple
[source,lua]
----
-- Éclair de bouche : une salve qui disparaît en un dixième de seconde.
flash = emitterNew(muzzle)
emitterSetBlend(flash, PARTICLE_ADD)
emitterSetLife(flash, 0.05, 0.1)
emitterSetSpeed(flash, 2, 6)
emitterSetSize(flash, 0.15, 0.4)
function onFire()
emitterBurst(flash, 12)
end
----
[#emittersetsoftness]
==== emitterSetSoftness
[source,text]
----
emitterSetSoftness(emitter, distance)
----
3D seulement : estompe une particule sur `distance` unités du monde là où elle rencontre la géométrie derrière elle, si bien qu'une bouffée de fumée ne trace pas de ligne nette là où elle traverse un mur et qu'une fontaine s'enfonce dans son bassin au lieu de le trancher. `0` (la valeur par défaut) le désactive ; les valeurs négatives deviennent `0`. Tant qu'un émetteur l'utilise, la profondeur de la scène est rendue une fois de plus par image : un Raspberry Pi devrait donc l'utiliser avec parcimonie. Un émetteur 2D garde la valeur et l'ignore.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetlit,emitterSetLit>>, <<emittersetblend,emitterSetBlend>>
.Exemple
[source,lua]
----
-- La fumée du brasero s'adoucit contre le poteau qu'elle longe en montant.
smoke = emitterNew(brazierTop)
emitterSetRate(smoke, 25)
emitterSetLife(smoke, 1.5, 2.5)
emitterSetSize(smoke, 0.2, 0.9, 0.2)
emitterSetColor(smoke, 160, 160, 170, 120, 120, 120, 130, 0)
emitterSetLit(smoke, true)
emitterSetSoftness(smoke, 0.3)
emitterStart(smoke)
----
[#emittersetspeed]
==== emitterSetSpeed
[source,text]
----
emitterSetSpeed(emitter, min, max)
----
La vitesse de départ de chaque nouvelle particule, tirée au hasard entre les deux, en unités de l'overlay par seconde (2D) ou en unités du monde par seconde (3D) ; par défaut de `50` à `100` en 2D et de `1` à `2` en 3D. Le maximum est relevé au minimum quand il est plus petit. La vitesse décide jusqu'où va une particule avant que le freinage, la gravité et sa durée de vie prennent le relais.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetdirection,emitterSetDirection>>, <<emittersetdrag,emitterSetDrag>>, <<emittersetgravity,emitterSetGravity>>
.Exemple
[source,lua]
----
-- Une fontaine projette ses gouttes fort et droit ; la poussière bouge à peine.
emitterSetSpeed(fountain, 4.0, 5.0)
emitterSetSpread(fountain, 8)
emitterSetSpeed(dust, 0.05, 0.2)
emitterSetSpread(dust, 180)
----
[#emittersetdirection]
==== emitterSetDirection
[source,text]
----
emitterSetDirection(emitter, x, y)
emitterSetDirection(emitter, x, y, z)
----
La direction moyenne dans laquelle partent les nouvelles particules ; seule la direction compte, pas la longueur. Par défaut en 2D `(0, -1)`, vers le haut de l'écran puisque le Y de l'overlay croît vers le bas ; par défaut en 3D `(0, 1, 0)`, vers le haut de l'axe Y du monde. Un émetteur 2D ignore `z` ; un émetteur 3D qui reçoit deux valeurs prend `z = 0`. Un vecteur nul est traité comme `(0, -1, 0)`. Pour un émetteur 3D, la direction s'exprime dans les axes du monde, pas dans ceux du nœud : tournez-la vous-même quand le nœud tourne.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetspread,emitterSetSpread>>, <<emittersetspeed,emitterSetSpeed>>
.Exemple
[source,lua]
----
-- Les gaz s'échappent derrière le vaisseau, où qu'il soit tourné.
function onOverlayUpdate()
local heading = math.rad(shipYaw)
emitterSetDirection(exhaust, -math.sin(heading), 0, -math.cos(heading))
return OVERLAY_NOT_UPDATED
end
----
[#emittersetspread]
==== emitterSetSpread
[source,text]
----
emitterSetSpread(emitter, degrees)
----
La moitié de l'angle du cône (3D) ou de l'éventail (2D), autour de la direction, dans lequel les nouvelles particules sont projetées, de `0` à `180` ; `0` est une ligne droite, `180` toutes les directions. `30` par défaut ; les valeurs hors plage sont bornées. En 3D, les directions sont réparties uniformément sur la calotte du cône, si bien qu'un cône large ne se resserre pas vers son axe.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetdirection,emitterSetDirection>>, <<emittersetradius,emitterSetRadius>>
.Exemple
[source,lua]
----
-- Le feu d'artifice part dans tous les sens ; la flamme de la torche reste une colonne serrée.
emitterSetSpread(fireworks, 180)
emitterSetSpread(flame, 15)
----
[#emittersetgravity]
==== emitterSetGravity
[source,text]
----
emitterSetGravity(emitter, x, y)
emitterSetGravity(emitter, x, y, z)
----
Une accélération constante sur chaque particule, en unités de l'overlay (2D) ou en unités du monde (3D) par seconde au carré : `(0, 500)` tire les étincelles 2D vers le bas de l'écran, `(0, -20)` laisse monter la fumée 2D, `(0, -9.8, 0)` est la Terre dans la scène. Aucune par défaut. Elle s'applique aux particules vivantes dès l'image suivante. Les particules ne sont pas des corps physiques ; donnez-leur `emitterSetCollide` pour qu'elles rebondissent sur un sol ou sur la scène.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetdrag,emitterSetDrag>>, <<emittersetcollide,emitterSetCollide>>, <<emittersetspeed,emitterSetSpeed>>
.Exemple
[source,lua]
----
-- Les étincelles tombent, la fumée monte, toutes deux de la même meuleuse.
emitterSetGravity(sparks, 0, 500)
emitterSetGravity(smoke, 0, -20)
-- Sous l'eau, les bulles filent vers la surface.
emitterSetGravity(bubbles, 0, 3, 0)
----
[#emittersetdrag]
==== emitterSetDrag
[source,text]
----
emitterSetDrag(emitter, perSecond)
----
La vitesse à laquelle les particules perdent leur vélocité : à chaque image, la vélocité est multipliée par `1 - perSecond * dt`, jamais en dessous de `0`, si bien que `1` enlève l'essentiel de la vitesse en une seconde et que `0` (la valeur par défaut) la garde pour toujours. Les valeurs négatives deviennent `0`. La fumée, les braises et les feux d'artifice ralentissent ; les étincelles et la pluie, non. S'applique aux particules vivantes dès l'image suivante.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetgravity,emitterSetGravity>>, <<emittersetspeed,emitterSetSpeed>>
.Exemple
[source,lua]
----
-- Une bombe de feu d'artifice : sortie rapide, puis suspendue et retombant.
fireworks = emitterNew()
emitterSetBlend(fireworks, PARTICLE_ADD)
emitterSetSpeed(fireworks, 120, 320)
emitterSetSpread(fireworks, 180)
emitterSetGravity(fireworks, 0, 220)
emitterSetDrag(fireworks, 0.8)
emitterSetLife(fireworks, 0.8, 1.6)
----
[#emittersetsize]
==== emitterSetSize
[source,text]
----
emitterSetSize(emitter, start, finish)
emitterSetSize(emitter, start, finish, variation)
----
La taille à la naissance et à la mort, en unités de l'overlay (2D) ou en unités du monde (3D), qui change régulièrement au fil de la vie de la particule ; par défaut de `16` à `4` en 2D et de `0.2` à `0.05` en 3D. `variation` (de `0` à `1`, `0` par défaut) multiplie les deux tailles de chaque nouvelle particule par un même facteur aléatoire entre `1 - variation` et `1 + variation`, si bien qu'une bouffée est un peu plus grande ou plus petite toute sa vie. Les tailles négatives deviennent `0` et `variation` est borné. Une particule texturée est dessinée comme un carré de cette taille ; une image plus large est écrasée.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetlife,emitterSetLife>>, <<emittersetcolor,emitterSetColor>>, <<emittersetspin,emitterSetSpin>>
.Exemple
[source,lua]
----
-- La fumée grossit en s'éclaircissant ; les étincelles rétrécissent jusqu'à rien.
emitterSetSize(smoke, 12, 48, 0.2)
emitterSetSize(sparks, 5, 1)
----
[#emittersetcollide]
==== emitterSetCollide
[source,text]
----
emitterSetCollide(emitter, mode)
emitterSetCollide(emitter, mode, bounce)
emitterSetCollide(emitter, mode, bounce, friction)
emitterSetCollide(emitter, mode, bounce, friction, floor)
----
Ce sur quoi rebondissent les particules. `COLLIDE_NONE` (par défaut), c'est rien ; `COLLIDE_FLOOR`, c'est un plan horizontal à la hauteur `floor`, sous lequel (Y plus grand en 2D, Y plus petit en 3D) une particule est remise sur le plan et réfléchie ; `COLLIDE_SCENE`, c'est le monde physique, pour les émetteurs 3D seulement, par un lancer de rayon par particule le long de son mouvement à chaque image, plafonné à `200` lancers par émetteur et par image et repris là où ils s'étaient arrêtés, si bien qu'un gros émetteur répartit le coût sur quelques images. Sur un émetteur 2D, `COLLIDE_SCENE` est accepté et ne fait rien. Un mode autre que ces trois-là met fin au script.
*Paramètres :*
* `mode` -- `COLLIDE_NONE`, `COLLIDE_FLOOR` ou `COLLIDE_SCENE`.
* `bounce` -- la part de vitesse qui survit à un choc, de `0` à `1`, `0.5` par défaut ; bornée.
* `friction` -- la part de vitesse de glissement perdue à chaque choc, de `0` à `1`, `0.2` par défaut ; bornée.
* `floor` -- hauteur du plan du sol, en unités du monde pour un émetteur 3D ou en unités de l'overlay pour un émetteur 2D, `0` par défaut. Utilisé par `COLLIDE_FLOOR` seulement.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetgravity,emitterSetGravity>>, <<emittersettrail,emitterSetTrail>>, <<emittersetsoftness,emitterSetSoftness>>
.Exemple
[source,lua]
----
-- Les étincelles rebondissent sur le sol de l'atelier (ligne 440 de l'overlay) et laissent des traînées.
sparks = emitterNew()
emitterSetBlend(sparks, PARTICLE_ADD)
emitterSetGravity(sparks, 0, 500)
emitterSetCollide(sparks, COLLIDE_FLOOR, 0.4, 0.2, 440)
emitterSetTrail(sparks, 6, 3)
-- Les braises de la scène rebondissent sur tout ce que contient le monde physique.
emitterSetCollide(embers, COLLIDE_SCENE, 0.3, 0.5)
----
[#emittersetcolor]
==== emitterSetColor
[source,text]
----
emitterSetColor(emitter, r, g, b, a, r2, g2, b2, a2)
----
La couleur et l'opacité à la naissance (`r, g, b, a`) et à la mort (`r2, g2, b2, a2`), chacune de `0` à `255`, qui changent régulièrement au fil de la vie de la particule ; les valeurs hors plage sont bornées. Par défaut, du blanc opaque au blanc transparent. La teinte multiplie la texture ou le disque intégré, si bien qu'une image blanche prend exactement la couleur. Un fondu en entrée et en sortie demande deux émetteurs ou une texture qui contient la forme ; une recette suit une seule rampe droite.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetblend,emitterSetBlend>>, <<emittersetlife,emitterSetLife>>, <<emittersetsize,emitterSetSize>>
.Exemple
[source,lua]
----
-- Flamme : jaune vif à la base, qui meurt en rouge terne et disparaît.
emitterSetColor(flame, 255, 200, 90, 255, 255, 40, 0, 0)
-- Les grains de poussière apparaissent à partir de rien et restent suspendus ; leur durée de vie les achève.
emitterSetColor(dust, 255, 240, 200, 0, 255, 240, 200, 160)
----
[#emittersetspin]
==== emitterSetSpin
[source,text]
----
emitterSetSpin(emitter, min, max)
----
La vitesse de rotation de chaque nouvelle particule en degrés par seconde, tirée au hasard entre les deux ; les valeurs négatives tournent dans l'autre sens, et une plage qui chevauche `0` donne les deux. Chaque particule part d'un angle aléatoire, qu'elle tourne ou non. Aucune par défaut. Le maximum est relevé au minimum quand il est plus petit. La rotation se voit sur une particule texturée ou sur une traînée ; le disque intégré est rond et paraît identique sous tout angle.
*Depuis :* 3.00.
*Voir aussi :* <<emittersettexture,emitterSetTexture>>, <<emittersetsize,emitterSetSize>>
.Exemple
[source,lua]
----
-- Les feuilles virevoltent ; les braises tournent à peine.
emitterSetTexture(leaves, leafSprite)
emitterSetSpin(leaves, -180, 180)
emitterSetSpin(embers, -20, 20)
----
[#emittersetradius]
==== emitterSetRadius
[source,text]
----
emitterSetRadius(emitter, radius)
----
Les nouvelles particules naissent n'importe où à cette distance au plus de l'origine de l'émetteur, uniformément sur un disque (2D) ou dans une boule (3D), en unités de l'overlay ou du monde. `0` par défaut, un point ; les valeurs négatives deviennent `0`. Un rayon remplit un volume, une tête de torche ou une nappe de poussière dans l'air, là où un point donnerait une aiguille.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetspread,emitterSetSpread>>, <<emittersetposition,emitterSetPosition>>
.Exemple
[source,lua]
----
-- De la poussière dans une boule d'air de la taille d'une pièce, presque toute déjà là au début du niveau.
air = nodeNew()
nodeSetPosition(air, 0, 2, 0)
dust = emitterNew(air)
emitterSetRadius(dust, 2.5)
emitterSetSpread(dust, 180)
emitterSetSpeed(dust, 0.05, 0.2)
emitterSetLife(dust, 4, 6)
emitterStart(dust)
emitterBurst(dust, 120)
----
[#emittersetlit]
==== emitterSetLit
[source,text]
----
emitterSetLit(emitter, lit)
----
3D seulement : éclaire chaque particule avec les lumières et le ciel de la scène comme s'il s'agissait d'une petite sphère tournée vers la caméra, si bien que la fumée prend la couleur d'un brasero et que la poussière scintille au soleil. Désactivé par défaut ; les ombres ne sont pas consultées. Avec `PARTICLE_ADD`, l'éclairage s'applique quand même à la teinte avant qu'elle soit ajoutée. Un émetteur 2D garde la valeur et l'ignore.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetsoftness,emitterSetSoftness>>, <<emittersetblend,emitterSetBlend>>
.Exemple
[source,lua]
----
-- La fumée au-dessus de chaque brasero prend la lueur de son feu.
for _, brazier in ipairs(braziers) do
local smoke = emitterNew(brazier.top)
emitterSetRate(smoke, 12)
emitterSetLife(smoke, 2.5, 4)
emitterSetColor(smoke, 90, 80, 75, 90, 50, 45, 45, 0)
emitterSetLit(smoke, true)
emitterStart(smoke)
end
----
[#emittersetlocal]
==== emitterSetLocal
[source,text]
----
emitterSetLocal(emitter, local)
----
Indique si les particules vivantes suivent l'émetteur. `false` (par défaut) laisse chacune là où elle est née, si bien qu'une torche portée à travers une pièce laisse de la fumée derrière elle ; `true` garde tout le nuage relatif à l'origine de l'émetteur, si bien que le panache d'un propulseur se déplace avec le vaisseau. Basculer le réglage fait passer les particules vivantes d'un référentiel à l'autre sans saut. Sur un émetteur 3D, l'origine est la position du nœud dans le monde ; sur un émetteur 2D, c'est le point donné par `emitterSetPosition`, si bien qu'un nuage 2D local glisse lui aussi avec ce point. Les traînées suivent la même règle.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetposition,emitterSetPosition>>, <<emittersetradius,emitterSetRadius>>
.Exemple
[source,lua]
----
-- Une aura magique qui reste enroulée autour de la magicienne pendant qu'elle marche.
aura = emitterNew(nodeFind("Chest", wizard))
emitterSetLocal(aura, true)
emitterSetRadius(aura, 0.6)
emitterSetSpeed(aura, 0.05, 0.15)
emitterSetSpread(aura, 180)
emitterSetBlend(aura, PARTICLE_ADD)
emitterStart(aura)
----
[#emittersetmax]
==== emitterSetMax
[source,text]
----
emitterSetMax(emitter, count)
----
La réserve : le nombre de particules qui peuvent être vivantes en même temps, `1000` par défaut. Une réserve pleine n'en crée plus, ni par le flux ni par une salve, tant que certaines ne sont pas mortes. Changer ce nombre reconstruit la réserve et tue chaque particule vivante : réglez-le donc avant de démarrer. Dimensionnez la réserve au débit multiplié par la plus longue durée de vie, plus la plus grosse salve. Un nombre inférieur à `1` met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>, <<emittergetcount,emitterGetCount>>
.Exemple
[source,lua]
----
-- 300 gouttes par seconde vivant jusqu'à 1,6 seconde demandent environ 480 ; gardez de la marge.
fountain = emitterNew(spout)
emitterSetRate(fountain, 300)
emitterSetLife(fountain, 1.0, 1.6)
emitterSetMax(fountain, 600)
emitterStart(fountain)
----
[#emittersetposition]
==== emitterSetPosition
[source,text]
----
emitterSetPosition(emitter, x, y)
----
2D seulement : où naissent les nouvelles particules, en coordonnées de l'overlay ; `(0, 0)` par défaut. Réglez-la à chaque image pour suivre un sprite ou un corps physique 2D. Les particules vivantes restent où elles sont, sauf si `emitterSetLocal` est activé. Un émetteur 3D suit son nœud et met fin au script ici ; déplacez plutôt le nœud.
*Depuis :* 3.00.
*Voir aussi :* <<emitterdraw,emitterDraw>>, <<emittersetlocal,emitterSetLocal>>, <<emittersetradius,emitterSetRadius>>
.Exemple
[source,lua]
----
-- Les étincelles jaillissent de là où se trouve le sprite de la meuleuse à cette image.
function onOverlayUpdate()
overlayClear()
spriteDraw(grinder, grinderX, grinderY)
emitterSetPosition(sparks, grinderX + 24, grinderY + 8)
emitterDraw(sparks)
return OVERLAY_UPDATED
end
----
[#emittersettrail]
==== emitterSetTrail
[source,text]
----
emitterSetTrail(emitter, length, width)
----
Un ruban derrière chaque particule, passant par ses `length` dernières positions, une enregistrée par image (de `2` à `16` ; `0` ou `1` pour aucun, la valeur par défaut), large de `width` en unités du monde ou de l'overlay, en 2D comme en 3D. Le ruban porte la teinte actuelle de la particule, s'estompe jusqu'au transparent vers sa queue, et prend pour texture la colonne du milieu de l'image de la particule, si bien que le bord flou du disque devient celui du ruban. Les valeurs sont bornées à la plage. Changer la longueur reconstruit le stockage des traînées : les particules vivantes recommencent donc leurs rubans. Étincelles, braises, balles traçantes, comètes.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetcollide,emitterSetCollide>>, <<emittersetsize,emitterSetSize>>, <<emittersetlocal,emitterSetLocal>>
.Exemple
[source,lua]
----
-- Balles traçantes : minuscules, rapides, et rien que des traînées.
tracers = emitterNew(gunMuzzle)
emitterSetBlend(tracers, PARTICLE_ADD)
emitterSetSpeed(tracers, 40, 50)
emitterSetSpread(tracers, 2)
emitterSetSize(tracers, 0.04, 0.04)
emitterSetLife(tracers, 0.8, 1.0)
emitterSetTrail(tracers, 8, 0.03)
----
[#emitterstart]
==== emitterStart
[source,text]
----
emitterStart(emitter)
----
Lance le flux continu au débit réglé ; dès lors, l'émetteur crée des particules à chaque image jusqu'à `emitterStop`. Démarrer un émetteur qui émet déjà ne change rien. Le flux n'a besoin d'aucun appel de dessin pour tourner ; un émetteur 2D qui émet sans être dessiné consomme quand même sa réserve.
*Depuis :* 3.00.
*Voir aussi :* <<emitterstop,emitterStop>>, <<emittersetrate,emitterSetRate>>, <<emitterburst,emitterBurst>>
.Exemple
[source,lua]
----
-- Le moteur s'allume quand le joueur met les gaz et s'éteint quand il les coupe.
function onThrottle(open)
if open then
emitterStart(exhaust)
else
emitterStop(exhaust)
end
end
----
[#emitterstop]
==== emitterStop
[source,text]
----
emitterStop(emitter)
----
Met fin au flux ; les particules vivantes finissent leur vie et toute fraction de particule due est abandonnée. `emitterBurst` fonctionne encore sur un émetteur arrêté, et `emitterIsActive` reste à `true` jusqu'à la mort de la dernière particule. Utilisez `emitterClear` pour supprimer aussi les particules d'un coup.
*Depuis :* 3.00.
*Voir aussi :* <<emitterclear,emitterClear>>, <<emitterstart,emitterStart>>, <<emitterisactive,emitterIsActive>>
.Exemple
[source,lua]
----
-- La torche s'éteint : arrêter la flamme, laisser la dernière fumée s'éloigner.
function onTorchDoused()
emitterStop(flame)
emitterStop(smoke)
soundPlay(hiss)
end
----
[#emitterburst]
==== emitterBurst
[source,text]
----
emitterBurst(emitter, count)
----
Crée d'un coup `count` particules selon la recette actuelle, flux ou pas : une explosion, une éclaboussure, une bouffée. Seules sont créées celles pour lesquelles la réserve a de la place ; `emitterSetMax` règle cette place. Un émetteur 2D lâche sa salve à son point `emitterSetPosition`, un émetteur 3D à son nœud, répartie sur `emitterSetRadius`.
*Depuis :* 3.00.
*Voir aussi :* <<emitterstart,emitterStart>>, <<emittersetmax,emitterSetMax>>, <<emittersetradius,emitterSetRadius>>
.Exemple
[source,lua]
----
-- Feu d'artifice : une bombe éclate quelque part au-dessus du disque toutes les secondes environ.
function onOverlayUpdate()
if frames % 60 == 0 then
emitterSetPosition(fireworks, 120 + (frames * 7) % (width - 240), 80 + (frames * 13) % 160)
emitterBurst(fireworks, 260)
end
overlayClear()
emitterDraw(fireworks)
frames = frames + 1
return OVERLAY_UPDATED
end
----
[#emitterclear]
==== emitterClear
[source,text]
----
emitterClear(emitter)
----
Tue sur-le-champ chaque particule vivante et oublie toute fraction de particule due. Le flux n'est pas arrêté : un émetteur démarré continue de créer de nouvelles particules à l'image suivante, alors appelez aussi `emitterStop` pour le vider pour de bon. Servez-vous-en quand une scène change et que les anciennes étincelles ne doivent pas traîner.
*Depuis :* 3.00.
*Voir aussi :* <<emitterstop,emitterStop>>, <<emitterdelete,emitterDelete>>
.Exemple
[source,lua]
----
-- On recommence le niveau : les émetteurs restent, les anciennes particules s'en vont.
function resetLevel()
for _, emitter in ipairs(levelEmitters) do
emitterStop(emitter)
emitterClear(emitter)
end
placePlayer()
end
----
[#emitterdraw]
==== emitterDraw
[source,text]
----
emitterDraw(emitter)
----
2D seulement, depuis `onOverlayUpdate` : met en file d'attente les particules de l'émetteur pour qu'elles soient dessinées à cette image, au-dessus ou au-dessous de l'overlay selon `emitterSetLayer`, en coordonnées de l'overlay projetées sur la fenêtre. Les émetteurs d'une même couche se dessinent dans l'ordre où ils ont été mis en file. La file se vide à chaque image : l'appel a donc sa place dans chaque image où les particules doivent se voir ; ne pas dessiner un émetteur ne l'arrête pas. Jusqu'à `64` émetteurs par couche se dessinent dans une image. Un émetteur 3D se dessine lui-même dans la scène et met fin au script ici.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetlayer,emitterSetLayer>>, <<emittersetposition,emitterSetPosition>>, <<onoverlayupdate,onOverlayUpdate>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
overlayClear()
emitterSetPosition(smoke, chimneyX, chimneyY)
emitterDraw(smoke)
emitterDraw(sparks)
fontPrint(20, 20, "Score " .. score)
return OVERLAY_UPDATED
end
----
[#emittergetcount]
==== emitterGetCount
[source,text]
----
count = emitterGetCount(emitter)
----
Le nombre de particules de l'émetteur vivantes à cette image, de `0` à la taille de la réserve. Utile pour régler une réserve, pour un budget à l'écran, ou pour savoir quand une salve s'est éteinte.
*Renvoie :* le nombre de particules vivantes, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<emittersetmax,emitterSetMax>>, <<emitterisactive,emitterIsActive>>
.Exemple
[source,lua]
----
-- Overlay de développement : quelle part de chaque réserve est utilisée.
function onOverlayUpdate()
overlayClear()
fontPrint(20, 20, string.format("live %d / %d", emitterGetCount(fireworks), fireworksPool))
return OVERLAY_UPDATED
end
----
[#emitterisactive]
==== emitterIsActive
[source,text]
----
active = emitterIsActive(emitter)
----
Indique si l'émetteur émet en continu ou a encore des particules vivantes : `true` depuis `emitterStart` ou `emitterBurst` jusqu'à ce que le flux soit arrêté et que la dernière particule soit morte, `false` une fois l'explosion finie. Un émetteur en émission continue dont la réserve se trouve vide est toujours actif.
*Renvoie :* booléen.
*Depuis :* 3.00.
*Voir aussi :* <<emitterstop,emitterStop>>, <<emittergetcount,emitterGetCount>>, <<emitterdelete,emitterDelete>>
.Exemple
[source,lua]
----
-- Les explosions ponctuelles vivent dans une liste et sont libérées quand elles s'éteignent.
function onOverlayUpdate()
for i = #explosions, 1, -1 do
if not emitterIsActive(explosions[i]) then
emitterDelete(explosions[i])
table.remove(explosions, i)
end
end
return OVERLAY_NOT_UPDATED
end
----
[#font]
=== Police
Singe a deux moteurs de rendu de texte. La police console intégrée est dessinée par <<overlayprint,overlayPrint>> et n'a besoin d'aucune ressource ; la famille `font*` charge des fichiers TrueType à une taille en points fixe et les rend soit directement sur l'overlay avec `fontPrint`, soit dans un sprite réutilisable avec `fontToSprite`. Les handles de police sont des entiers renvoyés par `fontLoad`, et le moteur garde une police sélectionnée globale, que `fontLoad` règle sur la police qu'il vient de charger et que `fontSelect` change. Le texte est rendu dans la couleur de premier plan actuelle, à la qualité choisie par `fontQuality`, et les positions du texte sont des coordonnées de l'overlay. Libérez les polices avec `fontUnload` ; `Singe/FreeSansBold.ttf` est fourni avec le moteur et toujours disponible.
[#fontload]
==== fontLoad
[source,text]
----
result = fontLoad(filename, pointSize)
----
Ouvre une police TrueType à une taille en points et en fait la police sélectionnée. La taille est fixée pour toute la vie du handle : chargez donc le même fichier une fois par taille dont vous avez besoin. Le nom passe par le système de fichiers virtuel comme toute autre ressource ; préfixez-le de `DIR` pour les fichiers livrés avec votre jeu, ou utilisez `Singe/FreeSansBold.ttf` pour la police fournie. Un fichier qui ne peut pas être ouvert met fin au script avec `Unable to open` et le nom.
*Paramètres :*
* `filename` -- chemin d'un fichier `.ttf`.
* `pointSize` -- taille en points, entière.
*Renvoie :* handle de police entier.
*Depuis :* 1.x
*Voir aussi :* <<fontselect,fontSelect>>, <<fontunload,fontUnload>>, <<fontquality,fontQuality>>
.Exemple
[source,lua]
----
-- Deux tailles de la police fournie : grande pour le titre, petite pour le HUD.
fontQuality(FONT_QUALITY_BLENDED)
titleFont = fontLoad("Singe/FreeSansBold.ttf", 32)
hudFont = fontLoad("Singe/FreeSansBold.ttf", 18)
fontSelect(titleFont)
titleSprite = fontToSprite("GAME OVER")
fontSelect(hudFont)
----
[#fontprint]
==== fontPrint
[source,text]
----
fontPrint(x, y, text)
----
Rend une chaîne avec la police sélectionnée, la qualité actuelle et la couleur de premier plan actuelle (plus la couleur de fond pour `FONT_QUALITY_SHADED`), puis la copie sur l'overlay avec son coin supérieur gauche en `(x, y)`. Appelez-la depuis `onOverlayUpdate`, à chaque image s'il le faut : le moteur garde chaque chaîne qu'il rend, indexée par la police, la qualité, les couleurs et le texte, et un appel ultérieur qui demande la même ne fait que la copier. Les chaînes qui ne sont pas affichées pendant environ une seconde sont oubliées, et celles d'une police disparaissent quand elle est déchargée : le texte qui change ne coûte donc pas plus qu'avant, et celui qui ne change pas est presque gratuit. Une page entière de meilleurs scores réaffichée à chaque image ne pose aucun problème. Sans police sélectionnée, une erreur met fin au script.
*Paramètres :*
* `x`, `y` -- coin supérieur gauche en coordonnées de l'overlay ; les fractions sont tronquées.
* `text` -- la chaîne à rendre.
*Depuis :* 1.x
*Voir aussi :* <<fonttosprite,fontToSprite>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>, <<overlayprint,overlayPrint>>
.Exemple
[source,lua]
----
-- Affichage en direct du calibrage audio ; le texte change à chaque image, donc on le rend directement.
function onOverlayUpdate()
overlayClear()
colorForeground(255, 255, 255, 255)
fontSelect(hudFont)
fontPrint(MARGIN_X, MARGIN_Y, "AUDIO DELAY CALIBRATION")
fontPrint(MARGIN_X, MARGIN_Y + 30, "Delay: " .. calibrationDelay .. " ms")
fontPrint(MARGIN_X, MARGIN_Y + 60, "Left / Right: 10 ms Up / Down: 1 ms")
return OVERLAY_UPDATED
end
----
[#fontquality]
==== fontQuality
[source,text]
----
fontQuality(mode)
----
Choisit comment `fontPrint` et `fontToSprite` rastérisent le texte à partir de maintenant. Le réglage est global, pas propre à chaque police, et commence à `FONT_QUALITY_SOLID`. Les sprites déjà créés par `fontToSprite` gardent la qualité avec laquelle ils ont été rendus. Une valeur autre que les trois constantes met fin au script sur une erreur.
*Paramètres :*
* `mode` -- `FONT_QUALITY_SOLID` (`1`) : le plus rapide, bords sur un bit, sans anticrénelage. `FONT_QUALITY_SHADED` (`2`) : glyphes anticrénelés sur un rectangle plein de la couleur de fond. `FONT_QUALITY_BLENDED` (`3`) : glyphes anticrénelés avec un alpha par pixel, qui se mélangent par-dessus n'importe quoi ; le choix habituel.
*Depuis :* 1.x
*Voir aussi :* <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>, <<colorbackground,colorBackground>>
.Exemple
[source,lua]
----
-- Texte mélangé sur la vidéo, mais une étiquette sur fond plein pour l'affichage de débogage.
fontQuality(FONT_QUALITY_BLENDED)
titleSprite = fontToSprite("HIGH SCORES")
colorBackground(0, 0, 0, 255)
fontQuality(FONT_QUALITY_SHADED)
debugLabel = fontToSprite("FRAME")
fontQuality(FONT_QUALITY_BLENDED)
----
[#fontselect]
==== fontSelect
[source,text]
----
fontSelect(id)
----
Fait d'une police chargée celle qu'utilisent `fontPrint` et `fontToSprite`. La sélection persiste jusqu'au prochain `fontSelect` ou `fontLoad`, puisque charger sélectionne aussi. Elle peut changer aussi souvent que nécessaire, y compris plusieurs fois dans un même `onOverlayUpdate`. Un handle inconnu ou déchargé (ou pas de nombre du tout) laisse la police actuelle sélectionnée et affiche un avertissement, car Hypseus Singe le laisse passer.
*Depuis :* 1.x
*Voir aussi :* <<fontload,fontLoad>>, <<fontprint,fontPrint>>, <<fonttosprite,fontToSprite>>
.Exemple
[source,lua]
----
-- Le titre dans la grande police, le score dans la petite, à chaque image.
function onOverlayUpdate()
overlayClear()
fontSelect(titleFont)
fontPrint(TITLE_X, 40, "HIGH SCORES")
fontSelect(hudFont)
fontPrint(8, 8, "Score: " .. score)
return OVERLAY_UPDATED
end
----
[#fonttosprite]
==== fontToSprite
[source,text]
----
result = fontToSprite(text)
----
Rend une chaîne avec la police, la qualité et les couleurs sélectionnées dans un nouveau sprite et renvoie son handle. Le sprite est dès lors indépendant de la police : il survit donc à `fontUnload`, et il fonctionne avec tous les appels `sprite*` : dessinez-le, mesurez-le, mettez-le à l'échelle ou faites-le tourner, et libérez-le avec `spriteUnload`. Pour une chaîne qui change, rendez un nouveau sprite et déchargez l'ancien. Sans police sélectionnée, une erreur met fin au script.
NOTE: Rendre du texte dans un sprite pour les performances est déconseillé. `fontPrint` garde désormais ce qu'il rend, si bien qu'afficher la même chaîne à chaque image coûte à peu près autant que dessiner un sprite fait à partir d'elle, sans sprite à gérer. Utilisez `fontToSprite` quand le texte doit être un sprite : pour le mettre à l'échelle, le faire tourner, le retourner ou le mesurer avec `spriteGetWidth` et `spriteGetHeight`.
*Renvoie :* handle de sprite entier.
*Depuis :* 1.x
*Voir aussi :* <<fontprint,fontPrint>>, <<spritedraw,spriteDraw>>, <<spritegetwidth,spriteGetWidth>>, <<spriteunload,spriteUnload>>
.Exemple
[source,lua]
----
-- Un titre qui tourne et grandit en apparaissant : du texte qui doit être un sprite, pour être tourné et mis à l'échelle.
titleSprite = fontToSprite("GAME OVER")
titleFrames = 0
function onOverlayUpdate()
titleFrames = titleFrames + 1
spriteRotate(titleSprite, (titleFrames * 12) % 360)
spriteScale(titleSprite, math.min(titleFrames / 30, 1))
spriteDraw(titleSprite, overlayGetWidth() / 2, overlayGetHeight() / 2, true)
return OVERLAY_UPDATED
end
----
[#fontunload]
==== fontUnload
[source,text]
----
fontUnload(id)
----
Ferme une police et invalide son handle. Les sprites faits à partir d'elle avec `fontToSprite` ne sont pas touchés. Si la police déchargée était la police sélectionnée, aucune police n'est plus sélectionnée ensuite et le prochain `fontPrint` ou `fontToSprite` met fin au script sur une erreur, jusqu'à ce que `fontSelect` ou `fontLoad` en choisisse une autre.
*Depuis :* 2.00
*Voir aussi :* <<fontload,fontLoad>>, <<fontselect,fontSelect>>, <<onshutdown,onShutdown>>
.Exemple
[source,lua]
----
-- Libérer les polices et les sprites de texte qu'elles ont produits.
function onShutdown()
spriteUnload(titleSprite)
for _, line in ipairs(descriptionLines) do
spriteUnload(line)
end
fontUnload(titleFont)
fontUnload(hudFont)
end
----
[#guiref]
=== Gui
Une GUI est un arbre de documents RmlUi de taille fixe, rendu dans sa propre texture à chaque image : `guiNew` en crée une et renvoie un handle entier, `guiLoad` y place un document RML et renvoie un handle de document, `guiDraw` la compose par-dessus l'overlay et `materialSetGui` l'affiche sur un matériau de la scène. Les documents sont du RML proche du HTML, mis en forme par du RCSS proche du CSS, et chargés par le système de fichiers virtuel comme les sprites ; `Singe/gui.rcss` est le thème fourni et `Singe/FreeSansBold.ttf` la police de repli. Les aides par id `guiGetValue`, `guiSetValue` et `guiSetHandler` atteignent les éléments sans passer par le modèle objet ; tout le reste est l'API Lua de RmlUi sur la globale `rmlui`, où une GUI est le contexte nommé `gui` suivi de son handle. Seize GUI au plus existent à la fois, chacune avec trente-deux documents. Une GUI a besoin du périphérique GPU, si bien que `guiNew` met fin au script sur une machine qui n'en a pas ; un mauvais handle de GUI lève `No GUI N`, et un mauvais handle de document `No document N in GUI G`. Voir <<gui,GUI>> dans Développement de jeux.
[#guiclose]
==== guiClose
[source,text]
----
guiClose(gui, document)
----
Décharge le document de la GUI, en abandonnant ses éléments, ses gestionnaires et ses scripts. Le handle du document n'est plus valide ensuite. Un document fermé par son propre script via l'API de RmlUi (`document:Close()`) est déchargé de la même façon. Lève `No document N in GUI G` pour un handle non valide.
*Depuis :* 3.00.
*Voir aussi :* <<guiload,guiLoad>>, <<guihide,guiHide>>, <<guidelete,guiDelete>>
.Exemple
[source,lua]
----
-- La page de saisie du meilleur score ne sert qu'une fois ; on la ferme une fois le nom saisi.
guiSetHandler(gui, entryPage, "enter", "click", function(g, d)
scores[#scores + 1] = { name = guiGetValue(g, d, "name"), score = score }
guiClose(g, d)
entryPage = nil
end)
----
[#guidelete]
==== guiDelete
[source,text]
----
guiDelete(gui)
----
Libère la GUI, tous ses documents et sa texture, et met fin à la mise à jour et au rendu qu'elle coûtait à chaque image. Les matériaux qui l'affichaient par `materialSetGui` reviennent à leur propre texture. Le handle n'est plus valide ensuite et met fin au script s'il est réutilisé ; comme une GUI continue d'être rendue, qu'elle soit dessinée ou non, supprimez celles dont un jeu n'a plus besoin plutôt que de les masquer.
*Depuis :* 3.00.
*Voir aussi :* <<guinew,guiNew>>, <<guiclose,guiClose>>, <<materialsetgui,materialSetGui>>
.Exemple
[source,lua]
----
-- La page des options ne vit que tant que le joueur s'y trouve.
function closeOptions()
guiDelete(optionsGui)
optionsGui = nil
discPlay()
end
----
[#guidraw]
==== guiDraw
[source,text]
----
guiDraw(gui)
guiDraw(gui, x, y)
guiDraw(gui, x, y, width, height)
----
Compose la texture de la GUI par-dessus l'overlay pour cette image, en coordonnées de l'overlay, si bien qu'elle respecte la bordure Sinden et l'échelle d'overscan comme tout le reste de l'overlay : sur tout l'overlay avec un seul argument, à sa propre taille avec un coin en `x, y`, ou mise à l'échelle dans le rectangle de `width` sur `height`. La place de l'appel dans le dessin du script décide de ce qui est sous la GUI et de ce qui est dessus : tout ce qui est dessiné sur l'overlay avant lui reste dessous, et tout ce qui est dessiné après lui -- jusqu'au prochain `guiDraw` ou au prochain tour du script -- va sur une couche à part, composée par-dessus cette GUI. Un pointeur dessiné après le HUD est dessiné par-dessus le HUD ; un second document dessiné après cela se place par-dessus le pointeur, avec sa propre couche au-dessus. Seize couches de ce genre au plus par image ; un appel au-delà dessine sa GUI sans rien par-dessus. `overlayClear` efface la couche sur laquelle on est en train de dessiner, et `overlaySetMonochrome` ne passe en gris que l'overlay de base. L'appel dure une image ; faites-le depuis `onOverlayUpdate` (ou `singeMain`) à chaque image où la GUI doit être visible. Les GUI se dessinent au-dessus de l'overlay dans l'ordre des appels (la dernière au-dessus), sous les particules `PARTICLE_OVER` et l'indicateur de pause, et la dernière GUI dessinée sous le pointeur est celle qui reçoit la souris. Seize appels au plus sont pris en compte par image ; les suivants sont ignorés. Tout autre nombre d'arguments lève `Expected 1, 3 or 5 arguments`.
*Paramètres :*
* `x`, `y` -- le coin supérieur gauche, en coordonnées de l'overlay.
* `width`, `height` -- la taille de dessin, en pixels de l'overlay ; la taille propre de la GUI s'ils sont omis.
*Depuis :* 3.00.
*Voir aussi :* <<guinew,guiNew>>, <<materialsetgui,materialSetGui>>, <<overlaysetresolution,overlaySetResolution>>
.Exemple
[source,lua]
----
-- Le HUD occupe le coin inférieur droit sur un tiers de la largeur de l'overlay, et le viseur
-- du pistolet est dessiné après lui, si bien que le panneau ne le cache jamais.
function onOverlayUpdate()
overlayClear()
guiSetValue(hud, hudPage, "lives", tostring(lives))
guiDraw(hud, overlayGetWidth() * 0.66, overlayGetHeight() * 0.8, overlayGetWidth() * 0.33, overlayGetHeight() * 0.2)
local x, y = mouseGetPosition(0)
spriteDraw(sight, x, y, true)
return OVERLAY_UPDATED
end
----
[#guidrawscreen]
==== guiDrawScreen
[source,text]
----
guiDrawScreen(gui)
guiDrawScreen(gui, x, y)
guiDrawScreen(gui, x, y, width, height)
----
Identique à <<guidraw,`guiDraw`>>, sauf que le rectangle se mesure sur toute la fenêtre et non sur l'image. Les coordonnées restent des coordonnées de l'overlay -- `0, 0` est le coin supérieur gauche de la fenêtre et `overlayGetWidth(), overlayGetHeight()` le coin inférieur droit -- si bien qu'une GUI peut se placer sur l'illustration du bezel, à côté de l'image, au-dessus ou au-dessous. Sans bezel, et sans `--scalefactor`, `--shiftx` ni `--shifty`, l'image occupe toute la fenêtre et cet appel est exactement `guiDraw`.
Les GUI en espace écran se dessinent après tout le reste, illustration du bezel comprise, et partagent avec `guiDraw` les seize dessins par image. Un clic les trouve de la même façon : le pointeur est ramené dans l'espace écran avant d'être testé, si bien qu'une GUI en espace écran qui accepte les entrées reçoit la souris où qu'elle ait été dessinée.
*Paramètres :*
* `x`, `y` -- le coin supérieur gauche, en coordonnées de l'overlay mesurées sur la fenêtre.
* `width`, `height` -- la taille de dessin ; la taille propre de la GUI s'ils sont omis.
*Depuis :* 3.00.
*Voir aussi :* <<guidraw,guiDraw>>, xref:Manual.adoc#bezels[Bezels], <<scorebezelenable,scoreBezelEnable>>
.Exemple
[source,lua]
----
-- Une carte sur le montant gauche de l'illustration de la borne, hors de l'image.
function onOverlayUpdate()
overlayClear()
guiDrawScreen(mapGui, 8, 44, 38, 114)
return OVERLAY_UPDATED
end
----
[#guigetheight]
==== guiGetHeight
[source,text]
----
height = guiGetHeight(gui)
----
La hauteur de la GUI dans ses propres pixels, telle que donnée à `guiNew`. Met fin au script pour un handle non valide.
*Renvoie :* un entier.
*Depuis :* 3.00.
*Voir aussi :* <<guigetwidth,guiGetWidth>>, <<guinew,guiNew>>
.Exemple
[source,lua]
----
-- Centrer une petite boîte de dialogue sur l'overlay.
local x = (overlayGetWidth() - guiGetWidth(dialog)) / 2
local y = (overlayGetHeight() - guiGetHeight(dialog)) / 2
guiDraw(dialog, x, y)
----
[#guigetstats]
==== guiGetStats
[source,text]
----
stats = guiGetStats()
----
Ce qu'a coûté le rendu de toutes les GUI lors de la dernière image terminée, pour régler les documents qui utilisent des filtres, des masques et des ombres (chacun d'eux dessinant à travers une couche de la taille de la GUI). Les compteurs couvrent toutes les GUI ensemble. Ils valent tous zéro pour une image où aucune GUI n'a changé d'aspect, puisque rien n'a été redessiné (voir <<guirefresh,guiRefresh>>).
*Renvoie :* une table avec les champs `drawCalls` (dessins indexés, y compris les écritures de masque, les compositions et les passes de filtre), `layerPushes` (couches ouvertes pour les filtres, les images de masque et les ombres de boîte), `filterPasses` (passes pleine taille et copies dans les chaînes de filtres ; un flou en coûte plusieurs), `maskWrites` (dessins dans le masque de découpe du stencil), `layerBytes` (octets de textures de couche actuellement alloués ; chacune a la taille de sa GUI, à quatre octets par pixel) et `passes` (passes de rendu commencées ; chaque changement de couche en commence une).
*Depuis :* 3.00.
*Voir aussi :* <<guinew,guiNew>>, <<gui,GUI>>
.Exemple
[source,lua]
----
-- Surveiller ce que coûte un écran rempli d'effets.
local s = guiGetStats()
debugPrint(string.format("gui draws %d layers %d filters %d passes %d, %d KB of layers", s.drawCalls, s.layerPushes, s.filterPasses, s.passes, s.layerBytes // 1024))
----
[#guigetvalue]
==== guiGetValue
[source,text]
----
value = guiGetValue(gui, document, id)
----
La valeur de l'élément qui porte cet `id` dans le document, sous forme de chaîne : la valeur d'un contrôle de formulaire (le texte d'un champ de texte, la `value` d'une option de `select`, un curseur dans le format propre à RmlUi comme `"85.000000"`, l'attribut `value` d'une case à cocher ou d'un bouton radio, `"on"` s'il n'en a pas, qu'il soit coché ou non ; son état est son attribut `checked`, lu par l'API de RmlUi), ou, pour tout autre élément, son RML intérieur. Renvoie `nil` quand aucun élément n'a cet id, si bien qu'on peut sonder une page sans mettre fin au script ; un mauvais handle de GUI ou de document y met fin. Les valeurs de plus de 4095 caractères sont coupées.
*Renvoie :* une chaîne, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<guisetvalue,guiSetValue>>, <<guisethandler,guiSetHandler>>
.Exemple
[source,lua]
----
-- Lire tout le formulaire quand on appuie sur Start.
guiSetHandler(gui, doc, "go", "click", function(g, d)
player.name = guiGetValue(g, d, "name")
player.hard = guiGetValue(g, d, "difficulty") == "hard"
player.volume = math.floor(tonumber(guiGetValue(g, d, "volume")))
end)
----
[#guigetwidth]
==== guiGetWidth
[source,text]
----
width = guiGetWidth(gui)
----
La largeur de la GUI dans ses propres pixels, telle que donnée à `guiNew`. Met fin au script pour un handle non valide.
*Renvoie :* un entier.
*Depuis :* 3.00.
*Voir aussi :* <<guigetheight,guiGetHeight>>, <<guinew,guiNew>>
.Exemple
[source,lua]
----
-- Le point sondé est-il dans la moitié droite de la page du moniteur ?
local g, px, py = sceneProbeGui(mouseX, mouseY)
if g ~= nil and px > guiGetWidth(g) / 2 then
showRightTooltip()
end
----
[#guihide]
==== guiHide
[source,text]
----
guiHide(gui, document)
----
Masque le document : il n'est pas dessiné et ne reçoit aucune entrée, mais il garde ses éléments et ses valeurs, et `guiShow` le fait revenir tel qu'il était. Lève `No document N in GUI G` pour un handle non valide.
*Depuis :* 3.00.
*Voir aussi :* <<guishow,guiShow>>, <<guiclose,guiClose>>
.Exemple
[source,lua]
----
-- Deux pages dans une GUI : la page des options se masque pendant que celle des commandes s'affiche.
guiSetHandler(gui, options, "controls", "click", function()
guiHide(gui, options)
guiShow(gui, controls)
end)
----
[#guiload]
==== guiLoad
[source,text]
----
document = guiLoad(gui, filename)
----
Charge un document RML dans la GUI, l'affiche et renvoie un handle de document entier pour les autres appels. Le nom passe par le système de fichiers virtuel comme celui d'un sprite, résolu par rapport au dossier depuis lequel Singe a été lancé : préfixez-le donc de `DIR` pour les fichiers livrés avec votre jeu ; les feuilles de style, les images et les polices nommées dans le document se résolvent par rapport au fichier qui les nomme, puis par la même recherche, si bien qu'un jeu empaqueté les trouve. Les blocs `<script>` et les attributs `on*` du document s'exécutent dans l'état Lua du jeu, et le document est accessible sous `rmlui.contexts["guiN"].documents["<body id>"]`. Un document qui ne peut pas être chargé met fin au script avec le nom et la raison donnée par RmlUi (un fichier manquant, une erreur d'analyse) ; les avertissements propres à RmlUi sur le balisage et le style vont à la console. Chaque GUI contient jusqu'à trente-deux documents.
*Renvoie :* le handle du document, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<guinew,guiNew>>, <<guishow,guiShow>>, <<guihide,guiHide>>, <<guiclose,guiClose>>, <<guiloadfont,guiLoadFont>>
.Exemple
[source,lua]
----
-- La page de titre et le HUD partagent une même GUI de la taille de l'overlay.
gui = guiNew(overlayGetWidth(), overlayGetHeight())
title = guiLoad(gui, DIR .. "gui/title.rml")
hud = guiLoad(gui, DIR .. "gui/hud.rml")
guiHide(gui, hud)
----
[#guiloadfont]
==== guiLoadFont
[source,text]
----
guiLoadFont(filename)
guiLoadFont(filename, fallback)
----
Charge une police TrueType ou OpenType pour toutes les GUI, si bien que les documents peuvent la nommer dans `font-family` par le nom de famille contenu dans le fichier (`font-weight` et `font-style` choisissant parmi les styles d'une même famille). Avec `fallback` à `true`, la police fournit aussi tout glyphe qu'aucune autre police n'a, ce que fait jusque-là la `Singe/FreeSansBold.ttf` du moteur (famille `FreeSans`, graisse bold, la police qu'utilise le thème fourni). Chargez les polices avant les documents qui les utilisent. Chaque police coûte un atlas de glyphes par taille à laquelle elle sert. Un fichier qui ne peut pas être ouvert met fin au script avec `Unable to load` et le nom.
*Paramètres :*
* `filename` -- le fichier de police, par le système de fichiers virtuel comme pour `fontLoad`.
* `fallback` -- `true` pour en faire la police de repli ; `false` (par défaut) pour une police ordinaire.
*Depuis :* 3.00.
*Voir aussi :* <<guiload,guiLoad>>, <<fontload,fontLoad>>
.Exemple
[source,lua]
----
-- La police de titrage du jeu pour les titres ; la FreeSans fournie reste pour le texte courant.
guiLoadFont(DIR .. "fonts/Orbitron-Bold.ttf")
gui = guiNew(overlayGetWidth(), overlayGetHeight())
menu = guiLoad(gui, DIR .. "gui/menu.rml")
----
[#guinew]
==== guiNew
[source,text]
----
gui = guiNew(width, height)
----
Crée une GUI de `width` sur `height` pixels, rendue dans sa propre texture à chaque image, qu'elle soit dessinée ou non, et renvoie son handle. La taille est l'espace de coordonnées du document : `100%` du body est la texture entière, et un `dp` est l'un de ses pixels. Une GUI de la taille de l'overlay convient à une page plein écran ; un élément de HUD ou un moniteur dans la scène en demande une plus petite, et un Raspberry Pi demande que tout reste modeste. Une nouvelle GUI ne fait qu'afficher jusqu'à ce que `guiSetInput` active les entrées. Lève `The size must be positive` pour une taille nulle ou négative, et `No GUI available` quand seize sont déjà utilisées ou que la machine n'a pas de périphérique GPU (une GUI a besoin du même périphérique que la scène 3D).
*Paramètres :*
* `width` -- la largeur de la texture, en pixels.
* `height` -- la hauteur de la texture, en pixels.
*Renvoie :* le handle de la GUI, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<guiload,guiLoad>>, <<guidraw,guiDraw>>, <<guidelete,guiDelete>>, <<materialsetgui,materialSetGui>>
.Exemple
[source,lua]
----
-- Une page de la taille de l'overlay, dessinée par-dessus le disque.
overlaySetResolution(discGetWidth(), discGetHeight())
gui = guiNew(overlayGetWidth(), overlayGetHeight())
doc = guiLoad(gui, DIR .. "gui/attract.rml")
function onOverlayUpdate()
overlayClear()
guiDraw(gui)
return OVERLAY_UPDATED
end
----
[#guirefresh]
==== guiRefresh
[source,text]
----
guiRefresh(gui)
----
Redessine la GUI à l'image suivante même si rien en elle ne semble avoir changé. Une GUI n'est redessinée que quand son aspect change : quand le document d'une image dessine exactement ce que l'image précédente a dessiné, et que rien de nouveau n'a été créé entre-temps, la texture contient déjà ce rendu et elle est gardée, ce qui épargne au GPU l'essentiel du coût d'une GUI sur les images où un HUD ou un menu ne bouge pas. Les changements faits par le RML, le RCSS, l'interface Lua de RmlUi ou les appels `gui*` sont tous vus. Cet appel est l'échappatoire si l'un d'eux venait à être manqué -- une GUI qui cesse de se mettre à jour alors qu'elle devrait le faire -- et un jeu ne devrait pas en avoir besoin ; si c'est le cas, cela vaut la peine de le signaler. Une GUI qui n'existe pas met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<guidraw,guiDraw>>, <<guigetstats,guiGetStats>>
.Exemple
[source,lua]
----
-- Un contournement, le temps qu'un changement manqué soit signalé : redessiner le HUD à chaque image.
function onOverlayUpdate()
guiRefresh(hud)
guiDraw(hud)
return OVERLAY_UPDATED
end
----
[#guisethandler]
==== guiSetHandler
[source,text]
----
guiSetHandler(gui, document, id, event, function)
guiSetHandler(gui, document, id, event, nil)
----
Appelle `function(gui, document, id, event, value)` chaque fois que l'élément qui porte cet `id` déclenche l'événement nommé : `"click"` pour les boutons et les lignes, `"change"` pour les contrôles de formulaire, `"submit"` pour un formulaire, ou tout autre nom d'événement RmlUi (`"focus"`, `"mouseover"`, `"keydown"`, ...). `value` est la valeur de l'élément à ce moment, sous forme de chaîne, telle que la renverrait `guiGetValue` (un curseur arrive tel que RmlUi le formate, `"85.000000"` : passez-le donc par `tonumber`). Une seule fonction est gardée par élément et par événement ; en régler une autre la remplace, et `nil` la retire. Le gestionnaire appartient à cet `id`, pas à un élément : quand l'élément est remplacé -- une liste reconstruite par `inner_rml` crée de nouveaux éléments -- réglez de nouveau le gestionnaire après la reconstruction et il s'attache à l'élément qui porte l'id à présent. Les gestionnaires s'exécutent au moment où les événements se produisent, entre les callbacks du jeu lui-même, et une erreur dans l'un d'eux met fin au jeu comme une erreur dans n'importe quel callback. Les ids font au plus 63 caractères. Lève `No element "id" in document N of GUI G` quand l'élément n'existe pas, et `Argument 5 must be a function or nil` pour toute autre chose à cette place.
*Paramètres :*
* `id` -- l'attribut `id` de l'élément.
* `event` -- le nom de l'événement RmlUi.
* `function` -- la fonction Lua à appeler, ou `nil` pour ne plus en appeler.
*Depuis :* 3.00.
*Voir aussi :* <<guigetvalue,guiGetValue>>, <<guisetvalue,guiSetValue>>, <<guisetinput,guiSetInput>>
.Exemple
[source,lua]
----
-- Un curseur de volume agit pendant qu'il bouge ; Start lit le reste du formulaire.
guiSetHandler(gui, doc, "volume", "change", function(g, d, id, event, value)
soundSetVolume(math.floor(tonumber(value)))
end)
guiSetHandler(gui, doc, "go", "click", function(g, d)
startGame(guiGetValue(g, d, "name"), guiGetValue(g, d, "difficulty"))
end)
----
[#guisetinput]
==== guiSetInput
[source,text]
----
guiSetInput(gui, enabled)
----
Indique si la souris et le pistolet optique, les touches, le texte tapé et les contacts du framework venus d'une manette ou d'une souris parviennent à la GUI. Une nouvelle GUI ne fait qu'afficher, ce qui convient à un HUD ou à un panneau ; activez les entrées pour un menu ou un formulaire. Tant qu'elles sont activées, le pointeur atteint la GUI partout où elle est dessinée à plat (la dernière dessinée au-dessus) ou montrée sur une surface de la scène, chaque touche lui est transmise sauf `Tab`, que la GUI ne reçoit que pendant la saisie dans un champ de texte (RmlUi s'en sert pour déplacer le focus et ne la rend jamais, et un jeu dont les touches comprennent `Tab` la perdrait sinon), un champ de texte qui a le focus active la saisie de texte de SDL, et les directions de la manette arrivent comme des touches fléchées, avec `ACTION_1` pour Entrée et `ACTION_2` pour Échap. Ce qu'un élément utilise n'atteint jamais `onInputPressed`, `onKeyPressed` ni les callbacks de la souris ; ce que rien n'a utilisé leur parvient comme d'habitude. Met fin au script pour un handle non valide.
*Depuis :* 3.00.
*Voir aussi :* <<guisethandler,guiSetHandler>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>
.Exemple
[source,lua]
----
-- Le HUD ne prend jamais d'entrées ; la page de pause, si, tant qu'elle est affichée.
guiSetInput(hud, false)
function onInputPressed(what)
if what == SWITCH_START1 then
guiShow(gui, pausePage)
guiSetInput(gui, true)
singeSetPauseFlag(true)
end
end
----
[#guisetvalue]
==== guiSetValue
[source,text]
----
guiSetValue(gui, document, id, value)
----
Règle la valeur de l'élément qui porte cet `id` : la valeur d'un contrôle de formulaire (le texte d'un champ de texte, le nombre d'un curseur, la `value` d'une option de `select` ; l'état `checked` d'une case à cocher est un attribut, réglé par l'API de RmlUi), ou, pour tout autre élément, son RML intérieur, si bien qu'un paragraphe ou une `div` peut être réécrit avec du texte simple ou avec du balisage. Le document refait sa mise en page à l'image suivante. Lève `No element "id" in document N of GUI G` quand l'élément n'existe pas.
*Paramètres :*
* `id` -- l'attribut `id` de l'élément.
* `value` -- la nouvelle valeur, une chaîne ; les nombres sont convertis.
*Depuis :* 3.00.
*Voir aussi :* <<guigetvalue,guiGetValue>>, <<guisethandler,guiSetHandler>>
.Exemple
[source,lua]
----
-- La ligne du score et la rangée des vies sont réécrites à chaque image ; la rangée est du balisage.
function onOverlayUpdate()
guiSetValue(hud, hudPage, "score", string.format("%08d", score))
guiSetValue(hud, hudPage, "lives", string.rep("<img src=\"ship.png\"/>", lives))
overlayClear()
guiDraw(hud)
return OVERLAY_UPDATED
end
----
[#guishow]
==== guiShow
[source,text]
----
guiShow(gui, document)
----
Affiche un document masqué par `guiHide` (un document est affiché quand `guiLoad` rend la main, l'appel n'est donc pas nécessaire à ce moment-là). Un document affiché est dessiné partout où sa GUI l'est et reçoit les entrées quand sa GUI les reçoit. Lève `No document N in GUI G` pour un handle non valide.
*Depuis :* 3.00.
*Voir aussi :* <<guihide,guiHide>>, <<guiload,guiLoad>>
.Exemple
[source,lua]
----
-- La page de fin de partie apparaît quand la dernière vie s'en va.
function onLastLifeLost()
guiSetValue(gui, gameOver, "final", string.format("%08d", score))
guiShow(gui, gameOver)
end
----
[#joint]
=== Articulation
Les articulations relient deux corps, ou un corps et le monde quand le second nœud vaut `-1`, et renvoient un handle d'articulation entier que prennent les autres appels. Les ancres et les axes sont donnés dans l'espace du monde au moment où l'articulation est créée, avec les corps là où ils se trouvent alors : placez donc les nœuds et donnez-leur des corps avant de les articuler. Les distances sont en unités du monde et les angles de charnière en degrés. Supprimer l'un des corps, ou son nœud, supprime l'articulation ; un handle qui a été supprimé met fin au script quand il est repassé. Voir <<physics,Physique>> dans Développement de jeux, Articulations.
[#jointhinge]
==== jointHinge
[source,text]
----
joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz)
----
Crée une charnière entre les deux corps, passant par le point du monde `(ax, ay, az)` selon l'axe `(dx, dy, dz)` : une porte, une roue, un levier, une bascule. Les corps gardent ce point et cet axe en commun et ne peuvent que tourner autour. L'axe est normalisé, sa longueur n'a donc pas d'importance ; un axe nul devient `(0, 1, 0)`. Les deux nœuds doivent porter un corps, ou bien `nodeB` peut valoir `-1` pour attacher `nodeA` au monde par une charnière. Met fin au script quand un nœud n'a pas de corps ou que l'articulation ne peut pas être créée.
*Paramètres :*
* `nodeA` -- le corps dont la rotation est mesurée ; un angle `jointSetLimits` positif est sa rotation autour de l'axe selon la règle de la main droite.
* `nodeB` -- l'autre corps, ou `-1` pour le monde.
* `ax`, `ay`, `az` -- l'ancre, un point du monde par lequel passe l'axe de la charnière.
* `dx`, `dy`, `dz` -- l'axe de la charnière dans l'espace du monde.
*Renvoie :* Le handle de l'articulation.
*Depuis :* 3.00.
*Voir aussi :* <<jointsetlimits,jointSetLimits>>, <<jointdelete,jointDelete>>, <<jointball,jointBall>>
.Exemple
[source,lua]
----
-- Une porte attachée au monde par une charnière le long de son bord gauche, qui s'ouvre de 100 degrés dans un sens.
door = nodeNew()
nodeSetMesh(door, meshBox(1.4, 2.2, 0.1), wood)
nodeSetPosition(door, -0.1, -0.62, 1.4)
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 1.4, 2.2, 0.1)
bodySetMass(door, 8)
hinge = jointHinge(door, -1, -0.85, -0.62, 1.4, 0, 1, 0)
jointSetLimits(hinge, 0, 100)
-- Plus tard, une poussée près de la poignée l'ouvre en grand.
bodyApplyImpulse(door, 0, 0, -60, 0.5, -0.62, 1.45)
----
[#jointball]
==== jointBall
[source,text]
----
joint = jointBall(nodeA, nodeB, ax, ay, az)
----
Crée une rotule au point du monde `(ax, ay, az)` : les corps peuvent tourner dans tous les sens autour de lui mais ne peuvent pas se séparer. Un pendule, un maillon de chaîne, une enseigne suspendue, l'épaule d'un ragdoll. Les deux nœuds doivent porter un corps, ou bien `nodeB` peut valoir `-1` pour suspendre `nodeA` à un point fixe du monde. Une rotule n'a pas de limites, donc `jointSetLimits` la refuse. Met fin au script quand un nœud n'a pas de corps ou que l'articulation ne peut pas être créée.
*Paramètres :*
* `nodeA` -- le premier corps.
* `nodeB` -- l'autre corps, ou `-1` pour le monde.
* `ax`, `ay`, `az` -- l'ancre, le point du monde autour duquel pivotent les corps.
*Renvoie :* Le handle de l'articulation.
*Depuis :* 3.00.
*Voir aussi :* <<jointhinge,jointHinge>>, <<jointdelete,jointDelete>>, <<bodyapplyimpulse,bodyApplyImpulse>>
.Exemple
[source,lua]
----
-- Une chaîne de quatre maillons pendue à un crochet, chacun relié par une rotule à celui du dessus.
local above = -1
for i = 1, 4 do
local link = nodeNew()
nodeSetMesh(link, meshSphere(0.12, 16), iron)
nodeSetPosition(link, 2, 3 - i * 0.3, 0)
bodyNew(link, BODY_DYNAMIC, SHAPE_SPHERE, 0.12)
bodySetMass(link, 1)
jointBall(link, above, 2, 3.15 - i * 0.3, 0)
above = link
end
----
[#jointslider]
==== jointSlider
[source,text]
----
joint = jointSlider(nodeA, nodeB, ax, ay, az, dx, dy, dz)
----
Ne laisse les corps glisser l'un par rapport à l'autre que selon l'axe `(dx, dy, dz)` passant par l'ancre `(ax, ay, az)`, en gardant leur orientation : un tiroir, un piston, un ascenseur, une porte coulissante. L'axe est normalisé, sa longueur n'a donc pas d'importance ; un axe nul devient `(0, 1, 0)`. Les deux nœuds doivent porter un corps, ou bien `nodeB` peut valoir `-1` pour faire courir `nodeA` le long d'un rail fixé dans le monde. Met fin au script quand un nœud n'a pas de corps ou que l'articulation ne peut pas être créée.
*Paramètres :*
* `nodeA` -- le corps dont la course est mesurée ; une distance `jointSetLimits` positive va dans le sens de l'axe.
* `nodeB` -- l'autre corps, ou `-1` pour le monde.
* `ax`, `ay`, `az` -- un point du monde sur le rail.
* `dx`, `dy`, `dz` -- la direction de la course dans l'espace du monde.
*Renvoie :* Le handle de l'articulation.
*Depuis :* 3.00.
*Voir aussi :* <<jointsetlimits,jointSetLimits>>, <<jointhinge,jointHinge>>, <<bodysetvelocity,bodySetVelocity>>
.Exemple
[source,lua]
----
-- Une caisse sur un rail selon X, poussée de côté au départ et arrêtée 2,4 unités plus loin.
crate = nodeNew()
nodeSetMesh(crate, meshBox(0.5, 0.5, 0.5), wood)
nodeSetPosition(crate, -0.6, 0.9, -3.0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
slider = jointSlider(crate, -1, -0.6, 0.9, -3.0, 1, 0, 0)
jointSetLimits(slider, 0, 2.4)
bodySetVelocity(crate, 3, 0, 0)
----
[#jointsetlimits]
==== jointSetLimits
[source,text]
----
jointSetLimits(joint, low, high)
----
Borne l'angle d'une charnière en degrés, ou la course d'une glissière en unités du monde, de part et d'autre de la position de départ de l'articulation. Un angle de charnière positif est une rotation de `nodeA` autour de l'axe selon la règle de la main droite ; une distance de glissière positive va dans le sens de l'axe. `low` est ramené à `0` au plus et `high` à `0` au moins, si bien que la pose de départ est toujours dans la plage. Rappelez-la pour changer la plage à tout moment. Met fin au script pour une rotule ou un handle qui n'existe plus.
*Paramètres :*
* `joint` -- le handle venu de `jointHinge` ou de `jointSlider`.
* `low` -- l'angle ou la distance négatifs les plus éloignés, `0` ou moins.
* `high` -- l'angle ou la distance positifs les plus éloignés, `0` ou plus.
*Remarques :* Les deux corps échangent leurs rôles s'ils sont donnés dans l'autre ordre, ce qui inverse le signe de la plage.
*Depuis :* 3.00.
*Voir aussi :* <<jointhinge,jointHinge>>, <<jointslider,jointSlider>>
.Exemple
[source,lua]
----
-- Un levier qui bascule de 30 degrés de chaque côté, puis se bloque une fois l'énigme résolue.
lever = nodeNew()
nodeSetMesh(lever, meshBox(0.1, 0.8, 0.1), iron)
nodeSetPosition(lever, 4, 1.2, 0)
bodyNew(lever, BODY_DYNAMIC, SHAPE_BOX, 0.1, 0.8, 0.1)
pivot = jointHinge(lever, -1, 4, 0.8, 0, 0, 0, 1)
jointSetLimits(pivot, -30, 30)
function lockLever()
jointSetLimits(pivot, 0, 0)
end
----
[#jointdelete]
==== jointDelete
[source,text]
----
jointDelete(joint)
----
Supprime l'articulation ; les corps sont de nouveau libres l'un de l'autre et gardent la vitesse qu'ils avaient. Les articulations disparaissent aussi avec l'un ou l'autre de leurs corps, donc cet appel ne sert qu'à libérer deux corps qui restent tous les deux. Met fin au script pour un handle qui n'existe pas ou qui a déjà été supprimé.
*Depuis :* 3.00.
*Voir aussi :* <<jointhinge,jointHinge>>, <<jointball,jointBall>>, <<bodydelete,bodyDelete>>
.Exemple
[source,lua]
----
-- La corde casse quand la caisse qui y pend est heurtée assez fort.
function onCollision(a, b, x, y, z, speed)
if (a == crate or b == crate) and speed > 6 and rope then
jointDelete(rope)
rope = nil
soundPlay(snap)
end
end
----
[#joystick]
=== Joystick
Tout périphérique de jeu que Singe ouvre est une manette et se lit par les appels
`controller*` ; un périphérique que SDL ne reconnaît pas leur parvient par une
affectation que Singe écrit pour lui, si bien qu'il n'y a rien d'autre à appeler.
Voir xref:Manual.adoc#devices[Tout périphérique est une manette]. Les deux appels
de cette section servent à laisser un stick tenir lieu de souris.
[#joymouseenable]
==== joyMouseEnable
[source,text]
----
joyMouseEnable(enabled)
----
Extension Hypseus. Active ou désactive le pilotage de la souris par le joystick pendant que le jeu tourne. Quand il est actif, le stick gauche de l'emplacement de manette `0` déplace le pointeur que voit le jeu, par le même `onMouseMoved` qu'atteint une vraie souris, en restant à l'intérieur de l'image ; `--js_range` est la vitesse et l'axe obéit à sa propre zone morte. C'est ce qui rend un jeu de pistolet optique jouable sur une borne qui a un stick et pas de souris.
Il part de `--joymouse`, désactivé par défaut, et un `singeReload` l'y ramène. Singe diffère d'Hypseus sur ce défaut : le `controls.cfg` fourni lie déjà ce stick aux quatre contacts de direction, et lui faire piloter aussi le curseur sans qu'on le demande surprendrait chaque jeu existant.
*Paramètres :*
* `enabled` -- booléen.
*Renvoie :* rien.
*Depuis :* 3.00 (Hypseus)
*Voir aussi :* <<joymouseisenabled,joyMouseIsEnabled>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
.Exemple
[source,lua]
----
-- Un jeu de tir qui laisse le joueur viser au stick quand aucune souris n'est branchée.
if mouseHowMany() == 0 then
joyMouseEnable(true)
end
----
[#joymouseisenabled]
==== joyMouseIsEnabled
[source,text]
----
enabled = joyMouseIsEnabled()
----
Indique si le stick pilote le curseur de la souris en ce moment, que cela ait été décidé par `--joymouse` ou par `joyMouseEnable`.
*Renvoie :* booléen.
*Depuis :* 3.00
*Voir aussi :* <<joymouseenable,joyMouseEnable>>
[#keyboard]
=== Clavier
Singe lit le clavier dans l'un de deux modes, choisi avec `keyboardSetMode`. En `MODE_NORMAL` (le mode par défaut), les touches sont traduites par les affectations de `controls.cfg` en valeurs `SWITCH_*` qui atteignent `onInputPressed` et `onInputReleased`, et les touches non affectées sont ignorées ; en `MODE_FULL`, chaque touche atteint le script brute, par `onKeyPressed` et `onKeyReleased` ainsi que par `onInputPressed` et `onInputReleased`. Quel que soit le mode, `keyboardIsDown`, `keyboardGetLastDown` et `keyboardGetLastUp` interrogent l'état réel du clavier, ce que veut le modèle à fil d'exécution (voir <<threaded,À fil d'exécution>>). Un scancode désigne la position physique d'une touche (la touche `A` est le scancode `4` sur toutes les dispositions) alors qu'un keysym est le caractère que produit la touche ; la table `SCANCODE` de `Framework.singe` nomme chaque scancode, `keyboardIsDown` prend des scancodes, et `onKeyPressed` livre les deux. Les callbacks eux-mêmes sont décrits sous <<enginecallbacks,Callbacks du moteur>>.
[#keyboardcatchquit]
==== keyboardCatchQuit
[source,text]
----
keyboardCatchQuit(catch)
----
Extension Hypseus. Confie le contact de sortie au script, exactement comme le fait `singeSetQuitKeyEnabled(not catch)` : `keyboardCatchQuit(true)` empêche le moteur de quitter sur `INPUT_QUIT`, et `false` le lui rend. C'est une fonction définie dans `Framework.singe`, conservée pour que les jeux écrits pour Hypseus Singe tournent sans modification.
*Remarques :* Alias hérité.
WARNING: Le nouveau code doit appeler <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<singesetquitkeyenabled,singeSetQuitKeyEnabled>>, <<singequit,singeQuit>>
.Exemple
[source,lua]
----
-- Code de jeu Hypseus ; l'écriture moderne est singeSetQuitKeyEnabled(false).
if keyboardCatchQuit ~= nil then
keyboardCatchQuit(true)
end
function onInputPressed(what)
if what == SWITCH_QUIT then
askToQuit()
end
end
----
[#keyboardgetlastdown]
==== keyboardGetLastDown
[source,text]
----
scancode = keyboardGetLastDown()
----
Renvoie le scancode de la touche enfoncée le plus récemment dans l'image en cours, ou `0` quand rien n'a été enfoncé depuis l'image précédente. La valeur est effacée après chaque appel à `onOverlayUpdate`, ce qui en fait une interrogation ponctuelle « une touche vient-elle d'être enfoncée ? » pour le modèle à fil d'exécution. Les boutons des manettes et de la souris sont aussi enregistrés ici, sous leurs codes `GAMEPAD_N` et `MOUSE_N`, puisque le moteur les fait passer par le même chemin que les touches. La valeur est conservée quels que soient le mode du clavier et les affectations de `controls.cfg`.
*Renvoie :* scancode entier, ou `0`.
*Depuis :* 2.10
*Voir aussi :* <<keyboardgetlastup,keyboardGetLastUp>>, <<keyboardisdown,keyboardIsDown>>
.Exemple
[source,lua]
----
-- Modèle à fil d'exécution : réagir à un appui de touche par image.
function singeMain()
while true do
local key = keyboardGetLastDown()
if key == SCANCODE.ESCAPE.value then
singeQuit()
elseif key == SCANCODE.SPACE.value then
jump()
end
singeYield()
end
end
----
[#keyboardgetlastup]
==== keyboardGetLastUp
[source,text]
----
scancode = keyboardGetLastUp()
----
Renvoie le scancode de la touche relâchée le plus récemment dans l'image en cours, ou `0` quand rien n'a été relâché depuis l'image précédente. La valeur est effacée après chaque appel à `onOverlayUpdate`, comme celle de `keyboardGetLastDown`, et enregistre de la même façon les codes des boutons des manettes et de la souris.
*Renvoie :* scancode entier, ou `0`.
*Depuis :* 2.10
*Voir aussi :* <<keyboardgetlastdown,keyboardGetLastDown>>, <<keyboardisdown,keyboardIsDown>>
.Exemple
[source,lua]
----
-- Un tir chargé part quand on lâche la touche de tir.
function singeMain()
while true do
if keyboardIsDown(SCANCODE.LCTRL.value) then
charge = math.min(charge + 1, chargeMax)
elseif keyboardGetLastUp() == SCANCODE.LCTRL.value then
fireShot(charge)
charge = 0
end
singeYield()
end
end
----
[#keyboardgetmode]
==== keyboardGetMode
[source,text]
----
mode = keyboardGetMode()
----
Renvoie le mode du clavier en vigueur : `MODE_NORMAL` (`0`) ou `MODE_FULL` (`1`). Chaque script démarre en `MODE_NORMAL`, y compris après un rechargement.
*Renvoie :* mode entier.
*Depuis :* 1.x (RDG)
*Voir aussi :* <<keyboardsetmode,keyboardSetMode>>
.Exemple
[source,lua]
----
-- Fonction de saisie de texte partagée qui rétablit le mode qu'avait l'appelant.
function promptForName()
local previous = keyboardGetMode()
keyboardSetMode(MODE_FULL)
entering = true
restoreMode = previous
end
----
[#keyboardgetmodifiers]
==== keyboardGetModifiers
[source,text]
----
modifiers = keyboardGetModifiers()
----
Renvoie les touches de modification maintenues en ce moment, sous forme de masque de bits `KMOD_*` de SDL. Testez-le avec un et bit à bit contre les valeurs de la table `MODIFIER` : les entrées d'une seule touche comme `MODIFIER.LSHIFT` et les entrées combinées `MODIFIER.SHIFT`, `MODIFIER.CTRL`, `MODIFIER.ALT` et `MODIFIER.GUI`, qui couvrent l'un ou l'autre côté. La fonction lit l'état réel, donc elle fonctionne dans tous les modes du clavier et depuis n'importe quel callback.
*Renvoie :* masque de bits entier.
*Depuis :* 2.10
*Voir aussi :* <<keyboardgetlastdown,keyboardGetLastDown>>, <<onkeypressedonkeyreleased,onKeyPressed>>
.Exemple
[source,lua]
----
-- Maj+Tab affiche ou masque l'overlay des statistiques.
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.TAB.value then
if keyboardGetModifiers() & MODIFIER.SHIFT.value ~= 0 then
showStats = not showStats
end
end
end
----
[#keyboardisdown]
==== keyboardIsDown
[source,text]
----
down = keyboardIsDown(scancode)
----
Teste si une touche est physiquement maintenue en ce moment, d'après l'état que le moteur garde pour chaque événement de touche qu'il voit. Elle ne dépend ni du mode du clavier ni de `controls.cfg`, et elle fonctionne depuis n'importe quel callback ; elle convient donc aux déplacements qui doivent continuer tant qu'une touche est maintenue. Les scancodes hors de la plage que définit SDL renvoient `false` au lieu de mettre fin au script.
*Paramètres :*
* `scancode` -- une valeur de la table `SCANCODE`, comme `SCANCODE.LEFT.value`.
*Renvoie :* booléen.
*Depuis :* 2.10
*Voir aussi :* <<keyboardgetlastdown,keyboardGetLastDown>>, <<controllergetbutton,controllerGetButton>>
.Exemple
[source,lua]
----
-- Déplacement dans huit directions à partir des touches maintenues.
function onOverlayUpdate()
local dx = 0
local dy = 0
if keyboardIsDown(SCANCODE.LEFT.value) then dx = dx - 1 end
if keyboardIsDown(SCANCODE.RIGHT.value) then dx = dx + 1 end
if keyboardIsDown(SCANCODE.UP.value) then dy = dy - 1 end
if keyboardIsDown(SCANCODE.DOWN.value) then dy = dy + 1 end
playerX = playerX + dx * speed
playerY = playerY + dy * speed
drawPlayer()
return OVERLAY_UPDATED
end
----
[#keyboardsetmode]
==== keyboardSetMode
[source,text]
----
keyboardSetMode(mode)
----
Bascule entre les deux modèles de clavier. En `MODE_NORMAL`, seules les touches affectées dans `controls.cfg` atteignent le script, sous forme de valeurs `SWITCH_*`, la répétition des touches est supprimée pour que chaque appui n'arrive qu'une fois, et le moteur agit sur ses propres contacts (pause, sortie, capture d'écran, capture de la souris). En `MODE_FULL`, chaque touche atteint `onInputPressed` et `onInputReleased` sous forme de son keysym, et `onKeyPressed` et `onKeyReleased` sous forme de keysym et de scancode, le texte que produit un appui atteint `onTextInput`, la répétition des touches est livrée pour la saisie de texte, et les affectations clavier des contacts propres au moteur sont ignorées pour que le jeu garde toutes les touches ; les affectations des boutons des manettes et de la souris agissent toujours, et ces boutons atteignent toujours le script sous forme de leurs valeurs `SWITCH_*`, puisqu'on ne peut pas les taper. La plupart des jeux règlent le mode une fois au démarrage ; un jeu avec saisie de texte passe en `MODE_FULL` le temps de la saisie, puis revient. Toute autre valeur met fin au script. En `MODE_FULL`, les keysyms de quatre touches sont aussi des numéros de contact -- Retour arrière vaut `8` (`SWITCH_BUTTON3`), Tab `9` (`SWITCH_COIN1`), Entrée `13` et Échap `27` -- si bien qu'un script qui lit `onInputPressed` pour un contact de manette ou de souris et qui attend aussi ces touches doit vérifier `keyboardIsDown` pour la touche avant de traiter la valeur comme le contact.
*Paramètres :*
* `mode` -- `MODE_NORMAL` (`0`) ou `MODE_FULL` (`1`).
*Depuis :* 1.x (RDG)
*Voir aussi :* <<keyboardgetmode,keyboardGetMode>>, <<oninputpressedoninputreleased,onInputPressed>>, <<onkeypressedonkeyreleased,onKeyPressed>>
.Exemple
[source,lua]
----
-- La saisie du meilleur score prend les touches brutes, puis le mode de jeu revient.
function beginInitials()
initials = ""
keyboardSetMode(MODE_FULL)
end
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.RETURN.value then
keyboardSetMode(MODE_NORMAL)
saveScore(initials, score)
elseif keysym >= string.byte("a") and keysym <= string.byte("z") and #initials < 3 then
initials = initials .. string.char(keysym):upper()
end
end
----
[#light]
=== Lumière
Les lumières sont des nœuds portant une lumière, créés par `lightNew`, puis déplacés, rattachés à un parent et animés avec les appels de nœud. Jusqu'à huit lumières visibles éclairent une image, dans l'ordre des nœuds ; `sceneSetAmbient` et un ciel venu de `sceneSetSky` ajoutent une lumière venue de partout. Les lumières directionnelles et les projecteurs éclairent le long du -Z du nœud, donc orientez-les avec `nodeLookAt` ou `nodeSetRotation` ; les lumières ponctuelles n'ont besoin que d'une position. Les couleurs sont des entiers de `0` à `255`, les intensités des multiplicateurs dans la lumière linéaire de la scène, les angles des degrés et les distances des unités du monde. Chaque appel `lightSet*` met fin au script pour un nœud qui ne porte pas de lumière. Voir <<scenes3d,Scènes 3D>> pour l'éclairage, l'exposition et les ombres.
[#lightnew]
==== lightNew
[source,text]
----
node = lightNew(type)
node = lightNew(type, parent)
----
Crée un nouveau nœud portant une lumière et renvoie le handle du nœud. La lumière démarre blanche, d'intensité `1`, sans limite de portée et, pour un projecteur, avec un cône de `20` à `30` degrés. Placez-la et orientez-la avec les appels de nœud : `nodeSetPosition` pour une lumière ponctuelle ou un projecteur, `nodeLookAt` pour une lumière directionnelle ou un projecteur. Une lumière rattachée à un nœud qui bouge bouge avec lui, si bien qu'un phare sur une voiture ou une torche dans une main tient en un appel. Met fin au script pour un type inconnu ou quand aucun nœud ne peut être créé.
*Paramètres :*
* `type` -- `LIGHT_DIRECTIONAL` (un soleil ; seule sa direction compte), `LIGHT_POINT` (une ampoule qui faiblit avec la distance dans toutes les directions) ou `LIGHT_SPOT` (un cône le long du -Z du nœud).
* `parent` -- le nœud parent ; la racine s'il est omis.
*Renvoie :* Le handle du nœud de la lumière, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<lightsetcolor,lightSetColor>>, <<lightsetintensity,lightSetIntensity>>, <<lightsetrange,lightSetRange>>, <<lightsetcone,lightSetCone>>, <<scenesetambient,sceneSetAmbient>>
.Exemple
[source,lua]
----
-- Lumière principale chaude venue d'en haut à gauche, lumière d'appoint froide venue de la droite,
-- et un phare qui suit la voiture.
key = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(key, -3, 4, 3)
nodeLookAt(key, 0, 0, 0)
lightSetColor(key, 255, 235, 200)
lightSetIntensity(key, 1.4)
fill = lightNew(LIGHT_POINT)
nodeSetPosition(fill, 3, 1, 2)
lightSetColor(fill, 150, 190, 255)
lightSetIntensity(fill, 4)
lightSetRange(fill, 10)
headlight = lightNew(LIGHT_SPOT, car)
nodeSetPosition(headlight, 0.6, 0.5, -1.8)
lightSetIntensity(headlight, 30)
----
[#lightsetcolor]
==== lightSetColor
[source,text]
----
lightSetColor(node, r, g, b)
----
La couleur de la lumière, de `0` à `255` par canal (les valeurs hors plage sont ramenées dans la plage), traitée comme du sRGB ; blanche par défaut. La luminosité relève de `lightSetIntensity`, alors gardez la couleur pour la teinte (une ampoule chaude `255, 230, 190`, une lune froide `150, 190, 255`) et laissez au moins un canal près de `255`. Met fin au script quand le nœud ne porte pas de lumière.
*Depuis :* 3.00.
*Voir aussi :* <<lightsetintensity,lightSetIntensity>>, <<lightnew,lightNew>>
.Exemple
[source,lua]
----
-- L'enseigne au néon passe d'une couleur à l'autre.
signLight = lightNew(LIGHT_POINT)
nodeSetPosition(signLight, 2, 3, -1)
lightSetIntensity(signLight, 8)
lightSetRange(signLight, 6)
neon = { { 255, 40, 120 }, { 40, 220, 255 }, { 255, 210, 40 } }
function onOverlayUpdate()
local phase = math.floor(singeGetTicks() / 700) % #neon + 1
lightSetColor(signLight, neon[phase][1], neon[phase][2], neon[phase][3])
return OVERLAY_UPDATED
end
----
[#lightsetcone]
==== lightSetCone
[source,text]
----
lightSetCone(node, innerDegrees, outerDegrees)
----
La forme du cône d'un projecteur, les deux angles étant mesurés depuis l'axe (le -Z du nœud) : pleine luminosité à l'intérieur de `innerDegrees`, puis un fondu progressif jusqu'à rien à `outerDegrees`. Les valeurs par défaut sont `20` et `30`. Un faible écart entre les deux donne une poursuite de théâtre aux bords nets, un grand écart une flaque de lumière douce. Les valeurs sont enregistrées pour toutes les lumières, mais seul un `LIGHT_SPOT` s'en sert. Met fin au script quand le nœud ne porte pas de lumière.
*Paramètres :*
* `node` -- le nœud de la lumière.
* `innerDegrees` -- demi-angle de pleine luminosité, en degrés depuis l'axe.
* `outerDegrees` -- demi-angle où la lumière n'atteint plus rien, en degrés depuis l'axe ; porté à `innerDegrees` s'il est plus petit.
*Depuis :* 3.00.
*Voir aussi :* <<lightnew,lightNew>>, <<lightsetrange,lightSetRange>>, <<nodelookat,nodeLookAt>>
.Exemple
[source,lua]
----
-- Une poursuite sur le chanteur, aux bords nets, depuis le pont lumière.
spot = lightNew(LIGHT_SPOT)
nodeSetPosition(spot, 0, 8, 4)
nodeLookAt(spot, nodeGetWorldPosition(singer))
lightSetColor(spot, 255, 245, 220)
lightSetIntensity(spot, 80)
lightSetCone(spot, 8, 10)
lightSetShadow(spot, true)
----
[#lightsetintensity]
==== lightSetIntensity
[source,text]
----
lightSetIntensity(node, intensity)
----
Le multiplicateur de luminosité de la lumière dans la lumière linéaire de la scène, `1` par défaut. Une lumière directionnelle d'intensité `1` rend blanche une surface blanche tournée vers elle. Une lumière ponctuelle ou un projecteur faiblit avec la distance selon `1 / (1 + d^2)`, si bien que son intensité est la luminosité à la lumière elle-même, la moitié à une unité de distance et un centième à dix unités ; les ampoules demandent en général des valeurs bien au-dessus de `1`, de `10` à `50` pour une pièce, et `sceneSetExposure` ramène toute la scène dans la plage. Met fin au script quand le nœud ne porte pas de lumière.
*Depuis :* 3.00.
*Voir aussi :* <<lightsetrange,lightSetRange>>, <<scenesetexposure,sceneSetExposure>>, <<lightsetcolor,lightSetColor>>
.Exemple
[source,lua]
----
-- Une lanterne qui vacille dans le vent.
lantern = lightNew(LIGHT_POINT, lanternNode)
lightSetColor(lantern, 255, 200, 140)
lightSetRange(lantern, 9)
function onOverlayUpdate()
local flicker = 10 + math.random() * 3
lightSetIntensity(lantern, flicker)
return OVERLAY_UPDATED
end
----
[#lightsetrange]
==== lightSetRange
[source,text]
----
lightSetRange(node, range)
----
La portée d'une lumière ponctuelle ou d'un projecteur, en unités du monde ; `0` (la valeur par défaut) signifie aucune limite. La lumière s'éteint exactement à la portée, en plus de son atténuation avec la distance, si bien que les lumières n'apparaissent pas d'un coup quand des objets la franchissent. Une portée borne aussi jusqu'où doit aller la carte d'ombres d'une lumière qui projette des ombres, ce qui garde ses ombres nettes. Les lumières directionnelles l'ignorent. Met fin au script quand le nœud ne porte pas de lumière.
*Depuis :* 3.00.
*Voir aussi :* <<lightsetintensity,lightSetIntensity>>, <<lightsetshadow,lightSetShadow>>
.Exemple
[source,lua]
----
-- Des réverbères le long de la route, chacun éclairant son propre tronçon.
for i = 0, 9 do
local lamp = lightNew(LIGHT_POINT)
nodeSetPosition(lamp, 4, 5, -i * 20)
lightSetColor(lamp, 255, 220, 170)
lightSetIntensity(lamp, 20)
lightSetRange(lamp, 14)
end
----
[#lightsetshadow]
==== lightSetShadow
[source,text]
----
lightSetShadow(node, shadow)
----
Fait projeter à la lumière des ombres de tout ce qui est opaque dans la scène, ou l'en empêche ; désactivé par défaut. N'importe laquelle des huit lumières peut projeter des ombres, dans n'importe quelle combinaison. Un projecteur reçoit une carte d'ombres ; une lumière directionnelle reçoit une carte par cascade (`sceneSetShadowCascades`) ; une lumière ponctuelle reçoit une carte cubique de six vues, qui couvre toutes les directions (une ampoule dans une pièce) pour six fois le coût. `nodeSetShadow` dispense un maillage de projeter des ombres. Met fin au script quand le nœud ne porte pas de lumière.
*Remarques :* Chaque carte d'ombres coûte une passe supplémentaire sur la scène par image, une carte cubique six, et un soleil en cascades autant qu'il a de cascades ; la taille des cartes est réglée par `sceneSetShadowSize`. Un GPU intégré moderne gère sans peine huit lumières projetant des ombres ; un Raspberry Pi en veut une ou deux à `512`.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetshadowsize,sceneSetShadowSize>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<nodesetshadow,nodeSetShadow>>, <<lightnew,lightNew>>
.Exemple
[source,lua]
----
-- Le soleil projette des ombres ; les lampes ne font qu'éclairer. Le verre de l'ampoule
-- ne doit pas faire d'ombre à sa propre lampe.
sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 3, 6, 4)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
lamp = lightNew(LIGHT_POINT)
nodeSetPosition(lamp, 3, 3.1, -2)
lightSetIntensity(lamp, 12)
lightSetRange(lamp, 14)
nodeSetShadow(bulbGlass, false)
----
[#material]
=== Matériau
Un matériau, c'est l'aspect d'un maillage : une couleur de base avec alpha, une texture de base facultative tirée d'un sprite, d'un fichier KTX2, d'une vidéo ou d'une vue rendue, et les paramètres métal-rugosité de glTF avec quatre cartes facultatives. `materialNew` renvoie un handle entier ; passez-le à `nodeSetMesh` ou à `nodeSetMaterial`, et à chaque appel `materialSet*`. Les matériaux sont partagés : en modifier un modifie donc chaque maillage dessiné avec lui, et un nœud qui n'en a pas se dessine en blanc, à moitié rugueux. Les couleurs sont des entiers de `0` à `255` par canal et sont traitées comme du sRGB ; les facteurs comme la rugosité vont de `0` à `1`. Un mauvais handle met fin au script. Voir <<scenes3d,Scènes 3D>> pour la façon dont les matériaux sont éclairés.
[#materialnew]
==== materialNew
[source,text]
----
material = materialNew()
----
Alloue un nouveau matériau et renvoie son handle. Il démarre blanc et opaque (couleur `255, 255, 255, 255`), avec une rugosité de `0.5`, une métallicité de `0`, aucune couleur émissive, une répétition de `1, 1`, un filtrage linéaire, une seule face, éclairé, et sans texture ni cartes. Réglez ce qui en diffère et passez le handle à `nodeSetMesh`. Les modèles chargés apportent leurs propres matériaux : celui-ci sert donc aux maillages construits par le script. Met fin au script sur une machine sans GPU utilisable.
*Renvoie :* Le handle du matériau, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>, <<materialdelete,materialDelete>>, <<nodesetmesh,nodeSetMesh>>
.Exemple
[source,lua]
----
-- De l'or brossé pour un trophée, du verre bleu translucide pour sa vitrine.
gold = materialNew()
materialSetColor(gold, 255, 200, 60)
materialSetMetallic(gold, 1)
materialSetRoughness(gold, 0.35)
glass = materialNew()
materialSetColor(glass, 80, 160, 255, 90)
materialSetBlend(glass, true)
materialSetRoughness(glass, 0.1)
trophy = nodeNew()
nodeSetMesh(trophy, meshCone(0.3, 0.8, 24), gold)
----
[#materialdelete]
==== materialDelete
[source,text]
----
materialDelete(material)
----
Libère le matériau et toute texture qu'il a envoyée au GPU. Chaque nœud qui était dessiné avec lui revient à l'aspect par défaut (blanc, à moitié rugueux) : réaffectez donc d'abord ces nœuds avec `nodeSetMaterial` s'ils doivent garder un aspect. Le handle n'est plus valide ensuite et met fin au script s'il est réutilisé. Le matériau privé qu'un nœud se crée sous `nodeSetSprite` ou `nodeSetText` appartient au nœud et ne peut pas être supprimé ; l'appel met fin au script, et c'est en retirant le sprite ou le texte qu'on le libère. Les matériaux ne coûtent pas cher ; les supprimer compte surtout pour ceux qui portaient de grandes textures.
*Depuis :* 3.00.
*Voir aussi :* <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>
.Exemple
[source,lua]
----
-- Changer l'aspect du sol du niveau quand la saison change.
function setSeason(texture)
local look = materialNew()
materialSetTexture(look, texture)
materialSetTiling(look, 16, 16)
nodeSetMaterial(ground, look)
if groundLook then
materialDelete(groundLook)
end
groundLook = look
end
----
[#materialsetblend]
==== materialSetBlend
[source,text]
----
materialSetBlend(material, blend)
----
Avec `true`, le matériau est dessiné translucide, mélangé selon l'alpha de la couleur de base multiplié par l'alpha de la texture, après tous les maillages opaques et trié du plus éloigné au plus proche. Les maillages mélangés n'écrivent pas la profondeur : laissez-le donc à `false` (la valeur par défaut) pour les surfaces pleines, car un maillage opaque avec le mélange activé peut laisser transparaître ce qui est derrière lui. Servez-vous-en avec un alpha inférieur à `255` venu de `materialSetColor` ou une texture qui a de la transparence.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetcolor,materialSetColor>>, <<materialsettexture,materialSetTexture>>
.Exemple
[source,lua]
----
-- Un champ de force qui apparaît en fondu quand il est touché.
field = materialNew()
materialSetColor(field, 60, 200, 255, 0)
materialSetBlend(field, true)
materialSetDoubleSided(field, true)
nodeSetMesh(shield, meshSphere(2, 32), field)
function onShieldHit()
materialSetColor(field, 60, 200, 255, 160)
end
----
[#materialsetcolor]
==== materialSetColor
[source,text]
----
materialSetColor(material, r, g, b)
materialSetColor(material, r, g, b, a)
----
Règle la couleur de base, de `0` à `255` par canal (les valeurs hors plage sont ramenées dans la plage), `a` valant `255` par défaut. La couleur est traitée comme du sRGB et éclairée en lumière linéaire. Quand le matériau a une texture, une vidéo ou une vue, la couleur la multiplie : le blanc laisse donc l'image telle quelle et une couleur plus sombre la teinte. L'alpha ne se voit qu'avec `materialSetBlend`.
*Paramètres :*
* `material` -- le handle du matériau.
* `r, g, b` -- rouge, vert et bleu, de `0` à `255`.
* `a` -- opacité, de `0` à `255` ; `255` s'il est omis.
*Depuis :* 3.00.
*Voir aussi :* <<materialsettexture,materialSetTexture>>, <<materialsetblend,materialSetBlend>>, <<materialsetemissive,materialSetEmissive>>
.Exemple
[source,lua]
----
-- Les couleurs des équipes sur un seul et même maillage de voiture.
teamLooks = {}
for team, rgb in ipairs({ { 220, 40, 40 }, { 40, 90, 220 }, { 240, 200, 30 } }) do
local look = materialNew()
materialSetColor(look, rgb[1], rgb[2], rgb[3])
materialSetMetallic(look, 0.6)
materialSetRoughness(look, 0.3)
teamLooks[team] = look
end
nodeSetMesh(playerCar, carMesh, teamLooks[1])
----
[#materialsetcutoff]
==== materialSetCutoff
[source,text]
----
materialSetCutoff(material, cutoff)
----
Jette chaque texel dont l'alpha est inférieur à `cutoff` et dessine le reste entièrement opaque, dans la passe éclairée comme dans la passe d'ombre, si bien qu'une carte de feuillage projette l'ombre de ses feuilles plutôt que celle de son quad. C'est le masquage alpha de glTF : contrairement à <<materialsetblend,`materialSetBlend`>>, il n'a besoin d'aucun tri, mais un texel est gardé entier ou jeté, jamais mélangé. Le seuil est borné entre `0` et `1`, et `0`, la valeur par défaut, désactive le masquage. Un modèle chargé depuis glTF arrive avec le seuil que son fichier donne à un matériau `MASK`.
*Paramètres :*
* `material` -- handle de matériau entier.
* `cutoff` -- nombre ; l'alpha, de `0` à `1`, qu'il faut à un texel pour être dessiné.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetblend,materialSetBlend>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsettexture,materialSetTexture>>
.Exemple
[source,lua]
----
-- Un grillage : un seul quad texturé, les trous découpés, les deux faces dessinées.
fence = materialNew()
materialSetTexture(fence, spriteLoad(DIR .. "Art/chainLink.png"))
materialSetCutoff(fence, 0.5)
materialSetDoubleSided(fence, true)
panel = nodeNew()
nodeSetMesh(panel, meshPlane(4, 2), fence)
nodeSetPosition(panel, 0, 1, -5)
nodeSetRotation(panel, 90, 0, 0)
----
[#materialsetdoublesided]
==== materialSetDoubleSided
[source,text]
----
materialSetDoubleSided(material, doubleSided)
----
Dessine les deux faces de chaque triangle au lieu d'éliminer la face arrière. Nécessaire pour les formes ouvertes vues de dos, comme un simple `meshPlane` vu de dessous, un drapeau ou une carte de feuillage. Les formes fermées ont le même aspect dans les deux cas : laissez-le donc à `false` (la valeur par défaut) pour elles, car un matériau double face coûte du taux de remplissage et les ombres sont alors projetées par les deux faces aussi.
*Depuis :* 3.00.
*Voir aussi :* <<meshplane,meshPlane>>, <<materialsetblend,materialSetBlend>>
.Exemple
[source,lua]
----
-- Une banderole suspendue au-dessus de la rue, lisible des deux côtés.
cloth = materialNew()
materialSetTexture(cloth, spriteLoad(DIR .. "Art/banner.png"))
materialSetDoubleSided(cloth, true)
materialSetRoughness(cloth, 0.9)
banner = nodeNew()
nodeSetMesh(banner, meshPlane(3, 1), cloth)
nodeSetPosition(banner, 0, 4, -6)
nodeSetRotation(banner, 90, 0, 0)
----
[#materialsetemissive]
==== materialSetEmissive
[source,text]
----
materialSetEmissive(material, r, g, b)
----
La lumière que la surface émet d'elle-même, de `0` à `255` par canal (les valeurs hors plage sont ramenées dans la plage), ajoutée par-dessus l'éclairage : écrans, ampoules, néons, tableaux de bord. La valeur par défaut est le noir, aucune lueur. Elle n'éclaire rien d'autre et ne projette aucune lumière ; placez un nœud de lumière au même endroit pour cela. Avec `materialSetEmissiveMap`, la carte indique à quels endroits de la surface cette couleur s'applique. Les surfaces émissives assez lumineuses pour franchir le seuil de `sceneSetBloom` rayonnent.
*Remarques :* Un matériau non éclairé (`materialSetUnlit`) ne montre que sa couleur de base ; la couleur émissive y est ignorée.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetemissivemap,materialSetEmissiveMap>>, <<materialsetunlit,materialSetUnlit>>, <<scenesetbloom,sceneSetBloom>>
.Exemple
[source,lua]
----
-- Un voyant d'alerte qui pulse en rouge quand le réacteur surchauffe.
lampLook = materialNew()
materialSetColor(lampLook, 60, 10, 10)
nodeSetMesh(lamp, meshSphere(0.15, 16), lampLook)
function onOverlayUpdate()
local pulse = (math.sin(singeGetTicks() / 120) + 1) / 2
if overheating then
materialSetEmissive(lampLook, math.floor(255 * pulse), 20, 20)
else
materialSetEmissive(lampLook, 0, 0, 0)
end
return OVERLAY_UPDATED
end
----
[#materialsetemissivemap]
==== materialSetEmissiveMap
[source,text]
----
materialSetEmissiveMap(material, sprite)
materialSetEmissiveMap(material, "file.ktx2")
materialSetEmissiveMap(material, nil)
materialSetEmissiveMap(material)
----
Une texture qui indique quelles parties de la surface luisent. Sa couleur est multipliée par la couleur de `materialSetEmissive` : réglez donc celle-ci aussi, en blanc pour utiliser les couleurs propres de la carte, ou en une teinte. L'image vient d'un sprite chargé (une copie est envoyée au GPU, si bien que le sprite peut être déchargé ensuite) ou d'un fichier `.ktx2` désigné par son nom, gardé compressé sur le GPU. Avec `nil` ou sans second argument, la carte est retirée. Met fin au script quand la texture ne peut pas être envoyée au GPU ou que le fichier n'est pas une texture KTX2.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetemissive,materialSetEmissive>>, <<materialsettexture,materialSetTexture>>, <<materialsettiling,materialSetTiling>>
.Exemple
[source,lua]
----
-- Un pupitre de commande dont les boutons et les afficheurs s'allument.
panel = materialNew()
materialSetTexture(panel, spriteLoad(DIR .. "Art/panel.png"))
materialSetEmissiveMap(panel, spriteLoad(DIR .. "Art/panelLights.png"))
materialSetEmissive(panel, 255, 255, 255)
materialSetRoughness(panel, 0.6)
nodeSetMesh(nodeFind("Panel", cockpit), meshPlane(1.2, 0.4), panel)
function onPowerDown()
materialSetEmissive(panel, 0, 0, 0)
end
----
[#materialsetfilter]
==== materialSetFilter
[source,text]
----
materialSetFilter(material, filter)
----
La façon dont les textures du matériau sont échantillonnées. `FILTER_LINEAR` (la valeur par défaut) mélange les texels entre eux et les niveaux de mipmap entre eux, avec un filtrage anisotrope sur les surfaces vues sous un angle rasant. `FILTER_NEAREST` prend le texel le plus proche et le niveau de mipmap le plus proche, si bien que le pixel art reste net sur un quad. Chaque texture porte une chaîne de mipmaps complète, donc une texture vue en petit ne scintille jamais, dans un cas comme dans l'autre. Toute autre valeur met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<materialsettexture,materialSetTexture>>, <<materialsetunlit,materialSetUnlit>>
.Exemple
[source,lua]
----
-- Une tuile de 16 pixels sur 16 dessinée sur un bloc sans bavure.
pixelLook = materialNew()
materialSetTexture(pixelLook, spriteLoad(DIR .. "Art/dirt16.png"))
materialSetFilter(pixelLook, FILTER_NEAREST)
materialSetRoughness(pixelLook, 1)
block = nodeNew()
nodeSetMesh(block, meshBox(1, 1, 1), pixelLook)
----
[#materialsetgui]
==== materialSetGui
[source,text]
----
materialSetGui(material, gui)
materialSetGui(material, nil)
materialSetGui(material)
----
Affiche une interface créée par `guiNew` comme texture de couleur de base du matériau, mise à jour à chaque image : un moniteur, un tableau de bord de cockpit, une enseigne, avec le même document qu'un `guiDraw` afficherait à plat. Généralement associé à `materialSetUnlit` pour que la page ne soit pas ombrée par les lumières, et teinté par la couleur de base comme toute texture. Avec `nil` ou sans second argument, le matériau revient à sa propre texture, conservée en dessous ; supprimer l'interface fait de même. Régler une interface abandonne toute vidéo ou vue que le matériau affichait. Tant que l'interface prend les entrées, la souris ou un pistolet optique pointé sur une surface dessinée avec le matériau l'atteint par le même sondage que `sceneProbeGui`. Met fin au script pour un handle d'interface qui n'est pas valide.
*Depuis :* 3.00.
*Voir aussi :* <<guinew,guiNew>>, <<guidraw,guiDraw>>, <<sceneprobegui,sceneProbeGui>>, <<materialsetview,materialSetView>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
.Exemple
[source,lua]
----
-- La page des options sur l'écran de la borne modélisée.
options = guiNew(720, 480)
page = guiLoad(options, DIR .. "gui/options.rml")
screen = materialNew()
materialSetGui(screen, options)
materialSetUnlit(screen, true)
nodeSetMesh(nodeFind("Screen", cabinet), meshPlane(3.0, 2.0), screen)
----
[#materialsetmetallic]
==== materialSetMetallic
[source,text]
----
materialSetMetallic(material, metallic)
----
À quel point la surface est métallique, de `0` pour les diélectriques (peinture, plastique, bois, peau) à `1` pour les métaux, qui teintent leurs reflets et leurs réflexions de la couleur de base. La valeur par défaut est `0`. Les valeurs intermédiaires servent aux mélanges et aux surfaces usées ; la plupart des matériaux veulent l'une ou l'autre extrémité. Avec `materialSetMetallicRoughnessMap`, le canal bleu de la carte multiplie cette valeur. Un métal a peu de choses à refléter sans un ciel venu de `sceneSetSky` : associez donc les deux.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetroughness,materialSetRoughness>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>, <<scenesetsky,sceneSetSky>>
.Exemple
[source,lua]
----
-- Pare-chocs chromé, carrosserie peinte.
chrome = materialNew()
materialSetColor(chrome, 240, 240, 245)
materialSetMetallic(chrome, 1)
materialSetRoughness(chrome, 0.15)
paint = materialNew()
materialSetColor(paint, 30, 80, 200)
materialSetMetallic(paint, 0)
materialSetRoughness(paint, 0.4)
nodeSetMaterial(nodeFind("Bumper", car), chrome)
nodeSetMaterial(nodeFind("Body", car), paint)
----
[#materialsetmetallicroughnessmap]
==== materialSetMetallicRoughnessMap
[source,text]
----
materialSetMetallicRoughnessMap(material, sprite)
materialSetMetallicRoughnessMap(material, "file.ktx2")
materialSetMetallicRoughnessMap(material, nil)
materialSetMetallicRoughnessMap(material)
----
Une texture qui fait varier le fini sur la surface : son canal vert multiplie la rugosité de `materialSetRoughness` et son canal bleu la métallicité de `materialSetMetallic`, comme glTF les range, si bien qu'une carte produite par un outil glTF fonctionne telle quelle. Réglez les deux facteurs sur `1` pour n'utiliser que les valeurs de la carte. L'image vient d'un sprite ou d'un fichier `.ktx2` désigné par son nom ; `nil` ou l'absence de second argument retire la carte. Met fin au script quand la texture ne peut pas être envoyée au GPU.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetmetallic,materialSetMetallic>>, <<materialsetroughness,materialSetRoughness>>, <<materialsettexture,materialSetTexture>>
.Exemple
[source,lua]
----
-- Un blindage métallique rayé : brillant là où la peinture est partie.
plating = materialNew()
materialSetTexture(plating, DIR .. "Textures/plating.ktx2")
materialSetMetallicRoughnessMap(plating, DIR .. "Textures/platingMetalRough.ktx2")
materialSetMetallic(plating, 1)
materialSetRoughness(plating, 1)
materialSetNormalMap(plating, DIR .. "Textures/platingNormal.ktx2")
nodeSetMesh(hullPanel, meshPlane(4, 2), plating)
----
[#materialsetnormalmap]
==== materialSetNormalMap
[source,text]
----
materialSetNormalMap(material, sprite)
materialSetNormalMap(material, sprite, strength)
materialSetNormalMap(material, "file.ktx2")
materialSetNormalMap(material, "file.ktx2", strength)
materialSetNormalMap(material, nil)
materialSetNormalMap(material)
----
Une carte de normales en espace tangent (le genre bleuté habituel, plat à `128, 128, 255`) qui incline l'éclairage texel par texel, si bien que briques, rainures et rivets accrochent la lumière sans géométrie supplémentaire. Chaque maillage a des tangentes pour cela : celles du modèle quand le fichier en porte, sinon calculées à partir des UV. L'image vient d'un sprite ou d'un fichier `.ktx2` désigné par son nom ; `nil` ou l'absence de second argument retire la carte. Met fin au script quand la texture ne peut pas être envoyée au GPU.
*Paramètres :*
* `material` -- le handle du matériau.
* `sprite` ou `"file.ktx2"` -- la carte, tirée d'un sprite chargé ou d'un fichier KTX2 ; `nil` ou rien la retire.
* `strength` -- à quel point les reliefs inclinent l'éclairage : `1` (la valeur par défaut) tel que créé, `0` à plat, au-dessus de `1` exagéré.
*Depuis :* 3.00.
*Voir aussi :* <<materialsettexture,materialSetTexture>>, <<materialsetocclusionmap,materialSetOcclusionMap>>, <<materialsettiling,materialSetTiling>>
.Exemple
[source,lua]
----
-- Un mur de briques fait d'un plan plat, ses joints de mortier accrochant le soleil rasant.
brick = materialNew()
materialSetTexture(brick, spriteLoad(DIR .. "Art/brick.png"))
materialSetNormalMap(brick, spriteLoad(DIR .. "Art/brickNormal.png"), 1.5)
materialSetRoughness(brick, 0.9)
materialSetTiling(brick, 6, 2)
wall = nodeNew()
nodeSetMesh(wall, meshPlane(12, 4), brick)
nodeSetPosition(wall, 0, 2, -3)
nodeSetRotation(wall, 90, 0, 0)
----
[#materialsetoccluder]
==== materialSetOccluder
[source,text]
----
materialSetOccluder(material, occluder)
----
Fait écrire au matériau la profondeur et aucune couleur. Tout ce que le maillage
couvre est caché comme si le maillage était plein, mais le maillage lui-même ne
se voit pas : ce qui a été dessiné derrière lui -- un décor peint, le disque --
transparaît. C'est la doublure de quelque chose qui est dans une image mais pas
dans la scène : un pilier dans une pièce peinte derrière lequel un personnage 3D
doit passer, une embrasure de porte, une table. Un occulteur ne projette aucune
ombre, puisque la peinture en a déjà une, et ne reçoit aucune lumière. `false`
(la valeur par défaut) rend le matériau de nouveau ordinaire.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetblend,materialSetBlend>>, <<materialsetunlit,materialSetUnlit>>
.Exemple
[source,lua]
----
-- Un pilier qui est dans la peinture : une boîte invisible là où il se dresse, pour
-- que le héros disparaisse derrière et réapparaisse de l'autre côté.
hidden = materialNew()
materialSetOccluder(hidden, true)
pillar = nodeNew()
nodeSetMesh(pillar, meshBox(0.8, 4, 0.8), hidden)
nodeSetPosition(pillar, 2, 2, -3)
----
[#materialsetocclusionmap]
==== materialSetOcclusionMap
[source,text]
----
materialSetOcclusionMap(material, sprite)
materialSetOcclusionMap(material, sprite, strength)
materialSetOcclusionMap(material, "file.ktx2")
materialSetOcclusionMap(material, "file.ktx2", strength)
materialSetOcclusionMap(material, nil)
materialSetOcclusionMap(material)
----
Une carte d'occlusion ambiante, l'occlusion dans le canal rouge (blanc pour le dégagé, sombre pour les recoins), qui assombrit la lumière ambiante et celle du ciel dans les coins et les replis. La lumière directe des nœuds de lumière n'est pas affectée, comme dans glTF. L'image vient d'un sprite ou d'un fichier `.ktx2` désigné par son nom ; `nil` ou l'absence de second argument retire la carte. Met fin au script quand la texture ne peut pas être envoyée au GPU.
*Paramètres :*
* `material` -- le handle du matériau.
* `sprite` ou `"file.ktx2"` -- la carte, tirée d'un sprite chargé ou d'un fichier KTX2 ; `nil` ou rien la retire.
* `strength` -- quelle part de la carte appliquer, de `0` (rien) à `1` (la valeur par défaut, en entier).
*Depuis :* 3.00.
*Voir aussi :* <<materialsetnormalmap,materialSetNormalMap>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetenvironment,sceneSetEnvironment>>
.Exemple
[source,lua]
----
-- Ombrage précalculé pour les parois de la grotte, à moitié pour que les lampes atteignent encore les coins.
rock = materialNew()
materialSetTexture(rock, DIR .. "Textures/caveRock.ktx2")
materialSetNormalMap(rock, DIR .. "Textures/caveRockNormal.ktx2")
materialSetOcclusionMap(rock, DIR .. "Textures/caveRockAO.ktx2", 0.5)
materialSetRoughness(rock, 1)
nodeSetMaterial(nodeFind("Walls", cave), rock)
----
[#materialsetroughness]
==== materialSetRoughness
[source,text]
----
materialSetRoughness(material, roughness)
----
À quel point la surface est rugueuse, de `0` pour un reflet serré comme un miroir à `1` pour une surface mate. La valeur par défaut est `0.5`. Les reflets suivent le modèle GGX : les valeurs basses donnent un petit point brillant et des réflexions du ciel nettes, les valeurs hautes un lustre large et doux et des réflexions floues. Avec `materialSetMetallicRoughnessMap`, le canal vert de la carte multiplie cette valeur.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetmetallic,materialSetMetallic>>, <<materialsetmetallicroughnessmap,materialSetMetallicRoughnessMap>>
.Exemple
[source,lua]
----
-- Mouiller la route quand il pleut : le même asphalte, des reflets plus serrés.
asphalt = materialNew()
materialSetColor(asphalt, 50, 50, 55)
materialSetRoughness(asphalt, 0.95)
nodeSetMesh(road, meshPlane(8, 200), asphalt)
function onWeatherChange(raining)
if raining then
materialSetRoughness(asphalt, 0.25)
else
materialSetRoughness(asphalt, 0.95)
end
end
----
[#materialsettexture]
==== materialSetTexture
[source,text]
----
materialSetTexture(material, sprite)
materialSetTexture(material, "file.ktx2")
materialSetTexture(material, nil)
materialSetTexture(material)
----
Utilise l'image d'un sprite comme texture de couleur de base (une copie est envoyée au GPU, si bien que le sprite peut être déchargé ensuite), ou un fichier KTX2 désigné par son nom, gardé compressé sur le GPU dans le format par blocs dont dispose la machine (voir Textures compressées dans <<scenes3d,Scènes 3D>>). Avec `nil` ou sans second argument, la texture est retirée et le matériau ne montre que sa couleur de base. Régler une texture abandonne aussi toute vidéo ou vue que le matériau affichait. Les coordonnées de texture viennent du maillage ; les primitives plaquent une image une fois sur chaque face, et `materialSetTiling` la répète. Un nom qui n'est pas un fichier KTX2 met fin au script (chargez les autres images avec `spriteLoad`), tout comme un envoi au GPU qui échoue.
*Depuis :* 3.00.
*Voir aussi :* <<spriteload,spriteLoad>>, <<materialsettiling,materialSetTiling>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
.Exemple
[source,lua]
----
-- Une caisse texturée avec un sprite que le jeu charge déjà pour le menu,
-- et un sol tiré d'une texture compressée livrée avec le jeu.
crateArt = spriteLoad(DIR .. "Art/crate.png")
crateLook = materialNew()
materialSetTexture(crateLook, crateArt)
crate = nodeNew()
nodeSetMesh(crate, meshBox(1, 1, 1), crateLook)
floorLook = materialNew()
materialSetTexture(floorLook, DIR .. "Textures/tiles.ktx2")
materialSetTiling(floorLook, 10, 10)
nodeSetMesh(floor, meshPlane(20, 20), floorLook)
----
[#materialsettiling]
==== materialSetTiling
[source,text]
----
materialSetTiling(material, u, v)
----
Combien de fois les textures du matériau (la texture de base comme les cartes) se répètent sur la plage UV de `0` à `1` d'une surface, `1` et `1` par défaut. `24, 24` sur une carte de hauteurs de 120 unités de large répète une texture toutes les cinq unités. Les textures se répètent au-delà de leurs bords : l'image doit donc se raccorder proprement. Les valeurs inférieures à `1` montrent une partie de l'image ; les valeurs négatives la retournent en miroir.
*Paramètres :*
* `material` -- le handle du matériau.
* `u` -- répétitions dans la direction U de la surface.
* `v` -- répétitions dans la direction V de la surface.
*Depuis :* 3.00.
*Voir aussi :* <<materialsettexture,materialSetTexture>>, <<meshheightmap,meshHeightmap>>
.Exemple
[source,lua]
----
-- De l'herbe toutes les cinq unités sur les collines.
ground = materialNew()
materialSetTexture(ground, DIR .. "Textures/grass.ktx2")
materialSetTiling(ground, 24, 24)
materialSetRoughness(ground, 0.95)
hills = nodeNew()
nodeSetMesh(hills, meshHeightmap(DIR .. "Art/hills.png", 120, 14, 120), ground)
bodyNew(hills, BODY_STATIC, SHAPE_MESH)
----
[#materialsetunlit]
==== materialSetUnlit
[source,text]
----
materialSetUnlit(material, unlit)
----
Montre la couleur de base et la texture exactement telles qu'elles sont, en ignorant toutes les lumières, la lumière ambiante et le ciel : pour les écrans vidéo, les moniteurs qui montrent une vue, les skybox et les rendus en aplat. Le brouillard s'applique toujours. La couleur émissive et les cartes ne servent pas sur un matériau non éclairé. La valeur par défaut est `false`.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetemissive,materialSetEmissive>>, <<materialsetvideo,materialSetVideo>>, <<materialsetview,materialSetView>>
.Exemple
[source,lua]
----
-- Le fronton de la borne d'arcade : une image éclairée de l'intérieur, que les
-- lumières de la pièce ne doivent donc pas ombrer.
marquee = materialNew()
materialSetTexture(marquee, spriteLoad(DIR .. "Art/marquee.png"))
materialSetUnlit(marquee, true)
nodeSetMesh(nodeFind("Marquee", cabinet), meshPlane(0.7, 0.2), marquee)
----
[#materialsetview]
==== materialSetView
[source,text]
----
materialSetView(material, view)
materialSetView(material, nil)
materialSetView(material)
----
Affiche une vue rendue par `viewNew` comme texture de couleur de base du matériau, mise à jour à chaque image : un moniteur de surveillance, un rétroviseur, un portail. Généralement associé à `materialSetUnlit` pour que l'image ne soit pas ombrée par les lumières, et teinté par la couleur de base comme toute texture. Avec `nil` ou sans second argument, le matériau revient à sa propre texture, conservée en dessous. Régler une vue abandonne toute vidéo que le matériau affichait. Met fin au script pour un handle de vue qui n'est pas valide.
*Depuis :* 3.00.
*Voir aussi :* <<viewnew,viewNew>>, <<viewsetcamera,viewSetCamera>>, <<materialsetvideo,materialSetVideo>>, <<materialsetunlit,materialSetUnlit>>
.Exemple
[source,lua]
----
-- Un rétroviseur sur la voiture du joueur.
rearCamera = nodeNew(car)
nodeSetPosition(rearCamera, 0, 1.2, -0.5)
nodeSetRotation(rearCamera, 0, 180, 0)
rearView = viewNew(384, 256)
viewSetCamera(rearView, rearCamera)
mirror = materialNew()
materialSetView(mirror, rearView)
materialSetUnlit(mirror, true)
nodeSetMesh(nodeFind("Mirror", car), meshPlane(0.3, 0.1), mirror)
----
[#materialsetvideo]
==== materialSetVideo
[source,text]
----
materialSetVideo(material)
materialSetVideo(material, video)
----
Utilise le laserdisc (sans `video`) ou une vidéo chargée avec `videoLoad` comme texture de couleur de base, mise à jour à chaque image pendant la lecture. Généralement associé à `materialSetUnlit` pour que l'image ne soit pas teintée par les lumières. La vidéo continue de jouer, qu'elle soit aussi dessinée sur l'overlay ou non, et plusieurs matériaux peuvent montrer la même. Régler une vidéo abandonne toute texture ou vue qu'avait le matériau ; `materialSetTexture` la retire de nouveau.
*Remarques :* La forme disque met fin au script dans un jeu sans disque, tout comme un mauvais handle de vidéo.
*Depuis :* 3.00.
*Voir aussi :* <<videoload,videoLoad>>, <<materialsetunlit,materialSetUnlit>>, <<materialsettexture,materialSetTexture>>
.Exemple
[source,lua]
----
-- Une borne modélisée dont l'écran montre le laserdisc, et un mur de
-- moniteurs qui passent en boucle un extrait du mode attraction.
cabinet = modelInstance(modelLoad(DIR .. "Models/Cabinet.glb"))
screen = materialNew()
materialSetVideo(screen)
materialSetUnlit(screen, true)
nodeSetMesh(nodeFind("Screen", cabinet), meshPlane(0.6, 0.45), screen)
attract = videoLoad(DIR .. "Video/attract.mp4")
videoPlay(attract)
monitors = materialNew()
materialSetVideo(monitors, attract)
materialSetUnlit(monitors, true)
----
[#mesh]
=== Maillage
Un maillage, c'est de la géométrie sur le GPU : une primitive que construit le
moteur, un terrain tiré d'une image, ou des données de sommets venues de votre
script. Les handles sont des entiers renvoyés par les constructeurs `mesh*` ;
n'importe quel nombre de nœuds peut partager un même maillage, et les nœuds qui
dessinent le même maillage avec le même matériau se dessinent en un seul appel
instancié : créez donc un maillage et beaucoup de nœuds. Les tailles sont en
unités de la scène et chaque primitive est centrée sur l'origine de son nœud
sauf indication contraire, si bien que la transformation du nœud la place, la
tourne et la met à l'échelle. Les coordonnées de texture vont de `0` à `1` une
fois sur chaque face ou surface. Chaque constructeur a besoin du moteur de rendu
3D décrit dans <<scenes3d,Scènes 3D>> et met fin au script quand il n'est pas
disponible.
[#meshbox]
==== meshBox
[source,text]
----
mesh = meshBox(width, height, depth)
----
Une boîte de `width` selon X, `height` selon Y et `depth` selon Z, centrée sur
l'origine. Chacune des six faces a ses quatre sommets à elle avec une normale
plate, si bien que les arêtes sont ombrées nettement, et la texture est posée
une fois sur chaque face. Caisses, murs, sols et carrosseries de voiture sont
tous des boîtes.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetmesh,nodeSetMesh>>, <<meshplane,meshPlane>>, <<bodynew,bodyNew>>
.Exemple
[source,lua]
----
-- Une dalle de sol en pierre et une pile de caisses qui partagent un même maillage.
local floor = nodeNew()
nodeSetMesh(floor, meshBox(16, 0.2, 10), stone)
nodeSetPosition(floor, 0, -0.1, 0)
local crateMesh = meshBox(0.6, 0.6, 0.6)
for i = 1, 4 do
local crate = nodeNew()
nodeSetMesh(crate, crateMesh, wood)
nodeSetPosition(crate, 0, i * 0.6 - 0.3, -2)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
end
----
[#meshcone]
==== meshCone
[source,text]
----
mesh = meshCone(radius, height)
mesh = meshCone(radius, height, segments)
----
Un cône dressé sur Y avec une base fermée : le cercle de base de rayon `radius`
à `-height / 2` et la pointe à `+height / 2`. `segments` (`24` par défaut, au
moins `3`) est le nombre de facettes qui en font le tour ; les côtés sont ombrés
de façon lisse d'une facette à l'autre. La texture s'enroule une fois autour des
côtés et est posée une fois sur la base.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<meshcylinder,meshCylinder>>, <<nodesetmesh,nodeSetMesh>>
.Exemple
[source,lua]
----
-- Un abat-jour au-dessus de l'ampoule, et une rangée de pins faits chacun d'un cône.
local shade = nodeNew(stand)
nodeSetMesh(shade, meshCone(0.45, 0.5, 24), brass)
nodeSetPosition(shade, 0, 0.9, 0)
local foliage = meshCone(0.8, 2.0, 12)
for i = 1, 5 do
local tree = nodeNew()
nodeSetMesh(tree, foliage, pine)
nodeSetPosition(tree, i * 3 - 9, 1.0, -6)
end
----
[#meshcylinder]
==== meshCylinder
[source,text]
----
mesh = meshCylinder(radius, height)
mesh = meshCylinder(radius, height, segments)
----
Un cylindre selon Y, fermé aux deux bouts, de `-height / 2` à `+height / 2`.
`segments` (`24` par défaut, au moins `3`) est le nombre de facettes qui en font
le tour, ombrées de façon lisse. Pour une roue ou une pièce de monnaie, tournez
le nœud de `90` degrés autour de X ou de Z pour que l'axe soit là où il vous le
faut.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<meshcone,meshCone>>, <<meshtorus,meshTorus>>, <<nodesetrotation,nodeSetRotation>>
.Exemple
[source,lua]
----
-- Un lampadaire, et un pneu couché sur le flanc pour faire une roue.
local post = nodeNew()
nodeSetMesh(post, meshCylinder(0.08, 3, 10), dark)
nodeSetPosition(post, 3, 1.5, -2)
local tire = nodeNew(wheel)
nodeSetMesh(tire, meshCylinder(0.35, 0.25, 16), rubber)
nodeSetRotation(tire, 0, 0, 90)
----
[#meshdelete]
==== meshDelete
[source,text]
----
meshDelete(mesh)
----
Libère les tampons GPU du maillage et sa copie côté CPU. Les nœuds qui
l'utilisaient se retrouvent sans maillage (et sans poids de morphing) et ne
dessinent rien tant qu'on ne leur en donne pas un autre ; le handle devient
invalide et peut être réutilisé par un constructeur ultérieur. Les maillages ne
sont pas libérés quand un nœud est supprimé : supprimez donc vous-même un
maillage quand plus rien ne s'en servira.
*Depuis :* 3.00.
*Voir aussi :* <<meshnew,meshNew>>, <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
.Exemple
[source,lua]
----
-- Reconstruire la piste à partir d'une nouvelle carte de hauteurs entre deux manches.
function loadTrack(image)
if trackMesh then
meshDelete(trackMesh)
end
trackMesh = meshHeightmap(image, 200, 20, 200)
nodeSetMesh(track, trackMesh, tarmac)
bodyNew(track, BODY_STATIC, SHAPE_MESH)
end
----
[#meshheightmap]
==== meshHeightmap
[source,text]
----
mesh = meshHeightmap(image, sizeX, sizeY, sizeZ)
----
Un maillage de terrain tiré d'une image en niveaux de gris, de `2` à `1025`
pixels dans chaque sens : un sommet par pixel, répartis sur `sizeX` par `sizeZ`
unités centrées sur le nœud, chacun élevé de la valeur du canal rouge du pixel
(de `0` pour le noir à `255` pour le blanc) multipliée par `sizeY`, la ligne du
haut de l'image étant au bord lointain (`-Z`). Les normales sont calculées à
partir des pentes et les coordonnées de texture vont de `0` à `1` sur
l'ensemble, si bien que `materialSetTiling` règle la répétition de la texture.
Les échantillons de hauteur sont conservés : `terrainGetHeight` les lit, et un
corps `SHAPE_MESH` sur le nœud devient un champ de hauteurs. Un fichier qui ne
peut pas être ouvert ou décodé, une mauvaise taille ou l'absence de moteur de
rendu 3D met fin au script.
*Paramètres :*
* `image` -- le nom de l'image relatif au jeu, dans n'importe quel format que lit `spriteLoad`.
* `sizeX` -- la largeur du terrain selon X, en unités du monde.
* `sizeY` -- la hauteur d'un pixel blanc au-dessus d'un pixel noir.
* `sizeZ` -- la profondeur du terrain selon Z, en unités du monde.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<terraingetheight,terrainGetHeight>>, <<materialsettiling,materialSetTiling>>, <<bodynew,bodyNew>>
.Exemple
[source,lua]
----
local ground = materialNew()
materialSetTexture(ground, spriteLoad("grass.png"))
materialSetTiling(ground, 24, 24)
materialSetRoughness(ground, 0.95)
local hills = nodeNew()
nodeSetMesh(hills, meshHeightmap("hills.png", 120, 14, 120), ground)
bodyNew(hills, BODY_STATIC, SHAPE_MESH)
-- Les pieds du dragon sur la pente, où que se trouve le sol à cet endroit.
nodeSetPosition(dragon, -8, terrainGetHeight(hills, -8, 3) or 0, 3)
----
[#meshnew]
==== meshNew
[source,text]
----
mesh = meshNew(positions, normals, uvs, indices)
----
Construit une géométrie à partir de tables de nombres. Les tangentes que
demandent les cartes de normales sont calculées pour vous, de même que des
normales lisses quand `normals` vaut `nil`, en faisant la moyenne des triangles
qui partagent chaque sommet. Les triangles sont de face quand leurs sommets
tournent dans le sens inverse des aiguilles d'une montre, vus de l'extérieur ;
donnez au matériau `materialSetDoubleSided` si la face arrière doit se voir.
Une table de mauvaise forme, ou un indice hors de la liste des sommets,
met fin au script avec un message qui dit lequel des deux.
*Paramètres :*
* `positions` -- `x, y, z` pour chaque sommet, les uns après les autres ; au moins un sommet.
* `normals` -- `x, y, z` par sommet, autant que dans `positions`, ou `nil` pour les faire calculer.
* `uvs` -- `u, v` par sommet (de `0` à `1`, `v` vers le bas de l'image), ou `nil` pour aucune (chaque sommet à `0, 0`).
* `indices` -- trois numéros de sommet par triangle, comptés à partir de `1` dans l'ordre où les sommets apparaissent dans `positions` ; au moins un triangle.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetmesh,nodeSetMesh>>, <<materialsetdoublesided,materialSetDoubleSided>>, <<meshdelete,meshDelete>>
.Exemple
[source,lua]
----
-- Une flèche plate pointant selon +X, vue de dessus (double face pour qu'elle marche aussi vue de dessous).
local arrow = meshNew(
{ 0, 0, -0.5, 1, 0, 0, 0, 0, 0.5, -0.6, 0, -0.5, -0.6, 0, 0.5 },
nil,
nil,
{ 1, 2, 3, 4, 1, 3, 4, 3, 5 })
local marker = materialNew()
materialSetColor(marker, 255, 200, 40)
materialSetDoubleSided(marker, true)
local pointer = nodeNew()
nodeSetMesh(pointer, arrow, marker)
nodeSetPosition(pointer, 0, 0.05, 0)
----
[#meshplane]
==== meshPlane
[source,text]
----
mesh = meshPlane(width, depth)
mesh = meshPlane(width, depth, columns)
mesh = meshPlane(width, depth, columns, rows)
----
Un rectangle plat dans le plan XZ tourné vers +Y, de `width` selon X et `depth`
selon Z, centré sur l'origine, avec la texture posée une fois dessus. Deux
arguments donnent un seul quad. `columns` (et `rows`, qui vaut `columns` par
défaut) le subdivisent en autant de quads dans chaque sens, ce dont un corps mou
(`softNew`) a besoin pour se plier comme du tissu ; les deux doivent valoir au
moins `1`. Tournez le nœud de `90` degrés autour de X pour le dresser face à +Z.
Il n'a qu'une face, sauf si son matériau est double face.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetdoublesided,materialSetDoubleSided>>, <<materialsetvideo,materialSetVideo>>, <<softnew,softNew>>
.Exemple
[source,lua]
----
-- Le sol, un écran qui montre le disque, et un drapeau qui deviendra du tissu.
local plain = nodeNew()
nodeSetMesh(plain, meshPlane(60, 60), ground)
local tv = nodeNew()
nodeSetMesh(tv, meshPlane(3.0, 2.2), screen)
nodeSetPosition(tv, 0, 1.6, -4)
nodeSetRotation(tv, 90, 0, 0)
local flag = nodeNew()
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), cloth)
nodeSetPosition(flag, 0.8, 2.5, 0)
nodeSetRotation(flag, 90, 0, 0)
----
[#meshsphere]
==== meshSphere
[source,text]
----
mesh = meshSphere(radius)
mesh = meshSphere(radius, segments)
----
Une sphère de rayon `radius`, centrée sur l'origine, construite à partir de
bandes de latitude et de longitude : `segments` (`32` par défaut, au moins `3`)
autour de l'équateur et moitié moins d'un pôle à l'autre. La texture s'enroule
une fois autour, `v` allant du pôle du haut à celui du bas. Moins de segments
donnent un maillage plus léger pour des balles vues de loin ; davantage donnent
une silhouette plus lisse de près.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<meshtorus,meshTorus>>, <<nodesetmesh,nodeSetMesh>>, <<bodynew,bodyNew>>
.Exemple
[source,lua]
----
-- Une boule de verre sur le sol, et une petite sphère de repère qui partage un aspect.
local ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.7, 48), glass)
nodeSetPosition(ball, -1.5, 0.7, 0)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.7)
local dot = nodeNew()
nodeSetMesh(dot, meshSphere(0.08, 16), marker)
nodeSetPosition(dot, 0, 1.2, 0)
----
[#meshtorus]
==== meshTorus
[source,text]
----
mesh = meshTorus(radius, tubeRadius)
mesh = meshTorus(radius, tubeRadius, segments)
----
Un anneau couché dans le plan XZ autour de Y, centré sur l'origine : `radius`
du centre au milieu du tube et `tubeRadius` le rayon du tube lui-même.
`segments` (`32` par défaut, au moins `3`) font le tour de l'anneau et moitié
moins (au moins `3`) le tour du tube. La texture s'enroule une fois autour de
l'anneau en `u` et une fois autour du tube en `v`.
*Renvoie :* Le handle du maillage.
*Depuis :* 3.00.
*Voir aussi :* <<meshsphere,meshSphere>>, <<meshcylinder,meshCylinder>>
.Exemple
[source,lua]
----
-- Un anneau d'or dressé sur la tranche, que la boule doit traverser en roulant.
local gold = materialNew()
materialSetColor(gold, 255, 200, 80)
materialSetMetallic(gold, 1)
materialSetRoughness(gold, 0.25)
local ring = nodeNew()
nodeSetMesh(ring, meshTorus(1.2, 0.25, 40), gold)
nodeSetPosition(ring, 0, 1.2, -2)
nodeSetRotation(ring, 90, 0, 0)
----
[#_microphone]
=== Microphone
Un microphone, pour un jeu de chant ou pour tout ce qui écoute. Un seul
périphérique d'enregistrement est ouvert à la fois, entendu comme un seul canal
à 48000 échantillons par seconde quoi que donne le matériel ; SDL convertit.
Singe garde les huit dernières secondes de ce qu'il entend, et chaque appel
ci-dessous répond à partir de cela, si bien que poser la question ne coûte rien
que le jeu n'ait déjà payé.
Ce qu'un jeu veut en général, c'est <<micpitch,`micPitch`>> -- la note chantée --
et <<miclevel,`micLevel`>>. Un jeu qui analyse lui-même le son, ou qui l'envoie
quelque part, lit les échantillons avec <<micread,`micRead`>> ; les appels
<<voice,Voix>> les encodent pour un réseau.
Le microphone se ferme quand le jeu se termine. Rien n'est ouvert tant qu'un script ne le demande pas.
[NOTE]
====
Une note chantée parvient au jeu un peu après avoir été chantée : la carte son,
le pilote et les tampons la retiennent chacun un instant. Un jeu de chant qui
juge la synchronisation retranche ce délai. <<miccalibrate,`micCalibrate`>> le
mesure en jouant des clics par les haut-parleurs et en chronométrant leur
retour, et la réponse est conservée pour la machine, comme le
<<singesetaudiocalibration,calibrage audio>>. Le retour audio et le calibrage
n'ont été testés jusqu'ici que sans vrai matériel ; la hauteur, le niveau, la
lecture et l'enregistrement ont été testés avec un signal connu.
====
[#miccalibrate]
==== micCalibrate
[source,text]
----
started = micCalibrate()
----
Mesure la latence du microphone : joue cinq clics par les haut-parleurs, à une
fraction de seconde d'intervalle, et chronomètre le retour de chacun par le
microphone. Cela prend quelques secondes ; <<miccalibration,`micCalibration`>>
dit quand c'est fini. La réponse est la médiane des clics entendus, et elle est
conservée pour cette machine. Faites-le dans une pièce calme, avec les
haut-parleurs assez forts pour que le microphone les entende.
*Renvoie :* `true` quand il a démarré ; `false` sans microphone ouvert ou sans
haut-parleur pour jouer les clics.
*Depuis :* 3.00.
*Voir aussi :* <<miccalibration,micCalibration>>, <<miclatency,micLatency>>
[#miccalibration]
==== micCalibration
[source,text]
----
state, latency = micCalibration()
----
Où en est un calibrage.
*Renvoie :* `state`, l'un de `"idle"`, `"running"`, `"done"` ou `"failed"`
(moins de trois des clics ont été entendus en retour), et la latence en
millisecondes une fois qu'elle est connue, `nil` sinon.
*Depuis :* 3.00.
*Voir aussi :* <<miccalibrate,micCalibrate>>
.Exemple
[source,lua]
----
local state, latency = micCalibration()
if state == "done" then
message = "Your microphone runs " .. latency .. " ms behind."
elseif state == "failed" then
message = "I could not hear the clicks. Turn the speakers up and try again."
end
----
[#micclose]
==== micClose
[source,text]
----
micClose()
----
Arrête d'écouter, et arrête avec lui tout enregistrement et tout retour audio.
Le microphone se ferme aussi quand le jeu se termine.
*Depuis :* 3.00.
[#micisopen]
==== micIsOpen
[source,text]
----
open = micIsOpen()
----
*Renvoie :* `true` tant qu'un microphone est ouvert.
*Depuis :* 3.00.
[#miclatency]
==== micLatency
[source,text]
----
milliseconds = micLatency()
----
*Renvoie :* le temps qui sépare un son produit du moment où le jeu l'entend, tel
que mesuré sur cette machine par <<miccalibrate,`micCalibrate`>> (à n'importe
quel moment, dans n'importe quel jeu), ou réglé par
<<micsetlatency,`micSetLatency`>> ; `nil` jusque-là.
*Depuis :* 3.00.
[#miclevel]
==== micLevel
[source,text]
----
rms, peak = micLevel()
----
Quelle était l'intensité du dernier instant entendu.
*Renvoie :* la valeur RMS et la crête du dernier bloc d'échantillons, chacune de
`0` à `1`. Un son continu à pleine échelle donne environ `0.71` en RMS. Les deux
valent `0` sans microphone ouvert.
*Depuis :* 3.00.
[#miclist]
==== micList
[source,text]
----
names = micList()
----
*Renvoie :* une liste des noms des microphones de cette machine, pour
<<micopen,`micOpen`>>. Elle est refaite à chaque fois, si bien qu'un microphone
branché depuis le dernier appel y apparaît.
*Depuis :* 3.00.
[#micmonitor]
==== micMonitor
[source,text]
----
ok = micMonitor(on [, volume])
----
Joue ce qu'entend le microphone par les haut-parleurs, pour qu'un chanteur
s'entende par-dessus la musique. Désactivé tant qu'on ne le demande pas : sur la
plupart des machines, la voix revient avec assez de retard pour être gênante, et
sur un portable les haut-parleurs repassent dans le microphone. Tout ce qui
attendrait plus d'un dixième de seconde avant d'être joué est abandonné plutôt
qu'entendu en retard.
*Paramètres :*
* `on` -- booléen.
* `volume` -- nombre, de `0` à `1` ; `1` s'il n'est pas donné.
*Renvoie :* `true`, ou `false` sans microphone ouvert ou sans haut-parleur où jouer.
*Depuis :* 3.00.
[#micopen]
==== micOpen
[source,text]
----
opened = micOpen([name])
----
Commence à écouter le microphone désigné (pris dans <<miclist,`micList`>>) ou
celui de la machine par défaut. Un microphone déjà ouvert est d'abord fermé.
*Renvoie :* `true` quand il s'est ouvert.
*Depuis :* 3.00.
.Exemple
[source,lua]
----
if not micOpen() then
message = "No microphone. This game needs one."
end
----
[#micpitch]
==== micPitch
[source,text]
----
hz, clarity = micPitch()
----
La note chantée, trouvée dans les 43 dernières millisecondes entendues. La
fonction trouve une fondamentale entre 50 et 2000 Hz, ce qui couvre toutes les
voix chantées, et répond à un hertz ou deux près sur un son pur.
*Renvoie :* la fondamentale en Hz, et à quel point le son est nettement une seule
note, de `0` à `1`. Sans rien de voisé -- silence, souffle, toux, foule -- `hz`
vaut `0`. Convertissez les Hz en numéro de note avec
`69 + 12 * math.log(hz / 440, 2)` : 69 est le la au-dessus du do central, et
chaque 1 vaut un demi-ton.
*Depuis :* 3.00.
.Exemple
[source,lua]
----
local hz, clarity = micPitch()
if hz > 0 and clarity > 0.8 then
local note = 69 + 12 * math.log(hz / 440, 2)
local off = note - targetNote -- demi-tons trop haut (positif) ou trop bas
if math.abs(off) < 0.5 then
score = score + 1
end
end
----
[#micread]
==== micRead
[source,text]
----
samples = micRead([max])
----
Ce qui a été entendu depuis le dernier appel, le plus ancien en premier.
*Paramètres :*
* `max` -- nombre, le plus grand nombre d'échantillons à renvoyer ; tout ce qui attend s'il n'est pas donné.
*Renvoie :* une chaîne d'échantillons de 16 bits, deux octets chacun,
little-endian, un canal à 48000 par seconde : il y en a `#samples / 2`.
Lisez-en un avec `string.unpack("<i2", samples, n * 2 - 1)`. Un jeu qui ne lit
rien pendant plus de huit secondes perd les plus anciens.
*Depuis :* 3.00.
[#micrecordstart]
==== micRecordStart
[source,text]
----
started = micRecordStart(name)
----
Écrit tout ce qui est entendu à partir de maintenant dans un fichier WAV (un
canal, 16 bits, 48000 par seconde) jusqu'à <<micrecordstop,`micRecordStop`>>.
Le nom est celui de n'importe quel fichier que le jeu a le droit d'écrire, et il
va là où vont les autres fichiers écrits par le jeu.
*Renvoie :* `true` quand le fichier a été ouvert.
*Depuis :* 3.00.
[#micrecordstop]
==== micRecordStop
[source,text]
----
micRecordStop()
----
Termine l'enregistrement.
*Depuis :* 3.00.
[#micsetlatency]
==== micSetLatency
[source,text]
----
micSetLatency(milliseconds | nil)
----
Règle la latence du microphone de cette machine, comme le fait un menu de
service à partir d'une mesure prise d'une autre façon ; `nil` l'oublie. Elle est
conservée avec les autres réglages de la machine.
*Paramètres :*
* `milliseconds` -- nombre, de `0` à `1000`, ou `nil`.
*Depuis :* 3.00.
[#midi]
=== MIDI
Deux choses sans rapport partagent ce nom, et il vaut la peine de les distinguer.
Un **fichier MIDI**, c'est de la musique, et il se joue comme toute autre
musique : passez le `.mid` à <<musicload,`musicLoad`>>. Singe le synthétise à
partir d'une SoundFont, parce qu'un fichier MIDI contient des notes plutôt que du
son. Singe n'est livré avec aucune SoundFont -- une bonne pèse des dizaines de
mégaoctets, pour un format que presque aucun jeu n'utilise -- alors désignez-en
une avec `--soundfont`, placez-en une en `Singe/soundfont.sf2` pour qu'un jeu
empaqueté porte la sienne, ou laissez Singe trouver celle que votre distribution
a installée. La ligne `SoundFont:` de l'en-tête de la trace dit laquelle a été
utilisée. Sans SoundFont nulle part, le chargement d'un `.mid` échoue et le
signale.
Les **ports MIDI**, ce sont les appels ci-dessous. Ils envoient et reçoivent des
messages en direct sur un vrai port : une borne qui pilote un module de sons
externe, ou un clavier, un boîtier de faders ou une surface de contrôle lus
comme un périphérique d'entrée de plus. Rien ici ne synthétise quoi que ce soit,
et rien ici ne touche à un fichier MIDI.
Les ports sont numérotés à partir de `0`. Rien n'est ouvert avant la première
demande d'un script -- un port MIDI coûte un descripteur de fichier et une passe
de configuration du pilote, et un jeu qui ne parle jamais de MIDI ne le paie
jamais -- si bien que le premier de ces appels est celui qui fait l'inventaire
de la machine, et <<midirescan,`midiRescan`>> cherche de nouveau
un périphérique branché depuis. Un port d'entrée
et un port de sortie peuvent être ouverts à la fois. Les canaux vont de `1` à
`16`, comme on les compte, et non de `0` à `15` comme sur le fil, et toutes les
autres valeurs vont de `0` à `127`. Sur une machine sans aucun MIDI -- pas de
ports, ou pas de bibliothèque MIDI installée -- les nombres de ports valent `0`,
les appels d'ouverture répondent `false` et les appels d'envoi répondent
`false` ; rien n'échoue.
Linux atteint chaque port par le séquenceur ALSA, qui comprend à la fois les
interfaces matérielles et tout synthétiseur ou autre programme qui y déclare le
sien. macOS utilise CoreMIDI et Windows les appels MIDI multimédia, où le
périphérique de sortie `0` est en général le synthétiseur que fournit le système
lui-même.
[#midicloseinput]
==== midiCloseInput
[source,text]
----
midiCloseInput()
----
Cesse d'écouter le port d'entrée. <<onmidimessage,`onMidiMessage`>> n'est plus appelé. Fermer un port qui n'est pas ouvert ne fait rien.
*Paramètres :*
* Aucun.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<midiopeninput,midiOpenInput>>
[#midicloseoutput]
==== midiCloseOutput
[source,text]
----
midiCloseOutput()
----
Libère le port de sortie. Les appels d'envoi répondent `false` à partir de là. Fermer un port qui n'est pas ouvert ne fait rien.
*Paramètres :*
* Aucun.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<midiopenoutput,midiOpenOutput>>
[#midicontrolchange]
==== midiControlChange
[source,text]
----
sent = midiControlChange(channel, control, value)
----
Déplace un contrôleur sur le port de sortie ouvert : le volume est le contrôleur `7`, le panoramique le `10`, la pédale de sustain le `64`.
*Paramètres :*
* `channel` -- nombre ; de `1` à `16`.
* `control` -- nombre ; de `0` à `127`.
* `value` -- nombre ; de `0` à `127`.
*Renvoie :* `true` quand le message est parti.
*Depuis :* 3.00.
*Voir aussi :* <<midisend,midiSend>>
.Exemple
[source,lua]
----
-- Baisser le volume du module pendant qu'un personnage parle.
midiControlChange(1, 7, 40)
----
[#midiinputcount]
==== midiInputCount
[source,text]
----
count = midiInputCount()
----
Combien de ports d'entrée MIDI a cette machine. `0` quand il n'y en a aucun, ou quand la plateforme n'a pas de MIDI du tout.
*Paramètres :*
* Aucun.
*Renvoie :* un nombre.
*Depuis :* 3.00.
*Voir aussi :* <<midiinputname,midiInputName>>
[#midiinputname]
==== midiInputName
[source,text]
----
name = midiInputName(index)
----
Le nom d'un port d'entrée, pour un écran de réglages qui laisse un joueur en choisir un.
*Paramètres :*
* `index` -- nombre ; de `0` à `midiInputCount() - 1`.
*Renvoie :* une chaîne, ou `nil` quand l'indice ne désigne aucun port.
*Depuis :* 3.00.
*Voir aussi :* <<midiinputcount,midiInputCount>>
.Exemple
[source,lua]
----
-- Un écran de réglages qui liste les ports d'entrée, un par ligne.
function onOverlayUpdate()
overlayClear()
for i = 0, midiInputCount() - 1 do
overlayPrint(1, i + 1, midiInputName(i))
end
return OVERLAY_UPDATED
end
----
[#midiisinputopen]
==== midiIsInputOpen
[source,text]
----
open = midiIsInputOpen()
----
Indique si un port d'entrée est ouvert.
*Paramètres :*
* Aucun.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
*Voir aussi :* <<midiopeninput,midiOpenInput>>
[#midiisoutputopen]
==== midiIsOutputOpen
[source,text]
----
open = midiIsOutputOpen()
----
Indique si un port de sortie est ouvert. Bon à demander avant une rafale de messages, car sinon chacun répondrait `false` séparément.
*Paramètres :*
* Aucun.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
*Voir aussi :* <<midiopenoutput,midiOpenOutput>>
[#midinoteoff]
==== midiNoteOff
[source,text]
----
sent = midiNoteOff(channel, key)
----
Relâche une note sur le port de sortie ouvert. Une note laissée enfoncée sonne jusqu'à ce que quelque chose l'arrête, donc chaque `midiNoteOn` a besoin de l'un de ces appels.
*Paramètres :*
* `channel` -- nombre ; de `1` à `16`.
* `key` -- nombre ; de `0` à `127`, où `60` est le do central.
*Renvoie :* `true` quand le message est parti.
*Depuis :* 3.00.
*Voir aussi :* <<midinoteon,midiNoteOn>>
[#midinoteon]
==== midiNoteOn
[source,text]
----
sent = midiNoteOn(channel, key, velocity)
----
Fait sonner une note sur le port de sortie ouvert.
*Paramètres :*
* `channel` -- nombre ; de `1` à `16`. Le canal `10` est celui des percussions sur un périphérique General MIDI.
* `key` -- nombre ; de `0` à `127`, où `60` est le do central.
* `velocity` -- nombre ; de `0` à `127`, la force avec laquelle la touche a été frappée.
*Renvoie :* `true` quand le message est parti.
*Depuis :* 3.00.
*Voir aussi :* <<midinoteoff,midiNoteOff>>
.Exemple
[source,lua]
----
-- Une ponctuation sonore sur un module externe quand le joueur est touché.
midiNoteOn(10, 49, 110)
midiNoteOff(10, 49)
----
[#midiopeninput]
==== midiOpenInput
[source,text]
----
opened = midiOpenInput(index)
----
Écoute un port d'entrée. <<onmidimessage,`onMidiMessage`>> est alors appelé pour chaque message qui arrive. Le port d'entrée qui était ouvert, quel qu'il soit, est d'abord fermé.
*Paramètres :*
* `index` -- nombre ; de `0` à `midiInputCount() - 1`.
*Renvoie :* `true` quand le port s'est ouvert.
*Depuis :* 3.00.
*Voir aussi :* <<midicloseinput,midiCloseInput>>, <<onmidimessage,onMidiMessage>>
.Exemple
[source,lua]
----
if midiInputCount() > 0 then
midiOpenInput(0)
end
----
[#midiopenoutput]
==== midiOpenOutput
[source,text]
----
opened = midiOpenOutput(index)
----
Prend un port de sortie pour les appels d'envoi. Le port de sortie qui était ouvert, quel qu'il soit, est d'abord fermé.
*Paramètres :*
* `index` -- nombre ; de `0` à `midiOutputCount() - 1`.
*Renvoie :* `true` quand le port s'est ouvert.
*Depuis :* 3.00.
*Voir aussi :* <<midicloseoutput,midiCloseOutput>>
[#midioutputcount]
==== midiOutputCount
[source,text]
----
count = midiOutputCount()
----
Combien de ports de sortie MIDI a cette machine.
*Paramètres :*
* Aucun.
*Renvoie :* un nombre.
*Depuis :* 3.00.
*Voir aussi :* <<midioutputname,midiOutputName>>
[#midioutputname]
==== midiOutputName
[source,text]
----
name = midiOutputName(index)
----
Le nom d'un port de sortie.
*Paramètres :*
* `index` -- nombre ; de `0` à `midiOutputCount() - 1`.
*Renvoie :* une chaîne, ou `nil` quand l'indice ne désigne aucun port.
*Depuis :* 3.00.
*Voir aussi :* <<midioutputcount,midiOutputCount>>
[#midipitchbend]
==== midiPitchBend
[source,text]
----
sent = midiPitchBend(channel, value)
----
Infléchit la hauteur d'un canal. `8192` est le milieu, c'est-à-dire aucune inflexion ; jusqu'où vont `0` et `16383` dépend d'un réglage propre au périphérique récepteur, en général deux demi-tons.
*Paramètres :*
* `channel` -- nombre ; de `1` à `16`.
* `value` -- nombre ; de `0` à `16383`.
*Renvoie :* `true` quand le message est parti.
*Depuis :* 3.00.
[#midiprogramchange]
==== midiProgramChange
[source,text]
----
sent = midiProgramChange(channel, program)
----
Choisit le son que joue un canal. Les numéros sont ceux du périphérique récepteur ; sur un périphérique General MIDI, `0` est un piano à queue.
*Paramètres :*
* `channel` -- nombre ; de `1` à `16`.
* `program` -- nombre ; de `0` à `127`.
*Renvoie :* `true` quand le message est parti.
*Depuis :* 3.00.
[#midirescan]
==== midiRescan
[source,text]
----
midiRescan()
----
Cherche de nouveau les ports, pour un périphérique branché pendant que le jeu tourne. Un port ouvert reste ouvert. Les indices ne sont pas plus stables que la liste : relisez donc les noms après cet appel plutôt que de supposer que le port `2` est toujours ce qu'il était.
*Paramètres :*
* Aucun.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<midiinputcount,midiInputCount>>, <<midioutputcount,midiOutputCount>>
[#midisend]
==== midiSend
[source,text]
----
sent = midiSend(byte [, byte [, byte]])
sent = midiSend(string)
----
N'importe quel message, pour ceux que les appels nommés ne couvrent pas. Avec des nombres, il les envoie tels quels, c'est-à-dire un octet de statut et jusqu'à deux octets de données -- et notez que l'octet de statut porte le canal dans ses quatre bits de poids faible, compté à partir de `0`, contrairement à tous les autres appels de cette section. Avec une chaîne, il envoie la chaîne entière, et c'est ainsi que part un message système exclusif.
*Paramètres :*
* `byte` -- nombre ; de `0` à `255`, d'un à trois.
* `string` -- une chaîne d'octets, envoyée telle quelle.
*Renvoie :* `true` quand le message est parti.
*Depuis :* 3.00.
*Voir aussi :* <<midinoteon,midiNoteOn>>, <<midicontrolchange,midiControlChange>>
.Exemple
[source,lua]
----
-- Toutes les notes coupées sur le canal 1, ce qui n'a pas d'appel nommé.
midiSend(0xB0, 123, 0)
-- Un message système exclusif, octets compris.
midiSend(string.char(0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7))
----
[#model]
=== Modèle
Les modèles sont des fichiers glTF 2.0 binaires (`.glb`), chargés une fois et
placés autant de fois qu'on veut. Un modèle doit être autonome (tampons et
images à l'intérieur du fichier) ; un modèle qui renvoie à un fichier placé à
côté de lui est refusé. Le chargement donne un handle entier ; `modelInstance`
construit les nœuds du modèle dans la scène et renvoie leur racine, que prennent
ensuite tous les appels `node*` et `animation*`. Les positions et les tailles à
l'intérieur d'un modèle sont en unités du monde, telles que le fichier les
stocke ; le nœud racine met à l'échelle toute l'instance. Voir <<scenes3d,Scènes 3D>>.
[#modelload]
==== modelLoad
[source,text]
----
model = modelLoad(name)
----
Lit un `.glb` par la même recherche que toute autre ressource (dossier du jeu, dossier de données, base de données empaquetée), envoie une fois ses maillages et ses matériaux au GPU, et garde son arbre de nœuds, ses skins et ses animations pour l'instanciation. Appelez-la une fois au démarrage et instanciez le handle autant de fois qu'il le faut ; charger deux fois le même fichier en fait deux copies sur le GPU. Elle a besoin de la scène 3D, donc sur une machine sans GPU adapté elle échoue comme tout autre appel 3D. Un mauvais fichier met fin au script avec un message qui nomme le problème : le fichier n'a pas pu être lu, n'est pas du glTF, renvoie à un tampon ou à une image externe, échoue à la validation, contient une primitive qui n'a pas pu être envoyée au GPU, a un skin de plus de `128` articulations, ou a un maillage skinné qui nomme une articulation numérotée `128` ou plus. Un maillage skinné qui nomme une articulation au-delà des skins qui l'utilisent se charge, de même qu'une animation dont les images clés ne peuvent pas être lues (cet échantillonneur est ignoré) ; l'un et l'autre cas sont notés dans la trace du programme. Une texture qui ne peut pas être décodée, ou une image KTX2 manquante, ne fait pas échouer le chargement ; le matériau se passe de cette carte et la trace du programme (`--program`) dit laquelle.
*Renvoie :* Le handle du modèle, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<modelinstance,modelInstance>>, <<modelgetanimations,modelGetAnimations>>, <<modeldelete,modelDelete>>
.Exemple
[source,lua]
----
-- Charger une fois, placer trois fois.
treeModel = modelLoad(DIR .. "Models/Tree.glb")
for i = 1, 3 do
local tree = modelInstance(treeModel)
nodeSetPosition(tree, i * 3 - 6, 0, -4)
nodeSetRotation(tree, 0, i * 70, 0)
end
----
[#modeldelete]
==== modelDelete
[source,text]
----
modelDelete(model)
----
Libère les maillages, les matériaux et les données analysées du modèle. Chaque instance qui en a été faite est oubliée en même temps : ses nœuds restent dans la scène (position, parent et enfants intacts) mais ne dessinent rien, ses animations s'arrêtent là où elles en sont, et un appel `animation*` ultérieur sur sa racine met fin au script, parce que le nœud n'est plus une instance de modèle. Supprimez les racines des instances avec `nodeDelete` quand vous ne voulez pas non plus des nœuds. Un handle qui n'est pas un modèle chargé met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<modelload,modelLoad>>, <<modelinstance,modelInstance>>, <<nodedelete,nodeDelete>>
.Exemple
[source,lua]
----
-- En quittant le niveau du hangar : se débarrasser de ses accessoires et du modèle qui les fournit.
function unloadHangar()
for _, crate in ipairs(hangarCrates) do
nodeDelete(crate)
end
hangarCrates = {}
modelDelete(crateModel)
crateModel = nil
end
----
[#modelgetanimations]
==== modelGetAnimations
[source,text]
----
names = modelGetAnimations(model)
----
Liste les clips d'animation du modèle dans l'ordre du fichier, sous forme de table de chaînes, si bien que `names[3]` est le clip que joue `animationPlay(node, 3)`. Un clip que l'exportateur a laissé sans nom donne une chaîne vide ; on peut quand même le jouer par son numéro. Un modèle sans animations donne une table vide. Un handle qui n'est pas un modèle chargé met fin au script.
*Renvoie :* Une table de chaînes indicée à partir de `1`.
*Depuis :* 3.00.
*Voir aussi :* <<animationplay,animationPlay>>, <<modelload,modelLoad>>
.Exemple
[source,lua]
----
-- Trouver les clips par leur nom une seule fois, pour que le reste du jeu utilise des numéros.
guardModel = modelLoad(DIR .. "Models/Guard.glb")
guardClips = {}
for index, name in ipairs(modelGetAnimations(guardModel)) do
guardClips[name] = index
end
if guardClips["Idle"] == nil then
debugPrint("Guard.glb has no Idle clip")
end
----
[#modelinstance]
==== modelInstance
[source,text]
----
node = modelInstance(model)
node = modelInstance(model, parent)
----
Construit l'arbre de nœuds du modèle sous un nouveau nœud racine, lui-même placé sous `parent` ou sous la racine de la scène. La racine est un nœud ordinaire : placez-la, tournez-la, mettez-la à l'échelle ou donnez-lui un parent comme à n'importe quel autre, et supprimez-la avec `nodeDelete` pour retirer toute l'instance. Les nœuds propres au modèle prennent leurs noms glTF (ou le nom de leur maillage quand le nœud n'a pas de nom), si bien que `nodeFind(name, node)` les atteint ; un maillage à plusieurs matériaux devient un nœud enfant par partie ; les lumières du fichier deviennent des lumières de la scène avec la couleur, l'intensité, la portée et le cône du fichier ; les skins sont attachés et les poids de morphing réglés. Les nœuds gardent la pose du fichier jusqu'à ce qu'une animation joue. Un handle qui n'est pas un modèle chargé ou un `parent` qui n'est pas un nœud met fin au script, tout comme le manque de nœuds disponibles.
*Renvoie :* Le handle du nœud racine de l'instance.
*Depuis :* 3.00.
*Voir aussi :* <<modelload,modelLoad>>, <<nodefind,nodeFind>>, <<animationplay,animationPlay>>, <<nodedelete,nodeDelete>>
.Exemple
[source,lua]
----
-- Une tourelle sur la caisse du char, pour qu'elle tourne avec la caisse et pivote d'elle-même.
tank = modelInstance(hullModel)
turret = modelInstance(turretModel, tank)
nodeSetPosition(turret, 0, 1.1, -0.3)
barrel = nodeFind("Barrel", turret)
function onOverlayUpdate()
nodeSetRotation(turret, 0, aimYaw, 0)
nodeSetRotation(barrel, aimPitch, 0, 0)
return OVERLAY_UPDATED
end
----
[#mouse]
=== Souris
Singe lit jusqu'à quatre souris, et traite les pistolets optiques comme des souris. Dans le mode à une seule souris, celui par défaut, chaque dispositif de pointage pilote un curseur unique signalé comme souris `0` ; `mouseSetMode(MOUSE_MANY)` distingue les périphériques, ce dont a besoin une borne à deux pistolets optiques pour deux joueurs. Les positions arrivent en coordonnées de l'overlay, pas en pixels de l'écran, par `onMouseMoved` (voir <<enginecallbacks,Callbacks du moteur>>) et à la demande par `mouseGetPosition`. Les boutons et la molette de la souris sont des contacts comme les touches : ils atteignent `onInputPressed` et `onInputReleased` quand `controls.cfg` affecte un code des tables `MOUSE_0` à `MOUSE_3`, et les valeurs par défaut fournies affectent le bouton gauche à `SWITCH_BUTTON3`, le droit à `SWITCH_BUTTON1` et celui du milieu à `SWITCH_BUTTON2`. Le moteur démarre avec la souris capturée, et activée sauf si `--nomouse` a été donné ou si aucune souris n'a été trouvée.
[#mousedisable]
==== mouseDisable
[source,text]
----
mouseDisable()
----
Désactive l'entrée de la souris, exactement comme le fait `mouseSetEnabled(false)`. C'est une fonction définie dans `Framework.singe`, conservée pour que les anciens jeux tournent sans modification.
*Remarques :* Alias hérité.
WARNING: Le nouveau code doit appeler <<mousesetenabled,mouseSetEnabled>> avec `false`.
*Depuis :* 1.18 (RDG)
*Voir aussi :* <<mousesetenabled,mouseSetEnabled>>, <<mouseenable,mouseEnable>>
.Exemple
[source,lua]
----
-- Code d'un ancien jeu ; l'écriture moderne est mouseSetEnabled(false).
function cutsceneBegin()
mouseDisable()
discSearch(CUTSCENE_START)
end
----
[#mouseenable]
==== mouseEnable
[source,text]
----
mouseEnable()
----
Active l'entrée de la souris, exactement comme le fait `mouseSetEnabled(true)`, et, comme elle, la laisse désactivée quand Singe a été lancé avec `--nomouse`. C'est une fonction définie dans `Framework.singe`, conservée pour que les anciens jeux tournent sans modification.
*Remarques :* Alias hérité.
WARNING: Le nouveau code doit appeler <<mousesetenabled,mouseSetEnabled>> avec `true`.
*Depuis :* 1.18 (RDG)
*Voir aussi :* <<mousesetenabled,mouseSetEnabled>>, <<mousedisable,mouseDisable>>
.Exemple
[source,lua]
----
-- Code d'un ancien jeu ; l'écriture moderne est mouseSetEnabled(true).
function cutsceneEnd()
mouseEnable()
discSearch(LEVEL_START)
end
----
[#mousegetname]
==== mouseGetName
[source,text]
----
name = mouseGetName(mouse)
----
Le nom que le pilote de souris donne à un périphérique, ou `nil` quand aucun périphérique ne porte cet indice. C'est grâce à lui qu'on distingue deux pistolets d'une borne sur un écran de service, ce qui sinon relève de la devinette quand les deux se signalent comme souris `0` et `1`. Un indice hors de `0` à `3` met fin au script.
*Paramètres :*
* `mouse` -- indice du périphérique, de `0` à `3`.
*Renvoie :* chaîne, ou `nil`.
*Depuis :* 3.00
*Voir aussi :* <<mousehowmany,mouseHowMany>>, <<mousesetmode,mouseSetMode>>, <<controllergetname,controllerGetName>>
.Exemple
[source,lua]
----
-- Nommer les pistolets sur l'écran de calibrage.
mouseSetMode(MOUSE_MANY)
for device = 0, mouseHowMany() - 1 do
fontPrint(20, 20 + device * 18, string.format("Gun %d: %s", device + 1, mouseGetName(device)))
end
----
[#mousegetpadding]
==== mouseGetPadding
[source,text]
----
padding = mouseGetPadding()
----
Extension Hypseus, toujours `0`, pour la raison que donne <<controllergetpadding,`controllerGetPadding`>>. Hypseus ajoute ce décalage aux codes d'entrée que ses souris signalent en mode relatif ; les codes de Singe ne portent aucun décalage de ce genre, il n'y a donc rien à soustraire.
*Renvoie :* un entier, toujours `0`.
*Depuis :* 3.00
*Voir aussi :* <<controllergetpadding,controllerGetPadding>>
[#mousegetposition]
==== mouseGetPosition
[source,text]
----
x, y = mouseGetPosition(index)
----
Renvoie la dernière position signalée d'une souris en coordonnées de l'overlay, les mêmes valeurs que portait le dernier `onMouseMoved` de ce périphérique. En mode à une seule souris, seule la souris `0` bouge ; en mode multi-souris, chaque périphérique a sa propre position, qui part du centre de la vidéo. La position cesse d'être mise à jour tant que l'entrée de la souris est désactivée. Interrogez-la depuis `onOverlayUpdate` ou `singeMain` quand c'est plus simple que de suivre le callback. Un indice hors de `0` à `3` met fin au script.
*Paramètres :*
* `index` -- indice de la souris, de `0` à `3`.
*Renvoie :* deux entiers, `x` et `y`, en coordonnées de l'overlay.
*Depuis :* 2.00
*Voir aussi :* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<singewantscrosshairs,singeWantsCrosshairs>>
.Exemple
[source,lua]
----
-- Dessiner le réticule là où vise le pistolet.
function onOverlayUpdate()
overlayClear()
drawTargets()
if singeWantsCrosshairs() then
local x, y = mouseGetPosition(0)
spriteDraw(crosshair, x, y, true)
end
return OVERLAY_UPDATED
end
----
[#mousehowmany]
==== mouseHowMany
[source,text]
----
count = mouseHowMany()
----
Renvoie le nombre de souris que le moteur a trouvées au démarrage, de `0` à `4`. Les périphériques branchés plus tard ne sont pas comptés. Quand aucune n'a été trouvée, la souris reste désactivée. Servez-vous-en pour décider s'il faut proposer un second joueur au pistolet optique.
*Renvoie :* entier, de `0` à `4`.
*Depuis :* 1.18 (RDG)
*Voir aussi :* <<mousesetmode,mouseSetMode>>, <<mousegetposition,mouseGetPosition>>
.Exemple
[source,lua]
----
-- Deux pistolets présents : donner un curseur à chaque joueur.
if mouseHowMany() >= 2 then
mouseSetMode(MOUSE_MANY)
players = 2
else
players = 1
end
----
[#mousesetcaptured]
==== mouseSetCaptured
[source,text]
----
mouseSetCaptured(captured)
----
Capture la souris, en la confinant à la fenêtre de Singe et en masquant le curseur, ou la libère et montre de nouveau le curseur. Le moteur capture la souris au démarrage, si bien que la plupart des jeux n'appellent jamais cette fonction ; un éditeur de niveaux en fenêtre ou un jeu qui veut que le curseur du bureau reste visible la libère. Le contact affecté à `INPUT_GRAB` dans `controls.cfg` bascule le même état depuis le clavier.
*Paramètres :*
* `captured` -- booléen ; `true` capture et masque, `false` libère et montre.
*Depuis :* 2.00
*Voir aussi :* <<mousesetenabled,mouseSetEnabled>>, <<mousegetposition,mouseGetPosition>>
.Exemple
[source,lua]
----
-- L'éditeur de niveaux veut le curseur du bureau ; le jeu le masque de nouveau.
function editorToggle()
editing = not editing
mouseSetCaptured(not editing)
end
----
[#mousesetenabled]
==== mouseSetEnabled
[source,text]
----
mouseSetEnabled(enabled)
----
Active ou désactive l'entrée de la souris. Quand elle est désactivée, les événements de mouvement, de bouton et de molette sont écartés avant d'atteindre le script : `onMouseMoved` n'est plus appelé, les boutons de la souris ne produisent plus de contacts, et `mouseGetPosition` continue de renvoyer la dernière position qu'il avait. Le curseur lui-même bouge toujours. L'activation respecte `--nomouse` : quand Singe a été lancé avec cette option, la souris reste désactivée. Un argument non booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Depuis :* 3.00
*Voir aussi :* <<mousesetcaptured,mouseSetCaptured>>, <<mousesetmode,mouseSetMode>>, <<mouseenable,mouseEnable>>
.Exemple
[source,lua]
----
-- Ignorer le pistolet pendant que tourne la boucle du mode attraction.
function attractBegin()
mouseSetEnabled(false)
discSearch(ATTRACT_START)
end
function attractEnd()
mouseSetEnabled(true)
end
----
[#mousesetmode]
==== mouseSetMode
[source,text]
----
mouseSetMode(mode)
----
Choisit la façon dont les souris sont lues. En `MOUSE_SINGLE` (par défaut), les événements de pointeur de la fenêtre pilotent un curseur unique signalé comme périphérique `0`, les positions absolues comme les mouvements relatifs étant convertis en coordonnées de l'overlay. En `MOUSE_MANY`, chaque périphérique physique est lu séparément : les mouvements relatifs sont intégrés en une position propre à chaque périphérique, ramenée dans la zone de la vidéo, les périphériques absolus comme les tablettes et certains pistolets optiques sont projetés sur la vidéo, et `onMouseMoved` signale l'indice du périphérique. Les deux modes livrent les mêmes cinq boutons et la molette verticale sous forme de contacts. `SINGLE_MOUSE` et `MANY_MOUSE` sont acceptés comme anciennes écritures ; toute autre valeur met fin au script. `--manymouse` est la décision de l'utilisateur lui-même et passe avant cet appel : quand il est donné, le mode est `MOUSE_MANY` dès le départ et `mouseSetMode(MOUSE_SINGLE)` ne le désactive pas, si bien qu'on peut faire fonctionner une borne à deux pistolets de l'extérieur d'un jeu qui ne le demande jamais.
*Paramètres :*
* `mode` -- `MOUSE_SINGLE` (`100`) ou `MOUSE_MANY` (`200`).
*Depuis :* 1.18 (RDG)
*Voir aussi :* <<mousehowmany,mouseHowMany>>, <<onmousemoved,onMouseMoved>>, <<mousegetposition,mouseGetPosition>>
.Exemple
[source,lua]
----
-- Jeu coopératif au pistolet optique : chaque pistolet dirige son propre curseur.
mouseSetMode(MOUSE_MANY)
function onMouseMoved(x, y, xr, yr, which)
if which < 2 then
cursorX[which + 1] = x
cursorY[which + 1] = y
end
end
----
[#musicref]
=== Musique
Musique de fond, chargée et jouée à part des effets sonores, avec son propre volume. Hypseus fait tourner la musique sur un second mélangeur exactement pour cette raison, et Singe lui donne plutôt son propre gain : <<musicsetvolume,`musicSetVolume`>> ne touche jamais un effet sonore et <<soundsetvolume,`soundSetVolume`>> ne touche jamais la musique.
Un handle de musique est un type de handle à part, ni un handle de son ni un canal : un handle est une piste, et elle se joue, se met en pause, reprend et s'arrête indépendamment des autres. Il n'y a pas de limite de canaux, contrairement aux seize canaux d'effets sonores.
L'échelle de volume est celle d'Hypseus, de `0` à `128`, presque le double des `0` à `63` qu'utilise la famille `sound`, si bien qu'un jeu écrit pour Hypseus garde ses propres nombres. La musique démarre aussi fort que les effets sonores, au niveau qu'a réglé `--volume_nonvldp` ; Hypseus, lui, démarre son mélangeur de musique au tiers, ce que chaque jeu Hypseus écrase aussitôt avec `musicSetVolume(soundGetVolume() * 2)`.
Tous les formats que lit la famille sound sont lus ici : WAV, Ogg Vorbis, Opus, FLAC, MP3, WavPack et les autres. L'en-tête de trace écrit par `--program` et `--trace` liste exactement quels décodeurs contient la compilation, sur sa ligne `Audio:`.
Mettre le moteur en pause (la touche de pause, ou `singeSetPauseFlag`) met la musique en pause avec lui, et la reprise la relance ; un rechargement arrête et libère toutes les pistes.
[#musicgetposition]
==== musicGetPosition
[source,text]
----
seconds = musicGetPosition(id)
----
Où en est l'auditeur dans une piste en cours de lecture, en secondes : la position propre du mélangeur, moins ce qui est encore en route vers les haut-parleurs -- la file du périphérique, le calibrage audio de la machine et le retard audio du jeu -- si bien qu'une image calée dessus tombe avec le son. C'est l'horloge sur laquelle animer quand la musique mène, comme dans une démo ou un jeu de rythme, et elle reste avec le morceau quelle que soit sa durée, là où compter les images ou les ticks dérive. Le mélangeur avance sa position d'un tampon à la fois, si bien qu'entre deux avancées la réponse continue avec l'horloge, jamais de plus de 50 ms, et elle avance en douceur à chaque image. Une piste en pause reste immobile. Une piste qui ne joue pas répond `-1`.
*Paramètres :*
* `id` -- nombre ; un handle de musique.
*Renvoie :* nombre ; les secondes écoulées dans la piste, ou `-1`.
*Depuis :* 3.00
*Voir aussi :* <<musicplay,musicPlay>>, <<singegetaudiolatency,singeGetAudioLatency>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
.Exemple
[source,lua]
----
-- Un flash à chaque temps d'un morceau à 120 battements par minute, sur le temps tel qu'on l'entend.
function onOverlayUpdate()
colorBackground(0, 0, 0)
overlayClear()
local t = musicGetPosition(song)
if t >= 0 and (t * 2) % 1 < 0.1 then
colorBackground(255, 255, 255, 120)
overlayClear()
end
return OVERLAY_UPDATED
end
----
[#musicisplaying]
==== musicIsPlaying
[source,text]
----
playing = musicIsPlaying([id])
----
Extension Hypseus. Indique si cette piste joue, ou -- sans handle -- si une musique quelconque joue. Une piste en pause ne joue pas.
*Paramètres :*
* `id` -- nombre facultatif ; un handle de musique.
*Renvoie :* booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
.Exemple
[source,lua]
----
if not musicIsPlaying(theme) then
musicPlay(theme, -1)
end
----
[#musicload]
==== musicLoad
[source,text]
----
id = musicLoad(filename)
----
Extension Hypseus. Charge une piste de musique par le système de fichiers virtuel et renvoie son handle. Le fichier entier est décodé d'avance, comme pour un son, si bien qu'une longue piste coûte de la mémoire, sauf un chiptune qui ne dit pas sa durée ou ce qui ferait plus de 512 Mo une fois décodé, l'un comme l'autre décodé au fil de la lecture ; le handle est valide jusqu'à <<musicunload,`musicUnload`>> ou jusqu'à un rechargement.
Un fichier absent, ou qu'aucun décodeur compilé ne sait lire, met fin au script, comme le fait <<soundload,`soundLoad`>>.
*Paramètres :*
* `filename` -- chaîne ; un chemin que le système de fichiers virtuel peut atteindre.
*Renvoie :* nombre, le handle de musique.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<musicplay,musicPlay>>, <<musicunload,musicUnload>>, <<soundload,soundLoad>>
.Exemple
[source,lua]
----
local theme = musicLoad("music/attract.mp3")
musicSetVolume(96)
musicPlay(theme, -1)
----
[#musicpause]
==== musicPause
[source,text]
----
musicPause([id])
----
Extension Hypseus. Met cette piste en pause là où elle en est, ou toute la musique quand aucun handle n'est donné. <<musicresume,`musicResume`>> la relance.
*Paramètres :*
* `id` -- nombre facultatif ; un handle de musique.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<musicresume,musicResume>>, <<musicstop,musicStop>>
.Exemple
[source,lua]
----
-- Le joueur a ouvert la carte ; suspendre la musique plutôt que la reprendre au début.
musicPause()
----
[#musicplay]
==== musicPlay
[source,text]
----
id = musicPlay(id [, loops])
----
Extension Hypseus. Joue une piste depuis son début et répond son handle, et c'est ainsi que les jeux d'Hypseus le gardent (`sndHandle = musicPlay(music)`) pour arrêter le morceau plus tard. `loops` a le même sens que celui de <<soundplay,`soundPlay`>> : `0`, la valeur par défaut, joue une fois, `-1` joue indéfiniment, et `N` répète `N` fois de plus. Le nombre est ramené entre `-1` et `64`, comme le fait Hypseus, si bien qu'un jeu qui en demande davantage obtient ce qu'il aurait obtenu là-bas.
Jouer une piste qui joue déjà la relance depuis le début.
*Paramètres :*
* `id` -- nombre ; un handle de musique.
* `loops` -- nombre facultatif ; de `-1` à `64`. `0` par défaut.
*Renvoie :* nombre ; le handle qu'il a reçu.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<musicstop,musicStop>>, <<musicsetvolume,musicSetVolume>>
.Exemple
[source,lua]
----
musicPlay(theme, -1) -- Encore et encore, jusqu'à ce que quelque chose l'arrête.
----
[#musicresume]
==== musicResume
[source,text]
----
musicResume([id])
----
Extension Hypseus. Reprend une piste en pause, ou toute la musique en pause quand aucun handle n'est donné.
La forme sans argument d'Hypseus ne fait rien du tout ; celle de Singe reprend tout, ce qui va de pair avec <<musicpause,`musicPause`>>.
*Paramètres :*
* `id` -- nombre facultatif ; un handle de musique.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<musicpause,musicPause>>
.Exemple
[source,lua]
----
musicResume()
----
[#musicsetvolume]
==== musicSetVolume
[source,text]
----
volume = musicSetVolume(volume)
----
Extension Hypseus. Règle le gain de toute la musique à la fois, sur l'échelle de `0` à `128` d'Hypseus, et répond le niveau retenu. Elle ne touche à rien d'autre que la musique : les effets sonores gardent ce que leur a donné <<soundsetvolume,`soundSetVolume`>>, et `soundSetVolume` laisse la musique tranquille.
Un niveau hors de `0` à `128` met fin au script. Le niveau s'applique aux pistes déjà en cours de lecture et à toutes celles chargées ensuite, et revient au niveau de départ lors d'un rechargement.
*Paramètres :*
* `volume` -- nombre ; de `0` à `128`.
*Renvoie :* nombre, le niveau qui a été réglé.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<soundsetvolume,soundSetVolume>>, <<musicplay,musicPlay>>
.Exemple
[source,lua]
----
-- Ce qu'écrit un jeu Hypseus : le niveau du son, doublé pour l'échelle de la musique.
musicSetVolume(soundGetVolume() * 2)
----
[#musicstop]
==== musicStop
[source,text]
----
musicStop([id [, fadeMs]])
----
Extension Hypseus. Arrête cette piste, ou toute la musique quand aucun handle n'est donné (un handle `nil` compte comme aucun). `fadeMs` la fait disparaître en fondu sur ce nombre de millisecondes au lieu de la couper ; `0`, la valeur par défaut, l'arrête aussitôt. Un fondu négatif met fin au script.
*Paramètres :*
* `id` -- nombre facultatif ; un handle de musique.
* `fadeMs` -- nombre facultatif ; la durée du fondu de sortie, en millisecondes. `0` par défaut.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<musicplay,musicPlay>>, <<musicpause,musicPause>>
.Exemple
[source,lua]
----
musicStop(theme, 1500) -- Partie en une seconde et demie.
----
[#musicunload]
==== musicUnload
[source,text]
----
musicUnload(id)
----
Extension Hypseus. Arrête la piste et la libère. Le handle n'est plus valide ensuite et l'utiliser met fin au script.
*Paramètres :*
* `id` -- nombre ; un handle de musique.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<musicload,musicLoad>>
.Exemple
[source,lua]
----
musicUnload(theme)
----
[#nav]
=== Navigation
Les maillages de navigation précalculés à partir de la géométrie de la scène, et les agents de foule qui y marchent (voir <<scenes3d,Scènes 3D>>, Navigation). `navNew` et `navLoad` renvoient un handle de maillage et `navAgentNew` un handle d'agent ; ce sont tous deux des entiers, vérifiés à chaque appel, et un handle invalide met fin au script. Les positions sont en unités du monde, les vitesses en unités par seconde et les pentes en degrés. Jusqu'à huit maillages à la fois, 128 agents sur chacun et 256 en tout. Les requêtes et les agents ont besoin d'un maillage que `navBuild` a précalculé ou que `navLoad` a lu ; avant cela, les requêtes renvoient `nil`, `navDraw` ne dessine rien et `navAgentNew` met fin au script.
[#navaddnode]
==== navAddNode
[source,text]
----
navAddNode(nav, node)
----
Ajoute le maillage du nœud, et celui de chaque nœud situé sous lui, à la géométrie à partir de laquelle le maillage de navigation sera précalculé, tels qu'ils se trouvent dans le monde à cet instant : placez-les donc d'abord. Le nœud racine d'une instance de modèle ajoute le modèle entier, et un niveau construit à partir de primitives peut être ajouté nœud par nœud. Seule la géométrie des maillages compte : les corps, les sprites et le texte n'ajoutent rien. Met fin au script une fois que le maillage a été construit.
*Depuis :* 3.00.
*Voir aussi :* <<navnew,navNew>>, <<navbuild,navBuild>>, <<modelinstance,modelInstance>>
.Exemple
[source,lua]
----
-- Le sol et les murs sont de la géométrie praticable ; les caisses non, elles bougent.
local nav = navNew(0.4, 1.8, 45, 0.4)
navAddNode(nav, ground)
for _, wall in ipairs(walls) do
navAddNode(nav, wall)
end
navAddNode(nav, modelInstance(towerModel))
navBuild(nav)
----
[#navagentdelete]
==== navAgentDelete
[source,text]
----
navAgentDelete(agent)
----
Retire l'agent du maillage et libère son handle ; le nœud reste où il est et n'est plus déplacé. Supprimer le maillage avec `navDelete` fait de même pour chaque agent qui s'y trouve. Un handle invalide met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<navagentnew,navAgentNew>>, <<navdelete,navDelete>>
.Exemple
[source,lua]
----
-- Un garde qui meurt cesse de naviguer ; le ragdoll prend le modèle en charge à partir de là.
function killGuard(guard)
navAgentDelete(guard.agent)
guard.agent = nil
ragdollActivate(guard.model)
ragdollApplyImpulse(guard.model, "b_Spine02_03", 0, 20, -30)
end
----
[#navagentgetvelocity]
==== navAgentGetVelocity
[source,text]
----
x, y, z = navAgentGetVelocity(agent)
----
La façon dont la foule déplace l'agent à cette image, en unités du monde par seconde sur chaque axe ; sa longueur va jusqu'à la vitesse de l'agent et tombe à zéro quand il s'arrête. Pour choisir entre une animation d'attente, de marche ou de course, ou pour adapter le cycle de marche au terrain parcouru.
*Renvoie :* Trois nombres.
*Depuis :* 3.00.
*Voir aussi :* <<navagentmoveto,navAgentMoveTo>>, <<animationplay,animationPlay>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
for agent, fox in pairs(foxes) do
local vx, vy, vz = navAgentGetVelocity(agent)
local speed = math.sqrt(vx * vx + vz * vz)
local clip = (speed < 0.1) and "Survey" or ((speed < 2.5) and "Walk" or "Run")
if fox.clip ~= clip then
animationPlay(fox.node, clip, true, 1, 0.2)
fox.clip = clip
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navagentisarrived]
==== navAgentIsArrived
[source,text]
----
arrived = navAgentIsArrived(agent)
----
Indique si l'agent a atteint la dernière cible que lui a donnée `navAgentMoveTo` : à moins d'une fois et demie son rayon de la cible et presque arrêté. Il passe à `true` au moment où `onNavArrived` est déclenché et reste `true` jusqu'au prochain `navAgentMoveTo` ; il vaut `false` pour un agent qui n'a jamais été envoyé nulle part ou qui a été arrêté en chemin.
*Renvoie :* Vrai ou faux.
*Depuis :* 3.00.
*Voir aussi :* <<onnavarrived,onNavArrived>>, <<navagentmoveto,navAgentMoveTo>>, <<navagentstop,navAgentStop>>
.Exemple
[source,lua]
----
-- La patrouille attend un moment à chaque poste avant de repartir.
function onOverlayUpdate()
if navAgentIsArrived(patrol) then
waitFrames = waitFrames + 1
if waitFrames > 180 then
post = post % #posts + 1
waitFrames = 0
navAgentMoveTo(patrol, posts[post][1], posts[post][2], posts[post][3])
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navagentmoveto]
==== navAgentMoveTo
[source,text]
----
ok = navAgentMoveTo(agent, x, y, z)
----
Envoie l'agent marcher jusqu'au point du maillage le plus proche de `x, y, z`, en en cherchant un jusqu'à 2 unités sur les côtés et 4 vers le haut ou vers le bas. La foule planifie l'itinéraire au fil des images suivantes et guide l'agent autour des autres agents en chemin ; `onNavArrived` est déclenché une fois qu'il y est arrivé. Renvoie `false`, et laisse l'agent tel qu'il était, quand aucun point du maillage n'est assez proche ou que la foule refuse la demande. Une nouvelle cible remplace l'ancienne à tout moment.
*Paramètres :*
* `agent` -- le handle venant de `navAgentNew`.
* `x`, `y`, `z` -- la destination en unités du monde ; elle est ramenée sur le maillage.
*Renvoie :* Vrai quand l'agent s'est mis en route.
*Depuis :* 3.00.
*Voir aussi :* <<navagentstop,navAgentStop>>, <<navnearest,navNearest>>, <<navpath,navPath>>
.Exemple
[source,lua]
----
-- L'acolyte suit le héros, réorienté deux fois par seconde.
function onOverlayUpdate()
ticks = ticks + 1
if ticks % 30 == 0 then
local hx, hy, hz = nodeGetWorldPosition(hero)
if not navAgentMoveTo(sidekickAgent, hx, hy, hz) then
navAgentStop(sidekickAgent) -- Le héros est hors du maillage : il saute, il nage
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navagentnew]
==== navAgentNew
[source,text]
----
agent = navAgentNew(nav, node, radius, height, speed)
----
Place sur le maillage un agent de cette taille, au point le plus proche de la position du nœud dans le monde (la position brute quand rien ne se trouve à moins de 2 unités sur les côtés et de 4 vers le haut ou vers le bas), marchant à `speed` unités par seconde et accélérant à 8 unités par seconde au carré. Le rayon et la hauteur sont maintenus à `0.01` ou plus, comme `navNew` le fait pour les siens. Dès lors, le moteur déplace le nœud le long du maillage à chaque image, dans le repère de son parent, et le fait tourner autour de Y pour qu'il regarde dans la direction où il va chaque fois qu'il se déplace à plus de `0.05` unité par seconde ; les nœuds regardent vers -Z, comme les modèles. La foule guide les agents les uns autour des autres, anticipe les virages et les tient à distance. Pour piloter un contrôleur de joueur sur le nœud au lieu de placer le nœud, voir `navAgentSetPlayer`. Met fin au script quand le maillage n'est pas construit ou que 128 agents s'y trouvent déjà.
*Paramètres :*
* `nav` -- un maillage construit venant de `navNew` ou de `navLoad`.
* `node` -- le nœud à déplacer ; en général la racine d'une instance de modèle.
* `radius` -- le rayon de l'agent pour se tenir à l'écart des autres, en unités du monde ; le rayon du maillage lui-même ou moins.
* `height` -- la hauteur de l'agent, en unités du monde.
* `speed` -- la vitesse de marche maximale, en unités du monde par seconde.
*Renvoie :* Le handle de l'agent, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<navagentmoveto,navAgentMoveTo>>, <<navagentsetplayer,navAgentSetPlayer>>, <<navagentdelete,navAgentDelete>>, <<navrandompoint,navRandomPoint>>
.Exemple
[source,lua]
----
for i = 1, 6 do
local fox = modelInstance(foxModel)
nodeSetScale(fox, 0.01)
nodeSetPosition(fox, navRandomPoint(nav))
animationPlay(fox, "Walk", true)
local agent = navAgentNew(nav, fox, 0.35, 1.0, 1.4)
navAgentMoveTo(agent, navRandomPoint(nav))
end
function onNavArrived(agent)
navAgentMoveTo(agent, navRandomPoint(nav))
end
----
[#navagentsetplayer]
==== navAgentSetPlayer
[source,text]
----
navAgentSetPlayer(agent, player)
----
Avec `true`, l'agent cesse de placer son nœud et transmet à la place sa vitesse, à chaque image, au contrôleur de joueur du nœud sous forme d'un `playerMove` (horizontal seulement), si bien que les escaliers, les pentes, les poussées et les collisions continuent de fonctionner ; avant chaque mise à jour de la foule, la position du contrôleur dans le monde est relue comme celle de l'agent, si bien qu'être bousculé hors de sa route est pris en compte par la planification. Le nœud se tourne toujours dans le sens de sa marche. Le nœud a besoin d'un contrôleur venant de `playerNew`, sinon rien ne bouge. Avec `false`, l'agent recommence à placer le nœud lui-même.
*Depuis :* 3.00.
*Voir aussi :* <<playernew,playerNew>>, <<playermove,playerMove>>, <<navagentnew,navAgentNew>>
.Exemple
[source,lua]
----
-- Un compagnon qui suit le héros à travers le niveau et peut être bousculé.
local companion = nodeNew()
nodeSetPosition(companion, 2, 0.05, 0)
playerNew(companion, 0.3, 1.0)
local companionFox = modelInstance(foxModel)
nodeSetParent(companionFox, companion)
nodeSetScale(companionFox, 0.01)
local companionAgent = navAgentNew(nav, companion, 0.3, 1.0, 2.0)
navAgentSetPlayer(companionAgent, true)
----
[#navagentstop]
==== navAgentStop
[source,text]
----
navAgentStop(agent)
----
Oublie la cible de l'agent : la foule cesse de le guider vers quoi que ce soit, il ralentit jusqu'à s'immobiliser là où il est, et `onNavArrived` n'est pas déclenché. Les autres agents le contournent toujours. Envoyez-le quelque part avec `navAgentMoveTo` pour qu'il reparte.
*Depuis :* 3.00.
*Voir aussi :* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
.Exemple
[source,lua]
----
-- Les gardes se figent quand l'alarme sonne et se tournent vers le bruit.
function raiseAlarm(x, y, z)
for _, guard in ipairs(guards) do
navAgentStop(guard.agent)
nodeLookAt(guard.node, x, y, z)
animationPlay(guard.node, "Survey", true, 1, 0.3)
end
soundPlay(alarm)
end
----
[#navbuild]
==== navBuild
[source,text]
----
navBuild(nav)
----
Précalcule le maillage praticable à partir de tout ce que `navAddNode` a ajouté : les sols et les pentes jusqu'à l'angle du maillage, les marches jusqu'à sa hauteur, le rayon de l'agent étant retranché autour de chaque mur et de chaque à-pic, et tout ce qui a moins que la hauteur de l'agent au-dessus de lui étant laissé de côté. Le précalcul travaille sur des cellules larges d'un tiers du rayon de l'agent et en couvre au plus 4096 sur 4096 : un petit rayon sur un grand niveau est donc lent et peut échouer ; un dixième de seconde pour Sponza sur un ordinateur de bureau, quelques secondes sur un Raspberry Pi. La géométrie ajoutée est libérée ensuite, et plus rien ne peut être ajouté ; précalculez plutôt un autre maillage. `navSave` conserve le résultat. Met fin au script quand rien n'a été ajouté, que le maillage est déjà construit ou que le précalcul échoue.
*Depuis :* 3.00.
*Voir aussi :* <<navnew,navNew>>, <<navaddnode,navAddNode>>, <<navsave,navSave>>, <<navdraw,navDraw>>
.Exemple
[source,lua]
----
-- Le précalcul a lieu une fois, pendant que l'écran titre est affiché, et il est conservé pour la fois suivante.
local sponza = modelInstance(modelLoad("Models/Sponza.glb"))
local nav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(nav, sponza)
navBuild(nav)
navSave(nav, "sponza.nav")
if debugging then
navDraw(nav) -- Une image, pour juger le précalcul à l'œil
end
----
[#navdelete]
==== navDelete
[source,text]
----
navDelete(nav)
----
Libère le maillage et chaque agent qui s'y trouve ; leurs nœuds restent où ils sont. Le handle du maillage et ceux des agents sont ensuite invalides et mettent fin au script s'ils sont utilisés. L'emplacement est libre pour un nouveau `navNew` ou `navLoad`.
*Depuis :* 3.00.
*Voir aussi :* <<navnew,navNew>>, <<navagentdelete,navAgentDelete>>
.Exemple
[source,lua]
----
function loadLevel(index)
if levelNav then
navDelete(levelNav) -- Emporte avec lui les agents de l'ancien niveau
end
nodeDelete(levelRoot)
levelRoot = modelInstance(levels[index].model)
levelNav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(levelNav, levelRoot)
navBuild(levelNav)
end
----
[#navdraw]
==== navDraw
[source,text]
----
navDraw(nav)
navDraw(nav, r, g, b)
----
Dessine les polygones du maillage précalculé sous forme de lignes par-dessus la scène pour cette image, en cyan sauf autre couleur, chaque polygone en éventail de triangles, si bien qu'un polygone de plus de trois côtés montre ses diagonales. Appelez-le à chaque image tant que vous regardez ; il coûte trois lignes par triangle, et un maillage qui n'est pas encore construit ne dessine rien. Pour vérifier qu'un précalcul couvre le sol et évite les meubles, avec `physicsSetDebug`.
*Paramètres :*
* `nav` -- le maillage.
* `r`, `g`, `b` -- la couleur des lignes, de `0` à `255` chacune ; par défaut `0, 255, 255`.
*Depuis :* 3.00.
*Voir aussi :* <<linedraw,lineDraw>>, <<navbuild,navBuild>>, <<physicssetdebug,physicsSetDebug>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
if showNavMesh then
navDraw(nav) -- En cyan par-dessus le sol
local path = navPath(nav, heroX, heroY, heroZ, goalX, goalY, goalZ)
if path then
for i = 2, #path do
lineDraw(path[i - 1][1], path[i - 1][2], path[i - 1][3], path[i][1], path[i][2], path[i][3], 255, 60, 40)
end
end
end
return OVERLAY_NOT_UPDATED
end
----
[#navload]
==== navLoad
[source,text]
----
nav = navLoad(name, agentRadius, agentHeight)
----
Charge un maillage écrit par `navSave`, cherché d'abord parmi les fichiers du jeu (si bien qu'un maillage précalculé peut être livré avec le jeu, empaqueté ou non) puis dans le dossier de données du jeu. Le maillage revient construit, prêt pour les agents et les requêtes, et n'accepte plus de `navAddNode`. La pente et la marche avec lesquelles il a été précalculé sont dans le fichier ; le rayon et la hauteur de l'agent sont ceux avec lesquels la foule guide les agents, donnez donc les valeurs pour lesquelles il a été précalculé. Met fin au script quand le fichier ne peut pas être lu, n'est pas un maillage de navigation, est plus court que le maillage que décrit son en-tête (tronqué, ou modifié à la main) ou qu'aucun emplacement de maillage n'est libre.
*Paramètres :*
* `name` -- le nom du fichier, relatif au dossier du jeu ou au dossier de données.
* `agentRadius` -- le rayon d'agent pour lequel le maillage a été précalculé, en unités du monde.
* `agentHeight` -- la hauteur d'agent pour laquelle il a été précalculé, en unités du monde.
*Renvoie :* Le handle du maillage, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<navsave,navSave>>, <<navnew,navNew>>
.Exemple
[source,lua]
----
-- Le précalcul livré pour le musée ; des agents larges de 0,8 et hauts de 1,7.
local museum = modelInstance(modelLoad("museum.glb"))
local nav = navLoad("museum.nav", 0.4, 1.7)
for _, visitor in ipairs(visitors) do
visitor.agent = navAgentNew(nav, visitor.node, 0.35, 1.7, 1.2)
navAgentMoveTo(visitor.agent, navRandomPoint(nav))
end
----
[#navnearest]
==== navNearest
[source,text]
----
x, y, z = navNearest(nav, x, y, z)
----
Le point du maillage le plus proche d'un point donné, cherché jusqu'à 2 unités sur les côtés et 4 vers le haut ou vers le bas, ou `nil` quand rien n'est aussi proche. Pour ramener sur le sol un point d'apparition, un point de chute ou un clic avant de le confier à un agent, et pour trouver la hauteur du sol sous un point.
*Paramètres :*
* `nav` -- le maillage.
* `x`, `y`, `z` -- le point à ramener, en unités du monde.
*Renvoie :* Trois nombres, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<navpath,navPath>>, <<navagentmoveto,navAgentMoveTo>>, <<sceneunproject,sceneUnproject>>
.Exemple
[source,lua]
----
-- Faire apparaître l'ennemi sur le sol le plus proche du générateur, ou pas du tout.
function spawnEnemy(spawner)
local sx, sy, sz = nodeGetWorldPosition(spawner)
local x, y, z = navNearest(nav, sx, sy, sz)
if x then
local enemy = modelInstance(enemyModel)
nodeSetPosition(enemy, x, y, z)
enemies[#enemies + 1] = { node = enemy, agent = navAgentNew(nav, enemy, 0.4, 1.8, 2.5) }
end
end
----
[#navnew]
==== navNew
[source,text]
----
nav = navNew(agentRadius, agentHeight, maxSlope, maxStep)
----
Commence un maillage de navigation pour des agents de ce rayon et de cette hauteur, capables de monter des marches jusqu'à `maxStep` unités et des pentes jusqu'à `maxSlope` degrés. Le rayon et la hauteur sont maintenus à `0.01` ou plus, la pente entre `0` et `89` et la marche à `0` ou plus. Le rayon fixe aussi la résolution du précalcul, une cellule en faisant le tiers : il décide donc du temps que prend `navBuild` et de la taille du niveau qu'il peut couvrir. Ajoutez la géométrie avec `navAddNode`, puis `navBuild`. Met fin au script quand huit maillages existent déjà.
*Paramètres :*
* `agentRadius` -- le rayon des agents, en unités du monde ; les murs sont élargis d'autant.
* `agentHeight` -- leur hauteur ; les plafonds plus bas ne sont pas praticables.
* `maxSlope` -- la pente praticable la plus raide, en degrés.
* `maxStep` -- la plus haute marche qu'ils montent, en unités du monde.
*Renvoie :* Le handle du maillage, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<navaddnode,navAddNode>>, <<navbuild,navBuild>>, <<navload,navLoad>>
.Exemple
[source,lua]
----
-- Sponza pour un agent large de 0,8 et haut de 1,6 qui monte 0,3 et marche sur des pentes jusqu'à 45 degrés.
local sponza = modelInstance(modelLoad("Models/Sponza.glb"))
local nav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(nav, sponza)
navBuild(nav)
----
[#navpath]
==== navPath
[source,text]
----
points = navPath(nav, x0, y0, z0, x1, y1, z1)
----
Les coins du plus court chemin à pied d'un point à un autre, tous deux d'abord ramenés sur le maillage, sous forme d'une table de tables `{ x, y, z }`, la première au départ et la dernière à l'arrivée ; jusqu'à 256 coins. Quand l'arrivée ne peut pas être atteinte, le chemin s'arrête au point accessible le plus proche en chemin, si bien que le dernier coin est toujours un endroit où un agent peut se tenir. `nil` quand l'une des extrémités est loin du maillage ou qu'aucun chemin n'est trouvé. Pour dessiner un itinéraire, mesurer à quelle distance se trouve quelque chose à pied, ou déplacer vous-même quelque chose le long du chemin ; les agents le font d'eux-mêmes.
*Paramètres :*
* `nav` -- le maillage.
* `x0`, `y0`, `z0` -- où le chemin commence.
* `x1`, `y1`, `z1` -- où il finit.
*Renvoie :* Une table de tables `{ x, y, z }`, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<navagentmoveto,navAgentMoveTo>>, <<navraycast,navRaycast>>, <<navnearest,navNearest>>
.Exemple
[source,lua]
----
-- La distance à pied jusqu'à la sortie, pour l'affichage des distances de la carte.
function walkingDistance(fromX, fromY, fromZ, toX, toY, toZ)
local path = navPath(nav, fromX, fromY, fromZ, toX, toY, toZ)
local total = 0
for i = 2, #(path or {}) do
local dx = path[i][1] - path[i - 1][1]
local dz = path[i][3] - path[i - 1][3]
total = total + math.sqrt(dx * dx + dz * dz)
end
return path and total or nil
end
----
[#navrandompoint]
==== navRandomPoint
[source,text]
----
x, y, z = navRandomPoint(nav)
----
Un point au hasard sur le maillage praticable, partout où il s'étend, pour errer, éparpiller des objets à ramasser ou placer un point d'apparition. `nil` seulement quand le maillage n'est pas construit.
*Renvoie :* Trois nombres, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<navagentmoveto,navAgentMoveTo>>, <<navnearest,navNearest>>
.Exemple
[source,lua]
----
-- Éparpiller vingt pièces sur le sol, chacune un peu au-dessus.
for i = 1, 20 do
local x, y, z = navRandomPoint(nav)
local coin = nodeNew()
nodeSetMesh(coin, coinMesh, gold)
nodeSetPosition(coin, x, y + 0.3, z)
coins[#coins + 1] = coin
end
----
[#navraycast]
==== navRaycast
[source,text]
----
blocked, x, y, z = navRaycast(nav, x0, y0, z0, x1, y1, z1)
----
Indique si une marche en ligne droite d'un point à un autre quitte le maillage, contre un mur ou à un à-pic, et à quel endroit. Le départ est ramené sur le maillage ; `false` seul signifie que la voie est libre, ou que le départ est loin du maillage. En cas de blocage, le point est celui où la ligne franchit le bord du maillage, sa hauteur étant interpolée entre les deux extrémités. Une vérification peu coûteuse de l'existence d'un raccourci avant de demander `navPath`.
*Paramètres :*
* `nav` -- le maillage.
* `x0`, `y0`, `z0` -- où la marche commence.
* `x1`, `y1`, `z1` -- où elle finirait.
*Renvoie :* `false`, ou `true` et le point où la marche quitte le maillage.
*Depuis :* 3.00.
*Voir aussi :* <<navpath,navPath>>, <<physicsraycast,physicsRaycast>>
.Exemple
[source,lua]
----
-- Une grenade lancée ne roule que jusqu'où le sol le lui permet.
function landingSpot(fromX, fromY, fromZ, toX, toY, toZ)
local blocked, hx, hy, hz = navRaycast(nav, fromX, fromY, fromZ, toX, toY, toZ)
if blocked then
return hx, hy, hz
end
return toX, toY, toZ
end
----
[#navsave]
==== navSave
[source,text]
----
navSave(nav, name)
----
Écrit le maillage précalculé dans le dossier de données du jeu sous le nom `name`, pour `navLoad` la fois suivante, ou pour le copier dans le dossier du jeu afin de le livrer. Le maillage doit être construit. Met fin au script quand il ne l'est pas, ou quand le fichier ne peut pas être écrit.
*Depuis :* 3.00.
*Voir aussi :* <<navload,navLoad>>, <<navbuild,navBuild>>
.Exemple
[source,lua]
----
-- Précalculé une fois sur cette machine, puis chargé depuis le dossier de données aux lancements suivants.
local nav = navNew(0.4, 1.6, 45, 0.3)
navAddNode(nav, level)
navBuild(nav)
navSave(nav, "level1.nav")
debugPrint("Navigation mesh baked and saved.")
----
[#net]
=== Réseau
Ce que le moteur ajoute sous `Singe/Net.singe`, le client HTTP du menu.
La plupart des jeux n'y touchent jamais : publier un score ou récupérer un
catalogue passe par `Net.singe`, qui l'appelle pour vous.
[#nettrustsystem]
==== netTrustSystem
[source,text]
----
lists, report = netTrustSystem(context)
----
Ajoute les certificats racines de confiance propres à la machine à un contexte
client LuaSec, en plus de ce que le contexte contient déjà (normalement le
`Singe/cacerts.pem` fourni). Une connexion établie avec ce contexte est alors
vérifiée dès que l'une ou l'autre des listes connaît la racine du serveur. La
liste de la machine est, dans l'ordre : le fichier ou le répertoire que désigne
`SSL_CERT_FILE` ou `SSL_CERT_DIR` quand ils sont définis ; sinon, le bundle de
la distribution sous Linux (le premier des six emplacements habituels qui
existe), le magasin système racine sous Windows, ou les ancres système sous
macOS. `cacerts.local.pem`, dans le dossier de données, est ajouté lui aussi
quand il existe ; le moteur ne l'écrit jamais.
`lists` est le nombre de listes qui ont ajouté quelque chose. `report` nomme ce
qui a été consulté, un `what path` par ligne (`what` vaut `system` ou `local`),
pour un message d'erreur sur lequel une personne peut agir.
*Depuis :* 3.00.
*Voir aussi :* <<utilsha256,utilSha256>>
.Exemple
[source,lua]
----
-- Un seul contexte pour toutes les requêtes : le bundle fourni, puis par-dessus les racines de cette machine.
local ssl = require("ssl")
local context = ssl.newcontext({ mode = "client", protocol = "any", verify = "peer", cafile = "Singe/cacerts.pem" })
local lists, report = netTrustSystem(context)
debugPrint("Trusting the shipped bundle and " .. lists .. " more:\n" .. report)
----
[#node]
=== Nœud
Chaque objet d'une scène est un nœud d'un seul et même arbre : le nœud `0` est
la racine, et tous les autres nœuds sont suspendus sous un parent. Un nœud porte
une position, une rotation et une échelle relatives à ce parent, et peut
dessiner un maillage avec un matériau, montrer un sprite ou une ligne de texte,
ou briller comme une lumière ; les enfants héritent de la transformation de tout
ce qui se trouve au-dessus d'eux. Les handles sont des entiers renvoyés par
`nodeNew`, `modelInstance` et `lightNew` ; un handle dont le nœud a été supprimé
met fin au script quand il est repassé, et un handle libéré peut être
réutilisé par un `nodeNew` ultérieur. Les positions et les tailles sont en
unités de scène et les rotations en degrés, avec +Y vers le haut et -Z vers
l'avant, comme le décrit <<scenes3d,Scènes 3D>>.
[#nodenew]
==== nodeNew
[source,text]
----
node = nodeNew()
node = nodeNew(parent)
----
Crée un nœud vide à l'origine de son parent, sans rotation, à l'échelle `1`,
visible, projetant des ombres, sans maillage ni matériau. Sans `parent`, le
nœud va sous la racine ; avec un parent, il devient le dernier enfant de ce
nœud et se déplace désormais avec lui. Les nœuds vides servent de pivots et de
groupes : placez-en un, suspendez-y des maillages, des lumières ou une caméra,
puis déplacez le groupe d'un seul bloc.
*Renvoie :* Le handle du nœud.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetmesh,nodeSetMesh>>, <<nodesetparent,nodeSetParent>>, <<nodedelete,nodeDelete>>, <<lightnew,lightNew>>
.Exemple
[source,lua]
----
-- Une tourelle dont le canon pivote sur la tourelle, qui tourne sur la base.
local steel = materialNew()
materialSetColor(steel, 140, 145, 150)
local base = nodeNew()
local turret = nodeNew(base)
local barrel = nodeNew(turret)
nodeSetMesh(base, meshCylinder(1, 0.3), steel)
nodeSetMesh(turret, meshSphere(0.6), steel)
nodeSetMesh(barrel, meshCylinder(0.1, 1.2), steel)
nodeSetPosition(barrel, 0, 0, -0.8)
nodeSetRotation(barrel, 90, 0, 0)
----
[#nodedelete]
==== nodeDelete
[source,text]
----
nodeDelete(node)
----
Supprime le nœud et tout ce qui se trouve sous lui, en libérant chaque handle
de ce sous-arbre ; un sprite ou un texte posé sur l'un d'eux est libéré aussi.
Les maillages et les matériaux que dessinaient les nœuds ne sont pas touchés,
puisque d'autres nœuds peuvent les partager. Si le nœud supprimé était la
caméra, la scène revient à la vue par défaut. Le nœud racine ne peut pas être
supprimé ; le demander met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<nodenew,nodeNew>>, <<nodesetvisible,nodeSetVisible>>, <<meshdelete,meshDelete>>
.Exemple
[source,lua]
----
-- Les tirs vivent deux secondes, puis leurs nœuds disparaissent.
function fireShot(ship)
local shot = nodeNew()
nodeSetMesh(shot, shotMesh, tracer)
nodeSetPosition(shot, nodeGetWorldPosition(ship))
shots[#shots + 1] = { node = shot, born = os.clock() }
end
function reapShots()
for i = #shots, 1, -1 do
if os.clock() - shots[i].born > 2 then
nodeDelete(shots[i].node)
table.remove(shots, i)
end
end
end
----
[#nodefind]
==== nodeFind
[source,text]
----
node = nodeFind(name)
node = nodeFind(name, root)
----
Cherche, en profondeur d'abord, le premier nœud nommé `name`, en partant de
`root` (la racine de la scène quand il est omis) et en examinant `root`
lui-même avant ses enfants, puis le sous-arbre de chaque enfant à son tour.
Renvoie `nil` quand rien ne correspond. Les nœuds d'un modèle portent les noms
de leur fichier glTF, si bien qu'un os, un maillage ou un objet vide servant de
repère placé dans Blender peut être trouvé par ce nom sous la racine de
l'instance. Les noms donnés avec `nodeSetName` se trouvent de la même façon.
*Renvoie :* Un handle de nœud, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetname,nodeSetName>>, <<nodegetname,nodeGetName>>, <<modelinstance,modelInstance>>, <<nodegetchildren,nodeGetChildren>>
.Exemple
[source,lua]
----
-- Accrocher une torche à la main droite du héros, trouvée par le nom de son os.
local hero = modelInstance(heroModel)
local hand = nodeFind("hand_R", hero)
if hand then
local torch = modelInstance(torchModel)
nodeSetParent(torch, hand)
nodeSetPosition(torch, 0, 0.05, 0)
nodeSetRotation(torch, 0, 0, -90)
end
----
[#nodegetchildren]
==== nodeGetChildren
[source,text]
----
children = nodeGetChildren(node)
----
Les enfants directs du nœud sous forme d'une table de handles, dans l'ordre où
ils ont été attachés ; changer le parent d'un nœud le place à la fin de la
liste de son nouveau parent. Les petits-enfants ne sont pas inclus ; procédez
récursivement pour parcourir un sous-arbre entier. Une table vide signifie que
le nœud n'a pas d'enfants.
*Renvoie :* Une table de handles de nœuds.
*Depuis :* 3.00.
*Voir aussi :* <<nodegetparent,nodeGetParent>>, <<nodefind,nodeFind>>, <<nodesetparent,nodeSetParent>>
.Exemple
[source,lua]
----
-- Donner à chaque partie d'un accessoire chargé le même matériau usé.
local function paint(node, material)
nodeSetMaterial(node, material)
for _, child in ipairs(nodeGetChildren(node)) do
paint(child, material)
end
end
local crate = modelInstance(crateModel)
paint(crate, wornWood)
----
[#nodegetmorph]
==== nodeGetMorph
[source,text]
----
weight = nodeGetMorph(node, nameOrIndex)
----
Le poids actuel d'une cible de morphing du maillage du nœud, désignée par son
nom ou par son numéro à partir de `1`, dans l'ordre où `nodeGetMorphs` les
liste. Le nœud est celui qui porte le maillage, pas la racine d'une instance de
modèle. Un nom ou un numéro que le maillage n'a pas met fin au script.
*Renvoie :* Un nombre, `0` tant que rien n'a réglé la cible.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorphs,nodeGetMorphs>>
.Exemple
[source,lua]
----
-- Faire venir le sourire en douceur sur plusieurs images au lieu de l'afficher d'un coup.
function onOverlayUpdate()
local now = nodeGetMorph(face, "smile")
local target = happy and 1 or 0
nodeSetMorph(face, "smile", now + (target - now) * 0.1)
return OVERLAY_UPDATED
end
----
[#nodegetmorphs]
==== nodeGetMorphs
[source,text]
----
names = nodeGetMorphs(node)
----
Les cibles de morphing du maillage du nœud sous forme d'une table de noms, dans
l'ordre où le fichier les stocke ; cet ordre est la numérotation qu'acceptent
`nodeSetMorph` et `nodeGetMorph`. Les exportateurs qui n'écrivent pas de noms
donnent des chaînes vides, et ces cibles peuvent toujours être réglées par
numéro. Un nœud sans maillage, ou un maillage sans cibles, donne une table vide.
*Renvoie :* Une table de chaînes.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetmorph,nodeSetMorph>>, <<nodegetmorph,nodeGetMorph>>, <<modelload,modelLoad>>
.Exemple
[source,lua]
----
-- Associer une fois les noms des cibles du visage à des numéros, pour que la synchronisation labiale puisse les indexer.
local head = nodeFind("Head", hero)
local visemes = {}
for index, name in ipairs(nodeGetMorphs(head)) do
if name ~= "" then
visemes[name] = index
end
end
debugPrint(string.format("%d morph targets on the head", #nodeGetMorphs(head)))
----
[#nodegetname]
==== nodeGetName
[source,text]
----
name = nodeGetName(node)
----
Le nom du nœud : celui que lui a donné `nodeSetName`, ou son nom glTF pour un
nœud de modèle, une chaîne vide quand il n'a ni l'un ni l'autre. Le nœud racine
s'appelle `root`.
*Renvoie :* Une chaîne.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetname,nodeSetName>>, <<nodefind,nodeFind>>
.Exemple
[source,lua]
----
-- Afficher l'arbre des nœuds d'un modèle dans la console pour trouver les noms des os.
local function dump(node, depth)
debugPrint(string.rep(" ", depth) .. nodeGetName(node) .. " (" .. node .. ")")
for _, child in ipairs(nodeGetChildren(node)) do
dump(child, depth + 1)
end
end
dump(modelInstance(heroModel), 0)
----
[#nodegetparent]
==== nodeGetParent
[source,text]
----
parent = nodeGetParent(node)
----
Le handle du parent du nœud, ou `nil` pour la racine. Tout autre nœud a un
parent, ne serait-ce que la racine.
*Renvoie :* Un handle de nœud, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetparent,nodeSetParent>>, <<nodegetchildren,nodeGetChildren>>
.Exemple
[source,lua]
----
-- Remonter d'un nœud de maillage choisi jusqu'au sommet de son instance de modèle.
local function instanceRoot(node)
local parent = nodeGetParent(node)
while parent and parent ~= 0 do
node = parent
parent = nodeGetParent(node)
end
return node
end
----
[#nodegetposition]
==== nodeGetPosition
[source,text]
----
x, y, z = nodeGetPosition(node)
----
La position du nœud relative à son parent, telle que l'ont réglée en dernier
`nodeSetPosition`, `nodeMove` ou le moteur (un corps, un véhicule ou une
animation qui le déplace). Pour savoir où se trouve le nœud dans le monde,
utilisez `nodeGetWorldPosition`.
*Renvoie :* Trois nombres.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetposition,nodeSetPosition>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<nodemove,nodeMove>>
.Exemple
[source,lua]
----
-- La caméra suit la voiture, trois unités au-dessus, trois à droite et neuf derrière.
function onOverlayUpdate()
local cx, cy, cz = nodeGetPosition(car)
nodeSetPosition(camera, cx + 3, cy + 3, cz + 9)
nodeLookAt(camera, cx, cy, cz - 3)
return OVERLAY_UPDATED
end
----
[#nodegetquaternion]
==== nodeGetQuaternion
[source,text]
----
x, y, z, w = nodeGetQuaternion(node)
----
La rotation du nœud relative à son parent sous forme de quaternion unitaire, la
forme que stockent les modèles et les animations. Servez-vous-en pour copier une
rotation d'un nœud à l'autre ou pour garder une pose sans l'ambiguïté des
angles d'Euler.
*Renvoie :* Quatre nombres.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetquaternion,nodeSetQuaternion>>, <<nodegetrotation,nodeGetRotation>>
.Exemple
[source,lua]
----
-- Retenir la pose de la porte avant le test du ragdoll, puis la remettre.
local px, py, pz = nodeGetPosition(door)
local qx, qy, qz, qw = nodeGetQuaternion(door)
function resetDoor()
nodeSetPosition(door, px, py, pz)
nodeSetQuaternion(door, qx, qy, qz, qw)
end
----
[#nodegetrotation]
==== nodeGetRotation
[source,text]
----
x, y, z = nodeGetRotation(node)
----
La rotation du nœud relative à son parent sous forme d'angles d'Euler en
degrés, décomposée de la même façon que `nodeSetRotation` la compose : tournée
autour de Y, puis inclinée autour de X, puis mise en roulis autour de Z. Les
angles décrivent la même rotation que celle qui a été réglée, mais pas
forcément avec les mêmes nombres : `X` revient entre `-90` et `90`, et quand il
atteint `90` ou `-90`, la torsion est donnée à `Y` et `Z` est renvoyé à `0`.
*Renvoie :* Trois nombres.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
.Exemple
[source,lua]
----
-- Une boussole dans l'overlay, d'après le cap du vaisseau.
function onOverlayUpdate()
local _, heading = nodeGetRotation(ship)
overlayClear()
fontPrint(20, 20, string.format("Heading %03d", (360 - heading) % 360))
return OVERLAY_UPDATED
end
----
[#nodegetscale]
==== nodeGetScale
[source,text]
----
x, y, z = nodeGetScale(node)
----
L'échelle propre du nœud sur chaque axe, telle que l'a réglée `nodeSetScale`
(`1, 1, 1` pour un nouveau nœud). Les échelles des parents n'y sont pas
intégrées.
*Renvoie :* Trois nombres.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetscale,nodeSetScale>>
.Exemple
[source,lua]
----
-- Faire grandir le bonus jusqu'à sa taille normale, un peu à chaque image.
function onOverlayUpdate()
local size = nodeGetScale(powerUp)
if size < 1 then
nodeSetScale(powerUp, math.min(1, size + 0.05))
end
return OVERLAY_UPDATED
end
----
[#nodegetworldposition]
==== nodeGetWorldPosition
[source,text]
----
x, y, z = nodeGetWorldPosition(node)
----
L'endroit où se trouve l'origine du nœud dans l'espace du monde, avec la
transformation de chaque parent appliquée, tel qu'à la dernière image rendue.
Les changements faits à cette image (les vôtres, ou ceux d'un corps) se voient
après le rendu suivant, et un nœud créé à cette image renvoie `0, 0, 0`
jusque-là. C'est la position à donner à `sceneProject` ou à un test de
distance.
*Renvoie :* Trois nombres.
*Depuis :* 3.00.
*Voir aussi :* <<sceneproject,sceneProject>>, <<nodelookat,nodeLookAt>>, <<nodegetposition,nodeGetPosition>>
.Exemple
[source,lua]
----
-- Une étiquette de nom dans l'overlay au-dessus du canard, où que la physique l'ait emmené.
function onOverlayUpdate()
local dx, dy, dz = nodeGetWorldPosition(duck)
local sx, sy = sceneProject(dx, dy + 0.8, dz)
overlayClear()
if sx then
fontPrint(sx - 20, sy, "DUCK")
end
return OVERLAY_UPDATED
end
----
[#nodelookat]
==== nodeLookAt
[source,text]
----
nodeLookAt(node, x, y, z)
----
Tourne le nœud pour que son propre -Z pointe vers une position du monde, en
gardant son +Y aussi proche du haut du monde que la direction le permet. C'est
ce que veulent les caméras et les lumières, et cela convient aussi à une
tourelle ou à une tête qui suit une cible. Seule la rotation du nœud change ;
sa position reste. Si la cible coïncide avec la position du nœud, rien ne
change.
*Remarques :* Le point du monde est converti dans l'espace du parent à l'aide
de la transformation du parent à la dernière image rendue ; un nœud dont le
parent a bougé à cette même image vise depuis l'endroit où se trouvait le
parent.
*Depuis :* 3.00.
*Voir aussi :* <<cameraset,cameraSet>>, <<lightnew,lightNew>>, <<nodesetrotation,nodeSetRotation>>
.Exemple
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, -8, 3, 6)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
local camera = nodeNew()
nodeSetPosition(camera, 0, 2.2, 7)
nodeLookAt(camera, 0, 1.3, 0)
cameraSet(camera)
----
[#nodemove]
==== nodeMove
[source,text]
----
nodeMove(node, dx, dy, dz)
----
Déplace le nœud selon ses propres axes : le décalage est tourné par la rotation
actuelle du nœud et ajouté à sa position, si bien que
`nodeMove(ship, 0, 0, -0.1)` fait avancer le vaisseau dans la direction où il
pointe, quelle qu'elle soit, et que `nodeMove(ship, 0.1, 0, 0)` le fait glisser
latéralement vers la droite. Utilisez `nodeSetPosition` pour un emplacement
absolu dans l'espace du parent.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetposition,nodeSetPosition>>, <<noderotate,nodeRotate>>
.Exemple
[source,lua]
----
-- Commandes de char : tourner avec gauche et droite, avancer avec haut et bas.
function onOverlayUpdate()
if keyboardIsDown(SCANCODE_LEFT) then nodeRotate(tank, 0, 2, 0) end
if keyboardIsDown(SCANCODE_RIGHT) then nodeRotate(tank, 0, -2, 0) end
if keyboardIsDown(SCANCODE_UP) then nodeMove(tank, 0, 0, -0.08) end
if keyboardIsDown(SCANCODE_DOWN) then nodeMove(tank, 0, 0, 0.04) end
return OVERLAY_UPDATED
end
----
[#noderotate]
==== nodeRotate
[source,text]
----
nodeRotate(node, dx, dy, dz)
----
Tourne le nœud du nombre de degrés donné autour de ses propres axes, en plus de
sa rotation actuelle : `nodeRotate(wheel, 5, 0, 0)` à chaque image fait tourner
la roue sur son propre X, quelle que soit l'inclinaison de l'essieu. Les trois
angles sont combinés comme `nodeSetRotation` les combine (Y, puis X, puis Z)
avant d'être appliqués. Les angles positifs tournent dans le sens inverse des
aiguilles d'une montre quand on regarde le long de l'axe vers l'origine.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetrotation,nodeSetRotation>>, <<nodemove,nodeMove>>, <<nodesetquaternion,nodeSetQuaternion>>
.Exemple
[source,lua]
----
-- Le logo tourne d'un demi-degré par image autour de son propre axe vertical.
local logo = modelInstance(logoModel)
nodeSetPosition(logo, 0, -2.5, 0)
nodeSetRotation(logo, 0, -30, 0)
function onOverlayUpdate()
nodeRotate(logo, 0, 0.5, 0)
return OVERLAY_UPDATED
end
----
[#nodesetbillboard]
==== nodeSetBillboard
[source,text]
----
nodeSetBillboard(node, mode)
----
Tourne le nœud face à la caméra à chaque image. `BILLBOARD_ALL` lui fait face
de plein front, en prenant les trois axes de la caméra ; `BILLBOARD_Y` tourne
autour du haut du monde seulement, si bien que le nœud reste debout (arbres,
barres de vie) ; `BILLBOARD_NONE`, la valeur par défaut, ne tourne pas du tout.
La position et l'échelle du nœud dans le monde sont conservées, et sa propre
rotation est ignorée tant qu'un mode est réglé. Toute autre valeur met fin
au script. Cela fonctionne sur les maillages aussi bien que sur les sprites et
le texte, et se tourne vers la caméra qui dessine, quelle qu'elle soit : une vue
voit elle aussi le billboard de face, tandis que son ombre suit la caméra de la
fenêtre.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetsprite,nodeSetSprite>>, <<nodesettext,nodeSetText>>, <<cameraset,cameraSet>>
.Exemple
[source,lua]
----
-- Un nom au-dessus du renard, face à la caméra de plein front, et une icône de caisse
-- qui ne tourne qu'autour de son propre axe vertical.
local tag = nodeNew()
nodeSetParent(tag, fox)
nodeSetPosition(tag, 0, 110, 0)
nodeSetScale(tag, 50)
nodeSetText(tag, "FOX", 0.6)
nodeSetBillboard(tag, BILLBOARD_ALL)
nodeSetShadow(tag, false)
local icon = nodeNew()
nodeSetParent(icon, fox)
nodeSetPosition(icon, 0, 150, 0)
nodeSetScale(icon, 50)
nodeSetSprite(icon, spriteLoad("crate.png"), 0.5)
nodeSetBillboard(icon, BILLBOARD_Y)
----
[#nodesetlayer]
==== nodeSetLayer
[source,text]
----
nodeSetLayer(node, layer)
----
Choisit où, dans la fenêtre, le nœud est dessiné. `LAYER_SCENE`, la valeur par
défaut, le dessine dans la scène sous l'overlay ; `LAYER_FRONT` le dessine après
l'overlay, par la même caméra avec les mêmes lumières, dans une couche
transparente qui lui est propre. Un nœud de la couche avant emmène ses enfants
avec lui, quel que soit leur propre réglage. Toute autre valeur met fin au
script. Voir <<scenefront,Devant l'overlay>>.
*Depuis :* 3.00.
*Voir aussi :* <<sceneenable,sceneEnable>>, <<nodesetvisible,nodeSetVisible>>, <<emittersetlayer,emitterSetLayer>>
.Exemple
[source,lua]
----
-- Le vaisseau du joueur se tient sur l'écran de carte 2D tant qu'il est ouvert, et
-- retourne dans le monde quand il se ferme.
function showMap(open)
nodeSetLayer(ship, open and LAYER_FRONT or LAYER_SCENE)
end
----
[#nodesetmaterial]
==== nodeSetMaterial
[source,text]
----
nodeSetMaterial(node, material)
nodeSetMaterial(node, nil)
----
Pose un matériau sur le maillage du nœud, quel qu'il soit, en gardant le
maillage. `nil` le retire, ce qui laisse l'aspect par défaut : blanc, à moitié
rugueux, sans texture. Servez-vous-en pour changer le style d'une partie d'un
modèle, pour mettre et retirer un matériau de surbrillance, ou pour habiller un
maillage que le moteur a fait pour vous, comme le tube d'une corde venant de
`softNew`. Un handle de matériau qui n'existe pas met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetmesh,nodeSetMesh>>, <<materialnew,materialNew>>, <<softnew,softNew>>
.Exemple
[source,lua]
----
-- La corde pend de la grue ; le moteur fait son tube, nous l'habillons.
local hemp = materialNew()
materialSetColor(hemp, 170, 140, 90)
materialSetRoughness(hemp, 1)
local rope = nodeNew()
nodeSetPosition(rope, 4, 4, -1)
softNew(rope, SOFT_ROPE, 6.5, 4, -1, 16, 0.04)
nodeSetMaterial(rope, hemp)
softSetMass(rope, 1.5)
softPin(rope, 4, 4, -1, crane)
----
[#nodesetmesh]
==== nodeSetMesh
[source,text]
----
nodeSetMesh(node, mesh)
nodeSetMesh(node, mesh, material)
----
Donne au nœud un maillage à dessiner, avec un matériau ou, en son absence,
l'aspect par défaut (blanc, à moitié rugueux). Les deux arguments doivent être
des handles valides ; un maillage ou un matériau manquant met fin au script.
Tout nœud peut dessiner, y compris un nœud à l'intérieur d'une instance de
modèle, et un nombre quelconque de nœuds peuvent partager un même maillage. Les
poids de morphing du nœud sont dimensionnés selon les cibles du nouveau
maillage et commencent à `0`.
*Depuis :* 3.00.
*Voir aussi :* <<meshbox,meshBox>>, <<materialnew,materialNew>>, <<nodesetmaterial,nodeSetMaterial>>, <<nodesetvisible,nodeSetVisible>>
.Exemple
[source,lua]
----
-- Une carrosserie avec quatre roues, chacune un cylindre tourné pour que son X soit l'essieu.
local car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 0.9, 0)
local tireMesh = meshCylinder(0.35, 0.25, 16)
for _, p in ipairs({ { -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, { -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 } }) do
local wheel = nodeNew(car)
nodeSetPosition(wheel, p[1], p[2], p[3])
local tire = nodeNew(wheel)
nodeSetMesh(tire, tireMesh, rubber)
nodeSetRotation(tire, 0, 0, 90)
end
----
[#nodesetmorph]
==== nodeSetMorph
[source,text]
----
nodeSetMorph(node, nameOrIndex, weight)
----
Règle dans quelle mesure le maillage du nœud montre une cible de morphing
(blend shape), désignée par son nom ou par son numéro à partir de `1` ; les
poids vont en général de `0` à `1` et plusieurs cibles s'additionnent. Le nœud
est celui qui porte le maillage, c'est-à-dire, pour un modèle, un enfant de la
racine de l'instance (`nodeFind` par le nom du maillage, ou `nodeGetChildren`).
Une cible que le maillage n'a pas met fin au script. Une animation dotée
d'un canal de poids écrit les poids à chaque image tant qu'elle joue, ce qui
écrase ce que vous avez réglé.
*Remarques :* Jusqu'à huit cibles de poids non nul s'appliquent par maillage
et par image ; au-delà, ce sont les huit plus fortes en valeur absolue qui sont
utilisées.
*Depuis :* 3.00.
*Voir aussi :* <<nodegetmorph,nodeGetMorph>>, <<nodegetmorphs,nodeGetMorphs>>, <<animationplay,animationPlay>>
.Exemple
[source,lua]
----
-- Synchronisation labiale d'après une table d'ouvertures de bouche par image du disque.
local face = nodeFind("Head", hero)
function onOverlayUpdate()
nodeSetMorph(face, "mouthOpen", mouthByFrame[discGetFrame()] or 0)
return OVERLAY_UPDATED
end
----
[#nodesetname]
==== nodeSetName
[source,text]
----
nodeSetName(node, name)
----
Nomme le nœud pour que `nodeFind` puisse le trouver plus tard, en remplaçant le
nom qu'il avait (y compris le nom glTF d'un nœud de modèle). Les noms n'ont pas
besoin d'être uniques ; `nodeFind` renvoie la première correspondance dans
l'ordre en profondeur d'abord.
*Depuis :* 3.00.
*Voir aussi :* <<nodefind,nodeFind>>, <<nodegetname,nodeGetName>>
.Exemple
[source,lua]
----
-- Les caisses créées sont nommées pour que le script du niveau puisse les trouver par emplacement.
for slot = 1, 6 do
local crate = nodeNew()
nodeSetMesh(crate, crateMesh, wood)
nodeSetPosition(crate, slot * 1.5 - 5, 0.5, -3)
nodeSetName(crate, "crate" .. slot)
end
nodeSetVisible(nodeFind("crate4"), false)
----
[#nodesetparent]
==== nodeSetParent
[source,text]
----
nodeSetParent(node, parent)
----
Déplace le nœud, avec tout son sous-arbre, sous un autre parent, dont il
devient le dernier enfant. La position, la rotation et l'échelle locales du
nœud sont gardées telles quelles, si bien qu'il se trouve maintenant à ce
décalage du nouveau parent et se déplace avec lui ; passez `0` pour le ramener
à la racine. La racine ne peut pas être déplacée, et un nœud ne peut aller ni
sous lui-même ni sous l'un de ses propres descendants ; l'un comme l'autre
met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<nodegetparent,nodeGetParent>>, <<nodenew,nodeNew>>, <<nodesetposition,nodeSetPosition>>
.Exemple
[source,lua]
----
-- Ramasser une caisse : elle voyage dans la main à partir de maintenant, elle sera posée plus tard.
function pickUp(crate)
nodeSetParent(crate, nodeFind("hand_R", hero))
nodeSetPosition(crate, 0, 0.3, 0)
nodeSetRotation(crate, 0, 0, 0)
end
function putDown(crate, x, y, z)
nodeSetParent(crate, 0)
nodeSetPosition(crate, x, y, z)
end
----
[#nodesetposition]
==== nodeSetPosition
[source,text]
----
nodeSetPosition(node, x, y, z)
----
Place le nœud en `x, y, z` dans l'espace de son parent ; pour un nœud sous la
racine, c'est l'espace du monde. Les enfants suivent. Un nœud portant un corps
dynamique est ensuite déplacé par la physique ; réglez sa position avant
`bodyNew`, ou téléportez-le au moyen des appels de corps.
*Depuis :* 3.00.
*Voir aussi :* <<nodemove,nodeMove>>, <<nodegetposition,nodeGetPosition>>, <<nodesetrotation,nodeSetRotation>>
.Exemple
[source,lua]
----
-- Des caisses lâchées le long d'une crête, chacune six unités au-dessus du sol à cet endroit.
for i = 1, 10 do
local crate = nodeNew()
local x, z = -20 + i * 4, -12
nodeSetMesh(crate, meshBox(1, 1, 1), wood)
nodeSetPosition(crate, x, (terrainGetHeight(hills, x, z) or 0) + 6, z)
nodeSetRotation(crate, i * 17, i * 31, 0)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 1, 1, 1)
end
----
[#nodesetquaternion]
==== nodeSetQuaternion
[source,text]
----
nodeSetQuaternion(node, x, y, z, w)
----
Règle la rotation du nœud relative à son parent sous forme de quaternion, pour
des valeurs venant de `nodeGetQuaternion`, d'un modèle ou de vos propres
calculs. Le quaternion est normalisé à l'entrée, si bien qu'il n'a pas besoin
d'être de longueur unité.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetrotation,nodeSetRotation>>, <<nodegetquaternion,nodeGetQuaternion>>
.Exemple
[source,lua]
----
-- Le fantôme copie exactement la pose du héros, avec un temps de retard.
local qx, qy, qz, qw = nodeGetQuaternion(hero)
local px, py, pz = nodeGetPosition(hero)
history[#history + 1] = { px, py, pz, qx, qy, qz, qw }
if #history > 30 then
local old = table.remove(history, 1)
nodeSetPosition(ghost, old[1], old[2], old[3])
nodeSetQuaternion(ghost, old[4], old[5], old[6], old[7])
end
----
[#nodesetrotation]
==== nodeSetRotation
[source,text]
----
nodeSetRotation(node, x, y, z)
----
Règle la rotation du nœud relative à son parent sous forme d'angles d'Euler en
degrés, en remplaçant celle qu'il avait : le nœud est d'abord tourné autour de
son Y (lacet), puis incliné autour de son X (tangage), puis mis en roulis
autour de son Z. Les angles positifs tournent dans le sens inverse des
aiguilles d'une montre quand on regarde le long de l'axe vers l'origine, si
bien que `nodeSetRotation(plane, 90, 0, 0)` relève un plan de sol pour qu'il
fasse face à +Z.
*Depuis :* 3.00.
*Voir aussi :* <<noderotate,nodeRotate>>, <<nodesetquaternion,nodeSetQuaternion>>, <<nodelookat,nodeLookAt>>
.Exemple
[source,lua]
----
-- Un écran de moniteur posé sur un pied, face à la caméra en +Z.
local monitor = nodeNew()
nodeSetMesh(monitor, meshPlane(2.0, 1.5), screen)
nodeSetPosition(monitor, -3, 2.1, 1.06)
nodeSetRotation(monitor, 90, 0, 0)
-- Une rampe inclinée de 25 degrés autour de Z.
local ramp = nodeNew()
nodeSetMesh(ramp, meshBox(6, 0.2, 3), asphalt)
nodeSetRotation(ramp, 0, 0, 25)
----
[#nodesetscale]
==== nodeSetScale
[source,text]
----
nodeSetScale(node, scale)
nodeSetScale(node, x, y, z)
----
Met à l'échelle le nœud et tout ce qui se trouve sous lui, uniformément avec un
seul facteur ou par axe avec trois. Les modèles chargés sont souvent en
centimètres ou en mètres ; mettez une fois à l'échelle la racine de l'instance
pour l'adapter à votre scène, et placez les choses sous elle dans les unités
propres au modèle. Les sprites et le texte du nœud sont mis à l'échelle eux
aussi, en plus de leur `height`.
*Depuis :* 3.00.
*Voir aussi :* <<nodegetscale,nodeGetScale>>, <<modelinstance,modelInstance>>
.Exemple
[source,lua]
----
-- Le renard est modélisé en centimètres : un centième le ramène en mètres.
local fox = modelInstance(foxModel)
nodeSetParent(fox, player)
nodeSetScale(fox, 0.01)
nodeSetRotation(fox, 0, 90, 0)
-- Une sphère aplatie pour une flaque.
local puddle = nodeNew()
nodeSetMesh(puddle, meshSphere(0.5, 24), water)
nodeSetScale(puddle, 1.5, 0.05, 1.2)
----
[#nodesetshadow]
==== nodeSetShadow
[source,text]
----
nodeSetShadow(node, casts)
----
Indique si le maillage du nœud est dessiné dans les cartes d'ombres ; `true`
pour tout nouveau nœud. Désactivez-le pour le maillage qui entoure une lumière
(une ampoule, une flamme), afin qu'il ne fasse pas d'ombre à sa propre lumière,
et pour tout ce qui brille. Le nœud se dessine toujours et reçoit toujours les
ombres des autres. Cela ne s'applique qu'au nœud, pas à ses enfants.
*Depuis :* 3.00.
*Voir aussi :* <<lightsetshadow,lightSetShadow>>, <<nodesetvisible,nodeSetVisible>>, <<materialsetunlit,materialSetUnlit>>
.Exemple
[source,lua]
----
-- Une ampoule dans son abat-jour : le verre ne projette rien, l'abat-jour, si.
local shade = nodeNew(stand)
nodeSetMesh(shade, meshCone(0.45, 0.5, 24), brass)
nodeSetPosition(shade, 0, 0.9, 0)
local bulbMesh = nodeNew(stand)
nodeSetMesh(bulbMesh, meshSphere(0.08, 12), glow)
nodeSetPosition(bulbMesh, 0, 0.8, 0)
nodeSetShadow(bulbMesh, false)
local bulb = lightNew(LIGHT_POINT)
nodeSetParent(bulb, bulbMesh)
lightSetShadow(bulb, true)
----
[#nodesetsprite]
==== nodeSetSprite
[source,text]
----
nodeSetSprite(node, sprite)
nodeSetSprite(node, sprite, height)
nodeSetSprite(node, sprite, height, lit)
nodeSetSprite(node, nil)
nodeSetSprite(node)
----
Montre un sprite chargé sur le nœud : un quad haut de `height` unités du monde
(`1` par défaut) et aussi large que le format de l'image le veut, mélangé selon
l'alpha de l'image, dessiné des deux côtés, et éclairé par la scène sauf si
`lit` vaut `false`. Chaque image d'un sprite animé est envoyée et la première
s'affiche ; `nodeSetSpriteFrame` en choisit une autre. Le quad devient le
maillage du nœud, avec un matériau privé, et remplace tout maillage, sprite ou
texte qu'il avait ; `nil` ou l'absence de second argument retire l'image et
laisse le nœud vide. Met fin au script quand le moteur de rendu 3D n'est pas
disponible.
*Paramètres :*
* `sprite` -- un handle venant de `spriteLoad`, ou `nil` pour effacer.
* `height` -- la hauteur du quad en unités du monde, `1` par défaut ; l'échelle du nœud la multiplie.
* `lit` -- `true` (par défaut) applique à l'image les lumières et les ombres de la scène ; `false` la montre telle quelle.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetspriteframe,nodeSetSpriteFrame>>, <<nodesetbillboard,nodeSetBillboard>>, <<spriteload,spriteLoad>>, <<nodesettext,nodeSetText>>
.Exemple
[source,lua]
----
-- Une icône de caisse qui flotte au-dessus du renard et se tourne face au joueur.
local icon = nodeNew()
nodeSetParent(icon, fox)
nodeSetPosition(icon, 0, 150, 0)
nodeSetScale(icon, 50)
nodeSetSprite(icon, spriteLoad("crate.png"), 0.5)
nodeSetBillboard(icon, BILLBOARD_Y)
-- Une lueur non éclairée qui se lit de la même façon dans l'ombre.
local flare = nodeNew(torchTip)
nodeSetSprite(flare, spriteLoad("flare.png"), 0.3, false)
nodeSetBillboard(flare, BILLBOARD_ALL)
----
[#nodesetspriteframe]
==== nodeSetSpriteFrame
[source,text]
----
nodeSetSpriteFrame(node, frame)
----
Quelle image du sprite animé du nœud s'affiche, comptée à partir de `0`. Le
sprite du nœud n'avance pas tout seul ; pilotez-le depuis `onOverlayUpdate`. Un
nœud sans sprite, ou une image qu'il n'a pas, met fin au script ; un texte
et un sprite fixe n'ont que l'image `0`.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetsprite,nodeSetSprite>>, <<spriteload,spriteLoad>>
.Exemple
[source,lua]
----
-- Un sprite de feu de camp qui fait défiler ses images à dix par seconde.
local fire = nodeNew()
nodeSetPosition(fire, 2, 0.4, -1)
nodeSetSprite(fire, spriteLoad("campfire.gif"), 0.8, false)
nodeSetBillboard(fire, BILLBOARD_Y)
function onOverlayUpdate()
nodeSetSpriteFrame(fire, math.floor(os.clock() * 10) % fireFrames)
return OVERLAY_UPDATED
end
----
[#nodesettext]
==== nodeSetText
[source,text]
----
nodeSetText(node, text)
nodeSetText(node, text, height)
nodeSetText(node, nil)
nodeSetText(node)
----
Montre une ligne de texte sur le nœud, rendue dans la police sélectionnée
(`fontSelect`), à la qualité courante (`fontQuality`), dans la couleur de
premier plan (`colorForeground`), sur un quad haut de `height` unités du monde
(`1` par défaut) et aussi large que le texte rendu le veut. Il est non éclairé,
mélangé et dessiné des deux côtés : étiquettes de nom, panneaux, un score
au-dessus d'une table. Comme un sprite, il devient le maillage du nœud et
remplace tout maillage, sprite ou texte qui s'y trouvait ; appelez-le de
nouveau pour changer le texte, avec `nil` ou sans second argument pour le
retirer. Sans police sélectionnée, ou sans le moteur de rendu 3D, il met fin
au script.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetsprite,nodeSetSprite>>, <<nodesetbillboard,nodeSetBillboard>>, <<fontselect,fontSelect>>, <<colorforeground,colorForeground>>
.Exemple
[source,lua]
----
-- Un panneau fixe près de la lampe, et un score qui se met à jour quand il change.
fontSelect(signFont)
colorForeground(255, 240, 200)
local sign = nodeNew()
nodeSetPosition(sign, 4.2, 1.2, -1.2)
nodeSetRotation(sign, 0, -30, 0)
nodeSetText(sign, "LAMP", 0.5)
function setScore(points)
nodeSetText(scoreboard, string.format("%06d", points), 0.4)
end
----
[#nodesetvisible]
==== nodeSetVisible
[source,text]
----
nodeSetVisible(node, visible)
----
Cache ou montre le nœud et tout ce qui se trouve sous lui ; l'indicateur propre
d'un enfant s'applique toujours quand son parent est de nouveau montré. Les
maillages cachés ne sont pas dessinés et les lumières cachées cessent de
briller. Servez-vous-en pour garder prêt un marqueur ou un accessoire de
rechange sans avoir à le supprimer puis le reconstruire.
*Depuis :* 3.00.
*Voir aussi :* <<nodesetmesh,nodeSetMesh>>, <<nodedelete,nodeDelete>>
.Exemple
[source,lua]
----
-- Un marqueur qui apparaît là où la balle a touché la raquette pour la dernière fois.
local mark = nodeNew()
nodeSetMesh(mark, meshSphere(0.08, 12), markLook)
nodeSetVisible(mark, false)
function onCollision(a, b, x, y, z)
if (a == ball and b == paddle) or (a == paddle and b == ball) then
nodeSetPosition(mark, x, y, z)
nodeSetVisible(mark, true)
end
end
----
[#overlay]
=== Overlay
L'overlay est une surface RGBA 32 bits composée par-dessus la vidéo à chaque image ; tout le dessin 2D y aboutit. Ses coordonnées suivent la résolution de l'overlay réglée par `overlaySetResolution`, qui vaut par défaut la moitié de la largeur et de la hauteur de la vidéo, et sont mises à l'échelle de la fenêtre au moment de l'affichage. Dessinez depuis `onOverlayUpdate` (ou depuis `singeMain` dans le modèle <<threaded,À fil d'exécution>>) et renvoyez `OVERLAY_UPDATED` quand quelque chose a changé. Les primitives `overlay*` dessinent des contours d'un pixel dans la couleur de premier plan actuelle et découpent sans rien dire tout ce qui tombe hors de la surface. `overlayPrint` fait exception : il prend des coordonnées en cellules de caractères et copie telle quelle la police de console intégrée, sans tenir compte des réglages de couleur.
[#overlaybanner]
==== overlayBanner
[source,text]
----
overlayBanner(text [, height])
----
Extension Hypseus. Affiche un court message par-dessus l'image pendant une trentaine d'images dessinées, puis le retire. `height` est l'endroit où il se place, en pourcentage de la hauteur de l'image en partant du haut, de 1 à 95 ; la valeur par défaut est 47, celle d'Hypseus lui-même, et un nombre hors de cet intervalle est remplacé par la valeur par défaut au lieu d'être refusé.
Un message de plus de 60 caractères est ignoré, exactement comme Hypseus l'ignore. Le bandeau est dessiné par le même document fourni que celui qu'utilisent les sous-titres (`Singe/subtitle.rml`, mis en forme par `Singe/subtitle.rcss`), si bien qu'il ressemble à un sous-titre et se met en forme avec eux ; tant qu'il est affiché, il prend la place de tout sous-titre, et le sous-titre revient quand le bandeau expire. Le texte est dessiné comme du texte, si bien que `<`, `>` et `&` arrivent à l'écran tels quels.
*Paramètres :*
* `text` -- chaîne ; jusqu'à 60 caractères.
* `height` -- nombre facultatif ; de 1 à 95, en pourcentage de la hauteur de l'image en partant du haut. Par défaut 47.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<srtload,srtLoad>>, <<subtitles,Sous-titres>>, <<overlayprint,overlayPrint>>
.Exemple
[source,lua]
----
-- Dire au joueur ce qui vient de se passer sans construire de HUD pour cela.
overlayBanner("Extra life!", 20)
----
[#overlaybox]
==== overlayBox
[source,text]
----
overlayBox(x1, y1, x2, y2)
----
Dessine le contour du rectangle aligné sur les axes dont les coins opposés sont `(x1, y1)` et `(x2, y2)`, sous forme de quatre lignes d'un pixel dans la couleur de premier plan. Les deux coins sont inclus et peuvent être donnés dans n'importe quel ordre. Il n'existe pas de variante pleine ; remplissez tout l'overlay avec `overlayClear` sur une couleur de fond, ou une zone avec un sprite étiré.
*Depuis :* 2.00
*Voir aussi :* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>, <<overlayclear,overlayClear>>
.Exemple
[source,lua]
----
-- Encadrer l'overlay en rouge et tracer le contour de chaque bloc de plateforme.
function onOverlayUpdate()
overlayClear()
colorForeground(255, 0, 0, 255)
overlayBox(0, 0, overlayGetWidth() - 1, overlayGetHeight() - 1)
colorForeground(90, 90, 110, 255)
for _, b in ipairs(blocks) do
overlayBox(b.x - b.w / 2, b.y - b.h / 2, b.x + b.w / 2, b.y + b.h / 2)
end
return OVERLAY_UPDATED
end
----
[#overlaycircle]
==== overlayCircle
[source,text]
----
overlayCircle(x, y, radius)
----
Dessine le contour d'un cercle d'un pixel dans la couleur de premier plan, centré sur `(x, y)`, avec l'algorithme du point milieu. Un rayon de `0` ou moins ne dessine rien. Les coordonnées et le rayon sont des entiers en unités de l'overlay ; les fractions sont tronquées.
*Paramètres :*
* `x`, `y` -- le centre en coordonnées de l'overlay.
* `radius` -- le rayon en unités de l'overlay.
*Depuis :* 2.00
*Voir aussi :* <<overlayellipse,overlayEllipse>>, <<overlayplot,overlayPlot>>, <<colorforeground,colorForeground>>
.Exemple
[source,lua]
----
-- Un réticule de viseur qui devient rouge tant que le joueur est touché.
function onOverlayUpdate()
if hitTimer > 0 then
colorForeground(255, 0, 0, 255)
hitTimer = hitTimer - 1
else
colorForeground(255, 255, 255, 255)
end
overlayCircle(aimX, aimY, 8)
overlayLine(aimX - 12, aimY, aimX + 12, aimY)
overlayLine(aimX, aimY - 12, aimX, aimY + 12)
return OVERLAY_UPDATED
end
----
[#overlayclear]
==== overlayClear
[source,text]
----
overlayClear()
----
Remplit tout l'overlay avec la couleur de fond actuelle. Quand la couleur est la même que lors du dernier effacement, seul ce qui a été dessiné depuis est rempli de nouveau, si bien qu'effacer un overlay qui contient quelques lignes de texte ne coûte presque rien, quelle que soit sa taille. Le fond est au départ noir transparent : sans appel à `colorBackground`, cela efface donc l'overlay pour le rendre de nouveau invisible, et la vidéo apparaît au travers. Un fond partiellement opaque assombrit la vidéo ; un fond entièrement opaque la cache. La plupart des jeux l'appellent en tout premier dans `onOverlayUpdate`. Les arguments éventuels sont ignorés.
*Depuis :* 1.x
*Voir aussi :* <<colorbackground,colorBackground>>, <<onoverlayupdate,onOverlayUpdate>>
.Exemple
[source,lua]
----
-- Assombrir de moitié la vidéo en pause derrière le menu de service.
function onOverlayUpdate()
colorBackground(0, 0, 0, 127)
overlayClear()
spriteDraw(serviceTitle, (overlayGetWidth() - spriteGetWidth(serviceTitle)) / 2, 25)
return OVERLAY_UPDATED
end
----
[#overlayellipse]
==== overlayEllipse
[source,text]
----
overlayEllipse(x1, y1, x2, y2)
----
Dessine le contour d'une ellipse d'un pixel dans la couleur de premier plan, inscrite dans le rectangle aligné sur les axes dont les coins opposés sont `(x1, y1)` et `(x2, y2)`. Les coins peuvent être donnés dans n'importe quel ordre. Un rectangle de largeur et de hauteur égales donne un cercle : c'est donc aussi le moyen de dessiner un cercle à partir de son rectangle englobant plutôt que de son centre.
*Depuis :* 2.00
*Voir aussi :* <<overlaycircle,overlayCircle>>, <<overlaybox,overlayBox>>
.Exemple
[source,lua]
----
-- Mettre en évidence la borne sélectionnée avec un ovale ajusté à son panneau.
function onOverlayUpdate()
spriteDraw(cabinetSprite, CABINET_X, CABINET_Y)
if selected == CABINET then
colorForeground(255, 220, 0, 255)
overlayEllipse(CABINET_X - 6, CABINET_Y - 6, CABINET_X + CABINET_W + 6, CABINET_Y + CABINET_H + 6)
end
return OVERLAY_UPDATED
end
----
[#overlaygetfontheight]
==== overlayGetFontHeight
[source,text]
----
result = overlayGetFontHeight()
----
Renvoie la hauteur en pixels d'une cellule de caractère de la police de console intégrée, l'unité dans laquelle compte `overlayPrint`. La police de console est une seule image intégrée de 256 glyphes, chargée une fois avant l'exécution de tout script : cette valeur ne change donc jamais pendant que le programme tourne. Les arguments éventuels sont ignorés.
*Renvoie :* hauteur entière d'une cellule en pixels.
*Depuis :* 3.00
*Voir aussi :* <<overlaygetfontwidth,overlayGetFontWidth>>, <<overlayprint,overlayPrint>>, <<overlaygetheight,overlayGetHeight>>
.Exemple
[source,lua]
----
-- Combien de lignes de texte de console tiennent, quelle que soit la taille de l'overlay.
ROWS = overlayGetHeight() // overlayGetFontHeight()
overlayPrint(0, ROWS - 1, "along the bottom")
----
[#overlaygetfontwidth]
==== overlayGetFontWidth
[source,text]
----
result = overlayGetFontWidth()
----
Renvoie la largeur en pixels d'une cellule de caractère de la police de console intégrée, l'unité dans laquelle compte `overlayPrint`. Utilisez-la avec `overlayGetWidth` pour calculer combien de colonnes tiennent, ou pour placer du texte de console par rapport à quelque chose dessiné en pixels. Divisez avec `//` plutôt qu'avec `/` : la division ordinaire de Lua donne un nombre fractionnaire, et une position de cellule doit être entière. Les arguments éventuels sont ignorés.
*Renvoie :* largeur entière d'une cellule en pixels.
*Depuis :* 3.00
*Voir aussi :* <<overlaygetfontheight,overlayGetFontHeight>>, <<overlayprint,overlayPrint>>, <<overlaygetwidth,overlayGetWidth>>
.Exemple
[source,lua]
----
-- Centrer une ligne de texte de console sans coder en dur le nombre de colonnes.
local columns = overlayGetWidth() // overlayGetFontWidth()
local text = "GAME OVER"
overlayPrint((columns - #text) // 2, 10, text)
----
[#overlaygetheight]
==== overlayGetHeight
[source,text]
----
result = overlayGetHeight()
----
Renvoie la hauteur de la surface de l'overlay en unités de l'overlay : la dernière valeur donnée à `overlaySetResolution`, ou la moitié de la hauteur de la vidéo avant tout appel. Lisez-la au démarrage, après avoir réglé la résolution, pour disposer les positions en fonction de la zone de dessin réelle. Les arguments éventuels sont ignorés.
*Renvoie :* hauteur entière.
*Depuis :* 1.x
*Voir aussi :* <<overlaygetwidth,overlayGetWidth>>, <<overlaysetresolution,overlaySetResolution>>
.Exemple
[source,lua]
----
-- Ancrer le panneau de texte à la marge du bas, quelle que soit la taille de l'overlay.
overlaySetResolution(discGetWidth(), discGetHeight())
TEXT_Y = 200
TEXT_H = overlayGetHeight() - MARGIN_Y - TEXT_Y
TEXT_LINE_LIMIT = math.floor(TEXT_H / LINE_HEIGHT)
----
[#overlaygetwidth]
==== overlayGetWidth
[source,text]
----
result = overlayGetWidth()
----
Renvoie la largeur de la surface de l'overlay en unités de l'overlay : la dernière valeur donnée à `overlaySetResolution`, ou la moitié de la largeur de la vidéo avant tout appel. Servez-vous-en pour aligner à droite ou centrer des éléments au lieu de coder une largeur en dur. Les arguments éventuels sont ignorés.
*Renvoie :* largeur entière.
*Depuis :* 1.x
*Voir aussi :* <<overlaygetheight,overlayGetHeight>>, <<overlaysetresolution,overlaySetResolution>>
.Exemple
[source,lua]
----
-- Aligner le fronton à droite et centrer le titre, indépendamment de la résolution.
function onOverlayUpdate()
local marqueeX = overlayGetWidth() - MARGIN_X - spriteGetWidth(marqueeSprite)
spriteDraw(marqueeSprite, marqueeX, MARGIN_Y)
spriteDraw(titleSprite, (overlayGetWidth() - spriteGetWidth(titleSprite)) / 2, 25)
return OVERLAY_UPDATED
end
----
[#overlayline]
==== overlayLine
[source,text]
----
overlayLine(x1, y1, x2, y2)
----
Dessine une ligne de Bresenham d'un pixel dans la couleur de premier plan, de `(x1, y1)` à `(x2, y2)`, les deux extrémités comprises. Les pixels hors de l'overlay sont abandonnés, si bien que les extrémités peuvent se trouver hors de la surface. Les coordonnées sont tronquées en entiers.
*Depuis :* 2.00
*Voir aussi :* <<overlayplot,overlayPlot>>, <<overlaybox,overlayBox>>, <<colorforeground,colorForeground>>
.Exemple
[source,lua]
----
-- Dessiner le laser du pistolet jusqu'au point visé, et un simple réticule de visée.
function onOverlayUpdate()
overlayClear()
if firing then
colorForeground(255, 64, 64, 255)
overlayLine(GUN_X, GUN_Y, aimX, aimY)
end
colorForeground(255, 255, 255, 255)
overlayCircle(aimX, aimY, 6)
return OVERLAY_UPDATED
end
----
[#overlayplot]
==== overlayPlot
[source,text]
----
overlayPlot(x, y)
----
Donne la couleur de premier plan à un pixel de l'overlay. Un point hors de la surface est ignoré. Chaque appel verrouille puis déverrouille l'overlay : tracer des milliers de points par image depuis Lua est donc lent ; utilisez un sprite pour tout ce qui a une structure et gardez ceci pour les étoiles, les étincelles et les marques de débogage.
*Depuis :* 2.00
*Voir aussi :* <<overlayline,overlayLine>>, <<colorforeground,colorForeground>>
.Exemple
[source,lua]
----
-- Un champ d'étoiles qui scintillent lentement derrière le titre.
function onOverlayUpdate()
overlayClear()
for _, star in ipairs(stars) do
local shade = 128 + math.floor(127 * math.sin(frameCount * star.speed))
colorForeground(shade, shade, shade, 255)
overlayPlot(star.x, star.y)
end
frameCount = frameCount + 1
return OVERLAY_UPDATED
end
----
[#overlayprint]
==== overlayPrint
[source,text]
----
overlayPrint(column, row, text)
----
Affiche une chaîne avec la police de console à chasse fixe intégrée au moteur. Contrairement à tous les autres appels de dessin, la position est une cellule de caractère : le moteur multiplie `column` et `row` par la largeur et la hauteur d'un glyphe, si bien que `(0, 0)` est la cellule en haut à gauche et que `(1, 0)` se trouve un glyphe plus à droite. Les glyphes sont copiés tels quels depuis l'image de la police ; les couleurs de premier plan et de fond actuelles ne sont pas appliquées, et seul le fond propre de la police est transparent. Chaque octet de la chaîne choisit l'un des 256 glyphes de la police : le texte est donc traité comme des octets, pas comme de l'UTF-8. Les caractères qui dépasseraient le bord droit de l'overlay sont abandonnés, et un texte qui commence au-delà du bord ne dessine rien.
*Paramètres :*
* `column`, `row` -- la cellule de caractère, `0` en haut à gauche.
* `text` -- la chaîne à afficher.
*Remarques :* La police de console est destinée aux diagnostics et aux écrans de service. Utilisez `fontPrint` ou `fontToSprite` pour du texte en couleur ou proportionnel. La taille d'une cellule en pixels est `overlayGetFontWidth` sur `overlayGetFontHeight`, si bien que `overlayGetWidth() // overlayGetFontWidth()` donne le nombre de colonnes qui tiennent.
*Depuis :* 1.x
*Voir aussi :* <<fontprint,fontPrint>>, <<overlayclear,overlayClear>>, <<colorbackground,colorBackground>>
.Exemple
[source,lua]
----
-- Des diagnostics dans le coin pendant qu'un téléchargement se termine.
function onOverlayUpdate()
colorBackground(0, 0, 0, 127)
overlayClear()
overlayPrint(1, 1, "FRAME " .. discGetFrame())
overlayPrint(1, 2, download.url .. " " .. tostring(download.finished))
if download.finished then
overlayPrint(1, 3, download.result)
end
return OVERLAY_UPDATED
end
----
[#overlaysetmonochrome]
==== overlaySetMonochrome
[source,text]
----
overlaySetMonochrome(enabled)
----
Extension Hypseus. Montre l'overlay en gris : avant chaque envoi, le moteur convertit une copie de l'overlay en luminance avec les coefficients BT.601, en gardant le canal alpha tel quel (Hypseus le seuille). Les pixels propres de l'overlay restent en couleur, si bien que rien de ce que le script relit ne change, et le dessin continue comme d'habitude. Associez-le à `vldpSetMonochrome` pour un mode noir et blanc. Un argument non booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpsetmonochrome,vldpSetMonochrome>>, <<overlayclear,overlayClear>>
.Exemple
[source,lua]
----
-- Un seul réglage pilote à la fois le disque et l'overlay.
function applyChroma(color)
vldpSetMonochrome(not color)
overlaySetMonochrome(not color)
end
----
[#overlaysetresolution]
==== overlaySetResolution
[source,text]
----
overlaySetResolution(width, height)
----
Remplace l'overlay par une nouvelle surface de la taille donnée et abandonne tout ce qui a été dessiné jusque-là. Dès lors, chaque coordonnée de l'overlay, chaque position de sprite et chaque résultat de `overlayGetWidth` ou de `overlayGetHeight` est dans le nouvel espace, que le moteur met à l'échelle de la fenêtre au moment de l'affichage. Appelez-la une fois au démarrage, avant de dessiner ; la valeur par défaut est la moitié de la résolution de la vidéo dans chaque dimension. Une largeur ou une hauteur de `0` ou moins met fin au script avec une erreur. Une résolution plus élevée donne un placement plus fin et un texte plus net, au prix de plus de travail de composition par image ; la taille native du disque, donnée par `discGetWidth` et `discGetHeight`, est le choix courant.
*Paramètres :*
* `width` -- la nouvelle largeur de l'overlay en pixels, supérieure à `0`.
* `height` -- la nouvelle hauteur de l'overlay en pixels, supérieure à `0`.
*Depuis :* 2.00
*Voir aussi :* <<overlaygetwidth,overlayGetWidth>>, <<overlaygetheight,overlayGetHeight>>, <<discgetwidth,discGetWidth>>
.Exemple
[source,lua]
----
-- S'aligner sur le disque, puis disposer le menu dans cet espace de coordonnées.
overlaySetResolution(discGetWidth(), discGetHeight())
MARGIN_X = 16
MARGIN_Y = 16
MARQUEE_W = 200
MARQUEE_X = overlayGetWidth() - MARGIN_X - MARQUEE_W
CABINET_H = overlayGetHeight() - MARGIN_Y - CABINET_Y
----
[#setoverlayfullalpha]
==== setOverlayFullAlpha
[source,text]
----
setOverlayFullAlpha()
----
Extension Hypseus, acceptée et ignorée. Hypseus copie son overlay avec le canal alpha seuillé à `128`, si bien qu'un pixel à moitié transparent est soit entièrement opaque, soit absent, et cet appel lui demande d'envoyer plutôt la surface telle qu'elle est et de la mélanger correctement. L'overlay de Singe a toujours été en RGBA complet avec un véritable alpha par pixel : ce que cet appel demande est donc déjà en vigueur.
*Renvoie :* rien.
*Depuis :* 3.00
[#setoverlaylinearscale]
==== setOverlayLinearScale
[source,text]
----
setOverlayLinearScale(enabled)
----
Extension Hypseus. Choisit comment l'overlay est filtré sur son chemin vers la fenêtre. Avec `true`, la valeur par défaut et celle que rétablit un rechargement, il est échantillonné linéairement, si bien qu'un overlay de basse résolution s'adoucit au lieu de faire apparaître des blocs ; avec `false`, c'est le pixel le plus proche qui est pris, ce qui garde nets le pixel art, les contours d'un pixel et le petit texte. Le linéaire est la valeur par défaut parce que c'est ainsi que toutes les versions de Singe ont dessiné l'overlay ; Hypseus prend par défaut le pixel le plus proche, si bien qu'un jeu venu d'Hypseus qui n'appelle jamais ceci paraîtra un peu plus doux jusqu'à ce qu'il le fasse. Rien ne change dans le dessin, seulement la mise à l'échelle finale de la résolution de l'overlay vers la fenêtre, si bien que tout ce qui se trouve sur l'overlay -- les primitives, les polices, les sprites et la couche 3D -- passe ensemble par ce seul filtre, et que l'image du disque, derrière, n'est pas touchée. Le réglage survit à `overlaySetResolution`, qui remplace l'overlay. Un argument non booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<overlaysetresolution,overlaySetResolution>>, <<spritequality,spriteQuality>>
.Exemple
[source,lua]
----
-- Un overlay de 320x240 sur un écran 4K ; l'écran-titre peint adouci, le pixel art du jeu net.
overlaySetResolution(320, 240)
function showTitle()
setOverlayLinearScale(true)
mode = "title"
end
function startGame()
setOverlayLinearScale(false)
mode = "play"
end
----
[#setoverlayontop]
==== setOverlayOnTop
[source,text]
----
loaded, id = setOverlayOnTop(enabled)
----
Extension Hypseus. Choisit si l'overlay et les GUI dessinées par-dessus l'image sont au-dessus ou en dessous des illustrations du bezel. Seules les illustrations dessinées devant l'image (`--bezelflip`) peuvent se trouver au-dessus d'elles ; avec les illustrations derrière -- ce qui est le cas par défaut -- cela ne change donc rien. `false` est le réglage avec lequel un jeu démarre, et la valeur par défaut d'Hypseus lui-même. Il est conservé jusqu'au rechargement du script.
Le réglage n'est enregistré que lorsque des illustrations sont chargées ; sans elles, l'appel ne fait rien et répond `false` et `"0"`, exactement comme Hypseus. Un argument non booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen ; `true` place l'overlay et les GUI au-dessus des illustrations.
*Renvoie :* booléen, indiquant si des illustrations sont chargées, et leur identifiant sous forme de chaîne (`"0"` quand il n'y en a pas).
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<mainbezelloaded,mainBezelLoaded>>, <<guidrawscreen,guiDrawScreen>>, xref:Manual.adoc#bezels[Bezels]
.Exemple
[source,lua]
----
-- Le cadre de la borne chevauche l'image, mais la ligne de score doit rester lisible.
if mainBezelLoaded() then
setOverlayOnTop(true)
end
----
[#setoverlayopacity]
==== setOverlayOpacity
[source,text]
----
setOverlayOpacity(alpha)
----
Extension Hypseus. Le degré d'opacité de l'overlay entier, de `0` (invisible) à `255` (tel que le jeu l'a dessiné). Il multiplie l'alpha que portent déjà les pixels propres de l'overlay, si bien qu'un sprite à moitié transparent reste deux fois moins opaque que le reste ; il ne change pas le contenu de l'overlay, et rien n'a besoin d'être redessiné.
Le niveau est borné entre `0` et `255` au lieu d'être refusé, et un script démarre à `255`. Il est conservé jusqu'au rechargement du script, et il survit à un changement de résolution de l'overlay.
Les GUI, la scène 3D et les illustrations du bezel sont dessinées à part et ne sont pas affectées.
*Paramètres :*
* `alpha` -- nombre ; de `0` à `255`.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<setoverlaylinearscale,setOverlayLinearScale>>, <<overlaysetresolution,overlaySetResolution>>
.Exemple
[source,lua]
----
-- Faire disparaître en fondu tout l'affichage tête haute en une demi-seconde.
local fade = 255
function onOverlayUpdate()
fade = math.max(0, fade - 8)
setOverlayOpacity(fade)
return OVERLAY_UPDATED
end
----
[#physicsref]
=== Physique
Les appels au niveau du monde : la requête de rayon, la gravité, le mode 2D, la pause et le dessin de débogage. La physique est initialisée avec le moteur : il n'y a donc rien à activer avant le premier `bodyNew` ; le monde avance soixante fois par seconde d'après l'horloge, après les animations et avant le rendu de la scène. Les positions et les distances sont en unités du monde, et la gravité en unités par seconde au carré. Voir <<physics,Physique>> dans Développement de jeux pour la façon dont les corps, les nœuds et l'image s'articulent.
[#physicsraycast]
==== physicsRaycast
[source,text]
----
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz)
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz, maxDistance)
----
Trouve le premier corps le long d'un rayon partant de `(x, y, z)` dans la direction `(dx, dy, dz)`, à moins de `maxDistance` unités du monde. La direction est normalisée, si bien que n'importe quelle longueur convient, mais une direction nulle ne touche rien. Les déclencheurs et les joueurs sont touchés comme n'importe quel autre corps ; un joueur est signalé par son nœud. Avec `sceneUnproject` qui fournit deux points le long de la souris, c'est de la sélection ; tout droit vers le bas depuis un point, c'est une sonde de hauteur.
*Paramètres :*
* `x`, `y`, `z` -- le départ du rayon dans l'espace du monde.
* `dx`, `dy`, `dz` -- sa direction ; seule la direction compte.
* `maxDistance` -- jusqu'où chercher ; `1000` par défaut, et `0` ou moins signifie la valeur par défaut.
*Renvoie :* Le nœud du corps, le point d'impact et la normale de la surface à cet endroit, ou un seul `nil` quand rien n'est touché.
*Depuis :* 3.00.
*Voir aussi :* <<sceneunproject,sceneUnproject>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<linedraw,lineDraw>>
.Exemple
[source,lua]
----
-- Poser le marqueur sur le sol plat qui se trouve sous le curseur, quel qu'il soit.
function onOverlayUpdate()
local mx, my = mouseGetPosition(0)
local ox, oy, oz = sceneUnproject(mx, my, 0)
local fx, fy, fz = sceneUnproject(mx, my, 10)
local hit, hx, hy, hz, nx, ny, nz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz, 60)
nodeSetVisible(marker, hit ~= nil and ny > 0.7)
if hit then
nodeSetPosition(marker, hx, hy + 0.05, hz)
end
return OVERLAY_NOT_UPDATED
end
----
[#physicsset2d]
==== physicsSet2D
[source,text]
----
physicsSet2D(planar)
----
Avec `true`, les corps créés à partir de maintenant ne se déplacent qu'en X et en Y et ne tournent qu'autour de Z : le monde d'un jeu 2D, généralement en coordonnées de l'overlay, avec une gravité dirigée vers +Y pour que les choses tombent vers le bas de l'écran. Les corps déjà créés gardent leur liberté : appelez-le donc avant tout `bodyNew` ; `false` revient à la 3D complète pour les corps créés ensuite. Les joueurs sont différents : tant que le mode est actif, chaque joueur, quel que soit le moment où il a été créé, se déplace le long de X avec son Z maintenu à `0`, et `playerMove` prend une seule valeur. Le monde 2D fonctionne sans GPU, Raspberry Pi 3 compris.
*Depuis :* 3.00.
*Voir aussi :* <<physicssetgravity,physicsSetGravity>>, <<bodynew,bodyNew>>, <<playermove,playerMove>>
.Exemple
[source,lua]
----
-- Un monde de jeu de plateformes en pixels de l'overlay : la gravité vers le bas de l'écran, un sol et des murs.
physicsSet2D(true)
physicsSetGravity(0, 900, 0)
local width, height = overlayGetWidth(), overlayGetHeight()
floor = nodeNew()
nodeSetPosition(floor, width / 2, height - 10, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, width, 20, 50)
leftWall = nodeNew()
nodeSetPosition(leftWall, -10, height / 2, 0)
bodyNew(leftWall, BODY_STATIC, SHAPE_BOX, 20, height, 50)
----
[#physicssetdebug]
==== physicsSetDebug
[source,text]
----
physicsSetDebug(mask)
----
Dessine ce que contient la simulation sous forme de lignes par-dessus la scène à chaque image, tant que le masque est réglé : `DEBUG_SHAPES` pour la forme de chaque corps mobile en fil de fer (corps mous compris, et joueurs activés en jaune), `DEBUG_CONSTRAINTS` pour les articulations, les articulations de ragdoll et les contraintes de véhicule avec leurs limites, `DEBUG_CONTACTS` pour une croix rouge là où des corps se sont rencontrés à ce pas, et `DEBUG_VELOCITIES` pour une ligne le long de la vitesse de chaque corps mobile. Additionnez les indicateurs ; `DEBUG_ALL` vaut les quatre. Les formes des corps statiques restent masquées sauf si `DEBUG_STATIC` est ajouté, parce qu'un maillage de niveau fait beaucoup de lignes. `DEBUG_NONE` le désactive, et rien n'est dessiné ni ne coûte quoi que ce soit tant qu'il est désactivé.
*Remarques :* Une version de Singe compilée sans le moteur de rendu de débogage de Jolt affiche un message dans la console et ne dessine rien.
*Depuis :* 3.00.
*Voir aussi :* <<linedraw,lineDraw>>, <<navdraw,navDraw>>, <<physicssetenabled,physicsSetEnabled>>
.Exemple
[source,lua]
----
-- Basculer l'affichage de la physique avec une touche de débogage ; les formes statiques aussi, puisque le niveau est petit.
function onInputPressed(switch)
if switch == SWITCH_BUTTON3 then
showPhysics = not showPhysics
if showPhysics then
physicsSetDebug(DEBUG_ALL + DEBUG_STATIC)
else
physicsSetDebug(DEBUG_NONE)
end
end
end
----
[#physicssetenabled]
==== physicsSetEnabled
[source,text]
----
physicsSetEnabled(enabled)
----
Met la simulation en pause avec `false` et la reprend avec `true`. Pendant la pause, aucun pas ne s'exécute : les corps dynamiques restent immobiles et gardent leurs vitesses, les corps cinématiques cessent de suivre leurs nœuds, les joueurs cessent de bouger, et aucun `onCollision` ni `onTrigger` n'est déclenché. Le temps ne s'accumule pas pendant la pause : la reprise ne rejoue donc pas les pas manqués. La pause propre du jeu fait la même chose sans cet appel ; servez-vous de celui-ci pour un menu, une cinématique ou un arrêt sur image qui laisse tourner le reste du jeu.
*Depuis :* 3.00.
*Voir aussi :* <<physicssetgravity,physicsSetGravity>>, <<bodyisresting,bodyIsResting>>
.Exemple
[source,lua]
----
-- Figer le monde tant que l'inventaire est ouvert.
function openInventory()
inventoryOpen = true
physicsSetEnabled(false)
end
function closeInventory()
inventoryOpen = false
physicsSetEnabled(true)
end
----
[#physicssetgravity]
==== physicsSetGravity
[source,text]
----
physicsSetGravity(x, y, z)
----
Règle l'accélération que ressentent tous les corps dynamiques et tous les joueurs, en unités du monde par seconde au carré selon chaque axe du monde. La valeur par défaut est `0, -9.81, 0`, la gravité terrestre en mètres ; `0, 0, 0` est l'espace, et un jeu 2D en pixels de l'overlay veut quelque chose comme `0, 900, 0`, puisque le Y de l'overlay va vers le bas de l'écran. La direction opposée à la gravité est « le haut » pour les joueurs : le sens dans lequel ils se tiennent debout, sautent et montent les marches.
Un changement de gravité réveille tous les corps, corps mous compris, si bien qu'une caisse qui s'était immobilisée tombe quand la gravité tourne sur le côté, et qu'un tissu flotte quand change un vent fait de gravité. Régler de nouveau la même gravité ne réveille rien.
*Depuis :* 3.00.
*Voir aussi :* <<bodynew,bodyNew>>, <<physicsset2d,physicsSet2D>>, <<playersetgravityscale,playerSetGravityScale>>
.Exemple
[source,lua]
----
-- Le niveau lunaire : un sixième de l'attraction terrestre, et un saut plus long en conséquence.
function startLevel(name)
if name == "moon" then
physicsSetGravity(0, -1.62, 0)
jumpSpeed = 3.0
else
physicsSetGravity(0, -9.81, 0)
jumpSpeed = 4.5
end
end
----
[#player]
=== Joueur
Un joueur est un contrôleur de personnage posé sur un nœud : là où un corps va où les forces l'envoient, un joueur va où le script le dit, en marchant sur les sols, en montant les marches, en glissant le long des murs, en se laissant porter par les plateformes et en écartant les corps légers. Les joueurs sont désignés par leur nœud, et un nœud porte un corps ou un joueur, pas les deux. Les tailles et les vitesses sont en unités du monde (des mètres dans une scène 3D, des pixels de l'overlay dans un monde 2D) et les tolérances du contrôleur s'adaptent à la forme, si bien qu'une capsule de 30 pixels se comporte comme une capsule de 0,3 mètre. Le joueur pilote la position de son nœud à chaque pas et laisse la rotation au script. Chaque appel de cette section met fin au script quand le nœud n'a pas de joueur. Voir <<physics,Physique>> dans Développement de jeux, Joueurs.
[#playernew]
==== playerNew
[source,text]
----
playerNew(node, radius, height)
playerNew(node, SHAPE_CAPSULE, radius, height)
playerNew(node, SHAPE_CYLINDER, radius, height)
playerNew(node, SHAPE_SPHERE, radius, 0)
playerNew(node, SHAPE_BOX, width, height, depth)
playerNew(node, SHAPE_HULL)
----
Place un joueur sur le nœud, là où le nœud se trouve dans le monde à cet instant : une capsule de ce rayon et de cette hauteur totale, ou n'importe quelle forme convexe de `bodyNew` avec ses tailles, mise à l'échelle par l'échelle du nœud dans le monde. La forme repose sur l'origine du nœud, si bien que le nœud correspond aux pieds. Il commence avec une masse de `70` kilogrammes, une poussée de `300` newtons, une limite de pente de `45` degrés, une hauteur de marche de `0.3` mise à l'échelle de la forme, une échelle de gravité de `1` et les taux de nage par défaut. Remplace tout corps ou joueur qu'avait le nœud. Met fin au script pour `SHAPE_MESH` (un joueur a besoin d'une forme convexe ; utilisez `SHAPE_HULL`), quand la physique n'est pas disponible sur cette machine, ou quand le joueur ne peut pas être créé (une enveloppe avec trop peu de maillage, ou seize joueurs déjà présents).
*Paramètres :*
* `radius`, `height` -- une capsule : son rayon et sa hauteur totale selon la verticale, calottes comprises.
* `SHAPE_*` -- une constante de forme suivie de ses tailles de `bodyNew` ; `SHAPE_HULL` n'en prend aucune et enveloppe le maillage du nœud et tout ce qui se trouve sous lui.
*Remarques :* Un appel avec exactement trois arguments est toujours une capsule : une sphère a donc besoin d'un quatrième argument (n'importe quel nombre) après son rayon pour être lue comme `SHAPE_SPHERE`.
*Depuis :* 3.00.
*Voir aussi :* <<playermove,playerMove>>, <<playerdelete,playerDelete>>, <<bodynew,bodyNew>>, <<nodesetparent,nodeSetParent>>
.Exemple
[source,lua]
----
-- Le héros : une capsule avec le modèle Fox accroché au nœud, les pieds au sol.
hero = nodeNew()
nodeSetPosition(hero, 0, 0.05, 0)
playerNew(hero, 0.3, 1.0)
playerSetStep(hero, 0.35)
fox = modelInstance(modelLoad(DIR .. "Models/Fox.glb"))
nodeSetParent(fox, hero)
nodeSetScale(fox, 0.01)
animationPlay(fox, "Walk", true)
-- Un robot est une boîte ; il prend les virages à angle droit et ne peut pas rouler.
robot = nodeNew()
playerNew(robot, SHAPE_BOX, 0.8, 1.6, 0.6)
----
[#playerdelete]
==== playerDelete
[source,text]
----
playerDelete(node)
----
Retire le joueur ; le nœud et tout ce qui est suspendu sous lui restent où ils sont. Supprimer le nœud retire aussi son joueur, tout comme donner au nœud un corps avec `bodyNew`. Met fin au script quand le nœud n'a pas de joueur.
*Depuis :* 3.00.
*Voir aussi :* <<playernew,playerNew>>, <<playersetenabled,playerSetEnabled>>
.Exemple
[source,lua]
----
-- Le héros monte dans la voiture : le contrôleur disparaît, le modèle est du voyage.
function boardCar()
playerDelete(hero)
nodeSetParent(hero, car)
nodeSetPosition(hero, 0, 0.4, 0)
driving = true
end
----
[#playermove]
==== playerMove
[source,text]
----
playerMove(node, vx, vz)
playerMove(node, vx, vy, vz)
playerMove(node, vx)
----
Indique au joueur la vitesse qu'il veut à cette image, en unités du monde par seconde. Au sol ou en l'air, les deux valeurs horizontales sont utilisées telles quelles, en plus de celle de la plateforme sur laquelle il se tient ; la composante verticale vient de la gravité, du sol et de `playerJump`, et `vy` est ignoré. Quand le joueur nage, les trois valeurs le dirigent, et l'eau amène progressivement sa vitesse vers elles selon la traînée de `playerSetSwim`, en coulant à la vitesse de descente quand `vy` vaut `0`. Dans un monde 2D, un `vx` seul le fait avancer le long de X et tout `vz` est ignoré. Appelez-le à chaque image depuis `onOverlayUpdate` : la demande est consommée par les pas de cette image, et à une image sans demande, le joueur reste immobile.
*Paramètres :*
* `vx`, `vz` -- la vitesse horizontale voulue, en unités du monde par seconde (en 2D : pixels de l'overlay le long de X).
* `vy` -- la vitesse verticale voulue pendant la nage ; positive vers le haut.
*Depuis :* 3.00.
*Voir aussi :* <<playerjump,playerJump>>, <<playersetvelocity,playerSetVelocity>>, <<playerisswimming,playerIsSwimming>>, <<physicsset2d,physicsSet2D>>
.Exemple
[source,lua]
----
-- Marcher avec le stick gauche, remonter à la nage tant que le bouton est maintenu, regarder dans le sens de la marche.
function onOverlayUpdate()
local vx = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767 * 3
local vz = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_Y) / 32767 * 3
if playerIsSwimming(hero) then
playerMove(hero, vx, surfacing and 1.5 or 0, vz)
else
playerMove(hero, vx, vz)
end
nodeSetRotation(hero, 0, math.deg(math.atan(-vz, vx)), 0)
return OVERLAY_NOT_UPDATED
end
----
[#playerisswimming]
==== playerIsSwimming
[source,text]
----
swimming = playerIsSwimming(node)
----
Indique si le joueur était dans l'eau au dernier pas : le milieu de sa forme sous la surface d'un volume `bodySetWater`. Pendant la nage, `playerMove` dirige selon trois axes, la gravité laisse la place à la vitesse de descente, et le courant de l'eau emporte le joueur.
*Renvoie :* Booléen.
*Depuis :* 3.00.
*Voir aussi :* <<playersetswim,playerSetSwim>>, <<bodysetwater,bodySetWater>>, <<playermove,playerMove>>
.Exemple
[source,lua]
----
-- Changer l'animation quand le héros entre dans l'eau.
function onOverlayUpdate()
local swimming = playerIsSwimming(hero)
if swimming ~= wasSwimming then
animationPlay(fox, swimming and "Swim" or "Walk", true)
wasSwimming = swimming
end
return OVERLAY_NOT_UPDATED
end
----
[#playerjump]
==== playerJump
[source,text]
----
jumped = playerJump(node, speed)
----
Demande au joueur de quitter le sol à cette vitesse vers le haut, en unités du monde par seconde, au pas suivant. Ce n'est accordé que s'il y a, à cet instant, un sol praticable sous ses pieds : maintenir un bouton enfoncé ne fait donc pas rebondir le joueur, et un saut en plein air ne fait rien. Une vitesse négative est traitée comme `0`. Le joueur garde sa vitesse horizontale pendant le saut, et `playerMove` le dirige toujours en l'air.
*Renvoie :* `true` quand le saut a été accordé.
*Depuis :* 3.00.
*Voir aussi :* <<playerisonground,playerIsOnGround>>, <<playermove,playerMove>>, <<playersetgravityscale,playerSetGravityScale>>
.Exemple
[source,lua]
----
-- Sauter avec le bouton d'action, avec un son seulement quand le saut a vraiment eu lieu.
function onInputPressed(switch)
if switch == SWITCH_BUTTON1 then
if playerJump(hero, 4.5) then
soundPlay(jumpSound)
end
end
end
----
[#playersetswim]
==== playerSetSwim
[source,text]
----
playerSetSwim(node, sinkSpeed, drag)
----
Règle le comportement du joueur dans l'eau : à quelle vitesse il coule, en unités du monde par seconde, quand `playerMove` ne demande aucun mouvement vertical (`0.3` par défaut), et à quelle rapidité l'eau amène sa vitesse vers ce que demande `playerMove`, sous forme de taux par seconde (`2` par défaut, si bien que l'essentiel de l'écart a disparu en une seconde ; `0` ne change jamais la vitesse). Les deux valeurs sont ramenées à `0` au minimum. Un plongeur lourd coule vite avec une forte traînée ; un bouchon ne coule pas du tout.
*Paramètres :*
* `sinkSpeed` -- la dérive vers le bas sans commande verticale, en unités par seconde.
* `drag` -- le taux auquel l'eau amène la vitesse vers la demande, par seconde.
*Depuis :* 3.00.
*Voir aussi :* <<playerisswimming,playerIsSwimming>>, <<playermove,playerMove>>, <<bodysetwater,bodySetWater>>
.Exemple
[source,lua]
----
-- L'armure entraîne le héros vers le fond ; il faut l'enlever pour flotter.
function wearArmor(on)
armored = on
if on then
playerSetSwim(hero, 1.2, 4)
else
playerSetSwim(hero, 0.3, 2)
end
end
----
[#playersetvelocity]
==== playerSetVelocity
[source,text]
----
playerSetVelocity(node, x, y, z)
----
Règle la vitesse entière du joueur, en unités du monde par seconde selon chaque axe du monde, et abandonne toute demande `playerMove` faite à cette image. C'est la composante verticale qui dure : une vitesse vers le haut décolle le joueur du sol et il retombe sous l'effet de la gravité, ce qui donne un tremplin ou un rebond. La composante horizontale ne tient que jusqu'au pas suivant, où elle est remplacée par ce que demande `playerMove` (rien, quand il n'est pas appelé), parce qu'un joueur n'a pas d'élan horizontal propre. Un recul ou une ruée se fait donc ici avec une poussée vers le haut plus une vitesse `playerMove` fournie au fil des images suivantes.
*Depuis :* 3.00.
*Voir aussi :* <<playergetvelocity,playerGetVelocity>>, <<playermove,playerMove>>, <<playerjump,playerJump>>
.Exemple
[source,lua]
----
-- Un coup de l'ogre projette le héros en l'air et, au fil des images suivantes, loin de lui.
function onCollision(a, b, x, y, z, speed)
if (a == hero and b == ogre) or (a == ogre and b == hero) then
local hx, hy, hz = nodeGetPosition(hero)
local ox, oy, oz = nodeGetPosition(ogre)
local dx, dz = hx - ox, hz - oz
local len = math.sqrt(dx * dx + dz * dz)
playerSetVelocity(hero, 0, 3, 0)
knockX, knockZ, knockFrames = dx / len * 6, dz / len * 6, 20
health = health - 10
end
end
----
[#playergetvelocity]
==== playerGetVelocity
[source,text]
----
x, y, z = playerGetVelocity(node)
----
La vitesse du joueur après le dernier pas, en unités du monde par seconde selon chaque axe du monde, y compris ce que lui a donné le sol qui le porte. Lisez la composante verticale pour distinguer une montée d'une chute, ou le tout pour un bruit sourd à l'atterrissage.
*Renvoie :* Trois nombres.
*Depuis :* 3.00.
*Voir aussi :* <<playersetvelocity,playerSetVelocity>>, <<playerisonground,playerIsOnGround>>
.Exemple
[source,lua]
----
-- Dégâts de chute d'après la vitesse à laquelle le héros a touché le sol.
function onOverlayUpdate()
local _, vy = playerGetVelocity(hero)
local onGround = playerIsOnGround(hero)
if onGround and not wasOnGround and lastVy < -8 then
health = health - math.floor((-lastVy - 8) * 5)
end
wasOnGround = onGround
lastVy = vy
return OVERLAY_NOT_UPDATED
end
----
[#playerisonground]
==== playerIsOnGround
[source,text]
----
onGround = playerIsOnGround(node)
----
Indique si le joueur se tient sur un sol praticable au dernier pas : une surface pas plus raide que `playerSetSlope`. En l'air, ou sur une pente trop raide pour être gravie, la valeur est `false`. C'est le même test que fait `playerJump`.
*Renvoie :* Booléen.
*Depuis :* 3.00.
*Voir aussi :* <<playergetground,playerGetGround>>, <<playerjump,playerJump>>, <<playersetslope,playerSetSlope>>
.Exemple
[source,lua]
----
-- Coyote time : un saut est encore permis quelques images après avoir quitté un rebord.
function onOverlayUpdate()
if playerIsOnGround(hero) then
framesSinceGround = 0
else
framesSinceGround = framesSinceGround + 1
end
if jumpPressed and framesSinceGround < 6 then
playerSetVelocity(hero, vx, 4.5, vz)
end
return OVERLAY_NOT_UPDATED
end
----
[#playergetground]
==== playerGetGround
[source,text]
----
ground, nx, ny, nz = playerGetGround(node)
----
Ce sur quoi se tient le joueur, et la normale de la surface du sol à cet endroit. Le nœud est le corps sous ses pieds, praticable ou non ; il vaut `nil` en l'air, et la normale est alors `0, 1, 0`. Servez-vous du nœud pour distinguer du sol une plateforme mobile, de la glace ou une plaque de pression, et de la normale pour pencher un modèle dans une pente.
*Renvoie :* Le nœud sur lequel il se tient, ou `nil`, puis la normale.
*Depuis :* 3.00.
*Voir aussi :* <<playerisonground,playerIsOnGround>>, <<bodysetfriction,bodySetFriction>>
.Exemple
[source,lua]
----
-- Les pas sonnent différemment sur le pont de bois, et la plaque ouvre la grille.
function onOverlayUpdate()
local ground = playerGetGround(hero)
if ground == bridge then
footstep = woodStep
elseif ground == plate then
openGate()
else
footstep = stoneStep
end
return OVERLAY_NOT_UPDATED
end
----
[#playersetstep]
==== playerSetStep
[source,text]
----
playerSetStep(node, height)
----
La plus haute marche que le joueur monte sans sauter, en unités du monde ; tout ce qui est plus haut est un mur le long duquel il glisse. La valeur par défaut est `0.3` pour une forme dont la plus petite demi-taille est `0.3` (une capsule de rayon `0.3`), et elle s'adapte à la forme, si bien qu'un joueur de 30 pixels dans un monde 2D commence à 30 pixels. La même distance est celle jusqu'à laquelle le joueur reste collé au sol en passant le rebord d'une marche, si bien qu'une marche plus haute le garde aussi collé au sol dans les descentes. Les valeurs négatives sont ramenées à `0`.
*Depuis :* 3.00.
*Voir aussi :* <<playersetslope,playerSetSlope>>, <<playernew,playerNew>>
.Exemple
[source,lua]
----
-- Les marches de ce niveau montent de 0,35 chacune ; s'assurer que le héros les gravit.
playerNew(hero, 0.3, 1.0)
playerSetStep(hero, 0.4)
-- Les rebords du jeu de plateformes 2D font 26 pixels.
playerNew(runner, 16, 48)
playerSetStep(runner, 26)
----
[#playersetslope]
==== playerSetSlope
[source,text]
----
playerSetSlope(node, degrees)
----
La pente la plus raide que le joueur gravit, en degrés depuis l'horizontale ; un sol plus raide ne compte pas comme un appui, si bien que le joueur glisse dessus et ne peut pas en sauter. La valeur par défaut est `45`, et la valeur est bornée entre `0` et `89`. Une valeur basse fait des collines des obstacles, une valeur élevée laisse le joueur gravir presque tout ce qui n'est pas un mur.
*Depuis :* 3.00.
*Voir aussi :* <<playersetstep,playerSetStep>>, <<playerisonground,playerIsOnGround>>
.Exemple
[source,lua]
----
-- La chèvre des montagnes grimpe là où le héros glisse.
playerNew(goat, 0.25, 0.8)
playerSetSlope(goat, 70)
playerNew(hero, 0.3, 1.0)
playerSetSlope(hero, 40)
----
[#playersetmass]
==== playerSetMass
[source,text]
----
playerSetMass(node, kilograms)
----
La masse du joueur, utilisée quand il bouscule des corps dynamiques et comme son poids sur ceux sur lesquels il se tient. La valeur par défaut est `70` kilogrammes ; les valeurs inférieures à `0.001` sont relevées à ce minimum. La masse n'a aucun effet sur la façon dont le joueur tombe ou marche, seulement sur les bousculades : associez-la donc à `playerSetPush`.
*Depuis :* 3.00.
*Voir aussi :* <<playersetpush,playerSetPush>>, <<bodysetmass,bodySetMass>>
.Exemple
[source,lua]
----
-- Le troll fonce à travers ce que le héros doit contourner.
troll = nodeNew()
nodeSetPosition(troll, 6, 0, -3)
playerNew(troll, 0.6, 2.4)
playerSetMass(troll, 400)
playerSetPush(troll, 2000)
playerSetStep(troll, 0.8)
----
[#playersetpush]
==== playerSetPush
[source,text]
----
playerSetPush(node, strength)
----
La force avec laquelle le joueur bouscule les corps dynamiques qu'il heurte en marchant, en newtons. La valeur par défaut, `300`, déplace les caisses et les ballons mais pas les armoires ; `0` contourne tout comme si c'était statique. Les valeurs négatives sont ramenées à `0`. Le `bodySetMass` propre d'un corps décide de ce qu'une force donnée peut déplacer.
*Depuis :* 3.00.
*Voir aussi :* <<playersetmass,playerSetMass>>, <<bodysetmass,bodySetMass>>
.Exemple
[source,lua]
----
-- La potion de force permet au héros de déplacer le rocher pendant dix secondes.
function drinkStrength()
playerSetPush(hero, 3000)
strengthUntil = frames + 600
end
function onOverlayUpdate()
if strengthUntil and frames > strengthUntil then
playerSetPush(hero, 300)
strengthUntil = nil
end
return OVERLAY_NOT_UPDATED
end
----
[#playersetgravityscale]
==== playerSetGravityScale
[source,text]
----
playerSetGravityScale(node, scale)
----
Multiplie la gravité du monde pour ce seul joueur : `1` (la valeur par défaut) tombe comme tout le reste, `0` flotte sur place (une échelle, une salle en apesanteur, une monture volante), une valeur plus grande fait tomber plus vite pour une sensation de lourdeur, et une valeur plus petite donne un saut flottant. Cela ne change pas ce que fait `playerJump` au décollage, seulement la vitesse à laquelle le joueur redescend. Pendant la nage, la vitesse de descente de l'eau prend la place de la gravité.
*Depuis :* 3.00.
*Voir aussi :* <<physicssetgravity,physicsSetGravity>>, <<playerjump,playerJump>>, <<playersetswim,playerSetSwim>>
.Exemple
[source,lua]
----
-- Sur l'échelle, le héros grimpe avec le stick au lieu de tomber.
function onTrigger(trigger, other, entered)
if trigger == ladder and other == hero then
onLadder = entered
playerSetGravityScale(hero, entered and 0 or 1)
end
end
----
[#playersetposition]
==== playerSetPosition
[source,text]
----
playerSetPosition(node, x, y, z)
----
Téléporte le joueur à une position du monde, sans rien toucher en chemin, et l'arrête net. Le nœud se déplace avec lui et garde sa rotation. Servez-vous-en pour l'apparition, la réapparition et les transitions de niveau ; déplacer plutôt le nœud avec `nodeSetPosition` laisse le joueur où il était, et le nœud est ramené au pas suivant.
*Depuis :* 3.00.
*Voir aussi :* <<playersetvelocity,playerSetVelocity>>, <<nodesetposition,nodeSetPosition>>
.Exemple
[source,lua]
----
-- Retour au dernier point de contrôle après une chute dans la fosse.
function onTrigger(trigger, other, entered)
if trigger == pit and other == hero and entered then
lives = lives - 1
playerSetPosition(hero, checkpointX, checkpointY, checkpointZ)
end
end
----
[#playersetenabled]
==== playerSetEnabled
[source,text]
----
playerSetEnabled(node, enabled)
----
Avec `false`, le joueur quitte la simulation : son corps est retiré du monde, si bien qu'il ne bouge plus, ne tombe plus, ne pilote plus son nœud et ne bloque plus rien, `onTrigger` signale qu'il quitte chaque déclencheur dans lequel il se trouvait, et il est exclu du dessin de débogage. `true` remet le corps en place et le reprend là où se trouve le nœud. Désactivez un joueur tant qu'un ragdoll, une cinématique ou un véhicule a la main sur le personnage.
*Depuis :* 3.00.
*Voir aussi :* <<playerdelete,playerDelete>>, <<ragdollnew,ragdollNew>>, <<bodysetenabled,bodySetEnabled>>
.Exemple
[source,lua]
----
-- Un coup violent confie le squelette du héros au ragdoll.
function onCollision(a, b, x, y, z, speed)
if (a == hero or b == hero) and speed > 6 then
playerSetEnabled(hero, false)
ragdollActivate(fox)
end
end
----
[#ragdoll]
=== Ragdoll
Le squelette d'un modèle skinné confié à la physique (voir <<physics,Physique>>, Ragdolls). Un ragdoll est désigné par le nœud donné à `ragdollNew`, généralement la racine d'une instance de modèle, et un nœud sans ragdoll met fin au script dans chaque appel après `ragdollNew`. Les os sont nommés d'après les nœuds de leurs articulations, les noms que voit `nodeFind`, comparés sans tenir compte de la casse ; les distances sont en unités du monde, les angles en degrés, les impulsions en newtons-secondes et la force des moteurs en newtons-mètres. Jusqu'à huit ragdolls à la fois, de 48 os au plus chacun.
[#ragdollnew]
==== ragdollNew
[source,text]
----
ragdollNew(node)
----
Cherche dans le nœud et sous lui le premier maillage skinné et construit une recette de ragdoll à partir de son squelette : chaque articulation qui a une articulation enfant devient un os, une capsule allant de l'articulation vers ses enfants, reliée à l'os le plus proche au-dessus d'elle par une articulation swing-twist limitée à 45 degrés de balancement et à 30 de torsion. Les articulations sans articulation enfant, comme le bout des doigts et le bout d'une queue, suivent leur os parent et ne peuvent pas être nommées dans les autres appels. Rien n'est encore simulé : le modèle continue de s'animer jusqu'à `ragdollActivate`. L'appeler de nouveau sur le même nœud abandonne d'abord l'ancien ragdoll. Met fin au script quand la physique n'est pas disponible, quand aucun maillage skinné doté d'os ne se trouve sous le nœud, ou quand huit ragdolls existent déjà.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollactivate,ragdollActivate>>, <<ragdollsetjoint,ragdollSetJoint>>, <<modelinstance,modelInstance>>
.Exemple
[source,lua]
----
-- Un garde qui fonce sur le joueur ; le ragdoll est prêt avant le plaquage.
local guard = nodeNew()
nodeSetPosition(guard, -1, 0.05, -1)
playerNew(guard, 0.3, 0.9)
local guardFox = modelInstance(foxModel)
nodeSetParent(guardFox, guard)
nodeSetScale(guardFox, 0.01)
nodeSetRotation(guardFox, 0, 90, 0)
animationPlay(guardFox, "Run", true)
ragdollNew(guardFox) -- Recette seulement ; il court toujours
----
[#ragdolldelete]
==== ragdollDelete
[source,text]
----
ragdollDelete(node)
----
Supprime le ragdoll : ses corps et ses articulations s'il est actif, puis la recette. Les nœuds des articulations restent là où la physique les a laissés, si bien que le modèle reste couché tel qu'il est tombé jusqu'à ce qu'une animation le déplace, et le nœud est libre pour un nouveau `ragdollNew`. Un nœud sans ragdoll met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollnew,ragdollNew>>, <<ragdolldeactivate,ragdollDeactivate>>, <<nodedelete,nodeDelete>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
for i = #fallen, 1, -1 do
local body = fallen[i]
body.restingFrames = ragdollIsResting(body.node) and body.restingFrames + 1 or 0
if body.restingFrames > 300 then
ragdollDelete(body.node) -- Immobile depuis cinq secondes : on fait le ménage
nodeDelete(body.node)
table.remove(fallen, i)
end
end
return OVERLAY_NOT_UPDATED
end
----
[#ragdollsetjoint]
==== ragdollSetJoint
[source,text]
----
ragdollSetJoint(node, jointName, radius, swingDegrees, twistDegrees)
----
Règle la recette d'un os. Les valeurs sont lues quand les corps sont créés : appelez-le donc avant `ragdollActivate` ; un ragdoll déjà actif garde ses corps jusqu'à ce qu'il soit désactivé puis activé de nouveau. Le rayon `0` demande la capsule automatique, 0,22 fois la longueur de l'os et jamais plus fine que `0.02` unité. Le balancement est le demi-angle du cône dans lequel l'os peut se déplacer par rapport à son parent, la torsion jusqu'où il peut tourner autour de son propre axe ; les deux sont bornés entre `0` et `179`. Seuls les os peuvent être nommés, pas les articulations terminales ; le nom est comparé sans tenir compte de la casse, et un nom qui ne correspond à aucun os met fin au script.
*Paramètres :*
* `node` -- le nœud du ragdoll.
* `jointName` -- le nom du nœud de l'articulation, tel qu'il est dans le modèle.
* `radius` -- le rayon de la capsule en unités du monde, ou `0` pour l'automatique.
* `swingDegrees` -- jusqu'où l'os peut se balancer par rapport à son parent ; `45` par défaut.
* `twistDegrees` -- jusqu'où il peut se tordre autour de son propre axe ; `30` par défaut.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollnew,ragdollNew>>, <<ragdollactivate,ragdollActivate>>, <<nodefind,nodeFind>>
.Exemple
[source,lua]
----
ragdollNew(fox)
ragdollSetJoint(fox, "b_Neck_04", 0, 30, 20) -- Un cou plus raide
ragdollSetJoint(fox, "b_Tail01_012", 0.03, 80, 60) -- Une queue souple comme un fouet
for _, leg in ipairs({ "b_LeftLeg01_015", "b_RightLeg01_019" }) do
ragdollSetJoint(fox, leg, 0, 60, 15) -- Les hanches se balancent largement, se tordent à peine
end
ragdollActivate(fox)
----
[#ragdollactivate]
==== ragdollActivate
[source,text]
----
ragdollActivate(node)
----
Confie le squelette à la physique à partir de sa pose actuelle : chaque couche d'animation du nœud s'arrête, une capsule dynamique est créée pour chaque os là où se trouve son articulation à cet instant, les articulations swing-twist sont créées entre elles, et dès lors chaque pas de physique pilote les nœuds des articulations à partir des corps, si bien que le maillage skinné suit. Les os entrent en collision avec le monde et entre eux, sauf chaque os avec son parent. La pose à ce moment-là est celle vers laquelle tire `ragdollSetStrength`. Un ragdoll déjà actif est laissé tel quel. Met fin au script quand le corps d'un os ne peut pas être créé.
*Depuis :* 3.00.
*Voir aussi :* <<ragdolldeactivate,ragdollDeactivate>>, <<ragdollapplyimpulse,ragdollApplyImpulse>>, <<ragdollsetstrength,ragdollSetStrength>>
.Exemple
[source,lua]
----
local crashed = false
function onCollision(a, b, x, y, z, speed)
if not crashed and (a == guard or b == guard) and speed > 1.5 then
crashed = true
playerSetEnabled(guard, false) -- Le contrôleur lâche prise
ragdollActivate(guardFox) -- La physique prend le squelette en charge à partir d'ici
ragdollApplyImpulse(guardFox, "b_Spine02_03", 40, 30, 0)
end
end
----
[#ragdolldeactivate]
==== ragdollDeactivate
[source,text]
----
ragdollDeactivate(node)
----
Supprime les corps et les articulations et rend le squelette. Les nœuds des articulations gardent la pose dans laquelle le ragdoll les a laissés, et rien ne les déplace tant qu'aucune animation ne joue ; un `animationPlay` avec un fondu part de cette pose en mélangeant, si bien qu'un personnage à terre peut se relever. La recette reste, si bien que `ragdollActivate` peut reprendre la main plus tard à partir de l'endroit où l'animation a amené le squelette. Ne fait rien à un ragdoll qui n'est pas actif.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollactivate,ragdollActivate>>, <<animationplay,animationPlay>>
.Exemple
[source,lua]
----
-- Une fois le corps immobilisé, le héros se relève en une demi-seconde.
function onOverlayUpdate()
if downed and ragdollIsResting(hero) then
downed = false
ragdollDeactivate(hero)
animationPlay(hero, "GetUp", false, 1, 0.5)
playerSetEnabled(heroController, true)
end
return OVERLAY_NOT_UPDATED
end
----
[#ragdollsetstrength]
==== ragdollSetStrength
[source,text]
----
ragdollSetStrength(node, strength)
----
Motorise chaque articulation avec un moteur qui tire vers la pose dans laquelle le ragdoll a été activé, limité à ce nombre de newtons-mètres ; `0` (par défaut) coupe les moteurs et le corps devient mou. Les moteurs sont des ressorts à 4 Hz, à amortissement critique. La valeur peut être réglée avant l'activation ou pendant que le ragdoll bouge, et prend effet au pas suivant. Cela ne remet pas un corps debout, puisque rien ne prend appui sur le sol : quelques dizaines de newtons-mètres changent une chute en titubement et permettent à un personnage à terre de ramener ses membres.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
.Exemple
[source,lua]
----
-- Mis à terre, tout mou ; un instant plus tard, le combattant se raidit et se recroqueville.
ragdollActivate(fighter)
ragdollSetStrength(fighter, 0)
downFrames = 0
function onOverlayUpdate()
downFrames = downFrames + 1
if downFrames == 60 then
ragdollSetStrength(fighter, 60)
end
return OVERLAY_NOT_UPDATED
end
----
[#ragdollapplyimpulse]
==== ragdollApplyImpulse
[source,text]
----
hit = ragdollApplyImpulse(node, jointName, x, y, z)
----
Ajoute une impulsion en newtons-secondes, selon les axes du monde, au corps de l'os qui commence à l'articulation nommée : le coup de poing, le tir, l'explosion. Le ragdoll doit être actif : appelez donc d'abord `ragdollActivate`, puis poussez. Renvoie `false`, et ne fait rien, quand le ragdoll n'est pas actif ou quand aucun os ne porte ce nom (les articulations terminales ne sont pas des os) ; le nom est comparé sans tenir compte de la casse.
*Paramètres :*
* `node` -- le nœud du ragdoll.
* `jointName` -- le nom du nœud de l'articulation, tel qu'il est dans le modèle.
* `x`, `y`, `z` -- l'impulsion en newtons-secondes, selon les axes du monde.
*Renvoie :* Vrai quand l'os a été trouvé et que le ragdoll était actif.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollactivate,ragdollActivate>>, <<bodyapplyimpulse,bodyApplyImpulse>>, <<nodefind,nodeFind>>
.Exemple
[source,lua]
----
-- Un impact depuis la position du tireur : l'impulsion s'éloigne de l'arme.
function shoot(target, gunX, gunY, gunZ, boneName)
local tx, ty, tz = nodeGetWorldPosition(target)
local dx = tx - gunX
local dz = tz - gunZ
local length = math.sqrt(dx * dx + dz * dz)
if not ragdollIsActive(target) then
ragdollActivate(target)
end
ragdollApplyImpulse(target, boneName, dx / length * 30, 10, dz / length * 30)
end
----
[#ragdollisactive]
==== ragdollIsActive
[source,text]
----
active = ragdollIsActive(node)
----
Indique si la physique pilote le squelette en ce moment : `true` de `ragdollActivate` jusqu'à `ragdollDeactivate` ou `ragdollDelete`. Un nœud sans ragdoll met fin au script.
*Renvoie :* Vrai ou faux.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollactivate,ragdollActivate>>, <<ragdollisresting,ragdollIsResting>>
.Exemple
[source,lua]
----
-- La barre de vie flotte au-dessus du modèle seulement tant qu'il est debout.
function onOverlayUpdate()
overlayClear()
for _, enemy in ipairs(enemies) do
if not ragdollIsActive(enemy.node) then
local x, y, z = nodeGetWorldPosition(enemy.node)
local sx, sy = sceneProject(x, y + 1.2, z)
drawHealthBar(sx, sy, enemy.health)
end
end
return OVERLAY_UPDATED
end
----
[#ragdollisresting]
==== ragdollIsResting
[source,text]
----
resting = ragdollIsResting(node)
----
Indique si le corps de chaque os s'est mis en sommeil, ce que fait la physique quand un corps est resté immobile un moment. `false` tant que le ragdoll n'est pas actif : cela ne permet donc pas de distinguer un modèle inactif d'un modèle qui s'est immobilisé ; vérifiez aussi `ragdollIsActive`.
*Renvoie :* Vrai ou faux.
*Depuis :* 3.00.
*Voir aussi :* <<ragdollisactive,ragdollIsActive>>, <<ragdolldeactivate,ragdollDeactivate>>
.Exemple
[source,lua]
----
-- La manche se termine une fois que le perdant a cessé de bouger.
function onOverlayUpdate()
if roundOver and ragdollIsResting(loser) then
restingFrames = restingFrames + 1
if restingFrames == 90 then
showResults()
end
end
return OVERLAY_NOT_UPDATED
end
----
[#save]
=== Sauvegarde
Tout ce que le jeu doit encore savoir la prochaine fois qu'il démarre : les
meilleurs scores, les options du joueur, jusqu'où il est allé. Une table par
jeu, gardée en JSON dans le dossier de données propre au jeu (voir
<<singegetdatapath,`singeGetDataPath`>>), si bien qu'un `.game` empaqueté reste
en lecture seule et que deux jeux ne se marchent jamais sur les pieds.
Les clés sont à vous. Les valeurs peuvent être des nombres, des chaînes, des
booléens ou des tables de ceux-ci, imbriquées aussi profondément que vous le
voulez -- toute une table des meilleurs scores entre comme une seule valeur. Une
fonction, ou un handle vers quelque chose qui appartient au moteur, n'y entre
pas : ni l'une ni l'autre ne veut rien dire à l'exécution suivante, et en
sauvegarder une est une erreur plutôt qu'une surprise plus tard.
**Ce que vous réglez est ce qui est sauvegardé, et ce que vous récupérez est à
vous.** Les tables sont copiées à l'entrée et à la sortie : modifier une table
que vous avez relue ne change donc rien tant que vous ne la sauvegardez pas de
nouveau, et modifier une table que vous avez sauvegardée n'atteint pas la
sauvegarde après coup. Pour garder un aperçu que le joueur annule ensuite, il
suffit par conséquent de ne pas le sauvegarder.
L'écriture a lieu une fois à la fin de toute image qui a changé quelque chose,
quel que soit le nombre de clés que vous y avez réglées, et de nouveau quand le
jeu s'arrête -- y compris tout ce qui est réglé dans <<onshutdown,`onShutdown`>>.
Pour une borne qui peut perdre le courant à tout moment, <<saveflush,`saveFlush`>>
écrit immédiatement. Le fichier est écrit à côté de l'ancien puis renommé
par-dessus : une coupure de courant pendant une écriture laisse donc la
sauvegarde précédente plutôt que la moitié d'une nouvelle.
Les nombres entiers reviennent sous forme d'entiers. JSON n'a qu'une sorte de
nombre et Lua en a deux : sans cela, un score sauvegardé comme `100` reviendrait
en `100.0` et s'afficherait avec un point décimal.
[#saveclear]
==== saveClear
[source,text]
----
saveClear()
----
Oublie tout et écrit la sauvegarde vide. Pour la « remise à zéro des meilleurs scores » d'un menu de service.
*Paramètres :*
* Aucun.
*Renvoie :* rien.
*Depuis :* 3.00.
[#savedelete]
==== saveDelete
[source,text]
----
saveDelete(key)
----
Retire une clé. Supprimer une clé qui n'a jamais existé n'est pas une erreur.
*Paramètres :*
* `key` -- la clé à retirer.
*Renvoie :* rien.
*Depuis :* 3.00.
[#saveflush]
==== saveFlush
[source,text]
----
saveFlush()
----
Écrit la sauvegarde tout de suite plutôt qu'à la fin de l'image. Utilisez-la après quelque chose que vous seriez désolé de perdre -- un nouveau meilleur score sur une borne qu'on éteint à la prise.
*Paramètres :*
* Aucun.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<saveset,saveSet>>
[#saveget]
==== saveGet
[source,text]
----
value = saveGet(key [, default])
----
Ce qui a été sauvegardé sous cette clé. Quand rien ne l'a été, c'est la valeur par défaut qui revient, ou `nil` quand il n'y en a pas -- c'est ainsi qu'on distingue un premier lancement des suivants.
*Paramètres :*
* `key` -- la clé à lire.
* `default` -- facultatif ; ce qu'il faut répondre quand la clé n'existe pas.
*Renvoie :* la valeur sauvegardée, la valeur par défaut ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<saveset,saveSet>>
.Exemple
[source,lua]
----
-- Compter les lancements, et retenir les dix meilleurs scores.
local runs = saveGet("runs", 0) + 1
saveSet("runs", runs)
local best = saveGet("highScores", {})
best[#best + 1] = { name = initials, score = score }
table.sort(best, function(a, b) return a.score > b.score end)
while #best > 10 do table.remove(best) end
saveSet("highScores", best)
saveFlush()
----
[#savegetall]
==== saveGetAll
[source,text]
----
everything = saveGetAll()
----
Une copie de toute la sauvegarde, pour la lecture : l'afficher en entier pendant le débogage, ou parcourir des clés dont le jeu ne connaît pas les noms d'avance. Modifier ce qui revient ne change rien -- c'est <<savesetall,`saveSetAll`>> qui écrit une table entière.
*Paramètres :*
* Aucun.
*Renvoie :* une table ; vide quand rien n'a été sauvegardé.
*Depuis :* 3.00.
*Voir aussi :* <<savesetall,saveSetAll>>, <<saveget,saveGet>>
.Exemple
[source,lua]
----
-- Qu'y a-t-il dedans ? inspect est fourni justement pour cela.
debugPrint(require("inspect")(saveGetAll()))
----
[#saveset]
==== saveSet
[source,text]
----
saveSet(key, value)
----
Garde une valeur jusqu'à ce que le jeu soit désinstallé. L'écriture a lieu à la fin de l'image : régler vingt clés dans une boucle n'écrit donc qu'un seul fichier.
*Paramètres :*
* `key` -- la clé à écrire.
* `value` -- un nombre, une chaîne, un booléen ou une table de ceux-ci.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<saveget,saveGet>>, <<saveflush,saveFlush>>
[#savesetall]
==== saveSetAll
[source,text]
----
saveSetAll(everything)
----
Remplace toute la sauvegarde par cette table, en une seule étape. Les clés qui existaient et ne sont pas dans la table que vous donnez disparaissent.
Elle existe pour faire passer une ancienne sauvegarde à une nouvelle organisation. Le faire avec <<saveclear,`saveClear`>> et une boucle de <<saveset,`saveSet`>> laisse un moment où la sauvegarde est vide, et une machine éteinte à ce moment-là perd tout ; cette fonction n'a pas ce moment, car le fichier est écrit une seule fois à la fin de l'image à partir de la table terminée.
*Paramètres :*
* `everything` -- table ; ce que la sauvegarde doit désormais contenir. Les mêmes valeurs que prend `saveSet`.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<savegetall,saveGetAll>>, <<saveclear,saveClear>>
.Exemple
[source,lua]
----
-- La version 1 gardait un score dans deux clés ; la version 2 en garde une table.
if saveGet("version", 1) < 2 then
local old = saveGetAll()
saveSetAll({
version = 2,
scores = { { name = old.hiName, score = old.hiScore } },
runs = old.runs,
})
saveFlush()
end
----
[#scene]
=== Scène
La scène est la couche 3D dessinée entre la vidéo du disque et l'overlay ; les nœuds placés dans la couche de premier plan avec `nodeSetLayer` sont dessinés après l'overlay à la place. Elle est désactivée jusqu'à `sceneEnable(true)`, a la taille de l'overlay et est effacée à la couleur de `sceneSetBackground` à chaque image. Les appels `sceneSet*` règlent l'aspect de toute la couche : lumière ambiante, ciel, brouillard, exposition et courbe de tons, bloom, anticrénelage et qualité des ombres ; les couleurs sont des entiers de `0` à `255` et les distances sont en unités du monde. `sceneProject` et `sceneUnproject` font le lien entre l'espace du monde et les coordonnées de l'overlay, et `sceneGetStats` rend compte du travail de la dernière image. Voir <<scenes3d,Scènes 3D>>.
[#sceneenable]
==== sceneEnable
[source,text]
----
sceneEnable(enabled)
----
Active ou désactive la couche 3D. Les nœuds, les maillages et les matériaux peuvent être construits avant qu'elle soit active et survivent à sa désactivation ; seul le dessin s'arrête. Met fin au script quand on l'active sans GPU utilisable (voir <<scenes3d,Scènes 3D>>) : un jeu qui a une solution de repli en 2D interroge donc d'abord `singeCapabilities().SCENE`. Dans les deux cas, l'affichage se rafraîchit à l'image suivante.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetbackground,sceneSetBackground>>, <<scenesetantialias,sceneSetAntialias>>
.Exemple
[source,lua]
----
-- Se rabattre sur le simple overlay quand la 3D n'est pas disponible.
has3d = singeCapabilities().SCENE
if has3d then
sceneEnable(true)
buildArena()
else
debugPrint("No 3D here; using the 2D attract mode.")
end
function onPause()
sceneEnable(false)
end
----
[#scenegetsize]
==== sceneGetSize
[source,text]
----
width, height = sceneGetSize()
----
La taille de la couche, qui est la résolution de l'overlay : l'espace dans lequel travaillent `sceneProject` et `sceneUnproject`, et la taille à laquelle la scène est rendue. Changez-la avec `overlaySetResolution`. Avant que la couche ait reçu une taille, les deux valeurs valent `0`.
*Renvoie :* Deux entiers, la largeur et la hauteur en coordonnées de l'overlay.
*Depuis :* 3.00.
*Voir aussi :* <<overlaysetresolution,overlaySetResolution>>, <<sceneunproject,sceneUnproject>>
.Exemple
[source,lua]
----
-- Un réticule au centre de la vue, et ce qui se trouve dessous.
function onOverlayUpdate()
local width, height = sceneGetSize()
local cx = width / 2
local cy = height / 2
local ox, oy, oz = sceneUnproject(cx, cy, 0)
local fx, fy, fz = sceneUnproject(cx, cy, 50)
target = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
fontPrint(cx - 4, cy - 8, "+")
return OVERLAY_UPDATED
end
----
[#scenegetstats]
==== sceneGetStats
[source,text]
----
total, drawn, calls, textureKB = sceneGetStats()
----
Les chiffres de la dernière image pour la caméra de la fenêtre : les maillages rassemblés pour le dessin, combien d'entre eux étaient dans le champ de la caméra, les appels de dessin qu'ils sont devenus après instanciation, et la mémoire de textures que tient la scène, en kilo-octets. Pour la mise au point : une scène avec des centaines de `drawn` mais une poignée de `calls` partage bien ses maillages, et un `textureKB` qui ne tiendra pas dans la mémoire d'un Raspberry Pi réclame des textures KTX2. Les vues rendues ne comptent pas.
*Renvoie :* Quatre entiers.
*Depuis :* 3.00.
*Voir aussi :* <<sceneenable,sceneEnable>>, <<modelinstance,modelInstance>>, <<materialsettexture,materialSetTexture>>
.Exemple
[source,lua]
----
-- Un afficheur de débogage dans le coin pendant la mise au point du niveau.
function onOverlayUpdate()
if showStats then
local total, drawn, calls, textureKB = sceneGetStats()
fontPrint(10, 10, string.format("%d meshes, %d in view, %d calls, %d MB textures", total, drawn, calls, math.floor(textureKB / 1024)))
end
return OVERLAY_UPDATED
end
----
[#scenemeasure]
==== sceneMeasure
[source,text]
----
sceneMeasure(on)
ms = sceneMeasure(false)
----
Chronomètre la scène 3D. `sceneMeasure(true)` démarre : chaque image qui suit
attend que le GPU ait fini la scène et additionne ce temps, si bien que ce qui
est mesuré est le travail plutôt que la cadence de l'affichage.
`sceneMeasure(false)` arrête et renvoie le nombre moyen de millisecondes par
image, ou `nil` quand aucune image n'a été dessinée. L'attente coûte de la
fréquence d'images : chronométrez donc quelques secondes, pas une partie entière.
*Renvoie :* rien au démarrage ; un nombre ou `nil` à l'arrêt.
*Depuis :* 3.00.
*Voir aussi :* <<singesavetier,singeSaveTier>>, <<scenegetstats,sceneGetStats>>
.Exemple
[source,lua]
----
-- Deux secondes de l'écran titre disent si tous les effets passent.
sceneMeasure(true)
-- ... deux secondes plus tard :
local ms = sceneMeasure(false)
if ms and (ms > 8) then
sceneSetBloom(1.0, 0.0)
end
----
[#sceneprobegui]
==== sceneProbeGui
[source,text]
----
gui, x, y = sceneProbeGui(sx, sy)
----
Quelle GUI se trouve sur la surface sous un point de l'overlay, et à quel endroit de celle-ci : un rayon qui part de la caméra et passe par le point, avec la caméra de la dernière image rendue, lancé contre les triangles de chaque nœud visible dont le matériau affiche une GUI (`materialSetGui`), les coordonnées de texture de l'impact le plus proche donnant l'endroit sur la GUI. Renvoie le handle de la GUI et le pixel dans les coordonnées propres à cette GUI (`0, 0` en haut à gauche, `guiGetWidth`, `guiGetHeight` en bas à droite), ou `nil` seul quand le point n'est sur aucune de ces surfaces, que la scène est désactivée ou que rien n'a encore été rendu. Le moteur pose la même question quand la souris ou un pistolet optique vise la scène alors qu'une GUI qui accepte les entrées est sur une surface et que rien de dessiné à plat ne se trouve sous le pointeur : les clics sur un moniteur dans la scène n'ont donc besoin d'aucun script ; l'appel sert au jeu qui veut la réponse lui-même, pour une visée laser, un effet de survol ou un test.
*Paramètres :*
* `sx, sy` -- le point, en coordonnées de l'overlay (une position de souris ou de pistolet optique).
*Renvoie :* Un entier et deux entiers, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<materialsetgui,materialSetGui>>, <<guisetinput,guiSetInput>>, <<sceneproject,sceneProject>>, <<sceneunproject,sceneUnproject>>
.Exemple
[source,lua]
----
-- Le point d'un pistolet optique brille là où il tombe sur l'écran du cockpit.
function onMouseMoved(x, y)
local g, px, py = sceneProbeGui(x, y)
if g == screenGui then
guiSetValue(g, screenPage, "dot", string.format("%d,%d", px, py))
end
end
----
[#sceneproject]
==== sceneProject
[source,text]
----
x, y, depth, inFront = sceneProject(wx, wy, wz)
----
Où tombe un point du monde en coordonnées de l'overlay, avec la caméra de la dernière image rendue. `depth` vaut `0` au plan proche et `1` au plan lointain, pour trier des étiquettes ou les estomper avec la distance. `inFront` vaut `false` quand le point est derrière la caméra, et `x` et `y` n'ont alors aucun sens ; avant que la première image ait été rendue, il n'y a rien contre quoi projeter, et toutes les valeurs reviennent à `0` avec `inFront` à `false`. Les points situés devant mais hors de la vue reviennent avec des coordonnées au-delà des bords de l'overlay : vérifiez-les donc avec `sceneGetSize` avant de dessiner. L'usage habituel est une étiquette de nom ou une barre de vie dessinée avec `fontPrint` ou `spriteDraw` au-dessus d'un objet 3D.
*Paramètres :*
* `wx, wy, wz` -- le point du monde, en unités du monde.
*Renvoie :* Deux nombres, un nombre et un booléen.
*Depuis :* 3.00.
*Voir aussi :* <<sceneunproject,sceneUnproject>>, <<nodegetworldposition,nodeGetWorldPosition>>, <<scenegetsize,sceneGetSize>>
.Exemple
[source,lua]
----
-- Un nom et une barre de vie qui flottent au-dessus de chaque personnage.
function onOverlayUpdate()
for _, enemy in ipairs(enemies) do
local x, y, depth, visible = sceneProject(nodeGetWorldPosition(enemy.node))
if visible then
fontPrint(x - 30, y - 80, enemy.name)
overlayBox(x - 30, y - 60, x - 30 + enemy.health * 0.6, y - 54)
end
end
return OVERLAY_UPDATED
end
----
[#sceneunproject]
==== sceneUnproject
[source,text]
----
x, y, z = sceneUnproject(sx, sy, distance)
----
Le point du monde situé à `distance` unités le long du rayon qui passe par un pixel de l'overlay, avec la caméra de la dernière image rendue. Deux distances donnent un rayon pour désigner un objet avec `physicsRaycast` ; une seule place un objet « là où le joueur a cliqué ». Avant que la première image ait été dessinée, elle renvoie l'origine.
*Paramètres :*
* `sx, sy` -- le pixel de l'overlay, en coordonnées de l'overlay.
* `distance` -- la distance le long du rayon, en unités du monde ; `0` est le plan proche.
*Renvoie :* Trois nombres, le point du monde.
*Depuis :* 3.00.
*Voir aussi :* <<sceneproject,sceneProject>>, <<mousegetposition,mouseGetPosition>>, <<physicsraycast,physicsRaycast>>
.Exemple
[source,lua]
----
-- Poser un repère sur ce que le joueur a cliqué, ou à 8 unités s'il n'y a rien.
function onMouseDown(button)
local mx, my = mouseGetPosition(0)
local ox, oy, oz = sceneUnproject(mx, my, 0)
local fx, fy, fz = sceneUnproject(mx, my, 100)
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
if hit then
nodeSetPosition(marker, hx, hy, hz)
else
nodeSetPosition(marker, sceneUnproject(mx, my, 8))
end
end
----
[#scenesetambient]
==== sceneSetAmbient
[source,text]
----
sceneSetAmbient(r, g, b)
----
De la lumière venue de partout, de `0` à `255` par canal, traitée comme du sRGB ; par défaut, un gris sombre de `26, 26, 26`. Elle éclaircit les faces qu'aucune lumière n'atteint pour qu'elles ne soient pas noires, et `materialSetOcclusionMap` l'assombrit dans les creux. Quand un ciel est réglé et que `sceneSetEnvironment` est actif, la lumière du ciel remplace cette valeur.
*Depuis :* 3.00.
*Voir aussi :* <<lightnew,lightNew>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetsky,sceneSetSky>>
.Exemple
[source,lua]
----
-- Une cour au clair de lune : un bleu léger partout, une lune directionnelle froide.
sceneSetAmbient(20, 25, 45)
moon = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(moon, -5, 10, 3)
nodeLookAt(moon, 0, 0, 0)
lightSetColor(moon, 170, 190, 255)
lightSetIntensity(moon, 0.4)
----
[#scenesetantialias]
==== sceneSetAntialias
[source,text]
----
sceneSetAntialias(antialias)
----
Active ou désactive le multiéchantillonnage 4x ; il est actif par défaut partout où le GPU le propose pour les formats de la couche, et absent sans bruit là où il ne le propose pas. Le désactiver fait gagner de la vitesse sur un Raspberry Pi. Les cibles de rendu sont reconstruites aussitôt : appelez-la donc pendant la mise en place plutôt qu'à chaque image.
*Depuis :* 3.00.
*Voir aussi :* <<sceneenable,sceneEnable>>, <<scenesetshadowsize,sceneSetShadowSize>>
.Exemple
[source,lua]
----
-- Un réglage de qualité venu de l'écran d'options du jeu.
function applyQuality(level)
if level == "low" then
sceneSetAntialias(false)
sceneSetShadowSize(512)
sceneSetShadowCascades(2)
else
sceneSetAntialias(true)
sceneSetShadowSize(2048)
sceneSetShadowCascades(3)
end
end
----
[#scenesetbackground]
==== sceneSetBackground
[source,text]
----
sceneSetBackground(r, g, b)
sceneSetBackground(r, g, b, a)
----
La couleur à laquelle la couche est effacée à chaque image, de `0` à `255` par canal. `a` vaut `255` quand il est omis, une toile de fond opaque ; en dessous de `255`, la vidéo transparaît, et `0` (le choix habituel pour un jeu avec un disque) rend le fond entièrement transparent. Tant qu'elle n'est pas appelée, la couche est effacée en noir transparent. Un ciel de `sceneSetSky` est dessiné par-dessus le fond : la vidéo ne transparaît donc plus tant qu'un ciel est réglé.
*Paramètres :*
* `r, g, b` -- la couleur d'effacement.
* `a` -- l'alpha d'effacement ; `255` quand il est omis.
*Depuis :* 3.00.
*Voir aussi :* <<sceneenable,sceneEnable>>, <<scenesetsky,sceneSetSky>>, <<scenesetfog,sceneSetFog>>
.Exemple
[source,lua]
----
-- Le disque se voit derrière le cockpit 3D ; un ciel nocturne uni pour le menu.
function showCockpit()
sceneSetBackground(0, 0, 0, 0)
discPlay()
end
function showMenu()
discPause()
sceneSetBackground(10, 12, 30)
end
----
[#scenesetbloom]
==== sceneSetBloom
[source,text]
----
sceneSetBloom(threshold, strength)
----
Un halo autour de tout ce qui est plus lumineux que `threshold`, pris dans la lumière linéaire de la scène avant l'exposition et la courbe de tons (où `1` est le blanc), puis rajouté avec la force `strength`. Par défaut, le seuil vaut `1` et la force `0`, donc pas de bloom ; de `0.2` à `0.4`, c'est discret, `1` fait une brume. Le halo est flouté sur une chaîne de passes à demi-taille, si bien qu'il coûte peu, et seule la caméra de la fenêtre en bénéficie : les vues rendues s'en passent. Les valeurs négatives sont ramenées à `0`.
*Paramètres :*
* `threshold` -- la luminosité linéaire au-dessus de laquelle un pixel brille ; `1` est le blanc avant la courbe de tons.
* `strength` -- la part du halo qui est rajoutée ; `0` pour aucune.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetexposure,sceneSetExposure>>, <<materialsetemissive,materialSetEmissive>>, <<scenesettonemap,sceneSetTonemap>>
.Exemple
[source,lua]
----
-- Les néons et les phares brillent ; la chaussée, non.
sceneSetBloom(1.0, 0.35)
neonLook = materialNew()
materialSetColor(neonLook, 255, 40, 120)
materialSetEmissive(neonLook, 255, 40, 120)
nodeSetMesh(sign, meshBox(2, 0.3, 0.05), neonLook)
----
[#scenesetenvironment]
==== sceneSetEnvironment
[source,text]
----
sceneSetEnvironment(lit)
----
Indique si le ciel réglé par `sceneSetSky` éclaire la scène : sa lumière diffuse venue de toutes les directions à la place de `sceneSetAmbient`, et ses reflets sur les métaux et les surfaces brillantes, nets sur un miroir et de plus en plus flous à mesure que la rugosité augmente. Actif par défaut, si bien qu'un ciel éclaire la scène dès qu'il est réglé ; `false` ne garde le ciel que comme toile de fond, et la lumière ambiante uniforme revient. Sans ciel, cela n'a aucun effet.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetsky,sceneSetSky>>, <<scenesetambient,sceneSetAmbient>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
.Exemple
[source,lua]
----
-- Une toile de fond peinte qui ne doit pas teinter le chrome.
sceneSetSky(DIR .. "Skies/matteBackdrop.png")
sceneSetEnvironment(false)
sceneSetAmbient(40, 40, 45)
----
[#scenesetexposure]
==== sceneSetExposure
[source,text]
----
sceneSetExposure(stops)
----
Éclaircit ou assombrit toute la scène en diaphragmes photographiques, de `-10` à `10` (les valeurs hors plage sont ramenées dans la plage) : `+1` double la lumière, `-1` la divise par deux. La valeur par défaut est `0`. Elle est appliquée après le bloom et avant la courbe de tons : une scène lumineuse à `+2` se tasse donc vers le blanc au lieu d'être écrêtée, et une grotte et une plage peuvent partager les mêmes lumières. Animez-la pour une adaptation lente quand on passe de l'intérieur à la lumière du jour.
*Depuis :* 3.00.
*Voir aussi :* <<scenesettonemap,sceneSetTonemap>>, <<lightsetintensity,lightSetIntensity>>, <<scenesetskyintensity,sceneSetSkyIntensity>>
.Exemple
[source,lua]
----
-- Les yeux s'adaptent : rapprocher en douceur l'exposition de la cible de la zone à chaque image.
targetStops = 0
currentStops = 0
function onEnterArea(area)
targetStops = area.indoors and 1.5 or -0.5
end
function onOverlayUpdate()
currentStops = currentStops + (targetStops - currentStops) * 0.05
sceneSetExposure(currentStops)
return OVERLAY_UPDATED
end
----
[#scenesetfog]
==== sceneSetFog
[source,text]
----
sceneSetFog(r, g, b, near, far)
sceneSetFog()
----
Un brouillard de distance : dégagé jusqu'à `near` unités de la caméra, entièrement de la couleur du brouillard à `far` et au-delà, sur les maillages (y compris ceux qui ne sont pas éclairés) comme sur les particules ; les particules additives s'estompent au lieu de virer au gris. Sans argument, ou avec un `far` qui n'est pas plus grand que `near`, il n'y a pas de brouillard. Le ciel n'est pas embrumé : choisissez donc une couleur proche de son horizon, ou du fond quand il n'y a pas de ciel. Tout autre nombre d'arguments met fin au script.
*Paramètres :*
* `r, g, b` -- la couleur du brouillard, de `0` à `255`.
* `near` -- la distance où commence le brouillard, en unités du monde depuis la caméra.
* `far` -- la distance où il ne reste plus que le brouillard.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetsky,sceneSetSky>>, <<scenesetbackground,sceneSetBackground>>, <<camerasetperspective,cameraSetPerspective>>
.Exemple
[source,lua]
----
-- Une brume matinale sur les collines, qui se dissipe à mesure que le niveau avance.
sceneSetSky(DIR .. "Skies/dawn.hdr")
sceneSetFog(190, 200, 215, 40, 160)
function onOverlayUpdate()
if levelTime > 60 then
sceneSetFog()
end
return OVERLAY_UPDATED
end
----
[#scenesetshadowcascades]
==== sceneSetShadowCascades
[source,text]
----
sceneSetShadowCascades(count)
----
En combien de cartes l'ombre d'une lumière directionnelle est découpée le long de la vue, de `1` à `4` (les valeurs hors plage sont ramenées dans la plage ; la valeur par défaut est `3`). Chaque cascade couvre une tranche de la vue de la caméra jusqu'à `sceneSetShadowDistance`, la tranche la plus proche étant la plus petite, si bien que les ombres proches de la caméra sont nettes quelle que soit la taille du niveau. `1` ajuste une seule carte à toute la scène, ce qui convient aux petites scènes, et c'est toujours ce qui sert avec une caméra orthographique. Chaque cascade ajoute une passe d'ombre pour chaque lumière directionnelle qui projette des ombres ; `2` est un choix raisonnable sur un Raspberry Pi.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetshadowdistance,sceneSetShadowDistance>>, <<scenesetshadowsize,sceneSetShadowSize>>, <<lightsetshadow,lightSetShadow>>
.Exemple
[source,lua]
----
-- Un grand niveau en extérieur : des ombres nettes près du joueur, qui portent loin.
sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 30, 40, 20)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
sceneSetShadowCascades(4)
sceneSetShadowDistance(120)
sceneSetShadowSize(2048)
----
[#scenesetshadowdistance]
==== sceneSetShadowDistance
[source,text]
----
sceneSetShadowDistance(distance)
----
Jusqu'où les ombres en cascades portent depuis la caméra, en unités du monde ; la valeur par défaut est `60` et les valeurs inférieures à `1` sont ramenées à `1`. Au-delà, une lumière directionnelle ne projette aucune ombre. Des distances plus courtes rendent chaque cascade plus nette, puisque les mêmes cartes couvrent moins de terrain : réglez-la donc à peu près à la distance où le joueur peut encore distinguer une ombre. Elle ne compte qu'avec plus d'une cascade sous une caméra en perspective.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetshadowsize,sceneSetShadowSize>>
.Exemple
[source,lua]
----
-- Les ombres n'ont besoin d'atteindre que le mur du fond de l'arène.
sceneSetShadowCascades(2)
sceneSetShadowDistance(35)
sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 10, 20, 8)
nodeLookAt(sun, 0, 0, 0)
lightSetShadow(sun, true)
----
[#scenesetshadowsize]
==== sceneSetShadowSize
[source,text]
----
sceneSetShadowSize(size)
----
Le nombre de texels par côté de chaque carte d'ombres, cascade et face de cube, de `256` à `4096` (les valeurs hors plage sont ramenées dans la plage ; la valeur par défaut est `1024`). Plus c'est grand, plus c'est net et lent, en mémoire comme en temps, puisque chaque lumière qui projette des ombres a ses propres cartes à cette taille. `2048` convient à un ordinateur de bureau, `512` à un Raspberry Pi. Les cartes sont reconstruites à l'image suivante.
*Depuis :* 3.00.
*Voir aussi :* <<lightsetshadow,lightSetShadow>>, <<scenesetshadowcascades,sceneSetShadowCascades>>, <<scenesetantialias,sceneSetAntialias>>
.Exemple
[source,lua]
----
-- Dimensionner les cartes d'ombres selon la machine.
if singeGetPlatform() == "raspberrypi" then
sceneSetShadowSize(512)
else
sceneSetShadowSize(2048)
end
----
[#scenesetsky]
==== sceneSetSky
[source,text]
----
sceneSetSky(file)
sceneSetSky(nil)
sceneSetSky()
----
Enveloppe la scène dans un panorama équirectangulaire : un `.hdr` Radiance (tel que le fournissent les sites HDRI, en gardant sa plage), ou un PNG, un JPEG ou toute autre image que lit SDL_image, décodée depuis le sRGB. Le nom est relatif au jeu, comme pour `spriteLoad`. Le ciel est dessiné derrière tout le reste à la place de la couleur de fond (la vidéo ne transparaît donc plus) et éclaire la scène sauf avec `sceneSetEnvironment(false)`. Son centre fait face à -Z et son haut est en haut. Il est rééchantillonné dans un cube d'au plus 1024 texels par face : une image de 2048 sur 1024 suffit donc largement, et les plus grandes prennent un moment à préparer sur un Raspberry Pi. Sans argument, ou avec `nil`, le ciel est retiré. Met fin au script quand le fichier ne peut pas être lu ou que la 3D n'est pas disponible.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetskyintensity,sceneSetSkyIntensity>>, <<scenesetenvironment,sceneSetEnvironment>>, <<scenesetfog,sceneSetFog>>, <<scenesetbackground,sceneSetBackground>>
.Exemple
[source,lua]
----
-- Un ciel couvert pour le niveau des docks, avec un brouillard assorti à son horizon.
sceneEnable(true)
sceneSetSky(DIR .. "Skies/overcastHarbor.hdr")
sceneSetSkyIntensity(0.8)
sceneSetFog(190, 200, 215, 25, 90)
function onLevelEnd()
sceneSetSky()
sceneSetBackground(0, 0, 0, 0)
end
----
[#scenesetskyintensity]
==== sceneSetSkyIntensity
[source,text]
----
sceneSetSkyIntensity(scale)
----
La luminosité du ciel et de la lumière qu'il répand, `1` tel qu'il a été chargé ; les valeurs négatives sont ramenées à `0`. Utile pour atténuer un ciel PNG trop lumineux, pour faire d'un ciel `.hdr` la lumière principale de la scène, ou pour faire apparaître et disparaître le ciel en fondu. Elle multiplie le ciel lui-même, contrairement à `sceneSetExposure`, qui met tout à l'échelle.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetsky,sceneSetSky>>, <<scenesetexposure,sceneSetExposure>>, <<scenesetenvironment,sceneSetEnvironment>>
.Exemple
[source,lua]
----
-- Le crépuscule : le ciel et sa lumière baissent en dix secondes.
sceneSetSky(DIR .. "Skies/sunset.hdr")
duskStart = singeGetTicks()
function onOverlayUpdate()
local t = math.min((singeGetTicks() - duskStart) / 10000, 1)
sceneSetSkyIntensity(1 - 0.8 * t)
return OVERLAY_UPDATED
end
----
[#scenesettonemap]
==== sceneSetTonemap
[source,text]
----
sceneSetTonemap(curve)
----
La courbe qui fait passer la lumière linéaire de la scène à l'écran, appliquée après l'exposition. `TONEMAP_NEUTRAL` (la valeur par défaut) garde des couleurs fidèles et ne tasse que la lumière la plus vive ; `TONEMAP_ACES` est la courbe cinématographique, avec plus de contraste et une transition plus chaude vers le blanc ; `TONEMAP_NONE` écrête au blanc. Les matériaux vidéo, les surfaces non éclairées et le ciel passent par la même courbe : un jeu dont le disque doit apparaître exactement tel qu'il a été filmé veut donc `TONEMAP_NONE` avec une exposition de `0`. Toute autre valeur met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<scenesetexposure,sceneSetExposure>>, <<scenesetbloom,sceneSetBloom>>
.Exemple
[source,lua]
----
-- Un rendu cinéma pour la cinématique, des couleurs fidèles pour le jeu.
function onCutsceneStart()
sceneSetTonemap(TONEMAP_ACES)
sceneSetExposure(0.5)
end
function onCutsceneEnd()
sceneSetTonemap(TONEMAP_NEUTRAL)
sceneSetExposure(0)
end
----
[#script]
=== Script
WARNING: Ces fonctions permettent à un script Singe de passer la main à un autre script Singe : c'est le mécanisme derrière le lanceur intégré `Menu.singe`. Les développeurs de jeux ne devraient pas s'en servir ; construisez votre jeu comme un seul script et laissez le système de menus gérer l'enchaînement. Elles sont documentées ici par souci d'exhaustivité et pour quiconque maintient le menu lui-même.
Les deux appels prennent une table de la forme d'une entrée `GAMES[]` de `games.dat` et la mettent dans la file de l'exécuteur de scripts du moteur. La nouvelle configuration part de celle en cours : les options de ligne de commande comme le dossier de données et la trace sont donc reprises, mais jamais le disque, la vidéo ni le conteneur, car chaque entrée apporte les siens. Le script en cours se termine à la fin de l'image, `onShutdown` est appelé, et le script mis en file démarre avec un état Lua neuf (voir <<enginecallbacks,Callbacks du moteur>>). Le dossier de données du nouveau script suit la même règle qu'un lancement en ligne de commande (voir `singeGetDataPath`) ; quand il ne peut pas être créé, l'appel met fin au script appelant.
[#scriptexecute]
==== scriptExecute
[source,text]
----
scriptExecute(config)
----
Remplace le script en cours par celui que décrit la table. Le script en cours s'arrête à la fin de cette image et `onShutdown` s'exécute ; la main ne lui revient jamais, et quand le nouveau script quitte, le moteur passe à ce qui reste dans la file ou se ferme. Les clés chaînes que connaît le moteur sont `SCRIPT`, `CONTAINER`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `AUDIO_DELAY`, `LEGACY_SPRITE_ARGS`, `CANVAS_X` et `CANVAS_Y` ; les autres clés sont ignorées et les clés qui ne sont pas des chaînes sont sautées. Une valeur `SCRIPT`, `CONTAINER` ou `VIDEO` qui n'est pas une chaîne met fin au script, de même qu'un argument qui n'est pas une table.
*Paramètres :*
* `config` -- une table de champs de `games.dat`, avec au minimum `SCRIPT` ou `CONTAINER`.
*Depuis :* 2.00
*Voir aussi :* <<scriptpush,scriptPush>>, <<singequit,singeQuit>>
.Exemple
[source,lua]
----
-- Un lanceur qui ne revient jamais à lui-même.
function onInputPressed(what)
if what == SWITCH_START1 then
scriptExecute({
SCRIPT = DIR .. "../MyGame/MyGame.singe",
VIDEO = DIR .. "../MyGame/MyGame.mp4",
})
end
end
----
[#scriptpush]
==== scriptPush
[source,text]
----
scriptPush(config)
----
Démarre le script que décrit la table et met la configuration en cours dans la file derrière lui : quand le nouveau script quitte, le script en cours repart donc du début. Rien n'est conservé en place : le `onShutdown` du script en cours s'exécute maintenant, son état Lua est jeté, et l'exécution suivante repart de zéro, ce qui explique pourquoi `Menu.singe` sauvegarde sa sélection dans un fichier avant d'empiler. La table prend les mêmes clés que `scriptExecute`.
*Paramètres :*
* `config` -- une table de champs de `games.dat`, comme pour `scriptExecute`.
*Depuis :* 2.00
*Voir aussi :* <<scriptexecute,scriptExecute>>, <<singequit,singeQuit>>, <<onshutdown,onShutdown>>
.Exemple
[source,lua]
----
-- Menu.singe : lancer le jeu sélectionné et revenir ensuite.
function onInputPressed(what)
if what == SWITCH_START1 or what == SWITCH_BUTTON1 then
SHUTDOWN_FROM_PUSH = true
scriptPush(GAME_LIST[GAME_SELECTED])
end
end
----
[#singe]
=== Singe
Les fonctions de l'espace de noms `singe*` pilotent le moteur lui-même : la fenêtre, les captures d'écran, le système de pause, la sortie et le rechargement, le retard audio qui sert à la synchronisation labiale, et quelques chemins et indicateurs qui viennent de la ligne de commande et de `games.dat`. Rien ici ne prend de handle. Le comportement de la pause est décrit dans <<pausing,La pause>> et les valeurs de retard audio dans <<audiosync,Synchronisation audio>>.
[#allowsocketcall]
==== allowSocketCall
[source,text]
----
allowed = allowSocketCall()
----
Extension Hypseus, toujours `false` dans Singe. Hypseus répond si l'utilisateur l'a autorisé à envoyer des données à un serveur de statistiques, ce que son `-noserversend` désactive ; l'envoi lui-même y est mis en commentaire. Singe n'envoie rien nulle part : rien n'est donc permis et la réponse ne change jamais. Elle existe pour qu'un jeu qui pose la question ne s'arrête pas sur une fonction manquante.
*Renvoie :* un booléen, toujours `false`.
*Depuis :* 3.00
[#getuserstring]
==== getUserString
[source,text]
----
id = getUserString()
----
Extension Hypseus, toujours une chaîne vide. Malgré son nom, ce n'est pas une chaîne que l'utilisateur a réglée : Hypseus répond l'identifiant d'installation de la machine qu'il envoie avec les statistiques réseau, tiré du `MachineGuid` de Windows ou de l'identifiant de machine de Linux. Singe n'envoie rien nulle part et ne donnera pas à un jeu une empreinte de la machine sur laquelle il tourne.
*Renvoie :* une chaîne, toujours vide.
*Depuis :* 3.00
*Voir aussi :* <<allowsocketcall,allowSocketCall>>
[#singedisablepausekey]
==== singeDisablePauseKey
[source,text]
----
singeDisablePauseKey()
----
Donne la touche pause au script, exactement comme le fait `singeSetPauseKeyEnabled(false)`. C'est une fonction définie dans `Framework.singe`, conservée pour que les anciens jeux tournent sans modification.
*Remarques :* Alias hérité.
WARNING: Le nouveau code doit appeler <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> avec `false`.
*Depuis :* 1.18 (RDG)
*Voir aussi :* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singeenablepausekey,singeEnablePauseKey>>
.Exemple
[source,lua]
----
-- Code d'un ancien jeu ; l'écriture moderne est singeSetPauseKeyEnabled(false).
singeDisablePauseKey()
function onInputPressed(what)
if what == SWITCH_PAUSE then
togglePauseMenu()
end
end
----
[#singeenablepausekey]
==== singeEnablePauseKey
[source,text]
----
singeEnablePauseKey()
----
Rend la touche pause au moteur, exactement comme le fait `singeSetPauseKeyEnabled(true)`. C'est une fonction définie dans `Framework.singe`, conservée pour que les anciens jeux tournent sans modification.
*Remarques :* Alias hérité.
WARNING: Le nouveau code doit appeler <<singesetpausekeyenabled,singeSetPauseKeyEnabled>> avec `true`.
*Depuis :* 1.18 (RDG)
*Voir aussi :* <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singedisablepausekey,singeDisablePauseKey>>
.Exemple
[source,lua]
----
-- Code d'un ancien jeu ; l'écriture moderne est singeSetPauseKeyEnabled(true).
function pauseMenuClose()
singeSetPauseFlag(false)
singeEnablePauseKey()
end
----
[#utilsha256]
==== utilSha256
[source,text]
----
digest = utilSha256(data)
----
Le SHA-256 d'une chaîne, sous forme de 64 caractères hexadécimaux en minuscules. Sûr pour le
binaire : il fonctionne donc aussi bien sur les octets d'un fichier que sur du texte.
Pour décider si les octets que vous avez sont ceux qu'on vous a promis : un jeu téléchargé comparé
à l'empreinte que porte son entrée du catalogue, ou la clé publique d'un serveur comparée à une
valeur épinglée. Le module `md5` inclus est toujours là pour une somme de contrôle, mais une
empreinte qui sert à décider si l'on peut *faire confiance* à quelque chose doit être une empreinte
qui mérite encore la confiance.
*Paramètres :*
* `data` -- une chaîne d'octets. Vide, elle met fin au script.
*Renvoie :* chaîne, 64 caractères hexadécimaux en minuscules.
*Depuis :* 3.00.
.Exemple
[source,lua]
----
-- Refuser un téléchargement qui n'est pas arrivé intact.
local file = io.open("game.part", "rb")
local bytes = file:read("*a")
file:close()
if utilSha256(bytes) ~= expected then
os.remove("game.part")
end
----
[#singecapabilities]
==== singeCapabilities
[source,text]
----
capabilities = singeCapabilities()
----
Ce que cette machine peut faire tourner. La table a une entrée par nom de
capacité que peut utiliser une liste `REQUIRES` de `games.dat` (`SCENE`, `GUI`,
`SHADOWS`, `POST`, `SKIN`, `MORPH`), chacune `true` ou `false`, plus `tier` : la
classe de performances mesurée de la machine, de 0 (pas de 3D) à 3 (un GPU de
bureau), ou `nil` avant que le menu l'ait mesurée. Un jeu pose la question pour
pouvoir se passer de quelque chose plutôt que d'être refusé faute de l'avoir.
*Renvoie :* une table.
*Depuis :* 3.00.
*Voir aussi :* <<singehasgpu,singeHasGpu>>, <<singesavetier,singeSaveTier>>, <<scenemeasure,sceneMeasure>>
.Exemple
[source,lua]
----
-- Des ombres là où la machine les a, un simple soleil là où elle ne les a pas.
local machine = singeCapabilities()
lightSetShadow(sun, machine.SHADOWS)
if (machine.tier or 0) < 2 then
sceneSetBloom(1.0, 0.0)
end
----
[#singegetaudiocalibration]
==== singeGetAudioCalibration
[source,text]
----
milliseconds = singeGetAudioCalibration()
----
Renvoie le retard audio propre à la machine en vigueur, en millisecondes : la valeur que l'écran de calibrage du menu a sauvegardée dans `machine.cfg` à la racine des données, ou celle réglée depuis par `singeSetAudioCalibration`. Zéro quand la machine n'a jamais été calibrée. Il s'applique à chaque jeu de la machine, en plus du retard propre au jeu que donne `singeGetAudioDelay`.
*Renvoie :* un entier, en millisecondes.
*Depuis :* 3.00
*Voir aussi :* <<singesetaudiocalibration,singeSetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>, <<singegetaudiolatency,singeGetAudioLatency>>
.Exemple
[source,lua]
----
-- Menu.singe : démarrer l'écran de calibrage à partir de la valeur sauvegardée.
function calibrationBegin()
CALIBRATING = true
CAL_ORIGINAL = singeGetAudioCalibration()
CAL_VALUE = CAL_ORIGINAL
CAL_LATENCY = singeGetAudioLatency()
CAL_BEAT = singeGetTicks() - CAL_PERIOD
end
----
[#singegetaudiodelay]
==== singeGetAudioDelay
[source,text]
----
milliseconds = singeGetAudioDelay()
----
Renvoie le retard audio propre au jeu en vigueur, en millisecondes, qu'il vienne de `--audiodelay`, de la clé `AUDIO_DELAY` de l'entrée du jeu dans `games.dat` ou de `singeSetAudioDelay`. Zéro quand aucun n'a été réglé. Il s'ajoute au calibrage de la machine et à la latence mesurée du périphérique quand le moteur décide quand montrer chaque image de la vidéo.
*Renvoie :* un entier, en millisecondes.
*Depuis :* 3.00
*Voir aussi :* <<singesetaudiodelay,singeSetAudioDelay>>, <<singegetaudiocalibration,singeGetAudioCalibration>>
.Exemple
[source,lua]
----
-- Menu de service : afficher le décalage de synchronisation actuel à côté de son curseur.
function drawSyncSetting(y)
local delay = singeGetAudioDelay()
fontPrint(40, y, string.format("A/V offset: %+d ms", delay))
end
----
[#singegetaudiolatency]
==== singeGetAudioLatency
[source,text]
----
milliseconds = singeGetAudioLatency()
----
Renvoie la file du périphérique audio que le moteur a mesurée à l'ouverture du mélangeur, en millisecondes : le temps entre la remise des échantillons au mélangeur et leur arrivée au périphérique, avant tout calibrage ou retard propre au jeu. Le moteur la compense déjà ; un écran de calibrage en a besoin pour programmer son clic et son flash, et elle mérite d'être affichée dans une vue de diagnostic.
*Renvoie :* un entier, en millisecondes.
*Depuis :* 3.00
*Voir aussi :* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singegetaudiodelay,singeGetAudioDelay>>
.Exemple
[source,lua]
----
-- Menu.singe : le flash suit le clic de la file du périphérique plus la valeur candidate.
function calibrationUpdate()
local now = singeGetTicks()
local gap = CAL_LATENCY + CAL_VALUE
if now - CAL_BEAT >= CAL_PERIOD then
CAL_BEAT = now
CAL_CLICK_AT = now + math.max(0, -gap)
CAL_FLASH_AT = now + math.max(0, gap)
end
end
----
[#singegetdatapath]
==== singeGetDataPath
[source,text]
----
path = singeGetDataPath()
----
Renvoie le dossier dans lequel Singe écrit pour ce jeu, avec un séparateur de chemin final pour qu'un nom de fichier puisse y être ajouté directement. C'est un sous-dossier de la base `--datadir` qui porte le nom du jeu (pour un jeu empaqueté, le nom de la base de données puis le dossier du script à l'intérieur) ; sans `--datadir`, la base est `data` dans le dossier du jeu. Les captures d'écran, les index vidéo et `trace.txt` y vont, et les sauvegardes, les meilleurs scores et les réglages devraient y aller aussi ; Singe 2.x les écrivait à côté du jeu, qui peut être en lecture seule.
*Renvoie :* chemin (chaîne) terminé par un séparateur.
*Depuis :* 2.00
*Voir aussi :* <<singegetscriptpath,singeGetScriptPath>>, <<singescreenshot,singeScreenshot>>
.Exemple
[source,lua]
----
-- Menu.singe garde sa dernière sélection d'un lancement à l'autre.
CONFIG_FILE = singeGetDataPath() .. "menu.dat"
function saveConfig()
local cfg = io.open(CONFIG_FILE, "w")
if cfg then
cfg:write("GAME_SELECTED = " .. GAME_SELECTED .. "\n")
cfg:close()
else
debugPrint("Unable to write " .. CONFIG_FILE)
end
end
----
[#singegetgameid]
==== singeGetGameId
[source,text]
----
id = singeGetGameId()
----
Le `GAME_ID` de l'entrée de ce jeu dans `games.dat`, ou `nil` quand elle n'en a pas. C'est
l'identifiant sous lequel le service en ligne connaît le jeu, et la clé sous laquelle est tenu un
tableau des scores.
**Le format est un UUID**, sous la forme canonique de 36 caractères -- huit chiffres
hexadécimaux, trois groupes de quatre, puis douze, séparés par des traits d'union :
----
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
----
Rien d'autre n'est accepté. L'identifiant est un segment de chemin dans les URL du service : un
identifiant contenant une barre oblique ou un point d'interrogation serait publié puis ne serait
jamais joignable, et deux identifiants qui ne diffèrent que par la casse feraient deux jeux pour
la base de données et un seul pour quiconque les lit. La comparaison ignore la casse et le
stockage se fait en minuscules : un identifiant tapé en majuscules désigne donc le même jeu.
N'importe quel UUID fera l'affaire -- générez-en un comme bon vous semble, ou laissez
`publish.lua` en afficher un. Un jeu qui n'en a pas peut être joué mais pas classé : il n'y
aurait rien de stable sur quoi indexer un tableau des scores, et un identifiant inventé à
l'exécution créerait un tableau différent chaque fois que les fichiers seraient déplacés.
**Un `.game`, un identifiant.** Un `.game` dont le `games.dat` a plusieurs entrées -- une
compilation, ou un même jeu en deux langues -- est un seul téléchargement, et chacune de ses
entrées porte le même `GAME_ID`. Le service en ligne présente chaque entrée comme un jeu à part
entière, et n'accepte pas un `.game` dont les entrées ne s'accordent pas sur leur identifiant ou
dont l'une ne l'a pas. Leurs scores restent tout de même séparés : voir
<<onlinescores,Meilleurs scores en ligne>>.
*Renvoie :* chaîne, ou `nil`.
*Depuis :* 3.00.
*Voir aussi :* <<onlinescores,Meilleurs scores en ligne>>, <<gamesdat,games.dat>>
.Exemple
[source,lua]
----
-- Ne proposer le tableau des scores que si ce jeu a une identité sous laquelle être classé.
if singeGetGameId() then
showLeaderboardButton()
end
----
[#singegetheight]
==== singeGetHeight
[source,text]
----
height = singeGetHeight()
----
Renvoie la hauteur de la fenêtre de Singe en pixels d'écran, et non en unités de l'overlay, telle qu'est la fenêtre au moment de l'appel. Utilisez `overlayGetHeight` pour dessiner ; celle-ci sert à un écran de réglages qui décrit l'affichage, ou à décider combien de détails une scène peut se permettre.
*Renvoie :* un entier, en pixels.
*Depuis :* 1.x
*Voir aussi :* <<singegetwidth,singeGetWidth>>, <<overlaygetheight,overlayGetHeight>>
.Exemple
[source,lua]
----
-- Décrire l'affichage dans le menu de service.
function drawDisplayInfo(y)
fontPrint(40, y, string.format("Window %dx%d, overlay %dx%d", singeGetWidth(), singeGetHeight(), overlayGetWidth(), overlayGetHeight()))
end
----
[#singegetpauseflag]
==== singeGetPauseFlag
[source,text]
----
paused = singeGetPauseFlag()
----
Renvoie l'indicateur de pause du moteur : `true` tant que le jeu est mis en pause par la touche pause ou par `singeSetPauseFlag`. C'est indépendant du fait que le disque soit arrêté sur une image en pause (`discGetState`) ; lisez-le pour savoir si une pause a été demandée délibérément. Tant que la touche pause tient le jeu figé, aucun callback ne s'exécute : depuis un callback, l'indicateur ne peut donc être vu à `true` qu'après un `singeSetPauseFlag(true)` du script lui-même.
*Renvoie :* booléen.
*Depuis :* 1.x (RDG)
*Voir aussi :* <<singesetpauseflag,singeSetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
.Exemple
[source,lua]
----
-- Les minuteries restent immobiles tant que la pause propre au jeu est affichée.
function onOverlayUpdate()
if not singeGetPauseFlag() then
elapsed = elapsed + 1
updateEnemies()
end
drawFrame()
return OVERLAY_UPDATED
end
----
[#singegetscriptpath]
==== singeGetScriptPath
[source,text]
----
path = singeGetScriptPath()
----
Renvoie le chemin du fichier de script en cours tel que le moteur l'a résolu. `Framework.singe` tire de cette valeur la globale `DIR`, le dossier du script avec son séparateur final, et c'est de `DIR` que se servent la plupart des jeux pour construire les chemins de leurs ressources.
*Renvoie :* chemin (chaîne).
*Depuis :* 1.15 (RDG)
*Voir aussi :* <<singegetdatapath,singeGetDataPath>>
.Exemple
[source,lua]
----
-- Framework.singe tire DIR ainsi ; les jeux chargent ensuite relativement à lui.
DIR = singeGetScriptPath():match("(.*[/\\])") or "./"
titleSprite = spriteLoad(DIR .. "images/title.png")
----
[#singegetsysteminfo]
==== singeGetSystemInfo
[source,text]
----
info = singeGetSystemInfo()
----
Tout ce que l'en-tête de la trace sait de cette machine et de cette exécution,
dans une seule table. Elle est destinée à un écran de service et aux rapports de
bogue : la réponse à « sur quoi est-ce que ça tournait » sans demander au joueur
de trouver un journal.
*Renvoie :* une table avec ces champs, tous des chaînes sauf les quatre tailles :
* `version` -- la version du moteur, comme `SINGE_VERSION_STRING`.
* `os` -- le nom et la version du système d'exploitation, et l'architecture.
* `cpu` -- le processeur, son nombre de cœurs et la mémoire en mégaoctets.
* `renderer` -- le nom du moteur de rendu 2D de SDL utilisé.
* `gpu` -- le pilote SDL_GPU derrière la scène 3D, ou une mention indiquant que la
3D n'est pas disponible.
* `decoder` -- les décodeurs vidéo matériels que cette compilation essaiera, en toutes lettres.
* `audio` -- les formats audio intégrés à la compilation, séparés par des virgules.
* `soundFont` -- la SoundFont qu'utilise la lecture MIDI, ou la raison pour laquelle il n'y en a pas.
* `midi` -- les ports MIDI ouverts, ou une mention indiquant qu'aucun n'a été demandé.
* `dataPath` -- le dossier de données de ce jeu.
* `dataRoot` -- le dossier de données donné au moteur, un niveau au-dessus de
`dataPath`, sous lequel écrit chaque jeu de la machine.
* `platform` -- la partie plateforme du nom de publication, comme `Linux-x86_64` :
de quelle publication cet exécutable fait partie, et celle à demander au service.
* `executable` -- le chemin absolu de l'exécutable en cours.
* `windowWidth`, `windowHeight` -- des entiers, la fenêtre en pixels.
* `canvasWidth`, `canvasHeight` -- des entiers, le canevas en pixels.
*Depuis :* 3.00.
*Voir aussi :* <<singegetdatapath,singeGetDataPath>>, xref:Manual.adoc#servicetools[Les outils de service]
.Exemple
[source,lua]
----
-- Un écran de diagnostic à une touche.
local info = singeGetSystemInfo()
fontPrint(10, 10, "Singe " .. info.version .. " on " .. info.os)
fontPrint(10, 30, info.cpu)
fontPrint(10, 50, info.renderer .. " / " .. info.gpu)
----
[#singegetticks]
==== singeGetTicks
[source,text]
----
milliseconds = singeGetTicks()
----
Renvoie l'horloge, en millisecondes depuis le démarrage du moteur. C'est l'horloge des minuteries, des anti-rebonds et de l'animation, et elle continue de tourner tant que le jeu est figé par la touche pause : une minuterie qui ne doit pas compter le temps de pause doit donc lire `singeGetPauseFlag` ou mesurer à partir du moment du dégel. Le `os.clock()` de Lua mesure normalement le temps processeur, qui s'arrête pendant que le moteur est inactif ; Singe le remplace par les secondes d'horloge écoulées depuis le démarrage du moteur, si bien que les jeux existants qui s'en servaient comme d'une horloge continuent de fonctionner.
*Renvoie :* un entier, en millisecondes.
*Depuis :* 3.00
*Voir aussi :* <<singegetpauseflag,singeGetPauseFlag>>, <<debugprint,debugPrint>>
.Exemple
[source,lua]
----
-- Faire clignoter l'invite « insert coin » deux fois par seconde.
function drawAttractPrompt()
local phase = (singeGetTicks() // 250) % 2
if phase == 0 then
fontPrint(120, 200, "INSERT COIN")
end
end
----
[#singegetwidth]
==== singeGetWidth
[source,text]
----
width = singeGetWidth()
----
Renvoie la largeur de la fenêtre de Singe en pixels d'écran, le pendant de `singeGetHeight`. Le dessin sur l'overlay utilise plutôt `overlayGetWidth`.
*Renvoie :* un entier, en pixels.
*Depuis :* 1.x
*Voir aussi :* <<singegetheight,singeGetHeight>>, <<overlaygetwidth,overlayGetWidth>>
.Exemple
[source,lua]
----
-- Choisir un budget de textures d'après la taille de la fenêtre.
local pixels = singeGetWidth() * singeGetHeight()
if pixels > 1920 * 1080 then
textureSet = "high"
else
textureSet = "low"
end
----
[#singehasgpu]
==== singeHasGpu
[source,text]
----
present = singeHasGpu()
----
Indique si le périphérique GPU a démarré. Tout ce qui est dessiné par lui est
indisponible sans lui : la scène 3D, les particules 3D et chaque document de
GUI. Les particules 2D sont dessinées avec l'overlay et n'ont besoin d'aucun
périphérique. Posez cette question avant de demander l'un d'eux, car
<<guinew,guiNew>> et les appels de scène mettent fin au script au lieu de
répondre `false` -- il n'y a aucun moyen d'essayer puis de se rattraper.
Une machine peut en être dépourvue parce qu'elle n'a aucun pilote Vulkan,
Direct3D 12 ou Metal, ou parce que `--nogpu` a été donné. Le menu inclus
l'appelle au démarrage et choisit un moteur de rendu d'après la réponse (voir xref:Manual.adoc#menurenderers[Les deux moteurs de rendu]).
*Renvoie :* booléen, vrai quand un périphérique GPU est présent.
*Depuis :* 3.00.
*Voir aussi :* <<guinew,guiNew>>, <<singegetsysteminfo,singeGetSystemInfo>>, <<sceneperformance,Performances et configuration requise>>
.Exemple
[source,lua]
----
-- Un jeu avec un écran titre en 3D et un écran plat sur lequel se rabattre.
if singeHasGpu() then
buildSceneTitle()
else
buildSpriteTitle()
end
----
[#singeisrecording]
==== singeIsRecording
[source,text]
----
recording = singeIsRecording()
----
Indique si un enregistrement est en cours, ou s'il a été demandé et commence avec la prochaine image dessinée. Voir <<singerecordstart,singeRecordStart>>.
*Renvoie :* booléen.
*Depuis :* 3.00
*Voir aussi :* <<singerecordstart,singeRecordStart>>, <<singerecordstop,singeRecordStop>>
.Exemple
[source,lua]
----
-- Un point rouge dans le coin de l'écran de pause propre au jeu pendant qu'il est enregistré.
if singeIsRecording() then
overlayCircle(overlayGetWidth() - 12, 12, 5)
end
----
[#singepack]
==== singePack
[source,text]
----
result = singePack(directory, database)
----
Empaquette le jeu de `directory` dans la base de données en un seul fichier `database`, exactement comme le fait `singe --pack` en ligne de commande, et renvoie `true` quand cela a réussi. Le dossier doit être le dossier propre d'un jeu : un dossier qui ne contient ni dossier de support `Singe/`, ni lanceur, ni rien d'autre que l'empaqueteur refuse, et les raisons d'un refus sont affichées dans la console comme pour la ligne de commande. C'est ce qui permet à un outil de création de publier un jeu sans quitter le moteur.
*Paramètres :*
* `directory` -- le dossier du jeu, tel que le prend l'empaqueteur.
* `database` -- le fichier `.game` à écrire, remplacé s'il existe.
*Renvoie :* `true` en cas de réussite, `false` sinon.
*Depuis :* 3.00
*Voir aussi :* <<singlefile,Jeux en un seul fichier>>
.Exemple
[source,lua]
----
-- Publier le jeu à côté de son dossier et le signaler.
if singePack(singeGetDataPath() .. "CoinRun", singeGetDataPath() .. "CoinRun.game") then
debugPrint("packed")
end
----
[#singequit]
==== singeQuit
[source,text]
----
singeQuit()
----
Met fin au script en cours à la fin de cette image : la boucle de jeu s'arrête, `onShutdown` s'exécute, et le moteur passe au script suivant dans la file ou se ferme quand il n'y en a pas. Un jeu lancé depuis le menu avec `scriptPush` revient donc au menu, qui redémarre du début. Dans le modèle à fil d'exécution, revenir de `singeMain` l'appelle pour vous. Le contact affecté à `INPUT_QUIT` dans `controls.cfg` et la fermeture de la fenêtre mettent fin au script de la même façon. Ne l'appelez pas depuis `onShutdown`, qui est déjà le chemin de sortie.
*Depuis :* 1.x (RDG)
*Voir aussi :* <<onshutdown,onShutdown>>, <<singereload,singeReload>>, <<scriptpush,scriptPush>>
.Exemple
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_QUIT then
singeQuit()
elseif what == SWITCH_START1 and gameOver then
startGame()
end
end
----
[#singerelaunch]
==== singeRelaunch
[source,text]
----
result = singeRelaunch(path)
----
Arrête le moteur comme le fait `singeQuit` et, une fois que tout a été démonté, démarre l'exécutable situé à `path` à la place de celui-ci avec les mêmes arguments. C'est ainsi que l'outil Engine Update du menu fait passer une borne à un Singe plus récent qu'il a placé à côté de celui qui tourne : le nouveau fichier est rendu exécutable et démarré, et sous Windows, qui ne remplace pas un exécutable en cours d'exécution, il est démarré à côté de celui-ci sous son propre nom. Le fichier doit déjà exister ; quand ce n'est pas le cas, l'appel répond `false` sans rien faire. Rien n'est décompressé ni copié ici : le nouveau Singe décompresse ses propres fichiers de support à son premier lancement.
*Paramètres :*
* `path` -- l'exécutable à démarrer.
*Renvoie :* `true` quand la relance a été préparée, `false` quand le fichier n'est pas là.
*Depuis :* 3.00
*Voir aussi :* <<singequit,singeQuit>>, <<singegetsysteminfo,singeGetSystemInfo>>
.Exemple
[source,lua]
----
-- Un Singe plus récent a été téléchargé et vérifié ; lui passer la main.
local folder = singeGetSystemInfo().executable:match("^(.*[/\\])")
singeRelaunch(folder .. "Singe-v3.01-Linux-x86_64")
----
[#singerecordstart]
==== singeRecordStart
[source,text]
----
singeRecordStart([name])
----
Commence à enregistrer le jeu, image et son ensemble, dans un MP4 placé dans le dossier de données du jeu (voir `singeGetDataPath`), à partir de la prochaine image dessinée. Sans nom, les fichiers sont nommés à partir de `singe000.mp4` en montant, comme les captures d'écran ; avec un nom, l'enregistrement va dans ce fichier, `.mp4` étant ajouté quand le nom n'a pas d'extension, en remplaçant tout fichier de ce nom, et un nom qui contient un dossier met fin au script. Un appel pendant qu'un enregistrement est en cours est ignoré. Le contact affecté à `INPUT_RECORD` dans `controls.cfg`, `F10` par défaut, démarre et arrête les mêmes enregistrements.
L'image est la fenêtre entière telle qu'elle est affichée, bandes noires comprises, à la taille de la fenêtre, exactement ce que prend une capture d'écran ; le son est le mixage final, musique, effets et bandes-son des vidéos ensemble. Les deux sont encodés sur un fil d'exécution à eux, si bien que le jeu paie la recopie de chaque image depuis le GPU et peu de chose d'autre. L'image est en H.264 là où la machine a un encodeur matériel que Singe peut utiliser -- VA-API sous Linux, VideoToolbox sous macOS, Media Foundation sous Windows, V4L2 sur les cartes ARM qui en ont un -- et en MPEG-4 avec l'encodeur propre de FFmpeg partout ailleurs ; le son est en AAC. La trace nomme l'encodeur quand l'enregistrement démarre, et compte les images perdues quand il s'arrête.
En jeu, l'enregistrement tourne à 60 images par seconde sur l'horloge réelle. Une image qui arrive alors que l'encodeur est encore occupé est retirée de l'enregistrement plutôt que de faire attendre le jeu, et le son est décalé plus tard de la durée de la file du périphérique audio, si bien que les deux concordent comme à l'écran. Sous `--deterministic`, l'enregistrement tourne plutôt à une image par pas d'horloge, et le moteur attend l'encodeur au lieu de perdre quoi que ce soit ; lancée par `--record`, une exécution déterministe mixe aussi son son en mémoire un pas à la fois, si bien que l'enregistrement est exact image par image et échantillon par échantillon.
*Paramètres :*
* `name` -- chaîne facultative ; un nom de fichier, pas un chemin.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<singerecordstop,singeRecordStop>>, <<singeisrecording,singeIsRecording>>, <<singescreenshot,singeScreenshot>>, <<deterministic,Mode de test déterministe>>
.Exemple
[source,lua]
----
-- Le mode attraction enregistre sa propre bande-annonce : une boucle, puis arrêt.
function attractBegin()
singeRecordStart("trailer")
end
function attractEnd()
singeRecordStop()
end
----
[#singerecordstop]
==== singeRecordStop
[source,text]
----
singeRecordStop()
----
Termine l'enregistrement à la prochaine image dessinée : ce qui attend encore est encodé et le fichier est fermé, si bien qu'il se lit du début à la fin. Ignoré quand rien n'est en cours d'enregistrement. Un enregistrement encore en cours quand le jeu se termine est terminé de la même façon.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<singerecordstart,singeRecordStart>>, <<singeisrecording,singeIsRecording>>
.Exemple
[source,lua]
----
-- Enregistrer une partie : de la première pièce à la fin de partie.
function onGameOver()
singeRecordStop()
end
----
[#singereload]
==== singeReload
[source,text]
----
singeReload()
----
Relance le jeu depuis son script à la fin de cette image, comme le font `F5` et un fichier modifié sous `--reload` ; cet appel fonctionne sans l'option. Tous les sons s'arrêtent, l'état Lua est jeté sans appeler `onShutdown`, tout ce que le script a chargé est libéré, la scène, la physique (son dessin de débogage compris), les particules et la navigation sont réinitialisées, l'overlay revient à sa résolution par défaut, et le volume des effets sonores, le mode souris, la capture de la souris, le mode clavier, la touche pause, l'auditeur sonore et l'indicateur de pause reviennent à leurs valeurs par défaut. Les touches et boutons maintenus pendant le rechargement sont ignorés jusqu'à ce qu'ils soient relâchés, comme à un démarrage à neuf, et le disque est garé sur sa première image, en pause, avec la piste audio par défaut sélectionnée, comme au démarrage. Le moteur lui-même (la fenêtre, les manettes, les souris) reste tel quel. Il convient à l'entrée « redémarrer » d'un menu de débogage ou à un éditeur de niveaux qui recharge ce qu'il vient de sauvegarder.
*Depuis :* 3.00.
*Voir aussi :* <<singequit,singeQuit>>
.Exemple
[source,lua]
----
-- Éditeur : sauvegarder le niveau, puis relancer le jeu dessus.
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.F9.value then
saveLevel(singeGetDataPath() .. "level.lua")
singeReload()
end
end
----
[#singesavegeometry]
==== singeSaveGeometry
[source,text]
----
singeSaveGeometry()
----
Garde pour cette machine l'échelle, le décalage et la rotation actuels de
l'image. Ils vont dans `machine.cfg` à la racine des données, à côté du retard
audio, et le moteur les applique à chaque jeu qu'il démarre ensuite, depuis le
menu ou depuis la ligne de commande, et à travers `singeReload`. Une option
réellement tapée sur la ligne de commande l'emporte : `--scalefactor` remplace
l'échelle sauvegardée, `--shiftx` ou `--shifty` le décalage sauvegardé,
`--rotate` la rotation sauvegardée. Le fichier est écrit en entier : le retard
audio est donc gardé tel qu'il est.
C'est un réglage de la borne, pas d'un jeu : c'est ainsi qu'on cale un moniteur
une fois pour toutes plutôt qu'une fois par jeu. L'outil Display du menu inclus
n'est rien d'autre que cet appel derrière le bouton 1.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<vldpsetscale,vldpSetScale>>, <<vldpsetshift,vldpSetShift>>, <<vldpsetrotate,vldpSetRotate>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
.Exemple
[source,lua]
----
-- L'exploitant est content de l'image : la garder pour la borne.
function onInputPressed(input)
if input == SWITCH_BUTTON1 then
singeSaveGeometry()
end
end
----
[#singesavetier]
==== singeSaveTier
[source,text]
----
singeSaveTier(tier)
----
Garde la classe de performances de cette machine, de 0 à 3, dans `machine.cfg` à
côté du retard audio et de la géométrie, où <<singecapabilities,singeCapabilities>>
la rapporte et où le menu la compare au `TIER` de chaque jeu. Le menu inclus
mesure lui-même le niveau, d'après le temps que met son décor de fond 3D à se
dessiner (<<scenemeasure,sceneMeasure>>), et ne le sauvegarde que quand il
change ; rien ne le demande au joueur.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<singecapabilities,singeCapabilities>>, <<singesavegeometry,singeSaveGeometry>>
.Exemple
[source,lua]
----
-- Une machine sans aucune 3D est de niveau 0.
if not singeHasGpu() then
singeSaveTier(0)
end
----
[#singescreenshot]
==== singeScreenshot
[source,text]
----
singeScreenshot()
singeScreenshot(name)
----
Demande une capture d'écran de la fenêtre entière, bandes noires comprises, prise après que la prochaine image a été dessinée et sauvegardée en PNG dans le dossier de données du jeu (voir `singeGetDataPath`). Sans nom, les fichiers sont nommés à partir de `singe000.png` en montant ; chaque exécution du script cherche depuis zéro le premier nom libre, et les captures suivantes continuent après la dernière sauvegardée. Avec un nom, la capture va dans ce fichier, `.png` étant ajouté quand le nom n'a pas d'extension, en remplaçant tout fichier de ce nom ; un nom qui contient un dossier met fin au script. Nommer la capture sert aux scripts qui produisent des images exprès, comme ceux qui illustrent les tutoriels d'un livre : une nouvelle exécution remplace alors l'image au lieu d'en ajouter une. L'appel force un redessin pour que la capture soit prise même quand l'affichage est au repos. Le contact affecté à `INPUT_SCREENSHOT` dans `controls.cfg` fait la même chose.
*Depuis :* 1.x
*Voir aussi :* <<singegetdatapath,singeGetDataPath>>
.Exemple
[source,lua]
----
-- Un script de test capture quelques images en survolant la scène.
function onOverlayUpdate()
frames = frames + 1
if frames == 40 or frames == 85 or frames == 130 then
singeScreenshot()
end
if frames == 180 then
singeQuit()
end
end
----
[#singesetaudiocalibration]
==== singeSetAudioCalibration
[source,text]
----
singeSetAudioCalibration(milliseconds)
----
Règle le retard audio propre à la machine, l'applique aussitôt et l'écrit dans `machine.cfg` à la racine des données pour que chaque jeu de la machine le prenne en compte. L'écran de calibrage du menu l'appelle ; un jeu qui a son propre menu de service peut aussi le faire. La valeur est positive quand l'audio est entendu plus tard que ne l'indique le périphérique ; le moteur retarde d'autant la présentation de la vidéo. Les valeurs hors de `-1000` à `1000` mettent fin au script. Si `machine.cfg` ne peut pas être écrit, la valeur s'applique quand même pour cette exécution et un message part sur la console.
*Paramètres :*
* `milliseconds` -- entier, de `-1000` à `1000`, positif quand l'audio est en retard.
*Depuis :* 3.00
*Voir aussi :* <<singegetaudiocalibration,singeGetAudioCalibration>>, <<singesetaudiodelay,singeSetAudioDelay>>
.Exemple
[source,lua]
----
-- Menu.singe : garder ou abandonner le calibrage que le joueur a réglé.
function calibrationEnd(save)
if save then
singeSetAudioCalibration(CAL_VALUE)
end
CALIBRATING = false
end
----
[#singesetaudiodelay]
==== singeSetAudioDelay
[source,text]
----
singeSetAudioDelay(milliseconds)
----
Règle le retard audio propre au jeu pour cette exécution : de combien l'audio est entendu plus tard (positif) ou plus tôt (négatif) que ce que le moteur peut mesurer, pour que la présentation de la vidéo se décale en conséquence. Il s'applique au disque et à chaque vidéo que lit le script, en plus du calibrage de la machine. Rien n'est sauvegardé ; un jeu qui propose ce réglage dans un menu de service range la valeur avec ses autres réglages et rappelle cette fonction au démarrage. Les valeurs hors de `-1000` à `1000` mettent fin au script.
*Paramètres :*
* `milliseconds` -- entier, de `-1000` à `1000`, positif quand l'audio est en retard.
*Depuis :* 3.00
*Voir aussi :* <<singegetaudiodelay,singeGetAudioDelay>>, <<singesetaudiocalibration,singeSetAudioCalibration>>
.Exemple
[source,lua]
----
-- Menu de service : ajuster un peu le décalage et le retenir.
function onInputPressed(what)
if what == SWITCH_LEFT then
settings.audioDelay = math.max(-1000, settings.audioDelay - 10)
elseif what == SWITCH_RIGHT then
settings.audioDelay = math.min(1000, settings.audioDelay + 10)
end
singeSetAudioDelay(settings.audioDelay)
saveSettings()
end
----
[#singesetgamename]
==== singeSetGameName
[source,text]
----
singeSetGameName(title)
----
Règle le titre de la fenêtre de Singe. Appelez-la une fois au démarrage avec le nom d'affichage du jeu ; rien d'autre n'utilise cette valeur. L'argument doit être une chaîne ou un nombre.
*Paramètres :*
* `title` -- le titre de la fenêtre.
*Depuis :* 1.15 (RDG)
*Voir aussi :* <<singeversion,singeVersion>>
.Exemple
[source,lua]
----
singeSetGameName("Asteroid Patrol")
keyboardSetMode(MODE_NORMAL)
hudFont = fontLoad(DIR .. "fonts/FreeSansBold.ttf", 18)
----
[#singesetpauseflag]
==== singeSetPauseFlag
[source,text]
----
singeSetPauseFlag(paused)
----
Règle l'indicateur de pause du moteur depuis le script. `true` met en pause le disque s'il est en lecture, chaque vidéo chargée et chaque canal d'effets sonores, en retenant lesquels tournaient ; `false` reprend exactement ceux-là. Contrairement à la touche pause, cela ne fige pas le script : les callbacks continuent d'être appelés pour que le script puisse dessiner son menu de pause et décider quand effacer l'indicateur. Les jeux qui gèrent leur propre pause devraient aussi prendre la touche pause avec `singeSetPauseKeyEnabled(false)`, car la touche du moteur bascule le même indicateur et lutterait sinon contre le script. Un argument qui n'est pas un booléen met fin au script.
*Paramètres :*
* `paused` -- booléen.
*Depuis :* 1.x (RDG)
*Voir aussi :* <<singegetpauseflag,singeGetPauseFlag>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>
.Exemple
[source,lua]
----
singeSetPauseKeyEnabled(false)
function onInputPressed(what)
if what == SWITCH_PAUSE then
menuOpen = not menuOpen
singeSetPauseFlag(menuOpen)
elseif menuOpen and what == SWITCH_QUIT then
singeQuit()
end
end
----
[#singesetpausekeyenabled]
==== singeSetPauseKeyEnabled
[source,text]
----
singeSetPauseKeyEnabled(enabled)
----
Choisit à qui appartient la touche pause affectée à `INPUT_PAUSE` dans `controls.cfg`. Quand elle est activée (par défaut, et rétablie à chaque rechargement), elle appartient au moteur : appuyer sur la touche fige complètement le jeu (voir <<pausing,La pause>>), et en `MODE_NORMAL` le script ne reçoit jamais `SWITCH_PAUSE`. Quand elle est désactivée, le moteur ignore la touche et `SWITCH_PAUSE` arrive à `onInputPressed` et `onInputReleased` comme n'importe quel autre contact, si bien que le jeu peut gérer sa propre pause avec `singeSetPauseFlag`. Un argument qui n'est pas un booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Depuis :* 3.00
*Voir aussi :* <<singesetpauseflag,singeSetPauseFlag>>, <<singegetpauseflag,singeGetPauseFlag>>, <<singedisablepausekey,singeDisablePauseKey>>
.Exemple
[source,lua]
----
-- Le moteur peut mettre en pause pendant la partie, mais pas pendant que le joueur saisit ses initiales.
function beginInitials()
singeSetPauseKeyEnabled(false)
keyboardSetMode(MODE_FULL)
end
function endInitials()
keyboardSetMode(MODE_NORMAL)
singeSetPauseKeyEnabled(true)
end
----
[#singesetquitkeyenabled]
==== singeSetQuitKeyEnabled
[source,text]
----
singeSetQuitKeyEnabled(enabled)
----
Extension Hypseus. Choisit si le moteur quitte sur le contact affecté à `INPUT_QUIT` dans `controls.cfg`, depuis le clavier ou une manette. Quand c'est activé (par défaut, et rétabli à chaque rechargement), appuyer dessus termine le jeu. Quand c'est désactivé, le moteur l'ignore, si bien qu'un jeu peut d'abord demander confirmation ou sauvegarder ; le script reçoit `SWITCH_QUIT` par `onInputPressed` et `onInputReleased` dans les deux cas, et `singeQuit` termine toujours le jeu. Le bouton de fermeture de la fenêtre quitte toujours. `Framework.singe` définit `keyboardCatchQuit(catch)`, le nom que donne Hypseus Singe au même réglage, avec le sens inverse. Un argument qui n'est pas un booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<keyboardcatchquit,keyboardCatchQuit>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<singequit,singeQuit>>
.Exemple
[source,lua]
----
-- Demander avant de quitter une partie en cours.
singeSetQuitKeyEnabled(false)
function onInputPressed(what)
if what == SWITCH_QUIT then
if confirmQuit() then
singeQuit()
end
end
end
----
[#singeversion]
==== singeVersion
[source,text]
----
version = singeVersion()
----
Renvoie la version du moteur sous forme de nombre, le numéro majeur plus le mineur en fraction : `3.00` revient donc comme `3`. Comparez-la avec `<` et `>=` pour refuser un moteur trop ancien ou pour contourner des fonctionnalités ; `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR` et `SINGE_VERSION_STRING` sont aussi définies comme globales pour les scripts qui veulent les parties ou le texte.
*Renvoie :* nombre.
*Depuis :* 1.x (RDG)
*Voir aussi :* <<singesetgamename,singeSetGameName>>
.Exemple
[source,lua]
----
if singeVersion() < 3 then
error("This game needs Singe 3.00 or newer, found " .. SINGE_VERSION_STRING)
end
----
[#singewantscrosshairs]
==== singeWantsCrosshairs
[source,text]
----
wanted = singeWantsCrosshairs()
----
Renvoie `true` sauf si Singe a été démarré avec `--nocrosshair`. Ne dessinez le réticule de visée logiciel que quand elle renvoie `true` ; les joueurs dont les pistolets optiques affichent leur propre réticule à l'écran désactivent le réticule logiciel avec cette option. La valeur ne change pas pendant une exécution : lisez-la donc une seule fois.
*Renvoie :* booléen.
*Depuis :* 2.00
*Voir aussi :* <<mousegetposition,mouseGetPosition>>, <<onmousemoved,onMouseMoved>>
.Exemple
[source,lua]
----
drawCrosshair = singeWantsCrosshairs()
function onOverlayUpdate()
overlayClear()
drawTargets()
if drawCrosshair then
spriteDraw(reticle, cursorX, cursorY, true)
end
return OVERLAY_UPDATED
end
----
[#soft]
=== Corps mou
Tissus, corps à pression et cordes : des maillages dont les sommets sont simulés comme des particules (voir <<physics,Physique>>, Corps mous). Un corps mou est désigné par son nœud, et un nœud qui n'en a pas met fin au script dans chaque appel qui suit `softNew`. Les positions sont en unités du monde, la masse en kilogrammes, et les valeurs de raideur vont de `0` à `1`. Le moteur réécrit le maillage du nœud à partir des particules après chaque pas, et la simulation se termine d'elle-même quand le nœud est supprimé ou reçoit un autre maillage. Jusqu'à 16 corps mous à la fois, avec jusqu'à 32 épingles chacun.
[#softnew]
==== softNew
[source,text]
----
softNew(node, SOFT_CLOTH)
softNew(node, SOFT_BODY)
softNew(node, SOFT_ROPE, x, y, z, segments, radius)
----
Pour `SOFT_CLOTH` et `SOFT_BODY`, le maillage du nœud, de trois sommets ou plus, devient les particules : les sommets à la même position (à `0.0001` unité près) sont soudés en un seul, placés là où la transformation du nœud les met dans le monde, et les triangles donnent les contraintes d'étirement, de cisaillement et de flexion. Un tissu entre en collision par ses deux faces et convient à une feuille ouverte ; un corps est un maillage fermé que `softSetPressure` peut gonfler. Pour `SOFT_ROPE`, le nœud n'a besoin d'aucun maillage : `segments + 1` particules s'alignent de la position du nœud dans le monde jusqu'au point du monde donné, le nœud reçoit un nouveau maillage de tube à six pans de ce rayon autour d'elles (en gardant le matériau qu'il avait), et les particules entrent en collision comme des sphères de ce rayon, alors que les particules de tissu et de corps sont des sphères de rayon `0.03`. Chaque type commence avec un étirement de `0.9`, une flexion de `0.2`, une masse de `1` kilogramme, un amortissement de `0.1` et une pression de `0`. Un corps mou déjà présent sur le nœud est retiré avant que le nouveau soit créé : un appel qui échoue laisse donc le nœud sans aucun ; le maillage de tube d'une corde s'en va avec lui, ce qui explique pourquoi le nœud d'une corde ne peut pas devenir un tissu ou un corps (l'appel échoue faute de maillage). Met fin au script quand la physique n'est pas disponible, que le type est inconnu, que le nombre d'arguments ne correspond pas au type, que le maillage manque, qu'une corde n'a pas de segments ou pas de rayon, ou que 16 corps mous existent déjà.
*Paramètres :*
* `node` -- le nœud qui porte le maillage (tissu, corps), ou le point de départ de la corde.
* `x`, `y`, `z` -- l'autre bout de la corde, en unités du monde.
* `segments` -- le nombre de maillons de la corde, `1` ou plus ; une douzaine suffit largement.
* `radius` -- le rayon de la corde, en unités du monde ; c'est aussi la taille avec laquelle ses particules entrent en collision.
*Remarques :* Un maillage partagé par plusieurs nœuds est réécrit pour tous : donnez donc à un corps mou un maillage à lui. Les maillages skinnés ne peuvent pas être mous.
*Depuis :* 3.00.
*Voir aussi :* <<softpin,softPin>>, <<softsetstiffness,softSetStiffness>>, <<softsetpressure,softSetPressure>>, <<meshplane,meshPlane>>
.Exemple
[source,lua]
----
-- Un drapeau accroché au mât : une grille dressée, épinglée tout le long de son bord gauche.
local flag = nodeNew()
nodeSetMesh(flag, meshPlane(1.6, 1.0, 16, 10), cloth)
nodeSetPosition(flag, -4.16, 2.4, 0)
nodeSetRotation(flag, 90, 0, 0)
softNew(flag, SOFT_CLOTH)
softSetStiffness(flag, 0.95, 0.1)
softSetMass(flag, 0.5)
for _, y in ipairs({ 1.9, 2.15, 2.4, 2.65, 2.9 }) do
softPin(flag, -4.96, y, 0) -- Le long du mât
end
----
[#softdelete]
==== softDelete
[source,text]
----
softDelete(node)
----
Termine la simulation et libère son corps. Le nœud garde son maillage dans la forme où il a été écrit en dernier : un drap tombé reste drapé sur la caisse et une corde garde son tube ; la forme d'origine du maillage n'est pas rétablie. Un nœud sans corps mou met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<softnew,softNew>>, <<nodedelete,nodeDelete>>
.Exemple
[source,lua]
----
-- Une fois que le drap s'est posé sur la statue, le figer et cesser d'en payer le coût.
function onOverlayUpdate()
if covering then
settleFrames = settleFrames + 1
if settleFrames > 240 then
softDelete(sheet) -- Le drapé reste tel quel
covering = false
end
end
return OVERLAY_NOT_UPDATED
end
----
[#softpin]
==== softPin
[source,text]
----
softPin(node, x, y, z)
softPin(node, x, y, z, otherNode)
----
Trouve la particule la plus proche du point du monde, où que se trouvent les particules à cet instant, et la fixe. Seule, la particule est maintenue là où elle est ; avec `otherNode`, elle est déplacée à la position de ce nœud dans le monde avant chaque pas, si bien qu'une corde pend d'une grue qui pivote et qu'une cape suit les épaules. Le point n'a pas besoin d'être sur le maillage, puisque c'est la particule la plus proche qui l'emporte, et épingler de nouveau la même particule ne change que ce qu'elle suit. Au-delà de 32 épingles, l'appel ne fait rien.
*Paramètres :*
* `node` -- le nœud du corps mou.
* `x`, `y`, `z` -- un point du monde ; la particule la plus proche est épinglée.
* `otherNode` -- un nœud que la particule suivra désormais.
*Depuis :* 3.00.
*Voir aussi :* <<softunpin,softUnpin>>, <<softnew,softNew>>, <<nodegetworldposition,nodeGetWorldPosition>>
.Exemple
[source,lua]
----
-- Une corde qui part du crochet de la grue et descend, en se balançant avec la grue.
local crane = nodeNew()
nodeSetPosition(crane, 4, 4, -1)
local rope = nodeNew()
nodeSetPosition(rope, 4, 4, -1)
softNew(rope, SOFT_ROPE, 6.5, 4, -1, 16, 0.04)
nodeSetMaterial(rope, hemp)
softSetMass(rope, 1.5)
softPin(rope, 4, 4, -1, crane) -- Le bout du haut suit le crochet
----
[#softunpin]
==== softUnpin
[source,text]
----
softUnpin(node, x, y, z)
----
Libère la particule épinglée la plus proche du point du monde. Rien ne se passe quand cette particule n'est pas épinglée. Le corps est refait autour de la forme actuelle, ce qui arrête aussi net chaque particule : un drapeau qu'on lâche commence donc à tomber depuis le repos.
*Depuis :* 3.00.
*Voir aussi :* <<softpin,softPin>>
.Exemple
[source,lua]
----
-- Le coin de la bannière se déchire et se libère quand la rafale frappe.
function onOverlayUpdate()
gustFrames = gustFrames + 1
if gustFrames == 200 then
softUnpin(banner, 3.0, 4.5, 0)
soundPlay(ripSound)
end
return OVERLAY_NOT_UPDATED
end
----
[#softsetstiffness]
==== softSetStiffness
[source,text]
----
softSetStiffness(node, stretch, bend)
----
À quel point le matériau résiste à l'étirement (et, pour un tissu, au cisaillement) et au pliage, de `0` à `1` ; les valeurs hors plage sont ramenées dans la plage. `1` est rigide, `0` le plus lâche que permet le solveur. Par défaut `0.9` et `0.2`. Le corps est refait autour de la forme actuelle : les longueurs au repos deviennent les longueurs de cet instant, et chaque particule s'arrête ; réglez donc le matériau avant que le tissu commence à bouger. Une corde n'a que l'étirement.
*Paramètres :*
* `node` -- le nœud du corps mou.
* `stretch` -- la résistance à l'étirement, de `0` à `1` ; `0.9` par défaut.
* `bend` -- la résistance au pliage, de `0` à `1` ; `0.2` par défaut.
*Depuis :* 3.00.
*Voir aussi :* <<softsetmass,softSetMass>>, <<softsetdamping,softSetDamping>>
.Exemple
[source,lua]
----
-- De la soie, de la toile et une feuille de caoutchouc tirées de la même grille.
local silk = clothNode(silkMaterial)
softNew(silk, SOFT_CLOTH)
softSetStiffness(silk, 0.7, 0.02)
local canvas = clothNode(canvasMaterial)
softNew(canvas, SOFT_CLOTH)
softSetStiffness(canvas, 1.0, 0.4)
local rubber = clothNode(rubberMaterial)
softNew(rubber, SOFT_CLOTH)
softSetStiffness(rubber, 0.3, 0.1)
----
[#softsetpressure]
==== softSetPressure
[source,text]
----
softSetPressure(node, pressure)
----
Gonfle un `SOFT_BODY` : `0` (par défaut) donne un sac dégonflé, et quelques unités un ballon bien ferme ; une valeur négative devient `0`. Elle modifie le corps en cours de simulation sans le refaire et le réveille, si bien qu'un ballon peut être gonflé ou dégonflé pendant qu'il rebondit. Les tissus et les cordes n'ont pas de volume à gonfler.
*Depuis :* 3.00.
*Voir aussi :* <<softnew,softNew>>, <<softsetstiffness,softSetStiffness>>, <<meshsphere,meshSphere>>
.Exemple
[source,lua]
----
softNew(balloon, SOFT_BODY) -- Un meshSphere de 0.5
softSetMass(balloon, 0.6)
softSetPressure(balloon, 8) -- Un ballon bien ferme
function onOverlayUpdate()
if punctured then
pressure = math.max(pressure - 0.1, 0)
softSetPressure(balloon, pressure) -- Se dégonfle en sifflant en deux ou trois secondes
end
return OVERLAY_NOT_UPDATED
end
----
[#softsetmass]
==== softSetMass
[source,text]
----
softSetMass(node, kilograms)
----
La masse du corps entier, répartie également entre ses particules ; `1` par défaut, maintenue à `0.001` ou plus. Les particules épinglées ne pèsent rien quoi qu'il en soit. Le corps est refait autour de la forme actuelle et chaque particule s'arrête : réglez-la donc dès le départ.
*Depuis :* 3.00.
*Voir aussi :* <<softsetstiffness,softSetStiffness>>, <<softsetdamping,softSetDamping>>
.Exemple
[source,lua]
----
-- Un drap épinglé par ses quatre coins rattrape une caisse qu'on laisse tomber dessus.
local sheet = nodeNew()
nodeSetMesh(sheet, meshPlane(3, 3, 20, 20), linen)
nodeSetPosition(sheet, 0, 1.6, 0)
softNew(sheet, SOFT_CLOTH)
softSetStiffness(sheet, 1.0, 0.05)
softSetMass(sheet, 4) -- Assez lourd pour ne pas voleter
for _, c in ipairs({ { -1.5, -1.5 }, { 1.5, -1.5 }, { -1.5, 1.5 }, { 1.5, 1.5 } }) do
softPin(sheet, c[1], 1.6, c[2])
end
----
[#softsetdamping]
==== softSetDamping
[source,text]
----
softSetDamping(node, damping)
----
À quelle vitesse le mouvement des particules s'éteint, de `0` en montant ; `0.1` par défaut, et une valeur négative devient `0`. Des valeurs plus élevées calment un drapeau qui n'arrête pas d'onduler ou une corde qui se balance sans fin. Le corps est refait autour de la forme actuelle et chaque particule s'arrête.
*Depuis :* 3.00.
*Voir aussi :* <<softsetmass,softSetMass>>, <<softsetstiffness,softSetStiffness>>
.Exemple
[source,lua]
----
-- Un rideau dans l'air immobile se calme vite ; le même rideau près de la fenêtre ouverte continue de bouger.
local curtain = nodeNew()
nodeSetMesh(curtain, meshPlane(1.2, 2.4, 12, 24), velvet)
nodeSetPosition(curtain, 2, 2.4, -3)
nodeSetRotation(curtain, 90, 0, 0)
softNew(curtain, SOFT_CLOTH)
softSetDamping(curtain, windowOpen and 0.05 or 0.4)
for x = -0.6, 0.6, 0.1 do
softPin(curtain, 2 + x, 3.6, -3)
end
----
[#sound]
=== Son
Les effets sonores sont des clips audio (le WAV est le choix sûr) chargés en mémoire et mélangés par SDL_mixer, à part de l'audio du disque lui-même et de l'audio des ressources `video*`. `soundLoad` renvoie un handle de son qui identifie les données du clip ; `soundPlay` démarre ce clip sur l'un des seize canaux du mélangeur et renvoie le numéro du canal, de `0` à `15`, que prennent `soundPause`, `soundResume`, `soundStop`, `soundIsPlaying` et chaque appel `soundSet*`. Un même clip peut jouer sur plusieurs canaux à la fois, et un numéro de canal est réutilisé dès que le canal est libre : ne confondez donc pas les deux sortes de numéros. Un volume général des effets (`soundSetVolume`, de `0` à `63`) met chaque canal à l'échelle. Un canal en cours de lecture peut aussi se trouver dans la scène 3D, en suivant un nœud ou fixé en un point en unités du monde, où il est réparti en stéréo et atténué selon sa distance à l'auditeur (voir <<scenes3d,Scènes 3D>>) ; pour les jeux en 2D, `soundSetPan` répartit un canal sans scène.
[#soundfullstop]
==== soundFullStop
[source,text]
----
soundFullStop()
----
Arrête d'un coup tous les canaux d'effets, y compris ceux en pause, et les libère pour qu'ils soient réutilisés. Aucun clip n'est déchargé ; les handles de `soundLoad` restent valides. Chaque canal qui jouait ou était en pause le signale par `onSoundCompleted`, exactement comme après `soundStop`. Appelez-la lors d'un changement de scène ou avant une cinématique plutôt que de suivre chaque canal que vous avez démarré.
*Depuis :* 1.16
*Voir aussi :* <<soundstop,soundStop>>, <<onsoundcompleted,onSoundCompleted>>
.Exemple
[source,lua]
----
function enterAttractMode()
soundFullStop()
ambientChannel = -1
engineChannel = -1
discPlay()
end
----
[#soundgetposition]
==== soundGetPosition
[source,text]
----
x, y, z, gain = soundGetPosition(channel)
----
Indique où se trouve un canal placé par rapport à l'auditeur, sous forme d'une direction de longueur unité dans le repère de l'auditeur (`x` positif vers la droite, `y` positif vers le haut, `z` positif derrière l'auditeur), et le facteur de volume que lui donne actuellement sa distance, de `0` à `1`. Les valeurs sont rafraîchies une fois par image à partir de l'auditeur et du nœud ou du point du canal. Un canal qui n'a pas été placé dans la scène depuis son démarrage renvoie `0, 0, 0, 1`. Destiné aux indicateurs façon radar et à la vérification d'un mixage depuis un script de test ; un canal hors de `0` à `15` met fin au script.
*Renvoie :* trois nombres pour la direction et un pour le gain dû à la distance.
*Depuis :* 3.00.
*Voir aussi :* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>
.Exemple
[source,lua]
----
-- Pointer une flèche du HUD vers l'ennemi audible le plus proche.
function onOverlayUpdate()
overlayClear()
local x, y, z, gain = soundGetPosition(growlChannel)
if gain > 0 then
local angle = math.atan(x, -z)
drawThreatArrow(angle, gain)
end
return OVERLAY_UPDATED
end
----
[#soundgetvolume]
==== soundGetVolume
[source,text]
----
volume = soundGetVolume()
----
Renvoie le volume général des effets, de `0` à `63`. Au démarrage, il reflète le volume des effets donné en ligne de commande : un jeu qui affiche un réglage de volume doit donc le lire plutôt que de supposer le maximum.
*Renvoie :* entier, de `0` à `63`.
*Depuis :* 1.16
*Voir aussi :* <<soundsetvolume,soundSetVolume>>
.Exemple
[source,lua]
----
-- Baisser les effets pendant que le narrateur parle, puis les remettre.
function narratorBegin()
savedVolume = soundGetVolume()
soundSetVolume(math.floor(savedVolume / 3))
narratorChannel = soundPlay(narratorClip)
end
----
[#soundisplaying]
==== soundIsPlaying
[source,text]
----
playing = soundIsPlaying(channel)
----
Teste si un canal joue en ce moment. Un canal en pause répond `false`, tout comme un canal dont le clip est terminé. Un canal hors de `0` à `15` met fin au script ; un canal simplement devenu silencieux, non.
*Renvoie :* booléen.
*Depuis :* 1.16 (RDG)
*Voir aussi :* <<soundplay,soundPlay>>, <<soundpause,soundPause>>
.Exemple
[source,lua]
----
-- Ne pas relancer l'alarme tant qu'elle sonne encore.
function raiseAlarm()
if alarmChannel < 0 or not soundIsPlaying(alarmChannel) then
alarmChannel = soundPlay(alarmClip)
end
end
----
[#soundload]
==== soundLoad
[source,text]
----
id = soundLoad(filename)
----
Charge un clip audio en mémoire et renvoie son handle. Le chemin passe par le système de fichiers virtuel du jeu : il fonctionne donc de la même façon depuis un dossier ordinaire et depuis un jeu empaqueté. Chargez les clips une fois au démarrage, pas dans un callback appelé à chaque image. Un fichier qui ne peut pas être ouvert ou décodé met fin au script.
*Renvoie :* handle de son entier. C'est un handle de données, pas un numéro de canal.
*Depuis :* 1.x
*Voir aussi :* <<soundplay,soundPlay>>, <<soundunload,soundUnload>>
.Exemple
[source,lua]
----
-- Des handles de clips, chargés une fois et déchargés dans onShutdown.
shotClip = soundLoad(DIR .. "sounds/shot.wav")
missClip = soundLoad(DIR .. "sounds/miss.wav")
coinClip = soundLoad(DIR .. "sounds/coin.wav")
ambientClip = soundLoad(DIR .. "sounds/wind.wav")
----
[#soundloaddata]
==== soundLoadData
[source,text]
----
id = soundLoadData(data)
----
Extension Hypseus. Construit un son à partir d'octets contenus dans une chaîne Lua au lieu d'un fichier. Tous les formats que lit <<soundload,`soundLoad`>> sont lus ici, et le handle renvoyé s'utilise avec le reste de la famille `sound` exactement de la même façon.
La chaîne peut contenir des octets nuls ; c'est sa longueur, et non son premier zéro, qui décide de ce qui est lu. Les octets sont décodés pendant l'appel et la chaîne n'est pas conservée : elle peut donc être jetée ensuite. Des octets qui ne sont pas de l'audio, ou une chaîne vide, mettent fin au script, comme un fichier illisible dans `soundLoad`.
Servez-vous-en pour l'audio qu'un jeu génère, décompresse ou déchiffre lui-même. L'audio que le système de fichiers virtuel peut atteindre par son nom -- y compris l'audio contenu dans un `.game` empaqueté -- se charge mieux avec `soundLoad`, qui ne garde pas le fichier entier dans une chaîne Lua.
*Paramètres :*
* `data` -- chaîne d'octets.
*Renvoie :* nombre, le handle du son.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<soundload,soundLoad>>, <<spriteloaddata,spriteLoadData>>, <<soundplay,soundPlay>>
.Exemple
[source,lua]
----
-- Un effet sonore que le jeu extrait de sa propre archive.
local bytes = myArchive:extract("beep.wav")
local beep = soundLoadData(bytes)
soundPlay(beep)
----
[#soundpause]
==== soundPause
[source,text]
----
wasPlaying = soundPause(channel)
----
Met un canal en pause sur place ; `soundResume` le reprend au même point. Un canal en pause compte toujours comme occupé : `soundPlay` ne le donnera donc pas à un autre clip, et `soundIsPlaying` répond `false` pour lui. Mettre en pause un canal qui ne joue pas ne fait rien. Un canal hors de `0` à `15` met fin au script.
*Renvoie :* booléen, `true` quand le canal jouait au moment de l'appel.
*Depuis :* 1.16 (RDG)
*Voir aussi :* <<soundresume,soundResume>>, <<soundisplaying,soundIsPlaying>>
.Exemple
[source,lua]
----
-- Suspendre le bruit du moteur pendant que le message radio passe.
function radioBegin(messageClip)
engineWasRunning = soundPause(engineChannel)
radioChannel = soundPlay(messageClip)
end
----
[#soundplay]
==== soundPlay
[source,text]
----
channel = soundPlay(id)
channel = soundPlay(id, loops)
channel = soundPlay(id, loops, volume)
----
Démarre un clip chargé sur le premier canal libre et renvoie le numéro de ce canal. Un canal compte comme libre quand il ne joue pas et n'est pas en pause. Le nouveau canal démarre au volume général (ou à `volume` en dessous), centré, sans position dans la scène et avec la portée par défaut, quoi qu'ait réglé l'usage précédent du canal. Quand les seize canaux sont occupés, rien ne joue et c'est `-1` (`SOUND_ERROR_INVALID`) qui revient ; un jeu qui déclenche beaucoup de sons qui se chevauchent doit le vérifier. Un handle qui n'est pas chargé met fin au script.
*Paramètres :*
* `id` -- handle de son venant de `soundLoad`.
* `loops` -- `0` (par défaut) joue le clip une fois, un nombre positif le répète autant de fois en plus, `-1` le fait boucler jusqu'à ce qu'il soit arrêté. Toute valeur inférieure à `-1` met fin au script.
* `volume` -- le volume propre du canal, de `0` à `63`, sous le volume général ; par défaut `63`, le clip aussi fort que le permet le volume général. Une valeur hors de cette plage met fin au script. `soundSetChannelVolume` le change pendant que le clip joue.
*Renvoie :* numéro de canal entier, de `0` à `15`, ou `-1` quand aucun canal n'est libre.
*Depuis :* 1.x ; `volume` depuis 3.00.
*Voir aussi :* <<soundload,soundLoad>>, <<soundstop,soundStop>>, <<soundsetchannelvolume,soundSetChannelVolume>>, <<soundsetnode,soundSetNode>>
.Exemple
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
local channel = soundPlay(shotClip)
if channel < 0 then
debugPrint("All sound channels busy; shot dropped")
end
elseif what == SWITCH_COIN1 then
soundPlay(coinClip)
credits = credits + 1
end
end
----
[#soundresume]
==== soundResume
[source,text]
----
wasPaused = soundResume(channel)
----
Reprend un canal que `soundPause` a arrêté, là où il s'était interrompu. Reprendre un canal qui n'est pas en pause ne fait rien. Un canal hors de `0` à `15` met fin au script.
*Renvoie :* booléen, `true` quand le canal était en pause au moment de l'appel.
*Depuis :* 1.16 (RDG)
*Voir aussi :* <<soundpause,soundPause>>
.Exemple
[source,lua]
----
function onSoundCompleted(channel)
if channel == radioChannel then
radioChannel = -1
if engineWasRunning then
soundResume(engineChannel)
end
end
end
----
[#soundsetchannelvolume]
==== soundSetChannelVolume
[source,text]
----
soundSetChannelVolume(channel, volume)
----
Règle le volume propre d'un canal, en plus du volume général de `soundSetVolume` : le canal joue au volume général multiplié par celui-ci, multiplié encore par son atténuation due à la distance quand il est dans la scène. Chaque canal démarre à plein quand `soundPlay` le prend, sauf si un volume a été donné à `soundPlay` : un pas discret et un coup de feu bruyant peuvent ainsi partager un même volume général. Les valeurs hors de `0` à `63` mettent fin au script, tout comme un canal hors de `0` à `15`.
*Paramètres :*
* `channel` -- numéro de canal venant de `soundPlay`.
* `volume` -- entier, de `0` (muet) à `63` (aussi fort que le volume général).
*Depuis :* 3.00
*Voir aussi :* <<soundplay,soundPlay>>, <<soundsetvolume,soundSetVolume>>
.Exemple
[source,lua]
----
-- Un moteur qui gronde plus fort avec l'accélérateur : un clip en boucle, le volume de son canal
-- suivant la pédale à chaque image.
engine = soundPlay(engineClip, -1, 20)
function onOverlayUpdate()
soundSetChannelVolume(engine, math.floor(20 + throttle * 43))
end
----
[#soundsetlistener]
==== soundSetListener
[source,text]
----
soundSetListener(node)
soundSetListener()
----
Choisit le nœud depuis lequel on entend les canaux placés, tourné dans le même sens que lui. Sans argument, ou avec `nil`, l'auditeur est la caméra de la scène, ce qui convient à un jeu à la première personne ou à caméra fixe ; un jeu à la troisième personne préfère généralement le nœud du joueur, pour que les sons se répartissent autour du personnage plutôt que de la caméra. Un nœud supprimé plus tard fait revenir l'auditeur à la caméra. Un handle de nœud qui n'existe pas met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<cameraset,cameraSet>>
.Exemple
[source,lua]
----
-- Caméra de poursuite, mais entendre le monde depuis la voiture.
local car = modelInstance(carModel)
local chaseCamera = nodeNew(car)
nodeSetPosition(chaseCamera, 0, 2, 6)
cameraSet(chaseCamera)
soundSetListener(car)
----
[#soundsetnode]
==== soundSetNode
[source,text]
----
soundSetNode(channel, node)
soundSetNode(channel)
----
Attache un canal en cours de lecture à un nœud de la scène pour qu'on l'entende de l'endroit où se trouve le nœud à chaque image : le canal est réparti vers la direction du nœud vue depuis l'auditeur et atténué selon sa distance d'après `soundSetRange`. L'appeler sans nœud, ou avec `nil`, ressort le canal de la scène, au plein volume général. Un canal dont le nœud est supprimé revient de lui-même à une lecture ordinaire. L'attache dure jusqu'à ce que le canal soit arrêté ou réutilisé par `soundPlay`. Un canal hors de `0` à `15`, ou un nœud qui n'existe pas, met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<soundsetposition,soundSetPosition>>, <<soundsetrange,soundSetRange>>, <<soundsetlistener,soundSetListener>>
.Exemple
[source,lua]
----
-- Chaque brasero crépite depuis l'endroit où il se trouve.
for i, brazier in ipairs(braziers) do
local channel = soundPlay(crackleClip, -1)
if channel >= 0 then
soundSetNode(channel, brazier)
soundSetRange(channel, 1.5, 14)
end
end
----
[#soundsetpan]
==== soundSetPan
[source,text]
----
soundSetPan(channel, pan)
----
Répartit un canal entre les deux voies stéréo, pour les jeux sans scène : `-1` est tout à gauche, `1` tout à droite, `0` au centre, et les valeurs hors de cette plage sont ramenées dans la plage. S'éloigner du centre ne baisse que le haut-parleur opposé ; à `pan = 0.5`, le haut-parleur gauche joue à mi-gain et le droit à plein. L'appel ressort le canal de la scène si `soundSetNode` ou `soundSetPosition` l'y avait placé. Un canal hors de `0` à `15` met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<soundplay,soundPlay>>, <<soundsetnode,soundSetNode>>
.Exemple
[source,lua]
----
-- Répartir une voiture qui passe selon sa place sur l'overlay.
function carPassBy(x)
local channel = soundPlay(carClip)
if channel >= 0 then
soundSetPan(channel, (x / overlayGetWidth()) * 2 - 1)
end
end
----
[#soundsetposition]
==== soundSetPosition
[source,text]
----
soundSetPosition(channel, x, y, z)
----
Place un canal en cours de lecture en un point fixe de la scène, en unités du monde, en remplaçant tout nœud qu'il suivait. Il est ensuite réparti et atténué par rapport à l'auditeur comme un canal attaché à un nœud. Convient à un son qui ne bouge pas, comme une cascade ou un téléphone qui sonne ; une source en mouvement est plus simple avec `soundSetNode`. Un canal hors de `0` à `15` met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<soundsetnode,soundSetNode>>, <<soundsetrange,soundSetRange>>, <<soundgetposition,soundGetPosition>>
.Exemple
[source,lua]
----
-- La cascade ne bouge jamais : pas besoin de nœud.
waterfallChannel = soundPlay(waterfallClip, -1)
if waterfallChannel >= 0 then
soundSetPosition(waterfallChannel, 42, 3, -18)
soundSetRange(waterfallChannel, 6, 80)
end
----
[#soundsetrange]
==== soundSetRange
[source,text]
----
soundSetRange(channel, near, far)
----
Règle la façon dont un canal placé s'atténue avec la distance à l'auditeur. En deçà de `near`, il joue à plein volume ; au-delà, son gain est `near` divisé par la distance ; sur le dernier cinquième de `far`, il s'atténue en plus de façon linéaire, jusqu'au silence à `far`. Les valeurs par défaut sont `1` et `30` unités du monde, rétablies chaque fois que `soundPlay` réutilise le canal. Un murmure veut un petit `far`, une cascade un grand.
*Paramètres :*
* `channel` -- numéro de canal venant de `soundPlay`.
* `near` -- la distance en deçà de laquelle le canal est à plein volume, supérieure à `0`.
* `far` -- la distance à laquelle il devient muet, supérieure à `near`. Toute autre paire met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<soundsetnode,soundSetNode>>, <<soundsetposition,soundSetPosition>>, <<soundgetposition,soundGetPosition>>
.Exemple
[source,lua]
----
-- Les pas d'un garde ne s'entendent que de près ; la sirène porte loin.
local steps = soundPlay(footstepClip, -1)
soundSetNode(steps, guard)
soundSetRange(steps, 0.5, 8)
local siren = soundPlay(sirenClip, -1)
soundSetNode(siren, tower)
soundSetRange(siren, 10, 200)
----
[#soundsetvolume]
==== soundSetVolume
[source,text]
----
soundSetVolume(volume)
----
Règle le volume général des effets et l'applique aussitôt à chaque canal, en lecture ou en pause. Le volume propre de chaque canal donné par `soundSetChannelVolume` et, pour les canaux placés, l'atténuation due à la distance continuent de s'y ajouter. Les valeurs hors de `0` à `63` mettent fin au script.
*Paramètres :*
* `volume` -- entier, de `0` (muet) à `63` (le plus fort).
*Depuis :* 1.16
*Voir aussi :* <<soundgetvolume,soundGetVolume>>, <<soundsetchannelvolume,soundSetChannelVolume>>
.Exemple
[source,lua]
----
-- Curseur de volume du menu de service piloté par les contacts gauche et droit.
function onInputPressed(what)
local volume = soundGetVolume()
if what == SWITCH_LEFT then
soundSetVolume(math.max(0, volume - 4))
elseif what == SWITCH_RIGHT then
soundSetVolume(math.min(63, volume + 4))
end
soundPlay(clickClip)
end
----
[#soundstop]
==== soundStop
[source,text]
----
wasPlaying = soundStop(channel)
----
Arrête immédiatement un canal, qu'il soit en lecture ou en pause, et le libère pour qu'il soit réutilisé. Le canal le signale par `onSoundCompleted` exactement comme quand le clip se termine de lui-même : un script qui enchaîne des sons depuis ce callback doit donc vérifier s'il a arrêté le canal lui-même. Le `soundPlay` suivant peut redonner le même numéro : oubliez donc ce numéro côté Lua. Un canal hors de `0` à `15` met fin au script.
*Renvoie :* booléen, `true` quand le canal jouait au moment de l'appel. Un canal en pause renvoie `false` mais est bien arrêté.
*Depuis :* 1.x (RDG)
*Voir aussi :* <<soundfullstop,soundFullStop>>, <<onsoundcompleted,onSoundCompleted>>
.Exemple
[source,lua]
----
function playerDied()
if engineChannel >= 0 then
soundStop(engineChannel)
engineChannel = -1
end
soundPlay(explosionClip)
lives = lives - 1
end
----
[#soundunload]
==== soundUnload
[source,text]
----
soundUnload(id)
----
Libère un clip chargé et invalide son handle. Le mélangeur garde les données du clip en vie jusqu'à ce que chaque canal qui le joue encore se soit arrêté : décharger ne coupe donc pas court un canal en lecture. Tout usage ultérieur du handle, y compris un autre `soundUnload`, met fin au script. Sa place habituelle est `onShutdown`.
*Paramètres :*
* `id` -- handle de son venant de `soundLoad`, pas un numéro de canal.
*Depuis :* 2.00
*Voir aussi :* <<soundload,soundLoad>>, <<onshutdown,onShutdown>>
.Exemple
[source,lua]
----
function onShutdown()
soundUnload(shotClip)
soundUnload(missClip)
soundUnload(coinClip)
soundUnload(ambientClip)
fontUnload(hudFont)
end
----
[#sprite]
=== Sprite
Les sprites sont des images bitmap 2D chargées depuis le disque avec `spriteLoad`, ou rendues à partir de texte avec `fontToSprite`, et dessinées sur l'overlay en coordonnées de l'overlay. Chaque appel `sprite*` prend le handle entier comme premier argument (Singe 2.10 le mettait en dernier ; voir <<migrating,Migrer depuis Singe 2.10>>), et un handle déchargé met fin au script avec une erreur. Tout format que lit SDL_image est accepté ; un GIF ou un WEBP animé de deux images ou plus se charge comme une animation qui reste sur l'image `0`, arrêtée et sans boucle, jusqu'à ce que `spritePlay` et `spriteLoop` en décident autrement. La couleur `0` est dessinée transparente, comme dans Hypseus : l'indice de palette `0`, ou le noir pur, y compris le noir opaque d'une image qui a un canal alpha, dont l'alpha est respecté lui aussi. Pour garder le noir opaque d'une image, chargez-la après <<drawtransparent,`drawTransparent(true)`>>. Libérez chaque sprite avec `spriteUnload`, en général depuis <<onshutdown,onShutdown>>.
*Planches de sprites.* Hypseus Singe étend la famille avec les planches de sprites : `spriteLoadFrames` découpe une bande d'images égales, sur une seule rangée, en une animation qui ne joue pas, et `spriteDrawFrame` dessine une image choisie (comptée à partir de 1) sans toucher à la lecture. Pour le reste, une planche est une animation ordinaire, si bien que `spriteSetFrame` (compté à partir de 0), `spritePlay`, `spriteDraw`, `nodeSetSprite` et `emitterSetTexture` l'acceptent tous, et que `spriteFrameWidth` et `spriteFrameHeight` donnent la taille d'une image avant mise à l'échelle. Les entrées marquées « Extension Hypseus » existent pour que les jeux écrits pour Hypseus Singe tournent sans modification.
[#drawtransparent]
==== drawTransparent
[source,text]
----
drawTransparent(boolean)
----
Extension Hypseus. Décide si la couleur 0 est dessinée transparente sur les sprites chargés, et sur le texte rendu, après l'appel. `false`, la valeur par défaut, rend la couleur 0 transparente comme le fait Hypseus : l'indice de palette 0, ou le noir pur, y compris le noir opaque d'une image qui a un canal alpha, si bien qu'un lettrage noir ou un contour noir disparaît. `true` les dessine d'après leur seul canal alpha, si bien que le noir opaque reste noir. Les sprites déjà chargés gardent le réglage avec lequel ils ont été chargés, et chaque script commence à `false`.
*Renvoie :* rien.
*Depuis :* 3.00
.Exemple
[source,lua]
----
-- Le lettrage du logo est noir opaque : chargez-le d'après son seul alpha, puis revenez
-- à la transparence habituelle pour les illustrations propres au jeu.
drawTransparent(true)
logo = spriteLoad("art/logo.png")
drawTransparent(false)
----
[#spriteblend]
==== spriteBlend
[source,text]
----
spriteBlend(boolean, sprite)
----
Extension Hypseus, acceptée et ignorée. C'est un contournement propre à ce moteur : Hypseus charge les sprites avec le mode de mélange de SDL désactivé, et cet appel le réactive pour un sprite. Singe dessine déjà chaque sprite avec son canal alpha, si bien que le mélange n'est jamais désactivé au départ. Le handle du sprite est tout de même vérifié, si bien qu'un mauvais handle reste une erreur. Pour ajouter plutôt la lumière d'un sprite, ou multiplier par lui, voir <<spritesetblend,`spriteSetBlend`>>.
*Renvoie :* rien.
*Depuis :* 3.00
[#spritedraw]
==== spriteDraw
[source,text]
----
spriteDraw(id, x, y)
spriteDraw(id, x, y, centered)
spriteDraw(id, x, y, x2, y2)
spriteDraw(id, x, y, x2, y2, centered)
----
Copie le sprite sur l'overlay, avec sa rotation et son échelle actuelles. Appelez-la depuis `onOverlayUpdate`. Les deux premières formes dessinent à la taille que donnent `spriteGetWidth` et `spriteGetHeight`, avec `(x, y)` comme coin supérieur gauche ; les deux dernières étirent cette image dans le rectangle qui va de `(x, y)` à `(x2, y2)` inclus, toujours avec un échantillonnage au plus proche voisin, quoi que dise `spriteQuality`. Si le sprite est une animation en cours de lecture, le dessin la fait d'abord avancer du temps réel écoulé depuis le dessin précédent (ou depuis `spritePlay`), en passant autant d'images que ce temps en couvre, et montre chaque image au moins 10 ms, même quand le fichier en demande moins. Quand la dernière image est passée, la lecture revient à l'image `0` si `spriteLoop` est activé ; sinon elle reste sur la dernière image et s'arrête. Les parties qui sortent de l'overlay sont coupées.
*Paramètres :*
* `id` -- handle de sprite venu de `spriteLoad` ou de `fontToSprite`.
* `x`, `y` -- point d'ancrage en coordonnées de l'overlay ; les fractions sont tronquées.
* `x2`, `y2` -- coin opposé du rectangle cible, inclus, pour les formes étirées.
* `centered` -- booléen. Avec `true`, l'image (ou le rectangle étiré) est décalée pour que `(x, y)` soit son centre au lieu de son coin supérieur gauche.
*Remarques :* Une animation en cours de lecture qui n'est pas dessinée pendant un moment rattrape son retard au dessin suivant ; appelez `spritePause` si elle doit plutôt garder son image. Servez-vous de la forme centrée pour les sprites tournés, car la rotation change la boîte englobante et un ancrage au coin supérieur gauche semble dériver.
*Depuis :* 1.x. Formes centrée et étirée centrée ajoutées en 2.10.
*Voir aussi :* <<spriteload,spriteLoad>>, <<spriterotate,spriteRotate>>, <<spritegetwidth,spriteGetWidth>>, <<videodraw,videoDraw>>
.Exemple
[source,lua]
----
-- L'illustration de la borne à sa taille naturelle, l'image fixe d'attraction serrée dans sa boîte,
-- et un curseur ancré sur son centre.
function onOverlayUpdate()
overlayClear()
spriteDraw(cabinetSprite, CABINET_X, CABINET_Y)
spriteDraw(attractStill, THUMB_X, THUMB_Y, THUMB_X + THUMB_W - 1, THUMB_Y + THUMB_H - 1)
spriteDraw(cursorSprite, cursorX, cursorY, true)
return OVERLAY_UPDATED
end
----
[#spritedrawframe]
==== spriteDrawFrame
[source,text]
----
spriteDrawFrame(id, x, y, frame)
spriteDrawFrame(id, x, y, frame, scale)
spriteDrawFrame(id, x, y, frame, scaleX, scaleY)
----
Extension Hypseus. Copie une image d'une planche de sprites (ou de toute animation) sur l'overlay avec `(x, y)` comme coin supérieur gauche, sans changer l'image sur laquelle le sprite est arrêté ni son état de lecture. Les images sont numérotées à partir de `1`, comme Hypseus les compte ; une image hors de `1` au nombre d'images dessine l'image `1`. Une image fixe se dessine elle-même et ignore `frame`. Un miroir réglé par `spriteFlip` s'applique. La rotation et l'échelle propres au sprite, venues de `spriteRotate` et de `spriteScale`, sont ignorées : la première forme dessine l'image à sa taille naturelle, les autres multiplient sa largeur par `scaleX` (ou `scale`) et sa hauteur par `scaleY` (ou `scale`), avec un échantillonnage au plus proche voisin. Appelez-la depuis `onOverlayUpdate`.
*Paramètres :*
* `id` -- handle de sprite venu de `spriteLoadFrames` ou de `spriteLoad`.
* `x`, `y` -- coin supérieur gauche en coordonnées de l'overlay ; les fractions sont tronquées.
* `frame` -- numéro d'image, de `1` au nombre d'images ; les fractions sont tronquées.
* `scale`, `scaleX`, `scaleY` -- multiplicateurs de taille, `1` pour la taille naturelle.
*Remarques :* `spriteSetFrame` compte les images à partir de `0` ; `spriteDrawFrame` à partir de `1`.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spriteloadframes,spriteLoadFrames>>, <<spritedraw,spriteDraw>>, <<spritesetframe,spriteSetFrame>>, <<spriteframewidth,spriteFrameWidth>>
.Exemple
[source,lua]
----
-- Une bande de dix chiffres dessine le score, un chiffre par image.
digits = spriteLoadFrames(10, DIR .. "digits.png")
function drawScore(x, y, score)
local text = tostring(score)
for i = 1, #text do
spriteDrawFrame(digits, x + (i - 1) * spriteFrameWidth(digits), y, tonumber(text:sub(i, i)) + 1)
end
end
----
[#spritedrawgrid]
==== spriteDrawGrid
[source,text]
----
spriteDrawGrid(id, x, y, sourceX, sourceY, width, height)
spriteDrawGrid(id, x, y, sourceX, sourceY, width, height, x2, y2)
----
Extension Hypseus. Dessine un rectangle pris dans un sprite, avec son coin supérieur gauche en `x`, `y` : à sa propre taille ou, si `x2` et `y2` sont donnés, étiré pour remplir la boîte jusqu'à ce coin, comme <<spritedraw,`spriteDraw`>> étire un sprite entier. C'est le seul appel qui prend un rectangle source : `spriteDraw` prend le sprite entier et <<spritedrawframe,`spriteDrawFrame`>> une image entière d'une planche, alors que celui-ci prend n'importe quel rectangle. C'est ainsi qu'un jeu découpe une tuile, un chiffre ou une lettre dans une seule image sans la charger maintes fois.
La forme étirée est propre à Singe, et c'est sur elle que repose un jeu de course sur route. Chaque rangée de l'écran sous l'horizon est une tranche de route à une certaine distance : le jeu découpe donc une rangée dans une image de la route vue en coupe et l'étire à la largeur de la route à cette distance, centrée là où les virages l'ont menée. Une centaine d'appels par image dessinent toute la route, virages et collines compris, et rien n'est reconstruit. La même forme dessine un sprite en partie caché derrière le sommet d'une colline : découpez-en le haut et étirez-le à ce qui en reste visible. <<spritescale,`spriteScale`>> ne sait faire ni l'un ni l'autre, puisqu'il reconstruit toute l'image chaque fois que la taille change.
Le rectangle est en pixels du sprite et doit tenir à l'intérieur ; un rectangle qui n'y tient pas, ou dont la largeur ou la hauteur est nulle, met fin au script en donnant la taille du sprite, de même qu'un `x2` sans `y2`. Un étirement dont la boîte n'a pas de largeur ou pas de hauteur, parce que `x2` est à gauche de `x` ou `y2` au-dessus de `y`, ne dessine rien. L'étirement prend le pixel le plus proche au lieu de mélanger, ce qui garde les couleurs d'une rangée d'un pixel exactement telles qu'elles ont été dessinées. Le sprite d'où l'on dessine est le sprite tel qu'il est dessiné en ce moment, si bien qu'un <<spriterotate,`spriteRotate`>> ou un `spriteScale` change le sens des coordonnées.
Avec `LEGACY_SPRITE_ARGS`, le handle est en troisième position, `spriteDrawGrid(x, y, spr, sourceX, sourceY, width, height)`, là où Hypseus le place, et `x2`, `y2` suivent à la fin comme ici.
*Paramètres :*
* `id` -- nombre ; un handle de sprite.
* `x`, `y` -- nombres ; où arrive le coin supérieur gauche du rectangle, en coordonnées de l'overlay.
* `sourceX`, `sourceY` -- nombres ; le coin supérieur gauche du rectangle dans le sprite.
* `width`, `height` -- nombres ; sa taille, au moins 1.
* `x2`, `y2` -- nombres, facultatifs mais ensemble ; le coin inférieur droit, inclus, de la boîte dans laquelle l'étirer.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe ; la forme étirée est nouvelle en 3.00 et propre à Singe.
*Voir aussi :* <<spritedraw,spriteDraw>>, <<spritedrawframe,spriteDrawFrame>>
.Exemple
[source,lua]
----
-- Un nombre dessiné à partir d'une bande de dix chiffres, de 16 sur 24 chacun.
local function drawNumber(sheet, x, y, value)
local text = tostring(value)
for i = 1, #text do
local digit = tonumber(text:sub(i, i))
spriteDrawGrid(sheet, x + (i - 1) * 16, y, digit * 16, 0, 16, 24)
end
end
----
.Exemple
[source,lua]
----
-- Une route droite jusqu'à l'horizon, une rangée de l'écran à la fois. road.png fait 256 pixels de large et deux
-- rangées de haut : une coupe de route claire et une sombre, qui alternent le long de la route pour montrer
-- qu'elle défile. La rangée y de l'écran est le sol situé à EYE * FOCAL / (y - HORIZON) de distance.
local EYE = 60
local FOCAL = 160
local HORIZON = 110
local HALF = 100
local function drawRoad(road, traveled)
for y = HORIZON + 1, 239 do
local z = EYE * FOCAL / (y - HORIZON)
local half = HALF * FOCAL / z
local light = math.floor((z + traveled) / 60) % 2
spriteDrawGrid(road, 160 - half, y, 0, light, 256, 1, 160 + half, y)
end
end
----
[#spritedrawrotatedframe]
==== spriteDrawRotatedFrame
[source,text]
----
spriteDrawRotatedFrame(id, x, y)
spriteDrawRotatedFrame(id, x, y, scale)
spriteDrawRotatedFrame(id, x, y, scaleX, scaleY)
----
Extension Hypseus. Dessine l'image que <<spriterotateframe,`spriteRotateFrame`>> a tournée en dernier, *centrée* sur `x`, `y`. C'est le pendant de cet appel et la raison d'être des deux : <<spritedrawframe,`spriteDrawFrame`>> ignore délibérément la rotation et l'échelle d'un sprite, si bien qu'un jeu qui veut tourner une image d'une planche se sert plutôt de ces deux-là.
Si une échelle est donnée, l'image tournée est étirée d'autant au moment du dessin -- en plus de l'échelle avec laquelle elle a été tournée -- et le résultat reste centré sur `x`, `y`. `scaleX` seul met les deux axes à l'échelle.
Dessiner avant tout `spriteRotateFrame` met fin au script, puisqu'il n'y a aucune image tournée à dessiner.
Avec `LEGACY_SPRITE_ARGS`, le handle est en dernier, comme l'écrit Hypseus.
*Paramètres :*
* `id` -- nombre ; un handle de sprite.
* `x`, `y` -- nombres ; le *centre* de l'image dessinée, en coordonnées de l'overlay.
* `scale` ou `scaleX`, `scaleY` -- nombres facultatifs ; une échelle supplémentaire au moment du dessin.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spriterotateframe,spriteRotateFrame>>, <<spritedrawframe,spriteDrawFrame>>
.Exemple
[source,lua]
----
-- Une aiguille de boussole : une image de la planche, pointée là où va le joueur.
spriteRotateFrame(needle, heading, 1)
spriteDrawRotatedFrame(needle, 320, 240)
----
[#spriteflip]
==== spriteFlip
[source,text]
----
spriteFlip(id, horizontal, vertical)
----
Retourne le sprite en miroir de gauche à droite, de haut en bas, ou les deux, jusqu'à ce qu'il soit rappelé avec `false, false`. Le miroir s'applique à chaque dessin du sprite : `spriteDraw`, chaque image de `spriteDrawFrame` et l'image tournée de `spriteDrawRotatedFrame`. Il est appliqué après la rotation et l'échelle de `spriteRotate` et de `spriteScale`, si bien qu'un sprite tourné est retourné tel qu'il apparaît à l'écran, et qu'une image tournée est simplement retournée dans l'autre sens au lieu d'être tournée à nouveau. L'image retournée n'est reconstruite que lorsque les indicateurs changent ; une image dessinée par `spriteDrawFrame` est retournée une fois et gardée jusqu'à ce qu'une autre image soit demandée. `spriteDrawGrid` ignore le miroir, puisque son rectangle est découpé dans l'image non retournée.
Un personnage qui marche dans les deux sens n'a besoin que d'illustrations tournées d'un seul côté : retournez le sprite quand il fait demi-tour.
*Paramètres :*
* `id` -- handle de sprite.
* `horizontal` -- `true` retourne de gauche à droite.
* `vertical` -- `true` retourne de haut en bas.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<spriterotate,spriteRotate>>, <<spritedrawframe,spriteDrawFrame>>, <<spritedrawrotatedframe,spriteDrawRotatedFrame>>
.Exemple
[source,lua]
----
-- La planche du héros regarde vers la droite ; vers la gauche, ce sont les mêmes images, en miroir.
function onOverlayUpdate()
overlayClear()
spriteFlip(hero, heroVx < 0, false)
spriteDrawFrame(hero, heroX, heroY, walkFrame)
return OVERLAY_UPDATED
end
----
[#spriteframeheight]
==== spriteFrameHeight
[source,text]
----
height = spriteFrameHeight(id)
----
Extension Hypseus. Renvoie la hauteur d'une image du sprite avant toute mise à l'échelle ou rotation : la hauteur de la bande pour une planche de `spriteLoadFrames`, la hauteur d'une image pour une animation, et la hauteur de l'image pour une image fixe (Hypseus renvoie ici la surface dessinée). `spriteGetHeight` donne plutôt la taille telle qu'elle est dessinée.
*Renvoie :* entier.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spriteframewidth,spriteFrameWidth>>, <<spritegetheight,spriteGetHeight>>, <<spriteloadframes,spriteLoadFrames>>
.Exemple
[source,lua]
----
-- Centrer une image sur un point.
function drawCentered(sheet, frame, x, y)
spriteDrawFrame(sheet, x - spriteFrameWidth(sheet) / 2, y - spriteFrameHeight(sheet) / 2, frame)
end
----
[#spriteframewidth]
==== spriteFrameWidth
[source,text]
----
width = spriteFrameWidth(id)
----
Extension Hypseus. Renvoie la largeur d'une image du sprite avant toute mise à l'échelle ou rotation : la largeur de la bande divisée par le nombre d'images pour une planche de `spriteLoadFrames`, la largeur d'une image pour une animation, et la largeur de l'image pour une image fixe (Hypseus renvoie `0` dans ce cas). `spriteGetWidth` donne plutôt la taille telle qu'elle est dessinée.
*Renvoie :* entier.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spriteframeheight,spriteFrameHeight>>, <<spritegetwidth,spriteGetWidth>>, <<spriteloadframes,spriteLoadFrames>>
.Exemple
[source,lua]
----
-- Disposer côte à côte les images d'une bande.
function drawStrip(sheet, count, x, y)
for frame = 1, count do
spriteDrawFrame(sheet, x + (frame - 1) * spriteFrameWidth(sheet), y, frame)
end
end
----
[#spritegetframe]
==== spriteGetFrame
[source,text]
----
result = spriteGetFrame(id)
----
Renvoie l'indice, compté à partir de zéro, de l'image que le sprite montre en ce moment. Les images fixes et les animations qui n'ont pas avancé renvoient `0`. Les images n'avancent qu'à l'intérieur de `spriteDraw`, si bien que la valeur change d'un dessin à l'autre, et non entre deux appels quelconques. Servez-vous-en pour caler un son ou la logique du jeu sur une image particulière d'une animation.
*Renvoie :* indice d'image entier, `0` pour une image fixe.
*Depuis :* 2.10
*Voir aussi :* <<spritesetframe,spriteSetFrame>>, <<spriteisplaying,spriteIsPlaying>>, <<spritedraw,spriteDraw>>
.Exemple
[source,lua]
----
-- Déclencher le coup de feu sur l'image où apparaît l'éclair de bouche.
function onOverlayUpdate()
spriteDraw(muzzleFlash, gunX, gunY, true)
local frame = spriteGetFrame(muzzleFlash)
if frame == FLASH_FRAME and frame ~= lastFlashFrame then
soundPlay(gunshot)
end
lastFlashFrame = frame
return OVERLAY_UPDATED
end
----
[#spritegetframes]
==== spriteGetFrames
[source,text]
----
count = spriteGetFrames(id)
----
Extension Hypseus. Le nombre d'images d'un sprite : le nombre d'images d'une planche chargée par <<spriteloadframes,`spriteLoadFrames`>> ou d'un GIF ou WEBP animé, et `0` pour une image fixe. C'est le nombre jusqu'auquel va le numéro d'image, compté à partir de zéro, de <<spritesetframe,`spriteSetFrame`>>, et un de plus que le plus grand qu'il accepte.
*Paramètres :*
* `id` -- nombre ; un handle de sprite.
*Renvoie :* nombre ; `0` pour une image fixe.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spriteloadframes,spriteLoadFrames>>, <<spritegetframe,spriteGetFrame>>, <<spritesetframe,spriteSetFrame>>
.Exemple
[source,lua]
----
-- Passer à l'image suivante, quelle que soit la planche obtenue.
local frames = spriteGetFrames(walk)
if frames > 0 then
spriteSetFrame(walk, (spriteGetFrame(walk) + 1) % frames)
end
----
[#spritegetheight]
==== spriteGetHeight
[source,text]
----
result = spriteGetHeight(id)
----
Renvoie la hauteur du sprite tel qu'il serait dessiné à cet instant, en coordonnées de l'overlay. La mise à l'échelle et la rotation appliquées avec `spriteScale`, `spriteRotate` ou `spriteRotateAndScale` sont comprises, si bien qu'un sprite tourné renvoie la hauteur de sa boîte englobante tournée, et non celle de l'image source. Pour une animation, c'est l'image en cours qui est mesurée. Les formes étirées de `spriteDraw` n'influent pas sur la valeur.
*Renvoie :* hauteur entière.
*Depuis :* 2.00
*Voir aussi :* <<spritegetwidth,spriteGetWidth>>, <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>
.Exemple
[source,lua]
----
-- Empiler des lignes de description pré-rendues, espacées de la hauteur des sprites de texte.
function onOverlayUpdate()
local y = TEXT_Y
for _, line in ipairs(descriptionLines) do
spriteDraw(line, TEXT_X, y)
y = y + spriteGetHeight(line) + 1
end
return OVERLAY_UPDATED
end
----
[#spritegetwidth]
==== spriteGetWidth
[source,text]
----
result = spriteGetWidth(id)
----
Renvoie la largeur du sprite tel qu'il serait dessiné à cet instant, en coordonnées de l'overlay, y compris toute mise à l'échelle et toute rotation appliquées en ce moment. Pour une animation, c'est l'image en cours qui est mesurée. Avec `spriteGetHeight`, c'est la façon habituelle de centrer un sprite dans une boîte ou de mesurer un texte rendu par `fontToSprite`.
*Renvoie :* largeur entière.
*Depuis :* 2.00
*Voir aussi :* <<spritegetheight,spriteGetHeight>>, <<spritescale,spriteScale>>, <<fonttosprite,fontToSprite>>
.Exemple
[source,lua]
----
-- Centrer l'image de la borne dans son panneau, et le titre sur la largeur de l'overlay.
function onOverlayUpdate()
local x = CABINET_X + (CABINET_W - spriteGetWidth(cabinetSprite)) * 0.5
local y = CABINET_Y + (CABINET_H - spriteGetHeight(cabinetSprite)) * 0.5
spriteDraw(cabinetSprite, x, y)
spriteDraw(titleSprite, (overlayGetWidth() - spriteGetWidth(titleSprite)) / 2, 25)
return OVERLAY_UPDATED
end
----
[#spriteisplaying]
==== spriteIsPlaying
[source,text]
----
result = spriteIsPlaying(id)
----
Indique si l'animation du sprite avance en ce moment. Un sprite tout juste chargé renvoie `false` ; `spritePlay` la met en marche, et `spritePause` ou la fin d'une animation sans boucle l'arrête. Interrogez-la pour savoir quand une animation jouée une seule fois est terminée.
*Renvoie :* booléen, `true` tant que l'animation joue.
*Remarques :* `spritePlay` et `spritePause` ne vérifient pas la présence de données d'animation, si bien qu'une image fixe renvoie ce que ces appels ont réglé en dernier.
*Depuis :* 2.10
*Voir aussi :* <<spriteplay,spritePlay>>, <<spritepause,spritePause>>, <<spriteloop,spriteLoop>>
.Exemple
[source,lua]
----
-- Retenir le menu jusqu'à ce que l'animation du logo, jouée une seule fois, soit terminée.
function onOverlayUpdate()
overlayClear()
spriteDraw(logoReveal, overlayGetWidth() / 2, overlayGetHeight() / 2, true)
if not spriteIsPlaying(logoReveal) then
drawMenu()
end
return OVERLAY_UPDATED
end
----
[#spriteload]
==== spriteLoad
[source,text]
----
result = spriteLoad(filename)
result = spriteLoad(filename, width, height)
----
Charge une image et renvoie un handle entier pour les autres appels `sprite*`. Le nom passe par le système de fichiers virtuel du moteur, si bien que le fichier peut se trouver tel quel sur le disque ou dans un jeu empaqueté ; il est résolu par rapport au dossier dans lequel Singe a été lancé : ajoutez donc `DIR` devant pour les fichiers livrés avec votre jeu. Tous les formats de <<formats,Formats vidéo, audio et de conteneur>> sont lus. Un GIF ou un WEBP animé de deux images ou plus se charge comme une animation, arrêtée sur l'image `0`, sans lecture ni boucle ; tout autre fichier, y compris un GIF d'une seule image, se charge comme une image fixe. Une image de plus de 8 bits par canal, ou à pixels en virgule flottante, est convertie en RGBA 8 bits au chargement et traitée comme telle ensuite. Les pixels dont la valeur brute est `0` dans le format de pixel propre au fichier deviennent transparents. Le chargement est synchrone, et un fichier qui ne peut être ni ouvert ni décodé met fin au script avec le message d'erreur du chargeur au lieu de renvoyer `nil`.
La taille facultative sert à une image vectorielle -- un SVG -- qui n'a pas de taille propre digne de ce nom : l'image est rastérisée pour tenir dans cette boîte, en gardant ses propres proportions, au lieu d'être rastérisée à la taille du fichier puis agrandie. Demandez à <<spritegetwidth,`spriteGetWidth`>> et à <<spritegetheight,`spriteGetHeight`>> ce qu'elle a donné, car faire tenir un dessin carré dans une boîte large laisse de la largeur inutilisée. Tous les autres formats ignorent la taille.
*Paramètres :*
* `filename` -- chaîne ; l'image à charger.
* `width` -- nombre facultatif ; la largeur dans laquelle faire tenir un SVG, `1` ou plus.
* `height` -- nombre facultatif ; la hauteur dans laquelle faire tenir un SVG, `1` ou plus. Les deux doivent être donnés ensemble.
*Renvoie :* handle de sprite entier.
*Remarques :* Les animations ne démarrent pas d'elles-mêmes. Appelez `spriteLoop(id, true)` et `spritePlay(id)` après le chargement si l'animation doit jouer et se répéter.
*Depuis :* 1.x. Prise en charge des GIF/WEBP animés ajoutée en 2.10 ; la taille vectorielle en 3.00.
*Voir aussi :* <<spriteunload,spriteUnload>>, <<spriteplay,spritePlay>>, <<spriteloop,spriteLoop>>, <<fonttosprite,fontToSprite>>
.Exemple
[source,lua]
----
-- Charger les illustrations du menu une fois au démarrage ; la boucle d'attraction est un WEBP animé.
cabinetSprite = spriteLoad(DIR .. "cabinet.png")
marqueeSprite = spriteLoad(DIR .. "marquee.png")
attractLoop = spriteLoad(DIR .. "attract.webp")
spriteQuality(attractLoop, RENDER_SMOOTH)
spriteLoop(attractLoop, true)
spritePlay(attractLoop)
----
[#spriteloaddata]
==== spriteLoadData
[source,text]
----
id = spriteLoadData(data)
----
Extension Hypseus. Construit un sprite à partir d'une image contenue dans une chaîne Lua au lieu d'un fichier. Tous les formats que lit <<spriteload,`spriteLoad`>> sont lus ici, GIF et WEBP animés compris, et une animation chargée ainsi se comporte exactement comme une animation chargée depuis un fichier.
La chaîne peut contenir des octets nuls ; c'est sa longueur, et non son premier zéro, qui décide de ce qui est lu. L'image est décodée pendant l'appel et la chaîne n'est pas conservée. Des octets qui ne forment pas une image, ou une chaîne vide, mettent fin au script, comme le fait un fichier illisible dans `spriteLoad`.
Servez-vous-en pour des illustrations qu'un jeu génère, décompresse ou déchiffre lui-même. Les illustrations que le système de fichiers virtuel peut atteindre par leur nom -- y compris celles d'un `.game` empaqueté -- se chargent mieux avec `spriteLoad`, qui ne garde pas tout le fichier dans une chaîne Lua.
*Paramètres :*
* `data` -- chaîne d'octets.
*Renvoie :* nombre, le handle du sprite.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spriteload,spriteLoad>>, <<soundloaddata,soundLoadData>>
.Exemple
[source,lua]
----
-- Une image que le jeu garde dans son propre fichier de données.
local bytes = myArchive:extract("logo.png")
local logo = spriteLoadData(bytes)
spriteDraw(logo, 10, 10)
----
[#spriteloadframes]
==== spriteLoadFrames
[source,text]
----
result = spriteLoadFrames(count, filename)
----
Extension Hypseus. Charge un fichier image qui contient `count` images de même taille sur une seule rangée et renvoie un handle de sprite. La largeur d'une image est la largeur du fichier divisée par `count` (division entière ; les colonnes restantes à droite sont abandonnées) et sa hauteur est celle du fichier. La planche devient une animation ordinaire arrêtée sur l'image `0`, sans lecture ni boucle, si bien que `spriteDraw` montre l'image en cours plutôt que toute la bande (comme Hypseus la dessine), et que `spriteDrawFrame` choisit directement n'importe quelle image. Le nom passe par le système de fichiers virtuel, comme pour `spriteLoad`, et les pixels de valeur brute `0` deviennent transparents dans chaque image. Un nombre hors de `2` à `512`, un fichier image de moins de `count` pixels de large ou un fichier illisible met fin au script avec une erreur.
*Paramètres :*
* `count` -- nombre d'images sur la largeur du fichier, de `2` à `512`.
* `filename` -- le fichier image de la bande.
*Renvoie :* handle de sprite entier.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spritedrawframe,spriteDrawFrame>>, <<spriteframewidth,spriteFrameWidth>>, <<spriteload,spriteLoad>>, <<spriteunload,spriteUnload>>
.Exemple
[source,lua]
----
-- Une bande de cinq images de voiture ; l'image 3 est la voiture qui pointe droit devant.
carSprite = spriteLoadFrames(5, DIR .. "car.png")
carWidth = spriteFrameWidth(carSprite)
function onOverlayUpdate()
overlayClear()
spriteDrawFrame(carSprite, (overlayGetWidth() - carWidth) / 2, CAR_Y, carFrame)
return OVERLAY_UPDATED
end
----
[#spriteloop]
==== spriteLoop
[source,text]
----
spriteLoop(id, loop)
----
Indique si l'animation reprend à l'image `0` après sa dernière image ou s'y arrête. Les nouveaux sprites ne bouclent pas. Le réglage est consulté chaque fois que `spriteDraw` dépasse la dernière image, si bien qu'on peut le changer pendant que l'animation joue. Le régler sur un sprite déjà arrêté ne relance pas la lecture ; appelez `spritePlay` pour cela. L'argument doit être un booléen Lua.
*Paramètres :*
* `id` -- handle de sprite.
* `loop` -- booléen. `true` revient à l'image `0` ; `false` (la valeur par défaut) s'arrête sur la dernière image, et `spriteIsPlaying` se met à renvoyer `false`.
*Depuis :* 2.10
*Voir aussi :* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>, <<spritesetframe,spriteSetFrame>>
.Exemple
[source,lua]
----
-- La torche vacille sans fin ; l'explosion joue une fois par coup.
torchFlame = spriteLoad(DIR .. "torch.gif")
explosion = spriteLoad(DIR .. "explosion.gif")
spriteLoop(torchFlame, true)
spriteLoop(explosion, false)
spritePlay(torchFlame)
----
[#spritenew]
==== spriteNew
[source,text]
----
id = spriteNew(width, height)
----
Crée un sprite vierge, chaque pixel transparent, que <<spritesetpixels,`spriteSetPixels`>> remplira : une image que le script fabrique lui-même au lieu de la charger. Chaque côté fait de 1 à 4096 pixels ; toute autre valeur met fin au script. Le sprite est une image fixe et fait tout ce que fait n'importe quelle autre image fixe : il se dessine, tourne, change d'échelle, s'étire et se teinte, et il peut servir de texture à un matériau.
*Paramètres :*
* `width`, `height` -- nombres ; sa taille en pixels.
*Renvoie :* nombre, le handle du sprite.
*Depuis :* 3.00
*Voir aussi :* <<spritesetpixels,spriteSetPixels>>, <<spriteloaddata,spriteLoadData>>
.Exemple
[source,lua]
----
local minimap = spriteNew(64, 64)
----
[#spritepause]
==== spritePause
[source,text]
----
spritePause(id)
----
Arrête l'animation sur son image en cours. L'image continue d'être dessinée par `spriteDraw`, et le temps passé en pause n'est pas compté quand `spritePlay` reprend, si bien que l'image ne saute pas en avant. Mettre en pause un sprite qui ne joue pas ne change rien.
*Depuis :* 2.10
*Voir aussi :* <<spriteplay,spritePlay>>, <<spriteisplaying,spriteIsPlaying>>
.Exemple
[source,lua]
----
-- Figer l'ennemi en pleine foulée pendant que joue l'éclair de l'impact.
function onOverlayUpdate()
if playerHit and spriteIsPlaying(enemyWalk) then
spritePause(enemyWalk)
end
spriteDraw(enemyWalk, enemyX, enemyY, true)
return OVERLAY_UPDATED
end
----
[#spriteplay]
==== spritePlay
[source,text]
----
spritePlay(id)
----
Démarre ou reprend l'animation à partir de son image en cours, en comptant le premier pas à partir du moment de l'appel. Un sprite qui joue déjà est laissé tel quel, si bien que le minuteur d'image n'est pas remis à zéro. Un nouveau sprite doit être démarré avec cet appel ; le chargement seul ne l'anime pas.
*Remarques :* Une animation sans boucle qui a atteint sa dernière image reste sur cette image quand on la rejoue. Pour la rejouer, appelez d'abord `spriteSetFrame(id, 0)`.
*Depuis :* 2.10
*Voir aussi :* <<spritepause,spritePause>>, <<spritesetframe,spriteSetFrame>>, <<spriteloop,spriteLoop>>
.Exemple
[source,lua]
----
-- Relancer l'explosion, jouée une seule fois, au point d'impact.
function onCollision(a, b, x, y, z, speed)
if speed > IMPACT_SPEED then
blastX = x
blastY = y
spriteSetFrame(explosion, 0)
spritePlay(explosion)
soundPlay(boom)
end
end
----
[#spritequality]
==== spriteQuality
[source,text]
----
spriteQuality(id, mode)
----
Choisit l'échantillonnage employé quand le sprite est reconstruit pour une rotation ou une mise à l'échelle. `RENDER_PIXELATED` garde des bords de pixels nets, `RENDER_SMOOTH` interpole. Les nouveaux sprites sont pixelisés. Le réglage ne compte que tant qu'une rotation ou une échelle autre que `1.0` est en vigueur ; un sprite non transformé est dessiné directement depuis son image source, et les formes étirées de `spriteDraw` emploient toujours l'échantillonnage au plus proche voisin. Changer de mode reconstruit aussitôt l'image transformée : réglez-le donc une fois après le chargement plutôt qu'à chaque image.
*Paramètres :*
* `id` -- handle de sprite.
* `mode` -- `RENDER_PIXELATED` (`0`) ou `RENDER_SMOOTH` (`1`). Tout nombre non nul choisit le lissage.
*Depuis :* 2.10
*Voir aussi :* <<spritescale,spriteScale>>, <<spriterotate,spriteRotate>>, <<spriterotateandscale,spriteRotateAndScale>>
.Exemple
[source,lua]
----
-- Les illustrations photographiques du fronton doivent être interpolées ; le pixel art doit rester net.
marqueeSprite = spriteLoad(DIR .. "marquee.jpg")
playerSprite = spriteLoad(DIR .. "player.png")
spriteQuality(marqueeSprite, RENDER_SMOOTH)
spriteQuality(playerSprite, RENDER_PIXELATED)
spriteScale(marqueeSprite, 0.5)
spriteScale(playerSprite, 3)
----
[#spriteresetcolorkey]
==== spriteResetColorKey
[source,text]
----
spriteResetColorKey(id, enabled)
----
Extension Hypseus. Acceptée pour la compatibilité, elle ne fait rien. Dans Hypseus, l'indicateur réapplique la couleur transparente aux copies que produisent `spriteRotate` et `spriteScale`, qui dessineraient sinon leur fond opaque ; les copies transformées de Singe portent la transparence dans leur canal alpha, si bien qu'il n'y a rien à réappliquer. Un mauvais handle ou un argument non booléen met fin au script, comme pour tout autre appel de sprite.
*Paramètres :*
* `id` -- handle de sprite.
* `enabled` -- booléen, ignoré.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spriteload,spriteLoad>>
.Exemple
[source,lua]
----
-- Code d'un jeu Hypseus ; sans effet dans Singe.
badge = spriteLoad(DIR .. "badge.png")
spriteResetColorKey(badge, true)
----
[#spriterotate]
==== spriteRotate
[source,text]
----
spriteRotate(id, degrees)
----
Fait tourner le sprite dans le sens des aiguilles d'une montre autour de son centre. L'angle est ramené modulo 360 et peut être fractionnaire ou négatif. L'échelle actuelle est conservée. L'image tournée n'est reconstruite que lorsque l'angle change vraiment : répéter le même angle à chaque image ne coûte rien, alors qu'un nouvel angle à chaque image reconstruit à chaque image. La rotation agrandit la boîte englobante, si bien que `spriteGetWidth` et `spriteGetHeight` changent et que la forme centrée de `spriteDraw` est celle à employer.
*Depuis :* 2.10
*Voir aussi :* <<spriterotateandscale,spriteRotateAndScale>>, <<spritedraw,spriteDraw>>, <<spritequality,spriteQuality>>
.Exemple
[source,lua]
----
-- Des caisses d'une scène physique 2D, dessinées à l'angle de leurs corps.
function onOverlayUpdate()
overlayClear()
for _, crate in ipairs(crates) do
local x, y = nodeGetPosition(crate)
local _, _, angle = nodeGetRotation(crate)
spriteRotate(crateSprite, angle)
spriteDraw(crateSprite, x, y, true)
end
return OVERLAY_UPDATED
end
----
[#spriterotateandscale]
==== spriteRotateAndScale
[source,text]
----
spriteRotateAndScale(id, degrees, scale)
spriteRotateAndScale(id, degrees, scaleX, scaleY)
----
Règle ensemble la rotation et l'échelle. Le résultat est le même que `spriteRotate` suivi de `spriteScale`, mais l'image transformée est reconstruite une fois au lieu de deux, et pas du tout quand rien n'a changé. Les facteurs d'échelle devraient être supérieurs à `0` ; un facteur négatif est pris sans son signe, et un facteur qui ne laisserait aucun pixel, zéro compris, en laisse un à la place.
*Paramètres :*
* `id` -- handle de sprite.
* `degrees` -- rotation dans le sens des aiguilles d'une montre, ramenée modulo 360.
* `scale` -- facteur uniforme, `1.0` pour la taille naturelle.
* `scaleX`, `scaleY` -- facteurs horizontal et vertical séparés.
*Depuis :* 2.10
*Voir aussi :* <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>, <<spritequality,spriteQuality>>
.Exemple
[source,lua]
----
-- Une pièce qui tourne et rétrécit en volant vers le compteur de score.
function onOverlayUpdate()
if coinFlying then
coinT = math.min(coinT + 0.02, 1)
spriteRotateAndScale(coinSprite, coinT * 720, 1 - coinT * 0.75)
spriteDraw(coinSprite, coinX + (SCORE_X - coinX) * coinT, coinY + (SCORE_Y - coinY) * coinT, true)
coinFlying = coinT < 1
end
return OVERLAY_UPDATED
end
----
[#spriterotateframe]
==== spriteRotateFrame
[source,text]
----
spriteRotateFrame(id, degrees, frame)
----
Extension Hypseus. Prend une image dans une planche de sprites, la fait tourner de `degrees` dans le sens des aiguilles d'une montre, la met à l'échelle du <<spritescale,`spriteScale`>> propre au sprite, et garde le résultat pour <<spritedrawrotatedframe,`spriteDrawRotatedFrame`>>. La surface dessinée propre au sprite n'est pas touchée, si bien que <<spritedraw,`spriteDraw`>> et `spriteDrawFrame` continuent de montrer ce qu'ils montraient.
Les images sont numérotées à partir de 1, comme les numérote `spriteDrawFrame` (et contrairement à `spriteGetFrame` et à `spriteSetFrame`, qui comptent à partir de 0) ; une image hors de la planche devient l'image 1 au lieu d'une erreur. Avec une image fixe, c'est toute l'image qui tourne.
Tourner n'est pas gratuit : l'image est redessinée pixel par pixel à chaque fois, alors tournez-la quand l'angle change plutôt qu'à chaque image. L'angle est pris modulo 360, et <<spritequality,`spriteQuality`>> décide si les bords tournés sont lissés.
Avec `LEGACY_SPRITE_ARGS`, le handle est en dernier, `spriteRotateFrame(degrees, frame, spr)`, comme l'écrit Hypseus.
*Paramètres :*
* `id` -- nombre ; un handle de sprite.
* `degrees` -- nombre ; dans le sens des aiguilles d'une montre.
* `frame` -- nombre ; de 1 au nombre d'images.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<spritedrawrotatedframe,spriteDrawRotatedFrame>>, <<spriterotate,spriteRotate>>, <<spritescale,spriteScale>>
.Exemple
[source,lua]
----
-- Un sprite de vaisseau dont la coque est l'image 1 et la poussée l'image 2.
spriteRotateFrame(ship, angle, thrusting and 2 or 1)
spriteDrawRotatedFrame(ship, shipX, shipY)
----
[#spritescale]
==== spriteScale
[source,text]
----
spriteScale(id, scale)
spriteScale(id, scaleX, scaleY)
----
Met le sprite à l'échelle autour de son centre, uniformément ou par axe, en gardant la rotation actuelle. Les facteurs doivent être supérieurs à `0` ; `1.0` est la taille naturelle, et un facteur d'exactement `1.0` sur les deux axes sans rotation dessine directement l'image source. L'image mise à l'échelle n'est reconstruite que lorsqu'un facteur change, avec l'échantillonnage choisi par `spriteQuality`. Préférez la forme étirée de `spriteDraw` quand la taille cible change à chaque image, puisqu'elle ne reconstruit rien.
*Paramètres :*
* `id` -- handle de sprite.
* `scale` -- facteur uniforme.
* `scaleX`, `scaleY` -- facteurs horizontal et vertical séparés.
*Depuis :* 2.10
*Voir aussi :* <<spriterotateandscale,spriteRotateAndScale>>, <<spritequality,spriteQuality>>, <<spritedraw,spriteDraw>>
.Exemple
[source,lua]
----
-- Agrandir l'entrée de menu en surbrillance ; la reconstruction n'a lieu que lorsque la sélection bouge.
function onOverlayUpdate()
for i, label in ipairs(menuLabels) do
if i == selected then
spriteScale(label, 1.25)
else
spriteScale(label, 1)
end
spriteDraw(label, MENU_X, MENU_Y + i * LINE_HEIGHT, true)
end
return OVERLAY_UPDATED
end
----
[#spritesetalpha]
==== spriteSetAlpha
[source,text]
----
spriteSetAlpha(id, alpha)
----
Multiplie l'alpha de chaque pixel par `alpha / 255` à chaque dessin du sprite, sous toutes ses formes : `spriteDraw`, `spriteDrawFrame`, `spriteDrawGrid` et `spriteDrawRotatedFrame`, tourné ou non. `255`, la valeur par défaut, le dessine tel quel et `0` pas du tout, si bien qu'un logo apparaît et disparaît en fondu sans seconde image. Les valeurs hors limites sont ramenées dans la plage.
*Paramètres :*
* `id` -- nombre ; un handle de sprite.
* `alpha` -- nombre ; de `0` à `255`.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<spritesetblend,spriteSetBlend>>, <<spritesettint,spriteSetTint>>
.Exemple
[source,lua]
----
-- Le titre apparaît en fondu en deux secondes.
spriteSetAlpha(title, math.floor(255 * math.min(1, elapsed / 2)))
spriteDraw(title, 120, 80)
----
[#spritesetblend]
==== spriteSetBlend
[source,text]
----
spriteSetBlend(id, SPRITE_ALPHA | SPRITE_ADD | SPRITE_MULTIPLY)
----
La façon dont chaque dessin du sprite le pose sur ce qui est déjà sur l'overlay. `SPRITE_ALPHA`, la valeur par défaut, le dessine par-dessus selon son alpha. `SPRITE_ADD` ajoute sa couleur, pondérée par son alpha, à ce qui s'y trouve, si bien qu'il ne peut qu'éclaircir : halos, lumière, étincelles, et les shadebobs des vieilles démos, où une boule dessinée sur elle-même encore et encore finit par brûler jusqu'au blanc. `SPRITE_MULTIPLY` assombrit ce qui s'y trouve par sa couleur, pour les ombres et le verre teinté.
Les pixels ajoutés et multipliés gardent l'alpha propre de l'overlay, si bien qu'ils ne se voient que là où quelque chose est déjà dessiné sur l'overlay : sur un overlay transparent, au-dessus d'une vidéo ou de la scène 3D, ils ne changent rien. Dessinez d'abord quelque chose d'opaque, ou servez-vous de `emitterSetBlend` avec `PARTICLE_ADD`, qui ajoute par-dessus tout. Toute autre valeur que ces trois constantes met fin au script.
*Paramètres :*
* `id` -- nombre ; un handle de sprite.
* `blend` -- `SPRITE_ALPHA`, `SPRITE_ADD` ou `SPRITE_MULTIPLY`.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<spritesetalpha,spriteSetAlpha>>, <<spritesettint,spriteSetTint>>, <<emittersetblend,emitterSetBlend>>
.Exemple
[source,lua]
----
-- Shadebobs : une boule blanche douce ajoutée le long d'un trajet, jamais effacée, si bien que la traînée monte jusqu'au blanc.
spriteSetBlend(ball, SPRITE_ADD)
spriteSetAlpha(ball, 40)
for i = 0, 15 do
local a = t * 2 + i * 0.4
spriteDraw(ball, 320 + math.cos(a) * 200, 240 + math.sin(a * 1.3) * 150, true)
end
----
[#spritesetframe]
==== spriteSetFrame
[source,text]
----
spriteSetFrame(id, frame)
----
Fait sauter une animation à l'image donnée et relance le minuteur de délai de cette image, si bien que l'image est montrée pendant toute sa durée avant la suivante. L'état de lecture ne change pas : un sprite en pause reste en pause sur la nouvelle image et un sprite qui joue continue à partir d'elle. L'appel est ignoré pour les images fixes, pour les images hors de `0` à la dernière image, et quand le sprite montre déjà cette image.
*Remarques :* Les images sont numérotées ici à partir de `0` ; `spriteDrawFrame`, l'extension Hypseus, les numérote à partir de `1`.
*Depuis :* 2.10
*Voir aussi :* <<spritegetframe,spriteGetFrame>>, <<spriteplay,spritePlay>>, <<spritedrawframe,spriteDrawFrame>>
.Exemple
[source,lua]
----
-- Montrer la porte dans l'état que lui donne la logique du jeu.
function onOverlayUpdate()
if doorOpen then
spriteSetFrame(doorSprite, DOOR_OPEN_FRAME)
else
spriteSetFrame(doorSprite, DOOR_CLOSED_FRAME)
end
spriteDraw(doorSprite, DOOR_X, DOOR_Y)
return OVERLAY_UPDATED
end
----
[#spritesetpixels]
==== spriteSetPixels
[source,text]
----
spriteSetPixels(id, pixels)
spriteSetPixels(id, pixels, palette)
----
Remplace chaque pixel d'un sprite fixe à partir d'une chaîne, une rangée à la fois depuis le haut. Sans palette, la chaîne compte quatre octets par pixel : rouge, vert, bleu et alpha. Avec une palette, c'est un octet par pixel, chacun le numéro d'une couleur de la palette, qui compte 256 couleurs de trois octets (rouge, vert et bleu, toutes opaques) ou de quatre (avec l'alpha). Une chaîne de mauvaise longueur, une palette de mauvaise longueur ou un sprite animé met fin au script.
C'est ainsi qu'un script dessine une image pixel par pixel, assez vite pour le faire à chaque image : les plasmas, tunnels, feux et eaux des vieilles démos, une mini-carte, ou une carte de hauteurs en cours d'édition. Construire la chaîne est le travail du script : gardez donc l'image petite et étirez-la au moment de la dessiner. Une boucle Lua remplit une image de 320 sur 240 en quelques millisecondes sur un ordinateur de bureau. Le mode palette est la vieille façon d'animer à peu de frais : remettez les mêmes pixels avec la palette décalée d'une couleur, et tout change de couleur sans que rien soit recalculé. Un sprite tourné ou mis à l'échelle est de nouveau tourné et mis à l'échelle avec ses nouveaux pixels ; un matériau qui montre le sprite ne voit le changement que lorsque <<materialsettexture,`materialSetTexture`>> est rappelé avec le sprite, ce qui l'envoie de nouveau au GPU.
Une image chargée peut aussi recevoir de nouveaux pixels ; elle a la taille qu'elle avait au chargement.
*Paramètres :*
* `id` -- nombre ; un sprite fixe.
* `pixels` -- chaîne ; `width * height * 4` octets, ou `width * height` avec une palette.
* `palette` -- chaîne facultative ; 768 ou 1024 octets.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<spritenew,spriteNew>>, <<materialsettexture,materialSetTexture>>
.Exemple
[source,lua]
----
-- Feu : chaque pixel est la moyenne de ceux du dessous et refroidit en montant, à travers une palette
-- qui va du noir au blanc en passant par le rouge et le jaune. Étiré sur le bas de l'écran à chaque image.
local W = 160
local H = 60
local fire = spriteNew(W, H)
local heat = {}
local color = {}
for i = 0, 255 do
color[#color + 1] = string.char(math.min(255, i * 3), math.max(0, math.min(255, i * 3 - 255)), math.max(0, i * 3 - 510))
end
local palette = table.concat(color)
for i = 0, W * (H + 1) - 1 do
heat[i] = 0
end
function onOverlayUpdate()
for x = 0, W - 1 do
heat[H * W + x] = math.random(0, 255)
end
local rows = {}
for y = 0, H - 1 do
local row = {}
for x = 0, W - 1 do
local below = (y + 1) * W
local v = (heat[below + (x - 1) % W] + heat[below + x] + heat[below + (x + 1) % W] + heat[y * W + x]) // 4
heat[y * W + x] = math.max(0, v - 1)
row[x + 1] = heat[y * W + x]
end
rows[y + 1] = string.char(table.unpack(row))
end
spriteSetPixels(fire, table.concat(rows), palette)
overlayClear()
spriteDraw(fire, 0, 240, 639, 479)
return OVERLAY_UPDATED
end
----
[#spritesettint]
==== spriteSetTint
[source,text]
----
spriteSetTint(id, r, g, b)
----
Multiplie la couleur de chaque pixel par `r`, `g`, `b` / 255 à chaque dessin du sprite, sous toutes ses formes. Un sprite blanc se dessine dans n'importe quelle couleur, et tout sprite peut être assombri ou tiré vers une couleur, sans seconde image : une seule boule blanche devient tout un arc-en-ciel de boules, et un sprite blanc d'un pixel étiré avec `spriteDraw` devient un rectangle plein de n'importe quelle couleur. `255, 255, 255`, la valeur par défaut, le dessine tel quel. Les valeurs hors limites sont ramenées dans la plage.
*Paramètres :*
* `id` -- nombre ; un handle de sprite.
* `r`, `g`, `b` -- nombres ; de `0` à `255`.
*Renvoie :* rien.
*Depuis :* 3.00
*Voir aussi :* <<spritesetalpha,spriteSetAlpha>>, <<spritesetblend,spriteSetBlend>>
.Exemple
[source,lua]
----
-- Un anneau de boules, chacune de sa propre couleur, à partir d'une seule image blanche.
for i = 1, 12 do
local hue = i / 12
spriteSetTint(ball, 128 + 127 * math.sin(hue * 6.28), 128 + 127 * math.sin(hue * 6.28 + 2.1), 128 + 127 * math.sin(hue * 6.28 + 4.2))
spriteDraw(ball, 320 + math.cos(hue * 6.28) * 150, 240 + math.sin(hue * 6.28) * 150, true)
end
spriteSetTint(ball, 255, 255, 255)
----
[#spriteunload]
==== spriteUnload
[source,text]
----
spriteUnload(id)
----
Libère le sprite : l'image source, la copie transformée s'il y en a une, et toutes les images de l'animation. Le handle n'est plus valide ensuite et tout nouvel usage met fin au script avec une erreur. Les sprites créés par `fontToSprite` se libèrent de la même façon. Les sprites encore chargés à la fin du jeu sont libérés par le moteur, mais les jeux qui créent des sprites en cours de partie (du texte rendu à nouveau quand les scores changent, par exemple) doivent décharger les anciens, sans quoi la mémoire croît sans limite.
*Depuis :* 2.00
*Voir aussi :* <<spriteload,spriteLoad>>, <<fonttosprite,fontToSprite>>, <<onshutdown,onShutdown>>
.Exemple
[source,lua]
----
-- Refaire le rendu du score seulement quand il change, en libérant le sprite précédent.
function updateScoreSprite()
if scoreSprite ~= nil then
spriteUnload(scoreSprite)
end
scoreSprite = fontToSprite("SCORE " .. score)
end
function onShutdown()
spriteUnload(scoreSprite)
spriteUnload(cabinetSprite)
end
----
[#subtitleref]
=== Sous-titre
Des sous-titres SubRip par-dessus l'image du disque. Un jeu charge un `.srt` avec <<srtload,`srtLoad`>>, l'active avec <<srtenable,`srtEnable`>>, et le moteur affiche chaque sous-titre puis le retire à mesure que le disque l'atteint. Voir <<subtitles,Sous-titres>> pour le format du fichier, la synchronisation et la façon de changer leur mise en forme.
Les sous-titres et <<overlaybanner,`overlayBanner`>> partagent un même document livré, `Singe/subtitle.rml`, mis en forme par `Singe/subtitle.rcss` ; un jeu qui veut un autre aspect fournit son propre `subtitle.rml` à côté de son script, et Singe utilise celui-ci à la place (voir <<subtitles,Sous-titres>>). Tous deux sont dessinés par-dessus l'image, au-dessus de l'overlay et sous les GUI en espace écran, et ils ne reçoivent pas la souris.
[#srtclear]
==== srtClear
[source,text]
----
srtClear()
----
Extension Hypseus. Retire aussitôt ce qui est à l'écran -- un sous-titre ou un bandeau -- et laisse tels quels les sous-titres chargés et l'état d'activation. Le sous-titre qui était affiché reste masqué jusqu'à ce que le disque atteigne le suivant ; il n'y a rien à recharger ni rien à réactiver.
*Paramètres :* aucun.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<srtenable,srtEnable>>, <<overlaybanner,overlayBanner>>
.Exemple
[source,lua]
----
-- Une cinématique se termine plus tôt ; retirer la ligne encore affichée.
srtClear()
----
[#srtenable]
==== srtEnable
[source,text]
----
srtEnable(enabled)
----
Extension Hypseus. Indique si les sous-titres chargés s'affichent pendant la lecture du disque. Désactiver l'affichage retire avec lui le sous-titre affiché ; le réactiver affiche le sous-titre qui couvre l'image où se trouve le disque. Un script commence avec les sous-titres désactivés : un jeu qui les charge doit donc les activer.
Un argument non booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<srtload,srtLoad>>, <<srtclear,srtClear>>
.Exemple
[source,lua]
----
srtEnable(SETTINGS.subtitles)
----
[#srtload]
==== srtLoad
[source,text]
----
loaded = srtLoad(filename)
----
Extension Hypseus. Lit un fichier SubRip par le système de fichiers virtuel et convertit ses horodatages en images du disque, à la fréquence d'images propre au disque. Le chargement remplace ce qui était chargé avant ; <<srtenable,`srtEnable`>> l'affiche ensuite.
Il répond `false`, sans rien avoir chargé, quand le fichier est illisible, quand il ne contient aucun sous-titre analysable, ou quand il n'y a pas de disque sur lequel le caler -- un fichier mal formé donne donc un `false`, pas un plantage. Un avertissement qui nomme le fichier est écrit dans chacun de ces cas. Les sous-titres non analysables dans un fichier par ailleurs correct sont sautés, comme Hypseus les saute.
Comme les horodatages deviennent des numéros d'image au chargement du fichier, et que c'est le numéro d'image que le moteur compare, un sous-titre est retrouvé où que le disque soit envoyé : une recherche, un saut ou un pas d'une image tombe aussitôt sur la bonne ligne.
*Paramètres :*
* `filename` -- chaîne ; un chemin que le système de fichiers virtuel peut atteindre.
*Renvoie :* booléen, qui indique si au moins un sous-titre a été chargé.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<srtenable,srtEnable>>, <<srtposition,srtPosition>>, <<subtitles,Sous-titres>>
.Exemple
[source,lua]
----
if srtLoad("subs/" .. language .. ".srt") then
srtEnable(true)
end
----
[#srtloadtrack]
==== srtLoadTrack
[source,text]
----
loaded = srtLoadTrack(track)
----
Comme <<srtload,`srtLoad`>>, sauf que les sous-titres viennent de l'intérieur du conteneur même du disque plutôt que d'un fichier `.srt` placé à côté. Tout ce qui suit le chargement est identique : les sous-titres deviennent des numéros d'image du disque à la fréquence d'images du disque, si bien qu'ils sont retrouvés après n'importe quel saut, et <<srtenable,`srtEnable`>>, <<srtposition,`srtPosition`>> et <<srtclear,`srtClear`>> fonctionnent de même.
Le fichier entier est parcouru une fois pour recueillir la piste, sans décoder aucune image, si bien qu'un long disque prend un moment -- faites-le pendant que quelque chose d'autre est à l'écran plutôt qu'entre deux images de jeu.
Une piste qui contient des images plutôt que des mots (VobSub ou PGS) ne peut pas être lue : l'appel répond `false`, comme pour un `.srt` illisible, et laisse les sous-titres vidés. Il en va de même pour un numéro de piste qui ne désigne rien, et pour un jeu sans disque.
*Paramètres :*
* `track` -- nombre ; de `0` à `discGetSubtitleTracks() - 1`.
*Renvoie :* `true` quand au moins un sous-titre a été lu.
*Depuis :* 3.00.
*Voir aussi :* <<srtload,srtLoad>>, <<discgetsubtitletracks,discGetSubtitleTracks>>, <<srtenable,srtEnable>>
.Exemple
[source,lua]
----
-- Les sous-titres que portait la copie, avec un .srt à côté du jeu comme solution de repli.
if discGetSubtitleTracks() > 0 then
srtLoadTrack(0)
else
srtLoad(DIR .. "subtitles.srt")
end
srtEnable(true)
----
[#srtposition]
==== srtPosition
[source,text]
----
srtPosition(percent)
----
Extension Hypseus. La position d'un sous-titre, en pourcentage de la hauteur de l'image à partir du haut, de 1 à 95. La valeur par défaut est 80. Un nombre hors de cette plage est ignoré, comme Hypseus l'ignore, au lieu d'être ramené dans la plage ou refusé.
La hauteur s'applique aussitôt au sous-titre à l'écran et à chaque sous-titre suivant. <<overlaybanner,`overlayBanner`>> a sa propre hauteur et n'est pas concerné.
*Paramètres :*
* `percent` -- nombre ; de 1 à 95.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<srtload,srtLoad>>, <<subtitles,Sous-titres>>
.Exemple
[source,lua]
----
-- Garder les lignes à l'écart de l'afficheur de scores, en bas de l'écran.
srtPosition(70)
----
[#onlinescores]
=== Meilleurs scores en ligne
Un jeu envoie un score en une ligne :
[source,lua]
----
scoreSubmit(playerScore)
----
Tout le reste a été réglé une fois pour toutes, dans le menu : quel service, quel compte, et
sous quel nom le joueur apparaît (voir
xref:Manual.adoc#onlineservice[En ligne : comptes, jeux et scores]). Un jeu n'a besoin que d'un
`GAME_ID` dans son entrée de `games.dat` -- un UUID, sous lequel le tableau des scores est conservé. Voir
<<singegetgameid,singeGetGameId>> pour le format.
|===
| Appel | Ce qu'il fait
| `scoreBegin()`
| Annonce qu'une partie commence. Facultatif, mais utile : voir plus bas.
| `scoreSubmit(value [, board [, meta]])`
| Met un score en file d'attente. Renvoie `false` si la borne n'est pas connectée ou si le jeu n'a pas de `GAME_ID`.
| `scoreLowestWins([board])`
| Annonce qu'un tableau classe le score le plus bas en premier. Appelez-le avant le premier score du tableau.
| `scoreBoard(onDone [, board])`
| Récupère le tableau. `onDone(ok, result)` avec `result.top` (une liste de `{name, value}`),
`result.standing` (la `value` de ce joueur, son `rank` et le nombre de joueurs, `players`), et
`result.order`, `"highest"` ou `"lowest"`, le sens dans lequel le tableau classe. Une borne qui n'est pas
connectée peut aussi le récupérer : voir plus bas.
| `scorePlayerName()`
| Le nom sous lequel cette borne envoie ses scores, ou `nil` si aucun n'a été choisi.
| `scoreSignedIn()`
| Indique si cette borne est connectée à un compte en ligne.
| `scoreWaiting()`
| Combien de scores sont en file d'attente et pas encore envoyés.
| `scoreUpdate()`
| Une fois par image, depuis `onOverlayUpdate`. Rien n'avance sans lui.
| `scoreGameId()`
| Le `GAME_ID` du jeu, tiré de son entrée `games.dat`, ou `nil` s'il n'en a pas. Un jeu qui n'en a pas peut être joué, mais pas classé.
|===
**Un score est mis en file d'attente, pas envoyé.** Il est écrit sur le disque et remis au service
quand il y a une connexion, car une borne est souvent hors ligne et un jeu ne doit
jamais rester bloqué sur le réseau pour afficher son propre tableau. Un score en file d'attente
survit à l'extinction de la machine.
**Tout le monde peut lire un tableau.** Une borne qui n'est pas connectée récupère tout de même le
tableau d'un jeu que le service propose à tous, sans `standing` puisqu'il n'y a
aucun joueur à classer, si bien qu'un jeu peut montrer le tableau auquel il enverrait ses scores
et demander au joueur de se connecter. `scoreSignedIn()` distingue les deux cas : une borne
connectée qui n'a pas encore choisi de nom n'a pas non plus de `scorePlayerName()`, et
lui demander de se connecter serait une erreur. Un jeu que le service réserve à certains
comptes n'a pas de tableau pour une borne qui n'est connectée à aucun d'eux ; la
récupération échoue comme pour un jeu que le service ne connaît pas. Envoyer un score
demande toujours un compte.
**Un jeu peut avoir plusieurs tableaux.** Passez un nom de tableau pour séparer chaque mode
de jeu -- un contre-la-montre à part d'un total de points, le facile à part du
difficile -- et relisez-le avec le même nom. Les tableaux sont aussi séparés selon le script
en cours d'exécution : sans nom, un score va au tableau qui porte le nom du
script, c'est-à-dire son nom de fichier sans l'extension, si bien que `Collection/Raid.singe`
envoie à `raid` ; avec un nom, celui du script vient en premier, si bien que
`scoreSubmit(points, "hard")` depuis ce script envoie à `raid.hard`.
Chaque titre d'un `.game` porte le même `GAME_ID`, et c'est ce qui garde les
jeux d'une collection sur des tableaux bien à eux même quand deux d'entre eux nomment leurs
modes de la même façon, alors qu'un même jeu listé deux fois (en deux langues, par exemple : deux entrées de `games.dat`,
un seul script) garde un seul ensemble de tableaux. Les noms de tableau sont en minuscules et contiennent
des lettres, des chiffres, `-`, `_` et `.` ; tout autre caractère devient `-`. Le nom complet fait
au plus 32 caractères, et la partie du script est raccourcie en premier pour que le nom
du mode survive.
**Un tableau peut classer le score le plus bas en premier.** Coups, morts, secondes : certains
jeux comptent ce dont le joueur veut le moins. Appelez `scoreLowestWins` avec le nom du
tableau, tel que le prend `scoreSubmit`, avant le premier score du tableau, et chaque score
envoyé à ce tableau et chaque récupération de celui-ci suivent ce sens. Le premier score d'un tableau
fixe son sens pour de bon, si bien qu'aucun tableau n'est jamais classé dans les deux sens : un score envoyé
ensuite dans l'autre sens est refusé, et la file d'attente l'abandonne au lieu de le
réessayer. Sur un tel tableau, zéro est un score, tout comme un nombre inférieur à zéro, un
parcours sous le par.
.Exemple
[source,lua]
----
-- Un jeu de golf : le moins de coups gagne, et le par a son propre tableau.
scoreLowestWins("strokes")
scoreLowestWins("par")
function roundOver(strokes, coursePar)
scoreSubmit(strokes, "strokes")
scoreSubmit(strokes - coursePar, "par") -- -2, c'est deux sous le par
end
----
**Le nom affiché sur un tableau est un pseudo, jamais une adresse.** Il est choisi dans
l'outil Online Account, séparé du compte, et c'est la seule chose qu'un
joueur montre aux autres joueurs. De trois à vingt caractères parmi les lettres, les chiffres,
les traits d'union et les tirets bas, commençant et finissant par une lettre ou un chiffre -- pas
d'espaces, pas de points et pas de `@`, si bien qu'un pseudo ne peut même pas ressembler à une adresse e-mail.
La casse est ignorée pour l'unicité : `Ann` et `ANN` sont un seul et même nom.
**Un pseudo se choisit une fois.** Si l'on pouvait en changer librement, l'historique d'un tableau des scores
n'appartiendrait vraiment à personne : un joueur ne peut donc pas le changer ensuite, et une adresse
e-mail ne peut pas être changée du tout.
Un pseudo que personne ne devrait revendiquer (`admin`, `staff`), ou qui contient des mots qui n'ont pas leur
place sur un écran dans une salle publique, est refusé au moment du choix. Un score est
refusé tant qu'aucun nom n'a été choisi, car un score qui ne porte le nom de personne
ne figure sur un tableau en aucun sens utile.
**Ce que vaut un score.** La borne est la machine du joueur : un score envoyé
est donc une *affirmation* -- rien de ce que le client peut faire n'en fait davantage, et
le signer avec un secret livré dans le client n'aurait que l'apparence de la sécurité.
Rien ici ne prétend donc en vérifier un, et un jeu ne doit pas être écrit comme
si quelque chose d'autre allait le faire.
**`scoreBegin()` vaut la peine d'être appelé** pour la seule chose qui n'est pas une affirmation. Appelez-le
quand une partie commence ; le score qui suit porte la durée de la partie,
chronométrée à l'autre bout plutôt que sur cette machine. Ce nombre n'est pas
passé par la machine du joueur, et c'est ce qui lui donne sa valeur.
Rien n'est rejeté sur cette base : l'appeler coûte donc une ligne à un jeu et ne peut jamais
écarter un score légitime. Une borne hors ligne au début d'une partie
n'enregistre simplement aucune durée, ce qui est honnête, pas défaillant.
.Exemple
[source,lua]
----
function startLevel()
scoreBegin() -- le chronométrage commence maintenant, à l'autre bout
...
end
----
.Exemple
[source,lua]
----
-- À la fin d'une partie, envoyer le score puis montrer où il s'est placé.
function gameOver(points)
scoreSubmit(points)
scoreBoard(function(ok, board)
if not ok then
showMessage("Not signed in; score kept for later.")
return
end
for place, row in ipairs(board.top) do
showLine(place .. ". " .. row.name .. " " .. row.value)
end
end)
end
function onOverlayUpdate()
scoreUpdate()
return OVERLAY_UPDATED
end
----
[#stats]
=== Statistiques
L'overlay du développeur, dans le coin supérieur gauche : combien de temps l'image a pris et
son pire temps sur les deux dernières secondes, la fréquence d'images, la mémoire de Lua, combien de sprites,
de sons et de minuteurs sont vivants, combien de nœuds de la scène ont été dessinés et en combien
de lots, combien de mémoire de texture elle occupe, et ce que fait le disque.
Il est dessiné avec la police de débogage propre à SDL : il ne demande donc ni fichier de police, ni ressource, ni
document GUI, et il apparaît même sur une machine où le périphérique 3D n'a jamais
démarré. Il est dessiné après que <<singescreenshot,`singeScreenshot`>> a pris son image,
si bien qu'il n'apparaît jamais dans une capture d'écran.
Laissez-le désactivé dans un jeu livré. Rien ne vous empêche de le mettre derrière une touche dans
votre propre menu de service.
[#statsenable]
==== statsEnable
[source,text]
----
statsEnable(enabled)
----
Affiche ou masque l'overlay. Tant qu'il est affiché, l'écran est redessiné à chaque image, si bien que les nombres continuent de bouger même quand rien d'autre ne bouge à l'écran.
*Paramètres :*
* `enabled` -- booléen.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<statsisenabled,statsIsEnabled>>
.Exemple
[source,lua]
----
-- Une touche de développeur, derrière un indicateur, pour que le jeu livré n'ait pas de telle touche.
function onInputPressed(key)
if DEVELOPER and key == SWITCH_SERVICE then
statsEnable(not statsIsEnabled())
end
end
----
[#statsisenabled]
==== statsIsEnabled
[source,text]
----
shown = statsIsEnabled()
----
Indique si l'overlay est affiché.
*Paramètres :*
* Aucun.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
[#terrain]
=== Terrain
Un sol en carte de hauteurs créé avec `meshHeightmap`, qui transforme une image en niveaux de gris en un maillage d'un sommet par pixel ; `terrainGetHeight` relit ce maillage par le nœud qui le porte. Les positions et les hauteurs sont en unités du monde, et la position et l'échelle du nœud sont prises en compte. Voir « Le terrain » dans <<scenes3d,Scènes 3D>>.
[#terraingetheight]
==== terrainGetHeight
[source,text]
----
height = terrainGetHeight(node, x, z)
----
La hauteur, dans le monde, du maillage de carte de hauteurs porté par `node` au point `x, z` du monde, interpolée entre les quatre échantillons les plus proches, ou `nil` au-delà de ses bords ou quand le maillage du nœud n'est pas une carte de hauteurs. La position et l'échelle du nœud dans le monde comptent ; sa rotation est ignorée, puisque les terrains sont à plat. Servez-vous-en pour placer des arbres, faire apparaître des choses sur le sol, ou pour une caméra qui suit la surface, sans le coût d'un lancer de rayon. Met fin au script pour un handle de nœud non valide.
*Paramètres :*
* `node` -- le nœud dont le maillage vient de `meshHeightmap`.
* `x, z` -- la position du monde à échantillonner.
*Renvoie :* Un nombre, ou `nil` hors du terrain.
*Depuis :* 3.00.
*Voir aussi :* <<meshheightmap,meshHeightmap>>, <<physicsraycast,physicsRaycast>>, <<nodesetposition,nodeSetPosition>>
.Exemple
[source,lua]
----
-- Des arbres sur les collines, et une caméra qui ne descend jamais sous le sol.
for i = 1, 40 do
local x = math.random(-50, 50)
local z = math.random(-50, 50)
local tree = modelInstance(treeModel)
nodeSetPosition(tree, x, terrainGetHeight(hills, x, z) or 0, z)
end
function onOverlayUpdate()
local cx, cy, cz = nodeGetWorldPosition(camera)
local ground = terrainGetHeight(hills, cx, cz)
if ground and cy < ground + 1.5 then
nodeSetPosition(camera, cx, ground + 1.5, cz)
end
return OVERLAY_UPDATED
end
----
[#timer]
=== Minuteur
Faire arriver quelque chose plus tard, sans compter les images vous-même. Les minuteurs tournent sur
l'horloge du moteur, qui est l'horloge virtuelle avec `--deterministic`, si bien qu'un jeu
minuté se répète exactement d'une exécution à l'autre. Ils se déclenchent avec les autres
callbacks de l'image, sur le même fil d'exécution que tout le reste -- il n'y a rien à verrouiller, et
rien ne peut se déclencher pendant que votre script est au milieu de quelque chose.
Un minuteur qui a pris du retard -- un long chargement, un point d'arrêt -- se déclenche une fois quand
le jeu rattrape son retard, au lieu de se déclencher les vingt fois qu'il a manquées.
Tout est oublié quand le script se recharge, si bien qu'un minuteur de l'exécution précédente
ne se déclenche jamais dans la suivante.
[#timerafter]
==== timerAfter
[source,text]
----
id = timerAfter(milliseconds, function)
----
Appelle la fonction une fois, plus tard. La fonction reçoit l'identifiant du minuteur, si bien qu'une même fonction peut servir à plusieurs minuteurs.
*Paramètres :*
* `milliseconds` -- nombre ; zéro ou moins déclenche à l'image suivante.
* `function` -- appelée sous la forme `function(id)`.
*Renvoie :* un nombre ; l'identifiant du minuteur.
*Depuis :* 3.00.
*Voir aussi :* <<timercancel,timerCancel>>, <<timerevery,timerEvery>>
.Exemple
[source,lua]
----
-- Laisser l'explosion se terminer avant l'écran de fin de partie.
soundPlay(bang)
timerAfter(1500, function()
showGameOver()
end)
----
[#timercancel]
==== timerCancel
[source,text]
----
timerCancel(id)
----
Arrête un minuteur. Annuler un minuteur qui s'est déjà déclenché n'est pas une erreur, si bien que rien n'a besoin de vérifier avant.
*Paramètres :*
* `id` -- nombre ; venu de `timerAfter` ou de `timerEvery`.
*Renvoie :* rien.
*Depuis :* 3.00.
[#timerevery]
==== timerEvery
[source,text]
----
id = timerEvery(milliseconds, function)
----
Appelle la fonction encore et encore jusqu'à son annulation.
*Paramètres :*
* `milliseconds` -- nombre ; l'intervalle entre deux appels.
* `function` -- appelée sous la forme `function(id)`.
*Renvoie :* un nombre ; l'identifiant du minuteur.
*Depuis :* 3.00.
*Voir aussi :* <<timercancel,timerCancel>>
.Exemple
[source,lua]
----
-- Faire avancer le compte à rebours une fois par seconde, et l'arrêter quand il arrive à zéro.
local left = 30
local clock
clock = timerEvery(1000, function()
left = left - 1
if left <= 0 then
timerCancel(clock)
outOfTime()
end
end)
----
[#timerisactive]
==== timerIsActive
[source,text]
----
running = timerIsActive(id)
----
Indique si le minuteur se déclenchera encore : `true` pour un minuteur répétitif jusqu'à son annulation, `false` pour un minuteur à un coup qui s'est déjà déclenché.
*Paramètres :*
* `id` -- nombre.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
[#tween]
=== Tween
Faire passer un nombre d'une valeur à une autre au fil du temps, selon une courbe. Fondus, glissements,
mouvements de caméra, une barre de vie qui se vide, un menu qui glisse à l'écran -- c'est toujours la même chose.
`tweenValue` vous donne le nombre et vous laisse décider de ce qu'il signifie. C'est
voulu : le moteur a des nœuds, des sprites, des éléments de GUI, des matériaux et des lumières,
et un tween qui connaîtrait chacun d'eux ferait cinq jeux d'appels à garder en
phase. Un tween qui donne un nombre n'en fait aucun, et il fonctionne sur des choses dont le moteur
n'a jamais entendu parler.
Les courbes d'accélération sont les courbes habituelles. `EASE_LINEAR` n'est pas une courbe du tout ;
`EASE_QUAD_*`, `EASE_CUBIC_*`, `EASE_SINE_*` et `EASE_EXPO_*` vont de la plus douce
à la plus raide ; `EASE_BACK_*` dépasse puis revient ; `EASE_ELASTIC_*` oscille comme un ressort ;
`EASE_BOUNCE_*` tombe et rebondit. Chacune existe en `_IN` (lente au début),
`_OUT` (lente à la fin) et `_IN_OUT` (les deux).
[#tweencancel]
==== tweenCancel
[source,text]
----
tweenCancel(id)
----
Arrête un tween là où il en est. Aucune des deux fonctions n'est plus appelée, et la valeur reste là où elle était arrivée.
*Paramètres :*
* `id` -- nombre ; venu de `tweenValue`.
*Renvoie :* rien.
*Depuis :* 3.00.
[#tweenisactive]
==== tweenIsActive
[source,text]
----
running = tweenIsActive(id)
----
Indique si le tween est encore en cours.
*Paramètres :*
* `id` -- nombre.
*Renvoie :* `true` ou `false`.
*Depuis :* 3.00.
[#tweenvalue]
==== tweenValue
[source,text]
----
id = tweenValue(from, to, milliseconds, easing, onUpdate [, onDone])
----
Fait passer un nombre de `from` à `to` en `milliseconds`, en appelant `onUpdate(value, progress)` à chaque image -- `progress` va de 0 à 1 et ignore la courbe -- et `onDone()` une fois à la fin. Le dernier `onUpdate` tombe toujours exactement sur `to`, si bien que rien ne s'arrête un pixel trop tôt.
*Paramètres :*
* `from`, `to` -- nombres.
* `milliseconds` -- nombre ; zéro ou moins arrive à l'image suivante.
* `easing` -- l'une des valeurs `EASE_`.
* `onUpdate` -- appelée sous la forme `function(value, progress)`.
* `onDone` -- facultative ; appelée sans argument à la fin.
*Renvoie :* un nombre ; l'identifiant du tween.
*Depuis :* 3.00.
*Voir aussi :* <<tweencancel,tweenCancel>>
.Exemple
[source,lua]
----
-- Faire glisser le titre à l'écran et faire monter l'overlay en fondu en même temps.
tweenValue(-200, 40, 600, EASE_BACK_OUT, function(x)
titleX = x
end)
tweenValue(0, 255, 600, EASE_SINE_OUT, function(alpha)
setOverlayOpacity(math.floor(alpha))
end, function()
startAttractLoop()
end)
----
[#vehicle]
=== Véhicule
Voitures, motos, chars et bateaux construits sur un corps de châssis (voir <<physics,Physique>>, « Véhicules » et « Eau »). Un véhicule n'a pas de handle propre : on le désigne par le nœud de son châssis, qui doit déjà porter un corps `BODY_DYNAMIC` créé par `bodyNew`, et un nœud sans véhicule met fin au script dans tous les appels qui suivent `vehicleNew`. Le châssis est tourné vers -Z, avec Y vers le haut ; les distances sont en unités du monde, les forces en newtons, les couples en newtons-mètres et les régimes du moteur en tours par minute. Chaque appel de réglage marque le véhicule pour une reconstruction au pas de physique suivant, autour des positions de roues enregistrées quand les roues ont été ajoutées. Jusqu'à 16 véhicules à la fois, chacun avec jusqu'à 16 roues.
[#vehiclenew]
==== vehicleNew
[source,text]
----
vehicleNew(node, kind)
----
Fait du corps dynamique de `node` le châssis d'un véhicule. Une voiture, une moto ou un char a besoin d'au moins deux roues ajoutées par `vehicleAddWheel` avant que le pas de physique suivant le construise ; un bateau n'a pas de roues et il est poussé par une hélice qui ne fonctionne que sous une surface créée avec `bodySetWater`. Le nouveau véhicule part des valeurs par défaut : un moteur de 500 Nm qui culmine à 6000 tr/min et tourne au ralenti à 1000, la boîte automatique à cinq rapports de Jolt, 35 degrés de braquage, des freins de 1500 Nm et un frein à main de 4000 Nm, une suspension de 1,5 Hz avec un amortissement de 0,5, et aucune barre antiroulis. L'appeler de nouveau sur le même nœud supprime d'abord l'ancien véhicule et ses roues. Met fin au script pour un type inconnu, un nœud dont le corps n'est pas dynamique, un type à roues dans un monde 2D, ou quand 16 véhicules existent déjà.
*Paramètres :*
* `node` -- le nœud du châssis, qui porte un corps `BODY_DYNAMIC`.
* `kind` -- `VEHICLE_CAR` (n'importe quel nombre d'essieux), `VEHICLE_MOTORCYCLE` (deux roues en ligne, qui s'incline dans les virages), `VEHICLE_TANK` (des roues sur deux chenilles, qui tourne sur place) ou `VEHICLE_BOAT` (une coque flottante avec une hélice et un gouvernail).
*Depuis :* 3.00.
*Voir aussi :* <<vehicleaddwheel,vehicleAddWheel>>, <<vehicledrive,vehicleDrive>>, <<vehiclesetthrust,vehicleSetThrust>>, <<bodynew,bodyNew>>
.Exemple
[source,lua]
----
local car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 0.9, 0)
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
bodySetMass(car, 1500)
vehicleNew(car, VEHICLE_CAR)
for _, p in ipairs({ { -0.95, -0.2, -1.4 }, { 0.95, -0.2, -1.4 }, { -0.95, -0.2, 1.4 }, { 0.95, -0.2, 1.4 } }) do
local hub = nodeNew()
nodeSetParent(hub, car)
nodeSetPosition(hub, p[1], p[2], p[3])
vehicleAddWheel(car, hub, 0.35, 0.25, 0.4)
end
----
[#vehicledelete]
==== vehicleDelete
[source,text]
----
vehicleDelete(node)
----
Retire le véhicule de son châssis : la contrainte des roues, les commandes du conducteur et tous les réglages. Le corps reste un corps dynamique ordinaire, si bien qu'une épave continue de glisser et de culbuter, et les nœuds des roues restent là où le dernier pas les a posés ; plus rien ne les déplace, sauf si le script le fait. Un nœud sans véhicule met fin au script.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclenew,vehicleNew>>, <<bodydelete,bodyDelete>>
.Exemple
[source,lua]
----
function onCollision(a, b, x, y, z, speed)
if (a == car or b == car) and speed > 25 and not wrecked then
wrecked = true
vehicleDelete(car) -- Le châssis continue de culbuter comme un corps
soundPlay(crashSound)
emitterStart(smoke)
end
end
----
[#vehicleaddwheel]
==== vehicleAddWheel
[source,text]
----
index = vehicleAddWheel(node, wheelNode, radius, width, suspension)
----
Ajoute au véhicule une roue placée là où se trouve `wheelNode` par rapport au châssis lors de la prochaine construction du véhicule. Sans charge, la suspension tient la roue `suspension` unités sous ce point, et elle se comprime jusqu'à la moitié de cette longueur. Dès lors, le moteur de jeu écrit la transformation du nœud de la roue dans le monde après chaque pas, avec X comme essieu et Y vers le haut, si bien que le pneu visible a sa place sur un nœud enfant : un `meshCylinder` tourné de 90 degrés autour de Z. Les roues situées devant la position moyenne des roues, le long de l'axe avant, comptent comme roues avant : elles braquent par défaut et n'ont pas de frein à main. Un char répartit ses roues sur la chenille gauche ou droite selon le côté du châssis où elles se trouvent, et il lui en faut au moins une de chaque côté. Les valeurs inférieures à `0.001` sont relevées à cette valeur. Met fin au script à la 17e roue.
*Paramètres :*
* `node` -- le nœud du châssis.
* `wheelNode` -- le nœud auquel la roue est suspendue ; sa position par rapport au châssis est le point d'attache, et le moteur de jeu le pose désormais.
* `radius` -- le rayon de la roue, en unités du monde.
* `width` -- la largeur de la roue, en unités du monde.
* `suspension` -- la longueur de la suspension à pleine extension, en unités du monde ; elle se comprime jusqu'à la moitié.
*Renvoie :* L'indice de la roue, à partir de `0`, pour `vehicleSetWheel`, `vehicleIsWheelOnGround` et `vehicleGetWheelSlip`.
*Remarques :* Un véhicule n'est construit qu'une fois qu'il a deux roues, et une voiture ou une moto a aussi besoin d'une roue motrice ; d'ici là, il reste un simple corps et les appels de conduite ne font rien.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetwheel,vehicleSetWheel>>, <<vehiclesetsuspension,vehicleSetSuspension>>, <<nodefind,nodeFind>>
.Exemple
[source,lua]
----
-- Le modèle du camion porte des nœuds vides nommés d'après ses roues.
local truck = modelInstance(truckModel)
local wheels = {}
nodeSetPosition(truck, 0, 1.2, -8)
bodyNew(truck, BODY_DYNAMIC, SHAPE_BOX, 2.4, 1.4, 6.0)
bodySetMass(truck, 4000)
vehicleNew(truck, VEHICLE_CAR)
for i, name in ipairs({ "wheelFL", "wheelFR", "wheelRL", "wheelRR" }) do
wheels[i] = vehicleAddWheel(truck, nodeFind(name, truck), 0.5, 0.35, 0.45)
end
----
[#vehiclesetthrust]
==== vehicleSetThrust
[source,text]
----
vehicleSetThrust(node, maxForce, x, y, z)
----
Règle l'hélice d'un bateau : la force avec laquelle elle pousse à plein gaz, `2000` newtons par défaut, et le point du repère de la coque où elle pousse, `0, -0.2, 1` par défaut, juste sous le pont à la poupe. La poussée suit l'axe avant de la coque et n'a lieu que tant que ce point est sous une surface d'eau ; le même point décide si le gouvernail et le frein agissent. Une force négative devient `0`. Les types à roues gardent les valeurs et les ignorent.
*Paramètres :*
* `node` -- le nœud de la coque.
* `maxForce` -- la poussée à plein gaz, en newtons.
* `x`, `y`, `z` -- l'emplacement de l'hélice, dans le repère de la coque ; la poupe est vers +Z.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetrudder,vehicleSetRudder>>, <<bodysetwater,bodySetWater>>, <<bodysetbuoyancy,bodySetBuoyancy>>
.Exemple
[source,lua]
----
local raft = nodeNew()
nodeSetMesh(raft, meshBox(1.6, 0.3, 2.4), wood)
nodeSetPosition(raft, 1, 0.3, 4)
bodyNew(raft, BODY_DYNAMIC, SHAPE_BOX, 1.6, 0.3, 2.4)
bodySetMass(raft, 120)
bodySetBuoyancy(raft, 3.0) -- Flotte haut sur le bassin
vehicleNew(raft, VEHICLE_BOAT)
vehicleSetThrust(raft, 600, 0, -0.1, 1.2) -- Un hors-bord accroché à la poupe
vehicleSetRudder(raft, 300)
----
[#vehiclesetwheel]
==== vehicleSetWheel
[source,text]
----
vehicleSetWheel(node, index, steered, driven)
----
Indique si une roue tourne avec la direction et si le moteur l'entraîne, en remplaçant les valeurs par défaut pour cette roue : les roues avant braquent, et toutes les roues sont motrices sauf la roue avant d'une moto. Les roues motrices s'apparient d'un côté à l'autre du châssis en différentiels, un par essieu, et le couple du moteur est partagé à parts égales entre les essieux ; une roue motrice sans partenaire forme un essieu à elle seule. Un char ignore les deux indicateurs, puisque ses chenilles entraînent toutes les roues et font tourner la coque. Prend effet à la reconstruction du pas suivant. Met fin au script pour un indice auquel ne correspond aucune roue du véhicule.
*Paramètres :*
* `node` -- le nœud du châssis.
* `index` -- l'indice de la roue venu de `vehicleAddWheel`.
* `steered` -- `true` pour que la roue tourne jusqu'à `vehicleSetSteering` au braquage maximal.
* `driven` -- `true` pour que le moteur l'entraîne.
*Depuis :* 3.00.
*Voir aussi :* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetsteering,vehicleSetSteering>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
.Exemple
[source,lua]
----
-- Une voiture de sport à propulsion : les roues avant ne font que braquer.
vehicleSetWheel(car, wheels[1], true, false)
vehicleSetWheel(car, wheels[2], true, false)
vehicleSetWheel(car, wheels[3], false, true)
vehicleSetWheel(car, wheels[4], false, true)
if fourWheelSteer then
vehicleSetWheel(car, wheels[3], true, true)
vehicleSetWheel(car, wheels[4], true, true)
end
----
[#vehiclesetengine]
==== vehicleSetEngine
[source,text]
----
vehicleSetEngine(node, maxTorque, maxRpm)
vehicleSetEngine(node, maxTorque, maxRpm, minRpm)
----
Règle le moteur : le couple maximal en newtons-mètres, le régime maximal et le régime de ralenti en tours par minute. Les valeurs par défaut sont `500`, `6000` et `1000`, et le ralenti revient à `1000` quand il est omis. Le couple est maintenu à `1` ou plus, le régime maximal à `100` ou plus, et le ralenti entre `1` et le régime maximal. Un bateau n'a pas de moteur et ignore l'appel. Prend effet à la reconstruction du pas suivant.
*Paramètres :*
* `node` -- le nœud du châssis.
* `maxTorque` -- le couple maximal, en newtons-mètres.
* `maxRpm` -- le régime maximal, en tours par minute.
* `minRpm` -- le régime de ralenti, en tours par minute ; `1000` par défaut.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
.Exemple
[source,lua]
----
vehicleSetEngine(tank, 1500, 4000, 800) -- Un diesel : du couple à bas régime
vehicleSetEngine(bike, 150, 9000, 1200) -- Un moteur qui hurle dans les tours
function onOverlayUpdate()
local rpm = vehicleGetRpm(bike)
overlayClear()
fontPrint(20, 20, string.format("%5.0f rpm", rpm))
if rpm > 8500 then
spriteDraw(shiftLight, 200, 20) -- Il est temps de passer le rapport supérieur
end
return OVERLAY_UPDATED
end
----
[#vehiclesetgears]
==== vehicleSetGears
[source,text]
----
vehicleSetGears(node, { ratio, ... })
vehicleSetGears(node, { ratio, ... }, reverseRatio)
vehicleSetGears(node, { ratio, ... }, reverseRatio, automatic)
----
Règle la boîte de vitesses : d'un à huit rapports de marche avant, le premier en premier, le rapport de marche arrière (`2.9` par défaut, donné comme un nombre positif) et si la boîte passe les rapports d'elle-même (`true` par défaut). Un véhicule qui ne reçoit jamais cet appel utilise la boîte à cinq rapports de Jolt, `2.66`, `1.78`, `1.3`, `1.0` et `0.74`, avec la même marche arrière. Quand `automatic` est faux, la boîte ne passe plus les rapports d'elle-même, et aucun appel ne permet de les passer depuis le script. Un bateau n'a pas de boîte de vitesses et ignore l'appel. Prend effet à la reconstruction du pas suivant. Met fin au script quand la table est vide ou contient plus de huit rapports.
*Paramètres :*
* `node` -- le nœud du châssis.
* `{ ratio, ... }` -- les rapports de marche avant, en tours du moteur par tour de sortie de boîte, le premier en premier ; d'un à huit.
* `reverseRatio` -- le rapport de la marche arrière ; `2.9` par défaut.
* `automatic` -- `true` (par défaut) pour que la boîte passe les rapports d'elle-même.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclegetgear,vehicleGetGear>>, <<vehiclesetengine,vehicleSetEngine>>
.Exemple
[source,lua]
----
-- Une boîte six rapports à rapports rapprochés pour la voiture de course, une boîte trois rapports longue pour le bus.
vehicleSetGears(raceCar, { 3.2, 2.3, 1.8, 1.4, 1.1, 0.9 }, 3.0)
vehicleSetGears(bus, { 2.4, 1.3, 0.8 })
function onOverlayUpdate()
overlayClear()
fontPrint(20, 20, string.format("Gear %d %.0f rpm", vehicleGetGear(raceCar), vehicleGetRpm(raceCar)))
return OVERLAY_UPDATED
end
----
[#vehiclesetrudder]
==== vehicleSetRudder
[source,text]
----
vehicleSetRudder(node, maxTorque)
----
Règle le gouvernail d'un bateau : le couple autour de l'axe vertical de la coque quand la direction est à fond, `800` newtons-mètres par défaut. Le gouvernail mord avec la vitesse, un cinquième du couple à l'arrêt et la totalité à partir de 3 unités du monde par seconde, et il tourne dans l'autre sens quand le bateau recule. Il n'agit que tant que le point de l'hélice est sous l'eau. Un couple négatif devient `0` ; les types à roues ignorent l'appel.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetthrust,vehicleSetThrust>>, <<vehicledrive,vehicleDrive>>
.Exemple
[source,lua]
----
vehicleNew(patrolBoat, VEHICLE_BOAT)
vehicleSetThrust(patrolBoat, 4000, 0, -0.4, 3.5)
vehicleSetRudder(patrolBoat, 2500) -- Coque lourde, grand gouvernail
function onOverlayUpdate()
local helm = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X) / 32767
local gas = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
vehicleDrive(patrolBoat, gas, helm, 0)
return OVERLAY_NOT_UPDATED
end
----
[#vehiclesetsuspension]
==== vehicleSetSuspension
[source,text]
----
vehicleSetSuspension(node, frequency, damping)
----
Règle le ressort de chaque roue : sa fréquence propre en hertz (`1.5` par défaut, maintenue à `0.1` ou plus ; plus elle est haute, plus le ressort est raide) et son taux d'amortissement (`0.5` par défaut ; `0` rebondit, `1` se stabilise sans dépassement). Prend effet à la reconstruction du pas suivant, sur toutes les roues à la fois ; il n'y a pas de ressort par roue. Un bateau n'a pas de suspension et ignore l'appel.
*Paramètres :*
* `node` -- le nœud du châssis.
* `frequency` -- la fréquence propre du ressort, en hertz.
* `damping` -- le taux d'amortissement, à partir de `0`.
*Depuis :* 3.00.
*Voir aussi :* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclesetantiroll,vehicleSetAntiRoll>>
.Exemple
[source,lua]
----
-- Le buggy flotte sur les dunes ; la voiture de course bouge à peine sur ses ressorts.
vehicleSetSuspension(buggy, 1.0, 0.3)
vehicleSetSuspension(raceCar, 3.0, 0.7)
if surface == "gravel" then
vehicleSetSuspension(raceCar, 2.0, 0.5)
end
----
[#vehiclesetsteering]
==== vehicleSetSteering
[source,text]
----
vehicleSetSteering(node, maxDegrees)
----
Règle de combien les roues directrices tournent au braquage maximal, en degrés ; `35` par défaut, ramené entre `0` et `89`. Une commande de direction de `-1` ou `1` venue de `vehicleDrive` atteint cet angle, et des commandes plus petites une part proportionnelle de celui-ci. Prend effet à la reconstruction du pas suivant. Un char tourne par ses chenilles et un bateau par son gouvernail, si bien que tous deux ignorent l'appel.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetwheel,vehicleSetWheel>>, <<vehicledrive,vehicleDrive>>
.Exemple
[source,lua]
----
-- Le kart tourne dans un mouchoir de poche ; le camion a besoin d'un large arc.
vehicleSetSteering(kart, 45)
vehicleSetSteering(truck, 25)
vehicleSetSteering(bus, 20)
if difficulty == "arcade" then
vehicleSetSteering(truck, 35) -- Une conduite indulgente
end
----
[#vehiclesetbrakes]
==== vehicleSetBrakes
[source,text]
----
vehicleSetBrakes(node, brakeTorque, handBrakeTorque)
----
Règle le couple de freinage que chaque roue peut appliquer pédale au plancher, `1500` newtons-mètres par défaut, et le couple du frein à main sur les roues arrière, `4000` par défaut ; les roues avant, celles qui sont devant la position moyenne des roues, n'ont pas de frein à main. Les valeurs négatives deviennent `0`. Un char freine ses chenilles avec les valeurs par défaut des chenilles de Jolt et ignore l'appel, et un bateau freine par une traînée contre sa propre vitesse, si bien qu'il ignore aussi l'appel. Prend effet à la reconstruction du pas suivant.
*Paramètres :*
* `node` -- le nœud du châssis.
* `brakeTorque` -- le couple du frein au pied sur chaque roue, en newtons-mètres.
* `handBrakeTorque` -- le couple du frein à main sur les roues arrière, en newtons-mètres.
*Depuis :* 3.00.
*Voir aussi :* <<vehicledrive,vehicleDrive>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
.Exemple
[source,lua]
----
vehicleSetBrakes(car, 2200, 6000) -- Plaquettes de course et frein à main prêt pour la dérive
function onOverlayUpdate()
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
local handBrake = controllerGetButton(0, GAMEPAD_0.BUTTON_B.value) and 1 or 0
vehicleDrive(car, throttle(), steering(), brake, handBrake)
return OVERLAY_NOT_UPDATED
end
----
[#vehiclesetantiroll]
==== vehicleSetAntiRoll
[source,text]
----
vehicleSetAntiRoll(node, stiffness)
----
Ajoute une barre antiroulis sur chaque essieu, en newtons par mètre ; `0` (par défaut) n'en monte aucune, et une valeur négative devient `0`. Une barre relie les deux roues motrices d'un essieu, si bien qu'un essieu dont les roues ne sont pas motrices n'a pas de barre, et qu'une moto n'en a aucune. Quelques centaines gardent une voiture à plat dans les virages ; trop soulève les roues intérieures. Un char ignore l'appel. Prend effet à la reconstruction du pas suivant.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetsuspension,vehicleSetSuspension>>, <<vehiclesetwheel,vehicleSetWheel>>
.Exemple
[source,lua]
----
local car = nodeNew()
nodeSetMesh(car, meshBox(1.8, 0.5, 4.0), paint)
nodeSetPosition(car, 0, 0.9, 0)
bodyNew(car, BODY_DYNAMIC, SHAPE_BOX, 1.8, 0.5, 4.0)
bodySetMass(car, 1500)
vehicleNew(car, VEHICLE_CAR)
addFourWheels(car)
vehicleSetEngine(car, 600, 6500, 1000)
vehicleSetAntiRoll(car, 800) -- Reste à plat dans la chicane
----
[#vehicledrive]
==== vehicleDrive
[source,text]
----
vehicleDrive(node, forward, right)
vehicleDrive(node, forward, right, brake)
vehicleDrive(node, forward, right, brake, handBrake)
----
Règle les commandes du conducteur, qui restent valables jusqu'à l'appel suivant : appelez-la donc à chaque image depuis `onOverlayUpdate`. L'accélérateur et la direction vont de `-1` à `1`, le frein et le frein à main de `0` à `1` ; les valeurs hors limites sont ramenées dans la plage, et les commandes omises valent `0`. Une voiture ou une moto reçoit les quatre comme commandes de conducteur de Jolt, un accélérateur négatif faisant reculer par la marche arrière. Un char prend `forward` comme accélérateur, et `right` ralentit la chenille intérieure ; sans accélérateur, les chenilles tournent en sens inverse l'une de l'autre, si bien que la coque pivote sur place, et le frein à main est ignoré. Un bateau prend `forward` comme accélérateur de l'hélice, `right` comme gouvernail et `brake` comme une traînée contre la vitesse de la coque, ignore le frein à main, et ne fait rien du tout tant que l'hélice est hors de l'eau. Toute commande non nulle réveille un châssis qui s'est mis en sommeil.
*Paramètres :*
* `node` -- le nœud du châssis.
* `forward` -- accélérateur, de `-1` à `1` ; une valeur négative fait reculer.
* `right` -- direction, de `-1` (gauche) à `1` (droite).
* `brake` -- frein au pied, de `0` à `1` ; `0` par défaut.
* `handBrake` -- frein à main, de `0` à `1` ; `0` par défaut.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclegetspeed,vehicleGetSpeed>>, <<vehiclesetbrakes,vehicleSetBrakes>>, <<controllergetaxis,controllerGetAxis>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
local stick = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_X)
local steer = (math.abs(stick) > SINGE_DEAD_ZONE) and stick / 32767 or 0
local gas = controllerGetAxis(0, GAMEPAD_AXIS_RIGHT_TRIGGER) / 32767
local brake = controllerGetAxis(0, GAMEPAD_AXIS_LEFT_TRIGGER) / 32767
local handBrake = controllerGetButton(0, GAMEPAD_0.BUTTON_B.value) and 1 or 0
if controllerGetButton(0, GAMEPAD_0.BUTTON_A.value) then
gas = -gas -- Marche arrière tant que A est maintenu
end
vehicleDrive(car, gas, steer, brake, handBrake)
return OVERLAY_NOT_UPDATED
end
----
[#vehiclegetspeed]
==== vehicleGetSpeed
[source,text]
----
speed = vehicleGetSpeed(node)
----
La vitesse du corps du châssis le long de son axe avant, en unités du monde par seconde, négative quand il recule ; le mouvement latéral d'une dérive ne compte pas. Fonctionne pour tous les types, bateaux compris, dès l'appel à `vehicleNew`.
*Renvoie :* Un nombre.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclegetrpm,vehicleGetRpm>>, <<vehiclegetgear,vehicleGetGear>>, <<bodygetvelocity,bodyGetVelocity>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
local speed = vehicleGetSpeed(car)
overlayClear()
fontPrint(20, 20, string.format("%3.0f km/h", math.abs(speed) * 3.6))
if speed < -1 then
spriteDraw(reversingLamp, 300, 20)
end
return OVERLAY_UPDATED
end
----
[#vehiclegetrpm]
==== vehicleGetRpm
[source,text]
----
rpm = vehicleGetRpm(node)
----
Le régime du moteur en tours par minute, entre le ralenti et le régime maximal de `vehicleSetEngine`, pour un compte-tours ou la hauteur d'une boucle sonore de moteur. `0` pour un bateau, qui n'a pas de moteur, et pour un véhicule qui n'a pas encore été construit faute de roues.
*Renvoie :* Un nombre.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetengine,vehicleSetEngine>>, <<vehiclegetgear,vehicleGetGear>>
.Exemple
[source,lua]
----
-- Une barre de compte-tours qui devient rouge au-delà de 6000.
function onOverlayUpdate()
local rpm = vehicleGetRpm(car)
overlayClear()
if rpm > 6000 then
colorForeground(255, 40, 40)
else
colorForeground(60, 220, 60)
end
overlayBox(20, 20, 20 + math.floor(rpm / 6500 * 200), 32)
return OVERLAY_UPDATED
end
----
[#vehiclegetgear]
==== vehicleGetGear
[source,text]
----
gear = vehicleGetGear(node)
----
Le rapport que la boîte a engagé : `0` au point mort, `-1` en marche arrière, `1` et plus pour les rapports de marche avant, dans l'ordre où `vehicleSetGears` les a donnés. `0` pour un bateau et pour un véhicule qui n'a pas encore été construit.
*Renvoie :* Un entier.
*Depuis :* 3.00.
*Voir aussi :* <<vehiclesetgears,vehicleSetGears>>, <<vehiclegetrpm,vehicleGetRpm>>
.Exemple
[source,lua]
----
local lastGear = 0
function onOverlayUpdate()
local gear = vehicleGetGear(car)
if gear ~= lastGear then
soundPlay(gearChange)
lastGear = gear
end
overlayClear()
fontPrint(20, 20, (gear == -1) and "R" or ((gear == 0) and "N" or tostring(gear)))
return OVERLAY_UPDATED
end
----
[#vehicleiswheelonground]
==== vehicleIsWheelOnGround
[source,text]
----
touching = vehicleIsWheelOnGround(node, index)
----
Indique si le lancer de la suspension de cette roue a trouvé quelque chose contre quoi appuyer pendant ce pas. `false` pour un indice auquel ne correspond aucune roue du véhicule et pour un véhicule qui n'a pas encore été construit ; aucune erreur n'est levée dans un cas comme dans l'autre.
*Renvoie :* Vrai ou faux.
*Depuis :* 3.00.
*Voir aussi :* <<vehicleaddwheel,vehicleAddWheel>>, <<vehiclegetwheelslip,vehicleGetWheelSlip>>
.Exemple
[source,lua]
----
-- En l'air quand aucune roue ne touche ; un atterrissage après un saut secoue la caméra.
function onOverlayUpdate()
local airborne = true
for i = 0, 3 do
airborne = airborne and not vehicleIsWheelOnGround(car, i)
end
if wasAirborne and not airborne then
cameraShake = 12
end
wasAirborne = airborne
return OVERLAY_NOT_UPDATED
end
----
[#vehiclegetwheelslip]
==== vehicleGetWheelSlip
[source,text]
----
slip = vehicleGetWheelSlip(node, index)
----
Combien la roue patine ou glisse dans son sens de roulement, de `0` (adhérence) à `1` (patinage ou blocage), pour le crissement des pneus, la fumée et les traces de dérapage. Toujours `0` pour les roues d'un char, pour un indice auquel ne correspond aucune roue du véhicule, et pour un véhicule qui n'a pas encore été construit.
*Renvoie :* Un nombre.
*Depuis :* 3.00.
*Voir aussi :* <<vehicleiswheelonground,vehicleIsWheelOnGround>>, <<vehiclegetspeed,vehicleGetSpeed>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
local slip = math.max(vehicleGetWheelSlip(car, 2), vehicleGetWheelSlip(car, 3))
if slip > 0.4 and vehicleIsWheelOnGround(car, 2) then
emitterStart(tireSmoke)
if not soundIsPlaying(squealChannel) then
squealChannel = soundPlay(squeal)
end
else
emitterStop(tireSmoke)
end
return OVERLAY_NOT_UPDATED
end
----
[#view]
=== Vue
Une vue est une seconde caméra rendue dans une texture à chaque image, pour un écran, un miroir ou un portail dans la scène : `viewNew` en crée une et renvoie un handle entier, `viewSetCamera` l'attache à un nœud, et `materialSetView` l'affiche sur un matériau. Il peut en exister jusqu'à quatre à la fois ; chacune rend de nouveau toute la scène à sa propre taille, en pixels indépendants de l'overlay, avec la projection de la caméra principale, les ombres de l'image (cascades d'une lumière directionnelle comprises : chaque point prend la cascade la plus fine qui le couvre) et sans bloom : gardez-les donc peu nombreuses et petites. Chaque vue tourne les billboards (`nodeSetBillboard`) vers sa propre caméra et trie les dessins mélangés de l'arrière vers l'avant depuis celle-ci, si bien qu'une vue dont la caméra regarde d'ailleurs voit quand même les billboards de face et les objets transparents dans le bon ordre. Un mauvais handle met fin au script. Voir <<scenes3d,Scènes 3D>>.
[#viewdelete]
==== viewDelete
[source,text]
----
viewDelete(view)
----
Libère la vue et ses textures, et arrête la passe de rendu supplémentaire qu'elle coûtait. Les matériaux qui l'affichaient reviennent à leur propre texture. Le handle n'est plus valide ensuite et met fin au script s'il est réutilisé ; comme il ne peut exister que quatre vues à la fois, supprimez celles dont un niveau n'a plus besoin.
*Depuis :* 3.00.
*Voir aussi :* <<viewnew,viewNew>>, <<materialsetview,materialSetView>>
.Exemple
[source,lua]
----
-- Les écrans de la salle de sécurité ne sont rendus que pendant que le joueur s'y trouve.
function onEnterSecurityRoom()
feed = viewNew(512, 384)
viewSetCamera(feed, hallwayCamera)
materialSetView(monitorLook, feed)
end
function onLeaveSecurityRoom()
viewDelete(feed)
feed = nil
end
----
[#viewnew]
==== viewNew
[source,text]
----
view = viewNew(width, height)
----
Crée une caméra rendue à chaque image dans une texture de `width` sur `height` (chacune ramenée entre `1` et `4096`), depuis la vue par défaut jusqu'à ce que `viewSetCamera` désigne un nœud, et renvoie le handle de la vue pour `materialSetView`. La vue utilise la projection de `cameraSetPerspective` ou de `cameraSetOrthographic` à son propre format d'image. La scène est rendue une fois de plus par vue : `512` sur `384` suffit donc largement pour un écran, et un Raspberry Pi demande plus petit. Met fin au script quand la 3D n'est pas disponible ou que quatre vues sont déjà utilisées.
*Paramètres :*
* `width` -- la largeur de la texture en pixels.
* `height` -- la hauteur de la texture en pixels.
*Renvoie :* Le handle de la vue, un entier.
*Depuis :* 3.00.
*Voir aussi :* <<viewsetcamera,viewSetCamera>>, <<materialsetview,materialSetView>>, <<viewdelete,viewDelete>>
.Exemple
[source,lua]
----
-- Un écran de surveillance qui montre une caméra placée derrière le joueur.
eye = nodeNew()
nodeSetPosition(eye, 0, 2.2, -5)
nodeLookAt(eye, 0, 0.6, 0)
feed = viewNew(512, 384)
viewSetCamera(feed, eye)
screen = materialNew()
materialSetView(screen, feed)
materialSetUnlit(screen, true)
nodeSetMesh(monitor, meshPlane(2, 1.5), screen)
nodeSetRotation(monitor, 90, 0, 0)
----
[#viewsetcamera]
==== viewSetCamera
[source,text]
----
viewSetCamera(view, node)
viewSetCamera(view, nil)
viewSetCamera(view)
----
Le nœud depuis lequel regarde la vue, le long de son propre -Z comme la caméra principale, si bien que le mouvement de ses parents compte. Avec `nil` ou sans second argument, la vue revient à la vue par défaut, depuis `(0, 0, 5)` en regardant l'origine. Donnez au nœud une voiture pour parent pour un rétroviseur, ou reflétez la position de la caméra principale par rapport à un mur pour un miroir dans ce mur. Met fin au script pour un handle de nœud non valide.
*Depuis :* 3.00.
*Voir aussi :* <<viewnew,viewNew>>, <<cameraset,cameraSet>>, <<nodelookat,nodeLookAt>>
.Exemple
[source,lua]
----
-- L'écran du hall passe en revue trois caméras de sécurité.
cameras = { lobbyCamera, garageCamera, roofCamera }
current = 1
function onOverlayUpdate()
if singeGetTicks() - lastSwitch > 4000 then
current = current % #cameras + 1
lastSwitch = singeGetTicks()
viewSetCamera(feed, cameras[current])
end
return OVERLAY_UPDATED
end
----
[#video]
=== Vidéo
La famille `video*` gère des clips vidéo supplémentaires par-dessus le disque : un personnage qui parle dans un coin, une animation précalculée à un moment clé de l'histoire, plusieurs séquences simultanées. On peut charger autant de vidéos qu'on veut avec `videoLoad`, et chacune renvoie un handle entier opaque que toutes les autres fonctions `video*` prennent comme premier argument ; un handle qui a été déchargé met fin au script. Les vidéos sont dessinées sur l'overlay avec `videoDraw`, en coordonnées de l'overlay, et se composent comme des sprites, et une vidéo n'avance que pendant qu'elle est dessinée (ou utilisée comme matériau via <<materialsetvideo,materialSetVideo>>). Chaque vidéo a sa propre sélection de piste audio et son propre volume stéréo, indépendants du disque. Comme les sprites, les vidéos doivent être dessinées depuis `onOverlayUpdate` et finalement libérées avec `videoUnload` ; les règles de dessin sont dans <<overlay,Overlay>>.
[#videodraw]
==== videoDraw
[source,text]
----
videoDraw(id, x, y, x2, y2)
videoDraw(id, x, y, centered)
----
Fait avancer une vidéo jusqu'à l'image que demande son horloge et dessine cette image sur l'overlay. L'horloge suit l'audio de la vidéo quand elle en a, et le temps de l'horloge sinon, si bien qu'un appel peut avancer de zéro, une ou plusieurs images ; une vidéo qui atteint sa fin reboucle sur l'image `0`. La forme à cinq arguments étire l'image dans le rectangle allant de `(x, y)` à `(x2, y2)` inclus, toujours avec un échantillonnage au plus proche voisin et sans tenir compte de `videoScale`, `videoRotate` et `videoQuality`. La forme à quatre arguments dessine l'image à sa taille native transformée par `videoScale` et `videoRotate`, ancrée par son coin supérieur gauche ou, quand `centered` vaut `true`, par son centre ; l'image transformée n'est reconstruite que lorsque l'image ou la transformation change, et les coins laissés par la rotation sont transparents. Si le décodeur n'a pas encore produit d'image, rien n'est dessiné.
Une image qui n'est ni mise à l'échelle ni tournée par `videoScale` ou `videoRotate`, dessinée sur l'overlay là où rien n'a été dessiné depuis un `overlayClear` avec une couleur transparente -- une vidéo de fond dessinée en premier, par exemple -- est dessinée par le GPU à la place, sous l'overlay, et ne coûte presque rien ; tout ce qui est dessiné ensuite passe toujours par-dessus. Elle est alors mise à l'échelle de l'écran comme l'overlay, au lieu d'être d'abord copiée au plus proche voisin dans l'overlay. Partout ailleurs, l'image est copiée dans l'overlay par le CPU, ce qui coûte plusieurs millisecondes par image sur une petite machine : dessinez donc une vidéo avant ce qui passe par-dessus plutôt qu'après.
*Paramètres :*
* `id` -- handle de vidéo venant de `videoLoad`.
* `x`, `y` -- point d'ancrage en coordonnées de l'overlay.
* `x2`, `y2` -- coin opposé du rectangle de destination, inclus.
* `centered` -- booléen. `true` place le centre de l'image dessinée en `(x, y)`, `false` son coin supérieur gauche. Utilisez `true` pour les vidéos tournées.
*Remarques :* Une vidéo qui n'est pas dessinée n'avance pas : la pause est donc implicite tant qu'un clip est caché. Ne dessinez que depuis `onOverlayUpdate`.
*Depuis :* 2.00. La forme mise à l'échelle et tournée, avec l'indicateur `centered`, a été ajoutée en 2.10.
*Voir aussi :* <<videoload,videoLoad>>, <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<spritedraw,spriteDraw>>
.Exemple
[source,lua]
----
-- Clip du mode attraction dans un cadre fixe, qui boucle entre ses propres images de début et de fin.
function onOverlayUpdate()
overlayClear()
videoDraw(attractVideo, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W - 1, VIDEO_Y + VIDEO_H - 1)
if videoGetFrame(attractVideo) > ATTRACT_END then
videoSeek(attractVideo, ATTRACT_START)
end
return OVERLAY_UPDATED
end
----
[#videogetaudiotrack]
==== videoGetAudioTrack
[source,text]
----
track = videoGetAudioTrack(id)
----
Renvoie l'indice, à partir de zéro, de la piste audio que joue une vidéo chargée. `videoLoad` choisit la piste désignée par `--audio` ou par l'entrée `AUDIO_TRACK` de `games.dat` (par défaut `0`) quand le fichier a autant de pistes ; une vidéo sans aucun audio renvoie `-1`.
*Renvoie :* indice de piste entier.
*Depuis :* 2.10
*Voir aussi :* <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
.Exemple
[source,lua]
----
-- Afficher dans l'overlay des options quel doublage la cinématique utilise.
function drawAudioOption()
local track = videoGetAudioTrack(cutscene)
local label = "None"
if track >= 0 then
label = videoGetLanguageDescription(videoGetLanguage(cutscene, track))
end
overlayPrint(OPTION_X, OPTION_Y, "Cutscene audio: " .. label)
end
----
[#videogetaudiotracks]
==== videoGetAudioTracks
[source,text]
----
count = videoGetAudioTracks(id)
----
Renvoie le nombre de flux audio que contient une vidéo chargée. Un clip muet renvoie `0` ; protégez-en les boucles et l'arithmétique modulo.
*Renvoie :* nombre entier, `0` ou plus.
*Depuis :* 2.10
*Voir aussi :* <<videogetlanguage,videoGetLanguage>>, <<videosetaudiotrack,videoSetAudioTrack>>, <<videogetaudiotrack,videoGetAudioTrack>>
.Exemple
[source,lua]
----
-- N'offrir le menu des langues que si la cinématique propose vraiment un choix.
cutscene = videoLoad("videos/briefing.mkv")
if videoGetAudioTracks(cutscene) > 1 then
buildLanguageMenu(cutscene)
else
languageMenuEnabled = false
end
----
[#videogetframe]
==== videoGetFrame
[source,text]
----
frame = videoGetFrame(id)
----
Renvoie l'image sur laquelle une vidéo chargée est positionnée, à partir de zéro. La position ne change que pendant que la vidéo joue et est dessinée, et elle revient à `0` quand le clip boucle. Comparez-la à vos propres repères de début et de fin pour faire boucler une section, ou à `videoGetFrameCount` pour détecter la fin d'un clip joué une seule fois.
*Renvoie :* numéro d'image entier.
*Depuis :* 2.00
*Voir aussi :* <<videoseek,videoSeek>>, <<videogetframecount,videoGetFrameCount>>, <<videodraw,videoDraw>>
.Exemple
[source,lua]
----
-- Rendre la main au joueur quand le briefing a été joué une fois en entier.
function onOverlayUpdate()
if briefingRunning then
videoDraw(briefing, 0, 0, false)
if videoGetFrame(briefing) >= videoGetFrameCount(briefing) - 1 then
briefingRunning = false
videoPause(briefing)
end
end
return OVERLAY_UPDATED
end
----
[#videogetframecount]
==== videoGetFrameCount
[source,text]
----
count = videoGetFrameCount(id)
----
Renvoie le nombre total d'images d'une vidéo chargée, tel que compté dans la table des images du fichier lors de son indexation. Les images vont de `0` à `count - 1`. Servez-vous-en pour des barres de progression, pour reboucler, ou pour détecter qu'un clip joué une seule fois est presque fini.
*Renvoie :* nombre d'images entier.
*Depuis :* 2.00
*Voir aussi :* <<videogetframe,videoGetFrame>>, <<videoseek,videoSeek>>, <<videoisplaying,videoIsPlaying>>
.Exemple
[source,lua]
----
-- Ligne de progression sous un clip de chargement : un cadre en contour et un remplissage qui s'allonge.
function drawProgress()
local total = videoGetFrameCount(loadingClip)
local done = videoGetFrame(loadingClip) / (total - 1)
colorForeground(255, 255, 255, 255)
overlayBox(BAR_X - 1, BAR_Y - 1, BAR_X + BAR_W + 1, BAR_Y + 1)
overlayLine(BAR_X, BAR_Y, BAR_X + BAR_W * done, BAR_Y)
end
----
[#videogetheight]
==== videoGetHeight
[source,text]
----
height = videoGetHeight(id)
----
Renvoie la hauteur des images d'une vidéo chargée en pixels : la taille native du fichier, et non la taille à laquelle elle est dessinée. Combinez-la avec `videoScale` pour faire tenir un clip dans un cadre en gardant son format d'image.
*Renvoie :* hauteur entière en pixels.
*Depuis :* 2.00
*Voir aussi :* <<videogetwidth,videoGetWidth>>, <<videoscale,videoScale>>
.Exemple
[source,lua]
----
-- Faire tenir le clip dans une fenêtre de 320x240 sans le déformer.
local fitX = 320 / videoGetWidth(clip)
local fitY = 240 / videoGetHeight(clip)
local fit = math.min(fitX, fitY)
videoScale(clip, fit)
----
[#videogetlanguage]
==== videoGetLanguage
[source,text]
----
code = videoGetLanguage(id, track)
----
Renvoie l'étiquette de langue stockée dans le fichier pour une piste audio d'une vidéo chargée. L'étiquette est un code ISO 639 de trois lettres comme `"eng"` ou `"jpn"` ; une piste sans étiquette de trois lettres renvoie `"unk"`. Une piste hors de `0` à `videoGetAudioTracks(id) - 1` met fin au script.
*Paramètres :*
* `id` -- handle de vidéo.
* `track` -- indice de piste audio, à partir de zéro.
*Renvoie :* code de langue, une chaîne.
*Depuis :* 2.10
*Voir aussi :* <<videogetlanguagedescription,videoGetLanguageDescription>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videosetaudiotrack,videoSetAudioTrack>>
.Exemple
[source,lua]
----
-- Accorder le doublage de la cinématique à la langue choisie pour le disque.
local wanted = discGetLanguage(discGetAudioTrack())
for track = 0, videoGetAudioTracks(cutscene) - 1 do
if videoGetLanguage(cutscene, track) == wanted then
videoSetAudioTrack(cutscene, track)
break
end
end
----
[#videogetlanguagedescription]
==== videoGetLanguageDescription
[source,text]
----
name = videoGetLanguageDescription(code)
----
Cherche le nom anglais d'un code de langue. Contrairement aux autres fonctions `video*`, elle ne prend pas de handle ; c'est une simple consultation de table qui accepte les codes ISO 639-1 à deux lettres et les codes ISO 639-2 à trois lettres (les formes T comme B), comparés sans tenir compte de la casse. Les codes inconnus, y compris le `"unk"` renvoyé pour les pistes sans étiquette, donnent `"Unknown"`. `Framework.singe` lui donne l'alias `discGetLanguageDescription`, par symétrie avec l'API du disque.
*Paramètres :*
* `code` -- code de langue comme `"en"`, `"eng"` ou `"jpn"`.
*Renvoie :* nom de la langue, une chaîne, ou `"Unknown"`.
*Depuis :* 2.10
*Voir aussi :* <<videogetlanguage,videoGetLanguage>>, <<discgetlanguage,discGetLanguage>>
.Exemple
[source,lua]
----
-- Construire un menu de choix de la langue à partir des pistes d'une vidéo.
options = {}
for track = 0, videoGetAudioTracks(cutscene) - 1 do
local code = videoGetLanguage(cutscene, track)
local name = videoGetLanguageDescription(code)
table.insert(options, { track = track, label = name })
end
----
[#videogetvolume]
==== videoGetVolume
[source,text]
----
left, right = videoGetVolume(id)
volume = videoGetVolume()
----
Renvoie le volume stéréo d'une vidéo chargée sous forme de deux pourcentages. Une vidéo qui vient d'être chargée est au niveau de `--volume_nonvldp` sur les deux canaux (par défaut `100`). Notez que deux valeurs sont renvoyées, la gauche d'abord.
Appelée sans argument, c'est une extension d'Hypseus qui donne à la place le volume du laserdisc lui-même, sous la forme d'un seul nombre sur l'échelle de `0` à `64` d'Hypseus plutôt qu'en pourcentage. Ses jeux le doublent pour <<musicsetvolume,`musicSetVolume`>>, dont la plage va de `0` à `128`. Le niveau suit `--volume_vldp`.
*Renvoie :* deux entiers, gauche et droite, chacun de `0` à `100` ; ou, sans argument, un seul entier, de `0` à `64`.
*Depuis :* 2.00 ; la forme sans argument, pour le disque, 3.00
*Voir aussi :* <<videosetvolume,videoSetVolume>>, <<videoload,videoLoad>>
.Exemple
[source,lua]
----
-- Baisser la cinématique pendant une réplique, puis la rétablir.
function playVoiceOver(sound)
savedLeft, savedRight = videoGetVolume(cutscene)
videoSetVolume(cutscene, savedLeft / 4, savedRight / 4)
soundPlay(sound)
duckingCutscene = true
end
----
[#videogetwidth]
==== videoGetWidth
[source,text]
----
width = videoGetWidth(id)
----
Renvoie la largeur des images d'une vidéo chargée en pixels : la taille native du fichier, et non la taille à laquelle elle est dessinée.
*Renvoie :* largeur entière en pixels.
*Depuis :* 2.00
*Voir aussi :* <<videogetheight,videoGetHeight>>, <<videoscale,videoScale>>
.Exemple
[source,lua]
----
-- Centrer en haut de l'overlay un clip dessiné à sa taille native.
local x = (overlayGetWidth() - videoGetWidth(clip)) / 2
function onOverlayUpdate()
videoDraw(clip, x, 16, false)
return OVERLAY_UPDATED
end
----
[#videoisplaying]
==== videoIsPlaying
[source,text]
----
playing = videoIsPlaying(id)
----
Indique si une vidéo chargée est à l'état de lecture, c'est-à-dire si `videoPlay` a été appelé depuis le dernier `videoPause`. Une vidéo en lecture n'avance toujours que pendant qu'elle est dessinée. Les vidéos démarrent en pause après `videoLoad`.
*Renvoie :* booléen.
*Depuis :* 2.00
*Voir aussi :* <<videoplay,videoPlay>>, <<videopause,videoPause>>, <<videogetframe,videoGetFrame>>
.Exemple
[source,lua]
----
-- Une seule touche met en pause ou relance le clip incrusté.
function onInputPressed(what)
if what == SWITCH_BUTTON2 then
if videoIsPlaying(pipVideo) then
videoPause(pipVideo)
else
videoPlay(pipVideo)
end
end
end
----
[#videoload]
==== videoLoad
[source,text]
----
id = videoLoad(filename)
----
Ouvre un fichier vidéo et renvoie son handle. Tout conteneur et tout codec que le FFmpeg fourni sait décoder est accepté. Le moteur construit ou lit un index des images dans un dossier de données nommé d'après l'emplacement de la vidéo sous la racine des données de Singe, si bien que le premier chargement d'un gros fichier peut prendre un moment ; ne pas réussir à créer ce dossier met fin au script. La vidéo démarre en pause sur l'image `0`, avec sa piste audio choisie d'après `--audio` ou l'entrée `AUDIO_TRACK` de `games.dat` (par défaut `0`) quand le fichier a autant de pistes, et les deux canaux au niveau de `--volume_nonvldp`. Les images sont décodées en RVB pour pouvoir être dessinées sur l'overlay.
*Paramètres :*
* `filename` -- chemin du fichier vidéo, relatif au dossier du jeu ou à l'intérieur d'un jeu empaqueté.
*Renvoie :* handle de vidéo entier.
*Depuis :* 2.00
*Voir aussi :* <<videoplay,videoPlay>>, <<videodraw,videoDraw>>, <<videounload,videoUnload>>
.Exemple
[source,lua]
----
-- Charger le clip d'attraction du jeu choisi et le placer au début de sa boucle.
function loadGameAssets()
attractVideo = videoLoad(GAME_LIST[GAME_SELECTED].ATTRACT)
if GAME_LIST[GAME_SELECTED].AUDIO_TRACK then
videoSetAudioTrack(attractVideo, GAME_LIST[GAME_SELECTED].AUDIO_TRACK)
end
videoPlay(attractVideo)
videoSeek(attractVideo, GAME_LIST[GAME_SELECTED].ATTRACT_START)
end
----
[#videopause]
==== videoPause
[source,text]
----
videoPause(id)
----
Met en pause une vidéo chargée. Son horloge s'arrête, sa piste audio se met en pause et `videoDraw` continue de dessiner l'image sur laquelle elle s'est arrêtée. Mettre en pause une vidéo déjà en pause est sans danger.
*Depuis :* 2.00
*Voir aussi :* <<videoplay,videoPlay>>, <<videoisplaying,videoIsPlaying>>
.Exemple
[source,lua]
----
-- Figer le clip d'attraction pendant que l'écran de calibrage est affiché.
function calibrationBegin()
calibrating = true
if attractVideo then
videoPause(attractVideo)
end
end
----
[#videoplay]
==== videoPlay
[source,text]
----
videoPlay(id)
----
Démarre ou reprend une vidéo chargée à partir de son image actuelle. L'horloge repart de cette image, si bien qu'une vidéo reprise après une longue pause continue là où elle s'était arrêtée au lieu de sauter en avant, et la piste audio reprend avec elle. La vidéo n'avance que pendant qu'elle est dessinée avec `videoDraw` ou utilisée comme matériau.
*Depuis :* 2.00
*Voir aussi :* <<videopause,videoPause>>, <<videoseek,videoSeek>>, <<videodraw,videoDraw>>
.Exemple
[source,lua]
----
-- Lancer le briefing par-dessus le disque en pause.
function startBriefing()
discPause()
videoSeek(briefing, 0)
videoPlay(briefing)
briefingRunning = true
end
----
[#videoquality]
==== videoQuality
[source,text]
----
videoQuality(id, smooth)
----
Choisit le filtrage utilisé quand la forme à quatre arguments de `videoDraw` met la vidéo à l'échelle ou la fait tourner. Toute valeur non nulle choisit `RENDER_SMOOTH` (bilinéaire), zéro choisit `RENDER_PIXELATED` (au plus proche voisin), la valeur par défaut. Un changement marque l'image transformée pour qu'elle soit reconstruite au prochain dessin. Il n'a aucun effet sur la forme étirée à cinq arguments, qui utilise toujours le plus proche voisin, ni sur une vidéo non transformée.
*Paramètres :*
* `id` -- handle de vidéo.
* `smooth` -- `RENDER_PIXELATED` (`0`) ou `RENDER_SMOOTH` (`1`).
*Depuis :* 2.10
*Voir aussi :* <<videoscale,videoScale>>, <<videorotate,videoRotate>>, <<videodraw,videoDraw>>
.Exemple
[source,lua]
----
-- Un clip photographique réduit dans un coin rend mieux filtré.
pipVideo = videoLoad("videos/copilot.mkv")
videoScale(pipVideo, 0.4)
videoQuality(pipVideo, RENDER_SMOOTH)
videoPlay(pipVideo)
----
[#videorotate]
==== videoRotate
[source,text]
----
videoRotate(id, degrees)
----
Règle la rotation qu'applique la forme à quatre arguments de `videoDraw`, en degrés dans le sens horaire, selon la même convention que `spriteRotate`. L'angle est réduit modulo `360` et garde son signe : `450` et `90` sont donc identiques et `-90` fait tourner d'un quart de tour dans le sens antihoraire. L'échelle réglée par `videoScale` est conservée. La rotation se fait autour du centre de l'image : dessinez donc les vidéos tournées avec `centered` à `true`. Un angle modifié marque l'image transformée pour qu'elle soit reconstruite au prochain dessin.
*Paramètres :*
* `id` -- handle de vidéo.
* `degrees` -- rotation en degrés, dans le sens horaire.
*Depuis :* 2.10
*Voir aussi :* <<videorotateandscale,videoRotateAndScale>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
.Exemple
[source,lua]
----
-- Faire tourner lentement l'image du radar pendant qu'elle joue.
function onOverlayUpdate()
radarAngle = radarAngle + 0.5
videoRotate(radarVideo, radarAngle)
videoDraw(radarVideo, RADAR_CENTER_X, RADAR_CENTER_Y, true)
return OVERLAY_UPDATED
end
----
[#videorotateandscale]
==== videoRotateAndScale
[source,text]
----
videoRotateAndScale(id, degrees, scale)
videoRotateAndScale(id, degrees, scaleX, scaleY)
----
Règle la rotation et l'échelle en un seul appel, avec les mêmes règles que `videoRotate` et `videoScale` : des degrés dans le sens horaire réduits modulo `360`, et des facteurs d'échelle qui doivent être supérieurs à `0`. Utilisez-la quand les deux changent à chaque image, pour que l'image transformée soit reconstruite une fois plutôt que deux.
*Paramètres :*
* `id` -- handle de vidéo.
* `degrees` -- rotation en degrés, dans le sens horaire.
* `scale` -- facteur d'échelle uniforme, `1.0` pour la taille native.
* `scaleX`, `scaleY` -- facteurs horizontal et vertical séparés.
*Depuis :* 2.10
*Voir aussi :* <<videorotate,videoRotate>>, <<videoscale,videoScale>>, <<videodraw,videoDraw>>
.Exemple
[source,lua]
----
-- Un clip qu'on congédie s'en va en culbutant et en rapetissant.
function onOverlayUpdate()
if dismissing then
dismissTime = dismissTime + 1
videoRotateAndScale(popupVideo, dismissTime * 6, 1.0 - dismissTime / 60)
videoDraw(popupVideo, POPUP_CENTER_X, POPUP_CENTER_Y, true)
end
return OVERLAY_UPDATED
end
----
[#videoscale]
==== videoScale
[source,text]
----
videoScale(id, scale)
videoScale(id, scaleX, scaleY)
----
Règle l'échelle qu'applique la forme à quatre arguments de `videoDraw`, comme facteur de la taille native ; `1.0` laisse la taille inchangée. La rotation réglée par `videoRotate` est conservée. Une échelle modifiée marque l'image transformée pour qu'elle soit reconstruite au prochain dessin. Des facteurs de `0` ou moins ne peuvent pas être rendus et interrompent le moteur au prochain `videoDraw` : maintenez donc les échelles animées au-dessus de zéro. La forme étirée à cinq arguments de `videoDraw` ignore ce réglage.
*Paramètres :*
* `id` -- handle de vidéo.
* `scale` -- facteur d'échelle uniforme, supérieur à `0`.
* `scaleX`, `scaleY` -- facteurs horizontal et vertical séparés, chacun supérieur à `0`.
*Depuis :* 2.10
*Voir aussi :* <<videorotateandscale,videoRotateAndScale>>, <<videoquality,videoQuality>>, <<videogetwidth,videoGetWidth>>
.Exemple
[source,lua]
----
-- Montrer la cinématique au quart de sa taille dans le coin pendant que la partie continue.
pipVideo = videoLoad("videos/copilot.mkv")
videoScale(pipVideo, 0.25)
videoPlay(pipVideo)
function onOverlayUpdate()
videoDraw(pipVideo, PIP_X, PIP_Y, false)
return OVERLAY_UPDATED
end
----
[#videoseek]
==== videoSeek
[source,text]
----
videoSeek(id, frame)
----
Place une vidéo chargée sur `frame` et y fait repartir son horloge. L'état de lecture ou de pause ne change pas : une vidéo en lecture continue à partir de la nouvelle image à son prochain dessin, une vidéo en pause l'affiche. Le numéro d'image est ramené modulo le nombre d'images : `-1` est donc la dernière image, et une valeur au-delà de la fin retombe dans le clip.
*Paramètres :*
* `id` -- handle de vidéo.
* `frame` -- image cible, à partir de zéro.
*Depuis :* 2.00
*Voir aussi :* <<videogetframe,videoGetFrame>>, <<videogetframecount,videoGetFrameCount>>, <<videoplay,videoPlay>>
.Exemple
[source,lua]
----
-- Faire boucler le clip d'attraction entre les deux repères de games.dat.
function onOverlayUpdate()
videoDraw(attractVideo, VIDEO_X, VIDEO_Y, VIDEO_X + VIDEO_W - 1, VIDEO_Y + VIDEO_H - 1)
if videoGetFrame(attractVideo) > GAME_LIST[GAME_SELECTED].ATTRACT_END then
videoSeek(attractVideo, GAME_LIST[GAME_SELECTED].ATTRACT_START)
end
return OVERLAY_UPDATED
end
----
[#videosetaudiotrack]
==== videoSetAudioTrack
[source,text]
----
videoSetAudioTrack(id, track)
----
Fait passer une vidéo chargée sur une autre de ses pistes audio. L'audio en attente de l'ancienne piste est abandonné et la lecture se réaligne, si bien que le changement s'entend presque aussitôt. Choisir la piste déjà utilisée ne fait rien. Une piste hors de `0` à `videoGetAudioTracks(id) - 1` met fin au script, ce qui, pour un clip muet, veut dire n'importe quelle valeur.
*Paramètres :*
* `id` -- handle de vidéo.
* `track` -- indice de piste audio, à partir de zéro.
*Depuis :* 2.10
*Voir aussi :* <<videogetaudiotrack,videoGetAudioTrack>>, <<videogetaudiotracks,videoGetAudioTracks>>, <<videogetlanguage,videoGetLanguage>>
.Exemple
[source,lua]
----
-- Le menu enregistre une piste audio par jeu pour son clip d'attraction.
attractVideo = videoLoad(GAME_LIST[GAME_SELECTED].ATTRACT)
local wanted = GAME_LIST[GAME_SELECTED].AUDIO_TRACK
if wanted and wanted < videoGetAudioTracks(attractVideo) then
videoSetAudioTrack(attractVideo, wanted)
end
videoPlay(attractVideo)
----
[#videosetvolume]
==== videoSetVolume
[source,text]
----
videoSetVolume(id, left, right)
videoSetVolume(volume)
----
Règle le volume stéréo d'une vidéo chargée en pourcentages. Les valeurs sont ramenées entre `0` et `100` sans protester. Le réglage est mémorisé même pour un clip muet, et `videoGetVolume` le renvoie.
Appelée avec un seul argument, c'est une extension d'Hypseus qui règle à la place le volume du laserdisc lui-même, sur l'échelle de `0` à `64` d'Hypseus plutôt qu'en pourcentage : le pendant de <<videogetvolume,`videoGetVolume()`>> sans argument. Un niveau hors de `0` à `64` est ignoré au lieu d'être fatal, comme le fait Hypseus.
*Paramètres :*
* `id` -- handle de vidéo.
* `left`, `right` -- volumes des canaux, de `0` (muet) à `100` (plein).
* `volume` -- le volume du laserdisc, de `0` (muet) à `64` (plein).
*Depuis :* 2.00
*Voir aussi :* <<videogetvolume,videoGetVolume>>, <<discaudio,discAudio>>
.Exemple
[source,lua]
----
-- Éteindre l'audio de la cinématique en moins d'une demi-seconde quand la main revient au joueur.
function onOverlayUpdate()
if fadingOut then
fadeLevel = fadeLevel - 4
videoSetVolume(cutscene, fadeLevel, fadeLevel)
if fadeLevel <= 0 then
fadingOut = false
videoPause(cutscene)
end
end
return OVERLAY_UPDATED
end
----
[#videounload]
==== videoUnload
[source,text]
----
videoUnload(id)
----
Arrête le décodeur et libère tout ce que détient une vidéo chargée : décodeur, piste audio, images en cache et surface transformée. Le handle n'est plus valide ensuite, et le passer à une fonction `video*` met fin au script : effacez donc la variable qui le contenait. Les vidéos encore chargées quand le script se termine sont libérées par le moteur.
*Paramètres :*
* `id` -- handle de vidéo.
*Depuis :* 2.00
*Voir aussi :* <<videoload,videoLoad>>, <<videopause,videoPause>>
.Exemple
[source,lua]
----
-- Abandonner les ressources du jeu choisi avant de charger le suivant.
function unloadGameAssets()
if attractVideo then
videoUnload(attractVideo)
attractVideo = nil
end
end
----
[#vldp]
=== VLDP
L'espace de noms `vldp*` est antérieur à Singe 2.00 et existe pour que les anciens scripts continuent de tourner. `vldpGetHeight` et `vldpGetWidth` sont les mêmes liaisons que `discGetHeight` et `discGetWidth`, `vldpSetVerbose` ne fait rien, et `vldpGetPixel` est le seul membre sans équivalent moderne : il lit un pixel de l'image actuelle du disque en coordonnées de l'overlay et reste la façon habituelle de détecter les impacts sur de la vidéo préenregistrée. Sans disque, il renvoie du noir (voir <<withoutadisc,Jeux sans disque>>).
WARNING: *Hérité.* Préférez <<discgetheight,discGetHeight>> et <<discgetwidth,discGetWidth>> dans le nouveau code. `vldpGetPixel` reste d'actualité.
Hypseus Singe a ajouté onze autres membres, gardés sous ce préfixe pour que ses jeux tournent sans modification : `vldpFocusArea` et `vldpResetFocus` recadrent l'image du disque, `vldpGetScale` et `vldpSetScale` donnent et changent le facteur d'échelle de l'affichage, `vldpGetRotate` et `vldpSetRotate` font de même pour la rotation de la présentation, `vldpGetYUVPixel` lit les échantillons bruts, et `vldpFlash`, `vldpSetBlend`, `vldpSetLuma` et `vldpSetMonochrome` changent l'aspect de l'image elle-même. Chacun est marqué « Extension Hypseus » ci-dessous. L'échelle et la rotation sont deux des données qu'utilise xref:Manual.adoc#videorect[Le rectangle vidéo].
Les quatre réglages de l'image agissent sur l'image décodée, sur ses plans de luminance et de chrominance, avant qu'elle n'atteigne l'écran : `vldpSetBlend` et `vldpSetLuma` sont appliqués l'un après l'autre à mesure que chaque image est transférée, `vldpSetMonochrome` remplace la chrominance, et `vldpFlash` remplace carrément l'image le temps d'une image. Ils coûtent un passage sur le plan de luminance par image tant qu'ils sont actifs et rien du tout quand ils sont désactivés, ils laissent `vldpGetPixel` et `vldpGetYUVPixel` lire l'image telle qu'elle a été décodée, et avec un framefile ils suivent la lecture d'un segment à l'autre. Tous quatre se désactivent quand le script se recharge (sauf le monochrome, qui revient à ce que demandait `--monochrome`), et aucun ne touche une vidéo dessinée avec `videoDraw`.
[#vldpflash]
==== vldpFlash
[source,text]
----
vldpFlash()
----
Extension Hypseus. Met une image blanche sur le disque, comme le flash d'un vrai lecteur de laserdisc pendant une recherche. Elle apparaît aussitôt et la mise à jour suivante du disque la retire, si bien qu'elle dure une image dessinée, que le disque soit en lecture, en pause ou arrêté.
Ce blanc est la valeur que flashe Hypseus, 90 % de luminance avec une chrominance neutre, et non un blanc pur : c'est ce qu'est réellement le blanc d'un signal vidéo. L'overlay, les GUI et l'illustration du bezel sont dessinés par-dessus comme d'habitude, si bien qu'un affichage tête haute reste lisible à travers le flash.
Sans disque, ou sur un disque dont la première image n'a pas encore été décodée, l'appel ne fait rien.
*Paramètres :* aucun.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<discsearch,discSearch>>, <<vldpsetluma,vldpSetLuma>>
.Exemple
[source,lua]
----
-- Souligner un coup au but.
vldpFlash()
----
[#vldpfocusarea]
==== vldpFocusArea
[source,text]
----
vldpFocusArea(x, y, width, height)
vldpFocusArea()
----
Extension Hypseus. N'affiche qu'un rectangle de l'image du disque, donné en pixels du disque, étiré dans l'espace qu'occupe normalement l'image entière, avec la mise à l'échelle lissée habituelle du moteur de rendu ; les proportions suivent ce que donnent `width` et `height`. Le rectangle est maintenu à l'intérieur de l'image, et un rectangle entièrement hors de celle-ci met fin au script. Sans arguments, l'image entière est de nouveau affichée (Hypseus ignore cet appel et utilise `vldpResetFocus` ; Singe accepte les deux). Le recadrage n'est qu'un effet d'affichage : `vldpGetPixel`, `vldpGetYUVPixel`, la projection de la souris et le disque sur un matériau 3D gardent tous les coordonnées non recadrées, si bien que la détection des impacts sur une image agrandie doit être mise à l'échelle par l'appelant. Sans disque, l'appel ne fait rien. Remis à zéro quand le script se recharge.
*Paramètres :*
* `x`, `y` -- coin supérieur gauche en pixels du disque.
* `width`, `height` -- taille du rectangle en pixels du disque.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpresetfocus,vldpResetFocus>>, <<vldpgetwidth,vldpGetWidth>>, <<vldpgetpixel,vldpGetPixel>>
.Exemple
[source,lua]
----
-- Zoomer sur les 80 % centraux de l'image pendant la poursuite.
local w, h = vldpGetWidth(), vldpGetHeight()
vldpFocusArea(w * 0.1, h * 0.1, w * 0.8, h * 0.8)
function onChaseEnded()
vldpFocusArea()
end
----
[#vldpgetheight]
==== vldpGetHeight
[source,text]
----
height = vldpGetHeight()
----
Renvoie la hauteur de la vidéo du disque en pixels, ou la hauteur du canevas sans disque. C'est la même liaison que `discGetHeight`, enregistrée sous son nom d'avant 2.00.
*Renvoie :* hauteur entière en pixels.
*Remarques :*
WARNING: *Alias hérité.* Utilisez <<discgetheight,discGetHeight>> dans le nouveau code.
*Depuis :* 1.x
*Voir aussi :* <<discgetheight,discGetHeight>>, <<vldpgetwidth,vldpGetWidth>>
.Exemple
[source,lua]
----
-- Un script 1.x qui dimensionne son overlay d'après le disque.
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
centerX = vldpGetWidth() / 2
centerY = vldpGetHeight() / 2
----
[#vldpgetpixel]
==== vldpGetPixel
[source,text]
----
r, g, b = vldpGetPixel(x, y)
----
Lit la couleur d'un pixel de l'image du disque actuellement à l'écran. Les coordonnées sont des coordonnées de l'overlay : le moteur les divise par le rapport d'échelle entre l'overlay et la vidéo et les tronque, si bien que vous passez les mêmes nombres que ceux avec lesquels vous dessinez. Renvoie `0, 0, 0` quand il n'y a pas de disque, quand aucune image n'a encore été décodée, ou quand les coordonnées mises à l'échelle tombent hors de la vidéo. Les jeux qui s'en servent marquent en général les cibles dans la vidéo source d'une couleur distinctive et testent le pixel sous le réticule quand on appuie sur la gâchette, au lieu de créer des boîtes de collision pour chaque image.
*Paramètres :*
* `x`, `y` -- coordonnées de l'overlay ; les valeurs fractionnaires sont acceptées.
*Renvoie :* trois entiers, rouge, vert et bleu, chacun de `0` à `255`.
*Depuis :* 1.x
*Voir aussi :* <<discgetframe,discGetFrame>>, <<mousegetposition,mouseGetPosition>>, <<overlaysetresolution,overlaySetResolution>>
.Exemple
[source,lua]
----
-- Détection d'un tir au pistolet optique : les cibles sont peintes en magenta pur sur les images des ennemis.
function onInputPressed(what)
if what == SWITCH_BUTTON3 then
local x, y = mouseGetPosition(0)
local r, g, b = vldpGetPixel(x, y)
if r > 200 and g < 50 and b > 200 then
scoreHit()
else
scoreMiss()
end
end
end
----
[#vldpgetrotate]
==== vldpGetRotate
[source,text]
----
degrees = vldpGetRotate()
----
Extension Hypseus. Renvoie la rotation de la présentation en degrés dans le sens horaire : l'option `--rotate`, ou ce que `vldpSetRotate` a réglé en dernier, et `0` quand ni l'une ni l'autre ne l'a fait. Seuls `0`, `90`, `180` et `270` peuvent être renvoyés, car ce sont les seuls angles selon lesquels Singe tourne.
*Renvoie :* entier, `0`, `90`, `180` ou `270`.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpsetrotate,vldpSetRotate>>, <<vldpgetscale,vldpGetScale>>
.Exemple
[source,lua]
----
-- Une borne en mode portrait veut son panneau des scores le long du grand côté.
if vldpGetRotate() == 90 or vldpGetRotate() == 270 then
layoutPanel(PANEL_TALL)
else
layoutPanel(PANEL_WIDE)
end
----
[#vldpgetscale]
==== vldpGetScale
[source,text]
----
scale = vldpGetScale()
----
Extension Hypseus. Renvoie le facteur d'échelle de l'affichage en pourcentage : l'option `--scalefactor`, ou ce que `vldpSetScale` a appliqué en dernier, et `100` quand rien n'a été donné. Les jeux Hypseus divisent `100` par cette valeur pour adapter la géométrie de leur overlay à l'image réduite. Elle n'a rien à voir avec `vldpFocusArea` ni avec la résolution de l'overlay.
*Renvoie :* entier, de `25` à `100`.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpsetscale,vldpSetScale>>, <<vldpfocusarea,vldpFocusArea>>, <<overlaygetwidth,overlayGetWidth>>
.Exemple
[source,lua]
----
-- Code d'un jeu Hypseus : les viseurs suivent l'échelle de l'image.
if vldpGetScale ~= nil then
gunScale = 100 / vldpGetScale()
else
gunScale = 1
end
----
[#vldpgetshift]
==== vldpGetShift
[source,text]
----
x, y = vldpGetShift()
----
Le décalage de l'image dans la fenêtre, en pourcentages : les options `--shiftx`
et `--shifty`, ou ce que <<vldpsetshift,vldpSetShift>> a appliqué en dernier, et
`0, 0` quand rien n'a été donné. À l'extrême, un décalage remplit exactement la
place qu'a laissée `--scalefactor`, si bien que `100` avec l'échelle à `100` ne
déplace rien.
*Renvoie :* deux entiers, chacun de `-100` à `100`.
*Depuis :* 3.00.
*Voir aussi :* <<vldpsetshift,vldpSetShift>>, <<vldpgetscale,vldpGetScale>>
.Exemple
[source,lua]
----
-- Indiquer où l'exploitant a placé l'image.
local x, y = vldpGetShift()
fontPrint(10, 10, string.format("shift %d, %d", x, y))
----
[#vldpgetwidth]
==== vldpGetWidth
[source,text]
----
width = vldpGetWidth()
----
Renvoie la largeur de la vidéo du disque en pixels, ou la largeur du canevas sans disque. C'est la même liaison que `discGetWidth`, enregistrée sous son nom d'avant 2.00.
*Renvoie :* largeur entière en pixels.
*Remarques :*
WARNING: *Alias hérité.* Utilisez <<discgetwidth,discGetWidth>> dans le nouveau code.
*Depuis :* 1.x
*Voir aussi :* <<discgetwidth,discGetWidth>>, <<vldpgetheight,vldpGetHeight>>
.Exemple
[source,lua]
----
-- Un script 1.x qui dimensionne son overlay d'après le disque.
overlaySetResolution(vldpGetWidth(), vldpGetHeight())
centerX = vldpGetWidth() / 2
centerY = vldpGetHeight() / 2
----
[#vldpgetyuvpixel]
==== vldpGetYUVPixel
[source,text]
----
y, u, v = vldpGetYUVPixel(x, y)
----
Extension Hypseus. Lit les échantillons bruts d'un pixel de l'image du disque actuellement affichée, avant toute conversion de couleur : la luminance et les deux échantillons de chrominance, chacun de `0` à `255`, la chrominance étant à la résolution 4:2:0 de l'image décodée (le neutre est `128`). Les coordonnées sont des coordonnées de l'overlay, mises à l'échelle et tronquées exactement comme le fait `vldpGetPixel`. Renvoie `-1, -1, -1` quand il n'y a pas de disque, quand aucune image n'a encore été décodée, ou quand les coordonnées mises à l'échelle tombent hors de la vidéo. Insensible à `vldpFocusArea` et à `vldpSetMonochrome`. Moins coûteux que `vldpGetPixel` quand seule la luminosité compte.
*Paramètres :*
* `x`, `y` -- coordonnées de l'overlay ; les valeurs fractionnaires sont acceptées.
*Renvoie :* trois entiers, `Y`, `U` et `V`, chacun de `0` à `255`, ou `-1, -1, -1`.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpgetpixel,vldpGetPixel>>, <<discgetframe,discGetFrame>>
.Exemple
[source,lua]
----
-- La route est sombre ; tout ce qui est clair sous la voiture est un obstacle.
function carHitsSomething()
local y = vldpGetYUVPixel(carX + carWidth / 2, carY + carHeight)
return y >= 120
end
----
[#vldpresetfocus]
==== vldpResetFocus
[source,text]
----
vldpResetFocus()
----
Extension Hypseus. Affiche de nouveau l'image entière du disque après `vldpFocusArea`, exactement comme le fait `vldpFocusArea()` sans arguments. C'est le nom qu'Hypseus donne lui-même à cette remise à zéro, conservé pour que ses jeux tournent sans modification.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpfocusarea,vldpFocusArea>>
.Exemple
[source,lua]
----
function onChaseEnded()
vldpResetFocus()
end
----
[#vldpsetblend]
==== vldpSetBlend
[source,text]
----
vldpSetBlend(enabled)
----
Extension Hypseus. Lisse l'image du disque verticalement : chaque échantillon de luminance devient la moyenne de lui-même et des échantillons situés juste au-dessus et juste au-dessous. C'est le filtre de mélange à trois coefficients propre à Hypseus, et il existe pour adoucir les bords durs d'un transfert entrelacé sans adoucir l'image horizontalement.
Les plans de chrominance sont laissés tels quels, si bien que le lissage change le détail de la luminosité et non la couleur. Le désactiver remet l'image exactement telle qu'elle a été décodée.
Sans disque, l'appel ne fait rien mais il est mémorisé, et prend donc effet si un disque est chargé plus tard. Un argument non booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpsetluma,vldpSetLuma>>, <<vldpsetmonochrome,vldpSetMonochrome>>
.Exemple
[source,lua]
----
-- Une option de l'écran de service pour un transfert bruité.
vldpSetBlend(SETTINGS.softenVideo)
----
[#vldpsetluma]
==== vldpSetLuma
[source,text]
----
vldpSetLuma(enabled [, level])
----
Extension Hypseus. Éclaircit ou assombrit l'image du disque. `level` va de 0 à 8, et 4, la valeur par défaut, ne change rien : l'écart à 4 est ajouté à chaque échantillon de luminance en huitièmes, si bien que 0 divise la luminosité par deux, que 8 l'augmente de moitié, et que les crans intermédiaires sont réguliers. Un niveau hors de 0 à 8 met fin au script.
Comme seule la luminance change, la couleur reste où elle est : monter le niveau éclaircit l'image sans délaver la couleur, ce que fait le réglage d'image d'un vrai lecteur et ce que ne ferait pas un multiplicateur de couleur. Le désactiver, ou demander le niveau 4, remet l'image exactement telle qu'elle a été décodée.
`vldpSetLuma(true)` sans niveau ne produit donc aucun effet visible, ce qui est aussi le comportement d'Hypseus.
Sans disque, l'appel ne fait rien mais il est mémorisé, et prend donc effet si un disque est chargé plus tard.
*Paramètres :*
* `enabled` -- booléen.
* `level` -- nombre facultatif ; de 0 à 8, 4 étant neutre. Par défaut 4.
*Renvoie :* rien.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpsetblend,vldpSetBlend>>, <<vldpsetmonochrome,vldpSetMonochrome>>, <<vldpflash,vldpFlash>>
.Exemple
[source,lua]
----
-- Une scène de nuit que le joueur ne distingue pas ; laissez-le l'éclaircir.
vldpSetLuma(true, 6)
----
[#vldpsetmonochrome]
==== vldpSetMonochrome
[source,text]
----
vldpSetMonochrome(enabled)
----
Extension Hypseus. N'affiche le disque qu'en luminance : les plans de chrominance sont remplacés par des échantillons neutres au moment où chaque image est transférée, si bien qu'un disque en pause change aussitôt. Avec un framefile, le réglage suit la lecture d'un segment à l'autre. `vldpGetPixel` et `vldpGetYUVPixel` continuent de renvoyer les échantillons de couleur, et les vidéos supplémentaires dessinées avec `videoDraw` ne sont pas touchées. Associez-le à `overlaySetMonochrome` pour un mode noir et blanc. Sans disque, l'appel ne fait rien ; remis à zéro quand le script se recharge. Un argument non booléen met fin au script.
*Paramètres :*
* `enabled` -- booléen.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<overlaysetmonochrome,overlaySetMonochrome>>, <<vldpgetpixel,vldpGetPixel>>
.Exemple
[source,lua]
----
-- Un commutateur DIP choisit entre la couleur et le noir et blanc.
vldpSetMonochrome(not dipChroma)
overlaySetMonochrome(not dipChroma)
----
[#vldpsetrotate]
==== vldpSetRotate
[source,text]
----
applied = vldpSetRotate(degrees)
----
Extension Hypseus. Fait tourner toute la présentation dans le sens horaire : le disque, l'overlay, les GUI, la scène 3D et les particules tournent ensemble, et les positions de la souris et des pistolets optiques sont tournées en sens inverse pour qu'un clic tombe toujours là où il est dessiné. Un quart de tour échange la forme de la zone présentée, si bien qu'une fenêtre en portrait est remplie au lieu de recevoir une bande cernée de noir.
Hypseus accepte n'importe quel angle de `0` à `359` ; Singe n'accepte que `0`, `90`, `180` et `270`, et répond `false` pour toute autre valeur au lieu de mettre fin au script. Un angle quelconque laisserait la projection de la souris et la bordure Sinden sans signification raisonnable, et aucun jeu de la bibliothèque Hypseus n'en demande.
La rotation coûte une cible de rendu de la taille de l'écran et une copie (blit) supplémentaire par image tant qu'elle n'est pas nulle, et rien du tout quand elle l'est. Remise à la valeur de `--rotate` quand le script se recharge.
*Paramètres :*
* `degrees` -- entier, `0`, `90`, `180` ou `270`.
*Renvoie :* booléen, vrai quand la rotation a été appliquée.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpgetrotate,vldpGetRotate>>, <<vldpsetscale,vldpSetScale>>
.Exemple
[source,lua]
----
-- Une section de shoot vertical sur un moniteur pivotant.
if not vldpSetRotate(90) then
debugPrint("This build will not turn the screen.")
end
----
[#vldpsetscale]
==== vldpSetScale
[source,text]
----
applied = vldpSetScale(percent)
----
Extension Hypseus. Réduit l'image autour de son centre à `percent` de la fenêtre, l'équivalent en cours de jeu de `--scalefactor`, et reconstruit le rectangle vidéo pour que le disque, l'overlay, les GUI, la scène 3D, les particules, l'éventuelle bordure Sinden et la projection de la souris le suivent tous. Les valeurs hors de `25` à `100` répondent `false` et ne changent rien, au lieu de mettre fin au script.
Les changements sont limités à un toutes les 15 millisecondes, comme Hypseus limite son propre zoom par touche maintenue ; un appel pendant cet intervalle répond `false`, si bien qu'un script qui pilote l'échelle depuis une touche maintenue doit continuer d'appeler jusqu'à ce que ça prenne. Remis à la valeur de `--scalefactor` quand le script se recharge.
*Paramètres :*
* `percent` -- entier, de `25` à `100`.
*Renvoie :* booléen, vrai quand l'échelle a été appliquée.
*Depuis :* 3.00, comme extension d'Hypseus Singe.
*Voir aussi :* <<vldpgetscale,vldpGetScale>>, <<vldpsetrotate,vldpSetRotate>>
.Exemple
[source,lua]
----
-- Dézoomer d'un cran chaque fois que le joueur appuie sur la touche de réglage.
function onInputPressed(input)
if input == SWITCH_BUTTON3 then
vldpSetScale(vldpGetScale() - 5)
end
end
----
[#vldpsetshift]
==== vldpSetShift
[source,text]
----
vldpSetShift(x, y)
----
Déplace l'image dans la fenêtre, l'équivalent en cours de jeu de `--shiftx` et
`--shifty`, et reconstruit le rectangle vidéo pour que le disque, l'overlay, les
GUI, la scène 3D, les particules, l'éventuelle bordure Sinden et la projection
de la souris le suivent tous. Les valeurs sont des pourcentages de la place
qu'a laissée `--scalefactor`, si bien qu'avec l'échelle à `100` il n'y a nulle
part où aller. Une valeur hors de `-100` à `100` met fin au script,
contrairement à <<vldpsetscale,vldpSetScale>>, qui répond `false`.
*Paramètres :*
* `x` -- entier, de `-100` à `100`.
* `y` -- entier, de `-100` à `100`.
*Renvoie :* rien.
*Depuis :* 3.00.
*Voir aussi :* <<vldpgetshift,vldpGetShift>>, <<vldpsetscale,vldpSetScale>>, <<singesavegeometry,singeSaveGeometry>>
.Exemple
[source,lua]
----
-- Décaler l'image vers la gauche chaque fois que l'exploitant appuie sur la touche.
function onInputPressed(input)
if input == SWITCH_LEFT then
local x, y = vldpGetShift()
vldpSetShift(math.max(-100, x - 1), y)
end
end
----
[#vldpsetverbose]
==== vldpSetVerbose
[source,text]
----
vldpSetVerbose(...)
----
Ne fait rien. Elle activait autrefois la journalisation dans le lecteur vidéo d'origine ; l'appel accepte n'importe quels arguments, écrit une ligne de trace et rend la main. La trace du moteur et celle du script sont désormais des options de ligne de commande (`--program` et `--trace`).
*Remarques :*
WARNING: *Non implémenté.* Conservé uniquement pour la rétrocompatibilité. Utilisez `--trace` sur la ligne de commande pour tracer les appels à l'API.
*Depuis :* 1.x
*Voir aussi :* <<debugprint,debugPrint>>, <<discgetstate,discGetState>>
.Exemple
[source,lua]
----
-- Code de démarrage 1.x porté. L'appel est ignoré ; lancez plutôt Singe avec --trace.
vldpSetVerbose(true)
overlaySetResolution(discGetWidth(), discGetHeight())
discSearch(TITLE_FRAME)
----
[#voice]
=== Voix
Les éléments à partir desquels se construit une discussion vocale, sans le réseau :
Opus pour rendre une voix assez petite pour être envoyée, puis la restituer, un
processeur qui nettoie le son d'un microphone avant l'envoi, et un tampon de gigue
pour jouer dans le bon ordre ce qui arrive en retard et dans le désordre. Envoyer
les paquets et trouver les autres joueurs est l'affaire du jeu lui-même, ou d'une
future version de Singe.
Les échantillons sont des chaînes de valeurs 16 bits, deux octets chacune, en
little-endian, telles que les donne <<micread,`micRead`>> ; avec deux canaux,
elles alternent gauche et droite. Une *trame* est une tranche de temps fixe :
20 millisecondes, soit 960 échantillons à 48000, est la trame habituelle pour une voix.
Chaque objet ici est un handle. <<voicefree,`voiceFree`>> en libère un ; tous
sont libérés quand le jeu se termine.
.Un microphone, nettoyé, encodé, puis décodé à nouveau
[source,lua]
----
micOpen()
local processor = voiceProcessorNew()
local encoder = opusEncoderNew()
local decoder = opusDecoderNew()
local pending = ""
function onOverlayUpdate()
pending = pending .. micRead()
while #pending >= 960 * 2 do
local frame = pending:sub(1, 960 * 2)
pending = pending:sub(960 * 2 + 1)
local cleaned, voiced = voiceProcess(processor, frame)
if voiced then
local packet = opusEncode(encoder, cleaned) -- voilà ce que vous envoyez
local heard = opusDecode(decoder, packet) -- et voilà ce que l'autre bout joue
end
end
return OVERLAY_UPDATED
end
----
[#jitterget]
==== jitterGet
[source,text]
----
packet, status = jitterGet(jitter [, span])
----
Le paquet qui doit être joué maintenant, et le tampon avancé d'un pas. Appelez-le
une fois par durée de trame, quoi qu'il soit arrivé.
*Paramètres :*
* `span` -- nombre, les échantillons à prendre ; le pas s'il n'est pas donné.
*Renvoie :* le paquet, ou `nil`, et un état : `"ok"`, `"missing"` (rien n'est
dû : passez `nil` à <<opusdecode,`opusDecode`>> pour combler le trou), ou
`"insertion"` (le tampon grandit pour absorber les paquets en retard : jouez un peu
de silence).
*Depuis :* 3.00.
[#jitternew]
==== jitterNew
[source,text]
----
jitter = jitterNew([step])
----
Un tampon de gigue : les paquets y entrent selon leur horodatage à mesure qu'ils
arrivent, et en ressortent dans l'ordre, un pas à la fois, avec un léger retard
pour qu'un paquet en retard passe encore.
*Paramètres :*
* `step` -- nombre, les échantillons dont avance chaque <<jitterget,`jitterGet`>> ;
`960` s'il n'est pas donné.
*Renvoie :* le handle du tampon de gigue.
*Depuis :* 3.00.
[#jitterput]
==== jitterPut
[source,text]
----
jitterPut(jitter, packet, timestamp [, span])
----
Un paquet qui est arrivé. Le tampon en garde sa propre copie.
*Paramètres :*
* `packet` -- chaîne, de 1 à 4000 octets.
* `timestamp` -- nombre, où commence le paquet, en échantillons : l'expéditeur les
compte, `0`, `960`, `1920`, et ainsi de suite.
* `span` -- nombre, les échantillons que couvre le paquet ; le pas s'il n'est pas donné.
*Depuis :* 3.00.
[#opusdecode]
==== opusDecode
[source,text]
----
samples = opusDecode(decoder, packet | nil)
----
Un paquet reconverti en échantillons. `nil` pour un paquet jamais arrivé demande à
Opus de combler le trou avec quelque chose de plausible, de la durée du dernier paquet.
*Renvoie :* les échantillons.
*Depuis :* 3.00.
[#opusdecodernew]
==== opusDecoderNew
[source,text]
----
decoder = opusDecoderNew([rate [, channels]])
----
*Paramètres :*
* `rate` -- nombre : 8000, 12000, 16000, 24000 ou 48000 ; `48000` s'il n'est pas donné.
* `channels` -- nombre, `1` ou `2` ; `1` s'il n'est pas donné.
*Renvoie :* le handle du décodeur.
*Depuis :* 3.00.
[#opusencode]
==== opusEncode
[source,text]
----
packet = opusEncode(encoder, samples)
----
Une trame d'échantillons en un paquet. Une trame représente 2,5 ; 5 ; 10 ; 20 ; 40 ou 60
millisecondes d'échantillons. Tout le reste est refusé.
*Renvoie :* le paquet, une chaîne.
*Depuis :* 3.00.
[#opusencodernew]
==== opusEncoderNew
[source,text]
----
encoder = opusEncoderNew([rate [, channels [, application]]])
----
*Paramètres :*
* `rate` -- nombre : 8000, 12000, 16000, 24000 ou 48000 ; `48000` s'il n'est pas donné.
* `channels` -- nombre, `1` ou `2` ; `1` s'il n'est pas donné.
* `application` -- chaîne : `"voip"` pour la parole, réglé pour être compris ;
`"audio"` pour la musique ; `"lowdelay"` pour le moins de retard possible, au prix
d'un peu de qualité. `"voip"` s'il n'est pas donné.
*Renvoie :* le handle de l'encodeur.
*Depuis :* 3.00.
[#opusencoderset]
==== opusEncoderSet
[source,text]
----
opusEncoderSet(encoder, bitrate [, complexity])
----
*Paramètres :*
* `bitrate` -- nombre, en bits par seconde ; `0` le laisse tel quel. Une voix est
claire de 16000 à 24000.
* `complexity` -- nombre, de `0` à `10` : la quantité de travail à consacrer à la qualité.
*Depuis :* 3.00.
[#voicefree]
==== voiceFree
[source,text]
----
voiceFree(handle)
----
Libère un encodeur ou un décodeur Opus, un processeur de voix ou un tampon de gigue.
*Depuis :* 3.00.
[#voiceprocess]
==== voiceProcess
[source,text]
----
cleaned, voiced = voiceProcess(processor, mic [, speaker])
----
Une trame du microphone, nettoyée : l'écho de ce que jouaient les haut-parleurs
retiré, le bruit de fond atténué, et le niveau égalisé.
*Paramètres :*
* `mic` -- chaîne, une trame d'échantillons venant du microphone.
* `speaker` -- chaîne, la même trame de ce qu'ont joué les haut-parleurs, s'il y a
lieu : les voix des autres joueurs, par exemple. C'est son écho qui est retiré.
*Renvoie :* la trame nettoyée, et `true` quand elle ressemble à une voix -- le moyen
de ne rien envoyer tant que personne ne parle.
*Depuis :* 3.00.
[#voiceprocessornew]
==== voiceProcessorNew
[source,text]
----
processor = voiceProcessorNew([frame [, rate [, tailMs]]])
----
*Paramètres :*
* `frame` -- nombre, les échantillons par appel ; `960` s'il n'est pas donné.
* `rate` -- nombre ; `48000` s'il n'est pas donné.
* `tailMs` -- nombre, l'écho le plus long à annuler, en millisecondes : combien de
temps un son résonne dans la pièce. `200` s'il n'est pas donné.
*Renvoie :* le handle du processeur.
*Depuis :* 3.00.
[#frameworkhelpers]
=== Fonctions utilitaires du framework
`Framework.singe` définit cinq petites fonctions utilitaires à lui, globales
comme le reste de l'API, dont se servent des jeux écrits pour des versions
antérieures. Du code nouveau peut leur préférer `lume` et `inspect`, parmi
<<_included_libraries,les bibliothèques incluses>>.
[cols="1,2",options="header"]
|===
| Fonction | Ce qu'elle fait
| `utilDeepCopy(value)` | Une copie de `value` jusqu'au bout : une table est copiée clé par clé, ses clés, ses valeurs et sa métatable copiées à leur tour ; tout le reste revient tel quel.
| `utilDump(value)` | `value` sous forme de texte à afficher. Une table devient `{ ["key"] = value,}`, les tables imbriquées entre leurs propres accolades ; tout le reste passe par `tostring`. Pour une table qui mérite d'être lue, `require("inspect")` fait mieux.
| `utilGetTableSize(t)` | Combien de clés contient `t`, toutes, pas seulement la séquence que mesure `#t`.
| `utilOffsetTable(source, offset)` | Une copie profonde d'une table de la forme de `SCANCODE`, `{ NAME = { name = NAME, value = N } }`, avec `offset` ajouté à chaque `value`.
| `utilTrim(s)` | `s` sans les blancs à ses deux extrémités.
|===
[#enginecallbacks]
=== Callbacks du moteur
Ce sont des fonctions que le script définit et que Singe appelle quand l'événement correspondant se produit ; toutes sont facultatives, et une fonction non définie est ignorée. Les callbacks d'entrée sont déclenchés à mesure que les événements sont lus, `onOverlayUpdate` s'exécute au plus une fois toutes les 15 millisecondes, et aucun d'eux ne s'exécute pendant que la touche pause tient le jeu figé (voir <<pausing,La pause>>). Une erreur dans un callback arrête le jeu avec une trace de la pile d'appels. Dans le modèle à fil d'exécution, `singeMain` remplace la plus grande partie de la logique par image (voir <<threaded,À fil d'exécution>>) : les autres callbacks sont toujours déclenchés, mais `Framework.singe` installe son propre `onOverlayUpdate` pour piloter la coroutine `singeMain`, si bien qu'un jeu à fil d'exécution définit `singeMain`, `onShutdown` et les callbacks d'entrée qu'il veut.
[#oncontrollermoved]
==== onControllerMoved
[source,text]
----
function onControllerMoved(axis, value, which)
end
----
Appelé pour chaque événement de mouvement d'axe d'une manette connectée, y compris les mouvements dans la zone morte, si bien qu'un stick sous un pouce le déclenche de nombreuses fois par seconde. Les directions au-delà de `SINGE_DEAD_ZONE` agissent aussi comme des contacts quand `controls.cfg` les affecte, si bien qu'un jeu qui n'a besoin que d'une direction tout ou rien peut affecter `GAMEPAD_0.AXIS_LEFT_X_L` et `GAMEPAD_0.AXIS_LEFT_X_R` et ignorer ce callback. La valeur est enregistrée pour `controllerGetAxis` avant que le callback ne s'exécute.
*Paramètres :*
* `axis` -- de `0` à `5`, selon les constantes `GAMEPAD_AXIS_*`.
* `value` -- position brute, de `-32768` à `32767` ; les gâchettes reposent à `0`.
* `which` -- indice de la manette, de `0` à `3`, l'indice qu'utilisent `controllerGetAxis` et les tables `GAMEPAD_N`.
*Voir aussi :* <<controllergetaxis,controllerGetAxis>>, <<controllergetbutton,controllerGetButton>>
.Exemple
[source,lua]
----
-- Direction analogique à partir du stick gauche de la première manette seulement.
function onControllerMoved(axis, value, which)
if which ~= 0 or axis ~= GAMEPAD_AXIS_LEFT_X then
return
end
if math.abs(value) < SINGE_DEAD_ZONE then
steering = 0
else
steering = value / 32768
end
end
----
[#oninputpressedoninputreleased]
==== onInputPressed / onInputReleased
[source,text]
----
function onInputPressed(what)
end
function onInputReleased(what)
end
----
Appelés quand une entrée logique s'enfonce ou se relâche. Ce qui arrive dépend du mode du clavier. En `MODE_NORMAL`, `what` est une valeur `SWITCH_*` : les touches, les boutons de manette, les directions d'axe au-delà de la zone morte, les boutons de la souris et la molette arrivent tous ainsi, traduits par `controls.cfg`, chaque appui une seule fois sans répétition des touches, et les entrées qui ne sont affectées à aucun contact sont ignorées. `SWITCH_PAUSE` n'arrive qu'après `singeSetPauseKeyEnabled(false)` ; sinon, le moteur fige le jeu sur cette touche et le script ne la voit jamais. En `MODE_FULL`, `what` est le keysym de la touche (la valeur du caractère, pas le scancode), les répétitions sont transmises, et les boutons de manette et de souris passent `0` ; chaque événement atteint aussi `onKeyPressed` et `onKeyReleased`, qui portent le scancode ou le code du bouton. Quand la touche pause fige le jeu, chaque contact que tient le script est d'abord relâché via `onInputReleased`, et au dégel tout ce qui est encore physiquement enfoncé est de nouveau appuyé, si bien qu'un jeu ne voit jamais l'état périmé d'un bouton. Les appuis dans la première seconde après le démarrage du script, ou après que la fenêtre a repris le focus, sont considérés comme hérités d'avant et avalés avec leur relâchement. Un contact n'arrive d'aucune commande : `SWITCH_MOUSE_DISCONNECT` est levé par le moteur quand une souris ou un pistolet optique est débranché, une fois comme un appui puis aussitôt une fois comme un relâchement, si bien qu'un jeu de tir au pistolet peut remarquer que son pistolet s'en va (voir xref:Manual.adoc#mousedevices[Souris, pistolets, et qui décide]).
*Voir aussi :* <<keyboardsetmode,keyboardSetMode>>, <<onkeypressedonkeyreleased,onKeyPressed>>, <<singesetpausekeyenabled,singeSetPauseKeyEnabled>>, <<controllergetbutton,controllerGetButton>>
.Exemple
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_COIN1 then
credits = credits + 1
soundPlay(coinClip)
elseif what == SWITCH_START1 and credits > 0 then
startGame()
elseif what == SWITCH_BUTTON3 then
fireShot(cursorX, cursorY)
elseif what == SWITCH_QUIT then
singeQuit()
end
end
function onInputReleased(what)
if what == SWITCH_BUTTON1 then
thrusting = false
end
end
----
[#onkeypressedonkeyreleased]
==== onKeyPressed / onKeyReleased
[source,text]
----
function onKeyPressed(keysym, scancode)
end
function onKeyReleased(keysym, scancode)
end
----
Appelés en `MODE_FULL` seulement, pour chaque touche qui s'enfonce ou se relâche, avec à la fois le keysym logique (le caractère que produit la touche dans la disposition actuelle) et le scancode physique (la position de la touche, à comparer avec la table `SCANCODE`). Les touches maintenues se répètent dans ce mode, ce que veut la saisie de texte. Les boutons de manette et de souris arrivent aussi, avec `keysym` à `0` et leur code `GAMEPAD_N` ou `MOUSE_N` comme `scancode`. Pour du texte, utilisez `keysym` ; pour des commandes où une touche sert de bouton, utilisez `scancode`, pour que l'affectation soit la même dans toutes les dispositions. Les affectations clavier des propres contacts du moteur n'agissent pas dans ce mode : un jeu en `MODE_FULL` doit donc fournir sa propre façon d'en sortir.
*Voir aussi :* <<keyboardsetmode,keyboardSetMode>>, <<keyboardgetmodifiers,keyboardGetModifiers>>, <<oninputpressedoninputreleased,onInputPressed>>
[#ontextinput]
==== onTextInput
[source,text]
----
function onTextInput(text)
end
----
Appelé en `MODE_FULL` seulement, avec le texte qu'a produit un appui de touche -- un caractère ou quelques-uns, en UTF-8 -- après application de l'état de Maj et de la disposition du clavier, si bien qu'une majuscule, un guillemet ou une lettre d'une disposition non anglaise arrive tel que la personne l'a tapé. `onKeyPressed` donne le même appui sous forme de son keysym sans Maj et de son scancode, et c'est lui qu'il faut utiliser pour les touches qui sont des commandes ; celui-ci est celui qu'il faut utiliser pour les touches qui sont du texte. Une GUI dont un champ de texte a le focus prend le texte en premier, et le script n'en entend rien.
*Depuis :* 3.00.
*Voir aussi :* <<onkeypressedonkeyreleased,onKeyPressed>>, <<keyboardsetmode,keyboardSetMode>>
.Exemple
[source,lua]
----
-- Initiales du meilleur score : les lettres comme texte ; Entrée et Retour arrière par position.
function onTextInput(text)
if text:match("^%a$") and #initials < 3 then
initials = initials .. text:upper()
end
end
function onKeyPressed(keysym, scancode)
if scancode == SCANCODE.RETURN.value then
finishInitials()
elseif scancode == SCANCODE.BACKSPACE.value then
initials = initials:sub(1, -2)
end
end
----
[#onmidimessage]
==== onMidiMessage
[source,text]
----
function onMidiMessage(status, data1, data2, bytes)
end
----
Appelé pour chaque message qui arrive sur le port d'entrée MIDI qu'a ouvert
<<midiopeninput,`midiOpenInput`>>, une fois par message, avec le reste des
entrées de l'image. Les trois nombres sont le message tel qu'un appareil l'envoie :
`status` porte le type dans ses quatre bits de poids fort et le canal dans ses
quatre bits de poids faible, en comptant à partir de `0`, si bien qu'une note sur
le premier canal arrive sous la forme `0x90`. `bytes` est le message entier sous forme
de chaîne, ce dont a besoin un message système exclusif et ce qu'il faut lire pour
un message de plus de trois octets.
Les messages qui arrivent quand aucun port n'est ouvert ne sont pas mis en file ;
rien de ce qui précède l'ouverture du port n'est transmis.
*Paramètres :*
* `status` -- nombre ; l'octet d'état, de `0x80` à `0xFF`.
* `data1` -- nombre ; le premier octet de données, ou `0` quand le message n'en a pas.
* `data2` -- nombre ; le second octet de données, ou `0` quand le message n'en a pas.
* `bytes` -- chaîne ; le message entier, quelle que soit sa longueur.
*Depuis :* 3.00.
*Voir aussi :* <<midiopeninput,midiOpenInput>>, <<midisend,midiSend>>
.Exemple
[source,lua]
----
-- Un clavier MIDI joue au jeu : le do central et les notes au-dessus sont les quatre boutons de tir.
local MIDI_NOTE_ON = 0x90
function onMidiMessage(status, data1, data2, bytes)
-- Le type est dans les quatre bits du haut ; le canal, dans les quatre du bas, est ignoré ici.
if (status & 0xF0) == MIDI_NOTE_ON and data2 > 0 then
local button = data1 - 60
if button >= 0 and button <= 3 then
fire(button)
end
end
end
----
[#onmousemoved]
==== onMouseMoved
[source,text]
----
function onMouseMoved(x, y, xRelative, yRelative, which)
end
----
Appelé pour chaque événement de mouvement de la souris, avec la position et le déplacement depuis l'événement précédent en coordonnées de l'overlay. En mode `MOUSE_SINGLE`, la position du pointeur de la fenêtre est convertie en coordonnées de l'overlay et `which` vaut toujours `0`. En mode `MOUSE_MANY`, le déplacement relatif de chaque périphérique est intégré en une position qui lui est propre, ramenée dans la zone de la vidéo, et `which` est l'indice du périphérique ; un périphérique absolu comme une tablette signale sa position projetée avec `xRelative` et `yRelative` à `0`. La position est enregistrée pour `mouseGetPosition` avant que le callback ne s'exécute, si bien qu'un jeu peut ignorer ce callback et interroger la souris à la place. Le dessin doit quand même attendre `onOverlayUpdate` : gardez ici la dernière position dans des variables globales et dessinez le curseur là-bas.
*Paramètres :*
* `x`, `y` -- position en coordonnées de l'overlay.
* `xRelative`, `yRelative` -- déplacement depuis l'événement précédent, en coordonnées de l'overlay.
* `which` -- indice de la souris, de `0` à `3`.
*Voir aussi :* <<mousegetposition,mouseGetPosition>>, <<mousesetmode,mouseSetMode>>, <<mousesetenabled,mouseSetEnabled>>
.Exemple
[source,lua]
----
cursorX = overlayGetWidth() // 2
cursorY = overlayGetHeight() // 2
function onMouseMoved(x, y, xRelative, yRelative, which)
cursorX = x
cursorY = y
end
function onOverlayUpdate()
overlayClear()
spriteDraw(reticle, cursorX, cursorY, true)
return OVERLAY_UPDATED
end
----
[#oncollision]
==== onCollision
[source,text]
----
function onCollision(nodeA, nodeB, x, y, z, speed)
end
----
Appelé une fois pour chaque nouveau contact entre deux corps après un pas de la physique, et plus jamais pour une paire qui reste en contact. Une caisse qui atterrit, une balle qui heurte la borne, la voiture du joueur qui accroche un mur. Les contacts avec des déclencheurs sont signalés par `onTrigger` à la place. Les événements d'un pas sont transmis ensemble après celui-ci, avant que l'image ne soit dessinée.
*Paramètres :*
* `nodeA`, `nodeB` -- les nœuds des deux corps.
* `x`, `y`, `z` -- où ils se sont touchés, en unités du monde.
* `speed` -- la vitesse à laquelle ils se sont rencontrés, en unités du monde par seconde.
*Depuis :* 3.00.
*Voir aussi :* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>
.Exemple
[source,lua]
----
-- Les atterrissages brutaux font du bruit là où ils se produisent.
function onCollision(nodeA, nodeB, x, y, z, speed)
if speed > 4 and (nodeA == crate or nodeB == crate) then
local channel = soundPlay(thudClip)
if channel >= 0 then
soundSetPosition(channel, x, y, z)
end
end
end
----
[#ontrigger]
==== onTrigger
[source,text]
----
function onTrigger(trigger, other, entered)
end
----
Appelé quand un corps entre dans un déclencheur créé avec `bodySetTrigger` ou en sort, une fois à l'entrée et une fois à la sortie, quelles que soient les formes en jeu (un maillage ou une forme composée qui touche en plusieurs endroits compte toujours pour un seul corps). Les déclencheurs ne poussent jamais rien : c'est donc la façon de remarquer qu'un corps franchit une porte, atteint un point de contrôle ou tombe dans une zone mortelle.
*Paramètres :*
* `trigger` -- le nœud du corps déclencheur.
* `other` -- le nœud qui y est entré ou en est sorti.
* `entered` -- `true` à l'entrée, `false` à la sortie.
*Depuis :* 3.00.
*Voir aussi :* <<bodysettrigger,bodySetTrigger>>, <<oncollision,onCollision>>
.Exemple
[source,lua]
----
function onTrigger(trigger, other, entered)
if trigger == finishLine and other == playerCar and entered then
lap = lap + 1
soundPlay(lapClip)
elseif trigger == pit and entered then
respawn(other)
end
end
----
[#onnavarrived]
==== onNavArrived
[source,text]
----
function onNavArrived(agent)
end
----
Appelé quand un agent de navigation atteint la cible que lui a donnée `navAgentMoveTo`, une fois par arrivée, après la mise à jour de la navigation de l'image. Envoyez l'agent ailleurs, ou jouez son animation d'attente. Un script qui préfère interroger peut appeler `navAgentIsArrived` à la place.
*Paramètres :*
* `agent` -- le handle venant de `navAgentNew`.
*Depuis :* 3.00.
*Voir aussi :* <<navagentmoveto,navAgentMoveTo>>, <<navagentisarrived,navAgentIsArrived>>
.Exemple
[source,lua]
----
-- Patrouiller entre des points de passage, indéfiniment.
function onNavArrived(agent)
local guard = guards[agent]
if guard then
guard.waypoint = guard.waypoint % #guard.route + 1
local next = guard.route[guard.waypoint]
navAgentMoveTo(agent, next.x, next.y, next.z)
end
end
----
[#onoverlayupdate]
==== onOverlayUpdate
[source,text]
----
function onOverlayUpdate()
return OVERLAY_UPDATED
end
----
Le seul callback où le dessin est permis. Singe l'appelle au plus une fois toutes les 15 millisecondes tant que le jeu n'est pas figé, en comptant à partir du début de l'appel précédent, si bien que le temps que prend le script lui-même est compris dans ces 15 millisecondes au lieu de s'y ajouter ; tout appel `overlay*`, `spriteDraw`, `videoDraw`, `fontPrint` ou similaire fait ailleurs n'apparaîtra pas à l'écran et risque de corrompre l'overlay. Renvoyez `OVERLAY_UPDATED` (`1`) quand l'overlay a changé, pour que Singe le compose de nouveau sur la vidéo, ou `OVERLAY_NOT_UPDATED` (`0`) quand rien n'a changé, pour que la composition précédente soit réutilisée ; ne rien renvoyer compte comme `OVERLAY_NOT_UPDATED`. L'affichage est de toute façon redessiné chaque fois qu'une nouvelle image vidéo arrive ou qu'une scène 3D est activée : la valeur renvoyée est donc une optimisation pour les écrans statiques. Après son retour, le moteur efface les valeurs derrière `keyboardGetLastDown` et `keyboardGetLastUp`. Dans le modèle à fil d'exécution, `Framework.singe` définit lui-même cette fonction pour reprendre `singeMain` et renvoie toujours `OVERLAY_UPDATED` ; ne la définissez pas en plus.
*Renvoie :* `OVERLAY_UPDATED` ou `OVERLAY_NOT_UPDATED`.
*Voir aussi :* <<singemain,singeMain>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<onshutdown,onShutdown>>
.Exemple
[source,lua]
----
function onOverlayUpdate()
if not hudDirty then
return OVERLAY_NOT_UPDATED
end
overlayClear()
fontPrint(20, 20, "SCORE " .. score)
fontPrint(20, 44, "LIVES " .. lives)
spriteDraw(reticle, cursorX, cursorY, true)
hudDirty = false
return OVERLAY_UPDATED
end
----
[#onshutdown]
==== onShutdown
[source,text]
----
function onShutdown()
end
----
Appelé une fois quand le script se termine, que ce soit par `singeQuit`, le contact de sortie, la fermeture de la fenêtre, `scriptExecute` ou `scriptPush`, après la dernière image et avant que l'état Lua ne soit abandonné. Il n'est pas appelé par `singeReload`. Enregistrez ici les réglages et libérez les sprites, les sons, les polices et les vidéos que le script a chargés ; le moteur arrête ensuite chaque son et libère ce qui reste, si bien qu'un handle oublié n'est pas une fuite, mais un déchargement explicite garde l'ordre d'arrêt sous le contrôle du script. N'appelez pas `singeQuit` depuis ici.
*Voir aussi :* <<singequit,singeQuit>>, <<soundunload,soundUnload>>, <<scriptpush,scriptPush>>
.Exemple
[source,lua]
----
function onShutdown()
saveHighScores()
spriteUnload(reticle)
fontUnload(hudFont)
soundUnload(shotClip)
soundUnload(coinClip)
end
----
[#onsoundcompleted]
==== onSoundCompleted
[source,text]
----
function onSoundCompleted(channel)
end
----
Appelé avec le numéro du canal chaque fois qu'un canal s'arrête : quand son clip se termine, quand la dernière boucle finit, et quand `soundStop` ou `soundFullStop` l'interrompt, si bien qu'un script qui enchaîne des sons depuis ici doit savoir quels canaux il a arrêtés lui-même. Le mélangeur note l'arrêt sur son propre fil d'exécution et le moteur transmet le callback sur le fil d'exécution du jeu entre deux images ; pendant que la touche pause tient le jeu figé, les avis attendent, et ils sont abandonnés quand un script se termine ou se recharge. Jusqu'à 64 fins sont mises en file entre deux images ; au-delà, celles d'une même image sont perdues.
*Paramètres :*
* `channel` -- le numéro de canal qu'a renvoyé l'appel à `soundPlay`.
*Voir aussi :* <<soundplay,soundPlay>>, <<soundstop,soundStop>>, <<soundisplaying,soundIsPlaying>>
.Exemple
[source,lua]
----
-- Jouer la liste de lecture musicale dans l'ordre, indéfiniment.
function onSoundCompleted(channel)
if channel == musicChannel and not musicStopped then
track = track % #playlist + 1
musicChannel = soundPlay(playlist[track])
end
end
----
[#singemain]
==== singeMain
[source,text]
----
function singeMain()
while true do
singeYield()
end
end
----
Le point d'entrée du modèle à fil d'exécution. Quand un script la définit, `Framework.singe` l'enveloppe dans une coroutine et installe un `onOverlayUpdate` qui reprend la coroutine une fois par image, si bien que la fonction se déroule comme une ligne droite de logique de jeu qui appelle `singeYield()` (un alias de `coroutine.yield`) partout où elle veut laisser passer une image. Tout ce qui est dessiné avant de céder la main atterrit sur l'overlay de cette image. Revenir de `singeMain` appelle `singeQuit` ; une erreur à l'intérieur arrête le jeu avec une trace de la pile d'appels. Les callbacks d'entrée, de son, de physique et de navigation sont toujours déclenchés entre les reprises, si bien qu'un jeu à fil d'exécution peut les mélanger avec de l'interrogation.
*Voir aussi :* <<onoverlayupdate,onOverlayUpdate>>, <<keyboardgetlastdown,keyboardGetLastDown>>, <<singequit,singeQuit>>
.Exemple
[source,lua]
----
function singeMain()
showTitle()
while credits == 0 do
singeYield()
end
for level = 1, #levels do
playLevel(levels[level])
if lives == 0 then
break
end
end
showGameOver()
end
----
[#_hypseus_compatibility]
=== Compatibilité avec Hypseus
IMPORTANT: Tout ce qui se trouve dans cette section existe pour qu'un jeu écrit pour Hypseus
Singe tourne sans modification. *Rien de tout cela n'est recommandé pour un nouveau jeu.*
Préférez l'appel Singe que nomme chaque entrée : c'est celui qui continuera de gagner des
fonctionnalités, et il ne traîne ni les échelles, ni les ordres d'arguments, ni les réponses
figées d'un autre moteur.
Hypseus Singe enregistre dans son moteur quelques noms auxquels Singe répond plutôt depuis
`Singe/Framework.singe`, pour qu'un jeu écrit pour Hypseus tourne sans modification.
Chacun n'est défini que si le moteur ne le fournit pas déjà, si bien qu'une future
implémentation dans le moteur l'emporte. `keyboardCatchQuit` est documenté
sous <<keyboard,Clavier>> ; `setOverlayLinearScale` et `setOverlayOnTop` sont de
vrais appels du moteur sous <<overlay,Overlay>> ; `mainBezelLoaded` et les sept appels
`scoreBezel` sont de vrais appels du moteur sous <<bezelref,Bezel>> ;
`getFValue`, `ratioGetX` et `ratioGetY` sont de vrais appels du moteur sous
<<commandlineref,Ligne de commande>> ; `joyMouseEnable` est un vrai appel du moteur
sous <<joystick,Joystick>> ; `allowSocketCall` et `getUserString` sont de vrais
appels du moteur sous <<singe,Singe>>, et `drawTransparent` sous <<sprite,Sprite>> ;
et toute la famille `music` est désormais un ensemble de vrais appels du moteur sous
<<musicref,Musique>>. Les six noms `spriteAnim` ci-dessous sont de
simples alias posés après l'enveloppement hérité des fonctions de sprite, si bien qu'ils
prennent l'ordre d'arguments qu'adopte le reste de la famille des sprites : celui
d'Hypseus (handle en dernier) avec `LEGACY_SPRITE_ARGS`, et celui de Singe 3.00
(handle en premier) sans.
[cols="1,1",options="header"]
|===
| Fonction | Fournie par Framework.singe sous la forme
| `setOverlaySize(preset [, width, height])` | `overlaySetResolution` avec une taille choisie : `1` est la taille de la vidéo du disque, `3` est `360x240`, `4` est la taille donnée (ignorée quand elle manque, comme dans Hypseus) et toute autre valeur, `2` compris, est la moitié du disque sur les deux axes. Sans disque, `discGetWidth` et `discGetHeight` donnent la taille du canevas, qui est ce dans quoi dessine un jeu sans disque.
| `setOverlayResolution(width, height)` | Un alias de <<overlaysetresolution,`overlaySetResolution`>>. Hypseus enregistre une même fonction sous les deux noms.
| `rewriteStatus()` | Fonction bouchon qui renvoie `false`. Dans Hypseus, elle indique si les chemins des ressources sont réécrits pour une installation `-espath` ; le système de fichiers virtuel de Singe trouve les fichiers d'un jeu là où ils ont été installés, si bien que rien n'est réécrit et que le jeu garde son propre dossier de ressources.
| `hypseusGetHeight()` | Un alias de <<singegetheight,`singeGetHeight`>>.
| `hypseusGetWidth()` | Un alias de <<singegetwidth,`singeGetWidth`>>.
| `takeScreenshot()` | Un alias de <<singescreenshot,`singeScreenshot`>>. Hypseus refuse une deuxième capture d'écran dans les cinq secondes ; Singe n'a pas une telle limite.
| `mouseHowManyReal()` | Un alias de <<mousehowmany,`mouseHowMany`>>. Hypseus compte les vraies souris à part de celles qu'il dérive d'une manette ; Singe n'a pas de souris dérivées, si bien que son compte est déjà celui des vraies.
| `spriteAnimGetFrame(spr)` | Un alias de <<spritegetframe,`spriteGetFrame`>>.
| `spriteAnimIsPlaying(spr)` | Un alias de <<spriteisplaying,`spriteIsPlaying`>>.
| `spriteAnimLoop(loop, spr)` | Un alias de <<spriteloop,`spriteLoop`>>.
| `spriteAnimPause(spr)` | Un alias de <<spritepause,`spritePause`>>.
| `spriteAnimPlay(spr)` | Un alias de <<spriteplay,`spritePlay`>>.
| `spriteAnimSetFrame(frame, spr)` | Un alias de <<spritesetframe,`spriteSetFrame`>>.
|===
[#unimplementedandlegacy]
=== Non implémenté et hérité
Une poignée de fonctions de l'API sont conservées pour la compatibilité des scripts, et celles marquées « Sans effet » ne font rien. On peut les appeler sans danger ; elles écrivent simplement un message de trace et rendent la main. Préférez les remplacements indiqués pour le nouveau code.
[cols="1,1,1",options="header"]
|===
| Fonction | État | Remplacement
| `discChangeSpeed` | Sans effet | -- (servait à la lecture à vitesse variable sur un vrai matériel LD)
| `discSearchBlanking` | Sans effet | -- (servait à masquer l'écran pendant un saut sur un vrai matériel LD)
| `discSkipBlanking` | Sans effet | -- (même raison)
| `discSetFPS` | Sans effet | La fréquence d'images est lue dans le fichier vidéo.
| `vldpGetHeight` | Fonctionne | <<discgetheight,`discGetHeight`>>
| `vldpGetWidth` | Fonctionne | <<discgetwidth,`discGetWidth`>>
| `vldpSetVerbose` | Sans effet | --
| `colorBackground(r,g,b)` | Fonctionne | Préférez la forme à 4 arguments, `colorBackground(r, g, b, a)`.
| `colorForeground(r,g,b)` | Fonctionne | Préférez la forme à 4 arguments, `colorForeground(r, g, b, a)`.
| `daphneGetWidth` / `daphneGetHeight` / `daphneScreenshot` | Alias dans `Framework.singe` | Préférez les équivalents `singe*`.
| `discPauseAtFrame` | Fonctionne (alias) | <<discsearch,`discSearch`>>.
|===
En outre, `Framework.singe` redéfinit `random = { new = math.random }` pour que les scripts d'avant 2.00 qui utilisent `random.new()` fonctionnent toujours. Le nouveau code doit utiliser `math.random` directement.