4. Énumeration des différentes constructions utilisées

La programmation réactive s'est avérée particulièrement efficace sur certains points du jeu. Elle permet entre outre la création de comportements. En réalité un comportement réactif n'est autre qu'une série d'instructions réactives. Il est possible de qualifier de comportement ces suites d'instructions car regroupées dans une unique fonction et appliquées à un objet, elles donnent l'illusion d'un comportement. Un exemple concret, dans le cadre de notre jeu, est le comportement " collision" . En effet ce comportement (parmi d'autres) est associé à chaque boule du billard. Il est à noter qu'il n'existe pas de nombreuses fonctions ou formes réactives, mais en revanche elle se sont révélées très efficaces, et très simples d'utilisation. La création de tels comportements est donc immédiate et intuitive dès qu'une connaissance suffisante de l'API Senior est acquise.

Voici une description détaillée des comportements réactifs qui ont été implémentés, ainsi que des instructions réactives utilisées. Les exemples présentés ci-dessous ont été volontairement raccourcis pour des raisons de clarté.

4.1. La mise en parallèle d'instructions basiques

L'instruction par& à été utilisée à de nombreuses reprises. Cette instruction réactive représente la base de la concurrence. Pour bien comprendre le type d'utilisation de cette instruction, voici l'exemple du comportement d'une boule de billard. La fonction entière sera décrite par la suite:

(define (ball-behavior o)
	   (par& 
	     (funcall& ball-move o)
	     (funcall& ball-collision o)
	     (funcall& ball-inertia o)
	     (funcall& common-render o)))

Ici, le comportement d'une boule de billard est entièrement décrit. L'instruction funcall& permet l'appel d'une fonction réactive (c'est une fonction contenant des instructions réactives). Ce comportement est donc ajouté à la machine réactive pour chacune des boules présentes sur le billard. La mise en parallèle se justifie bien, car il n'est pas nécessaire d'exécuter ces actions (les funcall&) en séquence. Ces actions sont totalement indépendantes. Les différents appels à l'instruction funcall& font que le comportement réactif associé à une boule (ball-behavior) est en réalité une suite de comportements réactifs exécutés en parallèle. Ce mécanisme est fortement intéressant dans le sens où l'ajout d'un comportement réactif devient immédiat et donc très simple d'utilisation.

4.2. Le contrôle de la durée de vie d'un comportement réactif

Il est important de comprendre que les instructions réactives ajoutées à la machine ne sont pas nécessairement terminées à la fin d'un instant, il est même très rare que cela arrive. En réalité, la grande majorité des instructions ne se termine pas d'une manière autonome. Dans l'exemple précèdent, les différents comportements (ball-move, ball-collision ...) sont des loop&, donc le comportement ball-behavior ne se termine pas tout seul. L'utilisation de l'instruction réactive until& s'est révélée très utile pour contrôler la durée de vie d'une comportement réactif. Un exemple simple montre ainsi l'utilité de l'instruction until&. Il s'agit du comportement de tir associé à la boule blanche du billard. En effet, ce comportement ne doit pas être présent dans la machine réactive lorsque des boules sont encore en mouvement, sinon il serait possible de frapper la boule blanche alors que les boules ne se sont pas encore arrêtées:

(define (whiteball-fire o)
  (until& 'moving
   (loop&
    (scan& 'keypress-event
	 (if (= (car (the-value&)) sdlk-return)
	     (tir-de-la-boule-blanche o))))))

On peut voir que ce comportement sera retiré de la machine dès qu'un événement 'moving sera généré. Sinon, tant que cet événement est absent lors d'un instant, le comportement attend la pression de la touche enter afin de déclencher le tir de la boule blanche, qui elle même déclenchera le mouvement des autres boules lors d'une collision. Afin que l'on puisse de nouveau jouer, il faut remettre ce comportement réactif à la boule blanche dans la machine lorsque plus aucun événement 'moving n'est déclenché (les événements 'moving sont automatiquement générés par les boules qui bougent à chaque instant). Ainsi le code suivant était nécessaire:

(define (ball-stop-moving o)
    (loop&
     (await& (not& 'moving))                               (1)
     (funcall& whiteball-fire o)                           (2)
     (stop&)))
(1)
attent un instant où l'événement 'moving est absent
(2)
exécute le comportement whiteball-fire qui attend l'action de l'utilisateur.

Note

On pourrait croire qu'à chaque instant où l'utilisateur n'appuie pas sur entrée (donc trop souvent), que le code du comportement réactif (funcall& whiteball-fire) est dupliqué, mais en fait ce comportement est un loop& donc il ne se terminera que lorsqu'il y aura un événement 'moving dans la machine (voir until& dans l'exemple précèdent). Ainsi, l'appel à la fonction (funcall& whiteball-fire o) est parfaitement contrôlé et il n'y a pas duplication de code dans la machine réactive.

4.3. Ajout dynamique de comportements dans la machine

Il est possible de rajouter des instructions réactives à la machine, durant l'exécution d'un instant. Cette facilité a été très appréciée lorsqu'il a fallu gérer le comportement " changement de caméra" . En effet, une seule caméra ne peut être active à un instant donné. Ce comportement assure les mouvements de caméra effectués par l'utilisateur lorsque celui-ci bouge la souris. Si un tel comportement était présent dans la machine pour chacune des caméras ce serait un désastre. Les caméras " inactives"  subiraient toutes les transformations dues aux mouvements de souris, ce qui n'est absolument pas l'effet souhaité. Pour palier à ce problème, il a été utile, grâce à la forme until&, de supprimer le comportement associé à la caméra active, et d'ajouter un nouveau comportement dans la machine réactive pour la nouvelle caméra. Cette fonction gère le comportement de la camera active.

(define (cam-behavior o)
	    (until& 'change-cam
	      (loop&
		(funcall& update-camera o))))

Alors que ce comportement gère le changement de caméra.

(define (change-camera)
    (loop&
      (scan& 'keypress-event
	  (cond ((= (car (the-value&)) sdlk-f1) (engine-activate-camera engine 'cam1))
       		((= (car (the-value&)) sdlk-f2) (engine-activate-camera engine 'cam2)))
       	  (generate-in-machine& the-machine 'change-cam)
	  (add& the-machine (funcall& cam-behavior (engine-get-activ-cam engine))))))

Le comportement cam-behavior reste dans la machine réactive tant que l'utilisateur n'a pas pressé la touche F1 ou F2 (car deux caméras). En effet, dés que celui-ci presse la touche adéquate, l'événement réactif 'change-cam est alors généré dans la machine réactive courante et le comportement sera terminé (a cause du until&) à l'instant suivant. Il ne reste plus alors qu'a rajouter dynamiquement le comportement réactif cam-behavior sur la nouvelle camera active. C'est le rôle de l'instruction (funcall& cam-behavior (engine-get-activ-cam engine)) .

4.4. Communication par événements réactifs valués

Au cours d'un instant, l'instruction réactive scan& permet d'intercepter et de traiter l'ensemble des événements d'un même type.

Ce comportement traite la collision de deux boules ...

(define (ball-collision o)
  (loop&
    (scan& 'pos
       (or (eq? o (the-value&))
           (collision (the-value&) o)))))

... et ce comportement le mouvement d'une boule.

(define (ball-move o)
  (loop&
    (atom& (translate-ball o))
    (atom& (rotate-ball o))
    (generate& 'pos o)    
    (stop&)))

A chaque instant, chacune des boules du billard génère un événement 'pos. Ainsi, une multitude d'événement 'pos est présent dans la machine réactive (autant que le nombre de boule). Le scan& est ici particulièrement interressant dans le sens où il est nécessaire de traiter la collision d'une boule avec toutes les autres. Ainsi puisque la valeur (the-value&) associée à un événement 'pos est l'instance de la boule ayant généré le 'pos, il devient simple de réaliser ce traitement. En effet, scan& effectue le traitement de son corps pour chacun des événements 'pos présents dans l'instant courant.

Note

la compléxité de cette méthode est alors d'ordre O(n²).

4.5. Communication depuis l'extérieur vers la machine

Afin de faire le lien entre la machine réactive et les événements clavier ou souris, il a été très pratique de pouvoir générer des événements réactifs à l'interieur de la machine. Ceci ce fait grâce à l'instruction (generate-in-the-machine& machine 'un-evenement un-objet-Scheme-associé). Dans l'exemple qui suit, l'utilisation de cette technique est illustré:

(let ((current-type (sdl-event-type)))
 (cond ((= current-type sdl-key-down-type) 
          (multiple-value-bind (key mod) (sdl-keyboard-event-info)
	    (generate-in-machine& the-machine 'keypress-event (list key mod))))
       ((= current-type sdl-key-up-type)
          (multiple-value-bind (key mod) (sdl-keyboard-event-info)
	   (generate-in-machine& the-machine 'keyrelease-event (list key mod))))))

De cette manière, lors de la prochaine réaction de la machine (appel à react&) , l'événement 'keypress-event sera présent au cours de cet instant (si il y a eu touche pressée évidement). Ainsi, tous les comportements réactifs qui écoutent les événements clavier seront prévenus. Le dernier paramètre de l'instruction generate-in-the-machine& permet de lier une valeur à l'événement. Ici, la valeur associée est une liste contenant la touche pressée et le modificateur (shift, control, ...). Ainsi, les comportements réactif peuvent et doivent vérifier que la touche pressée est celle attendue.

Note

il aurait été possible de tester la touche pressée à l'extérieur de la machine réactive, et de générer un événement plus spécifique dans la machine.

4.6. Utilisation d'événement local

Les événements locaux de Senior permettent d'isoler certaines instructions des événements réactifs extérieurs:

(par&
   (generate& 'event)	
   (local& 
     (await& 'event)
     (atom& (print "event impossible"))))

Dans cet exemple, le print ne se produira jamais. L'événement 'event généré dans la première branche du par& ne peut être intercepté par le await&, l'événement que await& attend doit être généré dans le corps de l'instruction local& .

(local&
   (par&
     (generate& 'event)	
     (seq& (await& 'event)
	   (atom& (print "event intercepté")))))

Au contraire, dans cet exemple, l'événement 'event généré dans le corps de local& sera intercepté par le await&. L'événement est local au corps de l'instruction local&.

4.7. Utilisation du let réactif

L'API Senior fournit des constructions réactives qui permettent au programmeur Scheme de l'utiliser de manière intuitive. C'est le cas de la forme let&. Si cette forme était absente, il aurait été plus difficile de gérer certaines situations qui se révèlent être très simple avec l'utilisation d'un let. En particulier pour simuler les variables statiques dans une fonction disponible dans certains langages. Il suffit pour cela, d'englober la fonction où lambda dans un evironement, soit d'un let. Lorsque l'on écrit du code réactif, il est impossible de faire ce genre de manipulation avec un let Scheme, d'où l'intérêt du let&. Cette fonction est le comportement réactif associé au déplacement de la visée (afin de savoir dans quel direction la balle blanche doit être frappé):

(define (whiteball-aim-direction o)
    (let& ((dalpha 0.0))
     (loop&
       (par&
         (scan& 'keypress-event
	  (cond ((= (car (the-value&)) sdlk-left) (set! dalpha -1.0))
		((= (car (the-value&)) sdlk-right) (set! dalpha 1.0))))
         (scan& 'keyrelease-event
	  (if (or (= (car (the-value&)) sdlk-right)
		  (= (car (the-value&)) sdlk-left))
	      (set! dalpha 0.0)))
	 (atom& (rotate-aim (white-ball-aim o) dalpha))))))

Le let réactif est util pour garder la mémoire de l'increment dalpha. Si cette forme réactive n'existait pas, il aurait été nécessaire de trouver une autre solution, ce qui compliquerait certainement le code pour rien.

4.8. Utilisation avancée

Après un certain temps de familiarisation avec la programmation réactive, il devient simple de trouver des solutions rapides et efficaces aux problèmes. Un des dernier problèmes à resoudre à été la disparition des boules dans les trous du billard. Il a semblé naturel, étant donnée l'approche développée tout au long du projet, d'identifier ce problème à un nouveau comportement réactif:

(define (ball-behavior o)
   (local& 'fallen
     (until& 'fallen
	(par& 
	   (funcall& ball-move o)
	   (funcall& ball-collision o)
	   (funcall& ball-inertia o)
	   (funcall& common-render o)
	   (funcall& ball-fallen o))
	(atom& (print "behavior killed!!!!")))))

(define (ball-fallen o)
   (loop&
      (if& (ball-isfallen o)
           (generate& 'fallen))
      (stop&)))

Malgré l'apparente complexité du comportement, l'implémentation ci-dessus à été élaborée en moins de dix minutes ! Une ébauche du comportement final été déjà réaliser. Seul la fonction ball-fallen, les instructions local& et until&, et l'ajout du comportement ball-fallen à la balle ont dû être rajoutés.

Tout d'abord, il a fallu rajouter le comportement ball-fallen ainsi qu'un algorithme de collision une boule avec un trou. Cet algorithme à été calquer sur l'algorithme de collision déjà existant entre deux boules. Le comportement ball-fallen ce contente simplement de générer un événement 'fallen si la boule à été en collision avec un trou. Ensuite, pour mettre en place l'arrêt du comportement de la boule lorsqu'elle est perdu, il s'est avéré nécessaire et intuitif de rajouter une instruction until& englobant les differents comportement associés à la boule.

Un problème est alors survenu: lorsqu'une boule tombe dans un trou, elle génère l'événement 'fallen. Comment faire pour que seul la boule qui est tombé dans le trou réagisse à l'événement généré? Il suffit simplement d'insérer le until& dans le corps d'une instruction local&. Puisque le comportement ball-fallen est inclus dans le corps du local&, l'événement 'fallen qu'il génère ne sera intercepté que par le until& présent dans le même local&. De cette manière, le comportement de chaque boule est isolé des événements 'fallen générés par les autres boules.