Nasal pour les nuls

Introduction

Nasal? c'est quoi?

C'est un langage de programmation[en] léger et fonctionnel, ce qui permet à FlightGear de l'embarquer dans son propre code source ($FGSOURCE/src/Scripting/*.?xx et $SGSOURCE/simgear/nasal/*.?xx). Il devient alors très simple d'ajouter de nouvelles fonctionnalités globalement (pour l'ensemble du simulateur) ou spécifiquement (pour un ou plusieurs objets, avions ou AI). Le tout sans avoir à recompiler FG et en apportant la souplesse du fichier texte (facile à lire/modifier/tester).
De très nombreuses fonctionnalités de votre avion favori sont certainement codées en nasal (radars, animations diverses, que sais-je encore?), et ceci non codé en dur mais dans un langage lisible, clair et compréhensible et dans la plupart des cas copiable, diffusable et modifiable à souhait1)
Je vais essayer ici de l'expliquer pour le rendre le plus simple possible (et au passage apprendre moi aussi).

Dit-on un script nasal et des scripts nasaux ?

Oui, normalement, mais dans FlightGear l'exception confirme la règle, nous dirons donc des scripts nasal ;-)

Où qu'il y a du nasal sur mon FG?

Il y en a un peu partout ;-) Mais il y a des endroits privilégiés (quand c'est bien rangé, on s'y retrouve toujours plus facilement ):

  • $FGROOT/Nasal : c'est LE répertoire où se trouvent les scripts nasal dédiés à l'ensemble du simulateur (mais pas uniquement),
  • $FGHOME/Nasal : là où vous placez vos propres créations et autres scripts nasal importés depuis le réseau vaste et sauvage,
  • $FGROOT/Aircraft/<nom_de_l_avion>/Nasal: les scripts dédiés à un avion en particulier,
  • ailleurs encore.

Je ne compouend pas bien le france, mais I speak fluide le anglais

Ça tombe bien, there is a dedicated web page just for you on the English wiki of FlightGear[en] (page dont je me suis largement inspiré ici, merci à Nelson M. pour son aide :-D)

Par où on commence

Les pré-requis

Une (bonne?) connaissance du système des propriétés de FlightGear est un plus non négligeable.

Un peu de syntaxe d'abord

Tout comme dans un forum, dans un courriel, une lettre, enfin à chaque fois qu'on écrit quelque chose qui doit être lu par quelqu'un d'autre, il faut faire attention à ce qu'on écrit et l'écrire de façon intelligible. Voici donc quelques règles à suivre pour que FlightGear comprenne bien ce que vous lui demandez de faire :

  • chaque ligne de code doit être terminée par un point-vigule (;),
  • nasal est sensible à la casse (ça ne veut pas dire qu'il est fragile mais que pour l'interpréteur la variable
    variable = 1;

    est différente de la variable

    Variable = 1;
  • il n'est pas possible d'utiliser une variable avant de la déclarer par un assignement avec l'opérateur ”=”,
    non_num = 10;
  • on peut utiliser le qualificatif “var” pour s'assurer que l'assignement se fait sur une variable locale, c'est une excellente habitude.
     ma_fonction = func {
        for(var mon_num = 0; mon_num < 100; mon_num += 1) {
            # ne fait pas de mic-mac avec la variable mon_num déclarée juste au-dessus.
        }
    }

nous en reparlerons un peu plus bas

  • les commentaires sont insérables dans le code en les faisant devancer du caractère dièse (#),
  • un bloc de commande s'ouvre avec une accolade ouvrante { et se ferme avec une accolade… fermante } mais les gurus savent que l'on peut s'en passer dans certains cas bien precis.
  • les paramètres d'une fonctions sont passés entre parenthèses et séparés par des virgules.

Avec quoi écrire un script?

Le plus simplement du monde avec un éditeur de texte des plus basiques. Inutile de sortir OpenOffice.org (ou équivalent pas beau) pour éditer un fichier texte de quelques dizaines d'octets. Voici une liste non-exhaustive:

Il existe également un éditeur intégré à FlightGear (menu Debug → Nasal Console) qui est présent sur 0.9.10 (personne n'en est vraiment sûr) et bien entendu sur la version CVS. Mais il faut vite se rendre à l'évidence, il n'est pas très commode pour éditer du texte. Pour palier à ce manque 2), il existe la possibilité d'éditer son script dans un éditeur (voir plus haut) et de le lancer dans FG après chaque modification en passant par la console (et donc sans avoir à relancer FG).
Méthodologie:

  1. éditez votre script Nasal avec votre éditeur favori, appelez-le “monscript.nas” et mettez-le dans $FGHOME/Nasal
  2. dans la console Nasal de FG entrez :
    io.load_nasal("/home/user/.fgfs/Nasal/monscript.nas")

    .
    Evidemment ”/home/user/.fgfs” est à remplacer par le chemin qui mène à $FGHOME.

Attention il faut que FG ait les droit en lecture sur le répertoire où se trouve votre code Nasal, de base il s'agit de $FGROOT/* et $FGHOME/*

  1. faites les modifs dans votre script,
  2. exécutez-le via la console,
  3. retour à l'étape 3 jusqu'à satisfaction complète ;-)

Un peu plus sérieusement

Les types de variables

“Une variable associe un nom (un symbole) à une valeur qui peut éventuellement varier au cours du temps. Plus précisément , une variable dénote une valeur.” Wikipedia France

En Nasal, les variables ne sont pas typées, elles peuvent donc contenir indifféremment du texte, un nombre ou une fonction. Elles doivent être déclarées, c'est-à-dire que lors de leur première utilisation, il faut indiquer au système qu'on veut créer une nouvelle variable. L'affectation d'une valeur à une variable s'effectue grâce au signe = (égal)

var montexte = "une phrase";
var monnombre = 12;
var mafonction = func { 
    print ( montexte ); 
    monnombre += 1;
}
mafonction ();

Ici le code crée trois variables dont une fonction, et exécute la variable-fonction mafonction () qui elle-même fait appel aux deux variables précédemment créées pour afficher le contenu de la variable montexte et ajouter 1 à la variable monnombre (qui désormais vaut 13).

Plus loin nous reparlerons plus précisément des fonctions.

Il est bien sûr très fortement déconseillé d'ajouter un nombre à une phrase, une phrase à un nombre ou un nombre à une fonction, cela ne donnerait pas du tout le résultat escompté (à moins de savoir ce que vous faites et encore si Nasal vous laisse seulement le faire ;-))

Il est impossible de donner un nom réservé à une fonction, par exemple

var var = 1;

va générer une erreur. Il est possible d'écrire

var mavariable = 1;
var mavariable = 2;

Il existe des valeurs réservées (on ne peut pas les modifier), la plus importante étant nil, qui signifie “rien, vide”, qui est utile pour faire des tests.

Il est également possible de stocker un “tableau” (appelé vecteur) ou un “hash”, dans ce cas les “cases” nécessaires seront automatiquement créées (auto-vivification).

var montableau = [3,2,1,0]; # crée un tableau de 4 cases et l'initialise avec les valeurs
if (montableau[1] == 2) print ("youpi!"); # le mot "youpi!" sera affiché
var montableau = [[5,6],[7,8]]; # crée un tableau à deux dimensions
if (montableau[0][1] == 6) print ("c'est énorme!"); # affichera le merveilleux texte

var monhash = {
    unefonctioninterne : func {
         if (arg[0] == 3) {  # variable tableau arg est spéciale, elle contient les paramètres passés à la fonction
             print ("c'est top");
             return;
        }
         if (arg[0] < 3) print ("c'est plus...");
         else print ("c'est moins...");
    },
     unefonction : func (valeur = 3) {
          me.unefonctioninterne (valeur); # le mot réservé "me." signifie "dans cette table de hashage"
          me.valeurinterne = valeur + 1;
     },
      uneautrefonction : func {
           if (me.valeurinterne > 10) {
                    me.valeurinterne = 1;
      },
       unevaleur : 108.55,
       uneautrevaleur : nil
};
monhash.unefonction(); # affichera "c'est top"
monhash.unefonction(5); # affichera "c'est moins..."

Il est aussi possible de créer des tableaux et tables de hashage vides

var montableau = []; # crée un tableau vide
var monhash = {}; # crée une table de hashage vide

le qualificatif "var"

Il est des mails et messages qui comptent dans le monde de l'informatique, celui de Melchior aura certainement sa portée aussi ;-)
Voici une traduction qui sera la meilleure explication: “La plupart des gens considèrent que le qualificatif var n'est qu'une sorte de décoration. Ce n'est pas le cas, et je considère comme cassé le code qui n'en comporte pas.
“var” assure que la variable déclarée est locale, cela va de même pour les fonctions déclarées par des variables. Si vous écrivez du code sans utiliser le “var” pour déclarer vos variables, alors vous risquez un bug plus tard qu'il sera très difficile de retrouver. Considérez ce bout de code:

output = func(v) { # missing "var"
state = getprop("/sim/foo/state"); # missing "var"
do_something(state, v);
}

dans lequel ni output, ni state n'ont été déclarées avec le qualificatif “var”. Tout va fonctionner normalement, jusqu'au jour où quelqu'un créera un fichier $FG_ROOT/Nasal/state.nas ou $FG_ROOT/Nasal/output.nas. Ce jour-là vos variables ne seront plus sûres et surtout vont outrepasser l'espace de nommage, et donc leurs fonctions associées, créé par les deux fichiers. Par exemple:

props = func { # missing "var"
print("I'm props. And I just wiped the whole props namespace.");
}

ou encore

foo = func { # missing "var"
props = " :-P "; # missing "var"
}
...
foo();

exécutez-les, et toutes les méthodes décrites dans $FG_ROOT/Nasal/props.nas (NdT: espace de nommage très important pour FG!!!) seront remplacées par [noms d'oiseaux] au lieu de se contenter d'être locaux. Notez-bien que ceci est également valable pour les variables contenant une fonction.

Il y a encore une raison pour laquelle le qualificatif “var” devrait être utilisé, même si on est certain qu'il n'y aura pas d'effet de bord (~ dégâts collatéraux). Le mot réservé “var” rend le code plus lisible et permet aux autres de le comprendre plus facilement (et donc de dénicher et corriger les éventuels bugs). Il indique clairement:“C'est ici que la variable débute sa vie de variable”

Les structures de test

Un programme qui exécute à la suite des commandes sans tenir compte de paramètres pour le diriger ou le limiter, peut être utile, mais peu intéressant. Il est souvent nécessaire d'indiquer au système quoi faire dans telle condition, ou quand s'arrêter, etc. Pour ce faire, il existe des structures de test

si condition est vrai
alors fais ça
sinon fais otechose

en Nasal, il existe plusieurs façon d'écrire ce “branchement conditionnel” :

if ... else ...

if (fuel_freeze) return;

Dans ce cas, si la variable fuel_freeze est différente de zéro (0 = false; tout le reste = true), alors la fonction return est exécutée, sinon le programme continue à l'instruction suivante.

if (selected_tanks == 0) {
    out_of_fuel = 1;
    } else {
        var fuel_per_tank = consumed_fuel / size(selected_tanks);
        #... le reste du bloc d'instructions
    }
#...la suite du script

Ici, si la valeur de la variable selected_tanks est égale à zéro (notez le double == pour le test, à différencier du = simple de l'affectation) alors on met 1 dans la variable out_of_fuel et on passe directement au reste du script, sinon on exécute le bloc d'instructions avant de continuer le reste du script.

... or ...

C'est un branchement conditionnel particulier qui permet d'améliorer la lecture du script en écrivant le test de façon intuitive :

prop != nil or return;

signifie : “la variable prop est différente de nil ou on arrête”, ce qui est l'équivalent en plus élégant de “si prop est égal à nil alors on arrête”.
C'est surtout très utile pour écrire des tests simples en cascade :

variable == `h` or variable == 3 or print ( "variable n'est pas égal à h ni à 3" );

On retrouve également or dans le cas de tests multiples.

Les tests multiples

Tester une variable c'est bien, en tester deux, c'est mieux… :-D\\zieutons le code suivant:

if (e == "y" or e == "z") {
    print ( "e est égal à y ou à z" );
} else {
    print ( "e n'est pas égal à y ou z" );
}

également il y a le test “et”

if (e == "y" and e == "z") {
    print ( "ce texte ne sera jamais écrit" );
} else {
    print ( "ce texte sera toujours écrit" );
}

en effet la variable e ne peut pas contenir à la fois y et z

Les test "encadrants"

Avant, pour tester si une variable était comprise entre deux valeurs, il fallait utiliser un test multiple

if (valeur > valeur_mini and valeur < valeur_maxi) {
    print ( "valeur est comprise entre valeur_maxi et valeur_mini" ); #notez que les valeurs des variables 
                                                                      #ne sont pas remplacées dans l'affichage 
                                                                      #du texte, il faut passer par une autre fonction
} else {
    print ( "c'est en dehors des clous!" );
}

de nos jours il existe un moyen plus élégant d'écrire cela:

if ( valeur_mini < valeur < valeur_maxi) {
    print ( "valeur est comprise entre valeur_maxi et valeur_mini" );
} else {
    print ( "c'est en dehors des clous!" );
}

L'affectation conditionnelle

Nasal est un langage complet et propose tout ce que les langages évolués contiennent. Il est possible d'affecter une valeur (à une variable, à un paramètre de fonction, etc.) en incluant un test. Par exemple supposons que nous voulions faire:

if (v > 10) {
    print ( "la variable v est supérieure à 10" );
} else {
    print ( "la variable v est inférieure à 10" );
}

c'est tout à fait correct mais peu élégant, il serait préférable de faire:

print ( "la variable v est ", (v > 10)? "supérieure" : "inférieure", " à 10" );

Les branchements conditionnels

Dans quelques langages il existe un possibilité très intéressante de tester un variable

# en bash
case $variable in
  "youpi" ) fais_un_truc_avec_ca;;
  "1" | "10") fais_un_autre_truc_avec $variable;;
  *) ;; #ne fais rien si $variable n'est ni égal à "youpi", ni à "1", ni à "10"
esac

#en C
switch (variable) {
     case 1: fais_un_truc(variable); break;
     case 10: fais_un_autre_truc(); break;
     default : break; # ne fais rien
}

en Nasal ce n'est pas possible, il vous faudra donc planter une forêt d'if

if (variable == "youpi") fais_un_truc_avec_ca();
if (variable == 1 or variable == 10) fais_un_autre_truc_avec(variable);

ou d'elsif

if (me.stage == 1) {
        cprint("", "1: press start button #1 -> spool up turbine #1 to N1 8.6--15%");
	setprop("/controls/rotor/brake", 0);
	engines.engine[0].ignitionN.setValue(1);
	engines.engine[0].starterN.setValue(1);
	me.next(4);
} elsif (me.stage == 2) {
	cprint("", "2: move power lever #1 forward -> fuel injection");
	engines.engine[0].powerN.setValue(0.13);
	me.next(2.5);
} elsif (me.stage == 3) {
	cprint("", "3: turbine #1 ignition (wait for EGT stabilization)");
	me.next(4.5);
} elsif (me.stage == 4) {
	cprint("", "4: move power lever #1 to idle position -> engine #1 spools up to N1 63%");
	engines.engine[0].powerN.setValue(0.63);
	me.next(5);
}

[code en provenance de Aircraft/Models/bo105.nas]

Les boucles

Ce ne sont pas de fonctions toutes prêtes pour vous faire réussir du premier coup des figures avec votre avion favori (mais elles peuvent être utiles pour vous aider à les faire) :

Au lieu d'écrire

var mafonction = func (n) { //faire un truc avec n };
mafonction(1);
mafonction(2);
mafonction(3);
# je continue?

, il est plus sensé et pratique et lisible et… j'en passe, d'utiliser les boucles qui permettent de faire répéter à l'ordinateur une commande autant de fois que nécessaire/souhaité.

for (variable = 0; variable < 100; variable += 1)

Cette boucle fait du trois en un ;-)

  1. elle affecte une valeur à une (plusieurs?) variable(s),
  2. elle effectue un test sur une (plusieurs?) variable(s), si le test est vrai alors elle continue,
  3. elle modifie la valeur de la (les?) variable(s),

Une manière spéciale d'écrire une boucle for:

var i = 0
for (; 1; i += 1) {
    #faire quelque chose avec i
    i < 100 or return;
}

cette boucle tournera 100 fois, cet exemple est là pour montrer que la syntaxe de cette structure est plutôt souple.

while (variable < 100)

cette boucle tournera tant que variable sera inférieur à 100. Attention, c'est à vous d'incrémenter la valeur de variable (contrairement à for (;;)). Elle permet cependant une utilisation pratique :

#première forme
var i = 0;
{ 
   print ( "première forme" );
   i += 1;
} while ( i < 10 );

#deuxième forme
i = 0;
while ( i < 10 ) { 
   print ( "deuxième forme" );
   i += 1;
}

La différence réside dans l'ordre d'exécution. Dans la première forme, le bloc d'instruction est exécuté avant le test, donc sera exécuté une fois de plus que la deuxième forme.

foreach (var element; ensemble)

Cette boucle permet de parcourir chacun des éléments d'un ensemble en les affectant à la variable element. Il n'y a aucune certitude sur l'ordre dans lequel les éléments seront traités.

forindex ()

Il s'agit d'une forme élégante et simplifiée de la boucle for qui s'applique sur une variable de type tableau.

me.data = ["a","b","c","d","e",
	"f","g","h","i","j",
	"k","l","m","n","o",
	"p","q","r","s","t",
	"u","v","w","x","y","z"];
me.cible = "r";
forindex (var i; me.data)
	if (me.data[i] == me.cible) { 
		printf ("%s est la %d lettre de l'alphabet", me.cible, i+1);
		break;
	}
}

est l'équivalent de

for (var i; i < size(me.data); i+=1)

Tip: dans la boucle for ci-dessus la fonction “size()” est utilisée pour connaître la taille en éléments du tableau me.data, il serait judicieux de placer cette valeur (constante) dans une variable afin d'éviter d'appeler “size” à chaque itération de la boucle.

settimer ()

Il n'est pas toujours judicieux d'utiliser ces boucles avec FlightGear, au risque de voir le simulateur tout bêtement s'arrêter le temps que la boucle ait terminé ce qu'elle avait à faire (autre débat que celui qui nous intéresse ici). Pour palier à ce “léger” désagrément, il y a des solutions, en fait une seule mais c'est déjà pas mal et ça permet même de faire mieux ;-). Voyons en détail ce bout de code récupéré sur le wiki anglais:

var boucle = func {
    print("affiche ce message toutes les deux secondes");
    settimer(boucle, 2);
}
boucle();        # lance la boucle

On y voit qu'une fonction (boucle ()) a été déclarée, elle contient une autre fonction settimer (boucle, 2) qui fait appel à l'objet boucle toutes les deux secondes. et voilà on a une boucle simple, sans compteur, qui ne gêne pas le fonctionnement de FlightGear, qui… ne s'arrête jamais :-/ c'est pas vraiment ce qu'on veut (enfin pas toujours).
Pour arrêter la boucle, il suffirait de “sortir” de la fonction boucle () avant qu'elle ne soit relancée par settimer, ce qui est envisageable en utilisant la fonction return

var boucleid = 0;
var compteur = 0;
var boucle = func (id) {
    print("affiche ce message toutes les deux secondes");
    id == boucleid or return;
    compteur < 10 or return;
    compteur += 1;
    settimer(boucle, 2);
}
boucle(id);        # lance la boucle
# on fait le reste pendant que la boucle tourne
boucleid += 1;

La boucle ici tournera 10 fois ou alors sera arrêtée par le script avant (en changeant la valeur de boucleid)

L'espace de nommage (TODO trouver un meilleur titre)

Je ne sais pas si c'est FlightGear qui permet au Nasal de partager les espaces de nommage des fichiers inclus dans $FGROOT/Nasal et $FGHOME/Nasal (ailleurs encore?), ou si Nasal sait le faire tout seul… Toujours est-il que cette possibilité existe et est très utilisée car très pratique.
Un petit exemple pour clarifier. Considérons deux fichiers placés dans $FGROOT/Nasal: a.nas et b.nas.
a.nas

var phrase = "du texte sympa";
var une_fonction = func {
    print (phrase);
}


b.nas

a.une_fonction ();
a.phrase = "une phrase encore plus sympa";
a.unefonction ();

ainsi le script b.nas va utiliser la fonction définie dans l'espace de nommage défini par a.nas. D'oùl'intérêt de suivre les recommandations détaillées plus haut sur l'utilisation du qualificatif “var”.

Les Listeners (lisseneurze)

Les “écouteurs” (ou “sniffeurs” ?) sont des <langage technique>fonctions de rappelfr (ou “callback functions”)</langage technique> attachées à des propriétés de FG. C'est-à-dire qu'à chaque fois qu'une propriété est modifiée/créée/supprimée, le listener appelle la fonction qui lui est associée.
Attention : les listeners ne fonctionnent pas avec toutes les propriétés, uniquement celles qui ne sont pas modifiées par la méthode de l'arbre des propriétés, comme par exemple la plupart des propriétés du modèle de vol…
FIXME ce que je viens d'écrire est incompréhensible…

Les fonctions callback et surtout... leurs paramètres

Les fonction de rappel peuvent prendre de zéro à quatre paramètres, les deux premiers doivent être des noeuds de propriétés, le troisième est un opérateur, le quatrième est un événement concernant les enfants du noeud de propriétés FIXME HELP j'y pige que dalle ;-)

setlistener ()

C'est une fonction qui permet de créer un listener (ou écouteur, ou sniffeur)

var listener_id = setlistener(<propriété>, <fonction> [, <startup=0> [, <runtime=1>]]);
  • listener_id contiendra l'identifiant unique donné par FG au listener s'il est nul c'est il y a eu un problème,
  • fonction : la fonction que le listener va appeler/exécuter,
  • propriété : la propriété que le listener va écouter pour exécuter la fonction,
  • startup : ce paramètre est optionnel, et sa valeur par défaut est à zéro.
    si ce paramètre est différent de zéro alors la propriété sera écoutée au démarrage du script
  • runtime : optionnel, les valeurs possibles sont :
    • 0 : la fonction est exécutée dès que la valeur de la propriété a été modifiée et est différente,
    • 1 : (par défaut): la fonction est exécutée dès que la valeur de la propriété a été modifiée (même si la valeur ne change pas),
    • 2: FIXME je pige pas tout bien :-D

Un exemple basique de listener:

setlistener("/sim/signals/exit", func { print("à bientôt !") });

ceci permet d'afficher sur le terminal le texte “à bientôt !” dès qu'on écrit quelque chose dans la propriété ”/sim/signals/exit”. Ce code aurait également pu s'écrire de la façon suivante:

var mafonction = func { print("à bientôt !") };
setlistener("/sim/signals/exit", mafonction() });

ou encore

var mafonction = func { print("à bientôt !") };
setlistener("/sim/signals/exit", mafonction() }, 0, 1);

ou encore…

Une utilisation plus poussée des listeners est bien sûr possible, mais je vous laisse la découvrir via la doc complète[en].

removelistener ()

Nous avons créé des listeners, c'est bien… Et on peut en créer autant qu'on le souhaite sans la moindre détérioration des performances de FlightGear (c'est Melchior qui le répète inlassablement :-D). Cependant il peut être utile de supprimer des listeners devenus inutiles, ou qui ne doivent servir qu'une seule fois.
Cette fonction prend en argument l'identifiant unique du listener à supprimer (voir setlistener ()) Elle renvoie le nombre de listeners encore actifs, ou -1 s'il y a eu une erreur.

var L = setlistener("/une/propriete", func {
    print("Je ne serai affiché qu'une fois");
    removelistener(L);
});

Cet exemple montre le cas d'un listener qui ne sera utilisé qu'une seule fois, lors du changement de la valeur de la propriété /une/propriete.

Les fonctions fournies par ...

... le code source

props.nas

globals.nas

debug.nas

string.nas

aircraft.nas

flightplan.nas

lead_target.nas

screen.nas

atc-chatter.nas

fuel.nas

material.nas

startup.nas

controls.nas

geo.nas

math.nas

multiplayer.nas

track_target.nas

gui.nas

prop_key_handler.nas

tutorial.nas

dynamic_view.nas

io.nas

view.nas

1) ceux sous licence GPL, ce qui est, hors cas très particuliers, la règle chez FlightGear.
2) FG n'est pas un éditeur de texte… et oui! c'est triste mais c'est comme ça
 
devel/nasal_pour_les_nuls.txt · Dernière modification: 2011/12/27 14:06 (modification externe)
 
Recent changes RSS feed GNU General Public License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki