vendredi 3 juin 2011

Le portage vers XBOX 360, un défi technique !

Bonjour à tous,
Ici Scriptopathe, en ce moment je suis un peu seul à travailler sur le projet, kriss étant parti (en vacances ?)...

Alors, on porte l'Usine vers XBOX 360 ?
En attendant d'avoir le cahier des charges complet, je commence à programmer le noyau du jeu. Et ce n'est pas forcément une partie de plaisir, étant donné qu'il y a d'importantes différences entre Ruby (langage dans lequel le jeu était programmé à la base) et C#.
De plus, il faut faire de nombreuses adaptations dans l'éditeur de jeu, afin d'avoir un système viable.
Le fait de passer de Ruby vers C# a amené son lot de contraintes, et parmi elles...

...Un problème assez gênant
(attention ça devient un peu technique, ça sera surtout utiles aux autres développeurs qui ont un problème semblable)
Dans la version Ruby de l'Usine en Folie, on avait la possibilité d'écrire du code Ruby dans les Evènements (personnages, objets, etc...), et de l'exécuter en jeu, via une fonction Ruby nommée eval(), qui permettait d'interpréter du code.

Dans l'éditeur, si on double cliquait sur un évènement, on pouvait modifier son contenu. Entre autres, dans chaque onglet, on pouvait écrire du code Ruby qui allait être exécuté lorsque une condition était remplie : lors de l'initialisation, lorsque l'évènement est à l'écran, au contact du héros, premier contact héros, héros + touche d'action pressée, contact avec un autre évènement, et contact avec un tir.







Cela était possible car Ruby est un langage interprété.

Or, il se trouve que ce n'est pas le cas de C# : pas de fonction eval(), l'exécution du code contenu dans les évènements paraissait alors impossible !

Mais... comment on fait alors ?
Je me suis posé la question de "Comment intepréter du code C# qui n'est chargé qu'à l’exécution ?".
Après pas mal de recherches, et des questions posées sur développez.net (je remercie d'ailleurs Dom Quiche pour ses conseils avisés) une solution est envisageable :
Compiler le code des évènements dans des dll chargées à l'exécution, puis créer des Delegates utilisés par les évènements pour exécuter ce code.

Alors, ce n'était pas si évident que ça, donc je vais un peu détailler comment il faut faire :

Etape 1 : la compilation !
En C#, l'unité minimale de compilation est l'assembly.
Que devra contenir notre assembly ?
L'assembly créée devra contenir le code de tous les évènements d'une map. Ainsi, on chargera une dll lors de chaque chargement de map. Les fichiers de maps que vous créerez au moyen de vos outils devront comporter le chemin relatif (à partir du répertoire courant du jeu donc) de la dll qui correspond au code de leurs évènement, afin que ces dernières puissent être retrouvées pendant le jeu.

L'assembly comportera un type, qui doit pouvoir être retrouvé par le jeu à l'exécution, donc, par exemple, l'idéal serait de stocker le nom de ce type dans le fichier map, ou de le créer à partir du nom de fichier (en remplaçant les "/" par des "_" par exemple).

Le type ainsi créé comportera des méthodes publiques et statiques, qui prendront en argument l'évènement qui doit "exécuter" les actions, ainsi que d'autres arguments (par exemple, dans le cas du contact avec un autre évènement, donner en argument l'évènement avec lequel il rentre en contact).

Et cela pose un nouveau problème : si vous compilez vos sources sans aucune référence vers vos projets, le compilo vous fera un scandale car les types que vous lui demanderez (par exemple Event pour les évènements) n'existent pas.
Pour remédier à cela,  il faudra ajouter des références vers des dll qui contiennent ces types (il faudra donc mettre les parties de votre jeu qui doivent être accessible depuis les "scripts" des évènements dans des dll).

En pratique ça donne quoi ?
Voici un exemple de code C# qui permet de compiler... du code C# :

            // Ce code devra être chargé depuis un fichier map.
            string code = "Code à compiler"; 


            // Paramètres du compileur
            CompilerParameters cparams = new CompilerParameters();

            // On va générer une dll, pas un exécutable
            cparams.GenerateExecutable = false;
            cparams.OutputAssembly = "Nom du fichier de la dll qui va être créée";
            cparams.ReferencedAssemblies.Add("Chemin d'accès à la dll");

            // Compilation
            CSharpCodeProvider provider = new CSharpCodeProvider();
            ICodeCompiler compiler = provider.CreateCompiler();
            CompilerResults results = compiler.CompileAssemblyFromSource(cparams, code)

            // Gestion de l'erreur
            if (results.Errors.HasErrors)
            {
                System.Text.StringBuilder errors = new System.Text.StringBuilder("Compiler Errors :\r\n");
                foreach (CompilerError error in results.Errors)
                {
                    errors.AppendFormat("Ligne {0}\t: {1}\n",
                           error.Line, error.ErrorText);
                   
                }
                errors.AppendFormat("Code :\n{0}\n\n", str);
                // Créer une fonction WriteLog qui écrive les erreurs dans un fichier,
                // de manière à pouvoir les récupérer depuis un autre programme.

                // Nom du log peut être généré à l'aide du nom du fichier compilé.
                WriteLog(errors.ToString(), "Nom du log")
            }

Voilà, histoire que vous sachiez ce qu'il faut utiliser pour faire quoi. Bien sur, ce code est à compléter et organiser en fonction de ce que vous voulez faire, mais les principales fonctions sont là !

Étape 2 : exécution !
A l'exécution, lorsque vous chargez votre map pour la première fois, chargez votre assembly, et mettez le en mémoire dans un Dictionnary<string, Assembly> :
public Dictionnary<string, Assembly> Assemblys = new Dictionnary<string, Assembly>();
Remarque : ne pas oublier la directive using System.Reflection;au début du code source.

Pour charger l'assembly, il suffit d'utiliser :
string Filename = "Chemin d'accès vers la dll";
Assemblys[Filename] = Assembly.LoadFrom(Filename);


Il va ensuite falloir récupérer le type qui est contenu dedans :
string Typename = "Nom du type" // nom récupéré à partir du fichier map par exemple
Type type = Assemblys[Filename].GetType("Nom du type");

Ou alors, si (et normalement c'est le cas) il n'y a qu'un seul type dans l'assembly :
Type type = Assemblys[Filename].GetTypes[0];

Pour récupérer une méthode :
MethodInfo method = type.GetMethod("Nom de la méthode");

Maintenant, il va falloir créer des délégates dans la classe qui va utiliser ces méthodes (disons Event pour "évènement") :
// Délegate qui acceptera un argument Event
delegate void DelegateSimple(Event self);
// Délégate qui acceptera deux arguments Event
delegate void DelegateWithEvent(Event self, Event evt);
// Ainsi de suite...

// De quoi les utiliser :
// Pour les collisions avec le héros
DelegateSimple MethodOnCollideHero;
// Collisions avec les Events
DelegateWithEvent MethodOnCollideEvent;


Ensuite, on utiliser les infos de méthodes qu'on a récupérées précédemment afin de les initialiser :
// method est un objet MethodInfo qu'on a récupéré précédemment
MethodOnCollideHero = (DelegateSimple)Delegate.CreateDelegate(typeof(DelegateSimple), method);


Et voilà, pour appeler la méthode il n'y a plus qu'à faire :
MethodOnCollideHero(this);

Revenons à l'Usine en Folie
Sur l'Usine en Folie, ce système fonctionne parfaitement. L'éditeur a été adapté à la syntaxe de C#, et permet de tester le code C# tapé pour indiquer des éventuelles erreurs :



Et voilà, comme quoi tout est possible !

Scriptopathe

Aucun commentaire:

Enregistrer un commentaire