mercredi 31 août 2011

Gagner du temps lors de la création d'interfaces

Bonjour à tous, c'est avec beaucoup d'émotion que je poste sur funky work pour la première fois. On m'appelle Scriptopathe, Scripto pour les intimes.

Au menu aujourd'hui...


Gagner du temps lors de la création d'interfaces... (oui oui j'insiste dessus !)
La création d'interfaces graphiques est toujours long, fastidieux et ennuyant lorsqu'on a beaucoup de contrôles à disposer pour modifier une structure de données comportant des éléments standards (nombres, strings, booléens, et autres objets composés de ces mêmes éléments).
Il faut créer l'objet contenant ces données, puis l'interface qui le modifie.

C'est pour simplifier et raccourcir ce dur labeur, que j'ai pensé à réunir les 2 choses dans le même objet.

Oh mon Dieu, il réunit les données et l'interface, quel désastre, on ne lui a jamais appris à programmer !

En fait, ce n'est pas vraiment une réunion totale des deux. Le principe est de donner des attributs simples aux données qui définiront la manière de les afficher.

Avant de poursuivre...
Lors de cet article, j'utiliserai Python comme langage de programmation, et wxPython comme toolkit graphique.  Cependant, la technique peut sans difficulté être portée vers une autre librairie et un autre langage (supportant la réflexion pour plus de facilité).

Le principe en lui-même !
L'idée est très simple : on va, dans l'objet contenant les données, donner de simples informations sur la manière de présenter ces données.
  • l'attribution d'attributs de représentation de données en lien avec les attributs affichables/modifiables d'un objet (un peu redondant dans la formulation j'en conviens)
  • La création de l'interface à partir des données récupérées : l'afficheur / modificateur de données.

Les attributs de représentation
Ce sont eux qui vont indiquer à l'interface ce qu'elle doit présenter à l'utilisateur. L'idéal est de créer une classe de base commune à tous les types, et une classe héritée de celle-ci pour chaque type d'élément affichable.
Les éléments de base à rendre disponibles par l'interface sont le nom de l'attribut et de l'objet parent (pour que l'interface puisse elle même modifier les données), et la priorité d'affichage.

Petit truc pythonique
En python, il est très aisé de produire un mécanisme permettant d'utiliser des mots clefs pour les arguments du constructeur de ces attributs, afin d'avoir une meilleure lisibilité, et de permettre d'omettre certains paramètres afin de leur assigner une valeur par défaut.
Exemple :
class DataRepresentation:
    def __init__(self, arg1=0, arg2=""):
         # Traitement

class StringDataRepresentation:
    def __init__(self, label="",  **kwargs)
        # Avec ça, les arguments spécifiques à DataRepresentation lui seront passés.
        # S'il y en a trop, ou qu'ils ne correspondent pas à ceux attendus, des exceptions seront levées.
        DataRepresentation.__init__(self, **kwargs)
        # Traitement...

 
Un raccourci de raccourci....
Pour que tous les attributs de données soient accessibles de la même manière par l'interface, créer une fonction simple qui les attribue elle-même selon une certaine règle est la bienvenue.
Le principe de cette fonction est simple :
  • Elle prend en arguments l'objet parent de l'attribut, le nom de l'attribut, l'attribut de données à lui associer.
  • Elle ajoute un attribut à l'objet parent, correspondant à l'attribut de données qui lui a été passé en argument. Le nom de cet attribut peut être celui de l'attribut qui lui est associé, suivi de "__data_attribute".
  • Elle peut faire tout le pré-processing que vous désirez faire :)

Des infos pour les attributs à la masse...
C'est bien joli de dire qu'on va donner des informations à destination de l'interface, qui varient en fonction de l'objet à représenter, mais, qu'est-ce qu'on lui dit exactement ?
Déjà, pour certains types de données, on peut définir quel contrôle utiliser pour la représenter. Par exemple, pour un int, on peut utiliser un SpinCtrl, afin de le modifier manuellement, ou bien un Choice, afin de choisir entre des propositions correspondant chacune à un numéro...
Et puis, des informations peuvent s'avérer nécessaires, par exemple, pour le cas précédent, quelles chaines affiche-t-on pour quelles valeurs de l'int ?

Utiliser les attributs de représentation pour créer l'interface
Les attributs de représentation peuvent alors être disponibles pour l'interface, via simple passage en argument de l'objet.
L'afficheur de données se contente de :
  • Lister les attributs d'un objet (au début celui passé en argument, puis ceux qu'il contient si c'est le cas !), et en extraire ceux qui sont affichables, c'est à dire, ceux qui ont des attributs de représentation.
  • Pour chacun de ses attributs, vérifier son type et celui de son attribut de représentation, et de placer des contrôles en conséquence.
C'est aussi simple que ça en théorie, et ça permet d'afficher n'importe quel objet.

Modifier les données
Il est certain que le but d'une telle interface n'est pas seulement de visualiser des données, mais aussi de les modifier ! Une fonction de l'afficheur / modificateur d'interface pourrait permettre d'affecter les données dans les champs à l'objet de données, lorsque l'utilisateur cliquerait sur un bouton tel que 'OK'.
L'algorithme récupérant les données est simple, néanmoins, il lui faut certaines informations supplémentaires, qu'il serait bon de sauvegarder quelque part. Ces informations sont des tuples (controle, nom de l'attribut, objet parent), qui peuvent être créés lors de la création du contrôle.
A partir de ces information, il suffit de faire une itération sur chaque tuple, de récupérer et convertir la valeur contenue dans chaque champ, et de l'affecter à l'attribut de l'objet parent, d'où l'utilité de connaître le nom de l'attribut et son objet parent...
En python, cela se fait simplement via :
setattr(obj_parent, nom_attribut, valeur)

Bilan
Cette solution peut être utile lorsque beaucoup d'informations simples et hétérogènes sont à afficher.
Elle est simple à mettre en place, et permet d'économiser du temps en créant en un seul coup un moyen de stocker/afficher/modifier des données !


Code source (data_modifier.py) :
http://nuki.music-all.be/past/index.php?page=Sources&id=1
Ce script n'a pas à être utilisé tel quel, mais sert de support à l'article.  Il doit servir d'exemple pour illustrer la théorie, mais il y a diverses manières spécifiques d'implémenter le concept.

Exemple du progrès :
Créer un interface se résumera alors à faire cela (si on prend le script posté plus haut):
  1. # -*- coding: latin-1 -*-
  2. from wx import *
  3. from data_modifier import *
  4. class Obj2:
  5.     def __init__(self):
  6.         # Création de 10 attributs booléens
  7.         for i in range(12):
  8.             bool = i % 2 == 0 and True or False
  9.             setattr(self, "Attr"+str(i), bool)
  10.             data(self, "Attr"+str(i), BoolDataRepresentation("Attr "+str(i+1)))
  11. class Obj:
  12.     def __init__(self):
  13.         self.Attr1 = 5
  14.         data(self, "Attr1", IntDataRepresentation(label="Attribut 1", type=CHOICE, choices=["Choix 1", "Choice 2", "Choix 3", "Choice 4", "OMG TRO BI1", "Funky work Roxxx"]))
  15.         self.Attr2 = False
  16.         data(self, "Attr2", BoolDataRepresentation(label="Attribut 2", type=RADIO_BUTTON))
  17.         self.Attr3 = True
  18.         data(self, "Attr3", BoolDataRepresentation(label="A la fin hohoho"), 15)
  19.         self.Attr4 = "Rapide oh pinaise"
  20.         data(self, "Attr4", StringDataRepresentation(label="Attribut 4"))
  21.         self.Attr5 = 125.4
  22.         data(self, "Attr5", IntDataRepresentation(label="Un float", is_float=True))
  23.         self.Attr6 = Obj2()
  24.         data(self, "Attr6", ObjectDataRepresentation(label="Un objet !", orientation=GridOrient(4, 4, 5, 5)))
  25.     def __str__(self):
  26.         return "Obj : " + "Attr1 = " + str(self.Attr1) + " Attr2 = " + str(self.Attr2) + " etc.."
  27. class TestObj:
  28.     def __init__(self):
  29.         self.Dat = [Obj(), Obj(), Obj(), Obj()]#[Obj(), Obj(), Obj(), Obj()]
  30.         data(self, "Dat", ObjectDataRepresentation(label=u"Une liste d'objets tiens", obj_type=Obj))
  31.         self.Dat2 = Obj()
  32.         data(self, "Dat2", ObjectDataRepresentation(label=u"Un objet", obj_type=Obj))
  33. class TestApp(wx.App):
  34.     def OnInit(self):
  35.         frame = Frame(None, -1, "Funky Work")
  36.         dat = TestObj()
  37.         DataModifier(frame, dat)
  38.         frame.Show()
  39.         return True
  40. app = TestApp(redirect=False)
  41. app.MainLoop()



Enjoy !

lundi 15 août 2011

Amusons-nous avec une GBA

Le (ou la, je ne sais pas comment on dit :) ) GameBoy Advance de Nintendo est sincèrement une console qui m'a plu.
Alors qu'a la base, je ne m'intéresse que à la programmation sur PC, je me suis dit, pourquoi ne pas plagier Åvygeil, qui s'intéresse à la programmation sur Mégadrive et me lancer vers une console ?
Grâce à une petite recherche Google, je suis tombé sur didacticiel fort agréable à lire.

Brève explication sur le fonctionnement des modes
La machine permet de manipuler 6 modes graphiques. Je vais les présenter brièvement.
Les modes 0, 1 et 2 sont des modes qui sont dit "de Tiles".
Tiles/Tuiles est un concept familier aux utilisateurs de RPG Maker, et même s'il plait souvent aux graphiste car il évite d'obliger le dessin de centaines map et en permet l'assemblage, sur console cette méthode offre un énorme avantage au niveau de la gestion de la mémoire, car les Tiles sont évidemment plus légers que les panoramas et permettent la composition de map presque infinie.
La répartition mémoire est définie de cette manière:
  • 64ko de mémoire vidéo (utilisé pour ces fameuses tuiles)
  • 32ko pour les sprites
Les fond peuvent utiliser la transparence et il est même possible d'effectuer des rotation sur les modes 1 et 2.
Les modes 3, 4, 5 sont les modes dit Bitmap. Leur répartition mémoire est définie de cette manière:
  • 80ko dédiés à l'écran 
  • 16ko seulement pour les sprites
Contrairement aux idées reçues, les mode7 n'appartient pas vraiment aux modes graphiques de la console. En effet, ils 'agit d'une astuce (couplée, sur superNes, avec une puce, la MarioChip, si je ne me trompe pas.. (FAUX, voir plus bas.)
Qui, au moyen de rotation et de zoom, permet la simulation de la profondeur (comme dans le célèbre MarioKart).
Cependant, nous ne nous étendrons pas sur le sujet.
 
Et bien si je me trompais :) merci Sylvain :
" Le "Mathematical, Argonaut, Rotation & I/O", c'est le nom de code du SuperFX, utilisé dans Starfox et Yoshi's Island. "). Sources

Les Sprites
Comme dans le RGSS et RPGMaker, un Sprite est un simple élément graphique  indépendant, déplaçable et transformable au moyen de zoom rotation et translation.
En général, dans un jeu, chacun des élément mobile est un Sprite.

Sons, touches et sauvegarde
Tout ceci est manipulé au moyen de Registres. C'est relativement simple mais l'article ne porte pas vraiment sur l'utilisation du C++ pour créer un jeu GBA.
Je ne m'attarderai donc pas sur ces points. Cependant, si vous avez des questions sur le développement sur GBA, n'hésitez pas à me contacter, je me ferai une joie de vous répondre.

Ce que nous avons vraiment essayé de faire


Il faut dire ce qui est, je me suis méchamment fait fumer par mes ambitions.
Concrètement, la première idée, après avoir vu que faire un "Hello World" (excusez-moi :) ) était relativement facile, je me suis demande pourquoi ne pas me lancer dans le développement d'un framework GBA qui reprendrait une structure proche de RPGMaker, permettant une éventuelle conversion RM > GBA.
Que de belles idées, et des larmes pleins les yeux tant je me sentais révolutionnaire...

Ce qui n'a pas fonctionné
Globalement, faire un jeu pour GameBoy ne demande pas spécialement plus de difficulté que de faire un jeu sur RPGMaker (difficulté scénaristique,   graphismes etc.) si ce n'est le développement pur de chaque module.
Cependant, la conversion de jeu RM > GBA est vraiment une tâche fastidieuse qui, au final, demanderais énormément d'investissement de la part du Maker pour s'adapter (et oui, la résolution pose soucis, les couleurs, le sons et pleins d'autres facteurs).
C'est pourquoi le projet est abandonné.
Oui, je sais que je suis un peu nul, mais je pense sincèrement que sur le long terme, ce genre de projet est impossible à maintenir. Par contre envisager un framework de développement, sous forme de code OpenSource, pourquoi pas. Mes expérimentation GBAsquienne ne sont pas encore terminée. Je garde bien au chaud quelques idées de mini jeu facilement développable que je pourrais déployer en quelques semaines de travail.

Ce qu'on retient de ce cuisant échec
Même si le projet d'éditeur/convertisseur/etc. (on l'aurait appelé GiX :D) n'a pas fonctionné, ce n'est pas dramatique. Je ne suis pas un grand fan de C/C++ mais je me suis bien amusé.
Et j'invite tout ceux qui se sentent un peu fou à ne pas hésiter à créer des mini-trucs bien rigolo sur cette console. Et pourquoi pas imaginer un projet de RPG avec tous les gentils amis du FunkyWork sur GBA !

Liens en annexes
Voici le didacticiel que j'ai utilisé. Il est vraiment bien fait ! Et se trouve évidemment sur developpez.com

Le mot de la fin
Je vous souhaite à tous une bonne fin de vacances !
Merci pour la lecture.
Bien à vous... MOLOK !

Ps: Si vous avez des questions, n'hésitez pas à les poser... ça y est ! FunkyWork revit !