Animations 2D interactives, en Haskell/Gloss

Voir aussi : video youtube - code source

Cet article présente la bibliothèque Haskell Gloss, et son utilisation à travers deux exemples.

Gloss

Gloss est une bibliothèque Haskell qui permet d’implémenter des animations 2D. Elle gère l’affichage (couleurs, primitives 2D, etc), l’animation, l’interaction avec l’utilisateur (clavier, souris, etc). En interne, elle s’appuie sur OpenGL mais son utilisation est très simple et s’intègre assez proprement au paradigme de la programmation fonctionnelle.

Exemple basique

Gloss propose plusieurs modes de fonctionnement : affichage simple, animation, interaction… Dans ce premier exemple, on va animer un disque, et controler un cercle à la souris.

On commence par importer les modules nécessaires et définir un type Model, qui représente les données de notre application (position du cercle et position du disque).

-- proto.hs

import Graphics.Gloss
import Graphics.Gloss.Interface.Pure.Game
import Linear.V2

data Model = Model
    { _paddlePos :: V2 Float
    , _ballPos :: V2 Float
    }

Gloss propose quelques types pour manipuler des points, vecteurs, etc, mais ici on utilise V2 de la bibliothèque linear, qui est plus évoluée.

On écrit ensuite le programme principal, qui initialise la fenêtre et le modèle initial, puis lance la fonction play.

main :: IO ()
main = do
    let window = InWindow "Proto" (600, 400) (0, 0) 
        bgcolor = makeColor 0.4 0.6 1.0 1.0
        fps = 30 
        model = Model (V2 0 0) (V2 100 200)
    play window bgcolor fps model handleDisplay handleEvent handleTime

Cette fonction réalise la boucle de jeu de l’application et gère les différents événements grâce aux fonctions passées en paramètres (un peu comme les fonctions callbacks avec Glut).

Par exemple, pour la fonction d’affichage, on calcule l’image à afficher, selon le modèle courant (ici le cercle et le disque).

handleDisplay :: Model -> Picture
handleDisplay (Model (V2 paddleX paddleY) (V2 ballX ballY)) = 
    Pictures
        [ translate paddleX paddleY $ color black $ circle 30
        , translate ballX ballY $ color white $ circleSolid 10
        ]

Pour les événements utilisateurs, on met à jour la position du cercle, si l’événement est un déplacement de la souris.

handleEvent :: Event -> Model -> Model
handleEvent (EventMotion (x, y)) m = m { _paddlePos = V2 x y }
handleEvent _ m = m

Enfin pour le déroulement du temps, on se contente ici de déplacer le disque selon une vitesse fixée.

handleTime :: Float -> Model -> Model
handleTime dt m = 
    let (V2 ballX ballY) = _ballPos m
    in m { _ballPos = V2 ballX (ballY - 20*dt) }

Exemple de jeu

Dans ce second exemple, on va implémenter un jeu simple avec une image, un état de jeu, une gestion de collision basique…

Tout d’abord, on implémente le jeu dans un module Game, indépendamment de la bibliothèque Gloss. Pour cela, on modélise l’état de jeu, la position de la raquette (bob), et la position/vitesse de la balle. Puis on écrit les fonctions permettant de créer et dérouler le jeu.

-- Game.hs

data Status = Running | Launching

data Game = Game
    { _ballPos :: V2 Float
    , _ballVel :: V2 Float
    , _bobPos :: V2 Float
    , _status :: Status
    }

newGame :: Game
newGame = Game (V2 0 ballY0) (V2 0 0) (V2 0 bobY0) Launching

moveGame :: Float -> Game -> Game
moveGame x g = ...

launchGame :: Game -> Game
launchGame g = ...

animateGame :: Float -> Game -> Game
animateGame dt g = ...

Au niveau du programme principal, le modèle de l’application contient un jeu et les ressources nécessaires pour son affichage. On initialise le tout dans la fonction principale, et on écrit les fonctions callbacks de façon à faire le lien entre les événements détectés par Gloss et notre module Game.

-- Main.hs

data Model = Model
    { _bobPicture :: Picture
    , _bobScale :: Float
    , _game :: Game
    }

main :: IO ()
main = do
    bob <- loadBMP "bob.bmp"
    let window = InWindow "Bong" (gameWidth, gameHeight) (0, 0) 
        bgcolor = makeColor 0.4 0.6 1.0 1.0
        fps = 30 
        model = Model bob 1 newGame
    play window bgcolor fps model handleDisplay handleEvent handleTime

handleTime :: Float -> Model -> Model
handleTime dt m = m { _game = animateGame dt (_game m) }

...

Voir le dépôt de code, pour le reste du code.

Conclusion

Gloss est une bibliothèque très pratique pour implémenter des animations 2D interactives simples, en Haskell. Le paradigme fonctionnel se prête assez bien à ce genre d’application (programmation événementielle). L’architecture d’une application avec Gloss est assez similaire à l’architecture Elm utilisée pour le développement web front-end. À noter qu’il existe d’autres approches, comme la programmation fonctionnelle réactive.

L’exemple de jeu présenté ici sera repris dans le prochain article pour illustrer les “lens” en Haskell.