Créer un jeu en programmation fonctionnelle

Un article de asmanur.

Article originellement publié sur le blog de l'auteur

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.

Sommaire

[modifier] 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.


[modifier] 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).


[modifier] 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)); ...]


[modifier] 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é).


[modifier] Conclusion

La programmation fonctionelle n'est pas moins adaptée que l'impératif et l'orienté objet pour la conception de jeux vidéos, contrairement aux idées répandues. L'approche est différente, mais intéressante en soi et profite des avantages du fonctionnel, notamment la rapidité de prototypage qui a été bien utile pendant les novendiales.

(aucun commentaire actuellement)