id Tech 4 : scripts

Tout permuter

Les scripts sont utilisés pour les événements dans les maps, les armes, et l'intelligence artificielle. Cette section ne couvre que les scripts utilisés par les maps de Doom 3.

(Les scripts de GUI [Graphic User Interface] sont une catégorie à part entière et sont couverts dans une autre page. )

Notes générales

  • Les scripts d'entité ne sont pas rechargés automatiquement avec la commande map. On doit utiliser la commande reloadScript et ensuite recharger la map.
  • La syntaxe du langage étant très près de celle du C++, les commentaires à ligne unique commencent avec le texte // ; les commentaires multilignes commencent avec /* et se terminent avec */.
  • Selon toute apparence, le fichier doom_events.script contient toutes les fonctions de script nécessitant des fonctions dans le code C++, par opposition aux fonctions déclarées et définies dans les fichiers .script.
  • Les noms des fonctions de script sont sensibles à la casse.
  • Certaines fonctions de script ont des propriétés équivalentes dans DarkRadiant. Par exemple, la fonction bind(), qui apparente une entité à une autre (et lui fera donc suivre les mouvements de l'autre entité) peut être utilisée dans DR en ajoutant la propriété bind, avec comme valeur le nom de l'entité parente.
  • L'entité $world représente toutes les brush (les murs et les planchers créés dans Dark Radiant) d'un niveau.
  • Dans les scripts d'intelligence artificielle, la fonction init() d'une entité (qui agit comme un constructeur C++) est polymorphique ; par exemple, la fonction monster_zombie::Init() (ai_monster_zombie.script) appelle la fonction du même nom qui appartient à l'objet monster_base (duquel l'objet monster_zombie est dérivé).
  • Pour accéder à un membre (x, y ou z) d'un vecteur (objet vector), on écrit le nom du vecteur suivi d'un trait souligné et la lettre qui correspond à la coordonnée :

    vector origine = $player1.getWorldOrigin();
    sys.print( "Origine : ("+ origine_x +", "+ origine_y +", "+ origine_z +")" );

  • Quand on déclare une nouvelle fonction dans un script, ne pas lui donner le même nom qu'une fonction déjà existante ; la nouvelle fonction ne sera pas reconnue par le jeu. Exemple : Pour une fonction qui utilise la fonction système fadeOut, lui donner un nom distinct :

    void monScript_fadeOut() // Fonctionnel, car les fonctions ont des noms différents
    {
        sys.fadeOut('0 0 0', 10);
    }

    et non :

    void fadeOut() // Non fonctionnel, car il y a un conflit entre les fonctions
    {
        sys.fadeOut('0 0 0', 10);
    }

  • La fonction waitAction() dans les scripts de personnages

    La fonction de script waitAction( string ) met le script en pause jusqu'à ce que la fonction finishAction( string ) soit appelée avec la même chaîne en paramètre.

    Par exemple, dans la fonction monster_base::sight_enemy() :

    animState( ANIMCHANNEL_TORSO, "Torso_Sight", 4 );
    waitAction( "sight" );

    Explication détaillée de toutes les étapes nécessaires pour jouer l'animation :

    La fonction animState() est reliée à une fonction C++, idActor::Event_AnimState(). Cette fonction appelle idActor::SetAnimState(), qui appelle idAnimState::SetState().

    Cette dernière trouve un pointeur vers la fonction de script Torso_Sight (représentée par un objet function_t* dans le C++) et exécute la fonction de script monster_base::Torso_Sight(). Enfin, celle-ci joue l'animation en question et avertit le script quand l'animation est terminée :

    finishAction( "sight" );

Fichiers importants

  • ai_monster_base.script

    Le fichier de script de base pour tous les monstres. Certaines des méthodes de l'objet monster_base sont redéfinies par des sous-catégories de monstre.

    Propriété : idle_sight_fov

    Indique si le joueur doit être dans le champ de vision du monstre pour être repéré; si true, il peut être derrière le monstre sans l'alerter.

    Cette propriété est initialisée avec la valeur true, mais prend la valeur false dans monster_base::checkForEnemy(). Conséquence : au départ, le monstre ne voit le joueur que si celui-ci est dans son champ de vision. Mais si le joueur se place tout juste derrière lui, idle_sight_fov deviendra false et le monstre pourra ensuite voir le joueur n'importe où (sauf s'il est derrière un mur).

  • ai_monster_zombie_base.script

    Contient l'objet de base pour les zombis.

  • ai_monster_zombie.script

    Contient le script qui est utilisé par plusieurs types de zombi comme monster_zombie_fat, monster_zombie_civilian et monster_zombie_chainsaw, entre autres.

  • doom_defs.script

    Contient toutes les constantes qui sont utilisées dans les scripts, dont :

    • les canaux d'animation (ANIMCHANNEL_ALL, ANIMCHANNEL_TORSO, ANIMCHANNEL_LEGS, ANIMCHANNEL_HEAD, ANIMCHANNEL_EYELIDS)
    • les directions de déplacement (FORWARD, LEFT, RIGHT, REL_LEFT, REL_RIGHT, etc.)

    Il est possible de créer des macros dans ce fichier de la même façon qu'en C++. Par exemple, pour utiliser une fonction _print() qui agira comme sys.println(), ajouter cette ligne :

    #define _print( texte ) sys.println( texte )

  • doom_events.script

    Utile comme référence pour toutes les fonctions de script. Les commentaires expliquent quelles entités peuvent utiliser certaines fonctions.

Astuces

  • Fonctions utiles

    Dans les exemples suivants, $player1 est le nom de l'entité du joueur et $entite est le nom d'une entité quelconque dans la scène (son type varie selon le contexte). Les fonctions sys sont globales.

    $enfant.bind( $parent ) Apparenter une classe à une autre. Cette fonction peut aussi être utilisée comme propriété dans DarkRadiant (propriété : bind ; valeur entiteParent)
    $entite.hide() Enlever un objet de la scène. Cette méthode fera disparaître l'objet et enlèvera sa détection de collision
    $entite.playAnim ( animChannel, anim ) L'entité jouera l'animation voulue pour la partie du corps voulue. Par exemple :

    // Jouer l'animation
    $player1.playAnim( ANIMCHANNEL_TORSO, "hard_land" );
    // Attendre la fin de l'animation
    sys.wait( $player1.animLength( ANIMCHANNEL_TORSO, "hard_land" ) );
    // Rejouer l'animation d'inactivité
    $player1.playAnim( ANIMCHANNEL_TORSO, "idle" );

    $entite.rotate( '90 0 0' ) Pour une entité func_rotating ou semblable : faire tourner un objet (90 degrés sur l'axe x à chaque seconde). Ne fonctionne pas avec l'entité du joueur ( player_doommarine )
    $entite.rotateOnce( '90 0 0' ) Faire tourner un objet de façon permanente, à moins que decelTime soit définie
    $player1.selectWeapon( "weapon_pistol" ) Équiper une arme. Les scripts pour les armes se trouvent sous pak000/script et pak008/script. Cette arme provient de pak000/script/weapon_pistol.script
    sys.firstPerson() Caméra en première personne
    sys.print( "Voici un message.\n" ) Afficher un message dans la console
    sys.println( "Voici un message." ) Afficher un message avec un saut de ligne
    sys.setcvar( "nom_cvar", "valeur" ) Modifier une CVar
    sys.trigger( $entite ) « Active » une entité. Toutes les entités ne peuvent pas être activées ; certaines ne réagiront pas. Dans les autres cas, les résultats varient : les lumières peuvent être allumées et éteintes, et les moniteurs réagissent selon les guis qu'ils affichent.

    Note : lorsqu'on active une entité gui, toutes les entités dans la scène affichant le même gui seront activées. Il s'agit peut-être (mais pas certainement) d'un bogue.

  • Utiliser un script avec une map

    1. Créer un fichier .script, puis l'enregistrer dans le même dossier que la map, avec le même nomou
      1. Créer un fichier .script, puis l'enregistrer (avec un nom suivant le format : map_nomDeLaMap) dans le dossier base/script (la map sera dans base/maps)
      2. Éditer pak000/script/doom_main.script pour ajouter le code suivant, avec le bon nom pour le script :

        #include "script/map_nomDeLaMap.script"

    2. Si une fonction doit être appelée au tout début du niveau, ouvrir la map dans DarkRadiant, ouvrir les propriétés de l'objet Worldspawn (sélectionner n'importe quelle brush statique qui compose le niveau et appuyer N), puis ajouter la propriété suivante (ne pas ajouter de parenthèses) :call : map_nomDeLaMap::maFonction
    3. Encapsuler tout le code du script dans l'espace de nom map_nomDeLaMap pour la sécurité (utiliser le mot-clé namespace). Toutes les fonctions dans ce script seront créées dans cet espace de nom
  • Ajouter une nouvelle fonction de script (événement) à une entité

    Lorsqu'on appelle une fonction d'entité (comme sys.print()) dans un script, on fait en réalité référence à une fonction dans le code C++. Pour créer une nouvelle fonction dans le script, il faut donc la créer dans le code, puis l'enregistrer comme une fonction accessible par les scripts.

    Quelques notes avant de commencer :

    • La classe utilisée pour cet exemple est idPlayer, la classe représentant l'entité du joueur.
    • Le mot chose utilisé ci-bas représente ici le nom de l'événement (la fonction qui sera appelée).
    • Habituellement, lorsqu'on veut connaître les fonctions d'une entité, il suffit de regarder les événements enregistrés dans sa classe C++, c'est-à-dire les objets idEventDef qu'elle contient. Or, l'objet sys est un cas spécial : il faut passer par la classe idThread (neo/game/script/Script_Thread.cpp). C'est d'ailleurs cette classe qui se charge des événements pour idGameLocal ; par exemple, la fonction de script setCamera() appelle idThread::Event_SetCamera(), qui elle-même fait référence à idGameLocal::SetCamera().
    1. Créer dans le .h et le .cpp (ici, Player.h et Player.cpp) la fonction à laquelle l'événement fera référence. Dans ce cas, la fonction sera idPlayer::Event_Chose(). (La fonction n'a pas besoin de commencer par « Event_ », mais c'est la syntaxe habituelle pour des fonctions de script.)
    2. Dans Player.cpp :
      1. Créer l'événement en haut du fichier, avec les autres :

        const idEventDef EV_Player_Chose( "chose" ); // Noter la casse de "chose"

        On peut ajouter deux autres paramètres (const char *) optionnels : un pour spécifier le type des arguments que la fonction acceptera, et un autre pour le type d'objet qui sera retourné, s'il y a lieu. Quant aux valeurs que ces arguments peuvent prendre, le fichier neo/game/gamesys/Event.h en explique beaucoup :

        • "d" : int
        • "f" : float
        • "v" : vecteur (idVec3)
        • "s" : string
        • "e" : entité (idEntity *)
        • "E" : entité nulle

        On peut aussi faire des combinaisons pour les arguments acceptés : "es", par exemple, signifie que la fonction prendra comme paramètres une entité et une string.

        Note importante : Dans le C++, la plupart des fonctions de script (sinon toutes) sont de type void. Pour les fonctions de script devant retourner une valeur, la classe idThread a cinq fonctions statiques : ReturnString(), ReturnFloat(), ReturnInt(), ReturnVector(), et ReturnEntity().

      2. Enregistrer l'événement et l'associer à la fonction C++ (sans point-virgule) :

        EVENT( EV_Player_Chose, idPlayer::Event_Chose )

    3. Déclarer la fonction dans pak008/script/doom_events.script :

      scriptEvent void chose();

    4. Dans le script de la map qui sera utilisée, appeler la fonction :

      $player1.chose();

    Notes supplémentaires :

    • Quand une fonction de script prend un nombre comme paramètre, je crois que le paramètre doit être de type float dans doom_events.script, peu importe le type de paramètre pour la fonction C++.
    • Dans le cas d'une valeur booléenne, la valeur doit être un float partout, y compris dans le code C++. On vérifiera tout simplement si le nombre passé est autre chose que 0 ; si oui, la valeur sera traitée comme true. Voir la fonction de script bindToJoint() dans doom_defs.script pour un exemple.

Problèmes

Le nom maFonction remplace ici une fonction quelconque.

  • Unknown value : "maFonction" (Erreur de console)

    La fonction n'est pas reconnue ; s'il s'agit d'une fonction nouvellement créée, vérifier qu'elle a été ajoutée dans doom_events.script.