Mise à jour: 15/04/2014
Tutoriel - Déclencheurs et variables synchronisées
Le but de ce tutoriel est de créer des déclencheurs pour les scripts, les tâches et les événements de vos missions qui soient fiables et synchronisés entre TOUS les joueurs, qu'ils se connectent en début de mission ou pas.
Exemple concret
Pour ne pas gonfler ce message qui est déjà assez gros...
J'ai créé un message à part avec un exemple concret de A à Z de la création et mise à jour d'une tâche par les variables synchronisées.
L'exemple se trouve ici:
Création et mise à jour d'une tâche
1) Cas pratique
Une fois n'est pas coutume, commençons par un exemple de mission.
Je reviendrai sur le contenu des nouvelles fonctions par après, mais je voudrais simplement illustrer la différence de code.
- Les joueurs reçoivent une tâche en arrivant sur le jeu, ils doivent monter dans des véhicules et se rendre à l'usine près de Kore
- Lorsqu'ils arrivent à l'usine, une nouvelle tâche est ajoutée, ils doivent rencontrer la résistance et prendre des munitions
- Là ils reçoivent une nouvelle tâche pour aller déposer ces caisses dans une cache de la résistance à Zaros
Si vous avez déjà lu le tutoriel sur les tâches vous voyez directement où je souhaite en arriver...
IMPORTANT! Je prends l'exemple des tâches ici, pas pour parler des tâches, vous pouvez utiliser les modules de BIS si vous voulez pour ça (ce module n'existait pas quand j'ai écris la Base Mission), mais pour parler de la synchronisation d'informations.
Bon, faut dire que le module de BIS pour les tâches c'est carrément de la merde en MP... mais chacun ses goûts.
C'est déjà bien qu'ils aient prévu quelque chose pour ceux qui ne programment pas. C'est juste dommage que ce soit si mal écrit.
Le premier point
Si vous n'utilisez pas la Base Mission, comment allez vous faire? Simplement avec createSimpleTask, dans un script ou dans un déclencheur. Mais si le groupe de joueur est déjà passé par l'usine, le joueur qui vient d'arriver ne le sait pas, il n'y a rien pour lui dire que l'objectif est déjà accompli.
De manière à contourner ce problème, vous devez:
- Ajouter une variable sur un objet de la carte avec setVariable
- Dans le déclencheur qui ajoute la tâche, il faut contrôler que cette variable existe avec getVariable
- En fonction de ce test, il faut créer la tâche et la mettre comme réussie ou créée
En code ça donne..
Dans votre déclencheur à la base:
Code : Tout sélectionner
MA_TACHE_1 = player createSimpleTask [localize "STR_tache_usine"];
MA_TACHE_1 setSimpleTaskDescription [localize "STR_tache_usine_description", localize "STR_tache_usine", localize "STR_tache_usine_waypoint"];
if (objet_synchro getVariable ["tache_usine_reussie",false]) then {
MA_TACHE_1 setTaskState "Succeeded";
};
Dans votre déclencheur qui détecte l'arrivée à l'usine:
Code : Tout sélectionner
MA_TACHE_1 setTaskState "Succeeded";
objet_synchro setVariable ["tache_usine_reussie",true,true];
Allez, un problème de résolu...
Avec la BaseMission, il suffit d'appeler la fonction de création des tâches et la tâche sera automatiquement synchronisée avec le joueur qui arrive, inutile de contrôler quoi que ce soit...
Dans votre déclencheur à la base:
Code : Tout sélectionner
["STR_tache_usine","created","STR_tache_usine_description","",1] call edt_fnc_task;
Dans votre déclencheur à l'usine:
Le deuxième point
Idem ici, on a un déclencheur qui détecte la présence des Blufor et qui va créer la tâche.
Même problème que ci-dessus, le joueur qui se connecte plus tard ne passera JAMAIS par l'usine, puisque tout le reste de l'action est tout à fait ailleurs. Donc il n'aura jamais la tâche.
Comment le résoudre?
La ça devient plus compliqué.
- Dans votre déclencheur, il faut modifier la condition pour qu'elle vérifie la présence des blufor OU le fait que la tache est ajoutée, comme ça quelqu'un qui se connecte plus tard, aura la tâche parce que la variable d'ajout est vraie
- Ensuite, dans votre déclencheur, il faut mettre une variable sur un objet (avec setVariable) pour indiquer que cette tâche a été ajoutée.
- Il faut ensuite créer la tâche.
- Il faut alors contrôler que la tâche est déjà réussie ou pas, en contrôlant encore une autre variable avec getVariable.
En code:
La condition de votre déclencheur qui détecte les blufor:
Code : Tout sélectionner
this || (objet_synchro getVariable ["tache_munitions_reussie",false])
Dans l'activation du déclencheur:
Code : Tout sélectionner
MA_TACHE_2 = player createSimpleTask [localize "STR_tache_munitions"];
MA_TACHE_2 setSimpleTaskDescription [localize "STR_tache_munitions_description", localize "STR_tache_munitions", localize "STR_tache_munitions_waypoint"];
if (objet_synchro getVariable ["tache_munitions_reussie",false]) then {
MA_TACHE_2 setTaskState "Succeeded";
};
Dans votre déclencheur qui détecte que la caisse de munitions a disparu:
Code : Tout sélectionner
MA_TACHE_2 setTaskState "Succeeded";
objet_synchro setVariable ["tache_munitions_reussie",true,true];
Et un autre problème de résolu...
Avec la BaseMission, ben vos tâches sont synchronisées, inutile de passer par le déclencheur pour recevoir la tâche, c'est la Base Mission qui vous la donne lorsque vous vous connectez.
Donc rien à écrire en plus que la mise à jour de la tâche!
Dans votre déclencheur à l'usine:
Code : Tout sélectionner
["STR_tache_munitions","created","STR_tache_munitions_description","",2] call edt_fnc_task;
Dans votre déclencheur qui détecte la disparition de la caisse:
La tâche, puisque créée avec edt_fnc_task sera synchronisée automatiquement avec le joueur qui se connecte! Qu'il passe par le déclencheur ou pas!
On voit directement que c'est nettement plus simple avec les commandes de la Base Mission...
Vous pouvez le faire dans l'éditeur, mais sérieusement, tous les modules et autres que vous devez relier par des synchronisations et tout ça, moi ça me saoule.. en même temps, je suis un programmeur
Pourquoi est-ce plus simple et qu'est-ce que "edt_fnc_completed"?
On en parle dans les points suivants....
2) Les variables synchronisées
Un des boulots de la Base Mission c'est de synchroniser tout ce qui se passe entre les joueurs et être sûr qu'un joueur qui se connecte en cours de jeu a bien les mêmes informations que les autres.
Pour cela, le serveur stocke les informations importantes et les envoie au joueur qui se connecte.
Pour pouvoir synchroniser le fonctionnement des déclencheurs (qui rappelons le encore sont LOCAUX au PC sur lequel ils sont exécutés et donc NE SONT PAS SYNCHRONISÉS, on va utiliser des variables synchronisées. Du coup quand on place un déclencheur, on sait qu'il s'exécutera s'il doit s'exécuter.
Pour synchroniser des variables on a 5 fonctions:
- edt_fnc_getVar : lit la valeur d'une variable synchronisée
- edt_fnc_setVar : assigne la valeur d'une variable synchronisée
- edt_fnc_status : donne l'état d'un objectif (c'est en fait un alias de edt_fnc_getVar)
- edt_fnc_completed : assigne l'état 'true' à un objectif (c'est un alias de edt_fnc_setVar)
- edt_fnc_canceled : assigne l'état 'false' à un objectif (idem que ci-dessus)
Le principe dans la Base Mission, c'est qu'on stocke des variables, qui sont représentées par un nom.
Par exemple: "tache_usine_réussie".
Et tout client qui se connecte, recevra le contenu de la variable et pourra le lire.
C'est de cette manière qu'on synchronise les tâches, le déclenchement ou non des déclencheurs. Les scripts qui tournent sur les objectifs (assassiner, détruire, ramasser, etc.).
Et pour moi c'est la manière la plus simple de m'assurer que tous les joueurs ont la même information et que la mission se comportera de la même manière pour tous.
3) Comment utiliser les variables synchronisées
Rien de plus simple, la plupart des scripts et fonctions acceptent ces variables pour soit, donner leur état, soit pour récupérer une information.
Prenons la fonction
edt_fnc_task comme exemple. On l'utilise de la manière suivante:
Code : Tout sélectionner
["STR_TASK_DESTROY_TITLE", "created", "STR_TASK_DESTROY_DESC","marker_task_destroy","jip_var_destroyed","jip_var_failed"] call edt_fnc_task;
Les paramètres dans l'ordre sont:
- Le titre de la tâche, ici une chaîne magique automatiquement traduite
- Le statut initial de la tâche
- La description de la tâche, ici encore une chaîne magique
- Le nom du marqueur qui indique où il faut mettre le marqueur de tâche (la petite boule noire sur carte) [Optionel]
- La variable synchronisée qui indique si la tâche est réussie [Optionel]
- La variable synchronisée qui indique si la tâche est échouée [Optionel]
Donc, on a créé la tâche avec une petite ligne de code. Et pour changer le statut de cette tâche, on ne touchera plus jamais à la tâche, il suffit de modifier le contenu de la variable qu'on a défini ici, les variables sont: "
jip_var_destroyed" et "
jip_var_failed"
edt_fnc_task est donc une fonction qui surveille le contenu d'une ou plusieurs variables.
A noter que le nom "
jip_var_" est une convention que j'ai choisi pour moi-même pour bien indiquer qu'il s'agit d'une variable synchronisée en JIP. Ca me permet entre autre, de faire des recherches dans
notepad++ dans une mission pour récupérer tous les endroits où sont utilisés cette variable. Vous pouvez donner le nom que vous voulez...
De l'autre côté, on a des scripts qui donnent une valeur à une variable. Par exemple, le script
assassiner.sqf.
Vous placez un camion sur carte et vous mettez dans son initialisation:
Code : Tout sélectionner
nul = [this,getPos this,"jip_var_destroyed","STR_TRUCK_DOWN"] execVM "assassiner.sqf";
Petite note au passage, pourquoi
assassiner.sqf et pas
détruire.sqf?
Parce que
détruire.sqf c'est quand on veut utiliser des explosifs pour détruire un objet. Ici je veux pouvoir le détruire à la roquette ou même au fusil mitrailleur.
Les paramètres ici sont:
- L'objet à détruire, on aurait pu donner un type pour qu'il soit créé à la volée.
- La position (elle n'est en fait utile que lorsqu'on donne un type)
- Le nom de la variable qui va être mise à "true" lorsque le véhicule est détruit.
- Le message qui s'affichera pour tous les joueurs quand le véhicule est détruit, une fois de plus une chaîne magique
Notez qu'on a utilisé le même nom de variable "
jip_var_destroyed" dans les deux cas. Ce qui veut dire qu'une fois le camion détruit, le script va assigner une valeur à la variable, ce qui va automatiquement mettre la tâche à jour.
Maintenant, la partie échec de la tâche.
Notre camion roule sur la carte, s'il atteint sa destination, la tâche doit échouer. Il suffit de placer un déclencheur qui détecte la présence de notre camion.
Dans la condition du déclencheur on mettra:
En partant du principe que le déclencheur est nommé: "
trigger_de_fin" et le camion est nommé "
mon_camion".
Dans l'activation du déclencheur on mettra:
Et voilà, on affecte un contenu à la variable "
jip_var_failed" qui sert à détecter l'échec de la tâche et du coup on ne touche pas à la tâche non plus.
En résumé, le code nécessaire pour créer une tâche, un objectif et l'échec de la tâche:
Code : Tout sélectionner
// A mettre dans le script "mission\start.sqf", dans la partie serveur, ou dans un déclencheur
["STR_TASK_DESTROY_TITLE", "created", "STR_TASK_DESTROY_DESC","marker_task_destroy","jip_var_destroyed","jip_var_failed"] call edt_fnc_task;
// A mettre dans l'initialisation du camion nommé: "mon_camion"
nul = [this,getPos this,"jip_var_destroyed","STR_TRUCK_DOWN"] execVM "assassiner.sqf";
// A mettre dans l'activation du déclencheur qui détecte l'arrivée du camion "mon_camion"
nul = ["jip_var_failed",true] call edt_fnc_setVar;
Si on compare avec tout ce qu'on devrait écrire normalement pour faire fonctionner tout ça et pour le synchroniser en JIP... ce n'est vraiment rien du tout!
edt_fnc_getVar
Pour lire le contenu d'une variable synchronisée on va utiliser la fonction suivante:
Les paramètres sont:
- Le nom de la variable à lire
- La valeur par défaut à donner si la variable n'a pas encore reçu de valeur (si on n'a pas encore appelé edt_fnc_setVar
edt_fnc_setVar
Pour assigner un contenu à une variable synchronisée on va utiliser la fonction suivante:
Les paramètres sont:
- Le nom de la variable à lire
- La valeur qu'on donne à la variable et qu'on peut ensuite récupérer avec edt_fnc_getVar
4) Les variables synchronisées simplifiées
La plupart du temps, je n'ai besoin que de deux états: "true" ou "false" pour un objectif.
Pour ça, j'ai crée 3 fonctions qui sont des raccourcis.
- edt_fnc_status
- edt_fnc_completed
- edt_fnc_canceled
Dans un déclencheur, pour tester un objectif on pourrait écrire dans la condition;
Mais il serait plus rapide de se contenter de:
Les deux fonctions font exactement la même chose!
D'ailleurs, quand vous écrivez le "
1" en fait derrière on stocke la variable dans "
tmf_edt_1", c'est une variable synchronisée comme les autres... les scripts de la base mission feront la conversion automatiquement tel que
assassiner.sqf:
Code : Tout sélectionner
nul = [this,getPos this,1,"STR_TRUCK_DOWN"] execVM "assassiner.sqf";
// Si on veut utiliser une chaîne:
nul = [this,getPos this,"tmf_edt_1","STR_TRUCK_DOWN"] execVM "assassiner.sqf";
Lorsque le camion sera détruit, mon déclencheur va s'activer puisqu'il surveille l'objectif "
1".
Attention toutefois à bien passer la valeur en numérique et pas en chaîne de caractère!
Si vous voulez passer une chaîne de caractère, ce sera: "
tmf_edt_1"
De la même manière, on peut donner une valeur "vraie" ou "fausse" à l'objectif en utilisant les fonctions:
Prenons un cas simple pour un début de mission.
Vous voulez que les joueurs démarrent en chute libre en début de mission et ensuite ils doivent rejoindre un poste avancé.
Si un joueur arrive en cours de route... inutile de le faire démarrer en chute libre... autant le mettre directement au poste avancé.
Mais comment savoir ça? Les joueurs ne sont plus dans le poste avancé (ils sont en cours de mission) et donc un déclencheur dans le poste avancé ne sert à rien!
Eh bien c'est simple.
Placez un déclencheur avec détection des blufor sur le poste avancé. Dans l'activation du déclencheur:
Ensuite dans le script "mission\start.sqf" dans la partie joueur, placez le code suivant:
Code : Tout sélectionner
titleText ["", "BLACK FADED"]; // Fondu au noir pendant qu'on teste où mettre le joueur.
sleep 3; // pour permettre la synchro des variables
if (1 call edt_fnc_status) then { // On teste si l'objectif est accompli et que les joueurs sont déjà passés par l'outpost
player setPos (markerPos "marqueur_outpost"); // On place le joueur sur un marqueur dans l'outpost
} else {
_pos = markerPos ("marqueur_parachutage"); // On récupère la position de largage des joueurs pour leur parachutage
_pos set [2, (2000 + random(200))]; // On défini une hauteur aléatoire
player setPos _pos; // On place le joueur en hauteur pour sa descente
};
titleFadeOut 5; // Fin du fondu au noir
Voilà...
On peut faire ça dans l'éditeur, mais ce ne sera pas lisible... et avec des déclencheurs et des synchros de variable à la main dans l'éditeur... moi je dis "bon courage!".
5) Ce qu'on ne peut pas encore voir
L'un des objectifs de la Base Mission est de pouvoir contenir plusieurs missions dans un même PBO.
Pour ce faire, les missions seront instanciées à la volée et peuvent être même instanciées en même temps.
On ne le voit pas, mais les tâches, les variables, etc. vivent dans un espace qui est propre à cette mission. Et qui n'entrerait pas en collision avec une mission lancée en parallèle dans la même partie.
Pourquoi prévoir ça?
Eh bien pour permettre par exemple d'avoir un serveur public, basé sur la Base Mission qui pourrait enchaîner automatiquement les missions à accomplir. Sans relancer un nouveau PBO à chaque fois. C'est l'objectif initial de tout ce projet, recréer la mission Foreveron d'ArmA 2.
Inutile de charger 20 missions sur votre serveur quand vous voulez organiser une soirée entre amis ou dans votre team, La mission contiendrait elle même déjà une série de missions. Et ça évite un temps de chargement à chaque fois, le script qui défini une mission est très petit, 10/20KB comparé à tous les scripts qui constituent les fonctionnalités de la Base Mission (Btc Revive, Drones, Artillerie, Menu, etc.).
On pourrait encore l'utiliser pour lancer des missions de type "side" sur une autre mission comme la Domination pour peu qu'on y intègre les scripts de la Base Mission. Ce qui permettrait d'avoir des missions plus avancées à jouer sur un serveur public.
Tout cela demande simplement un fichier SQF qui définit le contexte de la mission, place les unités et les objets et lance les scripts appropriés.
Il faudra probablement encore attendre quelques mois... mais ça viendra