Le fonctionnel, c'est bien aussi pour les jeux
Par asmanur, le samedi 25 juillet 2009, à 17:04 | Catégorie : Novendiales 2009 | Mots-clés : OCaml, jeu et novendiales | Source
Cet article a pour but d’exposer les avantages de la programmation fonctionelle dans le cadre dans la création de jeux vidéos. Cet article se base un peu (sinon beaucoup) sur l’expérience personnelle obtenue avec (un)faithful, un hack&slash tour par tour (un rogue-like en fait), ainsi les exemples en sont tirés.
Le dessin
La solution que l’on utilise pour éviter des appels à des routines de dessins un peu partout dans le code est pure et permet de factoriser les dépendances à la bibliothèque chargée du sale boulot.
L’idée est de caractériser ce que l’on affiche ; ainsi on construit un
type dessin qui contient toute l’information nécessaire à
l’affichage du jeu, un dessin peut être un morceau de texte à
afficher, un rectangle à dessiner, tout dépend du type de jeu. Un
dessin inclut aussi la position à laquelle il doit être dessiné ; il
suffit ensuite d’écrire une fonction dessiner qui sera la seule à
faire appel aux routines graphiques.
Ainsi une fonction de dessin retourne une liste de dessins (le fonctionnel c’est aussi l’art d’énoncer les bonnes tautologies) et zou il suffit d’itérer sur les dessins et on a l’affichage de la scène.
Cette méthode possède un avantage assez conséquent, on peut effectuer
du post-processing sur les dessins générés — c’est le mécanisme de
base utilisé dans les fondus de (un)faithful — autant pour les
déplacer à l’écran que pour modifier leur couleur ou le texte
affiché. Déplacer la caméra ? Il suffit de décaler tous les dessins
d’une fonction linéaire du temps pendant le fondu. Pour ce faire, on
préferera utiliser, à la place d’une boucle, des combinateurs sur les
listes qui permettent d’exprimer les traitements les plus communs sur
les listes de façon plus concise. Ici on utiliserait map qui
applique une fonction sur tous les éléments d’une liste et retourne la
liste des résultats.
Un monde immuable
En programmation fonctionelle, on préfère ne pas modifier l’état du monde par effet de bord mais plutôt par mise à jour. Cela possède un certain nombre d’avantages. Premièrement, il est très facile de mettre de coté les états pour retourner dans le passé de la partie ou pour faire un replay. Ensuite cela permet d’éviter que n’importe quelle fonction touche au monde, là on sait que chaque fonction ne pourra rien modifier autrement qu’au travers de sa valeur de retour, ça permet de vite retrouver l’origine d’un bug.
On s’en sert également pour organiser la transition graphique entre deux tours, avec une fonction prenant en argument le monde à la fin du tour précédant et le monde au début du tour suivant génère un diff qui sert à une partie des fondus. Bien sûr pour éviter que ce soit une usine à gaz, les garbage collector sont prévus pour ça(en).
Gestion d’événements
Pour gérer les évènements, un des moyens les plus adaptés au fonctionnel est probablement l’utilisation de callbacks. On associe donc à chaque type d’évenements une fonction différente. Ce moyen est très répandu aussi en impératif et en orienté objet. Le fonctionnel rend ça encore plus attrayant en permettant la création de callbacks dynamiquement.
Par exemple, dans (un)faithful, on a une fonction move qui prend
deux arguments, le premier est un vecteur désignant la direction du
mouvement, le second est le monde, et cette fonction renvoie le monde
modifié — on note le type d’une telle fonction vecteur -> monde
-> monde. En fait on peut voir cette fonction comme prenant en
argument un vecteur et renvoyant une opération sur le monde (monde ->
monde) — on écrit alors vecteur -> (monde -> monde). Cette
fonction, dite curryfiée,
peut alors être appliquée partiellement. Ainsi move (0, 1) est une
opération sur le monde qui déplacera le personage d’une case vers le
haut.
Cela permet de créer une liste associant des touches appuyées à des opérations sur le monde. Cette liste peut ressembler à :
[(Left, move (1, 0)); (Right, move (-1, 0)); …]
Un compromis à établir : équilibre entre données et fonctions
Il ne faut pas tomber dans un extrémisme du « tout fonction » ; le
problème de la fonction c’est que c’est une boîte noire aux yeux de
l’appelant, il n’y a aucun moyen de savoir ce qui est fait durant or
l’appelant peut vouloir avoir des détails. Si on modélise un effet
affectant un joueur par une fonction de type monde -> monde, on va
peut être se retrouver à coder des systèmes pour gérer la durée dans
chaque effets, etc. Parfois il est intéressant de restreindre les
possibilités pour pouvoir automatiser une partie des effets.
Un autre problème des fonctions est la sauvegarde ; il est très
difficile (sinon impossible) de sauvegarder une fonction telle quelle,
une solution est d’associer à chaque effet une clé, et de stocker une
liste constante (clé, effet), ainsi il suffit de stocker la clé (on
perd une partie de l’extensibilité offerte par l’application
partielle, en pratique on adopte donc un système plus sophistiqué).