dimanche, avril 11, 2010

Une bière fraiche ! Dans un verre propre, nom de ... !

Avec le départ du professeur Ribbens, c'est sans doute une page de l'histoire de Montef' qui s'est tournée, quelques années (mois ?) seulement après le début de ma carrière. Comme pour beaucoup des cours que j'ai suivi, j'ai peut-être bien eu la chance à avoir reçu du maître du Scheme en personne une initiation aux techniques des continuations et du "data-driven programming". Et sa rétrospective sur les LISP machines et techniques de garbage collection était tout simplement magistrale.

In my 3rd year at University, I've finally been taught a programming language that forced me to re-think everything I thought I knew : LISP (and I practiced mostly its Scheme dialect). Beyond the charismatic character of Pr. Ribbens whose motto could more or less be translated in "brew sana in f**ing bottlore sano", it introduced me to "data-driven programming" and design of language-specific processors. To make a long story short, "data-driven" is what you feel you should be using when you start going beyond 10 rooms in a Lone-Wolf game : keep the code short, simple and generic, and have it proceed through structured data in order to obtain the desired result.

C'est de "Data-driven" justement, qu'il est question ici, puisque j'ai décidé de ne pas directement *coder* la logique de mon jeu, mais de la décrire par des structures de données traitées par un moteur qui reste plus simple et plus générique. Une approche qui me ralentit peut-être par moment mais qui me titille : je veux en avoir le coeur net et vérifier par moi-même si oui ou non il sera possible de construire un jeu de plate-formes sophistiqué de cette manière.

Les "livres dont vous êtes le héros" sont sans doute le meilleur exemple possible de "data-driven programming": tout programmeur qui a un peu roulé sa bosse "sent" bien qu'il y a moyen de faire mieux que

sub Salle42
print "au détour d'un couloir obscur vous entendez un bruit sourd ..."
print "1. vous dégainez Voleuse de Vies"
print "2. vous vous avancez dans les escaliers"
input "votre choix"; choix
if choix = 1 then Salle44()
if choix=2 then VousEtesMort()
. Programmer une fonction par salle / évènement (en fait, par numéro de "chapitre" dans le livre) serait extrèmement pénible, la logique du jeu se retrouverait noyée par des éléments de second ordre comme "est-ce que le clic se trouve dans la zone s'avancer dans l'escalier ou dans dégainer la Voleuse de Vies?" Sans parler de la difficulté à gérer les modifications du scénario. On préfèrerait de loin pouvoir stocker toutes les "données" du jeu dans un format à part ... un fichier texte avec des "macro-commandes" pour les vieux patchs de mon genre (et leurs mentors) ... un document XML pour les afficiandos de l'UTF-8 et autres codeurs post-moderne. Une S-expression pour les fans de "recueil de petits problèmes en Scheme", je présume. Peu importe, finalement, la forme: ce qui comptera, c'est le fond, la sémantique, le modèle sous-jacent.

My "game script" and the state-machine-based-monsters is deeply influenced by this technique. Unlike your regular scripting language (Lua ?), building a data-driven game engine means that you're building with line of code a software microsystem that will process data in a specific context and for a specific purpose. You are free to define the line between code-bound function and data-driven function, and placing that line at the right place will be the key to efficient processing.

Bref. Pour mon jeu de plate-forme, le "modèle" de données est un peu plus complexe, fait en partie de pixels et de commandes qui décrivent les machines d'état des différents intervenants. Une réminiscence du cours "Ingénierie du Logiciel Orienté-Objet" et de ma confrontation avec l'UML, et dans une moindre mesure, avec le formalisme des automates à états fini. La frontière entre data-driven programming et langage de script complet (cf. microLua pour DS) est sans doute ténue ... Elle explique sans doute la décision parfois curieuse de garder certaines choses "en-dehors du script". La détection des collisions, notamment, ou la prise en charge des animations. J'ose espérer que cette volonté de "rester juste un cran en-dessous d'un DS-Basic" me permettra de garder un moteur de jeu suffisamment efficace.

Avec mon "InspectorWidget" désormais opérationnel, on se rapproche aussi d'un éditeur graphique pour ces machines d'état ... Et certains "défauts" du modèle actuel deviennent flagrant. Par exemple, Bilou saute, cours, nage, attend ... autant d'états que je dois déclarer et que je relierai ensuite les uns aux autres par des transitions, p.ex. "lorsque les boutons changent, si le bouton 'saut' est enfoncé, alors modifier la vitesse verticale et passer dans l'état 'saute'". Par contre, pour que l'appleman puisse être assomé quand Bilou tombe dessus, il faut que tous les états de l'appleman mêne vers l'état "appleman assommé" si la 'bonne' collision se produit. Il est facile d'en oublier l'un ou l'autre en cas de modification, et c'était d'ailleurs en partie la raison pour laquelle il était si difficile de s'en défaire dans les démos précédentes du jeu.

UML state machine formalism was one of other things I learnt that very same year, and it looked like a powerful way to express behaviours while avoiding repetitive boilerplate code to be described ... So my current data model for GOB behaviours is mostly implementing that. So far, it has the limitation that a transition is always flowing from exactly one state (to exactly one other state). It's expressive enough (that is, there isn't a behaviour you cannot implement with that), but it's not code-friendly in that when you actually want to implement a transition that should apply from (almost) all other states to a specific state, you can quickly forget something. It actually occured with the Appleman that couldn't be stomped when walking to the right simply becaused I missed some state20->state33 on hit0 [t] statement.

Je cherche donc depuis quelques semaines à ajuster le modèle de manière à pouvoir justement exprimer "depuis tous les états, ..." ou "pour tous les états où X est au sol, ..." et donner une transition unique qui s'applique à un groupe d'état d'entrée. Comme prévu de longue date, d'ailleurs (cf. ce schéma du comportement de l'Appleman). Outre l'économie de temps de parsing au démarrage du niveau et de quantité de mémoire (à mon avis négligeable), celà permettrait de pouvoir d'un seul clic dans le débuggeur forcer un point d'arrêt sur "je vais me faire blesser", quelque soit l'état actuel de Bilou. Pour l'instant, avant le debugging, il était nécessaire de passer tous les mouvements de Bilou en revue pour cliquer sur "transition vers l'état n° 15" depuis chaque état. Pas franchement folichon.

I haven't added such "group transitions" yet, but at least I made a nice step towards it by letting states and animation be defined in the context of .cmd files, so that each monsters' state machine can be written without having any knowledge of what other monster do and how many animations they use. Then, only a small amount of the states are "imported" by the master (level) script.

I'll have to adapt the level editor accordingly, but it should make InspectorWidget more friendly to use as the 'kind' of monster is now part of the state's name. Moreover, once the group transitions are in, activating a breakpoint on "jumping->hit" should equally activate "standing->hit", "walking->hit" etc. as long as you used a group transition for "{jumping, walking, standing, ...} -> hit"


Un premier pas dans cette direction, ç'a été de donner à chaque fichier ".cmd" (généralement un par personnage ou ennemi) son propre "répertoire" d'états et d'animations, désormais indépendants de ce que font les autres. Seuls certains de ces états seront "importés" par le script qui définit le niveau en cours pour placer les intervenants sur la map. L'éditeur de niveau devra être adapté, bien sûr (eeh oui. Faire et défaire ... that's the question), mais ce sera pour un mieux: plus besoin de passer en revue toutes les positions de Bilou, ni de savoir lequel des états de l'appleman convient comme état initial. Les groupes de transitions devraient s'y greffer plus agréablement.

Et InspectorWidget lui aussi en bénéficie, puisque les monstres sont maintenant nommés "fu01", "ap07" ou "wo00" plutôt que d'être représentés avec un simple nombre.

2 commentaires:

Romain a dit…

À te voir bûcher comme cela, tu pourrais en faire une thèse de ton projet. façon de parler hehe ;) continue

PypeBros a dit…

Houuulàlàaa . non. Ça ne rigole pas autant, pendant une thèse, crois-moi.