AI C++ scripting
for
Yeti
Jérôme Hubert
Grégory Pageot
Revision 26 - 29.01.07
TOC \o "1-3" \h \z \u
Introduction
Utilisation
Eléments requis
Génération des sources
Utilisation de la lib générée
Utilisation sous debugger
Procédure
Traces sous Debugger
Problèmes connus de syntaxe
Fonctionnalités non émulées
Robot de compilation
Génération des sources
Architecture AI lib / moteur
Généralités
API moteur / API de l'AI lib
Initialisation de la lib
Emulation des scripts en C++
Description des différentes API entre moteur et lib
API moteur spécifique au système de lib AI
API fournie par la DLL au moteur
API des AI fonctions
Séparation du comportement et des variables
Modèles
Variables d'instance
Variables de modèle
Model cast
Gestion des structs
Interruption de l'interprétation
Interruption du flux d'exécution
Intégration
Gestion des d'états
Interfaçage des états
Changements d'état
Metafonctions
Exécution conditionnelle : Every, EnterState
Fonctions scripts
Contexte courant
Fonctions générées depuis les fonctions scripts
AI fonctions de Yeti
Optimisation
Type object
Type Any
String et unistring
Strings locales / strings 12512g62m membres
Stratégie du string manager pour les scripts
Stratégie du string manager pour le code C++
Cohabitation avec les interruptions d'interprétation
Bool array
Maths classes
Références sur ressources moteur
Modifications de l'interpréteur
Divers
gcc
Precompiled header .gch
Compilation
Link
Options de compilation
PRX
Profiling
Outils
Utilisation du profiler pour l'AI sur Xbox360
Utilisation de Code Analyst sur PC
Outils de profiling intégrés
Statistiques
Performance
Taille des libs
Temps de compilation
Evolutions
Court terme : avec le système actuel de génération
Confort d'utilisation
Optimisations
Long terme : développement Gameplay programming en C++
Editeur et réflexivité pour les classes AI
Références sur data
Amélioration de syntaxe par rapport au code généré actuel
String
Modifications du pattern editor
Support multi DLLs
Migration
Confronté à la nécessité d'améliorer les
performances de Yeti pour
Une possibilité pour éviter ce problème est de développer le système d'AI en C++, mais il n'est pas envisageable de re-développer toutes les AIs existantes.
L'idée intermédiaire consistant à générer automatiquement une libraire C++ à partir des scripts AI de Yeti a donc été envisagée. Dans cette optique, le code C++ généré doit reproduire fidèlement le comportement de l'interpréteur.
Bienque l'exécution du code C++ ne soit pas optimale en raison du temps utilisé pour émuler l'interprétation des scripts, nous attendons un gain de performance important. D'après les benchs effectués préalablement sur Yeti, l'interprétation des scripts représente environ 60% du raster total de l'AI.
Les sources C++ sont créés à partir des scripts en post-process. Au niveau archivage, les scripts restent la référence.
Un certain nombre de sources ne sont pas générés et il faut les récupérer préalablement via Perforce :
F Pour les Gpp n'ayant pas accès aux sources le mieux est de fournir une copie de AI_DLLInterface.h via le batch de livraison de la version utilisateurs en le copiant dans engine\exe\DLL\TPL
Pour générer les sources des AIs il faut utiliser l'éditeur Yeti. Dans l'éditeur d'AI, dans le menu Build > Generate DLL sources choisir l'option :
"official maps (default)" pour créer les AIs pour toutes les maps finales du jeu (spécifiées dans la maps list) en excluant le code des patterns des maps test
"whole bigfile (default)" pour créer toutes les AIs contenues dans le big file (plus lent)
"current map (default)" pour créer les AIs de la map courante (plus rapide). Il faut avoir chargé une map pour utiliser cette option.
L'éditeur génère les fichiers sources correspondant aux scripts (en général un fichier .h + .cpp par modèle) ainsi que le projet Visual AIDLL.vcproj permettant de les compiler. Ces fichiers sont générés dans le répertoire exe\DLL\SRC pour les sources et exe\DLL\Make pour le vcproj.
F
Pour compiler
F Le fichier AIDLL.vcproj généré comprend les trois plateformes PC, Xbox 360 et PS3 en target Release et Debug.
F Lorsque les fichiers sources ont déjà été générés, seuls les sources comportant des différences sont mis à jour. Pour ce faire, les fichiers sont générés temporairement sous le nom fichiersource~, si ce fichier est différent du fichier fichiersource précédent, il le remplace sinon il est effacé. De cette manière seuls les fichiers modifiés sont mis à jour ce qui garantit un rebuild minimal avec le compilateur C++.
Pour utiliser la lib une fois compilée :
Dans l'éditeur il faut valider
l'option Build ->UseDLL puis
démarrer le jeu (GO). Lorsque le jeu est stoppé,
Sur XBox 360, le fonctionnement se rapproche du fonctionnent sur PC. Les libs sont générées sous forme de DLL (extension .XEX). Au lancement le moteur cherche en priorité la lib finale AI_X360_F.XEX, puis release AI_X360_R.XEX puis debug AI_X360_D.XEX. Si aucune lib n'est trouvée, le moteur se lance en mode scripts.
F En mode moteur C++, le byte code script n'est pas chargé pour économiser de la mémoire.
Sur PS3, la lib est intégrée en link statique dans le moteur.
Pour cela dans le projet MainEngine dans le folder AILibs, il faut :
dé-valider exclude from build dans les propriétés de Visual sur l'AI lib que vous voulez linker
valider exclude from build
dans les propriétés de Visual sur
Lorsque le jeu est
en mode C++, un logo "C++ version"
apparaît en haut à gauche de l'écran sur les versions de moteur non FINAL. version dépend de
d pour la lib debug
r pour la lib release
f pour la lib final
Pour utiliser le debugger de Visual ou ProDG :
Il est possible de debugger la librairie en utilisant le projet principal de Yeti (puis en plaçant des breakpoints directement dans les sources de la librairie et/ou en traçant à partir de l'interpréteur de scripts la fonction pst_Interpret).
Note : dans certains cas, lors de l'utilisation de
Pour les personnes ne disposant
pas des sources du moteur, il est possible d'utiliser le projet AIDLL.vcproj généré pour
Note : pour pouvoir connecter le debugger de Visual sur l'éditeur Yeti, il faut désactiver le CrashReporter. En effet le CrashReporter s'enregistre comme debugger auprès de l'application ce qui empêche de connecter le debugger de Visual par la suite (dans ce cas Yeti apparaît en grisé dans la liste des process dans Visual). Pour ce faire, il suffit de renommer CrashReporter.exe avant de lancer Yeti.
Le debugger reste connecté au process tant que vous n'en sortez pas (par shift+F5 notamment). Contrairement aux exes, il est possible de rebuilder sans stopper le debugger. Après un rebuild il est parfois nécessaire de réafficher l'interface du debugger avec Alt+3 par exemple (cela dépend de la configuration de vos fenêtres). De fait il n'est jamais nécessaire d'effectuer de sortir (Shift+ F5).
Si vous voulez absolument sortir du debugger (Shift+F5), il est important de stopper le moteur Yeti au préalable afin de décharger la DLL, dans le cas contraire cela plante Yeti.
Les fichiers générés sont placés dans le répertoire DLL\SRC. Dans le projet .vcproj le générateur reproduit les chemins d'accès définis dans Yeti pour plus de clarté :
Il est possible d'intercepter les AICheck sous debugger sans avoir les sources du moteur. Pour ce faire, les AICheck exécutent le callback AI_cl_DLL::AICheckCallback dans la DLL (fichier AI_DLLEntry.cpp).
Les AI_ASSERT de la DLL exécutent également ce callback.
Les identifiants générés dans la DLL sont dans un namespace AIDLL pour éviter les collisions avec le moteur en link statique. Sous le debugger il est nécessaire d'ajouter AIDLL:: dans les types visualisés dans la fenêtre watch pour que cela fonctionne.
Lorsque vous tracez le code d'un modèle, les données qui y sont attachées sont accessibles :
o me est le gao courant
o This() ou
(AIDLL::NomModel_Data*) mpc_VarInstance
visualise les variables d'instance courantes
o ou pour une variable précisément
((AIDLL::NomModel_Data*) mpc_VarInstance)->NomVariable
Vous pouvez ouvrir directement tous les pointeurs de type object (notamment me) : cela permet d'accéder au nom d'un gao lorsque vous débuggez avec la version éditeur ou la clé de big file avec la version moteur.
Pour accéder aux données d'un object model casté :
o Si dans le script, vous utilisez la forme : object oObj = (NomModel) obj;
(AIDLL::NomModel_Data*)oObj.m_pGenModel->
mpc_InstanceVar
visualise les variables d'instances
oObj.m_pGenModel->me est le gao
o Si dans le script, vous utilisez la forme :
(NomModel) obj.var1 = 0;
(NomModel) obj.var2 = 0;
une fois le model cast exécuté et tant qu'aucun autre model cast n'est exécuté :
(AIDLL::NomModel_Data*)AIDLL::AI_cl_DLL::ms_oDLL.
m_ModelCastCache.m_pInstanceVar visualise les variables d'instances
AIDLL::AI_cl_DLL::ms_oDLL.m_ModelCastCache.me est le gao
F Lorsque vous tracez finement le code d'un modèle qui utilise ce type de cast, il est conseillé de transformer le code script dans la forme précédente qui est plus fiable pour le debug et qui est plus performante en termes de temps d'exécution.
F
Les accesseurs inline ( This() notamment ) peuvent être utilisés dans les watch du debugger
lorsque la DLL est compilée en Debug.
A noter que sous Visual 2005 (contrairement à VC6 et 2003), à chaque step, le
debugger invalide les entrées correspondantes qui passent en grisées. Le bouton
Note : dès que possible nous essaierons d'utiliser les fonctionnalités de customisation du debugger de Visual 2005 pour simplifier les syntaxes à appliquer dans les fenêtres de watch
Sur la version moteur utilisant le link dynamique (Xbox360, PC), un système permet de décharger puis recharger la DLL à chaud pendant l'exécution comme sous l'éditeur. Il est commandé par une variable accessible dans le debugger. La procédure est la suivante :
Au préalable :
Lancer le moteur en mode C++
Ouvrir le projet AIDLL.vcproj sous Visual et attacher le debugger sur Yeti
Debugger à votre convenance...
Déchargement de la DLL :
Pour décharger la DLL : en étant breaké, afficher l'objet AIDLL::AI_cl_DLL::ms_oDLL dans la watch window
Déplier l'objet ms_oDLL, sélectionner l'adresse du pointeur membre mp_bUnloadDLLRequest et la drag and droper dans la case vide de la watch window. Caster cette adresse en (bool*)
Changer la valeur pointée par cette adresse de false à true puis relancer le jeu (F5)
F A ce stade la DLL est déchargée et le jeu est gelé
F Sans détacher le debugger, opérer les modifications souhaitées sur la DLL (recompiler, renommer la DLL pour sélectionner une autre target...)
Rechargement de la DLL :
Pour recharger la DLL et relancer, dans le debugger effectuer un "Break All" (menu Debug). Le debugger vous annonce que le code n'est pas visible => ce n'est pas gênant, ignorez cet avertissement
Repasser la valeur pointée par l'adresse dans la watch window de true à false puis relancer le jeu (F5)
F A ce stade le jeu recharge la DLL disponible et continue l'exécution
F Si aucune DLL n'est disponible le moteur repositionne la variable à true et se replace en état d'attente : dans ce cas, réitérez la procédure de rechargement (le byte code script n'étant pas chargé il faut obligatoirement une DLL pour relancer.)
F La DLL rechargée doit conserver les mêmes définitions de classe que les classes scripts actuellement chargées par le moteur.
F Si vous debuggez à partir des sources moteur vous pouvez changer directement la valeur du flag AI_cl_DLLManager::m_bUnloadDLLRequest (sans avoir à mapper l'adresse du booléen dans la fenêtre de watch).
Pour récupérer la main sur l'éditeur lorsque la DLL est sur un plantage :
o placer cette expression dans la watch window du debugger :
(AIDLL::AI_cl_DLL::ms_oDLL).mp_bCSuspended
o passer sa valeur de false à true
o ressortez du code fautif en sautant la partie de code posant problème grâce à l'option "set next statement" du debugger
o continuer l'exécution
F Le flag modifié court-circuite l'exécution des AIs dans l'éditeur. Pour réactiver les AIs C++ il faut appuyer sur stop / go ou redémarrer la map.
Sur Xbox360 quand un AI_Check (moteur) ou un AI_ASSERT (DLL) se déclenche, la stack courante est dumpée dans le log sous forme d'adresses hexa. Si l'outil XMW est connecté sur le moteur ces stacks sont traduites via les fichiers pdb sous forme de noms de fonction.
Lorsqu'un crash se produit, la stack courante est dumpée sous forme de noms de fonction. En général le nom de la fonction courante est suivi d'un offset caractérisant la ligne de code exacte ayant produit le crash.
Pour déterminer
Ouvrir le projet AIDLL.sln, et sélectionner la target correspondante
Modifier les propriétés du fichier .cpp incriminé comme suit
(C++>Output Files>Assembler Output> Assembly, Machine Code and Source)
Exemple
PROC ; AIDLL::DroneShaderSeeker::DroneShaderSeeker_20_PrepareDroneOnceSpawned
; _this$ = ecx
:
:
: This()->mo_Drone = model_instance<ObjectSpawner>( This()-
003f4 8b 8d 78 ff ff
ff mov ecx, DWORD PTR _this$[ebp]
003fa e8 00 00 00 00 call ?This@DroneShaderSeeker@AIDLL@@QBEPAVDroneShaderSeeker_Data@2@XZ ; AIDLL::DroneShaderSeeker::This
003ff 8b 50 1c mov edx, DWORD PTR [eax+28]
00402 52 push edx
00403 8d 4d d8 lea ecx, DWORD PTR $T152224[ebp]
00406 e8 00 00 00 00 call ??0?$model_instance@VObjectSpawner@AIDLL@@@AIDLL@@QAE@PAVgame_object@@@Z ; AIDLL::model_instance<AIDLL::ObjectSpawner>::model_instance<AIDLL::ObjectSpawner>
0040b 8b c8 mov ecx, eax
0040d e8 00 00 00 00 call ??C?$model_instance@VObjectSpawner@AIDLL@@@AIDLL@@QAEPAVObjectSpawner@1@XZ ; AIDLL::model_instance<AIDLL::ObjectSpawner>::operator->
00412 8b c8 mov ecx, eax
00414 e8 00 00 00 00 call ?oGetSpawnedObject@ObjectSpawner@AIDLL@@QAEPAVgame_object@@XZ ; AIDLL::ObjectSpawner::oGetSpawnedObject
00419 8b f0 mov esi, eax
0041b 8b 8d 78 ff ff
ff mov ecx, DWORD PTR _this$[ebp]
00421 e8 00 00 00 00 call ?This@DroneShaderSeeker@AIDLL@@QBEPAVDroneShaderSeeker_Data@2@XZ ; AIDLL::DroneShaderSeeker::This
00426 89 70 20 mov DWORD PTR [eax+32], esi
: if( This()->mo_Drone!= NULL)
Il existe quelques problèmes de syntaxe difficiles à corriger au niveau de la génération et qui requièrent une adaptation des scripts.
Certains cas sont détectés automatiquement à la compilation des scripts grâce à des vérifications supplémentaires ajoutées au compilateur produisant des warnings.
D'autres cas sont difficiles à détecter lors de la compilation script. Pour gérer ces problèmes, le déploiement du système de génération + compilation des sources AI C++ sur un robot de compilation permet de fournir un retour rapide aux équipes de Gameplay programmeurs sans que ceux-ci aient à utiliser Visual systématiquement pour vérifier leurs scripts.
Sur le robot la lib est générée en un seul metafile .cpp (environ 650000 lignes) puis elle est compilée en target debug sous Visual. Ce mode de compilation est le plus rapide (1 min 30 pour un rebuild complet) ce qui est un avantage pour l'utilisation avec le robot.
Note : la version actuelle du debugger de Visual ne gère pas correctement les sources de plus de 65535 lignes.
Note : gcc est moins tolérant que Visual : certaines erreurs n'apparaissent que sous gcc.
Quelques exemples de problèmes syntaxiques :
La portée des variables locales est différente en script et en C++. Par exemple dans le cas d'un switch, les déclarations de variables sont valides d'un case à l'autre en script. En C++ pour éviter certaines erreurs de compilation le code de chaque case est placé entre accolades. De ce fait, les variables définies dans un case ne sont pas valides dans un autre. Ce cas est détecté à la compilation de scripts et produit un warning. La solution dans ce genre de cas est de remonter la déclaration de variable plus haut dans le code de la fonction.
L'émulation du système de
metafonctions en C++ utilise des goto
(cf Metafonctions
Les string et unistring sont passées sous forme de const string& et const unistring& dans les fonctions générées depuis les scripts. En effet les paramètres de fonction sont "input only" en script. Pour des raisons d'efficacité il est donc plus intéressant de les passer sous forme de référence (sans le const le code ne compile pas sous gcc dans le cas où la string entrante est issue du retour d'une fonction). A la compilation il s'est avéré que quelques (rares) fonctions scripts utilisaient les strings entrantes en read / write.
Note : la portée des variables définies dans les boucles for ( for (int i = 0.) ) en scripts correspond à l'ancienne norme C++. Dans Visual et gcc il existe une option permettant de compiler en utilisant cette norme plutôt que la nouvelle norme incompatible (Force Conformance in For Loop Scope = No, -fno-for-scope). Cela évite de devoir effectuer un grand nombre d'adaptations dans les scripts.
Les fonctionnalités difficiles à émuler en C++ qui ne sont pas (ou peu) utilisées par les Gpps ont été passées en 'deprecated'. Des warnings sont générés à la compilation des scripts, ces fonctionnalités ne doivent plus être utilisées.
Le mot clé break sur n niveaux break(n); n'a pas été émulé. Pour éviter ce type de construction le plus simple est d'utiliser une variable locale.
.Avant |
Après. |
for (i = 0 ; i < 10 ; i++) |
bool bExit = false; for (i = 0 ; i < 10 ; i++) } if (bExit) break; |
Le robot de compilation permet à tous les GPP
d'avoir un retour rapide sur
Lorsque la tâche de build de l'AI lib (ici GRAW2_AI_DLL, GRAW2_AI_DLL_RebuildAll correspondant au night build) est en état "failure" :
Double cliquer sur la ligne correspondante
Une page HTML s'ouvre => cliquer sur le lien BuildLogs Folder
Explorer s'ouvre sur un répertoire : ouvrir le dernier log .xml qui y est généré afin de déterminer quelle est l'erreur de compilation (noter le fichier et le numéro de ligne notamment). Cette description d'erreur est également envoyée sous forme de mail par le robot.
Cliquer sur le raccourci :
L'explorer se trouve maintenant dans le répertoire Yeti du robot => aller dans DLL\SRC
Ouvrir le fichier .cpp contenant l'erreur de compilation à la ligne spécifiée
Identifier le modèle, la fonction (ou état), l'erreur dans le source C++ pour corriger le script correspondant.
Les sources sont générés par la compilation des scripts dans un mode particulier générant le code C++ au fur et à mesure.
L'avantage de se placer à la compilation plutôt que générer les sources C++ à posteriori, à partir du bytecode est double :
Note : la génération s'effectuant à la compilation, elle s'effectue sur le code script pré-processé :
nous réintégrons les commentaires dans le code généré
il est également possible de réintégrer les macros les plus simples pour obtenir un code plus clair. Actuellement cette option n'est pas utilisée car elle implique de ne pas avoir de macros de même nom définies différemment. D'autre part elle ne gère pas les macros définies dans les corps de fonction. Or ces deux cas sont fréquents dans les scripts existants.
Le code de génération a été isolé le plus possible du code du compilateur, de manière à ne pas alourdir celui-ci. L'intégration du source generator dans le compiler utilise un design pattern "observer" : cela peut permettre d'implémenter facilement un système bi-pass dans le futur.
Un certain nombre d'éléments syntaxiques systématiques de la génération sont définis en externe dans des fichiers pattern stockés dans le répertoire \exe\DLL\AIE_Patterns. Via ces patterns il est possible de modifier en partie la forme du code généré sans recompiler, ni même relancer l'éditeur (les fichiers pattern sont rechargés à chaque génération). Ces fichiers servent à la génération des .h, .cpp et .vcproj.
C Par exemple, pour changer les options de compilation dans le fichier AIDLL.vcproj généré il suffit de modifier le pattern VC_Proj.txt.
Le code généré des AIs est placé dans une librairie ayant un minimum de dépendances avec le moteur ; le but premier est de pouvoir compiler la librairie sans avoir les sources du moteur (excepté le fichier AI_DLLInterface.h). Cette librairie utilise par ailleurs un ensemble minimaliste d'includes de la libc.
L'orientation choisie pour le développement est de modifier au minimum le système d'interprétation actuel pour minimiser les changements dans les scripts et AI fonctions existants.
Le code AI généré ne remplace que les scripts : il utilise la liste de fonctions AI du moteur de la même manière que les scripts.
Le code généré est placé dans un namespace AIDLL pour éviter tout problème de collision dans le cas où la lib est linkée en statique avec le moteur.
L'interface avec
Pour ce faire, les API mises en place
spécifiquement pour l'AI lib utilisent un système inspiré de
Avec ce système, une seule fonction globale AI_BootDLL est exportée explicitement
depuis
La table des AI fonctions constituant l'API fournie
par le moteur aux scripts, forme un système d'indirection similaire (table de
pointeurs de fonctions) même si le contrôle de typage dans ce cas, ne
s'effectue pas "naturellement" avec le compilateur (cf. API des
AI fonctions
Lors du boot de la lib :
Note : le moteur initialise les GenModel dans un ordre en opérant par couche : il commence par les superclasses puis remonte par couche dans les arbres d'héritage.
C
Les détails d'implémentations concernant
les différentes API sont traités dans le chapitre Emulation
des scripts en C++
Le code C++ généré utilise un certain nombre de principes permettant de coller au système de scripts de Yeti. Dans ce chapitre, nous étudierons les aspects particuliers de l'émulation, tout ce qui est spécifique ou s'éloigne d'un code C++ traditionnel.
Pour pouvoir émuler le système de scripts, nous avons besoin d'exporter des services supplémentaires en complément des services standards fournis par les AI fonctions.
Dans un premier temps nous avions intégré ces services sous forme d'AI fonctions mais cela avait plusieurs inconvénients :
Les services fournis par cette API couvrent des aspects variés spécifiques à l'émulation.
La lib fournit une API globale (AI_cl_DLLInterface) qui permet notamment au moteur d'adresser les objets de la lib.
Les instances de modèle constituent un deuxième niveau d'API fournissant des interfaces (AI_cl_GenModelInterface) permettant d'initialiser puis d'exécuter le code équivalent à chaque modèle script.
Les AI fonctions sont stockées dans Yeti dans une grande table de pointeurs de fonctions définie sous forme void**. Le pointeur de cette table est importé à l'init et stocké sous forme globale publique dans la lib.
A ce stade, nous ne disposons pas du typage permettant d'appeler correctement ces fonctions depuis le code C++. Pour résoudre ce problème, le système de génération de sources dispose d'un système spécifique générant un fichier header AI_GenPrototypes.h comprenant les définitions de prototypes pour toutes les AI fonctions. Ce fichier est inclus dans le pre-compiled header de manière à être disponible dans tout le code généré.
Tout d'abord nous générons un typedef pour tout pointeur de fonction correspondant à une AI fonction. Cette définition de type permet d'appeler les pointeurs de fonction avec les bons paramètres correspondant à la fonction moteur.
Exemple
typedef void T_GFX_ViewportPickable_1361 ( uint32, bool);
Pour chaque AI fonction nous générons deux prototypes de fonctions utilisables dans le code généré.
La première forme exécute l'AI fonction sur l'objet courant
La deuxième forme (suffixée avec un _) exécute l'AI fonction sur un objet passé en paramètre et effectue les changements de contexte de gao nécessaires sur l'interpréteur.
inline void GFX_ViewportPickable(uint8 _par0, bool _par1)
inline void GFX_ViewportPickable_ (object _obj ,uint8 _par0, bool _par1)
Ces fonctions sont définies en inline pour éviter potentiellement les doubles appels lors de l'utilisation des AI fonctions.
D'autre part, ces fonctions permettent d'adapter les paramètres de certains types.
Pour certaines AI fonctions, nous voulons pouvoir
customiser le système de génération de prototypes. Pour ce faire, nous avons créé
une deuxième version de
Cette version étendue de la macro permet d'ajouter un masque de bits caractérisant plus précisément la fonction lorsque cela est nécessaire.
Ce masque permet de définir que :
En effet, dans certains cas particuliers nous devons customiser l'intégration de certaines AI fonctions.
D'autre part, cela permet de ré-implémenter certaines petites fonctions (notamment certaines fonctions maths) sous forme de fonction inline directement dans la lib ce qui évite un appel de fonction coûteux (lorsque le code est compilé avec les optimisations).
D'autre part, une fonction qui n'utilise pas le contexte de gao, ne nécessite pas d'appel sous la forme objet.AIfonction. De fait il n'est pas nécessaire de générer le prototype de cette forme ce qui allège le code de AI_GenPrototypes.h
F Un système de vérification s'assure en dynamique, lors de l'exécution des AI fonctions que ces flags sont positionnés à bon escient. Ces vérifications s'effectuent dans les DLL non FINAL couplées avec le moteur non FINAL sur Xbox360 ou en mode éditeur sur PC. Ce système de vérification de flags est également utilisé pour rasteriser les AI fonctions dans le moteur non FINAL. (cf AI_FUNCTION_CHECK_FLAGS et class _ai_function_flags_checker
F
Dans le cas où il y aurait un
assert de type « AI function flag mismatch in . » à l'exécution,
cela voudrait dire que la fonction qui pose problème serait mal flagée. C'est
par exemple le cas pour les fonctions utilisant le string manager. Il faut dans
ce cas indiquer qu'il a besoin d'un modèle. Dans la ligne suivante on indique
que la fonction n'a pas besoin d'un GAO mais qu'elle a par contre besoin,
implicitement, d'un modèle.
M_DefineFunctionEx(.,AI_E_FuncAttrib_DoNotNeedGAOContext)
Exemple
inline uint32 COL_stGetRaycastAllReport_624(.
inline uint32 COL_stGetRaycastAllReport_624_(.
Ce nom suffixé est employé à la fois dans l'AI_GenPrototypes.h et dans le code généré à partir des scripts.
Note : au début du développement nous utilisions cette décoration systématiquement pour toutes les AI fonctions, cependant cela nuit à la lisibilité du code généré. Exploiter le système de flags nous a permis de réduire son utilisation aux quelques cas qui posent problème.
typedef void T_AI_EnableTrack_40 ( uint32, bool);
inline void AI_EnableTrack( uint8 _par0, bool _par1)
inline void AI_EnableTrack_ (object _obj ,uint8 _par0, bool _par1)
AI_cl_DLL::ms_oDLL.mp_DLLEngineInterface
->Interpretor_SetReferencedObject(pObjectBackup);
avec
inline void AI_StopInterpretation ()
Les espaces mémoire des variables d'instances et des variables de modèles continuent à être gérés par l'interpréteur. Pour garder ce principe, le code est réparti en deux classes C++ par modèle (au lieu d'une classe dans un programme C++ habituel). La première classe contient le comportement (code) du modèle (GenModel), la seconde sert à mapper les données du modèle (GenModel_Data). De fait, dans le code généré, les accès aux variables utilisent une indirection supplémentaire via un pointeur mpc_InstanceVar.
Outre le pointeur
sur les variables d'instance,
Toutes les classes dérivées de AI_GenModel possèdent des méthodes virtuelles (possèdent un pointeur sur vftable).
Une instance statique de chaque classe générée NomModel est créée par défaut. Lorsque l'interpréteur invoque le code d'un état c'est cette instance de classe modèle qu'il utilise via la méthode virtuelle Run. A l'appel du Run, il transmet l'adresse du gao et l'adresse des variables d'instance qui seront affectées au préalable dans l'instance.
Ces classes de
modèle générées sont identifiées initialement dans la lib par les clés de big
file correspondant aux instances de AI_cl_Model
issues du moteur. Les associations avec ces clés sont générées dans le code de
l'AI lib. A l'init de la lib, les clés de bigfile permettent d'obtenir les
adresses des instances statiques de chaque classe générée. Cette adresse
d'instance C++ est alors stockée de manière générique côté interpréteur dans
Chaque classe modèle générée possède un système d'identification statique et dynamique (virtuel) différent des clés de big file et disponible après init de la lib :
Les classes GenModel_Data réalisent un mapping mémoire rigoureusement équivalent à ce que fait l'interpréteur. Pour ce faire nous avons du trouver un dénominateur commun entre le compilateur de scripts, l'alignement fourni par Visual C++ et gcc. Pour ce faire :
nous avons ajouté quelques octets de padding dans le cas des héritages dans le compilateur de scripts
dans Visual nous utilisons des directives d'alignement pragma pack sur 4 octets et _declspec (align (4))
dans gcc nous utilisons la directive d'alignement __attribute__(align(4)) et nous ajoutons des octets de padding ( char__padding__[] ) dans les cas d'héritage
Note : ces classes réalisant un mapping mémoire rigoureux des buffers de variables de l'interpréteur, elles ne peuvent comporter de fonction virtuelle, ce qui aurait pour effet d'insérer un pfvtable qui casserait le mapping.
Le pointeur mpc_InstanceVar est casté dans le type courant par un accesseur inline This() défini pour chaque classe. Ainsi, This() renvoie toujours le type adéquat quelque soit la fonction ou état dans lequel on se trouve, y compris dans les cas d'héritage.
Le pointeur mpc_InstanceVar est affecté au préalable à chaque fois qu'un modèle est exécuté sur une nouvelle instance (lors de l'exécution d'un état par l'interpréteur ou lorsque l'on appelle une fonction via un objet model casté).
Les avantages de cette méthode de mapping sont :
L'utilisation d'une variable d'instance de modèle dans le code généré depuis le script prend donc la forme :
This()->NomDeVariableScript
Les variables de modèle sont modélisées par des variables statiques de type référence en C++.
Dans un premier temps nous avons envisagé d'appliquer le même système de mapping que celui employé pour les variables d'instance en générant une seconde classe dédiée aux variables statiques. Cependant avec les variables statiques, ce système ne fonctionne pas dans le cas d'arbre d'héritage. Voici un contre exemple mettant en évidence ce problème :
class a ; |
|
class b : public a; |
class c : public a ; |
. l'unicité de va, vb, vc fait qu'il n'est pas possible d'avoir à la fois va / vb et va / vc contigus en mémoire. De fait le système par mapping ne peut pas fonctionner.
Nous nous sommes donc acheminés vers un système consistant à définir les variables statiques sous forme de pointeur statique. Chaque variable de modèle correspond à un pointeur statique dans les classes GenModel_Data également utilisées pour les variables d'instance. Les aspects particuliers de notre implémentation sont :
struct S##VarName ;
pour utiliser une variable de modèle il suffit écrire NomDeVariable._
pour changer l'adresse de la référence nous utilisons une macro effectuant *((void**)&NomDeVariable) = nouvelle adresse
Note : il n'y a aucun cas particulier à gérer en génération car l'opérateur "." est le plus prioritaire en C++, donc il suffit d'ajouter systématiquement "._" au nom de variable à la génération
Note : dans un premier temps nous utilisions un système basé sur des union reference / pointeur void*, mais gcc refuse de placer une référence dans une union
Avec ce système :
static MyGenClass_Data* Static()
L'utilisation d'une variable de modèle dans le code généré depuis le script s'écrit :
Static()->NomDeVariableScript._
Le model cast est émulé via une classe template model_instance<Type>.
Cette classe est chargée de réserver sur la pile les 12 octets nécessaires au stockage d'une classe modèle dérivée de AI_GenModel (gao, var instance, pvftable) et possède les services permettant d'affecter ces données en fonction du type de modèle et du gao requis (pour ce faire, elle utilise une fonction moteur dédiée).
A la construction d'un model_instance le code client fournit un gao et un type de destination du cast : model_instance<TypeCast> (gao)
La fonction moteur chargée de retrouver l'instance correspondante détermine le type effectivement instancié sur le gao (la classe fournie par le client peut être une superclasse du type utilisé). Cette fonction retourne le pointeur sur l'instance statique C++ associée à cette classe. Une fonction Clone des AI_GenModel permet de créer un objet du même type à l'adresse fournie par le model_instance via un new placement (ce système permet d'affecter le pvftable correspondant à l'instance dans l'espace réservé par le model_instance).
Dans le code généré à partir des scripts (déclaration locale, déclaration membre, paramètre de fonction script), une structure est définie comme le type NomModel_Data correspondant.
Dans le système de scripts, les structs sont des éléments faiblement typés. Pour l'interpréteur, une struct est un pointeur + une taille. De fait, certaines AI fonctions prévues pour recevoir ou renvoyer une struct peuvent s'interfacer avec n'importe quelle structure spécifique du code script.
Pour émuler ce comportement nous utilisons une classe intermédiaire struct_specifier contenant l'adresse et la taille de la structure à transmettre. Les AIs fonctions transmettant des structures utilisent ce type.
Chaque NomModel_Data généré possède un constructeur à partir de struct_specifier et un operateur de cast en struct_specifier.
Exemple
AI_GaoSwitch_Data (struct_specifier _rSpec)
operator struct_specifier ()
Ce système fonctionne mais sa particularité est de provoquer, dans le cas d'un retour de fonction, un accès à une zone de stack qui n'est plus dans la stack frame de la fonction courante. Cela fonctionne car l'accès s'effectue en même temps que le retour, néanmoins cela impose de désactiver les options de vérification de l'accès à la stack dans le compilateur (Basic Runtime Checks = Default sous Visual, -fno-stack-check sous gcc). Dans le cas contraire des exceptions se déclenchent.
En mode _DEBUG un CRC est ajouté dans le struct specifier : il est calculé lors de la création du struct_specifier puis vérifié lors de la lecture du struct_specifier par la classe destination. De cette façon nous nous assurons qu'aucun écrasement n'est intervenu lors de retour de fonction sur les données de la structure.
Les arrêts d'interprétation effectués par certaines AI fonctions ou changement d'état sont émulés par des longjump. Les longjump sont utilisables car nous n'effectuons pas d'allocations devant être libérées absolument via les destructions d'objets déclarés en local (le seul cas qui s'y apparente est le cas des string mais il existe un garde fou qui libère les strings locales restantes en fin de trame). Nous avons essayé d'émuler ce système par des exceptions C++ ce qui est plus propre. Le problème est que la gestion d'une exception, lorsqu'elle se déclenche, est relativement lent. Or de nombreux stop interprétations se produisent à chaque trame : utiliser les exceptions alourdit énormément le temps d'exécution (x5).
En
script les interruptions d'interprétation s'effectuent immédiatement si
l'interpréteur traite le code d'un état de base. S'il traite le code d'une
fonction, ou le code d'un état exécuté comme callback (via un broadcast message
ou un event) l'interruption ne s'effectue pas immédiatement : elle ne se
déclenchera que lorsque l'interpréteur retournera dans le code de l'état
appelant. Notre système permet d'évaluer si nous nous trouvons dans le code
d'une fonction, et selon, traite l'interruption immédiatement où la diffère
jusqu'au prochain retour vers le code de l'état appelant (cf.Fonctions
scripts
Les AI fonctions existantes provoquant des interruptions d'interprétation modifient le flag mb_StopBaseInterpretation de l'interpréteur (lorsqu'un script requiert l'interruption de son interprétation, cette variable passe à vrai). Nous exportons un accès direct en lecture sur ce flag du moteur vers la DLL à l'init (par un pointeur const bool*). Ainsi depuis la DLL nous pouvons savoir à tout moment si une interruption d'interprétation a été requise sans modifier l'implémentation des AI fonctions.
Ces
AI fonctions sont caractérisées via M_DefineFunctionEx afin que le prototype correspondant comporte un test provoquant
l'interruption de l'exécution lorsque nécessaire. (cf 5.1.3.3
Le
contexte de longjump est stocké par
le Run de l'état de base. Lorsqu'un
broadcast message exécute un code, celui-ci est non interruptible. Par contre,
le broadcast message peut être appelé par un état de base interruptible : dans ce
cas, le broadcast message est susceptible de déclencher une interruption de
l'interprétation en sortie. Nous définissons donc les AI fonctions BM_SendMessage avec le flag
correspondant (cf 5.1.3.3
Dans l'interpréteur de scripts, l'identification des états s'effectue par un index (zero based) dans une table contenant les adresses des nodes de byte code correspondants.
Dans le code C++ nous identifions également les états par cet index.
Dans
chaque classe NomModel générée, une
table statique de pointeurs sur méthodes est déclarée. Ces pointeurs référencent
les méthodes de
Le même principe est appliqué à l'exécution des exit states référencés dans une seconde table.
Exemple
void PCBipedModel::InitState()
A l'exécution l'interpréteur utilise systématiquement la fonction virtuelle Run des AI_GenModelInterface. Cette fonction est chargée (entre autres) de ventiler l'exécution vers l'état courant grâce à l'index d'état passé en paramètre et à la table de pointeurs de méthode de la classe.
void PCBipedModel::Run( int iStateID, bool _bRunExitState,
int16& _iJumpLabelIndex, void* _pInstVar, void* _pMe)
else
else
AI_cl_DLL::ms_oDLL.EndInterpretation(iBackupFunctionOccur);
Les
changements d'état provoquent le plus souvent une interruption de
l'interprétation. Pour plus d'informations à ce sujet, consulter le paragraphe
précédent (cf Intégration
avec les AI fonctions existantes
Il existe plusieurs formes pour changer l'état courant dans les scripts.
o Dans les scripts la forme :
NomDEtat;
.provoque :
l'affectation de l'état comme état courant
l'appel de l'état suivant
l'interruption de l'interprétation
En C++ cela se traduit par :
o Dans les scripts la forme :
NomDEtat;
stop;
.provoque :
l'affectation de l'état comme état courant
l'interruption de l'interprétation
En C++ cela se traduit par :
F Afin de se rapprocher de la syntaxe scripts, il est envisagé de placer ces codes de changement d'état dans des fonctions inline générées en début de fichier cpp.
Les fonctions peuvent changer l'état courant en utilisant les AI fonctions dédiées. Dans ce cas le nom de l'état n'apparaît pas en clair (les AI fonctions utilisent l'index d'état).
Note : ces fonctions peuvent déclencher une requête d'interruption de l'interprétation
Les
metafonctions utilisées dans les scripts ne sont pas simples à émuler en C++ en
monothread. Le plus problématique pour l'émulation est de ré-entrer au niveau
de la dernière metafonction utilisée dans l'état. Pour ce faire nous utilisons
une série de gotos conditionnels générés en entrée de
Aspects de l'émulation des metafonctions :
Le système utilise les metafonctions existantes de Yeti. Notre méthode d'intégration permet de s'interfacer sur toutes les metafonctions utilisables dans le script. Actuellement seules Wait et WaitForNFrames sont routées dans la lib, mais il est possible d'ajouter facilement les autres formes disponibles dans Yeti.
Une fonction dédiée du DLL engine (Interpretor_PrepareForMetaFunction) permet de préparer le système à l'exécution d'une metafonction moteur.
Un numéro de label de saut identifie la dernière metafonction exécutée dans le code de l'état courant, permettant de réentrer à l'endroit adéquat au prochain réveil de l'AI.
Une série de macros offrent une utilisation synthétique du système proche du code des scripts (l'utilisation de macros est nécessaire pour former les gotos et labels).
L'interruption de l'exécution en cours.
Nous avons simplifié l'interpréteur afin d'unifier le comportement en scripts et C++ : le contexte de métafonctions n'est plus empilé lors d'un appel de fonction dans l'interpréteur. C'est inutile et cela provoque d'étranges comportements en scripts dans de rares cas qui diffèrent du comportement C++.
Exemple de code généré utilisant des metafonctions Wait
void AI_BirdFlock::AI_BirdFlock_Init(int16 &_iJumpLabelIndex)
if( !DYN_bIsReady() )
return;
if( This()->mul_NumberOfBirds == 0 )
}
This()->muc_TypeOfBirdToSpawn = This()->muc_TypeOfBirds;
This()->mul_NumberOfBirdsToSpawn = This()->mul_NumberOfBirds;
// Compute flight direction (One object and one direction per frame)
if( This()->muc_TypeOfBirdsDir== 0 )
}
Static()->GComputeDirectionLocked._ = 0 ;
M_Wait( 1 , 1 );
}
if( This()->muc_ValidFlightDirections== 0 )
}
}
F
F Le switch généré en début de fonction permet de gérer les cas de réentrance après réveil de l'AI suite à l'exécution d'une métafonction. Lorsque l'état s'exécute sans faire suite à un appel de metafonction, _iJumpLabelIndex vaut -1.
F Dans l'interpréteur, l'index de jump label est stocké dans une variable supplémentaire stockée dans les instances de classe AI_cl_Track.
F On peut constater que les problèmes de restauration de l'état des variables locales sont similaires à ceux rencontrés dans le script (dans l'exemple, l'utilisation de variables membres pour stocker les compteurs de boucle remédie au problème).
Les exécutions conditionnelles every et enterstate sont réalisées sous forme d'une macro formant un if et des accolades. Des fonctions moteurs implémentées dans l'API DLL engine permettent de déterminer si l'exécution doit s'effectuer ou non.
#define every(i) if(AI_cl_DLL::ms_oDLL.mp_DLLEngineInterface->b_Interpretor_EveryFrame(i))
#define every_F(f) if(AI_cl_DLL::ms_oDLL.mp_DLLEngineInterface->b_Interpretor_EveryDuration(f))
#define enterState if(AI_cl_DLL::ms_oDLL.mp_DLLEngineInterface->b_Interpretor_EnterState())
Note : afin de gérer l'utilisation des metafonctions au sein des enter
state, le code de réentrance des metafonctions est généré avant la gestion de l'enterstate (cf Metafonctions
Les fonctions scripts sont traduites sous forme de fonctions membres dans les modèles C++.
int32, int32 Function (int32 _iInputOnly)
En C++, les paramètres de retour supplémentaires sont passées sous forme de pointeurs.
int32 MyModel::Function (int32* _p1, int32 _iInputOnly)
Exemple
class model_test
object oTest = (model_test*) obj;
uint16 iVal = oTest.Accessor();
C dans le cas du script, la variable m_VarMembre ne sera pas tronquée et sera copiée entièrement dans iVal. Pour garantir le même comportement en C++, nous retournons toujours la valeur avec la dynamique maximale (32 bits) laissant la responsabilité au code client de tronquer éventuellement la valeur, lors de l'affectation dans la variable de destination.
De nombreuses AI fonctions se réfèrent à un contexte courant stocké dans l'interpréteur (gao courant, modèle courant, instance courante.).
Il faut donc affecter le contexte courant de l'interpréteur de manière à ce que les AI fonctions se comportent de la même manière lorsqu'elles sont utilisées.
Lorsque l'on exécute un état depuis l'interpréteur, le contexte a été fixé préalablement.
Les cas qui nécessitent un changement du contexte sont :
La gestion des changements de contexte à l'appel et au retour des fonctions en script s'effectue au début et à la fin de chaque fonction C++ correspondante.
Afin de gérer simplement tous les cas de sortie prématurée pouvant intervenir dans du code via return, nous utilisons la résolution de portée gérée par le compilateur C++. Pour ce faire nous déclarons une instance locale (nommée _) d'une classe spécifique _context_swapper ayant un destructeur chargé de restaurer le contexte en sortie. Le constructeur affecte le contexte courant en entrée. Contructeurs et destructeur basculent le contexte en utilisant des fonctions moteur dédiées via l'API DLLengine.
L'utilisation du swap de contexte dans les fonctions générées depuis les scripts s'écrit sous cette forme :
void MyModel::Fonction ()
Il existe deux constructeurs différents :
Note : la fonction moteur d'affectation du contexte courant pour gao + modèle est plus coûteuse que celle assignant seulement le modèle. En effet elle doit parcourir les tracks du gao pour identifier précisément toutes les informations d'instance nécessaires dans le contexte de l'interpréteur.
Note : l'utilisation du GetType() au lieu de msp_Model à l'appel, permet de remplacer les uc_SearchModelEx par des uc_SearchModel moins couteux lors de cette recherche.
La classe de changement de contexte _context_swapper est également chargée d'émuler les interruptions d'interprétation différées de l'interpréteur de scripts.
En effet, en script, certaines interruptions d'interprétation ne s'effectuent pas immédiatement : elles ne se déclenchent pas tant que l'interpréteur traite le code d'une fonction. De fait, elles se déclenchent au moment où l'interpréteur retourne dans le code de l'état appelant.
Un compteur stocké dans
F Dans le cas où la fonction ne nécessite pas de changement de contexte mais est susceptible de provoquer l'interruption de l'interprétation en sortie (appel à BM_SendMessage par exemple), une classe n'effectuant que ce travail est utilisée (optimisation) : il s'agit de la classe _stop_interpret_manager.
Comme décrit au paragraphe API
des AI fonctions
inline void GFX_ViewportPickable_ (object _obj ,uint8 _par0, bool _par1)
Le swap du context dans une fonction n'est utile que dans le cas où :
Donc dans les fonctions qui :
et
. il n'est pas nécessaire de générer de swap de contexte.
F De fait, il est important de positionner les flags spécifiant quelles AI fonctions n'utilisent pas le contexte de l'interpréteur. De cette manière, le code généré peut gagner en efficacité.
A l'origine le type object était décrit par une classe object implémentant les opérateurs nécessaires (comparaison, cast.).
Après différentes simplifications, il s'est avéré que la classe object pouvait être remplacée par un simple pointeur. Néanmoins, pour garder un typage fort en C++ nous prémunissant de cast implicites non souhaités dans le code généré, nous avons défini une classe game_object et l'object comme typedef game_object* object.
Par ailleurs la classe game_object mappe le champ name (ou big_file key sur Xbox360) permettant d'identifier les gao facilement dans le debugger.
Dans l'interpréteur de scripts il existe un type particulier "Any" qui n'est utilisé qu'avec les AI fonctions d'accès aux tables (TAB_.). Dans un premier temps, de manière à avancer rapidement sans complexifier la génération de sources nous avons créé une classe Any permettant de transtyper tous les types de manière à ce que la lib compile. Ce système fonctionnait mais était relativement risqué : nous avons constaté notamment que certains endroits n'ayant aucun rapport avec Any ne compilaient que par l'intermédiaire de cette classe qui était utilisée implicitement par le compilateur pour transtyper certaines données de manière "contre-nature".
Ce type étant utilisé uniquement pour les
fonctions TAB_, nous avons décidé de
gérer un cas particulier dans la génération qui transforme le nom de
Par exemple
Les AI fonctions TAB_... correspondantes sont
alors définies en "DoNotGenerate"
via
De cette manière :
De manière similaire (dans l'idée) à ce que
fait l'interpréteur, nous identifions les strings définies en locales des
strings membres en fonction de l'adresse this.
Si this est une adresse de la stack, la variable est locale.
Dans un premier temps nous avons mis en place les classes string et unistring en interfaçant directement les fonctions du string manager via l'interface du DLL engine et en essayant de reproduire le plus fidèlement possible le comportement de l'interpréteur.
Cependant cette méthode engendrait des leaks conduisant
rapidement et systématiquement à des remplissages du string manager (nous avons
essayé de nombreuses solutions au niveau de
La stratégie employée pour les strings par le
système de scripts est de transférer la responsabilité de désallocation aux
clients potentiels de
Le transfert de la responsabilité de désallocation aux clients, rend le système sensible et peu robuste aux micro-changements induits par le compilateur C++ par rapport à l'interpréteur.
Après de nombreux essais infructueux nous nous sommes résignés à adapter légèrement la stratégie d'allocation du string manager tout en essayant de modifier le moins de code possible dans l'existant.
L'idée est de se rapprocher de la stratégie habituelle des classes d'encapsulation des strings en C++ (STL notamment) en termes de responsabilité concernant le cycle de vie du buffer : une string est responsable de l'allocation et de la désallocation de son buffer.
Pour ce faire :
Nos types string et unistring possèdent un destructeur chargé de désallouer la string
Nos types string et unistring effectuent une copie systématiquement lors d'une affectation ou concaténation.
Les passages de paramètres avec les AI fonctions s'effectuent par pointeur sans copie. Ces passages s'effectuent par l'intermédiaire des types string_address et unistring_address qui permettent un traitement particulier notamment en sortie d'AI fonction.
Les passages de paramètres dans les fonctions s'effectuent sans copie sous forme const& car en script les paramètres de fonctions sont input only.
Le string manager (AI_cl_StringManager) est enrichi d'un mode d'exécution spécifique AI C++ qui permet de modifier la stratégie de gestion des strings :
o le système d'identification de la variable référençant une string est ignoré
o les désallocations en provenance des AI fonctions sont ignorées
o les désallocations en provenance du code C++ sont toujours traitées
o un service supplémentaire permet de flager une string après création comme étant locale ou membre. Cela est utile pour assigner par pointeur dans son instance C++, une string allouée au sein d'une AI fonction (ce n'est qu'à l'affectation dans la variable que l'on peut savoir si elle est locale ou non).
Le système de gestion des tables (AI_cl_Table) est enrichi d'un mode d'exécution spécifique AI C++ qui permet de modifier la stratégie de gestion des tableaux de strings et d'unistrings :
o de même que pour un vector de string STL, les strings sont copiées lors de leur affectation dans le tableau.
o lors d'un remove ou d'un empty du tableau, les strings sont libérées
o lors d'un décalage des strings dans le tableau, il n'est pas nécessaire de mettre à jour le système de référencement du string manager puisque celui-ci est ignoré en mode AI C++
F dans le cas d'un Get ou d'un Top la string est copiée, dans le cas d'un Pop elle est passée par pointeur puis flagée comme locale ou non (optimisation) => voir code des AI fonctions correspondantes dans AI_Prototypes.h
F Le cas de strings membres de structs utilisées dans un tableau dynamique de structs n'est pas géré correctement par le système. Cependant les effets sont plus critiques en mode C++ qu'en mode script. Il ne faut pas utiliser de telles constructions. Un warning est affiché par le compilateur de scripts dans ces cas.
Le système d'interruption de l'interprétation
repose sur des fonctions C longjump
et non sur le mécanisme d'exception C++ pour des raisons de performances (cf Interruption
de l'interprétation
Dans l'interpréteur de scripts, les tableaux statiques de booléens utilisent un codage par bit et non par octet comme le fait C++.
La classe template AI_BoolArray permet d'obtenir des tableaux de booléens identiques en termes d'utilisation et de mapping mémoire bit à bit à ceux gérés par l'interpréteur. Seule la déclaration du tableau diffère légèrement du script.
Ce système convient également à l'émulation des tableaux de booléens à deux dimensions (tableau statique de tableaux de booléens bit à bit).
Les scripts proposent deux types mathématiques
natifs : le vector 3d et la matrice 4x4. L'émulation dans
Les vector et matrix possèdent des constructeurs avec ou sans init des valeurs par défaut.
Pour optimiser le passage de paramètres dans les AI fonctions, les vector et matrix sont transmis par référence. Le problème est qu'un tel typage ne compile pas dans tous les cas sous gcc qui refuse de passer une référence non const sur une instance temporaire, ce qui est le cas lorsque l'instance émane d'un retour de fonction par exemple. Dans les adapteurs générés pour les AI fonctions, les paramètres sont donc définis en const vector& et const matrix&, le specifier const est ensuite "annulé" par cast.
Dans les scripts Yeti il est possible de référencer des ressources moteur de différents types directement par leur nom. Ces références sont exploitées via des AI fonctions dédiées à l'utilisation de chaque type.
La plupart de ces références sont traitées par les AI fonctions comme l'adresse d'un gao. D'autres (gui et world) sont reconnues comme des clés de fichiers de big file.
Dans le code C++, à chaque référence identifiée dans le code script :
Une track T d'un gao G est en attente sur une métafonction
Un autre objet appelle une fonction sur G qui change l'état de T avec un SetNextState : cette opération annule l'état d'attente sur métafonction de T.
En sortant de la fonction script
le contexte de métafonction est restauré par l'interpréteur =>
F Ce fonctionnement n'est pas intuitif pour les Gpps et l'empilement du numéro de métafonction courante n'est pas réellement utile : nous l'avons donc supprimé de l'interpréteur. De cette manière nous obtenons un comportement équivalent en scripts et en C++.
Exemple int_p, curve_p, string_p
Note : les handles de table renvoyés par les AI fonctions ne sont pas des pointeurs
Comme mentionné dans les paragraphes
précédents, gcc est moins tolérant sur
Cependant gcc nous pose des problèmes d'une autre nature : il supporte mal la montée en charge contrairement à Visual. Il produit notamment des fichiers beaucoup plus gros que Visual pour un même travail (precompiled header gch de 160 Mo contre 28 Mo pour le pch, lib debug de 350 mo contre 70 mo.) et se trouve ensuite incapable de les gérer correctement.
Nous avons du optimiser la liste des includes que nous plaçons dans le precompiled header. A l'origine le gch pesait 160 Mo, mais gcc était incapable de l'utiliser lors de la compilation des fichiers .cpp (message d'erreur "had to rellocate pch"). La compilation avec precompiled header est de l'ordre de 15 min, sans le gch le temps s'envole à environ 2 heures ce qui n'est pas envisageable d'un point de vue pratique.
Dans un premier temps, nous avons eu l'idée de répliquer le modèle d'includes des scripts disponibles dans les infos de pre-processing du compilateur de scripts : les includes déplacés dans leurs .cpp clients ne figurent plus dans le precompiled header... Malheureusement, nous avons rencontré des problèmes d'inclusions cycliques insolubles, qui ne semblent pas poser problème en script, mais qui ne compilent pas en C++.
Dans un second temps, nous avons décidé d'appliquer ce principe uniquement sur les fichiers générés par le pattern editor (fichiers script dont le nom contient un $) : en effet le code généré par le pattern editor est plus simple, systématique et n'engendre pas ces problèmes d'includes cycliques. Les AI écrites par les GpP restent incluses via le precompiled header. Cette optimisation a permis de réduire la taille du precompiled header à 97Mo, taille que gcc est capable d'exploiter lors de la compilation (la limite est à 128Mo).
Les instanciations statiques des classes modèles sont désormais réparties dans chaque fichier .cpp généré des AI_GenModel. A l'origine, elles étaient concentrées dans AI_GenDllEntry.cpp. Avec les optimisations activées (release), gcc bloquait sur la compilation de ce fichier (boucle infinie ?).
D'autre part il n'est pas possible d'utiliser le mode de génération "tout dans un seul metafile" : contrairement à Visual, gcc est incapable de compiler le fichier généré de 650000 lignes et affiche un message de dépassement.
Lorsque les informations de debug sont
activées,
Pour contourner ce problème une solution est d'appliquer l'option de compilation
-mminimal-toc sur toutes les libs du projet (y compris l'AI lib) en debug.
Cette option réduit potentiellement
l'efficacité du code généré mais cela n'est pas gênant puisque cela concerne
uniquement
Les méthodes de passage de paramètres vers les AI fonctions de Yeti depuis le code script ne sont pas très rigoureuses. Le code C++ généré étant calqué sur le système scripts nous rencontrons des problèmes similaires. Pour que le code fonctionne en niveau d'optimisation -O2, il faut ajouter l'option -fno-strictaliasing (cette option a également été utilisée sur l'aiengine pour le mode interprété). Cette option désactive une optimisation incompatible avec les passages de paramètres non rigoureux.
Comme décrit précédemment (cf Problèmes
connus de syntaxe
La technologie fournie par Sony pour charger des modules de code dynamiquement est le système de PRX. Malheureusement ce système est beaucoup plus spécifique et restrictif que le système de DLL de Microsoft.
L'adressage du code ou des données stockées dans le PRX depuis l'elf est un système spécifique implémenté à la compilation qui translate les adresses. En conséquence il n'est pas possible d'appeler des fonctions du PRX via un pointeur de fonction, ni d'adresser une donnée statique du PRX via un pointeur.
Dans le cas de la lib nous utilisons des
interfaces abstraites "COM like". Dans le cas du PRX il n'est pas
possible d'appeler directement une méthode virtuelle de la lib via un pointeur
d'objet depuis le moteur vers la lib (le contraire fonctionne). L'utilisation
de PRX pour
F Ces contraintes nous ont amené à envisager l'utilisation du link statique sur PS3 (plus simple, plus performant et portable). Si l'on veut permettre aux équipes de production de construire l'elf avec une lib AI sans avoir les sources du moteur, il est envisageable de leur livrer les fichier lib des modules moteur. Un fichier projet spécifique leur permettra de linker ces lib et construire l'elf exécutable.
PC / Xbox360 |
Moteur |
AI_cl_GenModelInterface* poGenModel = m_pDLLInterface->GetGenModel (gpo_BigFile->ul_GetFileKey(ul_FileIndex)); poModel->vSetGenModel ( poGenModel ); if ( poGenModel != NULL ) |
DLL |
class MyModel : AI_GenModel MyModel::AssignStaticData (void* _pAdr, void* _pModel) |
PS3 |
Moteur |
AI_cl_GenModelInterface* poGenModel = AI_DLLInterface_GetGenModel (m_pDLLInterface, gpo_BigFile->ul_GetFileKey(ul_FileIndex) ); poModel->vSetGenModel ( poGenModel ); if ( poGenModel != NULL ) |
PRX |
// PRX declaration SYS_MODULE_INFO( AIDLL_Module, 0, 1, 0 ); SYS_MODULE_START( _start ); SYS_MODULE_STOP( _stop ); int _start(void); int _stop(void); int _start(void) int _stop(void) SYS_LIB_DECLARE(AIDLL, SYS_LIB_AUTO_EXPORT | SYS_LIB_WEAK_IMPORT); // Wrappers SYS_LIB_EXPORT(AI_DLLInterface_GetGenModel, AIDLL ); AI_cl_GenModelInterface* AI_DLLInterface_GetGenModel (void* _pDLL, uint32 _uiKey) SYS_LIB_EXPORT(AssignStaticData, AIDLL ); void AssignStaticData (void* _pGenModel, void* _pAdr, void* _pModel) // Code class MyModel : AI_GenModel MyModel::AssignStaticData (void* _pAdr, void* _pModel) |
Avec CodeAnalyst l'échantillonnage est beaucoup plus précis sur une machine ayant un processeur AMD. Sur Intel la période minimale est limitée à 1ms.
Le système de rasters spécifiques développés sur Graw pour l'AI script a été adapté pour fonctionner avec l'AI C++.
Le système s'active via les fichiers SDK\RASter\RAS_SpecRaster.h et SDK\RASter\RAS_SpecRaster.cpp.
La compilation de ces 2 fichiers doit être activée dans le vcproj, car ils sont exclus du build par défaut.
Dans SDK\RASter\RAS_SpecRaster.h il faut :
Définir le flag #define USE_SPEC_RASTER
Dans SDK\RASter\RAS_SpecRaster.cpp il faut :
Activer le raster spécifique "Callstack" :
const bool C_abManagedFamily[ERasterFamily_Number] =
Définir un des deux flags (exclusivement) #define FIND_SLOW_CALLSTACK ou
#define ANALYZE_CALLSTACK en fonction du mode de raster désiré.
FIND_SLOW_CALLSTACK liste la liste d'états de base exécutés sur une trame avec le temps d'exécution correspondant
ANALYZE_CALLSTACK liste la callstack complète avec les temps d'exécution pour chaque fonction à partir de l'état de base spécifié par la constante const char * C_szScriptToAnalyze = "Track 0x3407a8da State 1"; (la valeur hexa est la clé big file du model à analyser)
Le système dump toutes les 2 secondes l'ensemble de deux traces dans le log :
une trace correspondant à
une trace correspondant à la somme des temps d'exécution sur les 2 secondes. Dans le cas du mode callstack, le système identifie par callstacks identiques.
F En AI C++, en raster mode callstack, les index de fonctions ne sont pas mis à jour. La trace ne spécifie que le modèle contenant la fonction.
Voici quelques statistiques actuelles sur l'AI de la version officielle de Graw 2. Les libs utilisées pour ces tests ont été construites en incluant les AIs patterns pour toutes les maps officielles.
Nous obtenons un gain d'environ 50% sur le raster global de l'AI. Une statistique effectuée en début de projet conférait 60% du temps global AI à l'interprétation (40% aux AI fonctions), cela signifie que nous obtenons 600% de gain en performance sur l'interprétation. Cette estimation est corroborée par des mesures récentes : en DLL non FINAL nous disposons désormais d'un raster séparé pour l'interprétation et les AI fonctions.
Taille des fichiers :
taille des .dll (ou .xex) release sans infos de debug
différence sur la taille de l'elf final strippé dans le cas du link statique
PC dll |
Xbox360 dll |
PS3 lib (delta elf strippé) |
|
favor small code |
4480 ko |
5224 ko |
21.8Mb - 14.5Mb = 7.3Mb |
maximize speed |
6384 ko |
6600 ko |
31Mb - 14.5Mb = 16.5Mb |
maximise speed (-O3) |
X |
X |
31Mb - 14.5Mb = 16.5Mb |
favor small code + LTCG |
4532 ko |
5192 ko |
X |
maximize speed + LTCG |
6444 ko |
5448 ko |
X |
F ne pas charger le byte code script d'une map économise environ 3Mb
F le code AI est chargé d'un seul bloc et non en deux DLLs partie fixe / partie map car il y a des dépendances qu'il faudrait casser dans les classes de base des patterns
Quelques temps de compilation sur un Bi Xeon 3Ghz / 3Gb de RAM.
Target |
Compilation |
PC dll |
Xbox360 dll |
PS3 lib |
release avec infos de debug |
Locale Incredibuild |
6 min 30 2 min 39 |
3 min 29 | |
release sans infos de debug |
Locale Incredibuild |
2 min 05 |
7 min 40 X |
|
debug |
Locale Incredibuild |
3 min 20 3 min 43 | ||
finale (LTCG) |
Locale Incredibuild |
5 min 36 |
7 min 35 |
Dans le système actuel, l'effort de
développement s'est focalisé sur la fidélité de l'émulation des scripts par
En utilisant le mécanisme d'exception handler du système win32, il est peut-être possible d'effectuer des récupérations sur erreur lorsqu'un plantage intervient dans le code de l'AI lib. Tant qu'il n'y a pas d'écrasement mémoire irrémédiable, l'erreur peut-être récupérée sans engendrer l'arrêt de l'éditeur. Une fois l'erreur détectée par le système il suffit de stopper le jeu, puis sauter à un endroit sécurisé de la boucle moteur (longjump ?)
En adaptant les scripts, il sera possible d'activer le système d'intégration des macros (simples) dans le code généré : dans les scripts il faut supprimer les cas de macros portant le même nom et ayant des valeurs différentes, il faut éviter les déclarations de macros au sein des fonctions.
Il est possible d'automatiser (via OLE ?) Visual depuis l'éditeur d'AI Yeti. Cela permettrait de commander Visual depuis l'éditeur d'AI afin de rendre le travail des Gpps plus confortable. Voici quelques possibilités :
lancer Visual et ouvrir le projet AIDLL automatiquement
passer Visual en avant plan
ouvrir les fichiers .h et .cpp correspondant à un modèle sélectionné dans l'éditeur dans Visual
déclencher les build / rebuild depuis l'éditeur
lancer et connecter le debugger sur la DLL
placer un breakpoint
automatiquement sur
Il est également possible de pousser plus loin les différentes pistes d'optimisations :
Continuer à mettre à jour les flags des AI fonctions n'utilisant pas le context pour les nouvelles AI fonctions qui sont développées
Ré-implémenter en inline dans la lib toute les AI fonctions simples pour lesquelles l'appel est couteux relativement au contenu de la fonction (fonctions math notamment type sqrt.).
Passer les matrix et éventuellement vector en entrée de fonction en const& au lieu de les passer par valeur. Cela requiert des adaptations dans les scripts modifiant la valeur des paramètres entrants.
Optimisations bas niveau en améliorant le code des fonctions (memcpy.)
Eviter la multiplication des
appels de fonctions intermédiaires pour positionner le contexte : il est
envisageable de stocker, à l'init de
Mieux optimiser les changements de contexte : pour ce faire la génération pourrait s'effectuer en deux passes. Une première passe de compilation script servirait à construire le graphe d'appel des fonctions script. La deuxième passe qui effectuerait la génération du code (comme actuellement) disposerait du graphe d'appel pour optimiser plus efficacement les changements de contexte. En effet, il est inutile de générer un changement de contexte dans une fonction qui n'utilise que des fonctions qui, elles-même, n'utilisent pas le contexte.
Actuellement l'identification des modèles utiles pour la version par rapport aux modèles de test s'effectue seulement pour les scripts de pattern par sélection des répertoires correspondant aux maps officielles (excepté les répertoires spécifiés dans le fichier editor.ini). Il serait intéressant d'ajouter une propriété au modèle par un mot clé dans le fichier .vas permettant de spécifier que la classe ne figure pas dans le game data de la version finale. Cela permettrait d'optimiser la taille du code de la lib lorsque les sources sont générés par l'option "generate sources - official maps".
Le travail actuel en C++ sous forme de génération automatique en post build permet de migrer en souplesse vers un système C++. Par contre il rallonge considérablement le process et impose une partie du travail en double.
Actuellement mise au point scripts puis génération / compilation C++ |
Avec un système Gpp C++ |
1. modifications scripts compilation scripts 2. génération des sources C++ 3. compilation de la DLL |
1. modifications C++ compilation DLL |
A une étape clé (changement de projet), il est envisageable de migrer sur un mode de développement Gameplay programming purement C++. Dans cette optique il est très important de garder le meilleur des deux mondes scripts et C++, afin de conserver un mode de développement réactif et souple permettant des cycles développement / test courts.
Avec cette méthode de travail, les Gpps écrivent le code en C++ et archivent sur une base perforce dédiée.
Partant de la version actuelle de Yeti, voici les évolutions techniques possibles pour y parvenir en tirant parti de l'existant.
Le fait que les structures de données (variables d'instance, variables de modèle) soient connues et gérées par l'éditeur présente plusieurs avantages :
Les données métiers peuvent être éditées simplement via l'éditeur (notamment par les Level Designers)
Le code AI ne maintient pas lui-même les données :
o
cela facilite le rebuild et reload
à chaud de
o
cela évite d'effectuer les
allocations mémoires côté DLL, et/ou d'avoir à reconstruire les objets. Le code
C++ de
En termes de compatibilité avec l'existant :
o cela permet de conserver la granularité des AI instances par rapport aux GAO, le système de tracks actuel.
F Une solution qui évite de tout re-développer :
Afin de faire cohabiter code généré et code écrit manuellement il serait souhaitable d'ajouter un mot clé dans les fichiers .vas pour déclarer si le modèle correspondant est écrit directement en C++ ou en script.
F On peut noter que de nombreux moteurs de jeu (notamment ceux orientés édition) possèdent un système de réflexivité des données permettant à l'éditeur et parfois même au moteur de manipuler de manière générique les données métier :
o Unreal possède un système de réflexivité avec les fichiers .uc. Les classes C++ sont générées à partir des fichiers .uc.
o Renderware permet d'exporter des variables pour l'édition qui sont taggées dans le code C++.
o Dans les autres moteurs d'Ubi : GDS possède un système de réflexivité pour l'édition des données dans les plug-ins Max (l'expose system), Assassin possède également un système de réflexivité (fichiers descriptifs object models) .
Actuellement la table de références sur data est générée de manière globale en considérant la liste de toutes les data spécifiées sous forme texte (noms de ressource) dans le code script et résolue à la compilation script. Cette liste est toujours optimale puisqu'elle contient la liste des références utilisées. En effet il n'est pas possible de générer par défaut une table de références pour toute les data du jeu.
Sans compilation script, pour garder une efficacité comparable, il faut ajouter un mécanisme spécifique pour l'import de données. Dans la solution décrite dans le paragraphe précédent, les fichiers .vas continueront à être utilisés pour déclarer les classes. Le plus simple serait d'ajouter une syntaxe spécifique dans ces fichiers pour importer une donnée.
Exemple
import NomDeRessource;
Actuellement les références sont générées dans un fichier .h global inclus dans le header precompilé stdafx.h. Conserver cette architecture physique signifie qu'ajouter un import de donnée provoquerait la recompilation complète de la DLL.
L'autre solution est de générer les références uniquement dans des fichiers .cpp :
générer les macros définissant les
index de ressource ou clés de big file (selon le type de donnée, cf. Références
sur ressources moteur
pour les références de type object, garder la table de correspondance index / adresse gao dans le fichier AI_GenDllEntry.cpp
F pour que cette stratégie fonctionne il faut que les références existantes restent à des index stables dans la table en cas de suppression / ajout afin d'éviter de modifier de nombreux gen model. Pour ce faire il faut conserver l'état de cette table de manière persistante dans l'éditeur et lui appliquer une gestion à "trous". La table générée dans AI_GenDllEntry.cpp sera le reflet de cette table stockée côté éditeur.
Actuellement la syntaxe du code généré de l'AI C++ est conçue pour faciliter la génération depuis le code script et minimiser les modifications dans l'interpréteur. Dans le cas où les gameplay programmers travaillent directement en C++, il est possible de concevoir une syntaxe plus adaptée à l'écriture manuelle.
Dans l'idée de développer directement en C++, un élément qui simplifierait grandement la syntaxe serait d'unifier les classes GenModel et GenModel_Data. Les changements techniques que cela implique sont :
template<class TModel> TModel* model_cast<TModel> (object _pGao)
else
}
return pGenModel;
L'utilisation prendrait la forme :
MyModel* poModel = model_cast<MyModel> (me);
poModel->MyFunction ();
X = poModel->MyVariable;
Une première méthode est de rester proche du système existant : elle consiste à générer le changement de contexte en même temps que le prototype de fonction. Cela est tout à fait utilisable, mais empêche d'optimiser les changements de contexte en supprimant ceux qui sont inutiles (sinon il faudrait re-parser le contenu de la fonction à un moment pour générer correctement son header). Si les changements de contexte sont systématiques, il faudra travailler sur l'optimisation de leur implémentation en permettant d'accéder directement aux éléments à modifier dans l'interpréteur depuis la lib (pour éviter les appels fonctionnels).
Si l'on souhaite alléger la notation et la charge d'exécution en supprimant l'affectation du contexte courant au sein de l'interpréteur, il faut s'intéresser :
F Les AI fonctions utilisant le contexte étant minoritaires cela semble être une orientation intéressante : c'est la solution la plus propre, la plus évolutive, et économique en termes de performance en permettant la suppression des changements de contexte.
F
Pour les fonctions AI utilisant le
contexte, il est possible l'alléger la syntaxe en générant le wrapper inline comme
méthode de
Proposition
Les wrappers d'AI fonctions n'utilisant pas le contexte sont générés comme actuellement dans AI_GenPrototypes.h
Les wrappers d'AI fonctions utilisant le contexte sont générés dans un fichier AI_GenPrototypesWithContext.h
Dans le fichier AI_GenModel.h, dans la déclaration de classe AI_GenModel les wrappers sont inclus sous forme de méthode :
class AI_GenModel : public AI_cl_GenModelInterface
bool operator != (const AI_GenModel& _Obj)
operator object() const
object* operator& ()
virtual void* GetType() const
virtual void Run( int iStateID, bool _bRunExitState,
int16& _iJumpLabelIndex, void* _pInstVar,
void* _pMe, bool _bInteruptMode)
virtual AI_GenModel* Clone (void* _pPlacementAdr,
void* _pInstanceVar, object _me) = 0;
#include "AI_GenPrototypesWithContext.h"
};
F par ailleurs, on peut craindre que l'utilisation des long jumps soit bloquante pour l'évolution future du système (impossibilité de compter sur l'exécution des destructeurs des variables locales)
class MyModel
.
avec
class AI_cl_DLL
void LeaveFunction()
}
}
.
Le string manager pose des problèmes de désallocation dans un certain nombre de cas en script. Sa stratégie le rend sensible à l'utilisation des chaînes dans le script : il faut bien respecter un certain nombre de guidelines pour éviter les problèmes de désallocation ou d'écrasement de chaines. De ce fait, il y a une large demande des équipes dev et gpp pour un re-factor du string manager résolvant ces problèmes.
En C++ la stratégie le rend un peu plus robuste au niveau des désallocations cependant il y a des limites au système. Voici quelques idées concernant l'évolution du système :
le système ne gère pas correctement les tableaux dynamiques de structs contenant des strings. De manière générale, les structs sont passées sous forme non typées aux AI fonctions (adresse + taille). Donc une fois transmis dans l'AI fonction, aucun constructeur ou destructeur correspondant à un membre de la struct pour ne pourra être exécuté automatiquement par le langage. Si l'on veut résoudre ce problème pour du code C++ écrit à la main, je pense qu'il faudrait remonter le concept de container au niveau de la lib (une classe template vector par exemple) : la gestion du container s'effectuerait côté lib, les opérations d'allocations resteraient côté moteur. De cette manière les copies, remove d'éléments. seraient effectués côté C++ et pourraient ainsi appeler les constructeurs et destructeur des membres de la struct de manière cohérente avec le langage.
en raison des interruptions d'interprétation, le système doit rester robuste aux éléments déclarés en local non désalloués (utilisation des longjumps) comme c'est le cas actuellement
Actuellement le pattern editor génère du code script qui est ensuite converti en byte code. Afin de simplifier les mises à jour liées aux patterns en évitant la génération puis la compilation, je propose que le pattern editor génère un byte code (simple) qui lui soit propre. Ce byte code adressera les différents patterns livrés sous forme de fonctions C++. Le "langage pattern" étant un langage beaucoup plus synthétique que le C++ ou le script, l'utilisation d'un byte code se justifie ici pleinement. Je ne connais pas suffisamment le pattern editor pour déterminer les changements que cela implique.
Dans cette idée, les gpp développent les fonctions de base adressables comme patterns qui sont compilés puis livrés. Le pattern devient alors disponible dans le langage pattern. Ces patterns pourraient être livrés sans une DLL spécifique communiquant avec le reste du système uniquement via l'API d'AI fonctions moteur.
Dans le cas d'utilisation en plusieurs DLLs :
Note : ce principe inspiré de COM ne fonctionne pas avec les PRX sur PS3.
F
Néanmoins, si les patterns utilisent
un système de byte code (cf Modifications
du pattern editor
Dans un premier temps il faudrait garder la compatibilité entre le système C++ généré et écriture C++ manuelle, en faisant évoluer le système complet car il est probable que l'on souhaite réutiliser le code AI script existant sans avoir à tout re-développer. D'autre part cela permet de migrer au moment voulu.
Certaines évolutions proposées peuvent cohabiter avec la génération de code. Voici une proposition de road-map.
les modifications qui sont liées apparaissent dans un cadre correspondant
les modifications côté moteur apparaissent en bleu
les modifications dans les scripts existants en saumon
Phase d'évolution commune avec la génération de code |
|
Modifier les scripts pour éliminer les cas de macros différentes portant le même nom. Cela permettra d'activer le système de génération des macros (simples) dans les sources C++ produisant un code beaucoup plus clair |
REF _Ref156278965 \r \h 3 |
Caractériser dans les fichiers .vas les modèles correspondant à des structures dans les scripts existants |
8.2.3.1 |
Procéder aux évolutions de mapping des données pour unifier les classes modèles et modèle data : Modifier l'interpréteur pour ménager l'espace nécessaire aux pointeurs me et pvftable Modifier l'interpréteur pour appeler les factories de modèle à chaque création d'instance (remplacer l'agrégation de AI_cl_GenModelIntertace dans le AI_cl_Model par l'agrégation d'une factory AI_TModelFactory) Modifier le générateur de code pour générer les variables dans les classes modèles et différencier la génération des structs Modifier le générateur de code pour générer des model_cast stockés dans des GenModel* à la place des model_instance |
8.2.3.1 |
Modifier les scripts pour éviter l'utilisation du système de partage de variables d'instance lorsque deux instances du même modèle sont instanciées sur deux tracks différentes du même gao |
REF _Ref156278198 \r \h 8.2.3.2.2 |
Modifier l'interpréteur et l'implémentation de la DLL pour placer le numéro de track courante dans l'espace de variables d'instance en plus du me et du pvftable (cela permettra d'en disposer directement facilement pour les passages de contexte) |
REF _Ref156278198 \r \h 8.2.3.2.2 |
Modifier le système de passage du contexte dans les AI fonctions : Flager précisément les AI fonctions pour caractériser la nature du contexte utilisé par l'AI fonction. Ajouter en premier(s) paramètre(s) du corps de l'AI fonction le contexte nécessaire ([gao], [model], [track]). Modifier le corps des AI fonctions concernées pour utiliser ces paramètres au lieu des variables globales de l'interpréteur. Modifier l'interpréteur pour passer ces paramètres automatiquement en fonction des flags des AI fonctions (ainsi le système reste compatible avec le système script). Modifier la génération de code pour passer les paramètres nécessaires. Enlever le système de contexte en début de fonction. Le remplacer par un système gérant seulement les stop interprétation (_stop_interpret_manager existant ou un système de fonction_ + fonction inline comme décrit précédemment) |
REF _Ref156278198 \r \h 8.2.3.2.2 |
Ajouter une automation (OLE ?) de Visual depuis l'éditeur d'AI Yeti. |
REF _Ref156280453 \r \h 8.1.1 |
Phase d'évolution spécifique au code C++ écrit manuellement |
|
Ajouter un mot clé permettant de spécifier les modèles écrits en C++ manuellement. Développer un générateur permettant de générer des squelettes de fichier .h et .cpp en fonction des .vas (à la manière du générateur de code Rose cpp de Rational Rose) |
REF _Ref156279259 \r \h 8.2.1 |
Développer le système permettant d'importer des références sur data dans du code C++ écrit manuellement. Ce système peut facilement cohabiter avec le système généré en réservant des plages d'index dédiées à la génération et au code "manuel". |
REF _Ref156280684 \r \h 8.2.2 |
Modifier le système d'initialisation de la DLL dans AI_cl_DLLManager pour pouvoir adresser des classes dans différentes DLL. |
REF _Ref156279713 \r \h 8.2.6 |
Modifier le système pattern editor pour adresser des patterns existants en C++ dans une DLL dédiée plutôt que générer du code script (création d'un byte code spécifique aux patterns). |
REF _Ref155696332 \r \h 8.2.5 |
Développement des patterns utilisés sous forme C++ dans une DLL dédiée |
REF _Ref155696332 \r \h 8.2.5 |
Génération ultime des sources C++ à partir des scripts. Archivage sur une base perforce dédiée F Dès lors qu'une partie des AI sont écrites directement en C++, le système n'aura plus à être exécuté directement en scripts | |
Développer un ensemble de containers plus fiables et ouverts (string et tableaux) en faisant évoluer le système existant |
REF _Ref156281368 \r \h 8.2.4 |
Optimisation du code moteur en désactivant tout ce qui est devenu inutile dans l'interpréteur, la génération de code. | |
F Les évolutions futures des features du système sont simplifiées puisque les évolutions du langage n'ont plus à être répliquées dans l'interpréteur et le système de génération. Ces features peuvent tirer partie de la puissance du langage C++ et des debuggers existants. Exemple : ajouter une définition dans les fichiers .vas permettant de déclarer des pointeurs model castés dans les classes |
|